forked from mirrors/gecko-dev
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:
parent
a6ab12ea02
commit
b3d36fba68
8 changed files with 97 additions and 55 deletions
|
|
@ -1356,7 +1356,8 @@ class DrawTarget : public external::AtomicRefCounted<DrawTarget> {
|
||||||
/**
|
/**
|
||||||
* Method to generate hyperlink in PDF output (with appropriate backend).
|
* 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) {}
|
virtual void Destination(const char* aDestination, const Point& aPoint) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -654,39 +654,45 @@ SurfaceFormat GfxFormatForCairoSurface(cairo_surface_t* surface) {
|
||||||
return CairoContentToGfxFormat(cairo_surface_get_content(surface));
|
return CairoContentToGfxFormat(cairo_surface_get_content(surface));
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawTargetCairo::Link(const char* aDestination, const Rect& aRect) {
|
void DrawTargetCairo::Link(const char* aDest, const char* aURI,
|
||||||
if (!aDestination || !*aDestination) {
|
const Rect& aRect) {
|
||||||
|
if ((!aURI || !*aURI) && (!aDest || !*aDest)) {
|
||||||
// No destination? Just bail out.
|
// No destination? Just bail out.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to \-escape any single-quotes in the destination string, in order
|
// We need to \-escape any single-quotes in the destination and URI strings,
|
||||||
// to pass it via the attributes arg to cairo_tag_begin.
|
// 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
|
// 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
|
// 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.)
|
// (Encoding of non-ASCII chars etc gets handled later by the PDF backend.)
|
||||||
nsAutoCString dest(aDestination);
|
auto escapeForCairo = [](nsACString& aStr) {
|
||||||
for (size_t i = dest.Length(); i > 0;) {
|
for (size_t i = aStr.Length(); i > 0;) {
|
||||||
--i;
|
--i;
|
||||||
if (dest[i] == '\'') {
|
if (aStr[i] == '\'') {
|
||||||
dest.ReplaceLiteral(i, 1, "\\'");
|
aStr.ReplaceLiteral(i, 1, "\\'");
|
||||||
} else if (dest[i] == '\\') {
|
} else if (aStr[i] == '\\') {
|
||||||
dest.ReplaceLiteral(i, 1, "\\\\");
|
aStr.ReplaceLiteral(i, 1, "\\\\");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
double x = aRect.x, y = aRect.y, w = aRect.width, h = aRect.height;
|
double x = aRect.x, y = aRect.y, w = aRect.width, h = aRect.height;
|
||||||
cairo_user_to_device(mContext, &x, &y);
|
cairo_user_to_device(mContext, &x, &y);
|
||||||
cairo_user_to_device_distance(mContext, &w, &h);
|
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 '#'.
|
if (aDest && *aDest) {
|
||||||
attributes.AppendPrintf("dest='%s'", dest.get() + 1);
|
nsAutoCString dest(aDest);
|
||||||
} else {
|
escapeForCairo(dest);
|
||||||
attributes.AppendPrintf("uri='%s'", dest.get());
|
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
|
// We generate a begin/end pair with no content in between, because we are
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,8 @@ class DrawTargetCairo final : public DrawTarget {
|
||||||
return BackendType::CAIRO;
|
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,
|
virtual void Destination(const char* aDestination,
|
||||||
const Point& aPoint) override;
|
const Point& aPoint) override;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -235,10 +235,11 @@ DrawTargetRecording::~DrawTargetRecording() {
|
||||||
mRecorder->ClearDrawTarget(this);
|
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();
|
MarkChanged();
|
||||||
|
|
||||||
RecordEventSelf(RecordedLink(aDestination, aRect));
|
RecordEventSelf(RecordedLink(aLocalDest, aURI, aRect));
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawTargetRecording::Destination(const char* aDestination,
|
void DrawTargetRecording::Destination(const char* aDestination,
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@ class DrawTargetRecording final : public DrawTarget {
|
||||||
}
|
}
|
||||||
virtual bool IsRecording() const override { return true; }
|
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,
|
virtual void Destination(const char* aDestination,
|
||||||
const Point& aPoint) override;
|
const Point& aPoint) override;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1763,8 +1763,11 @@ class RecordedFilterNodeSetInput
|
||||||
|
|
||||||
class RecordedLink : public RecordedEventDerived<RecordedLink> {
|
class RecordedLink : public RecordedEventDerived<RecordedLink> {
|
||||||
public:
|
public:
|
||||||
RecordedLink(const char* aDestination, const Rect& aRect)
|
RecordedLink(const char* aLocalDest, const char* aURI, const Rect& aRect)
|
||||||
: RecordedEventDerived(LINK), mDestination(aDestination), mRect(aRect) {}
|
: RecordedEventDerived(LINK),
|
||||||
|
mLocalDest(aLocalDest),
|
||||||
|
mURI(aURI),
|
||||||
|
mRect(aRect) {}
|
||||||
|
|
||||||
bool PlayEvent(Translator* aTranslator) const override;
|
bool PlayEvent(Translator* aTranslator) const override;
|
||||||
template <class S>
|
template <class S>
|
||||||
|
|
@ -1776,7 +1779,8 @@ class RecordedLink : public RecordedEventDerived<RecordedLink> {
|
||||||
private:
|
private:
|
||||||
friend class RecordedEvent;
|
friend class RecordedEvent;
|
||||||
|
|
||||||
std::string mDestination;
|
std::string mLocalDest;
|
||||||
|
std::string mURI;
|
||||||
Rect mRect;
|
Rect mRect;
|
||||||
|
|
||||||
template <class S>
|
template <class S>
|
||||||
|
|
@ -4374,17 +4378,22 @@ inline bool RecordedLink::PlayEvent(Translator* aTranslator) const {
|
||||||
if (!dt) {
|
if (!dt) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
dt->Link(mDestination.c_str(), mRect);
|
dt->Link(mLocalDest.c_str(), mURI.c_str(), mRect);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <class S>
|
template <class S>
|
||||||
void RecordedLink::Record(S& aStream) const {
|
void RecordedLink::Record(S& aStream) const {
|
||||||
WriteElement(aStream, mRect);
|
WriteElement(aStream, mRect);
|
||||||
uint32_t len = mDestination.length();
|
uint32_t len = mLocalDest.length();
|
||||||
WriteElement(aStream, len);
|
WriteElement(aStream, len);
|
||||||
if (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);
|
ReadElement(aStream, mRect);
|
||||||
uint32_t len;
|
uint32_t len;
|
||||||
ReadElement(aStream, len);
|
ReadElement(aStream, len);
|
||||||
mDestination.resize(size_t(len));
|
mLocalDest.resize(size_t(len));
|
||||||
if (len && aStream.good()) {
|
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(
|
inline void RecordedLink::OutputSimpleEventInfo(
|
||||||
std::stringstream& aStringStream) const {
|
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 {
|
inline bool RecordedDestination::PlayEvent(Translator* aTranslator) const {
|
||||||
|
|
|
||||||
|
|
@ -615,7 +615,7 @@ nsDisplayListBuilder::Linkifier::Linkifier(nsDisplayListBuilder* aBuilder,
|
||||||
|
|
||||||
// Links don't nest, so if the builder already has a destination, no need to
|
// Links don't nest, so if the builder already has a destination, no need to
|
||||||
// check for a link element here.
|
// check for a link element here.
|
||||||
if (!aBuilder->mLinkSpec.IsEmpty()) {
|
if (!aBuilder->mLinkURI.IsEmpty() || !aBuilder->mLinkDest.IsEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -629,29 +629,35 @@ nsDisplayListBuilder::Linkifier::Linkifier(nsDisplayListBuilder* aBuilder,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is it a local (in-page) destination?
|
// Is it potentially a local (in-document) destination?
|
||||||
bool hasRef, eqExRef;
|
bool hasRef, eqExRef;
|
||||||
nsIURI* docURI;
|
nsIURI* docURI;
|
||||||
if (StaticPrefs::print_save_as_pdf_internal_destinations_enabled() &&
|
if (StaticPrefs::print_save_as_pdf_internal_destinations_enabled() &&
|
||||||
NS_SUCCEEDED(uri->GetHasRef(&hasRef)) && hasRef &&
|
NS_SUCCEEDED(uri->GetHasRef(&hasRef)) && hasRef &&
|
||||||
(docURI = aFrame->PresContext()->Document()->GetDocumentURI()) &&
|
(docURI = aFrame->PresContext()->Document()->GetDocumentURI()) &&
|
||||||
NS_SUCCEEDED(uri->EqualsExceptRef(docURI, &eqExRef)) && eqExRef) {
|
NS_SUCCEEDED(uri->EqualsExceptRef(docURI, &eqExRef)) && eqExRef) {
|
||||||
if (NS_FAILED(uri->GetRef(aBuilder->mLinkSpec)) ||
|
// Try to get a local destination name. If this fails, we'll leave the
|
||||||
aBuilder->mLinkSpec.IsEmpty()) {
|
// mLinkDest string empty, but still try to set mLinkURI below.
|
||||||
return;
|
if (NS_FAILED(uri->GetRef(aBuilder->mLinkDest))) {
|
||||||
|
aBuilder->mLinkDest.Truncate();
|
||||||
}
|
}
|
||||||
// The destination name is simply a string; we don't want URL-escaping
|
// The destination name is simply a string; we don't want URL-escaping
|
||||||
// applied to it.
|
// applied to it.
|
||||||
NS_UnescapeURL(aBuilder->mLinkSpec);
|
if (!aBuilder->mLinkDest.IsEmpty()) {
|
||||||
// Mark the link spec as being an internal destination
|
NS_UnescapeURL(aBuilder->mLinkDest);
|
||||||
aBuilder->mLinkSpec.Insert('#', 0);
|
|
||||||
} else {
|
|
||||||
if (NS_FAILED(uri->GetSpec(aBuilder->mLinkSpec)) ||
|
|
||||||
aBuilder->mLinkSpec.IsEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
// Record that we need to reset the builder's state on destruction.
|
||||||
mBuilderToReset = aBuilder;
|
mBuilderToReset = aBuilder;
|
||||||
}
|
}
|
||||||
|
|
@ -659,11 +665,12 @@ nsDisplayListBuilder::Linkifier::Linkifier(nsDisplayListBuilder* aBuilder,
|
||||||
void nsDisplayListBuilder::Linkifier::MaybeAppendLink(
|
void nsDisplayListBuilder::Linkifier::MaybeAppendLink(
|
||||||
nsDisplayListBuilder* aBuilder, nsIFrame* aFrame) {
|
nsDisplayListBuilder* aBuilder, nsIFrame* aFrame) {
|
||||||
// Note that we may generate a link here even if the constructor bailed out
|
// 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.
|
// an ancestor that was associated with a link element.
|
||||||
if (!aBuilder->mLinkSpec.IsEmpty()) {
|
if (!aBuilder->mLinkURI.IsEmpty() || !aBuilder->mLinkDest.IsEmpty()) {
|
||||||
auto* link = MakeDisplayItem<nsDisplayLink>(
|
auto* link = MakeDisplayItem<nsDisplayLink>(
|
||||||
aBuilder, aFrame, aBuilder->mLinkSpec.get(), aFrame->GetRect());
|
aBuilder, aFrame, aBuilder->mLinkDest.get(), aBuilder->mLinkURI.get(),
|
||||||
|
aFrame->GetRect());
|
||||||
mList->AppendToTop(link);
|
mList->AppendToTop(link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6351,8 +6358,8 @@ static bool ShouldUsePartialPrerender(const nsIFrame* aFrame) {
|
||||||
|
|
||||||
/* static */
|
/* static */
|
||||||
auto nsDisplayTransform::ShouldPrerenderTransformedContent(
|
auto nsDisplayTransform::ShouldPrerenderTransformedContent(
|
||||||
nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, nsRect* aDirtyRect)
|
nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
|
||||||
-> PrerenderInfo {
|
nsRect* aDirtyRect) -> PrerenderInfo {
|
||||||
PrerenderInfo result;
|
PrerenderInfo result;
|
||||||
// If we are in a preserve-3d tree, and we've disallowed async animations, we
|
// If we are in a preserve-3d tree, and we've disallowed async animations, we
|
||||||
// return No prerender decision directly.
|
// return No prerender decision directly.
|
||||||
|
|
@ -8531,7 +8538,8 @@ bool nsDisplayForeignObject::CreateWebRenderCommands(
|
||||||
void nsDisplayLink::Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) {
|
void nsDisplayLink::Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) {
|
||||||
auto appPerDev = mFrame->PresContext()->AppUnitsPerDevPixel();
|
auto appPerDev = mFrame->PresContext()->AppUnitsPerDevPixel();
|
||||||
aCtx->GetDrawTarget()->Link(
|
aCtx->GetDrawTarget()->Link(
|
||||||
mLinkSpec.get(), NSRectToRect(GetPaintRect(aBuilder, aCtx), appPerDev));
|
mLinkURI.get(), mLinkDest.get(),
|
||||||
|
NSRectToRect(GetPaintRect(aBuilder, aCtx), appPerDev));
|
||||||
}
|
}
|
||||||
|
|
||||||
void nsDisplayDestination::Paint(nsDisplayListBuilder* aBuilder,
|
void nsDisplayDestination::Paint(nsDisplayListBuilder* aBuilder,
|
||||||
|
|
|
||||||
|
|
@ -1656,7 +1656,8 @@ class nsDisplayListBuilder {
|
||||||
|
|
||||||
~Linkifier() {
|
~Linkifier() {
|
||||||
if (mBuilderToReset) {
|
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
|
// When we are inside a filter, the current ASR at the time we entered the
|
||||||
// filter. Otherwise nullptr.
|
// filter. Otherwise nullptr.
|
||||||
const ActiveScrolledRoot* mFilterASR;
|
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.
|
// Optimized versions for non-retained display list.
|
||||||
LayoutDeviceIntRegion mWindowDraggingRegion;
|
LayoutDeviceIntRegion mWindowDraggingRegion;
|
||||||
|
|
@ -6713,9 +6715,11 @@ class nsDisplayForeignObject : public nsDisplayWrapList {
|
||||||
class nsDisplayLink : public nsPaintedDisplayItem {
|
class nsDisplayLink : public nsPaintedDisplayItem {
|
||||||
public:
|
public:
|
||||||
nsDisplayLink(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
|
nsDisplayLink(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
|
||||||
const char* aLinkSpec, const nsRect& aRect)
|
const char* aLinkURI, const char* aLinkDest,
|
||||||
|
const nsRect& aRect)
|
||||||
: nsPaintedDisplayItem(aBuilder, aFrame),
|
: nsPaintedDisplayItem(aBuilder, aFrame),
|
||||||
mLinkSpec(aLinkSpec),
|
mLinkURI(aLinkURI),
|
||||||
|
mLinkDest(aLinkDest),
|
||||||
mRect(aRect) {}
|
mRect(aRect) {}
|
||||||
|
|
||||||
NS_DISPLAY_DECL_NAME("Link", TYPE_LINK)
|
NS_DISPLAY_DECL_NAME("Link", TYPE_LINK)
|
||||||
|
|
@ -6723,7 +6727,8 @@ class nsDisplayLink : public nsPaintedDisplayItem {
|
||||||
void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override;
|
void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
nsCString mLinkSpec;
|
nsCString mLinkURI;
|
||||||
|
nsCString mLinkDest;
|
||||||
nsRect mRect;
|
nsRect mRect;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue