diff --git a/PdfForQtLib/sources/pdfdrawspacecontroller.cpp b/PdfForQtLib/sources/pdfdrawspacecontroller.cpp index a5c9152..4709007 100644 --- a/PdfForQtLib/sources/pdfdrawspacecontroller.cpp +++ b/PdfForQtLib/sources/pdfdrawspacecontroller.cpp @@ -28,6 +28,7 @@ namespace pdf PDFDrawSpaceController::PDFDrawSpaceController(QObject* parent) : QObject(parent), m_document(nullptr), + m_optionalContentActivity(nullptr), m_pageLayoutMode(PageLayout::OneColumn), m_verticalSpacingMM(5.0), m_horizontalSpacingMM(1.0), @@ -41,12 +42,14 @@ PDFDrawSpaceController::~PDFDrawSpaceController() } -void PDFDrawSpaceController::setDocument(const PDFDocument* document) +void PDFDrawSpaceController::setDocument(const PDFDocument* document, const PDFOptionalContentActivity* optionalContentActivity) { if (document != m_document) { m_document = document; m_fontCache.setDocument(document); + m_optionalContentActivity = optionalContentActivity; + connect(m_optionalContentActivity, &PDFOptionalContentActivity::optionalContentGroupStateChanged, this, &PDFDrawSpaceController::repaintNeeded); recalculate(); } } @@ -348,6 +351,7 @@ PDFDrawWidgetProxy::PDFDrawWidgetProxy(QObject* parent) : { m_controller = new PDFDrawSpaceController(this); connect(m_controller, &PDFDrawSpaceController::drawSpaceChanged, this, &PDFDrawWidgetProxy::update); + connect(m_controller, &PDFDrawSpaceController::repaintNeeded, this, &PDFDrawWidgetProxy::repaintNeeded); } PDFDrawWidgetProxy::~PDFDrawWidgetProxy() @@ -355,9 +359,9 @@ PDFDrawWidgetProxy::~PDFDrawWidgetProxy() } -void PDFDrawWidgetProxy::setDocument(const PDFDocument* document) +void PDFDrawWidgetProxy::setDocument(const PDFDocument* document, const PDFOptionalContentActivity* optionalContentActivity) { - m_controller->setDocument(document); + m_controller->setDocument(document, optionalContentActivity); } void PDFDrawWidgetProxy::init(PDFWidget* widget) @@ -545,7 +549,7 @@ 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()); + PDFRenderer renderer(m_controller->getDocument(), m_controller->getFontCache(), m_controller->getOptionalContentActivity()); QList errors = renderer.render(painter, placedRect, item.pageIndex); if (!errors.empty()) diff --git a/PdfForQtLib/sources/pdfdrawspacecontroller.h b/PdfForQtLib/sources/pdfdrawspacecontroller.h index 32b4d1d..5f96be2 100644 --- a/PdfForQtLib/sources/pdfdrawspacecontroller.h +++ b/PdfForQtLib/sources/pdfdrawspacecontroller.h @@ -47,9 +47,11 @@ public: virtual ~PDFDrawSpaceController() override; /// Sets the document and recalculates the draw space. Document can be nullptr, - /// in that case, draw space is cleared. + /// in that case, draw space is cleared. Optional content activity can be nullptr, + /// in that case, no content is suppressed. /// \param document Document - void setDocument(const PDFDocument* document); + /// \param optionalContentActivity Optional content activity + void setDocument(const PDFDocument* document, const PDFOptionalContentActivity* optionalContentActivity); /// Sets the page layout. Page layout can be one of the PDF's page layouts. /// \param pageLayout Page layout @@ -93,8 +95,12 @@ public: /// Returns the font cache const PDFFontCache* getFontCache() const { return &m_fontCache; } + /// Returns optional content activity + const PDFOptionalContentActivity* getOptionalContentActivity() const { return m_optionalContentActivity; } + signals: void drawSpaceChanged(); + void repaintNeeded(); private: /// Recalculates the draw space. Preserves setted page rotation. @@ -118,6 +124,7 @@ private: static constexpr size_t REALIZED_FONT_CACHE_LIMIT = 128; const PDFDocument* m_document; + const PDFOptionalContentActivity* m_optionalContentActivity; PageLayout m_pageLayoutMode; LayoutItems m_layoutItems; @@ -140,9 +147,11 @@ public: virtual ~PDFDrawWidgetProxy() override; /// Sets the document and updates the draw space. Document can be nullptr, - /// in that case, draw space is cleared. + /// in that case, draw space is cleared. Optional content activity can be nullptr, + /// in that case, no content is suppressed. /// \param document Document - void setDocument(const PDFDocument* document); + /// \param optionalContentActivity Optional content activity + void setDocument(const PDFDocument* document, const PDFOptionalContentActivity* optionalContentActivity); void init(PDFWidget* widget); @@ -208,6 +217,7 @@ signals: void drawSpaceChanged(); void pageLayoutChanged(); void renderingError(PDFInteger pageIndex, const QList& errors); + void repaintNeeded(); private: struct LayoutItem diff --git a/PdfForQtLib/sources/pdfdrawwidget.cpp b/PdfForQtLib/sources/pdfdrawwidget.cpp index 5abd74c..961037e 100644 --- a/PdfForQtLib/sources/pdfdrawwidget.cpp +++ b/PdfForQtLib/sources/pdfdrawwidget.cpp @@ -50,6 +50,7 @@ PDFWidget::PDFWidget(QWidget* parent) : m_proxy = new PDFDrawWidgetProxy(this); m_proxy->init(this); connect(m_proxy, &PDFDrawWidgetProxy::renderingError, this, &PDFWidget::onRenderingError); + connect(m_proxy, &PDFDrawWidgetProxy::repaintNeeded, m_drawWidget, QOverload<>::of(&PDFDrawWidget::update)); } PDFWidget::~PDFWidget() @@ -57,9 +58,9 @@ PDFWidget::~PDFWidget() } -void PDFWidget::setDocument(const PDFDocument* document) +void PDFWidget::setDocument(const PDFDocument* document, const PDFOptionalContentActivity* optionalContentActivity) { - m_proxy->setDocument(document); + m_proxy->setDocument(document, optionalContentActivity); m_pageRenderingErrors.clear(); } diff --git a/PdfForQtLib/sources/pdfdrawwidget.h b/PdfForQtLib/sources/pdfdrawwidget.h index b6b2b1c..d5b9063 100644 --- a/PdfForQtLib/sources/pdfdrawwidget.h +++ b/PdfForQtLib/sources/pdfdrawwidget.h @@ -42,9 +42,11 @@ public: using PageRenderingErrors = std::map>; /// Sets the document to be viewed in this widget. Document can be nullptr, - /// in that case, widget contents are cleared. + /// in that case, widget contents are cleared. Optional content activity can be nullptr, + /// if this occurs, no content is suppressed. /// \param document Document - void setDocument(const PDFDocument* document); + /// \param optionalContentActivity Optional content activity + void setDocument(const PDFDocument* document, const PDFOptionalContentActivity* optionalContentActivity); PDFDrawWidget* getDrawWidget() const { return m_drawWidget; } QScrollBar* getHorizontalScrollbar() const { return m_horizontalScrollBar; } diff --git a/PdfForQtLib/sources/pdfoptionalcontent.cpp b/PdfForQtLib/sources/pdfoptionalcontent.cpp index 4ab8dd9..8009b6c 100644 --- a/PdfForQtLib/sources/pdfoptionalcontent.cpp +++ b/PdfForQtLib/sources/pdfoptionalcontent.cpp @@ -425,4 +425,218 @@ void PDFOptionalContentActivity::applyConfiguration(const PDFOptionalContentConf } } +PDFOptionalContentMembershipObject PDFOptionalContentMembershipObject::create(const PDFDocument* document, const PDFObject& object) +{ + PDFOptionalContentMembershipObject result; + const PDFObject& dereferencedObject = document->getObject(object); + if (dereferencedObject.isDictionary()) + { + const PDFDictionary* dictionary = dereferencedObject.getDictionary(); + if (dictionary->hasKey("VE")) + { + // Parse visibility expression + + std::set usedReferences; + std::function(const PDFObject&)> parseNode = [document, &parseNode, &usedReferences](const PDFObject& nodeObject) -> std::unique_ptr + { + const PDFObject& dereferencedNodeObject = document->getObject(nodeObject); + if (dereferencedNodeObject.isArray()) + { + // It is probably array. We must check, if we doesn't have cyclic reference. + if (nodeObject.isReference()) + { + if (usedReferences.count(nodeObject.getReference())) + { + throw PDFParserException(PDFTranslationContext::tr("Cyclic reference error in optional content visibility expression.")); + } + else + { + usedReferences.insert(nodeObject.getReference()); + } + } + + // Check the array + const PDFArray* array = dereferencedNodeObject.getArray(); + if (array->getCount() < 2) + { + throw PDFParserException(PDFTranslationContext::tr("Invalid optional content visibility expression.")); + } + + // Read the operator + const PDFObject& dereferencedNameObject = document->getObject(array->getItem(0)); + QByteArray operatorName; + if (dereferencedNameObject.isName()) + { + operatorName = dereferencedNameObject.getString(); + } + + Operator operatorType = Operator::And; + if (operatorName == "And") + { + operatorType = Operator::And; + } else if (operatorName == "Or") + { + operatorType = Operator::Or; + } + else if (operatorName == "Not") + { + operatorType = Operator::Not; + } + else + { + throw PDFParserException(PDFTranslationContext::tr("Invalid optional content visibility expression.")); + } + + // Read the operands + std::vector> operands; + operands.reserve(array->getCount()); + for (size_t i = 1, count = array->getCount(); i < count; ++i) + { + operands.push_back(parseNode(array->getItem(i))); + } + + return std::unique_ptr(new OperatorNode(operatorType, qMove(operands))); + } + else if (nodeObject.isReference()) + { + // Treat is as an optional content group + return std::unique_ptr(new OptionalContentGroupNode(nodeObject.getReference())); + } + else + { + // Something strange occured - either we should have an array, or we should have a reference to the OCG + throw PDFParserException(PDFTranslationContext::tr("Invalid optional content visibility expression.")); + return std::unique_ptr(nullptr); + } + }; + + result.m_expression = parseNode(dictionary->get("VE")); + } + else + { + // First, scan all optional content groups + PDFDocumentDataLoaderDecorator loader(document); + std::vector ocgs = loader.readReferenceArrayFromDictionary(dictionary, "OCGs"); + + if (!ocgs.empty()) + { + auto createOperatorOnOcgs = [&ocgs](Operator operator_) + { + std::vector> operands; + operands.reserve(ocgs.size()); + for (PDFObjectReference reference : ocgs) + { + operands.push_back(std::unique_ptr(new OptionalContentGroupNode(reference))); + } + return std::unique_ptr(new OperatorNode(operator_, qMove(operands))); + }; + + // Parse 'P' mode + QByteArray type = loader.readNameFromDictionary(dictionary, "P"); + if (type == "AllOn") + { + // All of entries in OCGS are turned on + result.m_expression = createOperatorOnOcgs(Operator::And); + } + else if (type == "AnyOn") + { + // Any of entries in OCGS is turned on + result.m_expression = createOperatorOnOcgs(Operator::Or); + } + else if (type == "AnyOff") + { + // Any of entries are turned off. It is negation of 'AllOn'. + std::vector> subexpression; + subexpression.push_back(createOperatorOnOcgs(Operator::And)); + result.m_expression = std::unique_ptr(new OperatorNode(Operator::Not, qMove(subexpression))); + } + else if (type == "AllOff") + { + // All of entries are turned off. It is negation of 'AnyOn' + std::vector> subexpression; + subexpression.push_back(createOperatorOnOcgs(Operator::Or)); + result.m_expression = std::unique_ptr(new OperatorNode(Operator::Not, qMove(subexpression))); + } + else + { + // Default value is AnyOn according to the PDF reference + result.m_expression = createOperatorOnOcgs(Operator::Or); + } + } + } + } + + return result; +} + +OCState PDFOptionalContentMembershipObject::evaluate(const PDFOptionalContentActivity* activity) const +{ + return m_expression ? m_expression->evaluate(activity) : OCState::Unknown; +} + +OCState PDFOptionalContentMembershipObject::OptionalContentGroupNode::evaluate(const PDFOptionalContentActivity* activity) const +{ + return activity->getState(m_optionalContentGroup); +} + +OCState PDFOptionalContentMembershipObject::OperatorNode::evaluate(const PDFOptionalContentActivity* activity) const +{ + OCState result = OCState::Unknown; + + switch (m_operator) + { + case Operator::And: + { + for (const auto& child : m_children) + { + result = result & child->evaluate(activity); + } + break; + } + + case Operator::Or: + { + for (const auto& child : m_children) + { + result = result | child->evaluate(activity); + } + break; + } + + case Operator::Not: + { + // We must handle case, when we have zero or more expressions (Not operator requires exactly one). + // If this case occurs, then we return Unknown state. + if (m_children.size() == 1) + { + OCState childState = m_children.front()->evaluate(activity); + + switch (childState) + { + case OCState::ON: + result = OCState::OFF; + break; + + case OCState::OFF: + result = OCState::ON; + break; + + default: + Q_ASSERT(result == OCState::Unknown); + break; + } + } + break; + } + + default: + { + Q_ASSERT(false); + break; + } + } + + return result; +} + } // namespace pdf diff --git a/PdfForQtLib/sources/pdfoptionalcontent.h b/PdfForQtLib/sources/pdfoptionalcontent.h index 503e6ae..4cd1f2c 100644 --- a/PdfForQtLib/sources/pdfoptionalcontent.h +++ b/PdfForQtLib/sources/pdfoptionalcontent.h @@ -25,6 +25,7 @@ namespace pdf { class PDFDocument; +class PDFOptionalContentActivity; class PDFOptionalContentProperties; class PDFOptionalContentConfiguration; @@ -73,6 +74,86 @@ constexpr OCState operator |(OCState left, OCState right) return (left == OCState::ON || right == OCState::ON) ? OCState::ON : OCState::OFF; } +/// Object describing optional content membership dictionary +class PDFOptionalContentMembershipObject +{ +public: + explicit PDFOptionalContentMembershipObject() = default; + + constexpr inline PDFOptionalContentMembershipObject(const PDFOptionalContentMembershipObject&) = delete; + constexpr inline PDFOptionalContentMembershipObject(PDFOptionalContentMembershipObject&&) = default; + constexpr inline PDFOptionalContentMembershipObject& operator=(const PDFOptionalContentMembershipObject&) = delete; + constexpr inline PDFOptionalContentMembershipObject& operator=(PDFOptionalContentMembershipObject&&) = default; + + /// Creates optional content membership dictionary. If creation fails, then + /// exception is thrown. + /// \param document Document owning the membership dictionary + /// \param object Object to be parsed + static PDFOptionalContentMembershipObject create(const PDFDocument* document, const PDFObject& object); + + /// Returns true, if this object is valid + bool isValid() const { return static_cast(m_expression); } + + /// Evaluate objects. If error occurs, then Uknown state is returned. + /// \param activity Activity + OCState evaluate(const PDFOptionalContentActivity* activity) const; + +private: + + enum class Operator + { + And, + Or, + Not + }; + + /// Node in the expression tree + class Node + { + public: + inline explicit Node() = default; + virtual ~Node() = default; + + virtual OCState evaluate(const PDFOptionalContentActivity* activity) const = 0; + }; + + /// Node reprsenting optional content group + class OptionalContentGroupNode : public Node + { + public: + inline explicit OptionalContentGroupNode(PDFObjectReference optionalContentGroup) : + m_optionalContentGroup(optionalContentGroup) + { + + } + + virtual OCState evaluate(const PDFOptionalContentActivity* activity) const override; + + private: + PDFObjectReference m_optionalContentGroup; + }; + + /// Node representing operator + class OperatorNode : public Node + { + public: + inline explicit OperatorNode(Operator operator_, std::vector>&& nodes) : + m_operator(operator_), + m_children(qMove(nodes)) + { + + } + + virtual OCState evaluate(const PDFOptionalContentActivity* activity) const override; + + private: + Operator m_operator; + std::vector> m_children; + }; + + std::unique_ptr m_expression; +}; + /// Activeness of the optional content class PDFFORQTLIBSHARED_EXPORT PDFOptionalContentActivity : public QObject { @@ -93,12 +174,15 @@ public: /// optional content groups in radio button group can be switched off). /// \param ocg Optional content group /// \param state New state of the optional content group - /// \note If something changed, then signalp \p optionalContentGroupStateChanged is emitted. + /// \note If something changed, then signal \p optionalContentGroupStateChanged is emitted. void setState(PDFObjectReference ocg, OCState state); /// Applies configuration to the current state of optional content groups void applyConfiguration(const PDFOptionalContentConfiguration& configuration); + /// Returns the properties of optional content + const PDFOptionalContentProperties* getProperties() const { return m_properties; } + signals: void optionalContentGroupStateChanged(PDFObjectReference ocg, OCState state); diff --git a/PdfForQtLib/sources/pdfpagecontentprocessor.cpp b/PdfForQtLib/sources/pdfpagecontentprocessor.cpp index 07601c3..c95f185 100644 --- a/PdfForQtLib/sources/pdfpagecontentprocessor.cpp +++ b/PdfForQtLib/sources/pdfpagecontentprocessor.cpp @@ -172,16 +172,22 @@ void PDFPageContentProcessor::initDictionaries(const PDFObject& resourcesObject) m_fontDictionary = getDictionary("Font"); m_xobjectDictionary = getDictionary("XObject"); m_extendedGraphicStateDictionary = getDictionary(PDF_RESOURCE_EXTGSTATE); + m_propertiesDictionary = getDictionary("Properties"); } -PDFPageContentProcessor::PDFPageContentProcessor(const PDFPage* page, const PDFDocument* document, const PDFFontCache* fontCache) : +PDFPageContentProcessor::PDFPageContentProcessor(const PDFPage* page, + const PDFDocument* document, + const PDFFontCache* fontCache, + const PDFOptionalContentActivity* optionalContentActivity) : m_page(page), m_document(document), m_fontCache(fontCache), + m_optionalContentActivity(optionalContentActivity), m_colorSpaceDictionary(nullptr), m_fontDictionary(nullptr), m_xobjectDictionary(nullptr), m_extendedGraphicStateDictionary(nullptr), + m_propertiesDictionary(nullptr), m_textBeginEndState(0), m_compatibilityBeginEndState(0) { @@ -312,6 +318,28 @@ void PDFPageContentProcessor::performRestoreGraphicState(ProcessOrder order) Q_UNUSED(order); } +void PDFPageContentProcessor::performMarkedContentPoint(const QByteArray& tag, const PDFObject& properties) +{ + Q_UNUSED(tag); + Q_UNUSED(properties); +} + +void PDFPageContentProcessor::performMarkedContentBegin(const QByteArray& tag, const PDFObject& properties) +{ + Q_UNUSED(tag); + Q_UNUSED(properties); +} + +void PDFPageContentProcessor::performMarkedContentEnd() +{ + +} + +bool PDFPageContentProcessor::isContentSuppressed() const +{ + return std::any_of(m_markedContentStack.cbegin(), m_markedContentStack.cend(), [](const MarkedContentState& state) { return state.contentSuppressed; }); +} + void PDFPageContentProcessor::processContent(const QByteArray& content) { PDFLexicalAnalyzer parser(content.constBegin(), content.constEnd()); @@ -791,6 +819,41 @@ void PDFPageContentProcessor::processCommand(const QByteArray& command) break; } + case Operator::MarkedContentPoint: + { + // MP, marked content point + invokeOperator(&PDFPageContentProcessor::operatorMarkedContentPoint); + break; + } + + case Operator::MarkedContentPointWithProperties: + { + // DP, marked content point with properties + operatorMarkedContentPointWithProperties(readOperand(0), readObjectFromOperandStack(1)); + break; + } + + case Operator::MarkedContentBegin: + { + // BMC, begin of sequence of marked content + invokeOperator(&PDFPageContentProcessor::operatorMarkedContentBegin); + break; + } + + case Operator::MarkedContentBeginWithProperties: + { + // BDC, begin of sequence of marked content with properties + operatorMarkedContentBeginWithProperties(readOperand(0), readObjectFromOperandStack(1)); + break; + } + + case Operator::MarkedContentEnd: + { + // EMC, end of marked content sequence + operatorMarkedContentEnd(); + break; + } + case Operator::CompatibilityBegin: { operatorCompatibilityBegin(); @@ -1889,6 +1952,59 @@ void PDFPageContentProcessor::operatorPaintXObject(PDFPageContentProcessor::PDFO } } +void PDFPageContentProcessor::operatorMarkedContentPoint(PDFOperandName name) +{ + performMarkedContentPoint(name.name, PDFObject()); +} + +void PDFPageContentProcessor::operatorMarkedContentPointWithProperties(PDFOperandName name, PDFObject properties) +{ + performMarkedContentPoint(name.name, properties); +} + +void PDFPageContentProcessor::operatorMarkedContentBegin(PDFOperandName name) +{ + operatorMarkedContentBeginWithProperties(qMove(name), PDFObject()); +} + +void PDFPageContentProcessor::operatorMarkedContentBeginWithProperties(PDFOperandName name, PDFObject properties) +{ + // TODO: Handle optional content of images/forms + + // Handle the optional content + if (name.name == "OC") + { + PDFObjectReference ocg; + if (m_propertiesDictionary && properties.isName()) + { + const PDFObject& ocgObject = m_propertiesDictionary->get(properties.getString()); + if (ocgObject.isReference()) + { + ocg = ocgObject.getReference(); + } + } + + m_markedContentStack.emplace_back(name.name, MarkedContentKind::OptionalContent, isContentSuppressedByOC(ocg)); + } + else + { + m_markedContentStack.emplace_back(name.name, MarkedContentKind::Other, false); + } + + performMarkedContentBegin(name.name, properties); +} + +void PDFPageContentProcessor::operatorMarkedContentEnd() +{ + if (m_markedContentStack.empty()) + { + throw PDFRendererException(RenderErrorType::Error, PDFTranslationContext::tr("Mismatched begin/end of marked content.")); + } + + m_markedContentStack.pop_back(); + performMarkedContentEnd(); +} + void PDFPageContentProcessor::operatorCompatibilityBegin() { ++m_compatibilityBeginEndState; @@ -2024,6 +2140,53 @@ void PDFPageContentProcessor::checkFillingColor() } } +PDFObject PDFPageContentProcessor::readObjectFromOperandStack(size_t startPosition) const +{ + auto tokenFetcher = [this, &startPosition]() + { + if (startPosition < m_operands.size()) + { + return m_operands[startPosition++]; + } + return PDFLexicalAnalyzer::Token(); + }; + + PDFParser parser(tokenFetcher); + return parser.getObject(); +} + +bool PDFPageContentProcessor::isContentSuppressedByOC(PDFObjectReference ocgOrOcmd) +{ + if (!m_optionalContentActivity) + { + // Optional content activity control is suppressed, treat all content as not suppressed + return false; + } + + if (m_optionalContentActivity->getProperties()->hasOptionalContentGroup(ocgOrOcmd)) + { + // Simplest case - we have single optional content group + return m_optionalContentActivity->getState(ocgOrOcmd) == OCState::OFF; + } + + PDFOptionalContentMembershipObject ocmd; + try + { + ocmd = PDFOptionalContentMembershipObject::create(m_document, PDFObject::createReference(ocgOrOcmd)); + } + catch (PDFParserException e) + { + m_errorList.push_back(PDFRenderError(RenderErrorType::Error, e.getMessage())); + } + + if (ocmd.isValid()) + { + return ocmd.evaluate(m_optionalContentActivity) == OCState::OFF; + } + + return false; +} + PDFPageContentProcessor::PDFPageContentProcessorState::PDFPageContentProcessorState() : m_currentTransformationMatrix(), m_fillColorSpace(), @@ -2306,7 +2469,8 @@ PDFPageContentProcessor::PDFPageContentProcessorStateGuard::PDFPageContentProces m_colorSpaceDictionary(processor->m_colorSpaceDictionary), m_fontDictionary(processor->m_fontDictionary), m_xobjectDictionary(processor->m_xobjectDictionary), - m_extendedGraphicStateDictionary(processor->m_extendedGraphicStateDictionary) + m_extendedGraphicStateDictionary(processor->m_extendedGraphicStateDictionary), + m_propertiesDictionary(processor->m_propertiesDictionary) { m_processor->operatorSaveGraphicState(); } @@ -2318,6 +2482,7 @@ PDFPageContentProcessor::PDFPageContentProcessorStateGuard::~PDFPageContentProce m_processor->m_fontDictionary = m_fontDictionary; m_processor->m_xobjectDictionary = m_xobjectDictionary; m_processor->m_extendedGraphicStateDictionary = m_extendedGraphicStateDictionary; + m_processor->m_propertiesDictionary = m_propertiesDictionary; m_processor->operatorRestoreGraphicState(); } diff --git a/PdfForQtLib/sources/pdfpagecontentprocessor.h b/PdfForQtLib/sources/pdfpagecontentprocessor.h index 9fcd1f2..5062ecb 100644 --- a/PdfForQtLib/sources/pdfpagecontentprocessor.h +++ b/PdfForQtLib/sources/pdfpagecontentprocessor.h @@ -34,15 +34,18 @@ namespace pdf { -static constexpr const char* PDF_RESOURCE_EXTGSTATE = "ExtGState"; +class PDFOptionalContentActivity; -// TODO: Implement optional content groups +static constexpr const char* PDF_RESOURCE_EXTGSTATE = "ExtGState"; /// Process the contents of the page. class PDFPageContentProcessor : public PDFRenderErrorReporter { public: - explicit PDFPageContentProcessor(const PDFPage* page, const PDFDocument* document, const PDFFontCache* fontCache); + explicit PDFPageContentProcessor(const PDFPage* page, + const PDFDocument* document, + const PDFFontCache* fontCache, + const PDFOptionalContentActivity* optionalContentActivity); virtual ~PDFPageContentProcessor(); enum class Operator @@ -379,6 +382,20 @@ protected: /// \param order If this function is called before the operation, or after the operation. virtual void performRestoreGraphicState(ProcessOrder order); + /// Implement to react on marked content point. Properties object can be null, in case + /// that no properties are provided. + /// \param tag Tag of the marked content point + /// \param properties Properties of the marked content point. + virtual void performMarkedContentPoint(const QByteArray& tag, const PDFObject& properties); + + /// Implement to react on marked content begin. + /// \param tag Tag of the marked content point + /// \param properties Properties of the marked content point. + virtual void performMarkedContentBegin(const QByteArray& tag, const PDFObject& properties); + + /// Implement to react on marked content end + virtual void performMarkedContentEnd(); + /// Returns current graphic state const PDFPageContentProcessorState* getGraphicState() const { return &m_graphicState; } @@ -386,6 +403,9 @@ protected: /// \param error Error message void addError(const QString& error) { m_errorList.append(PDFRenderError(RenderErrorType::Error, error)); } + /// Returns true, if graphic content is suppressed + bool isContentSuppressed() const; + private: /// Initializes the resources dictionaries void initDictionaries(const PDFObject& resourcesObject); @@ -406,6 +426,28 @@ private: /// \param content Content stream of the form void processForm(const QMatrix& matrix, const QRectF& boundingBox, const PDFObject& resources, const QByteArray& content); + enum class MarkedContentKind + { + OptionalContent, + Other + }; + + struct MarkedContentState + { + inline explicit MarkedContentState() = default; + inline explicit MarkedContentState(const QByteArray& tag, MarkedContentKind kind, bool contentSuppressed) : + tag(tag), + kind(kind), + contentSuppressed(contentSuppressed) + { + + } + + QByteArray tag; + MarkedContentKind kind = MarkedContentKind::Other; + bool contentSuppressed = false; + }; + struct PDFPageContentProcessorStateGuard { public: @@ -420,6 +462,7 @@ private: const PDFDictionary* m_fontDictionary; const PDFDictionary* m_xobjectDictionary; const PDFDictionary* m_extendedGraphicStateDictionary; + const PDFDictionary* m_propertiesDictionary; }; /// Wrapper for PDF Name @@ -593,6 +636,13 @@ private: // XObject: Do void operatorPaintXObject(PDFOperandName name); ///< Do, paint the X Object (image, form, ...) + // Marked content: MP, DP, BMC, BDC, EMC + void operatorMarkedContentPoint(PDFOperandName name); ///< MP, marked content point + void operatorMarkedContentPointWithProperties(PDFOperandName name, PDFObject properties); ///< DP, marked content point with properties + void operatorMarkedContentBegin(PDFOperandName name); ///< BMC, begin of sequence of marked content + void operatorMarkedContentBeginWithProperties(PDFOperandName name, PDFObject properties); ///< BDC, begin of sequence of marked content with properties + void operatorMarkedContentEnd(); ///< EMC, end of marked content sequence + // Compatibility: BX, EX void operatorCompatibilityBegin(); ///< BX, Compatibility mode begin (unrecognized operators are ignored) void operatorCompatibilityEnd(); ///< EX, Compatibility mode end @@ -612,13 +662,22 @@ private: /// Checks, if filling color is valid void checkFillingColor(); + /// Read object from operand stack + PDFObject readObjectFromOperandStack(size_t startPosition) const; + + /// Computes visibility of OCG/OCMD - returns false, if it is not suppressed, + /// or true, if it is suppressed. + bool isContentSuppressedByOC(PDFObjectReference ocgOrOcmd); + const PDFPage* m_page; const PDFDocument* m_document; const PDFFontCache* m_fontCache; + const PDFOptionalContentActivity* m_optionalContentActivity; const PDFDictionary* m_colorSpaceDictionary; const PDFDictionary* m_fontDictionary; const PDFDictionary* m_xobjectDictionary; const PDFDictionary* m_extendedGraphicStateDictionary; + const PDFDictionary* m_propertiesDictionary; // Default color spaces PDFColorSpacePointer m_deviceGrayColorSpace; @@ -631,6 +690,9 @@ private: /// Stack with saved graphic states std::stack m_stack; + /// Stack with marked content + std::vector m_markedContentStack; + /// Current graphic state PDFPageContentProcessorState m_graphicState; diff --git a/PdfForQtLib/sources/pdfpainter.cpp b/PdfForQtLib/sources/pdfpainter.cpp index a93ea67..2171da7 100644 --- a/PdfForQtLib/sources/pdfpainter.cpp +++ b/PdfForQtLib/sources/pdfpainter.cpp @@ -22,9 +22,14 @@ namespace pdf { - -PDFPainter::PDFPainter(QPainter* painter, PDFRenderer::Features features, QMatrix pagePointToDevicePointMatrix, const PDFPage* page, const PDFDocument* document, const PDFFontCache* fontCache) : - PDFPageContentProcessor(page, document, fontCache), +PDFPainter::PDFPainter(QPainter* painter, + PDFRenderer::Features features, + QMatrix pagePointToDevicePointMatrix, + const PDFPage* page, + const PDFDocument* document, + const PDFFontCache* fontCache, + const PDFOptionalContentActivity* optionalContentActivity) : + PDFPageContentProcessor(page, document, fontCache, optionalContentActivity), m_painter(painter), m_features(features), m_pagePointToDevicePointMatrix(pagePointToDevicePointMatrix) @@ -42,6 +47,12 @@ PDFPainter::~PDFPainter() void PDFPainter::performPathPainting(const QPainterPath& path, bool stroke, bool fill, bool text, Qt::FillRule fillRule) { + if (isContentSuppressed()) + { + // Content is suppressed, do not paint anything + return; + } + if ((!stroke && !fill) || path.isEmpty()) { // No operation requested - either path is empty, or neither stroking or filling @@ -84,6 +95,12 @@ void PDFPainter::performClipping(const QPainterPath& path, Qt::FillRule fillRule void PDFPainter::performImagePainting(const QImage& image) { + if (isContentSuppressed()) + { + // Content is suppressed, do not paint anything + return; + } + m_painter->save(); // TODO: Draw smooth images diff --git a/PdfForQtLib/sources/pdfpainter.h b/PdfForQtLib/sources/pdfpainter.h index b7676d4..a686c14 100644 --- a/PdfForQtLib/sources/pdfpainter.h +++ b/PdfForQtLib/sources/pdfpainter.h @@ -40,12 +40,14 @@ public: /// \param page Page, which will be drawn /// \param document Document owning the page /// \param fontCache Font cache + /// \param optionalContentActivity Activity of optional content explicit PDFPainter(QPainter* painter, PDFRenderer::Features features, QMatrix pagePointToDevicePointMatrix, const PDFPage* page, const PDFDocument* document, - const PDFFontCache* fontCache); + const PDFFontCache* fontCache, + const PDFOptionalContentActivity* optionalContentActivity); virtual ~PDFPainter() override; protected: diff --git a/PdfForQtLib/sources/pdfparser.cpp b/PdfForQtLib/sources/pdfparser.cpp index e4f39ec..ff0f6ba 100644 --- a/PdfForQtLib/sources/pdfparser.cpp +++ b/PdfForQtLib/sources/pdfparser.cpp @@ -641,8 +641,8 @@ PDFParser::PDFParser(const QByteArray& data, PDFParsingContext* context, Feature m_lexicalAnalyzer(data.constData(), data.constData() + data.size()), m_features(features) { - m_lookAhead1 = m_lexicalAnalyzer.fetch(); - m_lookAhead2 = m_lexicalAnalyzer.fetch(); + m_lookAhead1 = fetch(); + m_lookAhead2 = fetch(); } PDFParser::PDFParser(const char* begin, const char* end, PDFParsingContext* context, Features features) : @@ -650,8 +650,18 @@ PDFParser::PDFParser(const char* begin, const char* end, PDFParsingContext* cont m_lexicalAnalyzer(begin, end), m_features(features) { - m_lookAhead1 = m_lexicalAnalyzer.fetch(); - m_lookAhead2 = m_lexicalAnalyzer.fetch(); + m_lookAhead1 = fetch(); + m_lookAhead2 = fetch(); +} + +PDFParser::PDFParser(std::function tokenFetcher) : + m_tokenFetcher(qMove(tokenFetcher)), + m_context(nullptr), + m_lexicalAnalyzer(nullptr, nullptr), + m_features(None) +{ + m_lookAhead1 = fetch(); + m_lookAhead2 = fetch(); } PDFObject PDFParser::getObject() @@ -837,8 +847,8 @@ PDFObject PDFParser::getObject() } // Refill lookahead tokens - m_lookAhead1 = m_lexicalAnalyzer.fetch(); - m_lookAhead2 = m_lexicalAnalyzer.fetch(); + m_lookAhead1 = fetch(); + m_lookAhead2 = fetch(); if (m_lookAhead1.type == PDFLexicalAnalyzer::TokenType::Command && m_lookAhead1.data.toByteArray() == PDF_STREAM_END_COMMAND) @@ -902,8 +912,8 @@ void PDFParser::seek(PDFInteger offset) m_lexicalAnalyzer.seek(offset); // We must read lookahead symbols, because we invalidated them - m_lookAhead1 = m_lexicalAnalyzer.fetch(); - m_lookAhead2 = m_lexicalAnalyzer.fetch(); + m_lookAhead1 = fetch(); + m_lookAhead2 = fetch(); } bool PDFParser::fetchCommand(const char* command) @@ -921,7 +931,12 @@ bool PDFParser::fetchCommand(const char* command) void PDFParser::shift() { m_lookAhead1 = std::move(m_lookAhead2); - m_lookAhead2 = m_lexicalAnalyzer.fetch(); + m_lookAhead2 = fetch(); +} + +PDFLexicalAnalyzer::Token PDFParser::fetch() +{ + return m_tokenFetcher ? m_tokenFetcher() : m_lexicalAnalyzer.fetch(); } } // namespace pdf diff --git a/PdfForQtLib/sources/pdfparser.h b/PdfForQtLib/sources/pdfparser.h index d2a9201..ed6c6c0 100644 --- a/PdfForQtLib/sources/pdfparser.h +++ b/PdfForQtLib/sources/pdfparser.h @@ -276,13 +276,14 @@ public: enum Feature { None = 0x0000, - AllowStreams = 0x0001, + AllowStreams = 0x0001 }; Q_DECLARE_FLAGS(Features, Feature) explicit PDFParser(const QByteArray& data, PDFParsingContext* context, Features features); explicit PDFParser(const char* begin, const char* end, PDFParsingContext* context, Features features); + explicit PDFParser(std::function tokenFetcher); /// Fetches single object from the stream. Does not check /// cyclical references. If object cannot be fetched, then @@ -312,6 +313,11 @@ public: private: void shift(); + PDFLexicalAnalyzer::Token fetch(); + + /// Functor for fetching tokens + std::function m_tokenFetcher; + /// Parsing context (multiple parsers can share it) PDFParsingContext* m_context; diff --git a/PdfForQtLib/sources/pdfrenderer.cpp b/PdfForQtLib/sources/pdfrenderer.cpp index b788107..990ce1e 100644 --- a/PdfForQtLib/sources/pdfrenderer.cpp +++ b/PdfForQtLib/sources/pdfrenderer.cpp @@ -22,9 +22,10 @@ namespace pdf { -PDFRenderer::PDFRenderer(const PDFDocument* document, const PDFFontCache* fontCache) : +PDFRenderer::PDFRenderer(const PDFDocument* document, const PDFFontCache* fontCache, const PDFOptionalContentActivity* optionalContentActivity) : m_document(document), m_fontCache(fontCache), + m_optionalContentActivity(optionalContentActivity), m_features(Antialiasing | TextAntialiasing) { Q_ASSERT(document); @@ -32,6 +33,7 @@ PDFRenderer::PDFRenderer(const PDFDocument* document, const PDFFontCache* fontCa // TODO: Dodelat rotovani stranek // TODO: Dodelat obrazky - SMOOTH +// TODO: Clipovani na stranku QList PDFRenderer::render(QPainter* painter, const QRectF& rectangle, size_t pageIndex) const { @@ -54,7 +56,7 @@ QList PDFRenderer::render(QPainter* painter, const QRectF& recta matrix.translate(rectangle.left(), rectangle.bottom()); matrix.scale(rectangle.width() / mediaBox.width(), -rectangle.height() / mediaBox.height()); - PDFPainter processor(painter, m_features, matrix, page, m_document, m_fontCache); + PDFPainter processor(painter, m_features, matrix, page, m_document, m_fontCache, m_optionalContentActivity); return processor.processContents(); } @@ -72,7 +74,7 @@ QList PDFRenderer::render(QPainter* painter, const QMatrix& matr const PDFPage* page = catalog->getPage(pageIndex); Q_ASSERT(page); - PDFPainter processor(painter, m_features, matrix, page, m_document, m_fontCache); + PDFPainter processor(painter, m_features, matrix, page, m_document, m_fontCache, m_optionalContentActivity); return processor.processContents(); } diff --git a/PdfForQtLib/sources/pdfrenderer.h b/PdfForQtLib/sources/pdfrenderer.h index 9e89710..0117e61 100644 --- a/PdfForQtLib/sources/pdfrenderer.h +++ b/PdfForQtLib/sources/pdfrenderer.h @@ -26,12 +26,13 @@ class QPainter; namespace pdf { class PDFFontCache; +class PDFOptionalContentActivity; /// Renders the PDF page on the painter, or onto an image. class PDFRenderer { public: - explicit PDFRenderer(const PDFDocument* document, const PDFFontCache* fontCache); + explicit PDFRenderer(const PDFDocument* document, const PDFFontCache* fontCache, const PDFOptionalContentActivity* optionalContentActivity); enum Feature { @@ -58,6 +59,7 @@ public: private: const PDFDocument* m_document; const PDFFontCache* m_fontCache; + const PDFOptionalContentActivity* m_optionalContentActivity; Features m_features; }; diff --git a/PdfForQtViewer/pdfviewermainwindow.cpp b/PdfForQtViewer/pdfviewermainwindow.cpp index de717fb..670e1bb 100644 --- a/PdfForQtViewer/pdfviewermainwindow.cpp +++ b/PdfForQtViewer/pdfviewermainwindow.cpp @@ -269,7 +269,7 @@ void PDFViewerMainWindow::setDocument(const pdf::PDFDocument* document) m_optionalContentActivity = new pdf::PDFOptionalContentActivity(document, pdf::OCUsage::View, this); } - m_pdfWidget->setDocument(document); + m_pdfWidget->setDocument(document, m_optionalContentActivity); m_optionalContentTreeModel->setDocument(document); m_optionalContentTreeModel->setActivity(m_optionalContentActivity); m_optionalContentTreeView->expandAll();