From 9c97d0c7bc8f356198ea45cc71000377be95938d Mon Sep 17 00:00:00 2001 From: Jonathan Kew Date: Wed, 4 Aug 2021 12:52:38 +0000 Subject: [PATCH] Bug 1722300 - patch 4 - Implement internal destinations when generating PDF output through cairo. r=mattwoodrow Differential Revision: https://phabricator.services.mozilla.com/D120960 --- gfx/2d/2D.h | 1 + gfx/2d/DrawTargetCairo.cpp | 32 ++++++++- gfx/2d/DrawTargetCairo.h | 2 + gfx/2d/DrawTargetRecording.cpp | 5 ++ gfx/2d/DrawTargetRecording.h | 2 + gfx/2d/RecordedEvent.cpp | 2 + gfx/2d/RecordedEvent.h | 3 +- gfx/2d/RecordedEventImpl.h | 65 +++++++++++++++++- layout/generic/nsIFrame.cpp | 4 +- layout/painting/nsDisplayItemTypesList.h | 1 + layout/painting/nsDisplayList.cpp | 87 ++++++++++++++++++++---- layout/painting/nsDisplayList.h | 30 +++++++- 12 files changed, 210 insertions(+), 24 deletions(-) diff --git a/gfx/2d/2D.h b/gfx/2d/2D.h index 95eb3642e873..9e222581fbf5 100644 --- a/gfx/2d/2D.h +++ b/gfx/2d/2D.h @@ -1081,6 +1081,7 @@ class DrawTarget : public external::AtomicRefCounted { * Method to generate hyperlink in PDF output (with appropriate backend). */ virtual void Link(const char* aDestination, const Rect& aRect) {} + virtual void Destination(const char* aDestination, const Point& aPoint) {} /** * Returns a SourceSurface which is a snapshot of the current contents of the diff --git a/gfx/2d/DrawTargetCairo.cpp b/gfx/2d/DrawTargetCairo.cpp index d81bfb6c0ad0..0bf82016c1a3 100644 --- a/gfx/2d/DrawTargetCairo.cpp +++ b/gfx/2d/DrawTargetCairo.cpp @@ -678,8 +678,13 @@ void DrawTargetCairo::Link(const char* aDestination, const Rect& aRect) { cairo_user_to_device(mContext, &x, &y); cairo_user_to_device_distance(mContext, &w, &h); - nsPrintfCString attributes("rect=[%f %f %f %f] uri='%s'", x, y, w, h, - dest.get()); + nsPrintfCString attributes("rect=[%f %f %f %f] ", x, y, w, h); + if (dest[0] == '#') { + // The actual destination does not have a leading '#'. + attributes.AppendPrintf("dest='%s'", dest.get() + 1); + } else { + attributes.AppendPrintf("uri='%s'", dest.get()); + } // We generate a begin/end pair with no content in between, because we are // using the rect attribute of the begin tag to specify the link region @@ -688,6 +693,29 @@ void DrawTargetCairo::Link(const char* aDestination, const Rect& aRect) { cairo_tag_end(mContext, CAIRO_TAG_LINK); } +void DrawTargetCairo::Destination(const char* aDestination, + const Point& aPoint) { + if (!aDestination || !*aDestination) { + // No destination? Just bail out. + return; + } + + nsAutoCString dest(aDestination); + for (size_t i = dest.Length(); i > 0;) { + --i; + if (dest[i] == '\'') { + dest.ReplaceLiteral(i, 1, "\\'"); + } + } + + double x = aPoint.x, y = aPoint.y; + cairo_user_to_device(mContext, &x, &y); + + nsPrintfCString attributes("name='%s' x=%f y=%f internal", dest.get(), x, y); + cairo_tag_begin(mContext, CAIRO_TAG_DEST, attributes.get()); + cairo_tag_end(mContext, CAIRO_TAG_DEST); +} + already_AddRefed DrawTargetCairo::Snapshot() { if (!IsValid()) { gfxCriticalNote << "DrawTargetCairo::Snapshot with bad surface " diff --git a/gfx/2d/DrawTargetCairo.h b/gfx/2d/DrawTargetCairo.h index f6d5de9fe7d2..472bb308af10 100644 --- a/gfx/2d/DrawTargetCairo.h +++ b/gfx/2d/DrawTargetCairo.h @@ -61,6 +61,8 @@ class DrawTargetCairo final : public DrawTarget { } virtual void Link(const char* aDestination, const Rect& aRect) override; + virtual void Destination(const char* aDestination, + const Point& aPoint) override; virtual already_AddRefed Snapshot() override; virtual IntSize GetSize() const override; diff --git a/gfx/2d/DrawTargetRecording.cpp b/gfx/2d/DrawTargetRecording.cpp index bf6cbd82e8a4..aa064c077cc9 100644 --- a/gfx/2d/DrawTargetRecording.cpp +++ b/gfx/2d/DrawTargetRecording.cpp @@ -202,6 +202,11 @@ void DrawTargetRecording::Link(const char* aDestination, const Rect& aRect) { mRecorder->RecordEvent(RecordedLink(this, aDestination, aRect)); } +void DrawTargetRecording::Destination(const char* aDestination, + const Point& aPoint) { + mRecorder->RecordEvent(RecordedDestination(this, aDestination, aPoint)); +} + void DrawTargetRecording::FillRect(const Rect& aRect, const Pattern& aPattern, const DrawOptions& aOptions) { EnsurePatternDependenciesStored(aPattern); diff --git a/gfx/2d/DrawTargetRecording.h b/gfx/2d/DrawTargetRecording.h index 08d47ae6f2ae..939b91bb41e3 100644 --- a/gfx/2d/DrawTargetRecording.h +++ b/gfx/2d/DrawTargetRecording.h @@ -30,6 +30,8 @@ class DrawTargetRecording : public DrawTarget { virtual bool IsRecording() const override { return true; } virtual void Link(const char* aDestination, const Rect& aRect) override; + virtual void Destination(const char* aDestination, + const Point& aPoint) override; virtual already_AddRefed Snapshot() override; virtual already_AddRefed IntoLuminanceSource( diff --git a/gfx/2d/RecordedEvent.cpp b/gfx/2d/RecordedEvent.cpp index a1008d066a48..c03f952b84be 100644 --- a/gfx/2d/RecordedEvent.cpp +++ b/gfx/2d/RecordedEvent.cpp @@ -117,6 +117,8 @@ std::string RecordedEvent::GetEventName(EventType aType) { return "ExternalSourceSurfaceCreation"; case LINK: return "Link"; + case DESTINATION: + return "Destination"; default: return "Unknown"; } diff --git a/gfx/2d/RecordedEvent.h b/gfx/2d/RecordedEvent.h index 3985d970b8af..be7e4b5941af 100644 --- a/gfx/2d/RecordedEvent.h +++ b/gfx/2d/RecordedEvent.h @@ -30,7 +30,7 @@ const uint32_t kMagicInt = 0xc001feed; const uint16_t kMajorRevision = 10; // A change in minor revision means additions of new events. New streams will // not play in older players. -const uint16_t kMinorRevision = 2; +const uint16_t kMinorRevision = 3; struct ReferencePtr { ReferencePtr() : mLongPtr(0) {} @@ -392,6 +392,7 @@ class RecordedEvent { DETACHALLSNAPSHOTS, OPTIMIZESOURCESURFACE, LINK, + DESTINATION, LAST, }; diff --git a/gfx/2d/RecordedEventImpl.h b/gfx/2d/RecordedEventImpl.h index c019456d2404..af9df34ab779 100644 --- a/gfx/2d/RecordedEventImpl.h +++ b/gfx/2d/RecordedEventImpl.h @@ -1553,6 +1553,31 @@ class RecordedLink : public RecordedDrawingEvent { MOZ_IMPLICIT RecordedLink(S& aStream); }; +class RecordedDestination : public RecordedDrawingEvent { + public: + RecordedDestination(DrawTarget* aDT, const char* aDestination, + const Point& aPoint) + : RecordedDrawingEvent(DESTINATION, aDT), + mDestination(aDestination), + mPoint(aPoint) {} + + bool PlayEvent(Translator* aTranslator) const override; + template + void Record(S& aStream) const; + void OutputSimpleEventInfo(std::stringstream& aStringStream) const override; + + std::string GetName() const override { return "Destination"; } + + private: + friend class RecordedEvent; + + std::string mDestination; + Point mPoint; + + template + MOZ_IMPLICIT RecordedDestination(S& aStream); +}; + static std::string NameFromBackend(BackendType aType) { switch (aType) { case BackendType::NONE: @@ -3923,6 +3948,43 @@ inline void RecordedLink::OutputSimpleEventInfo( aStringStream << "Link [" << mDestination << " @ " << mRect << "]"; } +inline bool RecordedDestination::PlayEvent(Translator* aTranslator) const { + DrawTarget* dt = aTranslator->LookupDrawTarget(mDT); + if (!dt) { + return false; + } + dt->Destination(mDestination.c_str(), mPoint); + return true; +} + +template +void RecordedDestination::Record(S& aStream) const { + RecordedDrawingEvent::Record(aStream); + WriteElement(aStream, mPoint); + uint32_t len = mDestination.length(); + WriteElement(aStream, len); + if (len) { + aStream.write(mDestination.data(), len); + } +} + +template +RecordedDestination::RecordedDestination(S& aStream) + : RecordedDrawingEvent(DESTINATION, aStream) { + ReadElement(aStream, mPoint); + uint32_t len; + ReadElement(aStream, len); + mDestination.resize(size_t(len)); + if (len && aStream.good()) { + aStream.read(&mDestination.front(), len); + } +} + +inline void RecordedDestination::OutputSimpleEventInfo( + std::stringstream& aStringStream) const { + aStringStream << "Destination [" << mDestination << " @ " << mPoint << "]"; +} + #define FOR_EACH_EVENT(f) \ f(DRAWTARGETCREATION, RecordedDrawTargetCreation); \ f(DRAWTARGETDESTRUCTION, RecordedDrawTargetDestruction); \ @@ -3972,7 +4034,8 @@ inline void RecordedLink::OutputSimpleEventInfo( f(FLUSH, RecordedFlush); \ f(DETACHALLSNAPSHOTS, RecordedDetachAllSnapshots); \ f(OPTIMIZESOURCESURFACE, RecordedOptimizeSourceSurface); \ - f(LINK, RecordedLink); + f(LINK, RecordedLink); \ + f(DESTINATION, RecordedDestination); #define DO_WITH_EVENT_TYPE(_typeenum, _class) \ case _typeenum: { \ diff --git a/layout/generic/nsIFrame.cpp b/layout/generic/nsIFrame.cpp index 2bc1006a45f5..860ec64b3cc1 100644 --- a/layout/generic/nsIFrame.cpp +++ b/layout/generic/nsIFrame.cpp @@ -4004,8 +4004,8 @@ void nsIFrame::BuildDisplayListForChild(nsDisplayListBuilder* aBuilder, Maybe linkifier; if (StaticPrefs::print_save_as_pdf_links_enabled() && aBuilder->IsForPrinting()) { - linkifier.emplace(aBuilder, aChild); - linkifier->MaybeAppendLink(aBuilder, aChild, aLists.Content()); + linkifier.emplace(aBuilder, aChild, aLists.Content()); + linkifier->MaybeAppendLink(aBuilder, aChild); } nsIFrame* child = aChild; diff --git a/layout/painting/nsDisplayItemTypesList.h b/layout/painting/nsDisplayItemTypesList.h index bf565970e7df..f9f0d5d8de84 100644 --- a/layout/painting/nsDisplayItemTypesList.h +++ b/layout/painting/nsDisplayItemTypesList.h @@ -37,6 +37,7 @@ DECLARE_DISPLAY_ITEM_TYPE(COLUMN_RULE, TYPE_RENDERS_NO_IMAGES) DECLARE_DISPLAY_ITEM_TYPE(COMBOBOX_FOCUS, TYPE_RENDERS_NO_IMAGES) DECLARE_DISPLAY_ITEM_TYPE(COMPOSITOR_HITTEST_INFO, TYPE_RENDERS_NO_IMAGES) DECLARE_DISPLAY_ITEM_TYPE(CONTAINER, TYPE_RENDERS_NO_IMAGES | TYPE_IS_CONTAINER) +DECLARE_DISPLAY_ITEM_TYPE(DESTINATION, TYPE_RENDERS_NO_IMAGES) DECLARE_DISPLAY_ITEM_TYPE(EVENT_RECEIVER, TYPE_RENDERS_NO_IMAGES) DECLARE_DISPLAY_ITEM_TYPE(FIELDSET_BORDER_BACKGROUND, 0) DECLARE_DISPLAY_ITEM_TYPE(FILTER, TYPE_RENDERS_NO_IMAGES | TYPE_IS_CONTAINER) diff --git a/layout/painting/nsDisplayList.cpp b/layout/painting/nsDisplayList.cpp index 92c6e256e761..0a4d7e575309 100644 --- a/layout/painting/nsDisplayList.cpp +++ b/layout/painting/nsDisplayList.cpp @@ -552,13 +552,9 @@ nsRect nsDisplayListBuilder::OutOfFlowDisplayData::ComputeVisibleRectForFrame( } nsDisplayListBuilder::Linkifier::Linkifier(nsDisplayListBuilder* aBuilder, - nsIFrame* aFrame) { - // Links don't nest, so if the builder already has a destination, no need to - // check for a link element here. - if (!aBuilder->mLinkSpec.IsEmpty()) { - return; - } - + nsIFrame* aFrame, + nsDisplayList* aList) + : mList(aList) { // Find the element that we need to check for link-ness, bailing out if // we can't find one. Element* elem = Element::FromNodeOrNull(aFrame->GetContent()); @@ -566,29 +562,80 @@ nsDisplayListBuilder::Linkifier::Linkifier(nsDisplayListBuilder* aBuilder, return; } - // Check if we have actually found a link and it has a usable spec. - nsCOMPtr linkURI; - if (!elem->IsLink(getter_AddRefs(linkURI))) { + // If the element has an id and/or name attribute, generate a destination + // for possible internal linking. + auto maybeGenerateDest = [&](const nsAtom* aAttr) { + nsAutoString attrValue; + elem->GetAttr(aAttr, attrValue); + if (!attrValue.IsEmpty()) { + NS_ConvertUTF16toUTF8 dest(attrValue); + // Ensure that we only emit a given destination once, although there may + // be multiple frames associated with a given element; we'll simply use + // the first of them as the target of any links to it. + // XXX(jfkthame) This prevents emitting duplicate destinations *on the + // same page*, but does not prevent duplicates on subsequent pages, as + // each new page is handled by a new temporary DisplayListBuilder. This + // seems to be harmless in practice, though a bit wasteful of space. To + // fix, we need to maintain the set of already-seen destinations globally + // for the print job, rather than attached to the (per-page) builder. + if (aBuilder->mDestinations.EnsureInserted(dest)) { + auto* destination = MakeDisplayItem( + aBuilder, aFrame, dest.get(), aFrame->GetRect().TopLeft()); + mList->AppendToTop(destination); + } + } + }; + if (elem->HasID()) { + maybeGenerateDest(nsGkAtoms::id); + } + if (elem->HasName()) { + maybeGenerateDest(nsGkAtoms::name); + } + + // Links don't nest, so if the builder already has a destination, no need to + // check for a link element here. + if (!aBuilder->mLinkSpec.IsEmpty()) { return; } - if (NS_FAILED(linkURI->GetSpec(aBuilder->mLinkSpec)) || - aBuilder->mLinkSpec.IsEmpty()) { + + // Check if we have actually found a link. + nsCOMPtr uri; + if (!elem->IsLink(getter_AddRefs(uri))) { return; } + // Is it a local (in-page) destination? + bool hasRef, eqExRef; + nsIURI* docURI; + if (NS_SUCCEEDED(uri->GetHasRef(&hasRef)) && hasRef && + (docURI = aFrame->PresContext()->Document()->GetDocumentURI()) && + NS_SUCCEEDED(uri->EqualsExceptRef(docURI, &eqExRef)) && eqExRef) { + if (NS_FAILED(uri->GetRef(aBuilder->mLinkSpec)) || + aBuilder->mLinkSpec.IsEmpty()) { + return; + } + // Mark the link spec as being an internal destination + aBuilder->mLinkSpec.Insert('#', 0); + } else { + if (NS_FAILED(uri->GetSpec(aBuilder->mLinkSpec)) || + aBuilder->mLinkSpec.IsEmpty()) { + return; + } + } + // Record that we need to reset the builder's state on destruction. mBuilderToReset = aBuilder; } void nsDisplayListBuilder::Linkifier::MaybeAppendLink( - nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, nsDisplayList* aList) { + nsDisplayListBuilder* aBuilder, nsIFrame* aFrame) { // Note that we may generate a link here even if the constructor bailed out // without updating aBuilder->LinkSpec(), because it may have been set by // an ancestor that was associated with a link element. if (!aBuilder->mLinkSpec.IsEmpty()) { auto* link = MakeDisplayItem( aBuilder, aFrame, aBuilder->mLinkSpec.get(), aFrame->GetRect()); - aList->AppendToTop(link); + mList->AppendToTop(link); } } @@ -8323,7 +8370,9 @@ void nsDisplayTransform::Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx, } gfxContextMatrixAutoSaveRestore saveMatrix(aCtx); - Matrix4x4 trans = ShouldSkipTransform(aBuilder) ? Matrix4x4() : GetAccumulatedPreserved3DTransform(aBuilder); + Matrix4x4 trans = ShouldSkipTransform(aBuilder) + ? Matrix4x4() + : GetAccumulatedPreserved3DTransform(aBuilder); if (!IsFrameVisible(mFrame, trans)) { return; } @@ -10347,6 +10396,14 @@ void nsDisplayLink::Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) { NSRectToRect(GetPaintRect(), appPerDev)); } +void nsDisplayDestination::Paint(nsDisplayListBuilder* aBuilder, + gfxContext* aCtx) { + auto appPerDev = mFrame->PresContext()->AppUnitsPerDevPixel(); + aCtx->GetDrawTarget()->Destination( + mDestinationName.get(), + NSPointToPoint(GetPaintRect().TopLeft(), appPerDev)); +} + void nsDisplayListCollection::SerializeWithCorrectZOrder( nsDisplayList* aOutResultList, nsIContent* aContent) { // Sort PositionedDescendants() in CSS 'z-order' order. The list is already diff --git a/layout/painting/nsDisplayList.h b/layout/painting/nsDisplayList.h index e11580ed0714..483cbeed9319 100644 --- a/layout/painting/nsDisplayList.h +++ b/layout/painting/nsDisplayList.h @@ -1787,9 +1787,12 @@ class nsDisplayListBuilder { // Helper class to find what link spec (if any) to associate with a frame, // recording it in the builder, and generate the corresponding DisplayItem. + // This also takes care of generating a named destination for internal links + // if the element has an id or name attribute. class Linkifier { public: - Linkifier(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame); + Linkifier(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, + nsDisplayList* aList); ~Linkifier() { if (mBuilderToReset) { @@ -1797,11 +1800,11 @@ class nsDisplayListBuilder { } } - void MaybeAppendLink(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, - nsDisplayList* aList); + void MaybeAppendLink(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame); private: nsDisplayListBuilder* mBuilderToReset = nullptr; + nsDisplayList* mList; }; private: @@ -1984,6 +1987,7 @@ class nsDisplayListBuilder { const ActiveScrolledRoot* mFilterASR; std::unordered_set mScrollFramesToNotify; nsCString mLinkSpec; // Destination of link currently being emitted, if any. + nsTHashSet mDestinations; // Destination names emitted. bool mContainsBlendMode; bool mIsBuildingScrollbar; bool mCurrentScrollbarWillHaveLayer; @@ -7307,6 +7311,26 @@ class nsDisplayLink : public nsPaintedDisplayItem { nsRect mRect; }; +/** + * A display item to represent a destination within the document. + */ +class nsDisplayDestination : public nsPaintedDisplayItem { + public: + nsDisplayDestination(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, + const char* aDestinationName, const nsPoint& aPosition) + : nsPaintedDisplayItem(aBuilder, aFrame), + mDestinationName(aDestinationName), + mPosition(aPosition) {} + + NS_DISPLAY_DECL_NAME("Destination", TYPE_DESTINATION) + + void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override; + + private: + nsCString mDestinationName; + nsPoint mPosition; +}; + class FlattenedDisplayListIterator { public: FlattenedDisplayListIterator(nsDisplayListBuilder* aBuilder,