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/pdfcms.h b/Pdf4QtLib/sources/pdfcms.h index f3cb68e..380a1a0 100644 --- a/Pdf4QtLib/sources/pdfcms.h +++ b/Pdf4QtLib/sources/pdfcms.h @@ -250,7 +250,7 @@ public: using PDFCMSPointer = QSharedPointer; -class PDFCMSGeneric : public PDFCMS +class PDF4QTLIBSHARED_EXPORT PDFCMSGeneric : public PDFCMS { public: explicit inline PDFCMSGeneric() = default; diff --git a/Pdf4QtLib/sources/pdfdocumentbuilder.cpp b/Pdf4QtLib/sources/pdfdocumentbuilder.cpp index a9e786c..295a15d 100644 --- a/Pdf4QtLib/sources/pdfdocumentbuilder.cpp +++ b/Pdf4QtLib/sources/pdfdocumentbuilder.cpp @@ -1026,7 +1026,7 @@ void PDFPageContentStreamBuilder::replaceResources(PDFObjectReference contentStr QByteArray compressedData = PDFFlateDecodeFilter::compress(decodedStream); PDFDictionary updatedDictionary = *contentStreamObject.getStream()->getDictionary(); updatedDictionary.setEntry(PDFInplaceOrMemoryString("Length"), PDFObject::createInteger(compressedData.size())); - updatedDictionary.setEntry(PDFInplaceOrMemoryString("Filters"), PDFObject::createArray(std::make_shared(qMove(array)))); + updatedDictionary.setEntry(PDFInplaceOrMemoryString("Filter"), PDFObject::createArray(std::make_shared(qMove(array)))); PDFObject newContentStream = PDFObject::createStream(std::make_shared(qMove(updatedDictionary), qMove(compressedData))); m_documentBuilder->setObject(contentStreamReference, std::move(newContentStream)); } diff --git a/Pdf4QtLib/sources/pdfimage.h b/Pdf4QtLib/sources/pdfimage.h index f92dbbc..b19e6e4 100644 --- a/Pdf4QtLib/sources/pdfimage.h +++ b/Pdf4QtLib/sources/pdfimage.h @@ -58,6 +58,7 @@ private: class PDF4QTLIBSHARED_EXPORT PDFImage { public: + PDFImage() = default; /// Creates image from the content and the dictionary. If image can't be created, then exception is thrown. /// \param document Document @@ -112,8 +113,6 @@ public: const PDFImageData& getSoftMaskData() const { return m_softMask; } private: - PDFImage() = default; - PDFImageData m_imageData; PDFImageData m_softMask; PDFColorSpacePointer m_colorSpace; 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/Pdf4QtLib/sources/pdfutils.h b/Pdf4QtLib/sources/pdfutils.h index cfaa5da..900b6b5 100644 --- a/Pdf4QtLib/sources/pdfutils.h +++ b/Pdf4QtLib/sources/pdfutils.h @@ -190,7 +190,7 @@ private: }; /// Bit writer -class PDFBitWriter +class PDF4QTLIBSHARED_EXPORT PDFBitWriter { public: using Value = uint64_t; diff --git a/Pdf4QtViewer/CMakeLists.txt b/Pdf4QtViewer/CMakeLists.txt index cfcca12..6ef4518 100644 --- a/Pdf4QtViewer/CMakeLists.txt +++ b/Pdf4QtViewer/CMakeLists.txt @@ -47,6 +47,9 @@ add_library(Pdf4QtViewer SHARED pdfsanitizedocumentdialog.ui pdfsanitizedocumentdialog.cpp pdfsanitizedocumentdialog.h + pdfcreatebitonaldocumentdialog.ui + pdfcreatebitonaldocumentdialog.cpp + pdfcreatebitonaldocumentdialog.h pdf4qtviewer.qrc ) @@ -59,7 +62,7 @@ GENERATE_EXPORT_HEADER(Pdf4QtViewer PDF4QTVIEWERLIBSHARED_EXPORT EXPORT_FILE_NAME "${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/pdf4qtviewer_export.h") -target_link_libraries(Pdf4QtViewer PRIVATE Pdf4QtLib Qt6::Core Qt6::Gui Qt6::Widgets Qt6::PrintSupport Qt6::TextToSpeech Qt6::Xml Qt6::OpenGLWidgets) +target_link_libraries(Pdf4QtViewer PRIVATE Pdf4QtLib Qt6::Core Qt6::Gui Qt6::Widgets Qt6::PrintSupport Qt6::TextToSpeech Qt6::Xml Qt6::Svg Qt6::OpenGLWidgets) target_include_directories(Pdf4QtViewer INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(Pdf4QtViewer PUBLIC ${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}) diff --git a/Pdf4QtViewer/pdf4qtviewer.qrc b/Pdf4QtViewer/pdf4qtviewer.qrc index 2132b85..2732c21 100644 --- a/Pdf4QtViewer/pdf4qtviewer.qrc +++ b/Pdf4QtViewer/pdf4qtviewer.qrc @@ -102,5 +102,7 @@ resources/book.svg resources/wallet.svg resources/web.svg + resources/create-bitonal-document.svg + resources/sanitize-document.svg diff --git a/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.cpp b/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.cpp new file mode 100644 index 0000000..4e60af7 --- /dev/null +++ b/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.cpp @@ -0,0 +1,510 @@ +// 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 . + +#include "pdfcreateBitonaldocumentdialog.h" +#include "ui_pdfcreateBitonaldocumentdialog.h" + +#include "pdfwidgetutils.h" +#include "pdfdocumentwriter.h" +#include "pdfimage.h" +#include "pdfdbgheap.h" +#include "pdfexception.h" +#include "pdfwidgetutils.h" +#include "pdfimageconversion.h" +#include "pdfstreamfilters.h" +#include "pdfutils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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: + ImagePreviewDelegate(std::vector* imageConversionInfos, QObject* parent) : + QStyledItemDelegate(parent), + m_imageConversionInfos(imageConversionInfos) + { + m_yesRenderer.load(QString(":/resources/result-ok.svg")); + m_noRenderer.load(QString(":/resources/result-error.svg")); + } + + virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override + { + QStyledItemDelegate::paint(painter, option, index); + + QRect markRect = getMarkRect(option); + + if (index.isValid()) + { + const PDFCreateBitonalDocumentDialog::ImageConversionInfo& info = m_imageConversionInfos->at(index.row()); + if (info.conversionEnabled) + { + m_yesRenderer.render(painter, markRect); + } + else + { + m_noRenderer.render(painter, markRect); + } + } + } + + virtual bool editorEvent(QEvent* event, + QAbstractItemModel* model, + const QStyleOptionViewItem& option, + const QModelIndex& index) + { + Q_UNUSED(model); + Q_UNUSED(index); + + if (event->type() == QEvent::MouseButtonPress && index.isValid()) + { + QMouseEvent* mouseEvent = dynamic_cast(event); + if (mouseEvent && mouseEvent->button() == Qt::LeftButton) + { + // Do we click on yes/no mark? + QRectF markRect = getMarkRect(option); + if (markRect.contains(mouseEvent->position())) + { + PDFCreateBitonalDocumentDialog::ImageConversionInfo& info = m_imageConversionInfos->at(index.row()); + info.conversionEnabled = !info.conversionEnabled; + return true; + } + } + } + + return false; + } + + virtual bool helpEvent(QHelpEvent* event, + QAbstractItemView* view, + const QStyleOptionViewItem& option, + const QModelIndex& index) override + { + Q_UNUSED(index); + + if (!event || !view) + { + return false; + } + + if (event->type() == QEvent::ToolTip) + { + // Are we hovering over yes/no mark? + QRectF markRect = getMarkRect(option); + if (markRect.contains(event->pos())) + { + event->accept(); + QToolTip::showText(event->globalPos(), tr("Toggle this icon to switch image conversion to bitonic format on or off."), view); + return true; + } + } + + return false; + } + +private: + static constexpr QSize s_iconSize = QSize(24, 24); + + QRect getMarkRect(const QStyleOptionViewItem& option) const + { + QSize markSize = pdf::PDFWidgetUtils::scaleDPI(option.widget, s_iconSize); + QRect markRect(option.rect.left(), option.rect.top(), markSize.width(), markSize.height()); + return markRect; + } + + std::vector* m_imageConversionInfos; + mutable QSvgRenderer m_yesRenderer; + mutable QSvgRenderer m_noRenderer; +}; + +PDFCreateBitonalDocumentDialog::PDFCreateBitonalDocumentDialog(const pdf::PDFDocument* document, + const pdf::PDFCMS* cms, + pdf::PDFProgress* progress, + QWidget* parent) : + QDialog(parent), + ui(new Ui::PDFCreateBitonalDocumentDialog), + m_document(document), + m_cms(cms), + m_createBitonalDocumentButton(nullptr), + m_conversionInProgress(false), + m_processed(false), + m_leftPreviewWidget(new PDFCreateBitonalDocumentPreviewWidget(this)), + m_rightPreviewWidget(new PDFCreateBitonalDocumentPreviewWidget(this)), + m_progress(progress) +{ + 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); + + m_createBitonalDocumentButton = ui->buttonBox->addButton(tr("Perform"), QDialogButtonBox::ActionRole); + 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(1024, 768)); + updateUi(); + pdf::PDFWidgetUtils::style(this); + + ui->imageListWidget->setItemDelegate( new ImagePreviewDelegate(&m_imagesToBeConverted, this)); + + setGeometry(parent->geometry()); + + loadImages(); + updatePreview(); +} + +PDFCreateBitonalDocumentDialog::~PDFCreateBitonalDocumentDialog() +{ + Q_ASSERT(!m_conversionInProgress); + Q_ASSERT(!m_future.isRunning()); + + delete ui; +} + +void PDFCreateBitonalDocumentDialog::onPerformFinished() +{ + m_future.waitForFinished(); + m_conversionInProgress = false; + m_processed = true; + updateUi(); +} + +void PDFCreateBitonalDocumentDialog::createBitonalDocument() +{ + std::vector imagesToBeConverted; + std::copy_if(m_imagesToBeConverted.begin(), m_imagesToBeConverted.end(), std::back_inserter(imagesToBeConverted), [](const auto& item) { return item.conversionEnabled; }); + + // Do we have something to be converted? + if (imagesToBeConverted.empty()) + { + return; + } + + pdf::ProgressStartupInfo info; + info.showDialog = true; + info.text = tr("Converting images..."); + m_progress->start(imagesToBeConverted.size(), std::move(info)); + + pdf::PDFObjectStorage storage = m_document->getStorage(); + + pdf::PDFCMSGeneric genericCms; + pdf::PDFRenderErrorReporterDummy errorReporter; + + for (int i = 0; i < imagesToBeConverted.size(); ++i) + { + pdf::PDFObjectReference reference = imagesToBeConverted[i].imageReference; + std::optional pdfImage = getImageFromReference(reference); + + QImage image; + try + { + image = pdfImage->getImage(&genericCms, &errorReporter, nullptr); + } + catch (pdf::PDFException) + { + // Do nothing + } + + // Just for code safety - this should never occur in here. + if (image.isNull()) + { + continue; + } + + pdf::PDFImageConversion imageConversion; + imageConversion.setConversionMethod(m_conversionMethod); + imageConversion.setThreshold(m_manualThreshold); + imageConversion.setImage(image); + + if (imageConversion.convert()) + { + QImage bitonicImage = imageConversion.getConvertedImage(); + Q_ASSERT(bitonicImage.format() == QImage::Format_Mono); + + pdf::PDFBitWriter bitWriter(1); + bitWriter.reserve(bitonicImage.sizeInBytes()); + for (int row = 0; row < bitonicImage.height(); ++row) + { + for (int col = 0; col < bitonicImage.width(); ++col) + { + QRgb pixelValue = bitonicImage.pixel(col, row); + QRgb withoutAlphaValue = pixelValue & 0xFFFFFF; + int value = withoutAlphaValue > 0 ? 1 : 0; + bitWriter.write(value); + } + + bitWriter.finishLine(); + } + + QByteArray imageData = bitWriter.takeByteArray(); + QByteArray compressedData = pdf::PDFFlateDecodeFilter::compress(imageData); + + pdf::PDFArray array; + array.appendItem(pdf::PDFObject::createName("FlateDecode")); + + pdf::PDFDictionary dictionary; + dictionary.addEntry(pdf::PDFInplaceOrMemoryString("Type"), pdf::PDFObject::createName("XObject")); + dictionary.addEntry(pdf::PDFInplaceOrMemoryString("Subtype"), pdf::PDFObject::createName("Image")); + dictionary.addEntry(pdf::PDFInplaceOrMemoryString("Width"), pdf::PDFObject::createInteger(image.width())); + dictionary.addEntry(pdf::PDFInplaceOrMemoryString("Height"), pdf::PDFObject::createInteger(image.height())); + dictionary.addEntry(pdf::PDFInplaceOrMemoryString("ColorSpace"), pdf::PDFObject::createName("DeviceGray")); + dictionary.addEntry(pdf::PDFInplaceOrMemoryString("BitsPerComponent"), pdf::PDFObject::createInteger(1)); + dictionary.addEntry(pdf::PDFInplaceOrMemoryString("Predictor"), pdf::PDFObject::createInteger(1)); + dictionary.setEntry(pdf::PDFInplaceOrMemoryString("Length"), pdf::PDFObject::createInteger(compressedData.size())); + dictionary.setEntry(pdf::PDFInplaceOrMemoryString("Filter"), pdf::PDFObject::createArray(std::make_shared(qMove(array)))); + + pdf::PDFObject imageObject = pdf::PDFObject::createStream(std::make_shared(qMove(dictionary), qMove(compressedData))); + storage.setObject(reference, std::move(imageObject)); + } + + m_progress->step(); + } + + m_bitonalDocument = pdf::PDFDocument(std::move(storage), m_document->getInfo()->version, QByteArray()); + + m_progress->finish(); +} + +void PDFCreateBitonalDocumentDialog::onCreateBitonalDocumentButtonClicked() +{ + Q_ASSERT(!m_conversionInProgress); + Q_ASSERT(!m_future.isRunning()); + + m_conversionMethod = ui->automaticThresholdRadioButton->isChecked() ? pdf::PDFImageConversion::ConversionMethod::Automatic : pdf::PDFImageConversion::ConversionMethod::Manual; + m_manualThreshold = ui->thresholdEditBox->value(); + + m_conversionInProgress = true; + m_future = QtConcurrent::run([this]() { createBitonalDocument(); }); + m_futureWatcher.emplace(); + connect(&m_futureWatcher.value(), &QFutureWatcher::finished, this, &PDFCreateBitonalDocumentDialog::onPerformFinished); + m_futureWatcher->setFuture(m_future); + updateUi(); +} + +void PDFCreateBitonalDocumentDialog::loadImages() +{ + QSize iconSize(QSize(256, 256)); + ui->imageListWidget->setIconSize(iconSize); + QSize imageSize = iconSize * ui->imageListWidget->devicePixelRatioF(); + + for (pdf::PDFObjectReference reference : m_imageReferences) + { + std::optional pdfImage = getImageFromReference(reference); + if (!pdfImage) + { + continue; + } + + pdf::PDFCMSGeneric genericCms; + pdf::PDFRenderErrorReporterDummy errorReporter; + QImage image; + + try + { + image = pdfImage->getImage(&genericCms, &errorReporter, nullptr); + } + catch (pdf::PDFException) + { + // Do nothing + } + + if (image.isNull()) + { + continue; + } + + QListWidgetItem* item = new QListWidgetItem(ui->imageListWidget); + image = image.scaled(imageSize.width(), imageSize.height(), Qt::KeepAspectRatio, Qt::FastTransformation); + item->setIcon(QIcon(QPixmap::fromImage(image))); + Qt::ItemFlags flags = item->flags(); + flags.setFlag(Qt::ItemIsEditable, true); + item->setFlags(flags); + + ImageConversionInfo imageConversionInfo; + imageConversionInfo.imageReference = reference; + imageConversionInfo.conversionEnabled = true; + m_imagesToBeConverted.push_back(imageConversionInfo); + } +} + +void PDFCreateBitonalDocumentDialog::updateUi() +{ + ui->thresholdEditBox->setEnabled(ui->manualThresholdRadioButton->isChecked()); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(m_processed && !m_conversionInProgress); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(!m_conversionInProgress); + 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; + pdf::PDFObject imageObject = m_document->getObjectByReference(reference); + pdf::PDFRenderErrorReporterDummy errorReporter; + + if (!imageObject.isStream()) + { + // Image is not stream + return pdfImage; + } + + const pdf::PDFStream* stream = imageObject.getStream(); + try + { + pdf::PDFColorSpacePointer colorSpace; + const pdf::PDFDictionary* streamDictionary = stream->getDictionary(); + if (streamDictionary->hasKey("ColorSpace")) + { + const pdf::PDFObject& colorSpaceObject = m_document->getObject(streamDictionary->get("ColorSpace")); + if (colorSpaceObject.isName() || colorSpaceObject.isArray()) + { + pdf::PDFDictionary dummyDictionary; + colorSpace = pdf::PDFAbstractColorSpace::createColorSpace(&dummyDictionary, m_document, colorSpaceObject); + } + } + pdfImage.emplace(pdf::PDFImage::createImage(m_document, + stream, + colorSpace, + false, + pdf::RenderingIntent::Perceptual, + &errorReporter)); + } + catch (pdf::PDFException) + { + // Do nothing + } + + return pdfImage; +} + +} // namespace pdfviewer + + diff --git a/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.h b/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.h new file mode 100644 index 0000000..73e468a --- /dev/null +++ b/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.h @@ -0,0 +1,114 @@ +// 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 PDFCREATEBITONALDOCUMENTDIALOG_H +#define PDFCREATEBITONALDOCUMENTDIALOG_H + +#include "pdfcms.h" +#include "pdfdocument.h" +#include "pdfobjectutils.h" +#include "pdfimage.h" +#include "pdfimageconversion.h" +#include "pdfprogress.h" + +#include +#include + +namespace Ui +{ +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 + +public: + explicit PDFCreateBitonalDocumentDialog(const pdf::PDFDocument* document, + const pdf::PDFCMS* cms, + pdf::PDFProgress* progress, + QWidget* parent); + virtual ~PDFCreateBitonalDocumentDialog() override; + + pdf::PDFDocument takeBitonaldDocument() { return qMove(m_bitonalDocument); } + + struct ImageConversionInfo + { + pdf::PDFObjectReference imageReference; + bool conversionEnabled = true; + }; + +private: + void createBitonalDocument(); + void onCreateBitonalDocumentButtonClicked(); + void onPerformFinished(); + void loadImages(); + + void updateUi(); + void updatePreview(); + + std::optional getImageFromReference(pdf::PDFObjectReference reference) const; + + Ui::PDFCreateBitonalDocumentDialog* ui; + const pdf::PDFDocument* m_document; + const pdf::PDFCMS* m_cms; + QPushButton* m_createBitonalDocumentButton; + bool m_conversionInProgress; + bool m_processed; + QFuture m_future; + std::optional> m_futureWatcher; + pdf::PDFDocument m_bitonalDocument; + pdf::PDFObjectClassifier m_classifier; + std::vector m_imageReferences; + std::vector m_imagesToBeConverted; + + QImage m_previewImageLeft; + QImage m_previewImageRight; + + PDFCreateBitonalDocumentPreviewWidget* m_leftPreviewWidget; + PDFCreateBitonalDocumentPreviewWidget* m_rightPreviewWidget; + + pdf::PDFProgress* m_progress; + + pdf::PDFImageConversion::ConversionMethod m_conversionMethod = pdf::PDFImageConversion::ConversionMethod::Automatic; + int m_manualThreshold = 128; +}; + +} // namespace pdfviewer + +#endif // PDFCREATEBITONALDOCUMENTDIALOG_H diff --git a/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.ui b/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.ui new file mode 100644 index 0000000..b2b27d8 --- /dev/null +++ b/Pdf4QtViewer/pdfcreatebitonaldocumentdialog.ui @@ -0,0 +1,110 @@ + + + PDFCreateBitonalDocumentDialog + + + + 0 + 0 + 741 + 530 + + + + Create Bitonal Document + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Color to Bitonal Conversion Options + + + + + + Automatic (Otsu's 1D method) + + + true + + + + + + + 255 + + + 128 + + + + + + + User-Defined Intensity Threshold (0-255): + + + + + + + + + + QListView::Adjust + + + QListView::IconMode + + + + + + + + + buttonBox + accepted() + PDFCreateBitonalDocumentDialog + accept() + + + 370 + 508 + + + 370 + 264 + + + + + buttonBox + rejected() + PDFCreateBitonalDocumentDialog + reject() + + + 370 + 508 + + + 370 + 264 + + + + + diff --git a/Pdf4QtViewer/pdfprogramcontroller.cpp b/Pdf4QtViewer/pdfprogramcontroller.cpp index 7e66eb8..b8dd48a 100644 --- a/Pdf4QtViewer/pdfprogramcontroller.cpp +++ b/Pdf4QtViewer/pdfprogramcontroller.cpp @@ -33,6 +33,7 @@ #include "pdfrendertoimagesdialog.h" #include "pdfoptimizedocumentdialog.h" #include "pdfsanitizedocumentdialog.h" +#include "pdfcreatebitonaldocumentdialog.h" #include "pdfviewersettingsdialog.h" #include "pdfaboutdialog.h" #include "pdfrenderingerrorswidget.h" @@ -458,6 +459,10 @@ void PDFProgramController::initialize(Features features, { connect(action, &QAction::triggered, this, &PDFProgramController::onActionSanitizeTriggered); } + if (QAction* action = m_actionManager->getAction(PDFActionManager::CreateBitonalDocument)) + { + connect(action, &QAction::triggered, this, &PDFProgramController::onActionCreateBitonalDocumentTriggered); + } if (QAction* action = m_actionManager->getAction(PDFActionManager::Encryption)) { connect(action, &QAction::triggered, this, &PDFProgramController::onActionEncryptionTriggered); @@ -1249,6 +1254,19 @@ void PDFProgramController::onActionSanitizeTriggered() } } +void PDFProgramController::onActionCreateBitonalDocumentTriggered() +{ + auto cms = m_CMSManager->getCurrentCMS(); + PDFCreateBitonalDocumentDialog dialog(m_pdfDocument.data(), cms.data(), m_progress, m_mainWindow); + + if (dialog.exec() == QDialog::Accepted) + { + pdf::PDFDocumentPointer pointer(new pdf::PDFDocument(dialog.takeBitonaldDocument())); + pdf::PDFModifiedDocument document(qMove(pointer), m_optionalContentActivity, pdf::PDFModifiedDocument::ModificationFlags(pdf::PDFModifiedDocument::Reset | pdf::PDFModifiedDocument::PreserveUndoRedo)); + onDocumentModified(qMove(document)); + } +} + void PDFProgramController::onActionEncryptionTriggered() { auto queryPassword = [this](bool* ok) @@ -1569,6 +1587,7 @@ void PDFProgramController::updateActionsAvailability() m_actionManager->setEnabled(PDFActionManager::RenderToImages, hasValidDocument && canPrint); m_actionManager->setEnabled(PDFActionManager::Optimize, hasValidDocument); m_actionManager->setEnabled(PDFActionManager::Sanitize, hasValidDocument); + m_actionManager->setEnabled(PDFActionManager::CreateBitonalDocument, hasValidDocument); m_actionManager->setEnabled(PDFActionManager::Encryption, hasValidDocument); m_actionManager->setEnabled(PDFActionManager::Save, hasValidDocument); m_actionManager->setEnabled(PDFActionManager::SaveAs, hasValidDocument); diff --git a/Pdf4QtViewer/pdfprogramcontroller.h b/Pdf4QtViewer/pdfprogramcontroller.h index 4a3fbdf..d248224 100644 --- a/Pdf4QtViewer/pdfprogramcontroller.h +++ b/Pdf4QtViewer/pdfprogramcontroller.h @@ -109,6 +109,7 @@ public: RenderToImages, Optimize, Sanitize, + CreateBitonalDocument, Encryption, FitPage, FitWidth, @@ -337,6 +338,7 @@ private: void onActionRenderToImagesTriggered(); void onActionOptimizeTriggered(); void onActionSanitizeTriggered(); + void onActionCreateBitonalDocumentTriggered(); void onActionEncryptionTriggered(); void onActionFitPageTriggered(); void onActionFitWidthTriggered(); diff --git a/Pdf4QtViewer/pdfviewermainwindow.cpp b/Pdf4QtViewer/pdfviewermainwindow.cpp index 16b0fe4..037a194 100644 --- a/Pdf4QtViewer/pdfviewermainwindow.cpp +++ b/Pdf4QtViewer/pdfviewermainwindow.cpp @@ -164,6 +164,7 @@ PDFViewerMainWindow::PDFViewerMainWindow(QWidget* parent) : m_actionManager->setAction(PDFActionManager::RenderToImages, ui->actionRender_to_Images); m_actionManager->setAction(PDFActionManager::Optimize, ui->actionOptimize); m_actionManager->setAction(PDFActionManager::Sanitize, ui->actionSanitize); + m_actionManager->setAction(PDFActionManager::CreateBitonalDocument, ui->actionCreateBitonalDocument); m_actionManager->setAction(PDFActionManager::Encryption, ui->actionEncryption); m_actionManager->setAction(PDFActionManager::FitPage, ui->actionFitPage); m_actionManager->setAction(PDFActionManager::FitWidth, ui->actionFitWidth); diff --git a/Pdf4QtViewer/pdfviewermainwindow.ui b/Pdf4QtViewer/pdfviewermainwindow.ui index f1a0959..d7b3ffa 100644 --- a/Pdf4QtViewer/pdfviewermainwindow.ui +++ b/Pdf4QtViewer/pdfviewermainwindow.ui @@ -144,8 +144,9 @@ - + + @@ -952,6 +953,10 @@ + + + :/resources/sanitize-document.svg:/resources/sanitize-document.svg + Sanitize... @@ -979,6 +984,21 @@ Become a Sponsor + + + + :/resources/create-bitonal-document.svg:/resources/create-bitonal-document.svg + + + Create Bitonal Document... + + + Create Bitonal Document + + + Convert the colored images to monochromatic to create a bitonal document. + + diff --git a/Pdf4QtViewer/resources/create-bitonal-document.svg b/Pdf4QtViewer/resources/create-bitonal-document.svg new file mode 100644 index 0000000..6c113a7 --- /dev/null +++ b/Pdf4QtViewer/resources/create-bitonal-document.svg @@ -0,0 +1,58 @@ + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/Pdf4QtViewer/resources/sanitize-document.svg b/Pdf4QtViewer/resources/sanitize-document.svg new file mode 100644 index 0000000..74b5120 --- /dev/null +++ b/Pdf4QtViewer/resources/sanitize-document.svg @@ -0,0 +1,58 @@ + + + + + + + + image/svg+xml + + + + + + + + +