diff --git a/Pdf4QtLib/CMakeLists.txt b/Pdf4QtLib/CMakeLists.txt index 3300d3f..476f134 100644 --- a/Pdf4QtLib/CMakeLists.txt +++ b/Pdf4QtLib/CMakeLists.txt @@ -100,6 +100,8 @@ add_library(Pdf4QtLib SHARED sources/pdfobjecteditorwidget_impl.h sources/pdfdocumentsanitizer.h sources/pdfdocumentsanitizer.cpp + sources/pdfimageconversion.h + sources/pdfimageconversion.cpp cmaps.qrc ) diff --git a/Pdf4QtLib/sources/pdfimageconversion.cpp b/Pdf4QtLib/sources/pdfimageconversion.cpp new file mode 100644 index 0000000..3990486 --- /dev/null +++ b/Pdf4QtLib/sources/pdfimageconversion.cpp @@ -0,0 +1,188 @@ +#include "pdfimageconversion.h" + +namespace pdf +{ + +PDFImageConversion::PDFImageConversion() +{ + +} + +void PDFImageConversion::setImage(QImage image) +{ + m_image = std::move(image); + m_convertedImage = QImage(); + m_automaticThreshold = DEFAULT_THRESHOLD; +} + +void PDFImageConversion::setConversionMethod(ConversionMethod method) +{ + m_conversionMethod = method; +} + +void PDFImageConversion::setThreshold(int threshold) +{ + m_manualThreshold = threshold; +} + +bool PDFImageConversion::convert() +{ + if (m_image.isNull()) + { + return false; + } + + QImage bitonal(m_image.width(), m_image.height(), QImage::Format_Mono); + bitonal.fill(0); + + // Thresholding + int threshold = DEFAULT_THRESHOLD; + + switch (m_conversionMethod) + { + case pdf::PDFImageConversion::ConversionMethod::Automatic: + m_automaticThreshold = calculateOtsu1DThreshold(); + threshold = m_automaticThreshold; + break; + + case pdf::PDFImageConversion::ConversionMethod::Manual: + threshold = m_manualThreshold; + break; + + default: + Q_ASSERT(false); + break; + } + + for (int y = 0; y < m_image.height(); ++y) + { + for (int x = 0; x < m_image.width(); ++x) + { + QColor pixelColor = m_image.pixelColor(x, y); + int pixelValue = pixelColor.lightness(); + bool bit = (pixelValue >= threshold); + bitonal.setPixel(x, y, bit); + } + } + + m_convertedImage = std::move(bitonal); + return true; +} + +int PDFImageConversion::getThreshold() const +{ + switch (m_conversionMethod) + { + case pdf::PDFImageConversion::ConversionMethod::Automatic: + return m_automaticThreshold; + + case pdf::PDFImageConversion::ConversionMethod::Manual: + return m_manualThreshold; + + default: + Q_ASSERT(false); + break; + } + + return DEFAULT_THRESHOLD; +} + +QImage PDFImageConversion::getConvertedImage() const +{ + return m_convertedImage; +} + +int PDFImageConversion::calculateOtsu1DThreshold() const +{ + if (m_image.isNull()) + { + return 128; + } + + // Histogram of lightness occurences + std::array histogram = { }; + + for (int x = 0; x < m_image.width(); ++x) + { + for (int y = 0; y < m_image.height(); ++y) + { + int lightness = m_image.pixelColor(x, y).lightness(); + Q_ASSERT(lightness >= 0 && lightness <= 255); + + int clampedLightness = qBound(0, lightness, 255); + histogram[clampedLightness] += 1; + } + } + + float factor = 1.0f / float(m_image.width() * m_image.height()); + + std::array normalizedHistogram = { }; + std::array cumulativeProbabilities = { }; + std::array interClassVariance = { }; + + // Compute probabilities + for (size_t i = 0; i < histogram.size(); ++i) + { + normalizedHistogram[i] = histogram[i] * factor; + cumulativeProbabilities[i] = normalizedHistogram[i]; + + if (i > 0) + { + cumulativeProbabilities[i] += cumulativeProbabilities[i - 1]; + } + } + + // Calculate the inter-class variance for each threshold. Variables + // with the subscript 0 denote the background, while those with + // subscript 1 denote the foreground. + for (size_t i = 0; i < histogram.size(); ++i) + { + const float w0 = cumulativeProbabilities[i] - normalizedHistogram[i]; + const float w1 = 1.0f - w0; + + float u0 = 0.0f; + float u1 = 0.0f; + + // Calculate mean intensity value of the background. + if (!qFuzzyIsNull(w0)) + { + for (size_t j = 0; j < i; ++j) + { + u0 += j * normalizedHistogram[j]; + } + + u0 /= w0; + } + + // Calculate mean intensity value of the foreground. + if (!qFuzzyIsNull(w1)) + { + for (size_t j = i; j < histogram.size(); ++j) + { + u1 += j * normalizedHistogram[j]; + } + + u1 /= w1; + } + + const float variance = w0 * w1 * std::powf(u0 - u1, 2); + interClassVariance[i] = variance; + } + + // Find maximal value of the variance + size_t maxVarianceIndex = 0; + float maxVarianceValue = 0.0f; + + for (size_t i = 0; i < interClassVariance.size(); ++i) + { + if (interClassVariance[i] > maxVarianceValue) + { + maxVarianceValue = interClassVariance[i]; + maxVarianceIndex = i; + } + } + + return int(maxVarianceIndex); +} + +} // namespace pdf diff --git a/Pdf4QtLib/sources/pdfimageconversion.h b/Pdf4QtLib/sources/pdfimageconversion.h new file mode 100644 index 0000000..fc9f621 --- /dev/null +++ b/Pdf4QtLib/sources/pdfimageconversion.h @@ -0,0 +1,97 @@ +// Copyright (C) 2023 Jakub Melka +// +// This file is part of PDF4QT. +// +// PDF4QT is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// with the written consent of the copyright owner, any later version. +// +// PDF4QT is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with PDF4QT. If not, see . + +#ifndef PDFIMAGECONVERSION_H +#define PDFIMAGECONVERSION_H + +#include "pdfglobal.h" + +#include + +namespace pdf +{ + +/// This class facilitates various image conversions, +/// including transforming colored images into monochromatic (or bitonal) formats. +class PDF4QTLIBSHARED_EXPORT PDFImageConversion +{ +public: + PDFImageConversion(); + + enum class ConversionMethod + { + Automatic, ///< The threshold is determined automatically using an algorithm. + Manual ///< The threshold is manually provided by the user. + }; + + /// Sets the image to be converted using the specified conversion method. + /// This operation resets any previously converted image and the automatic threshold, + /// thereby erasing all prior image data. + /// \param image The image to be set for conversion. + void setImage(QImage image); + + /// Sets the method for image conversion. Multiple methods are available + /// for selection. If the manual method is chosen, an appropriate threshold + /// must also be set by the user. + /// \param method The conversion method to be used. + void setConversionMethod(ConversionMethod method); + + /// Sets the manual threshold value. When a non-manual (e.g., automatic) conversion + /// method is in use, this function will retain the manual threshold settings, + /// but the conversion will utilize an automatically calculated threshold for the image. + /// The manually set threshold is preserved and not overwritten. Therefore, if the + /// manual conversion method is later selected, the previously established manual + /// threshold will be applied. + /// \param threshold The manual threshold value to be set. + void setThreshold(int threshold); + + /// This method converts the image into a bitonal (monochromatic) format. If + /// the automatic threshold calculation is enabled, it executes Otsu's 1D algorithm + /// to determine the threshold. When the manual conversion method is selected, + /// the automatic threshold calculation is bypassed, and the predefined manual threshold + /// value is utilized instead. This method returns true if the conversion is + /// successful, and false otherwise. + bool convert(); + + /// Returns the threshold used in image conversion. If the automatic conversion method is + /// selected, this function should be called only after executing the convert() method; + /// otherwise, it may return invalid data. The automatic threshold calculation is + /// performed within the convert() method. + /// \returns The threshold value used in image conversion. + int getThreshold() const; + + /// Returns the converted image. This method should only be called after + /// the convert() method has been executed, and additionally, only if the + /// convert() method returns true. If these conditions are not met, the result + /// is undefined. + QImage getConvertedImage() const; + +private: + int calculateOtsu1DThreshold() const; + + static constexpr int DEFAULT_THRESHOLD = 128; + + QImage m_image; + QImage m_convertedImage; + ConversionMethod m_conversionMethod = ConversionMethod::Automatic; + int m_automaticThreshold = DEFAULT_THRESHOLD; + int m_manualThreshold = DEFAULT_THRESHOLD; +}; + +} // namespace pdf + +#endif // PDFIMAGECONVERSION_H diff --git a/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.cpp b/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.cpp index 5f2f588..8cac3d3 100644 --- a/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.cpp +++ b/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.cpp @@ -24,6 +24,7 @@ #include "pdfdbgheap.h" #include "pdfexception.h" #include "pdfwidgetutils.h" +#include "pdfimageconversion.h" #include #include @@ -39,6 +40,64 @@ namespace pdfviewer { +PDFCreateBitonalDocumentPreviewWidget::PDFCreateBitonalDocumentPreviewWidget(QWidget* parent) : + QWidget(parent) +{ + +} + +PDFCreateBitonalDocumentPreviewWidget::~PDFCreateBitonalDocumentPreviewWidget() +{ + +} + +void PDFCreateBitonalDocumentPreviewWidget::paintEvent(QPaintEvent* event) +{ + Q_UNUSED(event); + + QPainter painter(this); + painter.fillRect(rect(), Qt::white); + + // Caption rect + QRect captionRect = rect(); + captionRect.setHeight(painter.fontMetrics().lineSpacing() * 2); + + painter.fillRect(captionRect, QColor::fromRgb(0, 0, 128, 255)); + + if (!m_caption.isEmpty()) + { + painter.setPen(Qt::white); + painter.drawText(captionRect, m_caption, QTextOption(Qt::AlignCenter)); + } + + QRect imageRect = rect(); + imageRect.setTop(captionRect.bottom()); + imageRect = imageRect.adjusted(16, 16, -32, -32); + + if (imageRect.isValid() && !m_image.isNull()) + { + QRect imageDrawRect = imageRect; + imageDrawRect.setSize(m_image.size().scaled(imageRect.size(), Qt::KeepAspectRatio)); + imageDrawRect.moveCenter(imageRect.center()); + painter.drawImage(imageDrawRect, m_image); + } +} + +void PDFCreateBitonalDocumentPreviewWidget::setCaption(QString caption) +{ + if (m_caption != caption) + { + m_caption = caption; + update(); + } +} + +void PDFCreateBitonalDocumentPreviewWidget::setImage(QImage image) +{ + m_image = std::move(image); + update(); +} + class ImagePreviewDelegate : public QStyledItemDelegate { public: @@ -148,10 +207,18 @@ PDFCreateBitonalDocumentDialog::PDFCreateBitonalDocumentDialog(const pdf::PDFDoc m_cms(cms), m_createBitonalDocumentButton(nullptr), m_conversionInProgress(false), - m_processed(false) + m_processed(false), + m_leftPreviewWidget(new PDFCreateBitonalDocumentPreviewWidget(this)), + m_rightPreviewWidget(new PDFCreateBitonalDocumentPreviewWidget(this)) { ui->setupUi(this); + m_leftPreviewWidget->setCaption(tr("ORIGINAL")); + m_rightPreviewWidget->setCaption(tr("BITONIC")); + + ui->mainGridLayout->addWidget(m_leftPreviewWidget, 1, 1); + ui->mainGridLayout->addWidget(m_rightPreviewWidget, 1, 2); + m_classifier.classify(document); m_imageReferences = m_classifier.getObjectsByType(pdf::PDFObjectClassifier::Image); @@ -159,6 +226,10 @@ PDFCreateBitonalDocumentDialog::PDFCreateBitonalDocumentDialog(const pdf::PDFDoc connect(m_createBitonalDocumentButton, &QPushButton::clicked, this, &PDFCreateBitonalDocumentDialog::onCreateBitonalDocumentButtonClicked); connect(ui->automaticThresholdRadioButton, &QRadioButton::clicked, this, &PDFCreateBitonalDocumentDialog::updateUi); connect(ui->manualThresholdRadioButton, &QRadioButton::clicked, this, &PDFCreateBitonalDocumentDialog::updateUi); + connect(ui->automaticThresholdRadioButton, &QRadioButton::clicked, this, &PDFCreateBitonalDocumentDialog::updatePreview); + connect(ui->manualThresholdRadioButton, &QRadioButton::clicked, this, &PDFCreateBitonalDocumentDialog::updatePreview); + connect(ui->imageListWidget, &QListWidget::currentItemChanged, this, &PDFCreateBitonalDocumentDialog::updatePreview); + connect(ui->thresholdEditBox, &QSpinBox::editingFinished, this, &PDFCreateBitonalDocumentDialog::updatePreview); pdf::PDFWidgetUtils::scaleWidget(this, QSize(640, 380)); updateUi(); @@ -167,6 +238,7 @@ PDFCreateBitonalDocumentDialog::PDFCreateBitonalDocumentDialog(const pdf::PDFDoc ui->imageListWidget->setItemDelegate( new ImagePreviewDelegate(&m_imagesToBeConverted, this)); loadImages(); + updatePreview(); } PDFCreateBitonalDocumentDialog::~PDFCreateBitonalDocumentDialog() @@ -243,6 +315,40 @@ void PDFCreateBitonalDocumentDialog::updateUi() m_createBitonalDocumentButton->setEnabled(!m_conversionInProgress); } +void PDFCreateBitonalDocumentDialog::updatePreview() +{ + QModelIndex index = ui->imageListWidget->currentIndex(); + + m_previewImageLeft = QImage(); + m_previewImageRight = QImage(); + + if (index.isValid()) + { + const ImageConversionInfo& info = m_imagesToBeConverted.at(index.row()); + + std::optional pdfImage = getImageFromReference(info.imageReference); + Q_ASSERT(pdfImage); + + pdf::PDFCMSGeneric cmsGeneric; + pdf::PDFRenderErrorReporterDummy reporter; + QImage image = pdfImage->getImage(&cmsGeneric, &reporter, nullptr); + + pdf::PDFImageConversion imageConversion; + imageConversion.setConversionMethod(ui->automaticThresholdRadioButton->isChecked() ? pdf::PDFImageConversion::ConversionMethod::Automatic : pdf::PDFImageConversion::ConversionMethod::Manual); + imageConversion.setThreshold(ui->thresholdEditBox->value()); + imageConversion.setImage(image); + + if (imageConversion.convert()) + { + m_previewImageLeft = image; + m_previewImageRight = imageConversion.getConvertedImage(); + } + } + + m_leftPreviewWidget->setImage(m_previewImageLeft); + m_rightPreviewWidget->setImage(m_previewImageRight); +} + std::optional PDFCreateBitonalDocumentDialog::getImageFromReference(pdf::PDFObjectReference reference) const { std::optional pdfImage; @@ -285,3 +391,5 @@ std::optional PDFCreateBitonalDocumentDialog::getImageFromReferen } } // namespace pdfviewer + + diff --git a/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.h b/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.h index 4cd2106..a634721 100644 --- a/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.h +++ b/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.h @@ -34,6 +34,24 @@ class PDFCreateBitonalDocumentDialog; namespace pdfviewer { +class PDFCreateBitonalDocumentPreviewWidget : public QWidget +{ + Q_OBJECT + +public: + PDFCreateBitonalDocumentPreviewWidget(QWidget* parent); + virtual ~PDFCreateBitonalDocumentPreviewWidget() override; + + virtual void paintEvent(QPaintEvent* event) override; + + void setCaption(QString caption); + void setImage(QImage image); + +private: + QString m_caption; + QImage m_image; +}; + class PDFCreateBitonalDocumentDialog : public QDialog { Q_OBJECT @@ -56,6 +74,7 @@ private: void loadImages(); void updateUi(); + void updatePreview(); std::optional getImageFromReference(pdf::PDFObjectReference reference) const; @@ -70,6 +89,12 @@ private: pdf::PDFObjectClassifier m_classifier; std::vector m_imageReferences; std::vector m_imagesToBeConverted; + + QImage m_previewImageLeft; + QImage m_previewImageRight; + + PDFCreateBitonalDocumentPreviewWidget* m_leftPreviewWidget; + PDFCreateBitonalDocumentPreviewWidget* m_rightPreviewWidget; }; } // namespace pdfviewer diff --git a/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.ui b/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.ui index e9869c7..a772509 100644 --- a/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.ui +++ b/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.ui @@ -13,13 +13,23 @@ Create Bitonal Document - + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + Color to Bitonal Conversion Options - + @@ -50,99 +60,7 @@ - - - - - - - - - 255 - 255 - 255 - - - - - - - - - 240 - 240 - 240 - - - - - - - - - 255 - 255 - 255 - - - - - - - - - - - - - - - - - 255 - 255 - 255 - - - - - - - - - 240 - 240 - 240 - - - - - - - - - 255 - 255 - 255 - - - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - + QListView::Adjust