From 0277a9f0596e88e97909912af89c8d64598944f3 Mon Sep 17 00:00:00 2001 From: Jakub Melka Date: Sat, 14 Dec 2019 14:39:43 +0100 Subject: [PATCH] Precompiled pages --- .../sources/pdfdrawspacecontroller.cpp | 19 + PdfForQtLib/sources/pdfpainter.cpp | 583 +++++++++++++----- PdfForQtLib/sources/pdfpainter.h | 267 +++++++- PdfForQtLib/sources/pdfrenderer.cpp | 59 +- PdfForQtLib/sources/pdfrenderer.h | 15 +- 5 files changed, 725 insertions(+), 218 deletions(-) diff --git a/PdfForQtLib/sources/pdfdrawspacecontroller.cpp b/PdfForQtLib/sources/pdfdrawspacecontroller.cpp index 3a35f12..542328c 100644 --- a/PdfForQtLib/sources/pdfdrawspacecontroller.cpp +++ b/PdfForQtLib/sources/pdfdrawspacecontroller.cpp @@ -19,6 +19,7 @@ #include "pdfdrawspacecontroller.h" #include "pdfdrawwidget.h" #include "pdfrenderer.h" +#include "pdfpainter.h" #include @@ -599,9 +600,27 @@ void PDFDrawWidgetProxy::draw(QPainter* painter, QRect rect) // Clear the page space by white color painter->fillRect(placedRect, Qt::white); + /* PDFRenderer renderer(m_controller->getDocument(), m_controller->getFontCache(), m_controller->getOptionalContentActivity(), m_features, m_meshQualitySettings); QList errors = renderer.render(painter, placedRect, item.pageIndex); + if (!errors.empty()) + { + emit renderingError(item.pageIndex, errors); + }*/ + + PDFPrecompiledPage compiledPage; + PDFRenderer renderer(m_controller->getDocument(), m_controller->getFontCache(), m_controller->getOptionalContentActivity(), m_features, m_meshQualitySettings); + renderer.compile(&compiledPage, item.pageIndex); + + if (compiledPage.isValid()) + { + const PDFPage* page = m_controller->getDocument()->getCatalog()->getPage(item.pageIndex); + QMatrix matrix = renderer.createPagePointToDevicePointMatrix(page, placedRect); + compiledPage.draw(painter, page->getCropBox(), matrix, m_features); + } + + const QList& errors = compiledPage.getErrors(); if (!errors.empty()) { emit renderingError(item.pageIndex, errors); diff --git a/PdfForQtLib/sources/pdfpainter.cpp b/PdfForQtLib/sources/pdfpainter.cpp index 5660205..fb1d29e 100644 --- a/PdfForQtLib/sources/pdfpainter.cpp +++ b/PdfForQtLib/sources/pdfpainter.cpp @@ -23,6 +23,210 @@ namespace pdf { +PDFPainterBase::PDFPainterBase(PDFRenderer::Features features, + const PDFPage* page, + const PDFDocument* document, + const PDFFontCache* fontCache, + const PDFOptionalContentActivity* optionalContentActivity, + QMatrix pagePointToDevicePointMatrix, + const PDFMeshQualitySettings& meshQualitySettings) : + BaseClass(page, document, fontCache, optionalContentActivity, pagePointToDevicePointMatrix, meshQualitySettings), + m_features(features) +{ + +} + +void PDFPainterBase::performUpdateGraphicsState(const PDFPageContentProcessorState& state) +{ + const PDFPageContentProcessorState::StateFlags flags = state.getStateFlags(); + + // If current transformation matrix has changed, then update it + if (flags.testFlag(PDFPageContentProcessorState::StateCurrentTransformationMatrix)) + { + setWorldMatrix(getCurrentWorldMatrix()); + } + + if (flags.testFlag(PDFPageContentProcessorState::StateStrokeColor) || + flags.testFlag(PDFPageContentProcessorState::StateLineWidth) || + flags.testFlag(PDFPageContentProcessorState::StateLineCapStyle) || + flags.testFlag(PDFPageContentProcessorState::StateLineJoinStyle) || + flags.testFlag(PDFPageContentProcessorState::StateMitterLimit) || + flags.testFlag(PDFPageContentProcessorState::StateLineDashPattern) || + flags.testFlag(PDFPageContentProcessorState::StateAlphaStroking)) + { + m_currentPen.dirty(); + } + + if (flags.testFlag(PDFPageContentProcessorState::StateFillColor) || + flags.testFlag(PDFPageContentProcessorState::StateAlphaFilling)) + { + m_currentBrush.dirty(); + } + + // If current blend mode has changed, then update it + if (flags.testFlag(PDFPageContentProcessorState::StateBlendMode)) + { + // Try to simulate transparency groups. Use only first composition mode, + // outside the transparency groups (so we are on pages main transparency + // groups). + + const BlendMode blendMode = state.getBlendMode(); + if (canSetBlendMode(blendMode)) + { + if (!PDFBlendModeInfo::isSupportedByQt(blendMode)) + { + reportRenderErrorOnce(RenderErrorType::NotSupported, PDFTranslationContext::tr("Blend mode '%1' not supported.").arg(PDFBlendModeInfo::getBlendModeName(blendMode))); + } + + const QPainter::CompositionMode compositionMode = PDFBlendModeInfo::getCompositionModeFromBlendMode(blendMode); + setCompositionMode(compositionMode); + } + else if (blendMode != BlendMode::Normal && blendMode != BlendMode::Compatible) + { + reportRenderErrorOnce(RenderErrorType::NotSupported, PDFTranslationContext::tr("Blend mode '%1' is in transparency group, which is not supported.").arg(PDFBlendModeInfo::getBlendModeName(blendMode))); + } + } + + BaseClass::performUpdateGraphicsState(state); +} + +bool PDFPainterBase::isContentSuppressedByOC(PDFObjectReference ocgOrOcmd) +{ + if (m_features.testFlag(PDFRenderer::IgnoreOptionalContent)) + { + return false; + } + + return PDFPageContentProcessor::isContentSuppressedByOC(ocgOrOcmd); +} + +QPen PDFPainterBase::getCurrentPenImpl() const +{ + const PDFPageContentProcessorState* graphicState = getGraphicState(); + QColor color = graphicState->getStrokeColor(); + if (color.isValid()) + { + color.setAlphaF(getEffectiveStrokingAlpha()); + const PDFReal lineWidth = graphicState->getLineWidth(); + Qt::PenCapStyle penCapStyle = graphicState->getLineCapStyle(); + Qt::PenJoinStyle penJoinStyle = graphicState->getLineJoinStyle(); + const PDFLineDashPattern& lineDashPattern = graphicState->getLineDashPattern(); + const PDFReal mitterLimit = graphicState->getMitterLimit(); + + QPen pen(color); + + pen.setWidthF(lineWidth); + pen.setCapStyle(penCapStyle); + pen.setJoinStyle(penJoinStyle); + pen.setMiterLimit(mitterLimit); + + if (lineDashPattern.isSolid()) + { + pen.setStyle(Qt::SolidLine); + } + else + { + pen.setStyle(Qt::CustomDashLine); + pen.setDashPattern(QVector::fromStdVector(lineDashPattern.getDashArray())); + pen.setDashOffset(lineDashPattern.getDashOffset()); + } + + return pen; + } + else + { + return QPen(Qt::NoPen); + } +} + +QBrush PDFPainterBase::getCurrentBrushImpl() const +{ + const PDFPageContentProcessorState* graphicState = getGraphicState(); + QColor color = graphicState->getFillColor(); + if (color.isValid()) + { + color.setAlphaF(getEffectiveFillingAlpha()); + return QBrush(color, Qt::SolidPattern); + } + else + { + return QBrush(Qt::NoBrush); + } +} + +PDFReal PDFPainterBase::getEffectiveStrokingAlpha() const +{ + PDFReal alpha = getGraphicState()->getAlphaStroking(); + + auto it = m_transparencyGroupDataStack.crbegin(); + auto itEnd = m_transparencyGroupDataStack.crend(); + for (; it != itEnd; ++it) + { + const PDFTransparencyGroupPainterData& transparencyGroup = *it; + alpha *= transparencyGroup.alphaStroke; + + if (transparencyGroup.group.isolated) + { + break; + } + } + + return alpha; +} + +PDFReal PDFPainterBase::getEffectiveFillingAlpha() const +{ + PDFReal alpha = getGraphicState()->getAlphaFilling(); + + auto it = m_transparencyGroupDataStack.crbegin(); + auto itEnd = m_transparencyGroupDataStack.crend(); + for (; it != itEnd; ++it) + { + const PDFTransparencyGroupPainterData& transparencyGroup = *it; + alpha *= transparencyGroup.alphaFill; + + if (transparencyGroup.group.isolated) + { + break; + } + } + + return alpha; +} + +bool PDFPainterBase::canSetBlendMode(BlendMode mode) const +{ + // We will assume, that we can set blend mode, when + // all other blend modes on transparency stack are normal, + // or compatible. It should work. + + Q_UNUSED(mode); + return std::all_of(m_transparencyGroupDataStack.cbegin(), m_transparencyGroupDataStack.cend(), [](const PDFTransparencyGroupPainterData& group) { return group.blendMode == BlendMode::Normal || group.blendMode == BlendMode::Compatible; }); +} + +void PDFPainterBase::performBeginTransparencyGroup(ProcessOrder order, const PDFTransparencyGroup& transparencyGroup) +{ + if (order == ProcessOrder::BeforeOperation) + { + PDFTransparencyGroupPainterData data; + data.group = transparencyGroup; + data.alphaFill = getGraphicState()->getAlphaFilling(); + data.alphaStroke = getGraphicState()->getAlphaStroking(); + data.blendMode = getGraphicState()->getBlendMode(); + m_transparencyGroupDataStack.emplace_back(qMove(data)); + } +} + +void PDFPainterBase::performEndTransparencyGroup(ProcessOrder order, const PDFTransparencyGroup& transparencyGroup) +{ + Q_UNUSED(transparencyGroup); + + if (order == ProcessOrder::AfterOperation) + { + m_transparencyGroupDataStack.pop_back(); + } +} + PDFPainter::PDFPainter(QPainter* painter, PDFRenderer::Features features, QMatrix pagePointToDevicePointMatrix, @@ -31,9 +235,8 @@ PDFPainter::PDFPainter(QPainter* painter, const PDFFontCache* fontCache, const PDFOptionalContentActivity* optionalContentActivity, const PDFMeshQualitySettings& meshQualitySettings) : - PDFPageContentProcessor(page, document, fontCache, optionalContentActivity, pagePointToDevicePointMatrix, meshQualitySettings), - m_painter(painter), - m_features(features) + BaseClass(features, page, document, fontCache, optionalContentActivity, pagePointToDevicePointMatrix, meshQualitySettings), + m_painter(painter) { Q_ASSERT(painter); Q_ASSERT(pagePointToDevicePointMatrix.isInvertible()); @@ -65,7 +268,7 @@ void PDFPainter::performPathPainting(const QPainterPath& path, bool stroke, bool Q_ASSERT(stroke || fill); // Set antialiasing - const bool antialiasing = (text && m_features.testFlag(PDFRenderer::TextAntialiasing)) || (!text && m_features.testFlag(PDFRenderer::Antialiasing)); + const bool antialiasing = (text && hasFeature(PDFRenderer::TextAntialiasing)) || (!text && hasFeature(PDFRenderer::Antialiasing)); m_painter->setRenderHint(QPainter::Antialiasing, antialiasing); if (stroke) @@ -108,7 +311,7 @@ void PDFPainter::performImagePainting(const QImage& image) QImage adjustedImage = image; - if (m_features.testFlag(PDFRenderer::SmoothImages)) + if (hasFeature(PDFRenderer::SmoothImages)) { // Test, if we can use smooth images. We can use them under following conditions: // 1) Transformed rectangle is not skewed or deformed (so vectors (0, 1) and (1, 0) are orthogonal) @@ -158,61 +361,6 @@ void PDFPainter::performMeshPainting(const PDFMesh& mesh) m_painter->restore(); } -void PDFPainter::performUpdateGraphicsState(const PDFPageContentProcessorState& state) -{ - const PDFPageContentProcessorState::StateFlags flags = state.getStateFlags(); - - // If current transformation matrix has changed, then update it - if (flags.testFlag(PDFPageContentProcessorState::StateCurrentTransformationMatrix)) - { - m_painter->setWorldMatrix(getCurrentWorldMatrix(), false); - } - - if (flags.testFlag(PDFPageContentProcessorState::StateStrokeColor) || - flags.testFlag(PDFPageContentProcessorState::StateLineWidth) || - flags.testFlag(PDFPageContentProcessorState::StateLineCapStyle) || - flags.testFlag(PDFPageContentProcessorState::StateLineJoinStyle) || - flags.testFlag(PDFPageContentProcessorState::StateMitterLimit) || - flags.testFlag(PDFPageContentProcessorState::StateLineDashPattern) || - flags.testFlag(PDFPageContentProcessorState::StateAlphaStroking)) - { - m_currentPen.dirty(); - } - - if (flags.testFlag(PDFPageContentProcessorState::StateFillColor) || - flags.testFlag(PDFPageContentProcessorState::StateAlphaFilling)) - { - m_currentBrush.dirty(); - } - - // If current blend mode has changed, then update it - if (flags.testFlag(PDFPageContentProcessorState::StateBlendMode)) - { - // Try to simulate transparency groups. Use only first composition mode, - // outside the transparency groups (so we are on pages main transparency - // groups). - - const BlendMode blendMode = state.getBlendMode(); - if (canSetBlendMode(blendMode)) - { - const QPainter::CompositionMode compositionMode = PDFBlendModeInfo::getCompositionModeFromBlendMode(blendMode); - - if (!PDFBlendModeInfo::isSupportedByQt(blendMode)) - { - reportRenderErrorOnce(RenderErrorType::NotSupported, PDFTranslationContext::tr("Blend mode '%1' not supported.").arg(PDFBlendModeInfo::getBlendModeName(blendMode))); - } - - m_painter->setCompositionMode(compositionMode); - } - else if (blendMode != BlendMode::Normal && blendMode != BlendMode::Compatible) - { - reportRenderErrorOnce(RenderErrorType::NotSupported, PDFTranslationContext::tr("Blend mode '%1' is in transparency group, which is not supported.").arg(PDFBlendModeInfo::getBlendModeName(blendMode))); - } - } - - PDFPageContentProcessor::performUpdateGraphicsState(state); -} - void PDFPainter::performSaveGraphicState(ProcessOrder order) { if (order == ProcessOrder::AfterOperation) @@ -229,141 +377,242 @@ void PDFPainter::performRestoreGraphicState(ProcessOrder order) } } -void PDFPainter::performBeginTransparencyGroup(ProcessOrder order, const PDFTransparencyGroup& transparencyGroup) +void PDFPainter::setWorldMatrix(const QMatrix& matrix) +{ + m_painter->setWorldMatrix(matrix, false); +} + +void PDFPainter::setCompositionMode(QPainter::CompositionMode mode) +{ + m_painter->setCompositionMode(mode); +} + +void PDFPrecompiledPageGenerator::performPathPainting(const QPainterPath& path, bool stroke, bool fill, bool text, Qt::FillRule fillRule) +{ + Q_ASSERT(stroke || fill); + Q_ASSERT(path.fillRule() == fillRule); + + QPen pen = stroke ? getCurrentPen() : QPen(Qt::NoPen); + QBrush brush = fill ? getCurrentBrush() : QBrush(Qt::NoBrush); + m_precompiledPage->addPath(qMove(pen), qMove(brush), path, text); +} + +void PDFPrecompiledPageGenerator::performClipping(const QPainterPath& path, Qt::FillRule fillRule) +{ + Q_ASSERT(path.fillRule() == fillRule); + m_precompiledPage->addClip(path); +} + +void PDFPrecompiledPageGenerator::performImagePainting(const QImage& image) +{ + if (isContentSuppressed()) + { + // Content is suppressed, do not paint anything + return; + } + + m_precompiledPage->addImage(image); +} + +void PDFPrecompiledPageGenerator::performMeshPainting(const PDFMesh& mesh) +{ + m_precompiledPage->addMesh(mesh, getEffectiveFillingAlpha()); +} + +void PDFPrecompiledPageGenerator::performSaveGraphicState(PDFPageContentProcessor::ProcessOrder order) +{ + if (order == ProcessOrder::AfterOperation) + { + m_precompiledPage->addSaveGraphicState(); + } +} + +void PDFPrecompiledPageGenerator::performRestoreGraphicState(PDFPageContentProcessor::ProcessOrder order) { if (order == ProcessOrder::BeforeOperation) { - PDFTransparencyGroupPainterData data; - data.group = transparencyGroup; - data.alphaFill = getGraphicState()->getAlphaFilling(); - data.alphaStroke = getGraphicState()->getAlphaStroking(); - data.blendMode = getGraphicState()->getBlendMode(); - m_transparencyGroupDataStack.emplace_back(qMove(data)); + m_precompiledPage->addRestoreGraphicState(); } } -void PDFPainter::performEndTransparencyGroup(PDFPageContentProcessor::ProcessOrder order, const PDFPageContentProcessor::PDFTransparencyGroup& transparencyGroup) +void PDFPrecompiledPageGenerator::setWorldMatrix(const QMatrix& matrix) { - Q_UNUSED(transparencyGroup); - - if (order == ProcessOrder::AfterOperation) - { - m_transparencyGroupDataStack.pop_back(); - } + m_precompiledPage->addSetWorldMatrix(matrix); } -bool PDFPainter::isContentSuppressedByOC(PDFObjectReference ocgOrOcmd) +void PDFPrecompiledPageGenerator::setCompositionMode(QPainter::CompositionMode mode) { - if (m_features.testFlag(PDFRenderer::IgnoreOptionalContent)) - { - return false; - } - - return PDFPageContentProcessor::isContentSuppressedByOC(ocgOrOcmd); + m_precompiledPage->addSetCompositionMode(mode); } -QPen PDFPainter::getCurrentPenImpl() const +void PDFPrecompiledPage::draw(QPainter* painter, const QRectF& cropBox, const QMatrix& pagePointToDevicePointMatrix, PDFRenderer::Features features) const { - const PDFPageContentProcessorState* graphicState = getGraphicState(); - QColor color = graphicState->getStrokeColor(); - if (color.isValid()) + Q_ASSERT(painter); + Q_ASSERT(pagePointToDevicePointMatrix.isInvertible()); + + painter->save(); + + if (features.testFlag(PDFRenderer::ClipToCropBox)) { - color.setAlphaF(getEffectiveStrokingAlpha()); - const PDFReal lineWidth = graphicState->getLineWidth(); - Qt::PenCapStyle penCapStyle = graphicState->getLineCapStyle(); - Qt::PenJoinStyle penJoinStyle = graphicState->getLineJoinStyle(); - const PDFLineDashPattern& lineDashPattern = graphicState->getLineDashPattern(); - const PDFReal mitterLimit = graphicState->getMitterLimit(); - - QPen pen(color); - - pen.setWidthF(lineWidth); - pen.setCapStyle(penCapStyle); - pen.setJoinStyle(penJoinStyle); - pen.setMiterLimit(mitterLimit); - - if (lineDashPattern.isSolid()) + if (cropBox.isValid()) { - pen.setStyle(Qt::SolidLine); - } - else - { - pen.setStyle(Qt::CustomDashLine); - pen.setDashPattern(QVector::fromStdVector(lineDashPattern.getDashArray())); - pen.setDashOffset(lineDashPattern.getDashOffset()); - } - - return pen; - } - else - { - return QPen(Qt::NoPen); - } -} - -QBrush PDFPainter::getCurrentBrushImpl() const -{ - const PDFPageContentProcessorState* graphicState = getGraphicState(); - QColor color = graphicState->getFillColor(); - if (color.isValid()) - { - color.setAlphaF(getEffectiveFillingAlpha()); - return QBrush(color, Qt::SolidPattern); - } - else - { - return QBrush(Qt::NoBrush); - } -} - -PDFReal PDFPainter::getEffectiveStrokingAlpha() const -{ - PDFReal alpha = getGraphicState()->getAlphaStroking(); - - auto it = m_transparencyGroupDataStack.crbegin(); - auto itEnd = m_transparencyGroupDataStack.crend(); - for (; it != itEnd; ++it) - { - const PDFTransparencyGroupPainterData& transparencyGroup = *it; - alpha *= transparencyGroup.alphaStroke; - - if (transparencyGroup.group.isolated) - { - break; + QPainterPath path; + path.addPolygon(pagePointToDevicePointMatrix.map(cropBox)); + painter->setClipPath(path, Qt::IntersectClip); } } - return alpha; -} + painter->setRenderHint(QPainter::SmoothPixmapTransform, features.testFlag(PDFRenderer::SmoothImages)); -PDFReal PDFPainter::getEffectiveFillingAlpha() const -{ - PDFReal alpha = getGraphicState()->getAlphaFilling(); - - auto it = m_transparencyGroupDataStack.crbegin(); - auto itEnd = m_transparencyGroupDataStack.crend(); - for (; it != itEnd; ++it) + // Process all instructions + for (const Instruction& instruction : m_instructions) { - const PDFTransparencyGroupPainterData& transparencyGroup = *it; - alpha *= transparencyGroup.alphaFill; - - if (transparencyGroup.group.isolated) + switch (instruction.type) { - break; + case InstructionType::DrawPath: + { + const PathPaintData& data = m_paths[instruction.dataIndex]; + + // Set antialiasing + const bool antialiasing = (data.isText && features.testFlag(PDFRenderer::TextAntialiasing)) || (!data.isText && features.testFlag(PDFRenderer::Antialiasing)); + painter->setRenderHint(QPainter::Antialiasing, antialiasing); + painter->setPen(data.pen); + painter->setBrush(data.brush); + painter->drawPath(data.path); + break; + } + + case InstructionType::DrawImage: + { + const ImageData& data = m_images[instruction.dataIndex]; + const QImage& image = data.image; + + painter->save(); + + QMatrix imageTransform(1.0 / image.width(), 0, 0, 1.0 / image.height(), 0, 0); + QMatrix worldMatrix = imageTransform * painter->worldMatrix(); + + // Jakub Melka: Because Qt uses opposite axis direction than PDF, then we must transform the y-axis + // to the opposite (so the image is then unchanged) + worldMatrix.translate(0, image.height()); + worldMatrix.scale(1, -1); + + painter->setWorldMatrix(worldMatrix); + painter->drawImage(0, 0, image); + painter->restore(); + break; + } + + case InstructionType::DrawMesh: + { + const MeshPaintData& data = m_meshes[instruction.dataIndex]; + + painter->save(); + painter->setWorldMatrix(pagePointToDevicePointMatrix); + data.mesh.paint(painter, data.alpha); + painter->restore(); + break; + } + + case InstructionType::Clip: + { + painter->setClipPath(m_clips[instruction.dataIndex].clipPath, Qt::IntersectClip); + break; + } + + case InstructionType::SaveGraphicState: + { + painter->save(); + break; + } + + case InstructionType::RestoreGraphicState: + { + painter->restore(); + break; + } + + case InstructionType::SetWorldMatrix: + { + painter->setWorldMatrix(m_matrices[instruction.dataIndex] * pagePointToDevicePointMatrix); + break; + } + + case InstructionType::SetCompositionMode: + { + painter->setCompositionMode(m_compositionModes[instruction.dataIndex]); + break; + } + + default: + { + Q_ASSERT(false); + break; + } } } - return alpha; + painter->restore(); } -bool PDFPainter::canSetBlendMode(BlendMode mode) const +void PDFPrecompiledPage::addPath(QPen pen, QBrush brush, QPainterPath path, bool isText) { - // We will assume, that we can set blend mode, when - // all other blend modes on transparency stack are normal, - // or compatible. It should work. + m_instructions.emplace_back(InstructionType::DrawPath, m_paths.size()); + m_paths.emplace_back(qMove(pen), qMove(brush), qMove(path), isText); +} - Q_UNUSED(mode); - return std::all_of(m_transparencyGroupDataStack.cbegin(), m_transparencyGroupDataStack.cend(), [](const PDFTransparencyGroupPainterData& group) { return group.blendMode == BlendMode::Normal || group.blendMode == BlendMode::Compatible; }); +void PDFPrecompiledPage::addClip(QPainterPath path) +{ + m_instructions.emplace_back(InstructionType::Clip, m_clips.size()); + m_clips.emplace_back(qMove(path)); +} + +void PDFPrecompiledPage::addImage(QImage image) +{ + // Convert the image into format Format_ARGB32_Premultiplied for fast drawing. + // If this format is used, then no image conversion is performed while drawing. + if (image.format() != QImage::Format_ARGB32_Premultiplied) + { + image.convertTo(QImage::Format_ARGB32_Premultiplied); + } + + m_instructions.emplace_back(InstructionType::DrawImage, m_images.size()); + m_images.emplace_back(qMove(image)); +} + +void PDFPrecompiledPage::addMesh(PDFMesh mesh, PDFReal alpha) +{ + m_instructions.emplace_back(InstructionType::DrawMesh, m_meshes.size()); + m_meshes.emplace_back(qMove(mesh), alpha); +} + +void PDFPrecompiledPage::addSetWorldMatrix(const QMatrix& matrix) +{ + m_instructions.emplace_back(InstructionType::SetWorldMatrix, m_matrices.size()); + m_matrices.push_back(matrix); +} + +void PDFPrecompiledPage::addSetCompositionMode(QPainter::CompositionMode compositionMode) +{ + m_instructions.emplace_back(InstructionType::SetCompositionMode, m_compositionModes.size()); + m_compositionModes.push_back(compositionMode); +} + +void PDFPrecompiledPage::optimize() +{ + m_instructions.shrink_to_fit(); + m_paths.shrink_to_fit(); + m_clips.shrink_to_fit(); + m_images.shrink_to_fit(); + m_meshes.shrink_to_fit(); + m_matrices.shrink_to_fit(); + m_compositionModes.shrink_to_fit(); +} + +void PDFPrecompiledPage::finalize(qint64 compilingTimeNS, QList errors) +{ + m_compilingTimeNS = compilingTimeNS; + m_errors = qMove(errors); } } // namespace pdf diff --git a/PdfForQtLib/sources/pdfpainter.h b/PdfForQtLib/sources/pdfpainter.h index c38ec2d..5cd0437 100644 --- a/PdfForQtLib/sources/pdfpainter.h +++ b/PdfForQtLib/sources/pdfpainter.h @@ -19,6 +19,7 @@ #define PDFPAINTER_H #include "pdfutils.h" +#include "pdfpattern.h" #include "pdfrenderer.h" #include "pdfpagecontentprocessor.h" @@ -28,12 +29,76 @@ namespace pdf { +/// Base painter, encapsulating common functionality for all PDF painters (for example, +/// direct painter, or painter, which generates list of graphic commands). +class PDFPainterBase : public PDFPageContentProcessor +{ + using BaseClass = PDFPageContentProcessor; + +public: + explicit PDFPainterBase(PDFRenderer::Features features, + const PDFPage* page, + const PDFDocument* document, + const PDFFontCache* fontCache, + const PDFOptionalContentActivity* optionalContentActivity, + QMatrix pagePointToDevicePointMatrix, + const PDFMeshQualitySettings& meshQualitySettings); + +protected: + virtual void performUpdateGraphicsState(const PDFPageContentProcessorState& state) override; + virtual void performBeginTransparencyGroup(ProcessOrder order, const PDFTransparencyGroup& transparencyGroup); + virtual void performEndTransparencyGroup(ProcessOrder order, const PDFTransparencyGroup& transparencyGroup); + virtual bool isContentSuppressedByOC(PDFObjectReference ocgOrOcmd) override; + virtual void setWorldMatrix(const QMatrix& matrix) = 0; + virtual void setCompositionMode(QPainter::CompositionMode mode) = 0; + + /// Returns current pen + const QPen& getCurrentPen() { return m_currentPen.get(this, &PDFPainterBase::getCurrentPenImpl); } + + /// Returns current brush + const QBrush& getCurrentBrush() { return m_currentBrush.get(this, &PDFPainterBase::getCurrentBrushImpl); } + + /// Returns effective stroking alpha from transparency groups and current graphic state + PDFReal getEffectiveStrokingAlpha() const; + + /// Returns effective filling alpha from transparency groups and current graphic state + PDFReal getEffectiveFillingAlpha() const; + + /// Returns true, if blend mode can be set according the transparency group stack + bool canSetBlendMode(BlendMode mode) const; + + /// Returns, if feature is turned on + bool hasFeature(PDFRenderer::Feature feature) const { return m_features.testFlag(feature); } + +private: + /// Returns current pen (implementation) + QPen getCurrentPenImpl() const; + + /// Returns current brush (implementation) + QBrush getCurrentBrushImpl() const; + + struct PDFTransparencyGroupPainterData + { + PDFTransparencyGroup group; + PDFReal alphaStroke = 1.0; + PDFReal alphaFill = 1.0; + BlendMode blendMode = BlendMode::Normal; + }; + + PDFRenderer::Features m_features; + PDFCachedItem m_currentPen; + PDFCachedItem m_currentBrush; + std::vector m_transparencyGroupDataStack; +}; + /// Processor, which processes PDF's page commands on the QPainter. It works with QPainter /// and with transformation matrix, which translates page points to the device points. /// Only basic transparency is supported, advanced transparency, such as transparency groups, /// are not supported. Painter will try to emulate them so painting will not fail completely. -class PDFPainter : public PDFPageContentProcessor +class PDFPainter : public PDFPainterBase { + using BaseClass = PDFPainterBase; + public: /// Constructs new PDFPainter object, with default parameters. /// \param painter Painter, on which page content is drawn @@ -59,48 +124,182 @@ protected: virtual void performClipping(const QPainterPath& path, Qt::FillRule fillRule) override; virtual void performImagePainting(const QImage& image) override; virtual void performMeshPainting(const PDFMesh& mesh) override; - virtual void performUpdateGraphicsState(const PDFPageContentProcessorState& state) override; virtual void performSaveGraphicState(ProcessOrder order) override; virtual void performRestoreGraphicState(ProcessOrder order) override; - virtual void performBeginTransparencyGroup(ProcessOrder order, const PDFTransparencyGroup& transparencyGroup); - virtual void performEndTransparencyGroup(ProcessOrder order, const PDFTransparencyGroup& transparencyGroup); - virtual bool isContentSuppressedByOC(PDFObjectReference ocgOrOcmd) override; + virtual void setWorldMatrix(const QMatrix& matrix) override; + virtual void setCompositionMode(QPainter::CompositionMode mode) override; private: - /// Returns current pen - const QPen& getCurrentPen() { return m_currentPen.get(this, &PDFPainter::getCurrentPenImpl); } + QPainter* m_painter; +}; - /// Returns current brush - const QBrush& getCurrentBrush() { return m_currentBrush.get(this, &PDFPainter::getCurrentBrushImpl); } +/// Precompiled page contains precompiled graphic instructions of a PDF page to draw it quickly +/// on the target painter. It enables very fast drawing, because instructions are not decoded +/// and interpreted from the PDF stream, but they are just "played" on the painter. +class PDFPrecompiledPage +{ +public: - /// Returns current pen (implementation) - QPen getCurrentPenImpl() const; - - /// Returns current brush (implementation) - QBrush getCurrentBrushImpl() const; - - /// Returns effective stroking alpha from transparency groups and current graphic state - PDFReal getEffectiveStrokingAlpha() const; - - /// Returns effective filling alpha from transparency groups and current graphic state - PDFReal getEffectiveFillingAlpha() const; - - /// Returns true, if blend mode can be set according the transparency group stack - bool canSetBlendMode(BlendMode mode) const; - - struct PDFTransparencyGroupPainterData + enum class InstructionType { - PDFTransparencyGroup group; - PDFReal alphaStroke = 1.0; - PDFReal alphaFill = 1.0; - BlendMode blendMode = BlendMode::Normal; + Invalid, + DrawPath, + DrawImage, + DrawMesh, + Clip, + SaveGraphicState, + RestoreGraphicState, + SetWorldMatrix, + SetCompositionMode }; - QPainter* m_painter; - PDFRenderer::Features m_features; - PDFCachedItem m_currentPen; - PDFCachedItem m_currentBrush; - std::vector m_transparencyGroupDataStack; + struct Instruction + { + inline Instruction() = default; + inline Instruction(InstructionType type, size_t dataIndex) : + type(type), + dataIndex(dataIndex) + { + + } + + InstructionType type = InstructionType::Invalid; + size_t dataIndex = 0; + }; + + /// Paints page onto the painter using matrix + /// \param painter Painter, onto which is page drawn + /// \param cropBox Page's crop box + /// \param pagePointToDevicePointMatrix Page point to device point transformation matrix + /// \param features Renderer features + void draw(QPainter* painter, const QRectF& cropBox, const QMatrix& pagePointToDevicePointMatrix, PDFRenderer::Features features) const; + + void addPath(QPen pen, QBrush brush, QPainterPath path, bool isText); + void addClip(QPainterPath path); + void addImage(QImage image); + void addMesh(PDFMesh mesh, PDFReal alpha); + void addSaveGraphicState() { m_instructions.emplace_back(InstructionType::SaveGraphicState, 0); } + void addRestoreGraphicState() { m_instructions.emplace_back(InstructionType::RestoreGraphicState, 0); } + void addSetWorldMatrix(const QMatrix& matrix); + void addSetCompositionMode(QPainter::CompositionMode compositionMode); + + /// Optimizes page memory allocation to contain less space + void optimize(); + + /// Finalizes precompiled page + /// \param compilingTimeNS Compiling time in nanoseconds + /// \param errors List of rendering errors + void finalize(qint64 compilingTimeNS, QList errors); + + /// Returns compiling time in nanoseconds + qint64 getCompilingTimeNS() const { return m_compilingTimeNS; } + + /// Returns a list of rendering errors + const QList& getErrors() const { return m_errors; } + + /// Returns true, if page is valid (i.e. has nonzero instruction count) + bool isValid() const { return !m_instructions.empty(); } + +private: + struct PathPaintData + { + inline PathPaintData() = default; + inline PathPaintData(QPen pen, QBrush brush, QPainterPath path, bool isText) : + pen(qMove(pen)), + brush(qMove(brush)), + path(qMove(path)), + isText(isText) + { + + } + + QPen pen; + QBrush brush; + QPainterPath path; + bool isText = false; + }; + + struct ClipData + { + inline ClipData() = default; + inline ClipData(QPainterPath path) : + clipPath(qMove(path)) + { + + } + + QPainterPath clipPath; + }; + + struct ImageData + { + inline ImageData() = default; + inline ImageData(QImage image) : + image(qMove(image)) + { + + } + + QImage image; + }; + + struct MeshPaintData + { + inline MeshPaintData() = default; + inline MeshPaintData(PDFMesh mesh, PDFReal alpha) : + mesh(qMove(mesh)), + alpha(alpha) + { + + } + + PDFMesh mesh; + PDFReal alpha = 1.0; + }; + + qint64 m_compilingTimeNS = 0; + std::vector m_instructions; + std::vector m_paths; + std::vector m_clips; + std::vector m_images; + std::vector m_meshes; + std::vector m_matrices; + std::vector m_compositionModes; + QList m_errors; +}; + +/// Processor, which processes PDF's page commands and writes them to the precompiled page. +/// Precompiled page then can be used to execute these commands on QPainter. +class PDFPrecompiledPageGenerator : public PDFPainterBase +{ + using BaseClass = PDFPainterBase; + +public: + explicit inline PDFPrecompiledPageGenerator(PDFPrecompiledPage* precompiledPage, + PDFRenderer::Features features, + const PDFPage* page, + const PDFDocument* document, + const PDFFontCache* fontCache, + const PDFOptionalContentActivity* optionalContentActivity, + const PDFMeshQualitySettings& meshQualitySettings) : + BaseClass(features, page, document, fontCache, optionalContentActivity, QMatrix(), meshQualitySettings), + m_precompiledPage(precompiledPage) + { + + } + +protected: + virtual void performPathPainting(const QPainterPath& path, bool stroke, bool fill, bool text, Qt::FillRule fillRule) override; + virtual void performClipping(const QPainterPath& path, Qt::FillRule fillRule) override; + virtual void performImagePainting(const QImage& image) override; + virtual void performMeshPainting(const PDFMesh& mesh) override; + virtual void performSaveGraphicState(ProcessOrder order) override; + virtual void performRestoreGraphicState(ProcessOrder order) override; + virtual void setWorldMatrix(const QMatrix& matrix) override; + virtual void setCompositionMode(QPainter::CompositionMode mode) override; + +private: + PDFPrecompiledPage* m_precompiledPage; }; } // namespace pdf diff --git a/PdfForQtLib/sources/pdfrenderer.cpp b/PdfForQtLib/sources/pdfrenderer.cpp index dafd4a1..fb2f264 100644 --- a/PdfForQtLib/sources/pdfrenderer.cpp +++ b/PdfForQtLib/sources/pdfrenderer.cpp @@ -19,6 +19,8 @@ #include "pdfpainter.h" #include "pdfdocument.h" +#include + namespace pdf { @@ -36,21 +38,8 @@ PDFRenderer::PDFRenderer(const PDFDocument* document, Q_ASSERT(document); } -QList PDFRenderer::render(QPainter* painter, const QRectF& rectangle, size_t pageIndex) const +QMatrix PDFRenderer::createPagePointToDevicePointMatrix(const PDFPage* page, const QRectF& rectangle) const { - Q_UNUSED(painter); - Q_UNUSED(rectangle); - - 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); - QRectF mediaBox = page->getRotatedMediaBox(); QMatrix matrix; @@ -94,14 +83,29 @@ QList PDFRenderer::render(QPainter* painter, const QRectF& recta } } + return matrix; +} + +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); + + QMatrix matrix = createPagePointToDevicePointMatrix(page, rectangle); + PDFPainter processor(painter, m_features, matrix, page, m_document, m_fontCache, m_optionalContentActivity, m_meshQualitySettings); return processor.processContents(); } QList PDFRenderer::render(QPainter* painter, const QMatrix& matrix, size_t pageIndex) const { - Q_UNUSED(painter); - const PDFCatalog* catalog = m_document->getCatalog(); if (pageIndex >= catalog->getPageCount() || !catalog->getPage(pageIndex)) { @@ -116,4 +120,27 @@ QList PDFRenderer::render(QPainter* painter, const QMatrix& matr 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_optionalContentActivity, m_meshQualitySettings); + QList errors = generator.processContents(); + precompiledPage->optimize(); + precompiledPage->finalize(timer.nsecsElapsed(), qMove(errors)); + timer.invalidate(); +} + } // namespace pdf diff --git a/PdfForQtLib/sources/pdfrenderer.h b/PdfForQtLib/sources/pdfrenderer.h index 6fbfc3b..5794354 100644 --- a/PdfForQtLib/sources/pdfrenderer.h +++ b/PdfForQtLib/sources/pdfrenderer.h @@ -27,6 +27,7 @@ class QPainter; namespace pdf { class PDFFontCache; +class PDFPrecompiledPage; class PDFOptionalContentActivity; /// Renders the PDF page on the painter, or onto an image. @@ -60,6 +61,19 @@ public: /// Rendering errors are reported and returned in the error list. If no error occured, empty list is returned. QList render(QPainter* painter, const QMatrix& matrix, size_t pageIndex) const; + /// Compiles page (i.e. prepares compiled page). \p page should be empty page, onto which + /// are graphics commands written. No exception is thrown. Rendering errors are reported and written + /// to the compiled page. + /// \param precompiledPage Precompiled page pointer + /// \param pageIndex Index of page to be compiled + void compile(PDFPrecompiledPage* precompiledPage, size_t pageIndex) const; + + /// Creates page point to device point matrix for the given rectangle. It creates transformation + /// from page's media box to the target rectangle. + /// \param page Page, for which we want to create matrix + /// \param rectangle Page rectangle, to which is page media box transformed + QMatrix createPagePointToDevicePointMatrix(const PDFPage* page, const QRectF& rectangle) const; + /// Returns default renderer features static constexpr Features getDefaultFeatures() { return Antialiasing | TextAntialiasing | ClipToCropBox; } @@ -71,7 +85,6 @@ private: PDFMeshQualitySettings m_meshQualitySettings; }; - } // namespace pdf Q_DECLARE_OPERATORS_FOR_FLAGS(pdf::PDFRenderer::Features)