// Copyright (C) 2019-2022 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 "pdfrenderer.h" #include "pdfpainter.h" #include "pdfdocument.h" #include "pdfexecutionpolicy.h" #include "pdfprogress.h" #include "pdfannotation.h" #include #include #include #include #include "pdfdbgheap.h" namespace pdf { PDFRenderer::PDFRenderer(const PDFDocument* document, const PDFFontCache* fontCache, const PDFCMS* cms, const PDFOptionalContentActivity* optionalContentActivity, Features features, const PDFMeshQualitySettings& meshQualitySettings) : m_document(document), m_fontCache(fontCache), m_cms(cms), m_optionalContentActivity(optionalContentActivity), m_operationControl(nullptr), m_features(features), m_meshQualitySettings(meshQualitySettings) { Q_ASSERT(document); } QTransform PDFRenderer::createPagePointToDevicePointMatrix(const PDFPage* page, const QRectF& rectangle, PageRotation extraRotation) { PageRotation pageRotation = getPageRotationCombined(page->getPageRotation(), extraRotation); QRectF mediaBox = page->getRotatedBox(page->getMediaBox(), pageRotation); return createMediaBoxToDevicePointMatrix(mediaBox, rectangle, pageRotation); } QTransform PDFRenderer::createMediaBoxToDevicePointMatrix(const QRectF& mediaBox, const QRectF& rectangle, PageRotation rotation) { QTransform matrix; switch (rotation) { case PageRotation::None: { matrix.translate(rectangle.left(), rectangle.bottom()); matrix.scale(rectangle.width() / mediaBox.width(), -rectangle.height() / mediaBox.height()); matrix.translate(-mediaBox.left(), -mediaBox.top()); break; } case PageRotation::Rotate90: { matrix.translate(rectangle.left(), rectangle.top()); matrix.rotate(90); matrix.scale(rectangle.width() / mediaBox.width(), -rectangle.height() / mediaBox.height()); matrix.translate(-mediaBox.left(), -mediaBox.top()); break; } case PageRotation::Rotate270: { matrix.translate(rectangle.right(), rectangle.top()); matrix.rotate(-90); matrix.translate(-rectangle.height(), 0); matrix.scale(rectangle.width() / mediaBox.width(), -rectangle.height() / mediaBox.height()); matrix.translate(-mediaBox.left(), -mediaBox.top()); break; } case PageRotation::Rotate180: { matrix.translate(rectangle.left(), rectangle.top()); matrix.scale(rectangle.width() / mediaBox.width(), rectangle.height() / mediaBox.height()); matrix.translate(mediaBox.width(), 0); matrix.translate(-mediaBox.left(), -mediaBox.top()); matrix.scale(-1.0, 1.0); break; } default: { Q_ASSERT(false); break; } } return matrix; } void PDFRenderer::applyFeaturesToColorConvertor(const Features& features, PDFColorConvertor& convertor) { convertor.setMode(PDFColorConvertor::Mode::Normal); if (features.testFlag(ColorAdjust_Invert)) { convertor.setMode(PDFColorConvertor::Mode::InvertedColors); } if (features.testFlag(ColorAdjust_Grayscale)) { convertor.setMode(PDFColorConvertor::Mode::Grayscale); } if (features.testFlag(ColorAdjust_HighContrast)) { convertor.setMode(PDFColorConvertor::Mode::HighContrast); } if (features.testFlag(ColorAdjust_Bitonal)) { convertor.setMode(PDFColorConvertor::Mode::Bitonal); } if (features.testFlag(ColorAdjust_CustomColors)) { convertor.setMode(PDFColorConvertor::Mode::CustomColors); } } const PDFOperationControl* PDFRenderer::getOperationControl() const { return m_operationControl; } void PDFRenderer::setOperationControl(const PDFOperationControl* newOperationControl) { m_operationControl = newOperationControl; } QList PDFRenderer::render(QPainter* painter, const QRectF& rectangle, size_t pageIndex) const { const PDFCatalog* catalog = m_document->getCatalog(); if (pageIndex >= catalog->getPageCount() || !catalog->getPage(pageIndex)) { // Invalid page index return { PDFRenderError(RenderErrorType::Error, PDFTranslationContext::tr("Page %1 doesn't exist.").arg(pageIndex + 1)) }; } const PDFPage* page = catalog->getPage(pageIndex); Q_ASSERT(page); QTransform matrix = createPagePointToDevicePointMatrix(page, rectangle); PDFPainter processor(painter, m_features, matrix, page, m_document, m_fontCache, m_cms, m_optionalContentActivity, m_meshQualitySettings); processor.setOperationControl(m_operationControl); return processor.processContents(); } QList PDFRenderer::render(QPainter* painter, const QTransform& matrix, size_t pageIndex) const { const PDFCatalog* catalog = m_document->getCatalog(); if (pageIndex >= catalog->getPageCount() || !catalog->getPage(pageIndex)) { // Invalid page index return { PDFRenderError(RenderErrorType::Error, PDFTranslationContext::tr("Page %1 doesn't exist.").arg(pageIndex + 1)) }; } const PDFPage* page = catalog->getPage(pageIndex); Q_ASSERT(page); PDFPainter processor(painter, m_features, matrix, page, m_document, m_fontCache, m_cms, m_optionalContentActivity, m_meshQualitySettings); processor.setOperationControl(m_operationControl); return processor.processContents(); } void PDFRenderer::compile(PDFPrecompiledPage* precompiledPage, size_t pageIndex) const { const PDFCatalog* catalog = m_document->getCatalog(); if (pageIndex >= catalog->getPageCount() || !catalog->getPage(pageIndex)) { // Invalid page index precompiledPage->finalize(0, { PDFRenderError(RenderErrorType::Error, PDFTranslationContext::tr("Page %1 doesn't exist.").arg(pageIndex + 1)) }); return; } const PDFPage* page = catalog->getPage(pageIndex); Q_ASSERT(page); QElapsedTimer timer; timer.start(); PDFPrecompiledPageGenerator generator(precompiledPage, m_features, page, m_document, m_fontCache, m_cms, m_optionalContentActivity, m_meshQualitySettings); generator.setOperationControl(m_operationControl); QList errors = generator.processContents(); PDFColorConvertor colorConvertor = m_cms->getColorConvertor(); PDFRenderer::applyFeaturesToColorConvertor(m_features, colorConvertor); precompiledPage->convertColors(colorConvertor); precompiledPage->optimize(); precompiledPage->finalize(timer.nsecsElapsed(), qMove(errors)); timer.invalidate(); } PDFRasterizer::PDFRasterizer(QObject* parent) : BaseClass(parent), m_rendererEngine(RendererEngine::Blend2D) { } PDFRasterizer::~PDFRasterizer() { } void PDFRasterizer::reset(RendererEngine rendererEngine) { m_rendererEngine = rendererEngine; } QImage PDFRasterizer::render(PDFInteger pageIndex, const PDFPage* page, const PDFPrecompiledPage* compiledPage, QSize size, PDFRenderer::Features features, const PDFAnnotationManager* annotationManager, PageRotation extraRotation) { QImage image(size, QImage::Format_ARGB32_Premultiplied); QTransform matrix = PDFRenderer::createPagePointToDevicePointMatrix(page, QRect(QPoint(0, 0), size), extraRotation); if (m_rendererEngine == RendererEngine::Blend2D) { BLContext blContext; BLImage blImage; blContext.setHint(BL_CONTEXT_HINT_RENDERING_QUALITY, BL_RENDERING_QUALITY_MAX_VALUE); blImage.createFromData(image.width(), image.height(), BL_FORMAT_PRGB32, image.bits(), image.bytesPerLine()); if (blContext.begin(blImage) == BL_SUCCESS) { blContext.clearAll(); compiledPage->draw(blContext, page->getCropBox(), matrix, features, 1.0); if (annotationManager) { QList errors; PDFTextLayoutGetter textLayoutGetter(nullptr, pageIndex); annotationManager->drawPage(blContext, pageIndex, compiledPage, textLayoutGetter, matrix, errors); } blContext.end(); } } else { // Use standard software rasterizer. image.fill(Qt::white); QPainter painter(&image); compiledPage->draw(&painter, page->getCropBox(), matrix, features, 1.0); if (annotationManager) { QList errors; PDFTextLayoutGetter textLayoutGetter(nullptr, pageIndex); annotationManager->drawPage(&painter, pageIndex, compiledPage, textLayoutGetter, matrix, errors); } } // Calculate image DPI QSizeF rotatedSizeInMeters = page->getRotatedMediaBoxMM().size() / 1000.0; QSizeF rotatedSizeInPixels = image.size(); qreal dpiX = rotatedSizeInPixels.width() / rotatedSizeInMeters.width(); qreal dpiY = rotatedSizeInPixels.height() / rotatedSizeInMeters.height(); image.setDotsPerMeterX(qCeil(dpiX)); image.setDotsPerMeterY(qCeil(dpiY)); return image; } PDFRasterizer* PDFRasterizerPool::acquire() { m_semaphore.acquire(); QMutexLocker guard(&m_mutex); Q_ASSERT(!m_rasterizers.empty()); PDFRasterizer* rasterizer = m_rasterizers.back(); m_rasterizers.pop_back(); return rasterizer; } void PDFRasterizerPool::release(pdf::PDFRasterizer* rasterizer) { QMutexLocker guard(&m_mutex); Q_ASSERT(std::find(m_rasterizers.cbegin(), m_rasterizers.cend(), rasterizer) == m_rasterizers.cend()); m_rasterizers.push_back(rasterizer); // Jakub Melka: we must release it at the end, to ensure rasterizer is in the array before // semaphore is released, to avoid race condition. m_semaphore.release(); } void PDFRasterizerPool::render(const std::vector& pageIndices, const PDFRasterizerPool::PageImageSizeGetter& imageSizeGetter, const PDFRasterizerPool::ProcessImageMethod& processImage, PDFProgress* progress) { if (pageIndices.empty()) { return; } Q_ASSERT(imageSizeGetter); Q_ASSERT(processImage); QElapsedTimer timer; timer.start(); Q_EMIT renderError(PDFCatalog::INVALID_PAGE_INDEX, PDFRenderError(RenderErrorType::Information, PDFTranslationContext::tr("Start at %1...").arg(QTime::currentTime().toString(Qt::TextDate)))); if (progress) { ProgressStartupInfo info; info.showDialog = true; info.text = PDFTranslationContext::tr("Rendering document into images."); progress->start(pageIndices.size(), qMove(info)); } auto processPage = [this, progress, &imageSizeGetter, &processImage](const PDFInteger pageIndex) { const PDFPage* page = m_document->getCatalog()->getPage(pageIndex); if (!page) { if (progress) { progress->step(); } Q_EMIT renderError(pageIndex, PDFRenderError(RenderErrorType::Error, PDFTranslationContext::tr("Page %1 not found.").arg(pageIndex))); return; } QElapsedTimer totalPageTimer; totalPageTimer.start(); QElapsedTimer pageTimer; pageTimer.start(); // Precompile the page PDFPrecompiledPage precompiledPage; PDFCMSPointer cms = m_cmsManager->getCurrentCMS(); PDFRenderer renderer(m_document, m_fontCache, cms.data(), m_optionalContentActivity, m_features, m_meshQualitySettings); renderer.compile(&precompiledPage, pageIndex); qint64 pageCompileTime = pageTimer.restart(); for (const PDFRenderError& error : precompiledPage.getErrors()) { Q_EMIT renderError(pageIndex, error); } // We can const-cast here, because we do not modify the document in annotation manager. // Annotations are just rendered to the target picture. PDFModifiedDocument modifiedDocument(const_cast(m_document), const_cast(m_optionalContentActivity)); // Annotation manager PDFAnnotationManager annotationManager(m_fontCache, m_cmsManager, m_optionalContentActivity, m_meshQualitySettings, m_features, PDFAnnotationManager::Target::Print, nullptr); annotationManager.setDocument(modifiedDocument); // Render page to image pageTimer.restart(); PDFRasterizer* rasterizer = acquire(); qint64 pageWaitTime = pageTimer.restart(); QImage image = rasterizer->render(pageIndex, page, &precompiledPage, imageSizeGetter(page), m_features, &annotationManager, PageRotation::None); qint64 pageRenderTime = pageTimer.elapsed(); release(rasterizer); // Now, process the image PDFRenderedPageImage renderedPageImage; renderedPageImage.pageIndex = pageIndex; renderedPageImage.pageImage = qMove(image); renderedPageImage.pageCompileTime = pageCompileTime; renderedPageImage.pageWaitTime = pageWaitTime; renderedPageImage.pageRenderTime = pageRenderTime; renderedPageImage.pageTotalTime = totalPageTimer.elapsed(); processImage(renderedPageImage); if (progress) { progress->step(); } }; PDFExecutionPolicy::execute(PDFExecutionPolicy::Scope::Page, pageIndices.cbegin(), pageIndices.cend(), processPage); if (progress) { progress->finish(); } Q_EMIT renderError(PDFCatalog::INVALID_PAGE_INDEX, PDFRenderError(RenderErrorType::Information, PDFTranslationContext::tr("Finished at %1...").arg(QTime::currentTime().toString(Qt::TextDate)))); Q_EMIT renderError(PDFCatalog::INVALID_PAGE_INDEX, PDFRenderError(RenderErrorType::Information, PDFTranslationContext::tr("%1 miliseconds elapsed to render %2 pages...").arg(timer.nsecsElapsed() / 1000000).arg(pageIndices.size()))); } int PDFRasterizerPool::getDefaultRasterizerCount() { int hint = QThread::idealThreadCount() / 2; return getCorrectedRasterizerCount(hint); } int PDFRasterizerPool::getCorrectedRasterizerCount(int rasterizerCount) { return qBound(1, rasterizerCount, 256); } PDFImageWriterSettings::PDFImageWriterSettings() { m_formats = QImageWriter::supportedImageFormats(); constexpr const char* DEFAULT_FORMAT = "png"; if (m_formats.count(DEFAULT_FORMAT)) { selectFormat(DEFAULT_FORMAT); } else { selectFormat(m_formats.front()); } } void PDFImageWriterSettings::selectFormat(const QByteArray& format) { if (m_currentFormat != format) { m_currentFormat = format; QImageWriter writer; writer.setFormat(format); m_compression = 0; m_quality = 0; m_gamma = 0; m_optimizedWrite = false; m_progressiveScanWrite = false; m_subtypes = writer.supportedSubTypes(); m_currentSubtype = !m_subtypes.isEmpty() ? m_subtypes.front() : QByteArray(); // Jakub Melka: init default values based on image handler. Unfortunately, // image writer doesn't give us access to these values, so they are hardcoded. if (format == "jpeg" || format == "jpg") { m_quality = 75; m_optimizedWrite = false; m_progressiveScanWrite = false; } else if (format == "png") { m_compression = 50; m_quality = 50; m_gamma = 0; } else if (format == "tif" || format == "tiff") { m_compression = 1; } else if (format == "webp") { m_quality = 75; } m_supportedOptions.clear(); for (QImageIOHandler::ImageOption imageOption : { QImageIOHandler::CompressionRatio, QImageIOHandler::Quality, QImageIOHandler::Gamma, QImageIOHandler::OptimizedWrite, QImageIOHandler::ProgressiveScanWrite, QImageIOHandler::SupportedSubTypes }) { if (writer.supportsOption(imageOption)) { m_supportedOptions.insert(imageOption); } } } } int PDFImageWriterSettings::getCompression() const { return m_compression; } void PDFImageWriterSettings::setCompression(int compression) { m_compression = compression; } int PDFImageWriterSettings::getQuality() const { return m_quality; } void PDFImageWriterSettings::setQuality(int quality) { m_quality = quality; } float PDFImageWriterSettings::getGamma() const { return m_gamma; } void PDFImageWriterSettings::setGamma(float gamma) { m_gamma = gamma; } bool PDFImageWriterSettings::hasOptimizedWrite() const { return m_optimizedWrite; } void PDFImageWriterSettings::setOptimizedWrite(bool optimizedWrite) { m_optimizedWrite = optimizedWrite; } bool PDFImageWriterSettings::hasProgressiveScanWrite() const { return m_progressiveScanWrite; } void PDFImageWriterSettings::setProgressiveScanWrite(bool progressiveScanWrite) { m_progressiveScanWrite = progressiveScanWrite; } QByteArray PDFImageWriterSettings::getCurrentFormat() const { return m_currentFormat; } QByteArray PDFImageWriterSettings::getCurrentSubtype() const { return m_currentSubtype; } void PDFImageWriterSettings::setCurrentSubtype(const QByteArray& currentSubtype) { m_currentSubtype = currentSubtype; } PDFPageImageExportSettings::PDFPageImageExportSettings(const PDFDocument* document) : m_document(document) { m_fileTemplate = PDFTranslationContext::tr("Image_%"); } PDFPageImageExportSettings::ResolutionMode PDFPageImageExportSettings::getResolutionMode() const { return m_resolutionMode; } void PDFPageImageExportSettings::setResolutionMode(ResolutionMode resolution) { m_resolutionMode = resolution; } PDFPageImageExportSettings::PageSelectionMode PDFPageImageExportSettings::getPageSelectionMode() const { return m_pageSelectionMode; } void PDFPageImageExportSettings::setPageSelectionMode(PageSelectionMode pageSelectionMode) { m_pageSelectionMode = pageSelectionMode; } QString PDFPageImageExportSettings::getDirectory() const { return m_directory; } void PDFPageImageExportSettings::setDirectory(const QString& directory) { m_directory = directory; } QString PDFPageImageExportSettings::getFileTemplate() const { return m_fileTemplate; } void PDFPageImageExportSettings::setFileTemplate(const QString& fileTemplate) { m_fileTemplate = fileTemplate; } QString PDFPageImageExportSettings::getPageSelection() const { return m_pageSelection; } void PDFPageImageExportSettings::setPageSelection(const QString& pageSelection) { m_pageSelection = pageSelection; } int PDFPageImageExportSettings::getDpiResolution() const { return m_dpiResolution; } void PDFPageImageExportSettings::setDpiResolution(int dpiResolution) { m_dpiResolution = dpiResolution; } int PDFPageImageExportSettings::getPixelResolution() const { return m_pixelResolution; } void PDFPageImageExportSettings::setPixelResolution(int pixelResolution) { m_pixelResolution = pixelResolution; } bool PDFPageImageExportSettings::validate(QString* errorMessagePtr, bool validatePageSelection, bool validateFileSettings, bool validateResolution) const { QString dummy; QString& errorMessage = errorMessagePtr ? *errorMessagePtr : dummy; if (validateFileSettings) { if (m_directory.isEmpty()) { errorMessage = PDFTranslationContext::tr("Target directory is empty."); return false; } // Check, if target directory exists QDir directory(m_directory); if (!directory.exists()) { errorMessage = PDFTranslationContext::tr("Target directory '%1' doesn't exist.").arg(m_directory); return false; } if (m_fileTemplate.isEmpty()) { errorMessage = PDFTranslationContext::tr("File template is empty."); return false; } if (!m_fileTemplate.contains("%")) { errorMessage = PDFTranslationContext::tr("File template must contain character '%' for page number."); return false; } } // Check page selection if (validatePageSelection) { if (m_pageSelectionMode == PageSelectionMode::Selection) { std::vector pages = getPages(); if (pages.empty()) { errorMessage = PDFTranslationContext::tr("Page list is invalid. It should have form such as '1-12,17,24,27-29'."); return false; } if (pages.back() >= PDFInteger(m_document->getCatalog()->getPageCount())) { errorMessage = PDFTranslationContext::tr("Page list contains page, which is not in the document (%1).").arg(pages.back()); return false; } } } if (validateResolution) { if (m_resolutionMode == ResolutionMode::DPI && (m_dpiResolution < getMinDPIResolution() || m_dpiResolution > getMaxDPIResolution())) { errorMessage = PDFTranslationContext::tr("DPI resolution should be in range %1 to %2.").arg(getMinDPIResolution()).arg(getMaxDPIResolution()); return false; } if (m_resolutionMode == ResolutionMode::Pixels && (m_pixelResolution < getMinPixelResolution() || m_pixelResolution > getMaxPixelResolution())) { errorMessage = PDFTranslationContext::tr("Pixel resolution should be in range %1 to %2.").arg(getMinPixelResolution()).arg(getMaxPixelResolution()); return false; } } return true; } std::vector PDFPageImageExportSettings::getPages() const { std::vector result; switch (m_pageSelectionMode) { case PageSelectionMode::All: { result.resize(m_document->getCatalog()->getPageCount(), 0); std::iota(result.begin(), result.end(), 0); break; } case PageSelectionMode::Selection: { bool ok = false; QStringList parts = m_pageSelection.split(QChar(','), Qt::SkipEmptyParts, Qt::CaseSensitive); for (const QString& part : parts) { QStringList numbers = part.split(QChar('-'), Qt::KeepEmptyParts, Qt::CaseSensitive); switch (numbers.size()) { case 1: { const QString& numberString = numbers.front(); result.push_back(numberString.toLongLong(&ok) - 1); break; } case 2: { bool ok1 = false; bool ok2 = false; const QString& lowString = numbers.front(); const QString& highString = numbers.back(); const PDFInteger low = lowString.toLongLong(&ok1) - 1; const PDFInteger high = highString.toLongLong(&ok2) - 1; ok = ok1 && ok2 && low <= high && low >= 0; if (ok) { const PDFInteger count = high - low + 1; result.resize(result.size() + count, 0); std::iota(std::prev(result.end(), count), result.end(), low); } break; } default: { ok = true; break; } } // If error is detected, do not continue in parsing if (!ok) { break; } // We must remove duplicate pages std::sort(result.begin(), result.end()); result.erase(std::unique(result.begin(), result.end()), result.end()); } if (!ok) { result.clear(); } break; } default: break; } return result; } QString PDFPageImageExportSettings::getOutputFileName(PDFInteger pageIndex, const QByteArray& outputFormat) const { QString fileName = m_fileTemplate; fileName.replace('%', QString::number(pageIndex + 1)); QFileInfo fileInfo(fileName); if (fileInfo.suffix() != outputFormat) { fileName = QString("%1.%2").arg(fileName, QString::fromLatin1(outputFormat)); } // Add directory QString fileNameWithDirectory = QString("%1/%2").arg(m_directory, fileName); return QDir::toNativeSeparators(fileNameWithDirectory); } PDFRasterizerPool::PDFRasterizerPool(const PDFDocument* document, PDFFontCache* fontCache, const PDFCMSManager* cmsManager, const PDFOptionalContentActivity* optionalContentActivity, PDFRenderer::Features features, const PDFMeshQualitySettings& meshQualitySettings, int rasterizerCount, RendererEngine rendererEngine, QObject* parent) : BaseClass(parent), m_document(document), m_fontCache(fontCache), m_cmsManager(cmsManager), m_optionalContentActivity(optionalContentActivity), m_features(features), m_meshQualitySettings(meshQualitySettings), m_semaphore(rasterizerCount) { m_rasterizers.reserve(rasterizerCount); for (int i = 0; i < rasterizerCount; ++i) { m_rasterizers.push_back(new PDFRasterizer(this)); m_rasterizers.back()->reset(rendererEngine); } } } // namespace pdf