Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable automatic URL linking (bug 1019475) #19110

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions test/pdfs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -692,3 +692,4 @@
!issue19182.pdf
!issue18911.pdf
!issue19207.pdf
!link.pdf
Binary file added test/pdfs/link.pdf
Binary file not shown.
23 changes: 21 additions & 2 deletions web/annotation_layer_builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */

import { AnnotationLayer } from "pdfjs-lib";
import { AnnotationLayer, Util } from "pdfjs-lib";
import { PresentationModeState } from "./ui_utils.js";

/**
Expand Down Expand Up @@ -97,7 +97,7 @@ class AnnotationLayerBuilder {
* @returns {Promise<void>} A promise that is resolved when rendering of the
* annotations is complete.
*/
async render(viewport, options, intent = "display") {
async render(viewport, options, intent = "display", linkAnnotations) {
if (this.div) {
if (this._cancelled || !this.annotationLayer) {
return;
Expand All @@ -119,6 +119,25 @@ class AnnotationLayerBuilder {
return;
}

const uniqueLinks = linkAnnotations.filter(link => {
for (const annotation of annotations) {
function area(rect) {
return Math.abs(rect[2] - rect[0]) * Math.abs(rect[3] - rect[1]);
}
const intersect = Util.intersect(annotation.rect, link.rect); // Find the intersection between the annotation and the link.
if (
annotation.subtype === "Link" &&
annotation.url === link.url &&
intersect !== null &&
area(intersect) / area(link.rect) > 0.5 // If the overlap is more than 50%.
) {
return false;
}
}
return true;
});
annotations.push(...uniqueLinks);

// Create an annotation layer div and render the annotations
// if there is at least one annotation.
const div = (this.div = document.createElement("div"));
Expand Down
1 change: 1 addition & 0 deletions web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ const PDFViewerApplication = {
abortSignal: this._globalAbortController.signal,
enableHWA,
supportsPinchToZoom: this.supportsPinchToZoom,
enableAutolinking: AppOptions.get("enableAutolinking"),
});
this.pdfViewer = pdfViewer;

Expand Down
6 changes: 6 additions & 0 deletions web/app_options.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@ const defaultOptions = {
value: false,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
enableAutolinking: {
// TODO: remove it when unnecessary.
/** @type {boolean} */
value: false,
kind: OptionKind.VIEWER,
},
externalLinkRel: {
/** @type {string} */
value: "noopener noreferrer nofollow",
Expand Down
94 changes: 94 additions & 0 deletions web/autolinker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { createValidAbsoluteUrl, Util } from "../src/shared/util.js";
import { getOriginalIndex, normalize } from "./pdf_find_controller.js";

class Autolinker {
static #urlRegex =
/\b(?:https?:\/\/|mailto:|www.)(?:[[\S--\[]--\p{P}]|\/|[\p{P}--\[]+[[\S--\[]--\p{P}])+/gmv; // Regex can be tested and verified at https://regex101.com/r/zuMeQX/3.
Dismissed Show dismissed Hide dismissed

static #addLinkAnnotations(url, index, length, pdfPageView) {
// TODO refactor out the logic for a single match from this function
const convertedMatch = pdfPageView._textHighlighter._convertMatches(
[index],
[length]
)[0];

const range = new Range();
range.setStart(
pdfPageView._textHighlighter.textDivs[convertedMatch.begin.divIdx]
.firstChild,
convertedMatch.begin.offset
);
range.setEnd(
pdfPageView._textHighlighter.textDivs[convertedMatch.end.divIdx]
.firstChild,
convertedMatch.end.offset
);

const pageBox = pdfPageView.textLayer.div.getBoundingClientRect();
const linkAnnotations = [];
for (const linkBox of range.getClientRects()) {
if (linkBox.width === 0 || linkBox.height === 0) {
continue;
}

const bottomLeft = pdfPageView.getPagePoint(
linkBox.left - pageBox.left,
linkBox.top - pageBox.top
);
const topRight = pdfPageView.getPagePoint(
linkBox.left - pageBox.left + linkBox.width,
linkBox.top - pageBox.top + linkBox.height
);

const rect = Util.normalizeRect([
bottomLeft[0],
bottomLeft[1],
topRight[0],
topRight[1],
]);

linkAnnotations.push({
unsafeUrl: url,
url,
rect,
annotationType: 2,
rotation: 0,
// This is just the default for AnnotationBorderStyle. At some point we
// should switch to something better like `new LinkAnnotation` here.
borderStyle: {
width: 1,
rawWidth: 1,
style: 1, // SOLID
dashArray: [3],
horizontalCornerRadius: 0,
verticalCornerRadius: 0,
},
});
}
return linkAnnotations;
}

static processLinks(pdfPageView) {
const [text, diffs] = normalize(
pdfPageView._textHighlighter.textContentItemsStr.join("\n")
);
const matches = text.matchAll(Autolinker.#urlRegex);
const links = [];
for (const match of matches) {
const url = createValidAbsoluteUrl(match[0]);
if (url) {
const [index, length] = getOriginalIndex(
diffs,
match.index,
match[0].length
);
links.push(
...this.#addLinkAnnotations(url.href, index, length, pdfPageView)
);
}
}
return links;
}
}

export { Autolinker };
2 changes: 1 addition & 1 deletion web/pdf_find_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -1171,4 +1171,4 @@ class PDFFindController {
}
}

export { FindState, PDFFindController };
export { FindState, getOriginalIndex, normalize, PDFFindController };
15 changes: 13 additions & 2 deletions web/pdf_page_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js";
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
import { AppOptions } from "./app_options.js";
import { Autolinker } from "./autolinker.js";
import { DrawLayerBuilder } from "./draw_layer_builder.js";
import { GenericL10n } from "web-null_l10n";
import { SimpleLinkService } from "./pdf_link_service.js";
Expand Down Expand Up @@ -120,6 +121,8 @@ class PDFPageView {

#enableHWA = false;

#enableAutolinking = false;

#hasRestrictedScaling = false;

#isEditing = false;
Expand Down Expand Up @@ -148,6 +151,8 @@ class PDFPageView {
regularAnnotations: true,
};

#linkAnnotations = [];

#layers = [null, null, null, null];

/**
Expand Down Expand Up @@ -177,6 +182,7 @@ class PDFPageView {
options.maxCanvasPixels ?? AppOptions.get("maxCanvasPixels");
this.pageColors = options.pageColors || null;
this.#enableHWA = options.enableHWA || false;
this.#enableAutolinking = options.enableAutolinking || false;

this.eventBus = options.eventBus;
this.renderingQueue = options.renderingQueue;
Expand Down Expand Up @@ -399,7 +405,8 @@ class PDFPageView {
await this.annotationLayer.render(
this.viewport,
{ structTreeLayer: this.structTreeLayer },
"display"
"display",
this.#linkAnnotations
);
} catch (ex) {
console.error("#renderAnnotationLayer:", ex);
Expand Down Expand Up @@ -1086,9 +1093,13 @@ class PDFPageView {
viewport.rawDims
);

this.#renderTextLayer();
const textLayerP = this.#renderTextLayer();

if (this.annotationLayer) {
if (this.#enableAutolinking) {
await textLayerP;
this.#linkAnnotations = Autolinker.processLinks(this);
}
await this.#renderAnnotationLayer();
}

Expand Down
4 changes: 4 additions & 0 deletions web/pdf_viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ class PDFViewer {

#enableNewAltTextWhenAddingImage = false;

#enableAutolinking = false;

#eventAbortController = null;

#mlManager = null;
Expand Down Expand Up @@ -321,6 +323,7 @@ class PDFViewer {
this.#mlManager = options.mlManager || null;
this.#enableHWA = options.enableHWA || false;
this.#supportsPinchToZoom = options.supportsPinchToZoom !== false;
this.#enableAutolinking = options.enableAutolinking || false;

this.defaultRenderingQueue = !options.renderingQueue;
if (
Expand Down Expand Up @@ -990,6 +993,7 @@ class PDFViewer {
l10n: this.l10n,
layerProperties: this._layerProperties,
enableHWA: this.#enableHWA,
enableAutolinking: this.#enableAutolinking,
});
this._pages.push(pageView);
}
Expand Down
Loading