diff --git a/src/core/annotation.js b/src/core/annotation.js index ba2237453..f4932c195 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -3833,6 +3833,10 @@ class LinkAnnotation extends Annotation { docAttachments: annotationGlobals.attachments, }); } + + get overlaysTextContent() { + return true; + } } class PopupAnnotation extends Annotation { diff --git a/src/core/intersector.js b/src/core/intersector.js index e50f86b25..acd54f2f6 100644 --- a/src/core/intersector.js +++ b/src/core/intersector.js @@ -24,7 +24,7 @@ class SingleIntersector { #maxY = -Infinity; - #quadPoints; + #quadPoints = null; #text = []; @@ -36,7 +36,13 @@ class SingleIntersector { constructor(annotation) { this.#annotation = annotation; - const quadPoints = (this.#quadPoints = annotation.data.quadPoints); + const quadPoints = annotation.data.quadPoints; + if (!quadPoints) { + // If there are no quad points, we use the rectangle to determine the + // bounds of the annotation. + [this.#minX, this.#minY, this.#maxX, this.#maxY] = annotation.data.rect; + return; + } for (let i = 0, ii = quadPoints.length; i < ii; i += 8) { this.#minX = Math.min(this.#minX, quadPoints[i]); @@ -44,6 +50,9 @@ class SingleIntersector { this.#minY = Math.min(this.#minY, quadPoints[i + 5]); this.#maxY = Math.max(this.#maxY, quadPoints[i + 1]); } + if (quadPoints.length > 8) { + this.#quadPoints = quadPoints; + } } overlaps(other) { @@ -73,7 +82,7 @@ class SingleIntersector { } const quadPoints = this.#quadPoints; - if (quadPoints.length === 8) { + if (!quadPoints) { // We've only one quad, so if we intersect min/max bounds then we // intersect the quad. return true; @@ -150,7 +159,7 @@ class Intersector { constructor(annotations) { for (const annotation of annotations) { - if (!annotation.data.quadPoints) { + if (!annotation.data.quadPoints && !annotation.data.rect) { continue; } const intersector = new SingleIntersector(annotation); diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index b28d11f18..8ff424df7 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -276,7 +276,10 @@ class AnnotationElement { const container = document.createElement("section"); container.setAttribute("data-annotation-id", data.id); - if (!(this instanceof WidgetAnnotationElement)) { + if ( + !(this instanceof WidgetAnnotationElement) && + !(this instanceof LinkAnnotationElement) + ) { container.tabIndex = 0; } const { style } = container; @@ -797,16 +800,21 @@ class LinkAnnotationElement extends AnnotationElement { linkService.addLinkAttributes(link, data.url, data.newWindow); isBound = true; } else if (data.action) { - this._bindNamedAction(link, data.action); + this._bindNamedAction(link, data.action, data.overlaidText); isBound = true; } else if (data.attachment) { - this.#bindAttachment(link, data.attachment, data.attachmentDest); + this.#bindAttachment( + link, + data.attachment, + data.overlaidText, + data.attachmentDest + ); isBound = true; } else if (data.setOCGState) { - this.#bindSetOCGState(link, data.setOCGState); + this.#bindSetOCGState(link, data.setOCGState, data.overlaidText); isBound = true; } else if (data.dest) { - this._bindLink(link, data.dest); + this._bindLink(link, data.dest, data.overlaidText); isBound = true; } else { if ( @@ -848,9 +856,10 @@ class LinkAnnotationElement extends AnnotationElement { * @private * @param {Object} link * @param {Object} destination + * @param {string} [overlaidText] * @memberof LinkAnnotationElement */ - _bindLink(link, destination) { + _bindLink(link, destination, overlaidText = "") { link.href = this.linkService.getDestinationHash(destination); link.onclick = () => { if (destination) { @@ -861,6 +870,9 @@ class LinkAnnotationElement extends AnnotationElement { if (destination || destination === /* isTooltipOnly = */ "") { this.#setInternalLink(); } + if (overlaidText) { + link.title = overlaidText; + } } /** @@ -869,14 +881,18 @@ class LinkAnnotationElement extends AnnotationElement { * @private * @param {Object} link * @param {Object} action + * @param {string} [overlaidText] * @memberof LinkAnnotationElement */ - _bindNamedAction(link, action) { + _bindNamedAction(link, action, overlaidText = "") { link.href = this.linkService.getAnchorUrl(""); link.onclick = () => { this.linkService.executeNamedAction(action); return false; }; + if (overlaidText) { + link.title = overlaidText; + } this.#setInternalLink(); } @@ -884,12 +900,15 @@ class LinkAnnotationElement extends AnnotationElement { * Bind attachments to the link element. * @param {Object} link * @param {Object} attachment - * @param {str} [dest] + * @param {string} [overlaidText] + * @param {string} [dest] */ - #bindAttachment(link, attachment, dest = null) { + #bindAttachment(link, attachment, overlaidText = "", dest = null) { link.href = this.linkService.getAnchorUrl(""); if (attachment.description) { link.title = attachment.description; + } else if (overlaidText) { + link.title = overlaidText; } link.onclick = () => { this.downloadManager?.openOrDownloadData( @@ -906,13 +925,17 @@ class LinkAnnotationElement extends AnnotationElement { * Bind SetOCGState actions to the link element. * @param {Object} link * @param {Object} action + * @param {string} [overlaidText] */ - #bindSetOCGState(link, action) { + #bindSetOCGState(link, action, overlaidText = "") { link.href = this.linkService.getAnchorUrl(""); link.onclick = () => { this.linkService.executeSetOCGState(action); return false; }; + if (overlaidText) { + link.title = overlaidText; + } this.#setInternalLink(); } @@ -947,6 +970,9 @@ class LinkAnnotationElement extends AnnotationElement { return false; }; } + if (data.overlaidText) { + link.title = data.overlaidText; + } if (!link.onclick) { link.onclick = () => false; diff --git a/test/integration/annotation_spec.mjs b/test/integration/annotation_spec.mjs index e1c68c09b..1ab1ee5ac 100644 --- a/test/integration/annotation_spec.mjs +++ b/test/integration/annotation_spec.mjs @@ -247,6 +247,51 @@ describe("Text widget", () => { }); }); +describe("Link annotations with internal destinations", () => { + describe("bug1708041.pdf", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "bug1708041.pdf", + ".page[data-page-number='1'] .annotationLayer" + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must click on a link and check if it navigates to the correct page", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const pageOneSelector = ".page[data-page-number='1']"; + const linkSelector = `${pageOneSelector} #pdfjs_internal_id_42R`; + await page.waitForSelector(linkSelector); + const linkTitle = await page.$eval(linkSelector, el => el.title); + expect(linkTitle) + .withContext(`In ${browserName}`) + .toEqual("Go to the last page"); + await page.click(linkSelector); + const pageSixTextLayerSelector = + ".page[data-page-number='6'] .textLayer"; + await page.waitForSelector(pageSixTextLayerSelector, { + visible: true, + }); + await page.waitForFunction( + sel => { + const textLayer = document.querySelector(sel); + return document.activeElement === textLayer; + }, + {}, + pageSixTextLayerSelector + ); + }) + ); + }); + }); +}); + describe("Annotation and storage", () => { describe("issue14023.pdf", () => { let pages; diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 852ffe8d0..2337dbcaf 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -734,3 +734,4 @@ !issue20062.pdf !issue20102.pdf !issue20065.pdf +!bug1708041.pdf diff --git a/test/pdfs/bug1708041.pdf b/test/pdfs/bug1708041.pdf new file mode 100644 index 000000000..bd3d68c66 Binary files /dev/null and b/test/pdfs/bug1708041.pdf differ diff --git a/web/pdf_link_service.js b/web/pdf_link_service.js index e22243639..a2cb46245 100644 --- a/web/pdf_link_service.js +++ b/web/pdf_link_service.js @@ -192,6 +192,18 @@ class PDFLinkService { destArray: explicitDest, ignoreDestinationZoom: this._ignoreDestinationZoom, }); + + const ac = new AbortController(); + this.eventBus._on( + "textlayerrendered", + evt => { + if (evt.pageNumber === pageNumber) { + evt.source.textLayer.div.focus(); + ac.abort(); + } + }, + { signal: ac.signal } + ); } /**