Bug 1896076 - patch 2 - Support passing both a local destination name and a URI when generating a hyperlink. r=gfx-reviewers,lsalzman

To take advantage of the new cairo capability, extend the link APIs to support
passing both a local destination name and a URI from the display list to the
rendering backend.

With this change, links to parts of the document that aren't included in the
print-to-pdf output will point back to the online resource, instead of just being
dead links.

(Note that this won't work the same on macOS at present, as we don't use
the cairo pdf backend there.)

Differential Revision: https://phabricator.services.mozilla.com/D211995
This commit is contained in:
Jonathan Kew 2024-05-29 17:19:38 +00:00
parent a6ab12ea02
commit b3d36fba68
8 changed files with 97 additions and 55 deletions

View file

@ -1356,7 +1356,8 @@ class DrawTarget : public external::AtomicRefCounted<DrawTarget> {
/**
* Method to generate hyperlink in PDF output (with appropriate backend).
*/
virtual void Link(const char* aDestination, const Rect& aRect) {}
virtual void Link(const char* aLocalDest, const char* aURI,
const Rect& aRect) {}
virtual void Destination(const char* aDestination, const Point& aPoint) {}
/**

View file

@ -654,39 +654,45 @@ SurfaceFormat GfxFormatForCairoSurface(cairo_surface_t* surface) {
return CairoContentToGfxFormat(cairo_surface_get_content(surface));
}
void DrawTargetCairo::Link(const char* aDestination, const Rect& aRect) {
if (!aDestination || !*aDestination) {
void DrawTargetCairo::Link(const char* aDest, const char* aURI,
const Rect& aRect) {
if ((!aURI || !*aURI) && (!aDest || !*aDest)) {
// No destination? Just bail out.
return;
}
// We need to \-escape any single-quotes in the destination string, in order
// to pass it via the attributes arg to cairo_tag_begin.
// We need to \-escape any single-quotes in the destination and URI strings,
// in order to pass them via the attributes arg to cairo_tag_begin.
//
// We also need to escape any backslashes (bug 1748077), as per doc at
// https://www.cairographics.org/manual/cairo-Tags-and-Links.html#cairo-tag-begin
//
// (Encoding of non-ASCII chars etc gets handled later by the PDF backend.)
nsAutoCString dest(aDestination);
for (size_t i = dest.Length(); i > 0;) {
--i;
if (dest[i] == '\'') {
dest.ReplaceLiteral(i, 1, "\\'");
} else if (dest[i] == '\\') {
dest.ReplaceLiteral(i, 1, "\\\\");
auto escapeForCairo = [](nsACString& aStr) {
for (size_t i = aStr.Length(); i > 0;) {
--i;
if (aStr[i] == '\'') {
aStr.ReplaceLiteral(i, 1, "\\'");
} else if (aStr[i] == '\\') {
aStr.ReplaceLiteral(i, 1, "\\\\");
}
}
}
};
double x = aRect.x, y = aRect.y, w = aRect.width, h = aRect.height;
cairo_user_to_device(mContext, &x, &y);
cairo_user_to_device_distance(mContext, &w, &h);
nsPrintfCString attributes("rect=[%f %f %f %f]", x, y, w, h);
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());
if (aDest && *aDest) {
nsAutoCString dest(aDest);
escapeForCairo(dest);
attributes.AppendPrintf(" dest='%s'", dest.get());
}
if (aURI && *aURI) {
nsAutoCString uri(aURI);
escapeForCairo(uri);
attributes.AppendPrintf(" uri='%s'", uri.get());
}
// We generate a begin/end pair with no content in between, because we are

View file

@ -59,7 +59,8 @@ class DrawTargetCairo final : public DrawTarget {
return BackendType::CAIRO;
}
virtual void Link(const char* aDestination, const Rect& aRect) override;
virtual void Link(const char* aDest, const char* aURI,
const Rect& aRect) override;
virtual void Destination(const char* aDestination,
const Point& aPoint) override;

View file

@ -235,10 +235,11 @@ DrawTargetRecording::~DrawTargetRecording() {
mRecorder->ClearDrawTarget(this);
}
void DrawTargetRecording::Link(const char* aDestination, const Rect& aRect) {
void DrawTargetRecording::Link(const char* aLocalDest, const char* aURI,
const Rect& aRect) {
MarkChanged();
RecordEventSelf(RecordedLink(aDestination, aRect));
RecordEventSelf(RecordedLink(aLocalDest, aURI, aRect));
}
void DrawTargetRecording::Destination(const char* aDestination,

View file

@ -38,7 +38,8 @@ class DrawTargetRecording final : public DrawTarget {
}
virtual bool IsRecording() const override { return true; }
virtual void Link(const char* aDestination, const Rect& aRect) override;
virtual void Link(const char* aLocalDest, const char* aURI,
const Rect& aRect) override;
virtual void Destination(const char* aDestination,
const Point& aPoint) override;

View file

@ -1763,8 +1763,11 @@ class RecordedFilterNodeSetInput
class RecordedLink : public RecordedEventDerived<RecordedLink> {
public:
RecordedLink(const char* aDestination, const Rect& aRect)
: RecordedEventDerived(LINK), mDestination(aDestination), mRect(aRect) {}
RecordedLink(const char* aLocalDest, const char* aURI, const Rect& aRect)
: RecordedEventDerived(LINK),
mLocalDest(aLocalDest),
mURI(aURI),
mRect(aRect) {}
bool PlayEvent(Translator* aTranslator) const override;
template <class S>
@ -1776,7 +1779,8 @@ class RecordedLink : public RecordedEventDerived<RecordedLink> {
private:
friend class RecordedEvent;
std::string mDestination;
std::string mLocalDest;
std::string mURI;
Rect mRect;
template <class S>
@ -4374,17 +4378,22 @@ inline bool RecordedLink::PlayEvent(Translator* aTranslator) const {
if (!dt) {
return false;
}
dt->Link(mDestination.c_str(), mRect);
dt->Link(mLocalDest.c_str(), mURI.c_str(), mRect);
return true;
}
template <class S>
void RecordedLink::Record(S& aStream) const {
WriteElement(aStream, mRect);
uint32_t len = mDestination.length();
uint32_t len = mLocalDest.length();
WriteElement(aStream, len);
if (len) {
aStream.write(mDestination.data(), len);
aStream.write(mLocalDest.data(), len);
}
len = mURI.length();
WriteElement(aStream, len);
if (len) {
aStream.write(mURI.data(), len);
}
}
@ -4393,15 +4402,25 @@ RecordedLink::RecordedLink(S& aStream) : RecordedEventDerived(LINK) {
ReadElement(aStream, mRect);
uint32_t len;
ReadElement(aStream, len);
mDestination.resize(size_t(len));
mLocalDest.resize(size_t(len));
if (len && aStream.good()) {
aStream.read(&mDestination.front(), len);
aStream.read(&mLocalDest.front(), len);
}
ReadElement(aStream, len);
mURI.resize(size_t(len));
if (len && aStream.good()) {
aStream.read(&mURI.front(), len);
}
}
inline void RecordedLink::OutputSimpleEventInfo(
std::stringstream& aStringStream) const {
aStringStream << "Link [" << mDestination << " @ " << mRect << "]";
if (mLocalDest.empty()) {
aStringStream << "Link [" << mURI << " @ " << mRect << "]";
} else {
aStringStream << "Link [" << mLocalDest << " / " << mURI << " @ " << mRect
<< "]";
}
}
inline bool RecordedDestination::PlayEvent(Translator* aTranslator) const {

View file

@ -615,7 +615,7 @@ nsDisplayListBuilder::Linkifier::Linkifier(nsDisplayListBuilder* aBuilder,
// 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()) {
if (!aBuilder->mLinkURI.IsEmpty() || !aBuilder->mLinkDest.IsEmpty()) {
return;
}
@ -629,29 +629,35 @@ nsDisplayListBuilder::Linkifier::Linkifier(nsDisplayListBuilder* aBuilder,
return;
}
// Is it a local (in-page) destination?
// Is it potentially a local (in-document) destination?
bool hasRef, eqExRef;
nsIURI* docURI;
if (StaticPrefs::print_save_as_pdf_internal_destinations_enabled() &&
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;
// Try to get a local destination name. If this fails, we'll leave the
// mLinkDest string empty, but still try to set mLinkURI below.
if (NS_FAILED(uri->GetRef(aBuilder->mLinkDest))) {
aBuilder->mLinkDest.Truncate();
}
// The destination name is simply a string; we don't want URL-escaping
// applied to it.
NS_UnescapeURL(aBuilder->mLinkSpec);
// 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;
if (!aBuilder->mLinkDest.IsEmpty()) {
NS_UnescapeURL(aBuilder->mLinkDest);
}
}
if (NS_FAILED(uri->GetSpec(aBuilder->mLinkURI))) {
aBuilder->mLinkURI.Truncate();
}
// If we didn't get either kind of destination, we won't try to linkify at
// this level.
if (aBuilder->mLinkDest.IsEmpty() && aBuilder->mLinkURI.IsEmpty()) {
return;
}
// Record that we need to reset the builder's state on destruction.
mBuilderToReset = aBuilder;
}
@ -659,11 +665,12 @@ nsDisplayListBuilder::Linkifier::Linkifier(nsDisplayListBuilder* aBuilder,
void nsDisplayListBuilder::Linkifier::MaybeAppendLink(
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
// without updating aBuilder->mLinkURI/Dest, because it may have been set by
// an ancestor that was associated with a link element.
if (!aBuilder->mLinkSpec.IsEmpty()) {
if (!aBuilder->mLinkURI.IsEmpty() || !aBuilder->mLinkDest.IsEmpty()) {
auto* link = MakeDisplayItem<nsDisplayLink>(
aBuilder, aFrame, aBuilder->mLinkSpec.get(), aFrame->GetRect());
aBuilder, aFrame, aBuilder->mLinkDest.get(), aBuilder->mLinkURI.get(),
aFrame->GetRect());
mList->AppendToTop(link);
}
}
@ -6351,8 +6358,8 @@ static bool ShouldUsePartialPrerender(const nsIFrame* aFrame) {
/* static */
auto nsDisplayTransform::ShouldPrerenderTransformedContent(
nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, nsRect* aDirtyRect)
-> PrerenderInfo {
nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
nsRect* aDirtyRect) -> PrerenderInfo {
PrerenderInfo result;
// If we are in a preserve-3d tree, and we've disallowed async animations, we
// return No prerender decision directly.
@ -8531,7 +8538,8 @@ bool nsDisplayForeignObject::CreateWebRenderCommands(
void nsDisplayLink::Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) {
auto appPerDev = mFrame->PresContext()->AppUnitsPerDevPixel();
aCtx->GetDrawTarget()->Link(
mLinkSpec.get(), NSRectToRect(GetPaintRect(aBuilder, aCtx), appPerDev));
mLinkURI.get(), mLinkDest.get(),
NSRectToRect(GetPaintRect(aBuilder, aCtx), appPerDev));
}
void nsDisplayDestination::Paint(nsDisplayListBuilder* aBuilder,

View file

@ -1656,7 +1656,8 @@ class nsDisplayListBuilder {
~Linkifier() {
if (mBuilderToReset) {
mBuilderToReset->mLinkSpec.Truncate(0);
mBuilderToReset->mLinkURI.Truncate(0);
mBuilderToReset->mLinkDest.Truncate(0);
}
}
@ -1786,7 +1787,8 @@ class nsDisplayListBuilder {
// When we are inside a filter, the current ASR at the time we entered the
// filter. Otherwise nullptr.
const ActiveScrolledRoot* mFilterASR;
nsCString mLinkSpec; // Destination of link currently being emitted, if any.
nsCString mLinkURI; // URI of link currently being emitted, if any.
nsCString mLinkDest; // Local destination name of link, if any.
// Optimized versions for non-retained display list.
LayoutDeviceIntRegion mWindowDraggingRegion;
@ -6713,9 +6715,11 @@ class nsDisplayForeignObject : public nsDisplayWrapList {
class nsDisplayLink : public nsPaintedDisplayItem {
public:
nsDisplayLink(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
const char* aLinkSpec, const nsRect& aRect)
const char* aLinkURI, const char* aLinkDest,
const nsRect& aRect)
: nsPaintedDisplayItem(aBuilder, aFrame),
mLinkSpec(aLinkSpec),
mLinkURI(aLinkURI),
mLinkDest(aLinkDest),
mRect(aRect) {}
NS_DISPLAY_DECL_NAME("Link", TYPE_LINK)
@ -6723,7 +6727,8 @@ class nsDisplayLink : public nsPaintedDisplayItem {
void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override;
private:
nsCString mLinkSpec;
nsCString mLinkURI;
nsCString mLinkDest;
nsRect mRect;
};