// 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 . #include "pdfannotation.h" #include "pdfdocument.h" #include "pdfencoding.h" #include "pdfpainter.h" #include "pdfdrawspacecontroller.h" #include "pdfcms.h" #include "pdfwidgetutils.h" #include "pdfpagecontentprocessor.h" #include "pdfparser.h" #include "pdfdrawwidget.h" #include "pdfform.h" #include "pdfpainterutils.h" #include #include #include #include #include #include #include #include #include namespace pdf { PDFAnnotationBorder PDFAnnotationBorder::parseBorder(const PDFObjectStorage* storage, PDFObject object) { PDFAnnotationBorder result; object = storage->getObject(object); if (object.isArray()) { const PDFArray* array = object.getArray(); if (array->getCount() >= 3) { PDFDocumentDataLoaderDecorator loader(storage); result.m_definition = Definition::Simple; result.m_hCornerRadius = loader.readNumber(array->getItem(0), 0.0); result.m_vCornerRadius = loader.readNumber(array->getItem(1), 0.0); result.m_width = loader.readNumber(array->getItem(2), 1.0); if (array->getCount() >= 4) { result.m_dashPattern = loader.readNumberArray(array->getItem(3)); } } } return result; } PDFAnnotationBorder PDFAnnotationBorder::parseBS(const PDFObjectStorage* storage, PDFObject object) { PDFAnnotationBorder result; if (const PDFDictionary* dictionary = storage->getDictionaryFromObject(object)) { PDFDocumentDataLoaderDecorator loader(storage); result.m_definition = Definition::BorderStyle; result.m_width = loader.readNumberFromDictionary(dictionary, "W", 1.0); constexpr const std::array, 6> styles = { std::pair{ "S", Style::Solid }, std::pair{ "D", Style::Dashed }, std::pair{ "B", Style::Beveled }, std::pair{ "I", Style::Inset }, std::pair{ "U", Style::Underline } }; result.m_style = loader.readEnumByName(dictionary->get("S"), styles.begin(), styles.end(), Style::Solid); } return result; } PDFAnnotationBorderEffect PDFAnnotationBorderEffect::parse(const PDFObjectStorage* storage, PDFObject object) { PDFAnnotationBorderEffect result; if (const PDFDictionary* dictionary = storage->getDictionaryFromObject(object)) { PDFDocumentDataLoaderDecorator loader(storage); result.m_intensity = loader.readNumberFromDictionary(dictionary, "I", 0.0); constexpr const std::array, 2> effects = { std::pair{ "S", Effect::None }, std::pair{ "C", Effect::Cloudy } }; result.m_effect = loader.readEnumByName(dictionary->get("S"), effects.begin(), effects.end(), Effect::None); } return result; } PDFAppeareanceStreams PDFAppeareanceStreams::parse(const PDFObjectStorage* storage, PDFObject object) { PDFAppeareanceStreams result; auto processSubdictionary = [&result, storage](Appearance appearance, PDFObject subdictionaryObject) { subdictionaryObject = storage->getObject(subdictionaryObject); if (subdictionaryObject.isDictionary()) { const PDFDictionary* subdictionary = storage->getDictionaryFromObject(subdictionaryObject); for (size_t i = 0; i < subdictionary->getCount(); ++i) { result.m_appearanceStreams[std::make_pair(appearance, subdictionary->getKey(i).getString())] = subdictionary->getValue(i); } } else if (!subdictionaryObject.isNull()) { result.m_appearanceStreams[std::make_pair(appearance, QByteArray())] = subdictionaryObject; } }; if (const PDFDictionary* dictionary = storage->getDictionaryFromObject(object)) { processSubdictionary(Appearance::Normal, dictionary->get("N")); processSubdictionary(Appearance::Rollover, dictionary->get("R")); processSubdictionary(Appearance::Down, dictionary->get("D")); } return result; } PDFObject PDFAppeareanceStreams::getAppearance(Appearance appearance, const QByteArray& state) const { Key key(appearance, state); auto it = m_appearanceStreams.find(key); if (it == m_appearanceStreams.cend() && appearance != Appearance::Normal) { key.first = Appearance::Normal; it = m_appearanceStreams.find(key); } if (it == m_appearanceStreams.cend() && !state.isEmpty()) { key.second = QByteArray(); it = m_appearanceStreams.find(key); } if (it != m_appearanceStreams.cend()) { return it->second; } return PDFObject(); } QByteArrayList PDFAppeareanceStreams::getAppearanceStates(Appearance appearance) const { QByteArrayList result; for (const auto& item : m_appearanceStreams) { if (item.first.first != appearance) { continue; } result << item.first.second; } return result; } std::vector PDFAppeareanceStreams::getAppearanceKeys() const { std::vector result; std::transform(m_appearanceStreams.cbegin(), m_appearanceStreams.cend(), std::back_inserter(result), [](const auto& item) { return item.first; }); return result; } PDFAnnotation::PDFAnnotation() : m_flags(), m_structParent(0) { } void PDFAnnotation::draw(AnnotationDrawParameters& parameters) const { Q_UNUSED(parameters); } std::vector PDFAnnotation::getDrawKeys(const PDFFormManager* formManager) const { Q_UNUSED(formManager); return { PDFAppeareanceStreams::Key{ PDFAppeareanceStreams::Appearance::Normal, QByteArray() } }; } QPainter::CompositionMode PDFAnnotation::getCompositionMode() const { if (PDFBlendModeInfo::isSupportedByQt(getBlendMode())) { return PDFBlendModeInfo::getCompositionModeFromBlendMode(getBlendMode()); } return PDFBlendModeInfo::getCompositionModeFromBlendMode(BlendMode::Normal); } QPainterPath PDFAnnotation::parsePath(const PDFObjectStorage* storage, const PDFDictionary* dictionary, bool closePath) { QPainterPath path; PDFDocumentDataLoaderDecorator loader(storage); PDFObject pathObject = storage->getObject(dictionary->get("Path")); if (pathObject.isArray()) { for (const PDFObject& pathItemObject : *pathObject.getArray()) { std::vector pathItem = loader.readNumberArray(pathItemObject); switch (pathItem.size()) { case 2: { QPointF point(pathItem[0], pathItem[1]); if (path.isEmpty()) { path.moveTo(point); } else { path.lineTo(point); } break; } case 4: { if (path.isEmpty()) { // First path item must be 'Move to' command continue; } path.quadTo(pathItem[0], pathItem[1], pathItem[2], pathItem[3]); break; } case 6: { if (path.isEmpty()) { // First path item must be 'Move to' command continue; } path.cubicTo(pathItem[0], pathItem[1], pathItem[2], pathItem[3], pathItem[4], pathItem[5]); break; } default: break; } } } if (closePath) { path.closeSubpath(); } return path; } void PDFAnnotation::setLanguage(const QString& language) { m_language = language; } void PDFAnnotation::setBlendMode(const BlendMode& blendMode) { m_blendMode = blendMode; } void PDFAnnotation::setStrokingOpacity(const PDFReal& strokingOpacity) { m_strokingOpacity = strokingOpacity; } void PDFAnnotation::setFillingOpacity(const PDFReal& fillingOpacity) { m_fillingOpacity = fillingOpacity; } void PDFAnnotation::setAssociatedFiles(const PDFObject& associatedFiles) { m_associatedFiles = associatedFiles; } void PDFAnnotation::setOptionalContentReference(const PDFObjectReference& optionalContentReference) { m_optionalContentReference = optionalContentReference; } void PDFAnnotation::setStructParent(const PDFInteger& structParent) { m_structParent = structParent; } void PDFAnnotation::setColor(const std::vector& color) { m_color = color; } void PDFAnnotation::setAnnotationBorder(const PDFAnnotationBorder& annotationBorder) { m_annotationBorder = annotationBorder; } void PDFAnnotation::setAppearanceState(const QByteArray& appearanceState) { m_appearanceState = appearanceState; } void PDFAnnotation::setAppearanceStreams(const PDFAppeareanceStreams& appearanceStreams) { m_appearanceStreams = appearanceStreams; } void PDFAnnotation::setFlags(const Flags& flags) { m_flags = flags; } void PDFAnnotation::setLastModifiedString(const QString& lastModifiedString) { m_lastModifiedString = lastModifiedString; } void PDFAnnotation::setLastModified(const QDateTime& lastModified) { m_lastModified = lastModified; } void PDFAnnotation::setName(const QString& name) { m_name = name; } void PDFAnnotation::setPageReference(const PDFObjectReference& pageReference) { m_pageReference = pageReference; } void PDFAnnotation::setContents(const QString& contents) { m_contents = contents; } void PDFAnnotation::setRectangle(const QRectF& rectangle) { m_rectangle = rectangle; } void PDFAnnotation::setSelfReference(const PDFObjectReference& selfReference) { m_selfReference = selfReference; } PDFAnnotationPtr PDFAnnotation::parse(const PDFObjectStorage* storage, PDFObjectReference reference) { PDFObject object = storage->getObjectByReference(reference); PDFAnnotationPtr result; const PDFDictionary* dictionary = storage->getDictionaryFromObject(object); if (!dictionary) { return result; } PDFDocumentDataLoaderDecorator loader(storage); QRectF annotationsRectangle = loader.readRectangle(dictionary->get("Rect"), QRectF()); // Determine type of annotation QByteArray subtype = loader.readNameFromDictionary(dictionary, "Subtype"); if (subtype == "Text") { PDFTextAnnotation* textAnnotation = new PDFTextAnnotation; result.reset(textAnnotation); textAnnotation->m_open = loader.readBooleanFromDictionary(dictionary, "Open", false); textAnnotation->m_iconName = loader.readNameFromDictionary(dictionary, "Name"); textAnnotation->m_state = loader.readTextStringFromDictionary(dictionary, "State", "Unmarked"); textAnnotation->m_stateModel = loader.readTextStringFromDictionary(dictionary, "StateModel", "Marked"); } else if (subtype == "Link") { PDFLinkAnnotation* linkAnnotation = new PDFLinkAnnotation; result.reset(linkAnnotation); linkAnnotation->m_action = PDFAction::parse(storage, dictionary->get("A")); if (!linkAnnotation->m_action) { PDFDestination destination = PDFDestination::parse(storage, dictionary->get("Dest")); linkAnnotation->m_action.reset(new PDFActionGoTo(destination, PDFDestination())); } linkAnnotation->m_previousAction = PDFAction::parse(storage, dictionary->get("PA")); constexpr const std::array, 4> highlightMode = { std::pair{ "N", LinkHighlightMode::None }, std::pair{ "I", LinkHighlightMode::Invert }, std::pair{ "O", LinkHighlightMode::Outline }, std::pair{ "P", LinkHighlightMode::Push } }; linkAnnotation->m_highlightMode = loader.readEnumByName(dictionary->get("H"), highlightMode.begin(), highlightMode.end(), LinkHighlightMode::Invert); linkAnnotation->m_activationRegion = parseQuadrilaterals(storage, dictionary->get("QuadPoints"), annotationsRectangle); } else if (subtype == "FreeText") { PDFFreeTextAnnotation* freeTextAnnotation = new PDFFreeTextAnnotation; result.reset(freeTextAnnotation); constexpr const std::array, 2> intents = { std::pair{ "FreeTextCallout", PDFFreeTextAnnotation::Intent::Callout }, std::pair{ "FreeTextTypeWriter", PDFFreeTextAnnotation::Intent::TypeWriter } }; freeTextAnnotation->m_defaultAppearance = loader.readStringFromDictionary(dictionary, "DA"); freeTextAnnotation->m_justification = static_cast(loader.readIntegerFromDictionary(dictionary, "Q", 0)); freeTextAnnotation->m_defaultStyleString = loader.readTextStringFromDictionary(dictionary, "DS", QString()); freeTextAnnotation->m_calloutLine = PDFAnnotationCalloutLine::parse(storage, dictionary->get("CL")); freeTextAnnotation->m_intent = loader.readEnumByName(dictionary->get("IT"), intents.begin(), intents.end(), PDFFreeTextAnnotation::Intent::None); freeTextAnnotation->m_effect = PDFAnnotationBorderEffect::parse(storage, dictionary->get("BE")); std::vector differenceRectangle = loader.readNumberArrayFromDictionary(dictionary, "RD"); if (differenceRectangle.size() == 4) { freeTextAnnotation->m_textRectangle = annotationsRectangle.adjusted(differenceRectangle[0], differenceRectangle[1], -differenceRectangle[2], -differenceRectangle[3]); if (!freeTextAnnotation->m_textRectangle.isValid()) { freeTextAnnotation->m_textRectangle = QRectF(); } } std::vector lineEndings = loader.readNameArrayFromDictionary(dictionary, "LE"); if (lineEndings.size() == 2) { freeTextAnnotation->m_startLineEnding = convertNameToLineEnding(lineEndings[0]); freeTextAnnotation->m_endLineEnding = convertNameToLineEnding(lineEndings[1]); } } else if (subtype == "Line") { PDFLineAnnotation* lineAnnotation = new PDFLineAnnotation; result.reset(lineAnnotation); std::vector line = loader.readNumberArrayFromDictionary(dictionary, "L"); if (line.size() == 4) { lineAnnotation->m_line = QLineF(line[0], line[1], line[2], line[3]); } std::vector lineEndings = loader.readNameArrayFromDictionary(dictionary, "LE"); if (lineEndings.size() == 2) { lineAnnotation->m_startLineEnding = convertNameToLineEnding(lineEndings[0]); lineAnnotation->m_endLineEnding = convertNameToLineEnding(lineEndings[1]); } lineAnnotation->m_interiorColor = loader.readNumberArrayFromDictionary(dictionary, "IC"); lineAnnotation->m_leaderLineLength = loader.readNumberFromDictionary(dictionary, "LL", 0.0); lineAnnotation->m_leaderLineExtension = loader.readNumberFromDictionary(dictionary, "LLE", 0.0); lineAnnotation->m_leaderLineOffset = loader.readNumberFromDictionary(dictionary, "LLO", 0.0); lineAnnotation->m_captionRendered = loader.readBooleanFromDictionary(dictionary, "Cap", false); lineAnnotation->m_intent = (loader.readNameFromDictionary(dictionary, "IT") == "LineDimension") ? PDFLineAnnotation::Intent::Dimension : PDFLineAnnotation::Intent::Arrow; lineAnnotation->m_captionPosition = (loader.readNameFromDictionary(dictionary, "CP") == "Top") ? PDFLineAnnotation::CaptionPosition::Top : PDFLineAnnotation::CaptionPosition::Inline; lineAnnotation->m_measureDictionary = storage->getObject(dictionary->get("Measure")); std::vector captionOffset = loader.readNumberArrayFromDictionary(dictionary, "CO"); if (captionOffset.size() == 2) { lineAnnotation->m_captionOffset == QPointF(captionOffset[0], captionOffset[1]); } } else if (subtype == "Square" || subtype == "Circle") { PDFSimpleGeometryAnnotation* annotation = new PDFSimpleGeometryAnnotation((subtype == "Square") ? AnnotationType::Square : AnnotationType::Circle); result.reset(annotation); annotation->m_interiorColor = loader.readNumberArrayFromDictionary(dictionary, "IC"); annotation->m_effect = PDFAnnotationBorderEffect::parse(storage, dictionary->get("BE")); std::vector differenceRectangle = loader.readNumberArrayFromDictionary(dictionary, "RD"); if (differenceRectangle.size() == 4) { annotation->m_geometryRectangle = annotationsRectangle.adjusted(differenceRectangle[0], differenceRectangle[1], -differenceRectangle[2], -differenceRectangle[3]); if (!annotation->m_geometryRectangle.isValid()) { annotation->m_geometryRectangle = QRectF(); } } } else if (subtype == "Polygon" || subtype == "PolyLine") { PDFPolygonalGeometryAnnotation* annotation = new PDFPolygonalGeometryAnnotation((subtype == "Polygon") ? AnnotationType::Polygon : AnnotationType::Polyline); result.reset(annotation); std::vector vertices = loader.readNumberArrayFromDictionary(dictionary, "Vertices"); const size_t count = vertices.size() / 2; annotation->m_vertices.reserve(count); for (size_t i = 0; i < count; ++i) { annotation->m_vertices.emplace_back(vertices[2 * i], vertices[2 * i + 1]); } std::vector lineEndings = loader.readNameArrayFromDictionary(dictionary, "LE"); if (lineEndings.size() == 2) { annotation->m_startLineEnding = convertNameToLineEnding(lineEndings[0]); annotation->m_endLineEnding = convertNameToLineEnding(lineEndings[1]); } annotation->m_interiorColor = loader.readNumberArrayFromDictionary(dictionary, "IC"); annotation->m_effect = PDFAnnotationBorderEffect::parse(storage, dictionary->get("BE")); constexpr const std::array, 3> intents = { std::pair{ "PolygonCloud", PDFPolygonalGeometryAnnotation::Intent::Cloud }, std::pair{ "PolyLineDimension", PDFPolygonalGeometryAnnotation::Intent::Dimension }, std::pair{ "PolygonDimension", PDFPolygonalGeometryAnnotation::Intent::Dimension } }; annotation->m_intent = loader.readEnumByName(dictionary->get("IT"), intents.begin(), intents.end(), PDFPolygonalGeometryAnnotation::Intent::None); annotation->m_measure = storage->getObject(dictionary->get("Measure")); annotation->m_path = parsePath(storage, dictionary, subtype == "Polygon"); } else if (subtype == "Highlight" || subtype == "Underline" || subtype == "Squiggly" || subtype == "StrikeOut") { AnnotationType type = AnnotationType::Highlight; if (subtype == "Underline") { type = AnnotationType::Underline; } else if (subtype == "Squiggly") { type = AnnotationType::Squiggly; } else if (subtype == "StrikeOut") { type = AnnotationType::StrikeOut; } PDFHighlightAnnotation* annotation = new PDFHighlightAnnotation(type); result.reset(annotation); annotation->m_highlightArea = parseQuadrilaterals(storage, dictionary->get("QuadPoints"), annotationsRectangle); } else if (subtype == "Caret") { PDFCaretAnnotation* annotation = new PDFCaretAnnotation(); result.reset(annotation); std::vector differenceRectangle = loader.readNumberArrayFromDictionary(dictionary, "RD"); if (differenceRectangle.size() == 4) { annotation->m_caretRectangle = annotationsRectangle.adjusted(differenceRectangle[0], differenceRectangle[1], -differenceRectangle[2], -differenceRectangle[3]); if (!annotation->m_caretRectangle.isValid()) { annotation->m_caretRectangle = QRectF(); } } annotation->m_symbol = (loader.readNameFromDictionary(dictionary, "Sy") == "P") ? PDFCaretAnnotation::Symbol::Paragraph : PDFCaretAnnotation::Symbol::None; } else if (subtype == "Stamp") { PDFStampAnnotation* annotation = new PDFStampAnnotation(); result.reset(annotation); constexpr const std::array stamps = { std::pair{ "Approved", Stamp::Approved }, std::pair{ "AsIs", Stamp::AsIs }, std::pair{ "Confidential", Stamp::Confidential }, std::pair{ "Departmental", Stamp::Departmental }, std::pair{ "Draft", Stamp::Draft }, std::pair{ "Experimental", Stamp::Experimental }, std::pair{ "Expired", Stamp::Expired }, std::pair{ "Final", Stamp::Final }, std::pair{ "ForComment", Stamp::ForComment }, std::pair{ "ForPublicRelease", Stamp::ForPublicRelease }, std::pair{ "NotApproved", Stamp::NotApproved }, std::pair{ "NotForPublicRelease", Stamp::NotForPublicRelease }, std::pair{ "Sold", Stamp::Sold }, std::pair{ "TopSecret", Stamp::TopSecret } }; annotation->m_stamp = loader.readEnumByName(dictionary->get("Name"), stamps.begin(), stamps.end(), Stamp::Draft); constexpr const std::array stampsIntents = { std::pair{ "Stamp", StampIntent::Stamp }, std::pair{ "StampImage", StampIntent::StampImage }, std::pair{ "StampSnapshot", StampIntent::StampSnapshot }, }; annotation->m_intent = loader.readEnumByName(dictionary->get("IT"), stampsIntents.begin(), stampsIntents.end(), StampIntent::Stamp); } else if (subtype == "Ink") { PDFInkAnnotation* annotation = new PDFInkAnnotation(); result.reset(annotation); annotation->m_inkPath = parsePath(storage, dictionary, false); if (annotation->m_inkPath.isEmpty()) { PDFObject inkList = storage->getObject(dictionary->get("InkList")); if (inkList.isArray()) { const PDFArray* inkListArray = inkList.getArray(); for (size_t i = 0, count = inkListArray->getCount(); i < count; ++i) { std::vector points = loader.readNumberArray(inkListArray->getItem(i)); const size_t pointCount = points.size() / 2; for (size_t j = 0; j < pointCount; ++j) { QPointF point(points[j * 2], points[j * 2 + 1]); if (j == 0) { annotation->m_inkPath.moveTo(point); } else { annotation->m_inkPath.lineTo(point); } } } } } } else if (subtype == "Popup") { PDFPopupAnnotation* annotation = new PDFPopupAnnotation(); result.reset(annotation); annotation->m_opened = loader.readBooleanFromDictionary(dictionary, "Open", false); } else if (subtype == "FileAttachment") { PDFFileAttachmentAnnotation* annotation = new PDFFileAttachmentAnnotation(); result.reset(annotation); annotation->m_fileSpecification = PDFFileSpecification::parse(storage, dictionary->get("FS")); constexpr const std::array, 4> icons = { std::pair{ "Graph", FileAttachmentIcon::Graph }, std::pair{ "Paperclip", FileAttachmentIcon::Paperclip }, std::pair{ "PushPin", FileAttachmentIcon::PushPin }, std::pair{ "Tag", FileAttachmentIcon::Tag } }; annotation->m_icon = loader.readEnumByName(dictionary->get("Name"), icons.begin(), icons.end(), FileAttachmentIcon::PushPin); } else if (subtype == "Redact") { PDFRedactAnnotation* annotation = new PDFRedactAnnotation(); result.reset(annotation); annotation->m_redactionRegion = parseQuadrilaterals(storage, dictionary->get("QuadPoints"), annotationsRectangle); annotation->m_interiorColor = loader.readNumberArrayFromDictionary(dictionary, "IC"); annotation->m_overlayForm = dictionary->get("RO"); annotation->m_overlayText = loader.readTextStringFromDictionary(dictionary, "OverlayText", QString()); annotation->m_repeat = loader.readBooleanFromDictionary(dictionary, "Repeat", false); annotation->m_defaultAppearance = loader.readStringFromDictionary(dictionary, "DA"); annotation->m_justification = loader.readIntegerFromDictionary(dictionary, "Q", 0); } else if (subtype == "Sound") { PDFSoundAnnotation* annotation = new PDFSoundAnnotation(); result.reset(annotation); annotation->m_sound = PDFSound::parse(storage, dictionary->get("Sound")); constexpr const std::array, 2> icons = { std::pair{ "Speaker", PDFSoundAnnotation::Icon::Speaker }, std::pair{ "Mic", PDFSoundAnnotation::Icon::Microphone } }; annotation->m_icon = loader.readEnumByName(dictionary->get("Name"), icons.begin(), icons.end(), PDFSoundAnnotation::Icon::Speaker); } else if (subtype == "Movie") { PDFMovieAnnotation* annotation = new PDFMovieAnnotation(); result.reset(annotation); annotation->m_movieTitle = loader.readStringFromDictionary(dictionary, "T"); annotation->m_movie = PDFMovie::parse(storage, dictionary->get("Movie")); PDFObject activation = storage->getObject(dictionary->get("A")); if (activation.isBool()) { annotation->m_playMovie = activation.getBool(); } else if (activation.isDictionary()) { annotation->m_playMovie = true; annotation->m_movieActivation = PDFMovieActivation::parse(storage, activation); } } else if (subtype == "Screen") { PDFScreenAnnotation* annotation = new PDFScreenAnnotation(); result.reset(annotation); annotation->m_screenTitle = loader.readTextStringFromDictionary(dictionary, "T", QString()); annotation->m_appearanceCharacteristics = PDFAnnotationAppearanceCharacteristics::parse(storage, dictionary->get("MK")); annotation->m_action = PDFAction::parse(storage, dictionary->get("A")); annotation->m_additionalActions = PDFAnnotationAdditionalActions::parse(storage, dictionary->get("AA"), dictionary->get("A")); } else if (subtype == "Widget") { PDFWidgetAnnotation* annotation = new PDFWidgetAnnotation(); result.reset(annotation); constexpr const std::array, 5> highlightModes = { std::pair{ "N", PDFWidgetAnnotation::HighlightMode::None }, std::pair{ "I", PDFWidgetAnnotation::HighlightMode::Invert }, std::pair{ "O", PDFWidgetAnnotation::HighlightMode::Outline }, std::pair{ "P", PDFWidgetAnnotation::HighlightMode::Push }, std::pair{ "T", PDFWidgetAnnotation::HighlightMode::Toggle } }; annotation->m_highlightMode = loader.readEnumByName(dictionary->get("H"), highlightModes.begin(), highlightModes.end(), PDFWidgetAnnotation::HighlightMode::Invert); annotation->m_appearanceCharacteristics = PDFAnnotationAppearanceCharacteristics::parse(storage, dictionary->get("MK")); annotation->m_action = PDFAction::parse(storage, dictionary->get("A")); annotation->m_additionalActions = PDFAnnotationAdditionalActions::parse(storage, dictionary->get("AA"), dictionary->get("A")); } else if (subtype == "PrinterMark") { PDFPrinterMarkAnnotation* annotation = new PDFPrinterMarkAnnotation(); result.reset(annotation); } else if (subtype == "TrapNet") { PDFTrapNetworkAnnotation* annotation = new PDFTrapNetworkAnnotation(); result.reset(annotation); } else if (subtype == "Watermark") { PDFWatermarkAnnotation* annotation = new PDFWatermarkAnnotation(); result.reset(annotation); if (const PDFDictionary* fixedPrintDictionary = storage->getDictionaryFromObject(dictionary->get("FixedPrint"))) { annotation->m_matrix = loader.readMatrixFromDictionary(fixedPrintDictionary, "Matrix", QMatrix()); annotation->m_relativeHorizontalOffset = loader.readNumberFromDictionary(fixedPrintDictionary, "H", 0.0); annotation->m_relativeVerticalOffset = loader.readNumberFromDictionary(fixedPrintDictionary, "V", 0.0); } } else if (subtype == "Projection") { PDFProjectionAnnotation* annotation = new PDFProjectionAnnotation(); result.reset(annotation); } else if (subtype == "3D") { PDF3DAnnotation* annotation = new PDF3DAnnotation(); result.reset(annotation); annotation->m_stream = PDF3DStream::parse(storage, dictionary->get("3DD")); const std::vector& views = annotation->getStream().getViews(); PDFObject defaultViewObject = storage->getObject(dictionary->get("DV")); if (defaultViewObject.isDictionary()) { annotation->m_defaultView = PDF3DView::parse(storage, defaultViewObject); } else if (defaultViewObject.isInt()) { PDFInteger index = defaultViewObject.getInteger(); if (index >= 0 && index < PDFInteger(views.size())) { annotation->m_defaultView = views[index]; } } else if (defaultViewObject.isName() && !views.empty()) { QByteArray name = defaultViewObject.getString(); if (name == "F") { annotation->m_defaultView = views.front(); } else if (name == "L") { annotation->m_defaultView = views.back(); } } annotation->m_activation = PDF3DActivation::parse(storage, dictionary->get("3DA")); annotation->m_interactive = loader.readBooleanFromDictionary(dictionary, "3DI", true); annotation->m_viewBox = loader.readRectangle(dictionary->get("3DB"), QRectF()); } else if (subtype == "RichMedia") { PDFRichMediaAnnotation* annotation = new PDFRichMediaAnnotation(); result.reset(annotation); annotation->m_content = PDFRichMediaContent::parse(storage, dictionary->get("RichMediaContent")); annotation->m_settings = PDFRichMediaSettings::parse(storage, dictionary->get("RichMediaSettings")); } if (!result) { // Invalid annotation type return result; } // Load common data for annotation result->m_selfReference = reference; result->m_rectangle = annotationsRectangle; result->m_contents = loader.readTextStringFromDictionary(dictionary, "Contents", QString()); result->m_pageReference = loader.readReferenceFromDictionary(dictionary, "P"); result->m_name = loader.readTextStringFromDictionary(dictionary, "NM", QString()); QByteArray string = loader.readStringFromDictionary(dictionary, "M"); result->m_lastModified = PDFEncoding::convertToDateTime(string); if (!result->m_lastModified.isValid()) { result->m_lastModifiedString = loader.readTextStringFromDictionary(dictionary, "M", QString()); } result->m_flags = Flags(loader.readIntegerFromDictionary(dictionary, "F", 0)); result->m_appearanceStreams = PDFAppeareanceStreams::parse(storage, dictionary->get("AP")); result->m_appearanceState = loader.readNameFromDictionary(dictionary, "AS"); result->m_annotationBorder = PDFAnnotationBorder::parseBS(storage, dictionary->get("BS")); if (!result->m_annotationBorder.isValid()) { result->m_annotationBorder = PDFAnnotationBorder::parseBorder(storage, dictionary->get("Border")); } result->m_color = loader.readNumberArrayFromDictionary(dictionary, "C"); result->m_structParent = loader.readIntegerFromDictionary(dictionary, "StructParent", 0); result->m_optionalContentReference = loader.readReferenceFromDictionary(dictionary, "OC"); result->m_strokingOpacity = loader.readNumberFromDictionary(dictionary, "CA", 1.0); result->m_fillingOpacity = loader.readNumberFromDictionary(dictionary, "ca", result->m_strokingOpacity); result->m_blendMode = PDFBlendModeInfo::getBlendMode(loader.readNameFromDictionary(dictionary, "BM")); if (result->m_blendMode == BlendMode::Invalid) { result->m_blendMode = BlendMode::Normal; } result->m_language = loader.readTextStringFromDictionary(dictionary, "Lang", QString()); if (PDFMarkupAnnotation* markupAnnotation = result->asMarkupAnnotation()) { markupAnnotation->m_windowTitle = loader.readTextStringFromDictionary(dictionary, "T", QString()); markupAnnotation->m_popupAnnotation = loader.readReferenceFromDictionary(dictionary, "Popup"); markupAnnotation->m_richTextString = loader.readTextStringFromDictionary(dictionary, "RC", QString()); markupAnnotation->m_creationDate = PDFEncoding::convertToDateTime(loader.readStringFromDictionary(dictionary, "CreationDate")); markupAnnotation->m_inReplyTo = loader.readReferenceFromDictionary(dictionary, "IRT"); markupAnnotation->m_subject = loader.readTextStringFromDictionary(dictionary, "Subj", QString()); markupAnnotation->m_replyType = (loader.readNameFromDictionary(dictionary, "RT") == "Group") ? PDFMarkupAnnotation::ReplyType::Group : PDFMarkupAnnotation::ReplyType::Reply; markupAnnotation->m_intent = loader.readNameFromDictionary(dictionary, "IT"); markupAnnotation->m_externalData = storage->getObject(dictionary->get("ExData")); } return result; } PDFAnnotationQuadrilaterals PDFAnnotation::parseQuadrilaterals(const PDFObjectStorage* storage, PDFObject quadrilateralsObject, const QRectF annotationRect) { QPainterPath path; PDFAnnotationQuadrilaterals::Quadrilaterals quadrilaterals; PDFDocumentDataLoaderDecorator loader(storage); std::vector points = loader.readNumberArray(quadrilateralsObject); const size_t quadrilateralCount = points.size() % 8; path.reserve(int(quadrilateralCount) + 5); quadrilaterals.reserve(quadrilateralCount); for (size_t i = 0; i < quadrilateralCount; ++i) { const size_t offset = i * 8; QPointF p1(points[offset + 0], points[offset + 1]); QPointF p2(points[offset + 2], points[offset + 3]); QPointF p3(points[offset + 4], points[offset + 5]); QPointF p4(points[offset + 6], points[offset + 7]); path.moveTo(p1); path.lineTo(p2); path.lineTo(p4); path.lineTo(p3); path.closeSubpath(); quadrilaterals.emplace_back(PDFAnnotationQuadrilaterals::Quadrilateral{ p1, p2, p3, p4 }); } if (path.isEmpty() && annotationRect.isValid()) { // Jakub Melka: we are using points at the top, because PDF has inverted y axis // against the Qt's y axis. path.addRect(annotationRect); quadrilaterals.emplace_back(PDFAnnotationQuadrilaterals::Quadrilateral{ annotationRect.topLeft(), annotationRect.topRight(), annotationRect.bottomLeft(), annotationRect.bottomRight() }); } return PDFAnnotationQuadrilaterals(qMove(path), qMove(quadrilaterals)); } AnnotationLineEnding PDFAnnotation::convertNameToLineEnding(const QByteArray& name) { constexpr const std::array, 10> lineEndings = { std::pair{ AnnotationLineEnding::None, "None" }, std::pair{ AnnotationLineEnding::Square, "Square" }, std::pair{ AnnotationLineEnding::Circle, "Circle" }, std::pair{ AnnotationLineEnding::Diamond, "Diamond" }, std::pair{ AnnotationLineEnding::OpenArrow, "OpenArrow" }, std::pair{ AnnotationLineEnding::ClosedArrow, "ClosedArrow" }, std::pair{ AnnotationLineEnding::Butt, "Butt" }, std::pair{ AnnotationLineEnding::ROpenArrow, "ROpenArrow" }, std::pair{ AnnotationLineEnding::RClosedArrow, "RClosedArrow" }, std::pair{ AnnotationLineEnding::Slash, "Slash" } }; auto it = std::find_if(lineEndings.cbegin(), lineEndings.cend(), [&name](const auto& item) { return name == item.second; }); if (it != lineEndings.cend()) { return it->first; } return AnnotationLineEnding::None; } QColor PDFAnnotation::getStrokeColor() const { return getDrawColorFromAnnotationColor(getColor(), getStrokeOpacity()); } QColor PDFAnnotation::getFillColor() const { return QColor(); } QColor PDFAnnotation::getDrawColorFromAnnotationColor(const std::vector& color, PDFReal opacity) { switch (color.size()) { case 1: { const PDFReal gray = color.back(); return QColor::fromRgbF(gray, gray, gray, opacity); } case 3: { const PDFReal r = color[0]; const PDFReal g = color[1]; const PDFReal b = color[2]; return QColor::fromRgbF(r, g, b, opacity); } case 4: { const PDFReal c = color[0]; const PDFReal m = color[1]; const PDFReal y = color[2]; const PDFReal k = color[3]; return QColor::fromCmykF(c, m, y, k, opacity); } default: break; } QColor black(Qt::black); black.setAlphaF(opacity); return black; } QPen PDFAnnotation::getPen() const { QColor strokeColor = getStrokeColor(); const PDFAnnotationBorder& border = getBorder(); if (qFuzzyIsNull(border.getWidth())) { // No border is drawn return Qt::NoPen; } QPen pen(strokeColor); pen.setWidthF(border.getWidth()); if (!border.getDashPattern().empty()) { PDFLineDashPattern lineDashPattern(border.getDashPattern(), 0.0); pen.setStyle(Qt::CustomDashLine); pen.setDashPattern(QVector::fromStdVector(lineDashPattern.getDashArray())); pen.setDashOffset(lineDashPattern.getDashOffset()); } return pen; } QBrush PDFAnnotation::getBrush() const { QColor color = getFillColor(); if (color.isValid()) { return QBrush(color, Qt::SolidPattern); } return QBrush(Qt::NoBrush); } PDFAnnotationCalloutLine PDFAnnotationCalloutLine::parse(const PDFObjectStorage* storage, PDFObject object) { PDFDocumentDataLoaderDecorator loader(storage); std::vector points = loader.readNumberArray(object); switch (points.size()) { case 4: return PDFAnnotationCalloutLine(QPointF(points[0], points[1]), QPointF(points[2], points[3])); case 6: return PDFAnnotationCalloutLine(QPointF(points[0], points[1]), QPointF(points[2], points[3]), QPointF(points[4], points[5])); default: break; } return PDFAnnotationCalloutLine(); } PDFAnnotationAppearanceCharacteristics PDFAnnotationAppearanceCharacteristics::parse(const PDFObjectStorage* storage, PDFObject object) { PDFAnnotationAppearanceCharacteristics result; if (const PDFDictionary* dictionary = storage->getDictionaryFromObject(object)) { PDFDocumentDataLoaderDecorator loader(storage); result.m_rotation = loader.readIntegerFromDictionary(dictionary, "R", 0); result.m_borderColor = loader.readNumberArrayFromDictionary(dictionary, "BC"); result.m_backgroundColor = loader.readNumberArrayFromDictionary(dictionary, "BG"); result.m_normalCaption = loader.readTextStringFromDictionary(dictionary, "CA", QString()); result.m_rolloverCaption = loader.readTextStringFromDictionary(dictionary, "RC", QString()); result.m_downCaption = loader.readTextStringFromDictionary(dictionary, "AC", QString()); result.m_normalIcon = storage->getObject(dictionary->get("I")); result.m_rolloverIcon = storage->getObject(dictionary->get("RI")); result.m_downIcon = storage->getObject(dictionary->get("IX")); result.m_iconFit = PDFAnnotationIconFitInfo::parse(storage, dictionary->get("IF")); result.m_pushButtonMode = static_cast(loader.readIntegerFromDictionary(dictionary, "TP", PDFInteger(PDFAnnotationAppearanceCharacteristics::PushButtonMode::NoIcon))); } return result; } PDFAnnotationIconFitInfo PDFAnnotationIconFitInfo::parse(const PDFObjectStorage* storage, PDFObject object) { PDFAnnotationIconFitInfo info; if (const PDFDictionary* dictionary = storage->getDictionaryFromObject(object)) { PDFDocumentDataLoaderDecorator loader(storage); constexpr const std::array, 4> scaleConditions = { std::pair{ "A", PDFAnnotationIconFitInfo::ScaleCondition::Always }, std::pair{ "B", PDFAnnotationIconFitInfo::ScaleCondition::ScaleBigger }, std::pair{ "S", PDFAnnotationIconFitInfo::ScaleCondition::ScaleSmaller }, std::pair{ "N", PDFAnnotationIconFitInfo::ScaleCondition::Never } }; constexpr const std::array, 2> scaleTypes = { std::pair{ "A", PDFAnnotationIconFitInfo::ScaleType::Anamorphic }, std::pair{ "P", PDFAnnotationIconFitInfo::ScaleType::Proportional } }; std::vector point = loader.readNumberArrayFromDictionary(dictionary, "A"); if (point.size() != 2) { point.resize(2, 0.5); } info.m_scaleCondition = loader.readEnumByName(dictionary->get("SW"), scaleConditions.begin(), scaleConditions.end(), PDFAnnotationIconFitInfo::ScaleCondition::Always); info.m_scaleType = loader.readEnumByName(dictionary->get("S"), scaleTypes.begin(), scaleTypes.end(), PDFAnnotationIconFitInfo::ScaleType::Proportional); info.m_relativeProportionalPosition = QPointF(point[0], point[1]); info.m_fullBox = loader.readBooleanFromDictionary(dictionary, "FB", false); } return info; } PDFAnnotationAdditionalActions PDFAnnotationAdditionalActions::parse(const PDFObjectStorage* storage, PDFObject object, PDFObject defaultAction) { PDFAnnotationAdditionalActions result; if (const PDFDictionary* dictionary = storage->getDictionaryFromObject(object)) { result.m_actions[CursorEnter] = PDFAction::parse(storage, dictionary->get("E")); result.m_actions[CursorLeave] = PDFAction::parse(storage, dictionary->get("X")); result.m_actions[MousePressed] = PDFAction::parse(storage, dictionary->get("D")); result.m_actions[MouseReleased] = PDFAction::parse(storage, dictionary->get("U")); result.m_actions[FocusIn] = PDFAction::parse(storage, dictionary->get("Fo")); result.m_actions[FocusOut] = PDFAction::parse(storage, dictionary->get("Bl")); result.m_actions[PageOpened] = PDFAction::parse(storage, dictionary->get("PO")); result.m_actions[PageClosed] = PDFAction::parse(storage, dictionary->get("PC")); result.m_actions[PageShow] = PDFAction::parse(storage, dictionary->get("PV")); result.m_actions[PageHide] = PDFAction::parse(storage, dictionary->get("PI")); result.m_actions[FormFieldModified] = PDFAction::parse(storage, dictionary->get("K")); result.m_actions[FormFieldFormatted] = PDFAction::parse(storage, dictionary->get("F")); result.m_actions[FormFieldValidated] = PDFAction::parse(storage, dictionary->get("V")); result.m_actions[FormFieldCalculated] = PDFAction::parse(storage, dictionary->get("C")); } result.m_actions[Default] = PDFAction::parse(storage, defaultAction); return result; } PDFAnnotationManager::PDFAnnotationManager(PDFFontCache* fontCache, const PDFCMSManager* cmsManager, const PDFOptionalContentActivity* optionalActivity, PDFMeshQualitySettings meshQualitySettings, PDFRenderer::Features features, Target target, QObject* parent) : BaseClass(parent), m_document(nullptr), m_fontCache(fontCache), m_cmsManager(cmsManager), m_optionalActivity(optionalActivity), m_formManager(nullptr), m_meshQualitySettings(meshQualitySettings), m_features(features), m_target(target) { } PDFAnnotationManager::~PDFAnnotationManager() { } QMatrix PDFAnnotationManager::prepareTransformations(const QMatrix& pagePointToDevicePointMatrix, QPaintDevice* device, const PDFAnnotation::Flags annotationFlags, const PDFPage* page, QRectF& annotationRectangle) const { // "Unrotate" user coordinate space, if NoRotate flag is set QMatrix userSpaceToDeviceSpace = pagePointToDevicePointMatrix; if (annotationFlags.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; } if (annotationFlags.testFlag(PDFAnnotation::NoZoom)) { // Jakub Melka: we must adjust annotation rectangle to disable zoom. We calculate // inverse zoom as square root of absolute value of determinant of scale matrix. // Determinant corresponds approximately to zoom squared, and if we will have // unrotated matrix, and both axes are scaled by same value, then determinant will // be exactly zoom squared. Also, we will adjust to target device logical DPI, // if we, for example are using 4K, or 8K monitors. qreal zoom = 1.0 / qSqrt(qAbs(pagePointToDevicePointMatrix.determinant())); zoom = PDFWidgetUtils::scaleDPI_x(device, zoom); QRectF unzoomedRect(annotationRectangle.bottomLeft(), annotationRectangle.size() * zoom); unzoomedRect.translate(0, -unzoomedRect.height()); annotationRectangle = unzoomedRect; } return userSpaceToDeviceSpace; } void PDFAnnotationManager::drawWidgetAnnotationHighlight(QRectF annotationRectangle, const PDFAnnotation* annotation, QPainter* painter, QMatrix userSpaceToDeviceSpace) const { const bool isWidget = annotation->getType() == AnnotationType::Widget; if (m_formManager && isWidget) { // Is it a form field? const PDFFormManager::FormAppearanceFlags flags = m_formManager->getAppearanceFlags(); const bool isFocused = m_formManager->isFocused(annotation->getSelfReference()); if (isFocused || flags.testFlag(PDFFormManager::HighlightFields) || flags.testFlag(PDFFormManager::HighlightRequiredFields)) { const PDFFormField* formField = m_formManager->getFormFieldForWidget(annotation->getSelfReference()); if (!formField) { return; } // Nekreslíme zvýraznění push buttonů if (formField->getFieldType() == PDFFormField::FieldType::Button && formField->getFlags().testFlag(PDFFormField::PushButton)) { return; } QColor color; if (flags.testFlag(PDFFormManager::HighlightFields)) { color = Qt::blue; } if (flags.testFlag(PDFFormManager::HighlightRequiredFields) && formField->getFlags().testFlag(PDFFormField::Required)) { color = Qt::red; } if (isFocused) { color = Qt::yellow; } if (color.isValid()) { color.setAlphaF(0.2); // Draw annotation rectangle by highlight color QPainterPath highlightArea; highlightArea.addRect(annotationRectangle); highlightArea = userSpaceToDeviceSpace.map(highlightArea); painter->fillPath(highlightArea, color); } } } } void PDFAnnotationManager::drawPage(QPainter* painter, PDFInteger pageIndex, const PDFPrecompiledPage* compiledPage, PDFTextLayoutGetter& layoutGetter, const QMatrix& pagePointToDevicePointMatrix, QList& errors) const { Q_UNUSED(compiledPage); Q_UNUSED(layoutGetter); const PageAnnotations& annotations = getPageAnnotations(pageIndex); if (!annotations.isEmpty()) { const PDFPage* page = m_document->getCatalog()->getPage(pageIndex); Q_ASSERT(page); PDFRenderer::Features features = m_features; if (!features.testFlag(PDFRenderer::DisplayAnnotations)) { // Annotation displaying is disabled return; } Q_ASSERT(m_fontCache); Q_ASSERT(m_cmsManager); Q_ASSERT(m_optionalActivity); int fontCacheLock = 0; const PDFCMSPointer cms = m_cmsManager->getCurrentCMS(); m_fontCache->setCacheShrinkEnabled(&fontCacheLock, false); const PageAnnotation* annotationDrawnByEditor = nullptr; for (const PageAnnotation& annotation : annotations.annotations) { // If annotation draw is not enabled, then skip it if (!isAnnotationDrawEnabled(annotation)) { continue; } if (isAnnotationDrawnByEditor(annotation)) { Q_ASSERT(!annotationDrawnByEditor); annotationDrawnByEditor = &annotation; continue; } drawAnnotation(annotation, pagePointToDevicePointMatrix, page, cms.data(), false, errors, painter); } if (annotationDrawnByEditor) { drawAnnotation(*annotationDrawnByEditor, pagePointToDevicePointMatrix, page, cms.data(), true, errors, painter); } m_fontCache->setCacheShrinkEnabled(&fontCacheLock, true); } } void PDFAnnotationManager::drawAnnotation(const PageAnnotation& annotation, const QMatrix& pagePointToDevicePointMatrix, const PDFPage* page, const PDFCMS* cms, bool isEditorDrawEnabled, QList& errors, QPainter* painter) const { try { PDFObject appearanceStreamObject = m_document->getObject(getAppearanceStream(annotation)); if (!appearanceStreamObject.isStream() || isEditorDrawEnabled) { // Object is not valid appearance stream. We will try to draw default // annotation appearance, but we must consider also optional content. // We do not draw annotation, if it is not ignored and annotation // has reference to optional content. drawAnnotationDirect(annotation, pagePointToDevicePointMatrix, page, cms, isEditorDrawEnabled, painter); } else { drawAnnotationUsingAppearanceStream(annotation, appearanceStreamObject, pagePointToDevicePointMatrix, page, cms, painter); } } catch (PDFException exception) { errors.push_back(PDFRenderError(RenderErrorType::Error, exception.getMessage())); } catch (PDFRendererException exception) { errors.push_back(exception.getError()); } } bool PDFAnnotationManager::isAnnotationDrawEnabled(const PageAnnotation& annotation) const { const PDFAnnotation::Flags annotationFlags = annotation.annotation->getEffectiveFlags(); return !(annotationFlags.testFlag(PDFAnnotation::Hidden) || // Annotation is completely hidden (m_target == Target::Print && !annotationFlags.testFlag(PDFAnnotation::Print)) || // Target is print and annotation is marked as not printed (m_target == Target::View && annotationFlags.testFlag(PDFAnnotation::NoView)) || // Target is view, and annotation is disabled for screen annotation.annotation->isReplyTo()); // Annotation is reply to another annotation, display just the first annotation } bool PDFAnnotationManager::isAnnotationDrawnByEditor(const PageAnnotation& annotation) const { if (!m_formManager) { return false; } const PDFFormFieldWidgetEditor* editor = nullptr; if (annotation.annotation->getType() == AnnotationType::Widget) { editor = m_formManager->getEditor(m_formManager->getFormFieldForWidget(annotation.annotation->getSelfReference())); return editor && editor->isEditorDrawEnabled(); } return false; } void PDFAnnotationManager::drawAnnotationDirect(const PageAnnotation& annotation, const QMatrix& pagePointToDevicePointMatrix, const PDFPage* page, const PDFCMS* cms, bool isEditorDrawEnabled, QPainter* painter) const { if (!m_features.testFlag(PDFRenderer::IgnoreOptionalContent) && annotation.annotation->getOptionalContent().isValid()) { PDFPainter pdfPainter(painter, m_features, pagePointToDevicePointMatrix, page, m_document, m_fontCache, cms, m_optionalActivity, m_meshQualitySettings); if (pdfPainter.isContentSuppressedByOC(annotation.annotation->getOptionalContent())) { return; } } QRectF annotationRectangle = annotation.annotation->getRectangle(); { PDFPainterStateGuard guard(painter); painter->setRenderHint(QPainter::Antialiasing, true); painter->setWorldMatrix(pagePointToDevicePointMatrix, true); AnnotationDrawParameters parameters; parameters.painter = painter; parameters.annotation = annotation.annotation.data(); parameters.formManager = m_formManager; parameters.key = std::make_pair(annotation.appearance, annotation.annotation->getAppearanceState()); parameters.invertColors = m_features.testFlag(PDFRenderer::InvertColors); annotation.annotation->draw(parameters); if (parameters.boundingRectangle.isValid()) { annotationRectangle = parameters.boundingRectangle; } } // Draw highlighting of fields, but only, if target is View, // we do not want to render form field highlight, when we are // printing to the printer. if (m_target == Target::View && !isEditorDrawEnabled) { PDFPainterStateGuard guard(painter); drawWidgetAnnotationHighlight(annotationRectangle, annotation.annotation.get(), painter, pagePointToDevicePointMatrix); } } void PDFAnnotationManager::drawAnnotationUsingAppearanceStream(const PageAnnotation& annotation, const PDFObject& appearanceStreamObject, const QMatrix& pagePointToDevicePointMatrix, const PDFPage* page, const PDFCMS* cms, QPainter* painter) const { PDFDocumentDataLoaderDecorator loader(m_document); const PDFStream* formStream = appearanceStreamObject.getStream(); const PDFDictionary* formDictionary = formStream->getDictionary(); const PDFAnnotation::Flags annotationFlags = annotation.annotation->getEffectiveFlags(); 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")); const PDFInteger formStructuralParentKey = loader.readIntegerFromDictionary(formDictionary, "StructParent", page->getStructureParentKey()); if (formBoundingBox.isEmpty() || annotationRectangle.isEmpty()) { // Form bounding box is empty return; } QMatrix userSpaceToDeviceSpace = prepareTransformations(pagePointToDevicePointMatrix, painter->device(), annotationFlags, page, annotationRectangle); PDFRenderer::Features features = m_features; if (annotationFlags.testFlag(PDFAnnotation::NoZoom)) { features.setFlag(PDFRenderer::ClipToCropBox, false); } // 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 transformed appearance box space to the // annotation rectangle. const PDFReal scaleX = annotationRectangle.width() / transformedAppearanceBox.width(); const PDFReal scaleY = annotationRectangle.height() / transformedAppearanceBox.height(); const PDFReal translateX = annotationRectangle.left() - transformedAppearanceBox.left() * scaleX; const PDFReal translateY = annotationRectangle.bottom() - transformedAppearanceBox.bottom() * scaleY; QMatrix A(scaleX, 0.0, 0.0, scaleY, translateX, translateY); // Step 3) - compute final matrix AA QMatrix AA = formMatrix * A; bool isContentVisible = false; // Draw annotation { PDFPainterStateGuard guard(painter); PDFPainter pdfPainter(painter, features, userSpaceToDeviceSpace, page, m_document, m_fontCache, cms, m_optionalActivity, m_meshQualitySettings); pdfPainter.initializeProcessor(); // Jakub Melka: we must check, that we do not display annotation disabled by optional content PDFObjectReference oc = annotation.annotation->getOptionalContent(); isContentVisible = !oc.isValid() || !pdfPainter.isContentSuppressedByOC(oc); if (isContentVisible) { pdfPainter.processForm(AA, formBoundingBox, resources, transparencyGroup, content, formStructuralParentKey); } } // Draw highlighting of fields, but only, if target is View, // we do not want to render form field highlight, when we are // printing to the printer. if (isContentVisible && m_target == Target::View) { PDFPainterStateGuard guard(painter); painter->resetMatrix(); drawWidgetAnnotationHighlight(annotationRectangle, annotation.annotation.get(), painter, userSpaceToDeviceSpace); } } void PDFAnnotationManager::setDocument(const PDFModifiedDocument& document) { if (m_document != document) { m_document = document; m_optionalActivity = document.getOptionalContentActivity(); if (document.hasReset() || document.hasFlag(PDFModifiedDocument::Annotation)) { m_pageAnnotations.clear(); } } } PDFObject PDFAnnotationManager::getAppearanceStream(const PageAnnotation& pageAnnotation) const { auto getAppearanceStream = [&pageAnnotation] (void) -> PDFObject { return pageAnnotation.annotation->getAppearanceStreams().getAppearance(pageAnnotation.appearance, pageAnnotation.annotation->getAppearanceState()); }; QMutexLocker lock(&m_mutex); return pageAnnotation.appearanceStream.get(getAppearanceStream); } const PDFAnnotationManager::PageAnnotations& PDFAnnotationManager::getPageAnnotations(PDFInteger pageIndex) const { return const_cast(this)->getPageAnnotations(pageIndex); } PDFAnnotationManager::PageAnnotations& PDFAnnotationManager::getPageAnnotations(PDFInteger pageIndex) { Q_ASSERT(m_document); QMutexLocker lock(&m_mutex); 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& pageAnnotations = page->getAnnotations(); annotations.annotations.reserve(pageAnnotations.size()); for (PDFObjectReference annotationReference : pageAnnotations) { PDFAnnotationPtr annotationPtr = PDFAnnotation::parse(&m_document->getStorage(), 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; } bool PDFAnnotationManager::hasAnnotation(PDFInteger pageIndex) const { return !getPageAnnotations(pageIndex).isEmpty(); } bool PDFAnnotationManager::hasAnyPageAnnotation(const std::vector& pageIndices) const { return std::any_of(pageIndices.cbegin(), pageIndices.cend(), std::bind(&PDFAnnotationManager::hasAnnotation, this, std::placeholders::_1)); } PDFFormManager* PDFAnnotationManager::getFormManager() const { return m_formManager; } void PDFAnnotationManager::setFormManager(PDFFormManager* formManager) { m_formManager = formManager; } PDFRenderer::Features PDFAnnotationManager::getFeatures() const { return m_features; } void PDFAnnotationManager::setFeatures(PDFRenderer::Features features) { m_features = features; } PDFMeshQualitySettings PDFAnnotationManager::getMeshQualitySettings() const { return m_meshQualitySettings; } void PDFAnnotationManager::setMeshQualitySettings(const PDFMeshQualitySettings& meshQualitySettings) { m_meshQualitySettings = meshQualitySettings; } PDFFontCache* PDFAnnotationManager::getFontCache() const { return m_fontCache; } void PDFAnnotationManager::setFontCache(PDFFontCache* fontCache) { m_fontCache = fontCache; } const PDFOptionalContentActivity* PDFAnnotationManager::getOptionalActivity() const { return m_optionalActivity; } void PDFAnnotationManager::setOptionalActivity(const PDFOptionalContentActivity* optionalActivity) { m_optionalActivity = optionalActivity; } PDFAnnotationManager::Target PDFAnnotationManager::getTarget() const { return m_target; } void PDFAnnotationManager::setTarget(Target target) { m_target = target; } PDFWidgetAnnotationManager::PDFWidgetAnnotationManager(PDFDrawWidgetProxy* proxy, QObject* parent) : BaseClass(proxy->getFontCache(), proxy->getCMSManager(), proxy->getOptionalContentActivity(), proxy->getMeshQualitySettings(), proxy->getFeatures(), Target::View, parent), m_proxy(proxy) { Q_ASSERT(proxy); m_proxy->registerDrawInterface(this); } PDFWidgetAnnotationManager::~PDFWidgetAnnotationManager() { m_proxy->unregisterDrawInterface(this); } void PDFWidgetAnnotationManager::shortcutOverrideEvent(QWidget* widget, QKeyEvent* event) { Q_UNUSED(widget); Q_UNUSED(event); } void PDFWidgetAnnotationManager::keyPressEvent(QWidget* widget, QKeyEvent* event) { Q_UNUSED(widget); Q_UNUSED(event); } void PDFWidgetAnnotationManager::keyReleaseEvent(QWidget* widget, QKeyEvent* event) { Q_UNUSED(widget); Q_UNUSED(event); } void PDFWidgetAnnotationManager::mousePressEvent(QWidget* widget, QMouseEvent* event) { Q_UNUSED(widget); updateFromMouseEvent(event); } void PDFWidgetAnnotationManager::mouseDoubleClickEvent(QWidget* widget, QMouseEvent* event) { Q_UNUSED(widget); Q_UNUSED(event); } void PDFWidgetAnnotationManager::mouseReleaseEvent(QWidget* widget, QMouseEvent* event) { Q_UNUSED(widget); updateFromMouseEvent(event); } void PDFWidgetAnnotationManager::mouseMoveEvent(QWidget* widget, QMouseEvent* event) { Q_UNUSED(widget); updateFromMouseEvent(event); } void PDFWidgetAnnotationManager::wheelEvent(QWidget* widget, QWheelEvent* event) { Q_UNUSED(widget); Q_UNUSED(event); } void PDFWidgetAnnotationManager::updateFromMouseEvent(QMouseEvent* event) { PDFWidget* widget = m_proxy->getWidget(); std::vector currentPages = widget->getDrawWidget()->getCurrentPages(); if (!hasAnyPageAnnotation(currentPages)) { // All pages doesn't have annotation return; } m_tooltip = QString(); m_cursor = std::nullopt; bool appearanceChanged = false; // We must update appearance states, and update tooltip PDFWidgetSnapshot snapshot = m_proxy->getSnapshot(); const bool isDown = event->buttons().testFlag(Qt::LeftButton); const PDFAppeareanceStreams::Appearance hoverAppearance = isDown ? PDFAppeareanceStreams::Appearance::Down : PDFAppeareanceStreams::Appearance::Rollover; for (const PDFWidgetSnapshot::SnapshotItem& snapshotItem : snapshot.items) { PageAnnotations& pageAnnotations = getPageAnnotations(snapshotItem.pageIndex); for (PageAnnotation& pageAnnotation : pageAnnotations.annotations) { if (pageAnnotation.annotation->isReplyTo()) { // Annotation is reply to another annotation, do not interact with it continue; } const PDFAppeareanceStreams::Appearance oldAppearance = pageAnnotation.appearance; QRectF annotationRect = pageAnnotation.annotation->getRectangle(); QMatrix matrix = prepareTransformations(snapshotItem.pageToDeviceMatrix, widget, pageAnnotation.annotation->getEffectiveFlags(), m_document->getCatalog()->getPage(snapshotItem.pageIndex), annotationRect); QPainterPath path; path.addRect(annotationRect); path = matrix.map(path); if (path.contains(event->pos())) { pageAnnotation.appearance = hoverAppearance; // Generate tooltip if (m_tooltip.isEmpty()) { const PDFMarkupAnnotation* markupAnnotation = pageAnnotation.annotation->asMarkupAnnotation(); if (markupAnnotation) { QString title = markupAnnotation->getWindowTitle(); if (title.isEmpty()) { title = markupAnnotation->getSubject(); } if (title.isEmpty()) { title = PDFTranslationContext::tr("Info"); } const size_t repliesCount = pageAnnotations.getReplies(pageAnnotation).size(); if (repliesCount > 0) { title = PDFTranslationContext::tr("%1 (%2 replies)").arg(title).arg(repliesCount); } m_tooltip = QString("

%1

%2

").arg(title, markupAnnotation->getContents()); } } const PDFAction* linkAction = nullptr; const AnnotationType annotationType = pageAnnotation.annotation->getType(); if (annotationType == AnnotationType::Link) { const PDFLinkAnnotation* linkAnnotation = dynamic_cast(pageAnnotation.annotation.data()); Q_ASSERT(linkAnnotation); // We must check, if user clicked to the link area QPainterPath activationPath = linkAnnotation->getActivationRegion().getPath(); activationPath = snapshotItem.pageToDeviceMatrix.map(activationPath); if (activationPath.contains(event->pos()) && linkAnnotation->getAction()) { m_cursor = QCursor(Qt::PointingHandCursor); linkAction = linkAnnotation->getAction(); } } if (annotationType == AnnotationType::Widget) { if (m_formManager && m_formManager->hasFormFieldWidgetText(pageAnnotation.annotation->getSelfReference())) { m_cursor = QCursor(Qt::IBeamCursor); } else { m_cursor = QCursor(Qt::ArrowCursor); } } // Generate popup window if (event->type() == QEvent::MouseButtonPress && event->button() == Qt::LeftButton) { const PDFMarkupAnnotation* markupAnnotation = pageAnnotation.annotation->asMarkupAnnotation(); if (markupAnnotation) { QDialog* dialog = createDialogForMarkupAnnotations(widget, pageAnnotation, pageAnnotations); // Set proper dialog position - according to the popup annotation. If we // do not have popup annotation, then try to use annotations rectangle. if (const PageAnnotation* popupAnnotation = pageAnnotations.getPopupAnnotation(pageAnnotation)) { QPoint popupPoint = snapshotItem.pageToDeviceMatrix.map(popupAnnotation->annotation->getRectangle().bottomLeft()).toPoint(); popupPoint = widget->mapToGlobal(popupPoint); dialog->move(popupPoint); } else if (markupAnnotation->getRectangle().isValid()) { QPoint popupPoint = snapshotItem.pageToDeviceMatrix.map(markupAnnotation->getRectangle().bottomRight()).toPoint(); popupPoint = widget->mapToGlobal(popupPoint); dialog->move(popupPoint); } dialog->exec(); } if (linkAction) { emit actionTriggered(linkAction); } } } else { pageAnnotation.appearance = PDFAppeareanceStreams::Appearance::Normal; } const bool currentAppearanceChanged = oldAppearance != pageAnnotation.appearance; if (currentAppearanceChanged) { // We have changed appearance - we must mark stream as dirty pageAnnotation.appearanceStream.dirty(); appearanceChanged = true; } } } // If appearance has changed, then we must redraw the page if (appearanceChanged) { emit widget->getDrawWidgetProxy()->repaintNeeded(); } } QDialog* PDFWidgetAnnotationManager::createDialogForMarkupAnnotations(PDFWidget* widget, const PageAnnotation& pageAnnotation, const PageAnnotations& pageAnnotations) { QDialog* dialog = new QDialog(widget->getDrawWidget()->getWidget(), Qt::Popup); dialog->setAttribute(Qt::WA_DeleteOnClose, true); createWidgetsForMarkupAnnotations(dialog, pageAnnotation, pageAnnotations); return dialog; } void PDFWidgetAnnotationManager::createWidgetsForMarkupAnnotations(QWidget* parentWidget, const PageAnnotation& pageAnnotation, const PageAnnotations& pageAnnotations) { std::vector replies = pageAnnotations.getReplies(pageAnnotation); replies.insert(replies.begin(), &pageAnnotation); QScrollArea* scrollArea = new QScrollArea(parentWidget); scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); QVBoxLayout* layout = new QVBoxLayout(parentWidget); layout->addWidget(scrollArea); layout->setMargin(0); layout->setContentsMargins(QMargins()); QWidget* frameWidget = new QWidget(scrollArea); QVBoxLayout* frameLayout = new QVBoxLayout(frameWidget); frameLayout->setMargin(0); frameLayout->setSpacing(0); scrollArea->setWidget(frameWidget); const PDFMarkupAnnotation* markupMainAnnotation = pageAnnotation.annotation->asMarkupAnnotation(); QColor color = markupMainAnnotation->getDrawColorFromAnnotationColor(markupMainAnnotation->getColor(), 1.0); QColor titleColor = QColor::fromHslF(color.hueF(), color.saturationF(), 0.2, 1.0); QColor backgroundColor = QColor::fromHslF(color.hueF(), color.saturationF(), 0.9, 1.0); QString style = "QGroupBox { " "border: 2px solid black; " "border-color: rgb(%4, %5, %6); " "margin-top: 3ex; " "background-color: rgb(%1, %2, %3); " "}" "QGroupBox::title { " "subcontrol-origin: margin; " "subcontrol-position: top center; " "padding: 0px 8192px; " "background-color: rgb(%4, %5, %6); " "color: #FFFFFF;" "}"; style = style.arg(backgroundColor.red()).arg(backgroundColor.green()).arg(backgroundColor.blue()).arg(titleColor.red()).arg(titleColor.green()).arg(titleColor.blue()); for (const PageAnnotation* annotation : replies) { const PDFMarkupAnnotation* markupAnnotation = annotation->annotation->asMarkupAnnotation(); if (!markupAnnotation) { // This should not happen... continue; } QGroupBox* groupBox = new QGroupBox(scrollArea); frameLayout->addWidget(groupBox); QString title = markupAnnotation->getWindowTitle(); if (title.isEmpty()) { title = markupAnnotation->getSubject(); } QString dateTimeString = markupAnnotation->getCreationDate().toLocalTime().toString(Qt::SystemLocaleLongDate); title = QString("%1 (%2)").arg(title, dateTimeString).trimmed(); groupBox->setStyleSheet(style); groupBox->setTitle(title); QVBoxLayout* groupBoxLayout = new QVBoxLayout(groupBox); QLabel* label = new QLabel(groupBox); label->setTextInteractionFlags(Qt::TextBrowserInteraction); label->setWordWrap(true); label->setText(markupAnnotation->getContents()); label->setFixedWidth(PDFWidgetUtils::scaleDPI_x(label, 250)); label->setMinimumHeight(label->sizeHint().height()); groupBoxLayout->addWidget(label); } frameWidget->setFixedSize(frameWidget->minimumSizeHint()); parentWidget->setFixedSize(scrollArea->sizeHint()); } void PDFSimpleGeometryAnnotation::draw(AnnotationDrawParameters& parameters) const { Q_ASSERT(parameters.painter); parameters.boundingRectangle = getRectangle(); QPainter& painter = *parameters.painter; painter.setPen(getPen()); painter.setBrush(getBrush()); painter.setCompositionMode(getCompositionMode()); switch (getType()) { case AnnotationType::Square: { const PDFAnnotationBorder& border = getBorder(); const PDFReal hCornerRadius = border.getHorizontalCornerRadius(); const PDFReal vCornerRadius = border.getVerticalCornerRadius(); const bool isRounded = !qFuzzyIsNull(hCornerRadius) || !qFuzzyIsNull(vCornerRadius); if (isRounded) { painter.drawRoundedRect(getRectangle(), hCornerRadius, vCornerRadius, Qt::AbsoluteSize); } else { painter.drawRect(getRectangle()); } break; } case AnnotationType::Circle: { const PDFAnnotationBorder& border = getBorder(); const PDFReal width = border.getWidth(); QRectF rectangle = getRectangle(); rectangle.adjust(width, width, -width, -width); painter.drawEllipse(rectangle); break; } default: { Q_ASSERT(false); break; } } } QColor PDFSimpleGeometryAnnotation::getFillColor() const { return getDrawColorFromAnnotationColor(getInteriorColor(), getFillOpacity()); } bool PDFMarkupAnnotation::isReplyTo() const { return m_inReplyTo.isValid() && m_replyType == ReplyType::Reply; } void PDFMarkupAnnotation::setWindowTitle(const QString& windowTitle) { m_windowTitle = windowTitle; } void PDFMarkupAnnotation::setPopupAnnotation(const PDFObjectReference& popupAnnotation) { m_popupAnnotation = popupAnnotation; } void PDFMarkupAnnotation::setRichTextString(const QString& richTextString) { m_richTextString = richTextString; } void PDFMarkupAnnotation::setCreationDate(const QDateTime& creationDate) { m_creationDate = creationDate; } void PDFMarkupAnnotation::setInReplyTo(PDFObjectReference inReplyTo) { m_inReplyTo = inReplyTo; } void PDFMarkupAnnotation::setSubject(const QString& subject) { m_subject = subject; } void PDFMarkupAnnotation::setIntent(const QByteArray& intent) { m_intent = intent; } void PDFMarkupAnnotation::setExternalData(const PDFObject& externalData) { m_externalData = externalData; } void PDFMarkupAnnotation::setReplyType(ReplyType replyType) { m_replyType = replyType; } std::vector PDFTextAnnotation::getDrawKeys(const PDFFormManager* formManager) const { Q_UNUSED(formManager); return { PDFAppeareanceStreams::Key{ PDFAppeareanceStreams::Appearance::Normal, QByteArray() }, PDFAppeareanceStreams::Key{ PDFAppeareanceStreams::Appearance::Rollover, QByteArray() }, PDFAppeareanceStreams::Key{ PDFAppeareanceStreams::Appearance::Down, QByteArray() } }; } void PDFTextAnnotation::draw(AnnotationDrawParameters& parameters) const { QColor strokeColor = QColor::fromRgbF(0.0, 0.0, 0.0, getStrokeOpacity()); QColor fillColor = (parameters.key.first == PDFAppeareanceStreams::Appearance::Normal) ? QColor::fromRgbF(1.0, 1.0, 0.0, getFillOpacity()) : QColor::fromRgbF(1.0, 0.0, 0.0, getFillOpacity()); constexpr const PDFReal rectSize = 32.0; constexpr const PDFReal penWidth = 2.0; QPainter& painter = *parameters.painter; painter.setCompositionMode(getCompositionMode()); QRectF rectangle = getRectangle(); rectangle.setSize(QSizeF(rectSize, rectSize)); QPen pen(strokeColor); pen.setWidthF(penWidth); painter.setPen(pen); painter.setBrush(QBrush(fillColor, Qt::SolidPattern)); QRectF ellipseRectangle = rectangle; ellipseRectangle.adjust(penWidth, penWidth, -penWidth, -penWidth); // Draw the ellipse painter.drawEllipse(ellipseRectangle); QFont font = QApplication::font(); font.setPixelSize(16.0); QString text = getTextForIcon(m_iconName); QPainterPath textPath; textPath.addText(0.0, 0.0, font, text); textPath = QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0).map(textPath); QRectF textBoundingRect = textPath.boundingRect(); QPointF offset = rectangle.center() - textBoundingRect.center(); textPath.translate(offset); painter.fillPath(textPath, QBrush(strokeColor, Qt::SolidPattern)); parameters.boundingRectangle = rectangle; } PDFTextAnnotation::Flags PDFTextAnnotation::getEffectiveFlags() const { return getFlags() | NoZoom | NoRotate; } QIcon PDFTextAnnotation::createIcon(QString key, QSize size) { QIcon icon; QPixmap pixmap(size); pixmap.fill(Qt::transparent); QRect rectangle(QPoint(0, 0), size); rectangle.adjust(1, 1, -1, -1); QPainter painter(&pixmap); painter.setRenderHint(QPainter::Antialiasing); painter.setRenderHint(QPainter::TextAntialiasing); QString text = getTextForIcon(key); QFont font = QApplication::font(); font.setPixelSize(size.height() * 0.75); QPainterPath textPath; textPath.addText(0.0, 0.0, font, text); QRectF textBoundingRect = textPath.boundingRect(); QPointF offset = rectangle.center() - textBoundingRect.center(); textPath.translate(offset); painter.fillPath(textPath, QBrush(Qt::black, Qt::SolidPattern)); painter.end(); icon.addPixmap(qMove(pixmap)); return icon; } QString PDFTextAnnotation::getTextForIcon(const QString& key) { QString text = "?"; if (key == "Comment") { text = QString::fromUtf16(u"\U0001F4AC"); } else if (key == "Help") { text = "?"; } else if (key == "Insert") { text = QString::fromUtf16(u"\u2380"); } else if (key == "Key") { text = QString::fromUtf16(u"\U0001F511"); } else if (key == "NewParagraph") { text = QString::fromUtf16(u"\u2606"); } else if (key == "Note") { text = QString::fromUtf16(u"\u266A"); } else if (key == "Paragraph") { text = QString::fromUtf16(u"\u00B6"); } return text; } void PDFLineAnnotation::draw(AnnotationDrawParameters& parameters) const { QLineF line = getLine(); if (line.isNull()) { // Jakub Melka: do not draw empty lines return; } QPainter& painter = *parameters.painter; painter.setCompositionMode(getCompositionMode()); painter.setPen(getPen()); painter.setBrush(getBrush()); QPainterPath boundingPath; boundingPath.moveTo(line.p1()); boundingPath.lineTo(line.p2()); LineGeometryInfo info = LineGeometryInfo::create(line); const PDFReal leaderLineLength = getLeaderLineLength(); const PDFReal coefficientSigned = (leaderLineLength >= 0.0) ? 1.0 : -1.0; const PDFReal leaderLineOffset = getLeaderLineOffset() * coefficientSigned; const PDFReal leaderLineExtension = getLeaderLineExtension() * coefficientSigned; const PDFReal lineEndingSize = qMin(painter.pen().widthF() * 5.0, line.length() * 0.5); const bool hasLeaderLine = !qFuzzyIsNull(leaderLineLength) || !qFuzzyIsNull(leaderLineExtension); QLineF normalLine = info.transformedLine.normalVector().unitVector(); QPointF normalVector = normalLine.p1() - normalLine.p2(); QLineF lineToPaint = info.transformedLine; if (hasLeaderLine) { // We will draw leader lines at both start/end QPointF p1llStart = info.transformedLine.p1() + normalVector * leaderLineOffset; QPointF p1llEnd = info.transformedLine.p1() + normalVector * (leaderLineOffset + leaderLineLength + leaderLineExtension); QLineF llStart(p1llStart, p1llEnd); llStart = info.LCStoGCS.map(llStart); boundingPath.moveTo(llStart.p1()); boundingPath.lineTo(llStart.p2()); painter.drawLine(llStart); QPointF p2llStart = info.transformedLine.p2() + normalVector * leaderLineOffset; QPointF p2llEnd = info.transformedLine.p2() + normalVector * (leaderLineOffset + leaderLineLength + leaderLineExtension); QLineF llEnd(p2llStart, p2llEnd); llEnd = info.LCStoGCS.map(llEnd); boundingPath.moveTo(llEnd.p1()); boundingPath.lineTo(llEnd.p2()); painter.drawLine(llEnd); lineToPaint.translate(normalVector * (leaderLineOffset + leaderLineLength)); } QLineF lineToPaintOrig = info.LCStoGCS.map(lineToPaint); info = LineGeometryInfo::create(lineToPaintOrig); drawLine(info, painter, lineEndingSize, getStartLineEnding(), getEndLineEnding(), boundingPath, getCaptionOffset(), getContents(), getCaptionPosition() == CaptionPosition::Top); parameters.boundingRectangle = boundingPath.boundingRect(); parameters.boundingRectangle.adjust(-lineEndingSize, -lineEndingSize, lineEndingSize, lineEndingSize); } QColor PDFLineAnnotation::getFillColor() const { return getDrawColorFromAnnotationColor(getInteriorColor(), getFillOpacity()); } void PDFPolygonalGeometryAnnotation::draw(AnnotationDrawParameters& parameters) const { if (m_vertices.empty()) { // Jakub Melka: do not draw empty lines return; } QPainter& painter = *parameters.painter; painter.setCompositionMode(getCompositionMode()); painter.setPen(getPen()); painter.setBrush(getBrush()); const PDFReal penWidth = painter.pen().widthF(); switch (m_type) { case AnnotationType::Polygon: { if (m_path.isEmpty()) { QPolygonF polygon; polygon.reserve(int(m_vertices.size() + 1)); for (const QPointF& point : m_vertices) { polygon << point; } if (!polygon.isClosed()) { polygon << m_vertices.front(); } painter.drawPolygon(polygon, Qt::OddEvenFill); parameters.boundingRectangle = polygon.boundingRect(); parameters.boundingRectangle.adjust(-penWidth, -penWidth, penWidth, penWidth); } else { painter.drawPath(m_path); parameters.boundingRectangle = m_path.boundingRect(); parameters.boundingRectangle.adjust(-penWidth, -penWidth, penWidth, penWidth); } break; } case AnnotationType::Polyline: { const PDFReal lineEndingSize = painter.pen().widthF() * 5.0; QPainterPath boundingPath; if (m_path.isEmpty()) { const size_t pointCount = m_vertices.size(); const size_t lastPoint = pointCount - 1; for (size_t i = 1; i < pointCount; ++i) { if (i == 1) { // We draw first line drawLine(LineGeometryInfo::create(QLineF(m_vertices[i - 1], m_vertices[i])), painter, lineEndingSize, getStartLineEnding(), AnnotationLineEnding::None, boundingPath, QPointF(), QString(), true); } else if (i == lastPoint) { // We draw last line drawLine(LineGeometryInfo::create(QLineF(m_vertices[i - 1], m_vertices[i])), painter, lineEndingSize, AnnotationLineEnding::None, getEndLineEnding(), boundingPath, QPointF(), QString(), true); } else { QLineF line(m_vertices[i - 1], m_vertices[i]); boundingPath.moveTo(line.p1()); boundingPath.lineTo(line.p2()); painter.drawLine(line); } } } else { const PDFReal angle = 30; const PDFReal lineEndingHalfSize = lineEndingSize * 0.5; const PDFReal arrowAxisLength = lineEndingHalfSize / qTan(qDegreesToRadians(angle)); boundingPath = m_path; painter.drawPath(m_path); QMatrix LCStoGCS_start = LineGeometryInfo::create(QLineF(m_path.pointAtPercent(0.00), m_path.pointAtPercent(0.01))).LCStoGCS; QMatrix LCStoGCS_end = LineGeometryInfo::create(QLineF(m_path.pointAtPercent(0.99), m_path.pointAtPercent(1.00))).LCStoGCS; drawLineEnding(&painter, m_path.pointAtPercent(0), lineEndingSize, arrowAxisLength, getStartLineEnding(), false, LCStoGCS_start, boundingPath); drawLineEnding(&painter, m_path.pointAtPercent(1), lineEndingSize, arrowAxisLength, getEndLineEnding(), true, LCStoGCS_end, boundingPath); } parameters.boundingRectangle = boundingPath.boundingRect(); parameters.boundingRectangle.adjust(-lineEndingSize, -lineEndingSize, lineEndingSize, lineEndingSize); break; } default: Q_ASSERT(false); break; } } QColor PDFPolygonalGeometryAnnotation::getFillColor() const { return getDrawColorFromAnnotationColor(getInteriorColor(), getFillOpacity()); } PDFAnnotation::LineGeometryInfo PDFAnnotation::LineGeometryInfo::create(QLineF line) { LineGeometryInfo result; result.originalLine = line; result.transformedLine = QLineF(QPointF(0, 0), QPointF(line.length(), 0)); // Strategy: for simplification, we rotate the line clockwise so we will // get the line axis equal to the x-axis. const double angle = line.angleTo(QLineF(0, 0, 1, 0)); QPointF p1 = line.p1(); // Matrix LCStoGCS is local coordinate system of line line. It transforms // points on the line to the global coordinate system. So, point (0, 0) will // map onto p1 and point (length(p1-p2), 0) will map onto p2. result.LCStoGCS = QMatrix(); result.LCStoGCS.translate(p1.x(), p1.y()); result.LCStoGCS.rotate(angle); result.GCStoLCS = result.LCStoGCS.inverted(); return result; } void PDFAnnotation::drawLineEnding(QPainter* painter, QPointF point, PDFReal lineEndingSize, PDFReal arrowAxisLength, AnnotationLineEnding ending, bool flipAxis, const QMatrix& LCStoGCS, QPainterPath& boundingPath) const { QPainterPath path; const PDFReal lineEndingHalfSize = lineEndingSize * 0.5; switch (ending) { case AnnotationLineEnding::None: break; case AnnotationLineEnding::Square: { path.addRect(-lineEndingHalfSize, -lineEndingHalfSize, lineEndingSize, lineEndingSize); break; } case AnnotationLineEnding::Circle: { path.addEllipse(QPointF(0, 0), lineEndingHalfSize, lineEndingHalfSize); break; } case AnnotationLineEnding::Diamond: { path.moveTo(0.0, -lineEndingHalfSize); path.lineTo(lineEndingHalfSize, 0.0); path.lineTo(0.0, +lineEndingHalfSize); path.lineTo(-lineEndingHalfSize, 0.0); path.closeSubpath(); break; } case AnnotationLineEnding::OpenArrow: { path.moveTo(0.0, 0.0); path.lineTo(arrowAxisLength, lineEndingHalfSize); path.moveTo(0.0, 0.0); path.lineTo(arrowAxisLength, -lineEndingHalfSize); break; } case AnnotationLineEnding::ClosedArrow: { path.moveTo(0.0, 0.0); path.lineTo(arrowAxisLength, lineEndingHalfSize); path.lineTo(arrowAxisLength, -lineEndingHalfSize); path.closeSubpath(); break; } case AnnotationLineEnding::Butt: { path.moveTo(0.0, -lineEndingHalfSize); path.lineTo(0.0, lineEndingHalfSize); break; } case AnnotationLineEnding::ROpenArrow: { path.moveTo(0.0, 0.0); path.lineTo(-arrowAxisLength, lineEndingHalfSize); path.moveTo(0.0, 0.0); path.lineTo(-arrowAxisLength, -lineEndingHalfSize); break; } case AnnotationLineEnding::RClosedArrow: { path.moveTo(0.0, 0.0); path.lineTo(-arrowAxisLength, lineEndingHalfSize); path.lineTo(-arrowAxisLength, -lineEndingHalfSize); path.closeSubpath(); break; } case AnnotationLineEnding::Slash: { const PDFReal angle = 60; const PDFReal lineEndingHalfSizeForSlash = lineEndingSize * 0.5; const PDFReal slashAxisLength = lineEndingHalfSizeForSlash / qTan(qDegreesToRadians(angle)); path.moveTo(-slashAxisLength, -lineEndingHalfSizeForSlash); path.lineTo(slashAxisLength, lineEndingHalfSizeForSlash); break; } default: break; } if (!path.isEmpty()) { // Flip the x-axis (we are drawing endpoint) if (flipAxis && ending != AnnotationLineEnding::Slash) { QMatrix matrix; matrix.scale(-1.0, 1.0); path = matrix.map(path); } path.translate(point); path = LCStoGCS.map(path); painter->drawPath(path); boundingPath.addPath(path); } } void PDFAnnotation::drawLine(const PDFAnnotation::LineGeometryInfo& info, QPainter& painter, PDFReal lineEndingSize, AnnotationLineEnding p1Ending, AnnotationLineEnding p2Ending, QPainterPath& boundingPath, QPointF textOffset, const QString& text, bool textIsAboveLine) const { const PDFReal angle = 30; const PDFReal lineEndingHalfSize = lineEndingSize * 0.5; const PDFReal arrowAxisLength = lineEndingHalfSize / qTan(qDegreesToRadians(angle)); auto getOffsetFromLineEnding = [lineEndingHalfSize, arrowAxisLength](AnnotationLineEnding ending) { switch (ending) { case AnnotationLineEnding::Square: case AnnotationLineEnding::Circle: case AnnotationLineEnding::Diamond: return lineEndingHalfSize; case AnnotationLineEnding::ClosedArrow: return arrowAxisLength; default: break; } return 0.0; }; // Remove the offset from start/end const PDFReal startOffset = getOffsetFromLineEnding(p1Ending); const PDFReal endOffset = getOffsetFromLineEnding(p2Ending); QLineF adjustedLine = info.transformedLine; adjustedLine.setP1(adjustedLine.p1() + QPointF(startOffset, 0)); adjustedLine.setP2(adjustedLine.p2() - QPointF(endOffset, 0)); int textVerticalOffset = 0; const bool drawText = !text.isEmpty(); QPainterPath textPath; if (drawText) { QFont font = QApplication::font(); font.setPixelSize(12.0); QFontMetricsF fontMetrics(font, painter.device()); textVerticalOffset = fontMetrics.descent() + fontMetrics.leading(); textPath.addText(0, 0, font, text); textPath = QMatrix(1, 0, 0, -1, 0, 0).map(textPath); } drawLineEnding(&painter, info.transformedLine.p1(), lineEndingSize, arrowAxisLength, p1Ending, false, info.LCStoGCS, boundingPath); drawLineEnding(&painter, info.transformedLine.p2(), lineEndingSize, arrowAxisLength, p2Ending, true, info.LCStoGCS, boundingPath); if (drawText && !textIsAboveLine) { // We will draw text in the line QRectF textBoundingRect = textPath.controlPointRect(); QPointF center = textBoundingRect.center(); const qreal offsetY = center.y(); const qreal offsetX = center.x(); textPath.translate(textOffset + adjustedLine.center() - QPointF(offsetX, offsetY)); textBoundingRect = textPath.controlPointRect(); const qreal textPadding = 3.0; const qreal textStart = textBoundingRect.left() - textPadding; const qreal textEnd = textBoundingRect.right() + textPadding; textPath = info.LCStoGCS.map(textPath); painter.fillPath(textPath, QBrush(painter.pen().color(), Qt::SolidPattern)); boundingPath.addPath(textPath); if (textStart > adjustedLine.p1().x()) { QLineF leftLine(adjustedLine.p1(), QPointF(textStart, adjustedLine.p2().y())); painter.drawLine(info.LCStoGCS.map(leftLine)); } if (textEnd < adjustedLine.p2().x()) { QLineF rightLine(QPointF(textEnd, adjustedLine.p2().y()), adjustedLine.p2()); painter.drawLine(info.LCStoGCS.map(rightLine)); } // Include whole line to the bounding path adjustedLine = info.LCStoGCS.map(adjustedLine); boundingPath.moveTo(adjustedLine.p1()); boundingPath.lineTo(adjustedLine.p2()); } else { if (drawText) { // We will draw text above the line QRectF textBoundingRect = textPath.controlPointRect(); const qreal offsetY = textBoundingRect.top() - textVerticalOffset; const qreal offsetX = textBoundingRect.center().x(); textPath.translate(textOffset + adjustedLine.center() - QPointF(offsetX, offsetY)); textPath = info.LCStoGCS.map(textPath); painter.fillPath(textPath, QBrush(painter.pen().color(), Qt::SolidPattern)); boundingPath.addPath(textPath); } adjustedLine = info.LCStoGCS.map(adjustedLine); painter.drawLine(adjustedLine); boundingPath.moveTo(adjustedLine.p1()); boundingPath.lineTo(adjustedLine.p2()); } } void PDFHighlightAnnotation::draw(AnnotationDrawParameters& parameters) const { if (m_highlightArea.isEmpty()) { // Jakub Melka: do not draw empty highlight area return; } QPainter& painter = *parameters.painter; painter.setCompositionMode(getCompositionMode()); parameters.boundingRectangle = m_highlightArea.getPath().boundingRect(); painter.setPen(getPen()); painter.setBrush(getBrush()); switch (m_type) { case AnnotationType::Highlight: { painter.setCompositionMode(QPainter::CompositionMode_Multiply); painter.fillPath(m_highlightArea.getPath(), QBrush(getStrokeColor(), Qt::SolidPattern)); break; } case AnnotationType::Underline: { for (const PDFAnnotationQuadrilaterals::Quadrilateral& quadrilateral : m_highlightArea.getQuadrilaterals()) { QPointF p1 = quadrilateral[0]; QPointF p2 = quadrilateral[1]; QLineF line(p1, p2); painter.drawLine(line); } break; } case AnnotationType::Squiggly: { // Jakub Melka: Squiggly underline for (const PDFAnnotationQuadrilaterals::Quadrilateral& quadrilateral : m_highlightArea.getQuadrilaterals()) { QPointF p1 = quadrilateral[0]; QPointF p2 = quadrilateral[1]; // Calculate length (height) of quadrilateral const PDFReal height = (QLineF(quadrilateral[0], quadrilateral[2]).length() + QLineF(quadrilateral[1], quadrilateral[3]).length()) * 0.5; const PDFReal markSize = height / 7.0; // We can't assume, that line is horizontal. For example, rotated text with 45° degrees // counterclockwise, if it is highlighted with squiggly underline, it is not horizontal line. // So, we must calculate line geometry and transform it. QLineF line(p1, p2); LineGeometryInfo lineGeometryInfo = LineGeometryInfo::create(line); bool leadingEdge = true; for (PDFReal x = lineGeometryInfo.transformedLine.p1().x(); x < lineGeometryInfo.transformedLine.p2().x(); x+= markSize) { QLineF line; if (leadingEdge) { line = QLineF(x, 0.0, x + markSize, markSize); } else { // Falling edge line = QLineF(x, markSize, x + markSize, 0.0); } QLineF transformedLine = lineGeometryInfo.LCStoGCS.map(line); painter.drawLine(transformedLine); leadingEdge = !leadingEdge; } } break; } case AnnotationType::StrikeOut: { for (const PDFAnnotationQuadrilaterals::Quadrilateral& quadrilateral : m_highlightArea.getQuadrilaterals()) { QPointF p1 = (quadrilateral[0] + quadrilateral[2]) * 0.5; QPointF p2 = (quadrilateral[1] + quadrilateral[3]) * 0.5; QLineF line(p1, p2); painter.drawLine(line); } break; } default: break; } const qreal penWidth = painter.pen().widthF(); parameters.boundingRectangle.adjust(-penWidth, -penWidth, penWidth, penWidth); } std::vector PDFLinkAnnotation::getDrawKeys(const PDFFormManager* formManager) const { Q_UNUSED(formManager); return { PDFAppeareanceStreams::Key{ PDFAppeareanceStreams::Appearance::Down, QByteArray() } }; } void PDFLinkAnnotation::draw(AnnotationDrawParameters& parameters) const { if (parameters.key.first != PDFAppeareanceStreams::Appearance::Down || m_activationRegion.isEmpty() || m_highlightMode == LinkHighlightMode::None) { // Nothing to draw return; } QPainter& painter = *parameters.painter; parameters.boundingRectangle = getRectangle(); switch (m_highlightMode) { case LinkHighlightMode::Invert: { // Invert all painter.setCompositionMode(QPainter::CompositionMode_Difference); painter.fillPath(m_activationRegion.getPath(), QBrush(Qt::white, Qt::SolidPattern)); break; } case LinkHighlightMode::Outline: { // Invert the border painter.setCompositionMode(QPainter::CompositionMode_Difference); QPen pen = getPen(); pen.setColor(Qt::white); painter.setPen(pen); painter.setBrush(Qt::NoBrush); painter.drawPath(m_activationRegion.getPath()); break; } case LinkHighlightMode::Push: { // Draw border painter.setCompositionMode(getCompositionMode()); painter.setPen(getPen()); painter.setBrush(Qt::NoBrush); painter.drawPath(m_activationRegion.getPath()); break; } default: Q_ASSERT(false); break; } } PDFAnnotationDefaultAppearance PDFAnnotationDefaultAppearance::parse(const QByteArray& string) { PDFAnnotationDefaultAppearance result; PDFLexicalAnalyzer analyzer(string.constData(), string.constData() + string.size()); std::vector tokens; for (PDFLexicalAnalyzer::Token token = analyzer.fetch(); token.type != PDFLexicalAnalyzer::TokenType::EndOfFile; token = analyzer.fetch()) { tokens.push_back(qMove(token)); } auto readNumber = [&tokens](size_t index) -> PDFReal { Q_ASSERT(index >= 0 && index < tokens.size()); const PDFLexicalAnalyzer::Token& token = tokens[index]; if (token.type == PDFLexicalAnalyzer::TokenType::Real || token.type == PDFLexicalAnalyzer::TokenType::Integer) { return token.data.toDouble(); } return 0.0; }; for (size_t i = 0; i < tokens.size(); ++i) { const PDFLexicalAnalyzer::Token& token = tokens[i]; if (token.type == PDFLexicalAnalyzer::TokenType::Command) { QByteArray command = token.data.toByteArray(); if (command == "Tf") { if (i >= 1) { result.m_fontSize = readNumber(i - 1); } if (i >= 2) { result.m_fontName = tokens[i - 2].data.toByteArray(); } } else if (command == "g" && i >= 1) { result.m_fontColor = PDFAnnotation::getDrawColorFromAnnotationColor({ readNumber(i - 1) }, 1.0); } else if (command == "rg" && i >= 3) { result.m_fontColor = PDFAnnotation::getDrawColorFromAnnotationColor({ readNumber(i - 3), readNumber(i - 2), readNumber(i - 1) }, 1.0); } else if (command == "k" && i >= 4) { result.m_fontColor = PDFAnnotation::getDrawColorFromAnnotationColor({ readNumber(i - 4), readNumber(i - 3), readNumber(i - 2), readNumber(i - 1) }, 1.0); } } } return result; } void PDFFreeTextAnnotation::draw(AnnotationDrawParameters& parameters) const { QPainter& painter = *parameters.painter; painter.setCompositionMode(getCompositionMode()); parameters.boundingRectangle = getRectangle(); painter.setPen(getPen()); painter.setBrush(getBrush()); // Draw callout line const PDFAnnotationCalloutLine& calloutLine = getCalloutLine(); switch (calloutLine.getType()) { case PDFAnnotationCalloutLine::Type::Invalid: { // Annotation doesn't have callout line break; } case PDFAnnotationCalloutLine::Type::StartEnd: { QPainterPath boundingPath; QLineF line(calloutLine.getPoint(0), calloutLine.getPoint(1)); const PDFReal lineEndingSize = qMin(painter.pen().widthF() * 5.0, line.length() * 0.5); drawLine(LineGeometryInfo::create(line), painter, lineEndingSize, getStartLineEnding(), getEndLineEnding(), boundingPath, QPointF(), QString(), true); break; } case PDFAnnotationCalloutLine::Type::StartKneeEnd: { QPainterPath boundingPath; QLineF lineStart(calloutLine.getPoint(0), calloutLine.getPoint(1)); QLineF lineEnd(calloutLine.getPoint(1), calloutLine.getPoint(2)); PDFReal preferredLineEndingSize = painter.pen().widthF() * 5.0; PDFReal lineStartEndingSize = qMin(preferredLineEndingSize, lineStart.length() * 0.5); drawLine(LineGeometryInfo::create(lineStart), painter, lineStartEndingSize, getStartLineEnding(), AnnotationLineEnding::None, boundingPath, QPointF(), QString(), true); PDFReal lineEndEndingSize = qMin(preferredLineEndingSize, lineEnd.length() * 0.5); drawLine(LineGeometryInfo::create(lineEnd), painter, lineEndEndingSize, AnnotationLineEnding::None, getEndLineEnding() , boundingPath, QPointF(), QString(), true); break; } default: { Q_ASSERT(false); break; } } QRectF textRect = getTextRectangle(); if (!textRect.isValid()) { textRect = getRectangle(); } painter.drawRect(textRect); // Draw text PDFAnnotationDefaultAppearance defaultAppearance = PDFAnnotationDefaultAppearance::parse(getDefaultAppearance()); QFont font(defaultAppearance.getFontName()); font.setPixelSize(defaultAppearance.getFontSize()); painter.setPen(defaultAppearance.getFontColor()); Qt::Alignment alignment = Qt::AlignTop; switch (getJustification()) { case PDFFreeTextAnnotation::Justification::Left: alignment |= Qt::AlignLeft; break; case PDFFreeTextAnnotation::Justification::Centered: alignment |= Qt::AlignHCenter; break; case PDFFreeTextAnnotation::Justification::Right: alignment |= Qt::AlignRight; break; default: alignment |= Qt::AlignLeft; Q_ASSERT(false); break; } QTextOption option(alignment); option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); option.setUseDesignMetrics(true); painter.translate(textRect.left(), textRect.bottom()); painter.scale(1.0, -1.0); painter.drawText(QRectF(QPointF(0, 0), textRect.size()), getContents(), option); } void PDFCaretAnnotation::draw(AnnotationDrawParameters& parameters) const { QPainter& painter = *parameters.painter; painter.setCompositionMode(getCompositionMode()); parameters.boundingRectangle = getRectangle(); QRectF caretRect = getCaretRectangle(); if (caretRect.isEmpty()) { caretRect = getRectangle(); } QPointF controlPoint(caretRect.center()); controlPoint.setY(caretRect.top()); QPointF topPoint = controlPoint; topPoint.setY(caretRect.bottom()); QPainterPath path; path.moveTo(caretRect.topLeft()); path.quadTo(controlPoint, topPoint); path.quadTo(controlPoint, caretRect.topRight()); path.lineTo(caretRect.topLeft()); path.closeSubpath(); painter.fillPath(path, QBrush(getStrokeColor(), Qt::SolidPattern)); } void PDFInkAnnotation::draw(AnnotationDrawParameters& parameters) const { QPainter& painter = *parameters.painter; QPainterPath path = getInkPath(); painter.setPen(getPen()); painter.setBrush(getBrush()); painter.setCompositionMode(getCompositionMode()); QPainterPath boundingPath; QPainterPath currentPath; const int elementCount = path.elementCount(); bool hasSpline = false; for (int i = 0; i < elementCount; ++i) { QPainterPath::Element element = path.elementAt(i); switch (element.type) { case QPainterPath::MoveToElement: { // Reset the path if (!currentPath.isEmpty()) { boundingPath.addPath(currentPath); painter.drawPath(currentPath); currentPath.clear(); } currentPath.moveTo(element.x, element.y); break; } case QPainterPath::LineToElement: { const QPointF p0 = currentPath.currentPosition(); const QPointF p1(element.x, element.y); QLineF line(p0, p1); QPointF normal = line.normalVector().p2() - p0; // Jakub Melka: This computation should be clarified. We use second order Bezier curves. // We must calculate single control point. Let B(t) is bezier curve of second order. // Then second derivation is B''(t) = 2(p0 - 2*Pc + p1). Second derivation curve is its // normal. So, we calculate normal vector of the line (which has norm equal to line length), // then we get following formula: // // Pc = (p0 + p1) / 2 - B''(t) / 4 // // So, for bezier curves of second order, second derivative is constant (which is not surprising, // because second derivative of polynomial of order 2 is also a constant). Control point is then // used to paint the path. QPointF controlPoint = -normal * 0.25 + (p0 + p1) * 0.5; currentPath.quadTo(controlPoint, p1); break; } case QPainterPath::CurveToElement: case QPainterPath::CurveToDataElement: hasSpline = true; break; default: Q_ASSERT(false); break; } // Jakub Melka: If we have a spline, then we don't do anything... // Just copy the spline path. if (hasSpline) { currentPath = path; break; } } // Reset the path if (!currentPath.isEmpty()) { boundingPath.addPath(currentPath); painter.drawPath(currentPath); currentPath.clear(); } const qreal penWidth = painter.pen().widthF(); parameters.boundingRectangle = boundingPath.boundingRect(); parameters.boundingRectangle.adjust(-penWidth, -penWidth, penWidth, penWidth); } void PDFStampAnnotation::draw(AnnotationDrawParameters& parameters) const { QPainter& painter = *parameters.painter; painter.setCompositionMode(getCompositionMode()); QString text = getText(m_stamp); QColor color(Qt::red); switch (m_stamp) { case Stamp::Approved: color = Qt::green; break; case Stamp::AsIs: case Stamp::Confidential: break; case Stamp::Departmental: color = Qt::blue; break; case Stamp::Draft: break; case Stamp::Experimental: color = Qt::blue; break; case Stamp::Expired: case Stamp::Final: break; case Stamp::ForComment: color = Qt::green; break; case Stamp::ForPublicRelease: color = Qt::green; break; case Stamp::NotApproved: case Stamp::NotForPublicRelease: break; case Stamp::Sold: color = Qt::blue; break; case Stamp::TopSecret: break; default: Q_ASSERT(false); break; } color.setAlphaF(getFillOpacity()); const PDFReal textHeight = 16; QFont font("Courier New"); font.setBold(true); font.setPixelSize(textHeight); QFontMetricsF fontMetrics(font, painter.device()); const qreal textWidth = fontMetrics.width(text); const qreal rectangleWidth = textWidth + 10; const qreal rectangleHeight = textHeight * 1.2; const qreal penWidth = 2.0; QRectF rectangle = getRectangle(); rectangle.setSize(QSizeF(rectangleWidth, rectangleHeight)); QPen pen(color); pen.setWidthF(penWidth); painter.setPen(pen); painter.setBrush(Qt::NoBrush); painter.drawRoundedRect(rectangle, 5, 5, Qt::AbsoluteSize); // Draw text QPainterPath textPath; textPath.addText(0, 0, font, text); textPath = QMatrix(1, 0, 0, -1, 0, 0).map(textPath); QPointF center = textPath.boundingRect().center(); textPath.translate(rectangle.center() - center); painter.fillPath(textPath, QBrush(color, Qt::SolidPattern)); parameters.boundingRectangle = rectangle; parameters.boundingRectangle.adjust(-penWidth, -penWidth, penWidth, penWidth); } QString PDFStampAnnotation::getText(Stamp stamp) { QString text; switch (stamp) { case Stamp::Approved: text = PDFTranslationContext::tr("APPROVED"); break; case Stamp::AsIs: text = PDFTranslationContext::tr("AS IS"); break; case Stamp::Confidential: text = PDFTranslationContext::tr("CONFIDENTIAL"); break; case Stamp::Departmental: text = PDFTranslationContext::tr("DEPARTMENTAL"); break; case Stamp::Draft: text = PDFTranslationContext::tr("DRAFT"); break; case Stamp::Experimental: text = PDFTranslationContext::tr("EXPERIMENTAL"); break; case Stamp::Expired: text = PDFTranslationContext::tr("EXPIRED"); break; case Stamp::Final: text = PDFTranslationContext::tr("FINAL"); break; case Stamp::ForComment: text = PDFTranslationContext::tr("FOR COMMENT"); break; case Stamp::ForPublicRelease: text = PDFTranslationContext::tr("FOR PUBLIC RELEASE"); break; case Stamp::NotApproved: text = PDFTranslationContext::tr("NOT APPROVED"); break; case Stamp::NotForPublicRelease: text = PDFTranslationContext::tr("NOT FOR PUBLIC RELEASE"); break; case Stamp::Sold: text = PDFTranslationContext::tr("SOLD"); break; case Stamp::TopSecret: text = PDFTranslationContext::tr("TOP SECRET"); break; default: Q_ASSERT(false); break; } return text; } void PDFStampAnnotation::setStamp(const Stamp& stamp) { m_stamp = stamp; } void PDFStampAnnotation::setIntent(const StampIntent& intent) { m_intent = intent; } void PDFAnnotation::drawCharacterSymbol(QString text, PDFReal opacity, AnnotationDrawParameters& parameters) const { QColor strokeColor = QColor::fromRgbF(0.0, 0.0, 0.0, opacity); constexpr const PDFReal rectSize = 24.0; QPainter& painter = *parameters.painter; QRectF rectangle = getRectangle(); rectangle.setSize(QSizeF(rectSize, rectSize)); QFont font = QApplication::font(); font.setPixelSize(16.0); QPainterPath textPath; textPath.addText(0.0, 0.0, font, text); textPath = QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0).map(textPath); QRectF textBoundingRect = textPath.boundingRect(); QPointF offset = rectangle.center() - textBoundingRect.center(); textPath.translate(offset); painter.fillPath(textPath, QBrush(strokeColor, Qt::SolidPattern)); parameters.boundingRectangle = rectangle; } void PDFFileAttachmentAnnotation::draw(AnnotationDrawParameters& parameters) const { QString text = "?"; switch (getIcon()) { case FileAttachmentIcon::Graph: text = QString::fromUtf16(u"\U0001F4C8"); break; case FileAttachmentIcon::Paperclip: text = QString::fromUtf16(u"\U0001F4CE"); break; case FileAttachmentIcon::PushPin: text = QString::fromUtf16(u"\U0001F4CC"); break; case FileAttachmentIcon::Tag: text = QString::fromUtf16(u"\U0001F3F7"); break; default: Q_ASSERT(false); break; } parameters.painter->setCompositionMode(getCompositionMode()); drawCharacterSymbol(text, getStrokeOpacity(), parameters); } void PDFSoundAnnotation::draw(AnnotationDrawParameters& parameters) const { QString text = "?"; switch (getIcon()) { case Icon::Speaker: text = QString::fromUtf16(u"\U0001F508"); break; case Icon::Microphone: text = QString::fromUtf16(u"\U0001F3A4"); break; default: Q_ASSERT(false); break; } parameters.painter->setCompositionMode(getCompositionMode()); drawCharacterSymbol(text, getStrokeOpacity(), parameters); } const PDFAnnotationManager::PageAnnotation* PDFAnnotationManager::PageAnnotations::getPopupAnnotation(const PageAnnotation& pageAnnotation) const { const PDFMarkupAnnotation* markupAnnotation = pageAnnotation.annotation->asMarkupAnnotation(); if (markupAnnotation) { const PDFObjectReference popupAnnotation = markupAnnotation->getPopupAnnotation(); auto it = std::find_if(annotations.cbegin(), annotations.cend(), [this, popupAnnotation](const PageAnnotation& pa) { return pa.annotation->getSelfReference() == popupAnnotation; }); if (it != annotations.cend()) { return &*it; } } return nullptr; } std::vector PDFAnnotationManager::PageAnnotations::getReplies(const PageAnnotation& pageAnnotation) const { std::vector result; const PDFObjectReference reference = pageAnnotation.annotation->getSelfReference(); for (size_t i = 0, count = annotations.size(); i < count; ++i) { const PageAnnotation& currentAnnotation = annotations[i]; if (currentAnnotation.annotation->isReplyTo()) { const PDFMarkupAnnotation* markupAnnotation = currentAnnotation.annotation->asMarkupAnnotation(); Q_ASSERT(markupAnnotation); if (markupAnnotation->getInReplyTo() == reference) { result.push_back(¤tAnnotation); } } } auto comparator = [](const PageAnnotation* l, const PageAnnotation* r) { QDateTime leftDateTime = l->annotation->getLastModifiedDateTime(); QDateTime rightDateTime = r->annotation->getLastModifiedDateTime(); if (const PDFMarkupAnnotation* markupL = l->annotation->asMarkupAnnotation()) { leftDateTime = markupL->getCreationDate(); } if (const PDFMarkupAnnotation* markupR = r->annotation->asMarkupAnnotation()) { rightDateTime = markupR->getCreationDate(); } return leftDateTime < rightDateTime; }; std::sort(result.begin(), result.end(), comparator); return result; } void PDFWidgetAnnotation::draw(AnnotationDrawParameters& parameters) const { // Do not draw without form manager if (!parameters.formManager) { return; } // Do not draw without form field const PDFFormField* formField = parameters.formManager->getFormFieldForWidget(getSelfReference()); if (!formField) { return; } PDFPainterStateGuard guard(parameters.painter); parameters.painter->setCompositionMode(getCompositionMode()); const PDFFormFieldWidgetEditor* editor = parameters.formManager->getEditor(formField); if (editor && editor->isEditorDrawEnabled()) { editor->draw(parameters, true); } else { switch (formField->getFieldType()) { case PDFFormField::FieldType::Text: case PDFFormField::FieldType::Choice: { editor->draw(parameters, false); break; } case PDFFormField::FieldType::Button: { const PDFFormFieldButton* button = dynamic_cast(formField); switch (button->getButtonType()) { case PDFFormFieldButton::ButtonType::PushButton: { QRectF rectangle = getRectangle(); if (!getContents().isEmpty()) { QByteArray defaultAppearance = parameters.formManager->getForm()->getDefaultAppearance().value_or(QByteArray()); PDFAnnotationDefaultAppearance appearance = PDFAnnotationDefaultAppearance::parse(defaultAppearance); qreal fontSize = appearance.getFontSize(); if (qFuzzyIsNull(fontSize)) { fontSize = rectangle.height() * 0.6; } QFont font(appearance.getFontName()); font.setHintingPreference(QFont::PreferNoHinting); font.setPixelSize(qCeil(fontSize)); font.setStyleStrategy(QFont::ForceOutline); QFontMetrics fontMetrics(font); QPainter* painter = parameters.painter; painter->translate(rectangle.bottomLeft()); painter->scale(1.0, -1.0); painter->setFont(font); QStyleOptionButton option; option.state = QStyle::State_Enabled; option.rect = QRect(0, 0, qFloor(rectangle.width()), qFloor(rectangle.height())); option.palette = QApplication::palette(); if (parameters.key.first == PDFAppeareanceStreams::Appearance::Rollover) { option.state |= QStyle::State_MouseOver; } if (parameters.key.first == PDFAppeareanceStreams::Appearance::Down) { option.state |= QStyle::State_Sunken; } option.features = QStyleOptionButton::None; option.text = getContents(); option.fontMetrics = fontMetrics; QApplication::style()->drawControl(QStyle::CE_PushButton, &option, painter, nullptr); } else { // This is push button without text. Just mark the area as highlighted. QPainter* painter = parameters.painter; if (parameters.key.first == PDFAppeareanceStreams::Appearance::Rollover || parameters.key.first == PDFAppeareanceStreams::Appearance::Down) { switch (m_highlightMode) { case HighlightMode::Invert: { // Invert all painter->setCompositionMode(QPainter::CompositionMode_Difference); painter->fillRect(rectangle, QBrush(Qt::white, Qt::SolidPattern)); break; } case HighlightMode::Outline: { // Invert the border painter->setCompositionMode(QPainter::CompositionMode_Difference); QPen pen = getPen(); pen.setColor(Qt::white); painter->setPen(pen); painter->setBrush(Qt::NoBrush); painter->drawRect(rectangle); break; } case HighlightMode::Push: { // Draw border painter->setCompositionMode(getCompositionMode()); painter->setPen(getPen()); painter->setBrush(Qt::NoBrush); painter->drawRect(rectangle); break; } default: Q_ASSERT(false); break; } } } break; } case PDFFormFieldButton::ButtonType::RadioButton: case PDFFormFieldButton::ButtonType::CheckBox: { QRectF rectangle = getRectangle(); QPainter* painter = parameters.painter; painter->translate(rectangle.bottomLeft()); painter->scale(1.0, -1.0); QStyleOptionButton option; option.state = QStyle::State_Enabled; option.rect = QRect(0, 0, qFloor(rectangle.width()), qFloor(rectangle.height())); option.palette = QApplication::palette(); if (parameters.key.first == PDFAppeareanceStreams::Appearance::Rollover) { option.state |= QStyle::State_MouseOver; } if (parameters.key.first == PDFAppeareanceStreams::Appearance::Down) { option.state |= QStyle::State_Sunken; } if (parameters.key.second != "Off") { option.state |= QStyle::State_On; } else { option.state |= QStyle::State_Off; } option.features = QStyleOptionButton::None; option.text = QString(); QStyle::PrimitiveElement element = (button->getButtonType() == PDFFormFieldButton::ButtonType::CheckBox) ? QStyle::PE_IndicatorCheckBox : QStyle::PE_IndicatorRadioButton; QApplication::style()->drawPrimitive(element, &option, painter, nullptr); break; } default: { Q_ASSERT(false); break; } } break; } case PDFFormField::FieldType::Invalid: case PDFFormField::FieldType::Signature: break; default: { Q_ASSERT(false); break; } } } } std::vector PDFWidgetAnnotation::getDrawKeys(const PDFFormManager* formManager) const { if (!formManager) { return PDFAnnotation::getDrawKeys(formManager); } std::vector result; // Try get the form field, if we find it, then determine from form field type // the list of appearance states. const PDFFormField* formField = formManager->getFormFieldForWidget(getSelfReference()); if (!formField) { return PDFAnnotation::getDrawKeys(formManager); } switch (formField->getFieldType()) { case PDFFormField::FieldType::Invalid: break; case PDFFormField::FieldType::Button: { const PDFFormFieldButton* button = dynamic_cast(formField); switch (button->getButtonType()) { case PDFFormFieldButton::ButtonType::PushButton: { result = { PDFAppeareanceStreams::Key{ PDFAppeareanceStreams::Appearance::Normal, QByteArray() }, PDFAppeareanceStreams::Key{ PDFAppeareanceStreams::Appearance::Rollover, QByteArray() }, PDFAppeareanceStreams::Key{ PDFAppeareanceStreams::Appearance::Down, QByteArray() } }; break; } case PDFFormFieldButton::ButtonType::RadioButton: case PDFFormFieldButton::ButtonType::CheckBox: { result = getAppearanceStreams().getAppearanceKeys(); PDFAppeareanceStreams::Key offKey{ PDFAppeareanceStreams::Appearance::Normal, QByteArray() }; if (std::find(result.cbegin(), result.cend(), offKey) == result.cend()) { result.push_back(qMove(offKey)); } break; } default: { Q_ASSERT(false); break; } } break; } case PDFFormField::FieldType::Text: // Text has only default appearance break; case PDFFormField::FieldType::Choice: // Choices have always default appearance break; case PDFFormField::FieldType::Signature: // Signatures have always default appearance break; } if (result.empty()) { result = PDFAnnotation::getDrawKeys(formManager); } return result; } void PDFRedactAnnotation::draw(AnnotationDrawParameters& parameters) const { if (m_redactionRegion.isEmpty()) { // Jakub Melka: do not draw empty redact area return; } QPainter& painter = *parameters.painter; painter.setCompositionMode(getCompositionMode()); parameters.boundingRectangle = m_redactionRegion.getPath().boundingRect(); painter.setPen(getPen()); painter.setBrush(getBrush()); painter.drawPath(m_redactionRegion.getPath()); const qreal penWidth = painter.pen().widthF(); parameters.boundingRectangle.adjust(-penWidth, -penWidth, penWidth, penWidth); } QColor PDFRedactAnnotation::getFillColor() const { return getDrawColorFromAnnotationColor(getInteriorColor(), getFillOpacity()); } } // namespace pdf