Annotation painting

This commit is contained in:
Jakub Melka 2020-03-07 17:38:50 +01:00
parent 92c58f68ff
commit 0b2d94d115
17 changed files with 363 additions and 47 deletions

View File

@ -87,6 +87,7 @@ HEADERS += \
sources/pdfccittfaxdecoder.h \
sources/pdfcms.h \
sources/pdfcompiler.h \
sources/pdfdocumentdrawinterface.h \
sources/pdfexecutionpolicy.h \
sources/pdffile.h \
sources/pdfitemmodels.h \

View File

@ -18,6 +18,9 @@
#include "pdfannotation.h"
#include "pdfdocument.h"
#include "pdfencoding.h"
#include "pdfpainter.h"
#include "pdfdrawspacecontroller.h"
#include "pdfcms.h"
namespace pdf
{
@ -98,8 +101,10 @@ PDFAppeareanceStreams PDFAppeareanceStreams::parse(const PDFDocument* document,
auto processSubdicitonary = [&result, document](Appearance appearance, PDFObject subdictionaryObject)
{
if (const PDFDictionary* subdictionary = document->getDictionaryFromObject(subdictionaryObject))
subdictionaryObject = document->getObject(subdictionaryObject);
if (subdictionaryObject.isDictionary())
{
const PDFDictionary* subdictionary = document->getDictionaryFromObject(subdictionaryObject);
for (size_t i = 0; i < subdictionary->getCount(); ++i)
{
result.m_appearanceStreams[std::make_pair(appearance, subdictionary->getKey(i))] = subdictionary->getValue(i);
@ -732,4 +737,186 @@ PDFAnnotationAdditionalActions PDFAnnotationAdditionalActions::parse(const PDFDo
return result;
}
PDFAnnotationManager::PDFAnnotationManager(PDFDrawWidgetProxy* proxy, QObject* parent) :
BaseClass(parent),
m_document(nullptr),
m_proxy(proxy)
{
Q_ASSERT(proxy);
m_proxy->registerDrawInterface(this);
}
PDFAnnotationManager::~PDFAnnotationManager()
{
m_proxy->unregisterDrawInterface(this);
}
void PDFAnnotationManager::drawPage(QPainter* painter,
PDFInteger pageIndex,
const PDFPrecompiledPage* compiledPage,
PDFTextLayoutGetter& layoutGetter,
const QMatrix& pagePointToDevicePointMatrix) const
{
Q_UNUSED(compiledPage);
Q_UNUSED(layoutGetter);
PageAnnotations& annotations = getPageAnnotations(pageIndex);
if (!annotations.isEmpty())
{
const PDFPage* page = m_document->getCatalog()->getPage(pageIndex);
Q_ASSERT(page);
const PDFRenderer::Features features = m_proxy->getFeatures();
PDFFontCache* fontCache = m_proxy->getFontCache();
const PDFCMSPointer cms = m_proxy->getCMSManager()->getCurrentCMS();
const PDFOptionalContentActivity* optionalActivity = m_proxy->getOptionalContentActivity();
const PDFMeshQualitySettings& meshQualitySettings = m_proxy->getMeshQualitySettings();
fontCache->setCacheShrinkEnabled(this, false);
for (PageAnnotation& annotation : annotations.annotations)
{
PDFObject appearanceStreamObject = m_document->getObject(getAppearanceStream(annotation));
if (!appearanceStreamObject.isStream())
{
// Object is not valid appearance stream
continue;
}
PDFDocumentDataLoaderDecorator loader(m_document);
const PDFStream* formStream = appearanceStreamObject.getStream();
const PDFDictionary* formDictionary = formStream->getDictionary();
QRectF annotationRectangle = annotation.annotation->getRectangle();
QRectF formBoundingBox = loader.readRectangle(formDictionary->get("BBox"), QRectF());
QMatrix formMatrix = loader.readMatrixFromDictionary(formDictionary, "Matrix", QMatrix());
QByteArray content = m_document->getDecodedStream(formStream);
PDFObject resources = m_document->getObject(formDictionary->get("Resources"));
PDFObject transparencyGroup = m_document->getObject(formDictionary->get("Group"));
if (formBoundingBox.isEmpty() || annotationRectangle.isEmpty())
{
// Form bounding box is empty
continue;
}
// "Unrotate" user coordinate space, if NoRotate flag is set
QMatrix userSpaceToDeviceSpace = pagePointToDevicePointMatrix;
if (annotation.annotation->getFlags().testFlag(PDFAnnotation::NoRotate))
{
PDFReal rotationAngle = 0.0;
switch (page->getPageRotation())
{
case PageRotation::None:
break;
case PageRotation::Rotate90:
rotationAngle = -90.0;
break;
case PageRotation::Rotate180:
rotationAngle = -180.0;
break;
case PageRotation::Rotate270:
rotationAngle = -270.0;
break;
default:
Q_ASSERT(false);
break;
}
QMatrix rotationMatrix;
rotationMatrix.rotate(-rotationAngle);
QPointF topLeft = annotationRectangle.bottomLeft(); // Do not forget, that y is upward instead of Qt
QPointF difference = topLeft - rotationMatrix.map(topLeft);
QMatrix finalMatrix;
finalMatrix.translate(difference.x(), difference.y());
finalMatrix.rotate(-rotationAngle);
userSpaceToDeviceSpace = finalMatrix * userSpaceToDeviceSpace;
}
// Jakub Melka: perform algorithm 8.1, defined in PDF 1.7 reference,
// chapter 8.4.4 Appearance streams.
// Step 1) - calculate transformed appearance box
QRectF transformedAppearanceBox = formMatrix.mapRect(formBoundingBox);
// Step 2) - calculate matrix A, which maps from form space to annotation space
// Matrix A transforms from form space to transformed appearance box space
const PDFReal scaleX = transformedAppearanceBox.width() / formBoundingBox.width();
const PDFReal scaleY = transformedAppearanceBox.height() / formBoundingBox.height();
const PDFReal translateX = annotationRectangle.left() - formBoundingBox.left() * scaleX;
const PDFReal translateY = annotationRectangle.bottom() - formBoundingBox.bottom() * scaleY;
QMatrix A(scaleX, 0.0, 0.0, scaleY, translateX, translateY);
// Step 3) - compute final matrix AA
QMatrix AA = formMatrix * A;
PDFPainter pdfPainter(painter, features, userSpaceToDeviceSpace, page, m_document, fontCache, cms.get(), optionalActivity, meshQualitySettings);
pdfPainter.initializeProcessor();
// Jakub Melka: we must check, that we do not display annotation disabled by optional content
PDFObjectReference oc = annotation.annotation->getOptionalContent();
if (!oc.isValid() || !pdfPainter.isContentSuppressedByOC(oc))
{
pdfPainter.processForm(AA, formBoundingBox, resources, transparencyGroup, content);
}
}
fontCache->setCacheShrinkEnabled(this, true);
}
}
void PDFAnnotationManager::setDocument(const PDFDocument* document)
{
if (m_document != document)
{
m_document = document;
m_pageAnnotations.clear();
}
}
PDFObject PDFAnnotationManager::getAppearanceStream(PageAnnotation& pageAnnotation) const
{
auto getAppearanceStream = [&pageAnnotation] (void) -> PDFObject
{
return pageAnnotation.annotation->getAppearanceStreams().getAppearance(pageAnnotation.appearance, pageAnnotation.annotation->getAppearanceState());
};
return pageAnnotation.appearanceStream.get(getAppearanceStream);
}
PDFAnnotationManager::PageAnnotations& PDFAnnotationManager::getPageAnnotations(PDFInteger pageIndex) const
{
Q_ASSERT(m_document);
auto it = m_pageAnnotations.find(pageIndex);
if (it == m_pageAnnotations.cend())
{
// Create page annotations
const PDFPage* page = m_document->getCatalog()->getPage(pageIndex);
Q_ASSERT(page);
PageAnnotations annotations;
const std::vector<PDFObjectReference>& pageAnnotations = page->getAnnotations();
annotations.annotations.reserve(pageAnnotations.size());
for (PDFObjectReference annotationReference : pageAnnotations)
{
PDFAnnotationPtr annotationPtr = PDFAnnotation::parse(m_document, m_document->getObjectByReference(annotationReference));
if (annotationPtr)
{
PageAnnotation annotation;
annotation.annotation = qMove(annotationPtr);
annotations.annotations.emplace_back(qMove(annotation));
}
}
it = m_pageAnnotations.insert(std::make_pair(pageIndex, qMove(annotations))).first;
}
return it->second;
}
} // namespace pdf

View File

@ -18,11 +18,12 @@
#ifndef PDFANNOTATION_H
#define PDFANNOTATION_H
#include "pdfglobal.h"
#include "pdfutils.h"
#include "pdfobject.h"
#include "pdfaction.h"
#include "pdffile.h"
#include "pdfmultimedia.h"
#include "pdfdocumentdrawinterface.h"
#include <QPainterPath>
@ -31,6 +32,7 @@
namespace pdf
{
class PDFDocument;
class PDFDrawWidgetProxy;
enum class AnnotationType
{
@ -1021,6 +1023,59 @@ private:
PDFReal m_relativeVerticalOffset = 0.0;
};
/// Annotation manager manages annotations for document's pages. Each page
/// can have multiple annotations, and this object caches them. Also,
/// this object builds annotation's appearance streams, if necessary.
/// This object is not thread safe, functions can't be called from another
/// thread.
class PDFFORQTLIBSHARED_EXPORT PDFAnnotationManager : public QObject, public IDocumentDrawInterface
{
Q_OBJECT
private:
using BaseClass = QObject;
public:
explicit PDFAnnotationManager(PDFDrawWidgetProxy* proxy, QObject* parent);
virtual ~PDFAnnotationManager() override;
virtual void drawPage(QPainter* painter,
PDFInteger pageIndex,
const PDFPrecompiledPage* compiledPage,
PDFTextLayoutGetter& layoutGetter,
const QMatrix& pagePointToDevicePointMatrix) const override;
void setDocument(const PDFDocument* document);
private:
struct PageAnnotation
{
PDFAppeareanceStreams::Appearance appearance = PDFAppeareanceStreams::Appearance::Normal;
PDFCachedItem<PDFObject> appearanceStream;
PDFAnnotationPtr annotation;
};
struct PageAnnotations
{
bool isEmpty() const { return annotations.empty(); }
std::vector<PageAnnotation> annotations;
};
/// Returns current appearance stream for given page annotation
/// \param pageAnnotation Page annotation
PDFObject getAppearanceStream(PageAnnotation& pageAnnotation) const;
/// Returns reference to page annotation for given page index.
/// This function requires, that pointer to m_document is valid.
/// \param pageIndex Page index (must point to valid page)
PageAnnotations& getPageAnnotations(PDFInteger pageIndex) const;
const PDFDocument* m_document;
PDFDrawWidgetProxy* m_proxy;
mutable std::map<PDFInteger, PageAnnotations> m_pageAnnotations;
};
} // namespace pdf
#endif // PDFANNOTATION_H

View File

@ -0,0 +1,57 @@
// Copyright (C) 2020 Jakub Melka
//
// This file is part of PdfForQt.
//
// PdfForQt 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
// (at your option) any later version.
//
// PdfForQt 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 PDFForQt. If not, see <https://www.gnu.org/licenses/>.
#ifndef PDFDOCUMENTDRAWINTERFACE_H
#define PDFDOCUMENTDRAWINTERFACE_H
#include "pdfglobal.h"
class QPainter;
namespace pdf
{
class PDFPrecompiledPage;
class PDFTextLayoutGetter;
class PDFFORQTLIBSHARED_EXPORT IDocumentDrawInterface
{
public:
explicit inline IDocumentDrawInterface() = default;
virtual ~IDocumentDrawInterface() = default;
/// Performs drawing of additional graphics onto the painter using precompiled page,
/// optionally text layout and page point to device point matrix.
/// \param painter Painter
/// \param pageIndex Page index
/// \param compiledPage Compiled page
/// \param layoutGetter Layout getter
/// \param pagePointToDevicePointMatrix Matrix mapping page space to device point space
virtual void drawPage(QPainter* painter,
pdf::PDFInteger pageIndex,
const PDFPrecompiledPage* compiledPage,
PDFTextLayoutGetter& layoutGetter,
const QMatrix& pagePointToDevicePointMatrix) const;
/// Performs drawing of additional graphics after all pages are drawn onto the painter.
/// \param painter Painter
/// \param rect Draw rectangle (usually viewport rectangle of the pdf widget)
virtual void drawPostRendering(QPainter* painter, QRect rect) const;
};
} // namespace pdf
#endif // PDFDOCUMENTDRAWINTERFACE_H

View File

@ -466,7 +466,7 @@ PDFDocument PDFDocumentReader::readFromBuffer(const QByteArray& buffer)
}
else
{
throw PDFException(PDFTranslationContext::tr("Object stream %1 is invalid.").arg(objectStreamReference.objectNumber));
// Silently ignore this error. It is not critical, so, maybe this object will be null.
}
}
}

View File

@ -22,6 +22,7 @@
#include "pdfdocument.h"
#include "pdfrenderer.h"
#include "pdffont.h"
#include "pdfdocumentdrawinterface.h"
#include <QRectF>
#include <QObject>
@ -40,31 +41,6 @@ class PDFTextLayoutGetter;
class PDFAsynchronousPageCompiler;
class PDFAsynchronousTextLayoutCompiler;
class PDFFORQTLIBSHARED_EXPORT IDocumentDrawInterface
{
public:
explicit inline IDocumentDrawInterface() = default;
virtual ~IDocumentDrawInterface() = default;
/// Performs drawing of additional graphics onto the painter using precompiled page,
/// optionally text layout and page point to device point matrix.
/// \param painter Painter
/// \param pageIndex Page index
/// \param compiledPage Compiled page
/// \param layoutGetter Layout getter
/// \param pagePointToDevicePointMatrix Matrix mapping page space to device point space
virtual void drawPage(QPainter* painter,
pdf::PDFInteger pageIndex,
const PDFPrecompiledPage* compiledPage,
PDFTextLayoutGetter& layoutGetter,
const QMatrix& pagePointToDevicePointMatrix) const;
/// Performs drawing of additional graphics after all pages are drawn onto the painter.
/// \param painter Painter
/// \param rect Draw rectangle (usually viewport rectangle of the pdf widget)
virtual void drawPostRendering(QPainter* painter, QRect rect) const;
};
/// This class controls draw space - page layout. Pages are divided into blocks
/// each block can contain one or multiple pages. Units are in milimeters.
/// Pages are layouted in zoom-independent mode.

View File

@ -1656,7 +1656,7 @@ PDFRealizedFontPointer PDFFontCache::getRealizedFont(const PDFFontPointer& font,
return it->second;
}
void PDFFontCache::setCacheShrinkEnabled(void* source, bool enabled)
void PDFFontCache::setCacheShrinkEnabled(const void* source, bool enabled)
{
QMutexLocker lock(&m_mutex);
if (enabled)

View File

@ -419,7 +419,7 @@ public:
/// then cache can shrink.
/// \param source Source object
/// \param enabled Enable or disable cache shrinking
void setCacheShrinkEnabled(void* source, bool enabled);
void setCacheShrinkEnabled(const void* source, bool enabled);
/// Set font cache limits
void setCacheLimits(int fontCacheLimit, int instancedFontCacheLimit);
@ -434,7 +434,7 @@ private:
const PDFDocument* m_document;
mutable std::map<PDFObjectReference, PDFFontPointer> m_fontCache;
mutable std::map<std::pair<PDFFontPointer, PDFReal>, PDFRealizedFontPointer> m_realizedFontCache;
mutable std::set<void*> m_fontCacheShrinkDisabledObjects;
mutable std::set<const void*> m_fontCacheShrinkDisabledObjects;
};
/// Performs mapping from CID to GID (even identity mapping, if byte array is empty)

View File

@ -86,6 +86,8 @@ struct PDFObjectReference
{
return std::tie(objectNumber, generation) < std::tie(other.objectNumber, other.generation);
}
constexpr bool isValid() const { return objectNumber > 0; }
};
/// Represents version identification

View File

@ -220,6 +220,7 @@ void PDFPage::parseImpl(std::vector<PDFPage>& pages,
page.m_trimBox = loader.readRectangle(dictionary->get("TrimBox"), page.getCropBox());
page.m_artBox = loader.readRectangle(dictionary->get("ArtBox"), page.getCropBox());
page.m_contents = document->getObject(dictionary->get("Contents"));
page.m_annots = loader.readReferenceArrayFromDictionary(dictionary, "Annots");
pages.emplace_back(std::move(page));
}

View File

@ -134,6 +134,8 @@ public:
QRectF getRotatedMediaBoxMM() const;
QRectF getRotatedCropBox() const;
inline const std::vector<PDFObjectReference>& getAnnotations() const { return m_annots; }
static QRectF getRotatedBox(const QRectF& rect, PageRotation rotation);
private:
@ -158,6 +160,7 @@ private:
PDFObject m_resources;
PDFObject m_contents;
PDFObjectReference m_pageReference;
std::vector<PDFObjectReference> m_annots;
};
} // namespace pdf

View File

@ -224,10 +224,8 @@ PDFPageContentProcessor::~PDFPageContentProcessor()
PDFExecutionPolicy::endProcessingContentStream();
}
QList<PDFRenderError> PDFPageContentProcessor::processContents()
void PDFPageContentProcessor::initializeProcessor()
{
const PDFObject& contents = m_page->getContents();
// Clear the old errors
m_errorList.clear();
@ -254,6 +252,14 @@ QList<PDFRenderError> PDFPageContentProcessor::processContents()
m_graphicState.setFillColor(m_deviceGrayColorSpace->getDefaultColor(m_CMS, m_graphicState.getRenderingIntent(), this));
m_graphicState.setStateFlags(PDFPageContentProcessorState::StateAll);
updateGraphicState();
}
QList<PDFRenderError> PDFPageContentProcessor::processContents()
{
const PDFObject& contents = m_page->getContents();
// Initialize stream processor
initializeProcessor();
if (contents.isArray())
{

View File

@ -175,6 +175,22 @@ public:
/// then no new error is reported.
virtual void reportRenderErrorOnce(RenderErrorType type, QString message) override;
/// Processes form (XObject of type form)
/// \param Matrix Transformation matrix from form coordinate system to page coordinate system
/// \param boundingBox Bounding box, to which is drawed content clipped
/// \param resources Resources, assigned to the form
/// \param transparencyGroup Transparency group object
/// \param content Content stream of the form
void processForm(const QMatrix& matrix, const QRectF& boundingBox, const PDFObject& resources, const PDFObject& transparencyGroup, const QByteArray& content);
/// Initialize stream processor for processing content streams. For example,
/// graphic state is initialized to default, and default color spaces are initialized.
void initializeProcessor();
/// Computes visibility of OCG/OCMD - returns false, if it is not suppressed,
/// or true, if it is suppressed.
virtual bool isContentSuppressedByOC(PDFObjectReference ocgOrOcmd);
protected:
class PDFLineDashPattern
@ -528,10 +544,6 @@ protected:
/// Returns page bounding rectangle in device space
const QRectF& getPageBoundingRectDeviceSpace() const { return m_pageBoundingRectDeviceSpace; }
/// Computes visibility of OCG/OCMD - returns false, if it is not suppressed,
/// or true, if it is suppressed.
virtual bool isContentSuppressedByOC(PDFObjectReference ocgOrOcmd);
private:
/// Initializes the resources dictionaries
void initDictionaries(const PDFObject& resourcesObject);
@ -545,14 +557,6 @@ private:
/// Processes single command
void processCommand(const QByteArray& command);
/// Processes form (XObject of type form)
/// \param Matrix Transformation matrix from form coordinate system to page coordinate system
/// \param boundingBox Bounding box, to which is drawed content clipped
/// \param resources Resources, assigned to the form
/// \param transparencyGroup Transparency group object
/// \param content Content stream of the form
void processForm(const QMatrix& matrix, const QRectF& boundingBox, const PDFObject& resources, const PDFObject& transparencyGroup, const QByteArray& content);
/// Performs path painting
/// \param path Path, which should be drawn (can be emtpy - in that case nothing happens)
/// \param stroke Stroke the path

View File

@ -47,11 +47,12 @@ public:
QMatrix pagePointToDevicePointMatrix,
const PDFMeshQualitySettings& meshQualitySettings);
virtual bool isContentSuppressedByOC(PDFObjectReference ocgOrOcmd) override;
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;

View File

@ -28,6 +28,7 @@
#include <vector>
#include <iterator>
#include <functional>
namespace pdf
{
@ -76,6 +77,19 @@ public:
return m_object;
}
/// Returns the cached object. If object is dirty, then cached object is refreshed.
/// \param function Refresh function
inline const T& get(const std::function<T(void)>& function)
{
if (m_dirty)
{
m_object = function();
m_dirty = false;
}
return m_object;
}
/// Invalidates the cached item, so it must be refreshed from the cache next time,
/// if it is accessed.
inline void dirty()

View File

@ -89,6 +89,7 @@ PDFViewerMainWindow::PDFViewerMainWindow(QWidget* parent) :
m_isBusy(false),
m_isChangingProgressStep(false),
m_toolManager(nullptr),
m_annotationManager(nullptr),
m_textToSpeech(new PDFTextToSpeech(this))
{
ui->setupUi(this);
@ -262,6 +263,8 @@ PDFViewerMainWindow::PDFViewerMainWindow(QWidget* parent) :
updateMagnifierToolSettings();
connect(m_toolManager, &pdf::PDFToolManager::messageDisplayRequest, statusBar(), &QStatusBar::showMessage);
m_annotationManager = new pdf::PDFAnnotationManager(m_pdfWidget->getDrawWidgetProxy(), this);
connect(m_pdfWidget->getDrawWidgetProxy(), &pdf::PDFDrawWidgetProxy::drawSpaceChanged, this, &PDFViewerMainWindow::onDrawSpaceChanged);
connect(m_pdfWidget->getDrawWidgetProxy(), &pdf::PDFDrawWidgetProxy::pageLayoutChanged, this, &PDFViewerMainWindow::onPageLayoutChanged);
connect(m_pdfWidget, &pdf::PDFWidget::pageRenderingErrorsChanged, this, &PDFViewerMainWindow::onPageRenderingErrorsChanged, Qt::QueuedConnection);
@ -282,6 +285,9 @@ PDFViewerMainWindow::PDFViewerMainWindow(QWidget* parent) :
PDFViewerMainWindow::~PDFViewerMainWindow()
{
delete m_annotationManager;
m_annotationManager = nullptr;
delete ui;
}
@ -967,6 +973,7 @@ void PDFViewerMainWindow::setDocument(const pdf::PDFDocument* document)
m_optionalContentActivity = new pdf::PDFOptionalContentActivity(document, pdf::OCUsage::View, this);
}
m_annotationManager->setDocument(document);
m_toolManager->setDocument(document);
m_textToSpeech->setDocument(document);
m_pdfWidget->setDocument(document, m_optionalContentActivity);

View File

@ -28,6 +28,7 @@
#include "pdfwidgettool.h"
#include "pdfrecentfilemanager.h"
#include "pdftexttospeech.h"
#include "pdfannotation.h"
#include <QFuture>
#include <QTreeView>
@ -179,6 +180,7 @@ private:
bool m_isChangingProgressStep;
pdf::PDFToolManager* m_toolManager;
pdf::PDFAnnotationManager* m_annotationManager;
PDFTextToSpeech* m_textToSpeech;
};