Compare commits

...

16 Commits

Author SHA1 Message Date
Jonas Jenwald
3cd1b10433
Merge pull request #19576 from nicolo-ribaudo/fix-linkannotation
Fix autolinking with highlighted search results
2025-03-02 19:51:15 +01:00
Nicolò Ribaudo
b47d81536c
Fix autolinking with highlighted search results
The current logic assumes that all spans in the text layer contain
only one text node, and thus that the position information
returned by `highlighter._convertMatches` can be directly used
on the element's only child.

This is not true in case of highlighted search results: they will be
injected in the DOM as `<span>` elements, causing the `<span>`s
in the text layer to have more than one child.

This patch fixes the problem by properly converting the (span, offset)
pair in a (textNode, offset) pair that points to the right text node.
2025-03-02 17:09:58 +01:00
Tim van der Meij
146bc58f32
Merge pull request #19596 from Snuffleupagus/Array-methods-arrow-functions
Use arrow function with various Array methods
2025-03-02 16:09:09 +01:00
Tim van der Meij
be6ef64a2c
Merge pull request #19577 from Snuffleupagus/isValidExplicitDest-reuse
Re-use the `isValidExplicitDest` helper function in the worker/viewer
2025-03-02 15:50:54 +01:00
Tim van der Meij
e8bbb60869
Merge pull request #19582 from dhdaines/consume_response
Be sure to consume responses in case of error in downloading test files (issue 19580)
2025-03-02 15:48:26 +01:00
Tim van der Meij
1aee9d5c61
Merge pull request #19581 from Snuffleupagus/issue-19579
Write string-data into the `.error`-file created for broken test-manifest links (issue 19579)
2025-03-02 15:39:11 +01:00
Tim van der Meij
dfa6370161
Merge pull request #19589 from Snuffleupagus/RunLengthStream-fill
Replace a loop with `TypedArray.prototype.fill()` in the `RunLengthStream` class
2025-03-02 15:36:49 +01:00
Tim van der Meij
27c82ab8cc
Merge pull request #19568 from Snuffleupagus/FakeMLManager-no-bundle
Don't bundle the `FakeMLManager` class in regular builds
2025-03-02 15:30:03 +01:00
Tim van der Meij
93759f34f4
Merge pull request #19571 from yjoer/master
Add the missing return type for `PDFWorker.fromPort()`
2025-03-02 15:27:10 +01:00
Jonas Jenwald
2e62f426fe Use arrow function with various Array methods
A lot of this is quite old code, which we can shorten slightly by using arrow functions instead of "regular" functions.
2025-03-02 15:19:04 +01:00
Jonas Jenwald
a3d259a681 Replace a loop with TypedArray.prototype.fill() in the RunLengthStream class
This is a tiny bit shorter, which cannot hurt.
2025-03-02 13:10:34 +01:00
David Huggins-Daines
962f972aea Be sure to consume responses in case of error in downloading test files (issue 19580)
As mentioned in https://nodejs.org/docs/latest-v20.x/api/http.html#httpgeturl-options-callback
if a request results in an error, you still need to consume the data, or call
`response.resume()`, or your code will hang waiting for the request to close.
2025-03-01 07:53:56 -05:00
Jonas Jenwald
165d90fe26 Re-use the isValidExplicitDest helper function in the worker/viewer
Currently we re-implement the same helper function twice, which in hindsight seems like the wrong decision since that way it's quite easy for the implementations to accidentally diverge.
The reason for doing it this way was because the code in the worker-thread is able to check for `Ref`- and `Name`-instances directly, which obviously isn't possible in the viewer but can be solved by passing validation-functions to the helper.
2025-03-01 12:08:56 +01:00
Jonas Jenwald
d1b2476c14 Write string-data into the .error-file created for broken test-manifest links (issue 19579)
This appears to have broken in PR 17431, since prior to that the `downloadFile` function returned a string (via a callback) on failure and now we're instead rejecting with an Error.
2025-02-28 17:05:15 +01:00
Yeoh Joer
2221ccf160 Add the missing return type for PDFWorker.fromPort() 2025-02-28 02:49:44 +08:00
Jonas Jenwald
b5ac96da19 Don't bundle the FakeMLManager class in regular builds
Given that this functionality is only used in the development viewer and in TESTING builds, there's no reason to include this in the regular builds.
2025-02-27 12:59:58 +01:00
22 changed files with 309 additions and 238 deletions

View File

@ -14,15 +14,7 @@
*/
import {
collectActions,
isNumberArray,
MissingDataException,
PDF_VERSION_REGEXP,
recoverJsURL,
toRomanNumerals,
XRefEntryException,
} from "./core_utils.js";
import {
_isValidExplicitDest,
createValidAbsoluteUrl,
DocumentActionEventType,
FormatError,
@ -34,6 +26,15 @@ import {
stringToUTF8String,
warn,
} from "../shared/util.js";
import {
collectActions,
isNumberArray,
MissingDataException,
PDF_VERSION_REGEXP,
recoverJsURL,
toRomanNumerals,
XRefEntryException,
} from "./core_utils.js";
import {
Dict,
isDict,
@ -53,52 +54,13 @@ import { FileSpec } from "./file_spec.js";
import { MetadataParser } from "./metadata_parser.js";
import { StructTreeRoot } from "./struct_tree.js";
function isValidExplicitDest(dest) {
if (!Array.isArray(dest) || dest.length < 2) {
return false;
}
const [page, zoom, ...args] = dest;
if (!(page instanceof Ref) && !Number.isInteger(page)) {
return false;
}
if (!(zoom instanceof Name)) {
return false;
}
const argsLen = args.length;
let allowNull = true;
switch (zoom.name) {
case "XYZ":
if (argsLen < 2 || argsLen > 3) {
return false;
}
break;
case "Fit":
case "FitB":
return argsLen === 0;
case "FitH":
case "FitBH":
case "FitV":
case "FitBV":
if (argsLen > 1) {
return false;
}
break;
case "FitR":
if (argsLen !== 4) {
return false;
}
allowNull = false;
break;
default:
return false;
}
for (const arg of args) {
if (!(typeof arg === "number" || (allowNull && arg === null))) {
return false;
}
}
return true;
}
const isRef = v => v instanceof Ref;
const isValidExplicitDest = _isValidExplicitDest.bind(
null,
/* validRef = */ isRef,
/* validName = */ isName
);
function fetchDest(dest) {
if (dest instanceof Dict) {

View File

@ -414,9 +414,7 @@ class ChunkedStreamManager {
}
}
chunksToRequest.sort(function (a, b) {
return a - b;
});
chunksToRequest.sort((a, b) => a - b);
return this._requestChunks(chunksToRequest);
}

View File

@ -576,9 +576,7 @@ function getRanges(glyphs, toUnicodeExtraMap, numGlyphs) {
if (codes.length === 0) {
codes.push({ fontCharCode: 0, glyphId: 0 });
}
codes.sort(function fontGetRangesSort(a, b) {
return a.fontCharCode - b.fontCharCode;
});
codes.sort((a, b) => a.fontCharCode - b.fontCharCode);
// Split the sorted codes into ranges.
const ranges = [];
@ -1777,9 +1775,7 @@ class Font {
}
// removing duplicate entries
mappings.sort(function (a, b) {
return a.charCode - b.charCode;
});
mappings.sort((a, b) => a.charCode - b.charCode);
const finalMappings = [],
seenCharCodes = new Set();
for (const map of mappings) {

View File

@ -374,9 +374,7 @@ function decodeBitmap(
// Sorting is non-standard, and it is not required. But sorting increases
// the number of template bits that can be reused from the previous
// contextLabel in the main loop.
template.sort(function (a, b) {
return a.y - b.y || a.x - b.x;
});
template.sort((a, b) => a.y - b.y || a.x - b.x);
const templateLength = template.length;
const templateX = new Int8Array(templateLength);

View File

@ -48,11 +48,9 @@ class RunLengthStream extends DecodeStream {
}
} else {
n = 257 - n;
const b = repeatHeader[1];
buffer = this.ensureBuffer(bufferLength + n + 1);
for (let i = 0; i < n; i++) {
buffer[bufferLength++] = b;
}
buffer.fill(repeatHeader[1], bufferLength, bufferLength + n);
bufferLength += n;
}
this.bufferLength = bufferLength;
}

View File

@ -321,11 +321,7 @@ class SimpleDOMNode {
if (!this.childNodes) {
return this.nodeValue || "";
}
return this.childNodes
.map(function (child) {
return child.textContent;
})
.join("");
return this.childNodes.map(child => child.textContent).join("");
}
get children() {

View File

@ -18,6 +18,7 @@
*/
import {
_isValidExplicitDest,
AbortException,
AnnotationMode,
assert,
@ -595,15 +596,20 @@ function getFactoryUrlProp(val) {
throw new Error(`Invalid factory url: "${val}" must include trailing slash.`);
}
function isRefProxy(ref) {
return (
typeof ref === "object" &&
Number.isInteger(ref?.num) &&
ref.num >= 0 &&
Number.isInteger(ref?.gen) &&
ref.gen >= 0
);
}
const isRefProxy = v =>
typeof v === "object" &&
Number.isInteger(v?.num) &&
v.num >= 0 &&
Number.isInteger(v?.gen) &&
v.gen >= 0;
const isNameProxy = v => typeof v === "object" && typeof v?.name === "string";
const isValidExplicitDest = _isValidExplicitDest.bind(
null,
/* validRef = */ isRefProxy,
/* validName = */ isNameProxy
);
/**
* @typedef {Object} OnProgressParameters
@ -2336,6 +2342,7 @@ class PDFWorker {
/**
* @param {PDFWorkerParameters} params - The worker initialization parameters.
* @returns {PDFWorker}
*/
static fromPort(params) {
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
@ -3484,6 +3491,7 @@ const build =
export {
build,
getDocument,
isValidExplicitDest,
LoopbackPort,
PDFDataRangeTransport,
PDFDocumentLoadingTask,

View File

@ -45,6 +45,7 @@ import {
import {
build,
getDocument,
isValidExplicitDest,
PDFDataRangeTransport,
PDFWorker,
version,
@ -117,6 +118,7 @@ export {
InvalidPDFException,
isDataScheme,
isPdfFile,
isValidExplicitDest,
noContextMenu,
normalizeUnicode,
OPS,

View File

@ -1080,6 +1080,54 @@ function getUuid() {
const AnnotationPrefix = "pdfjs_internal_id_";
function _isValidExplicitDest(validRef, validName, dest) {
if (!Array.isArray(dest) || dest.length < 2) {
return false;
}
const [page, zoom, ...args] = dest;
if (!validRef(page) && !Number.isInteger(page)) {
return false;
}
if (!validName(zoom)) {
return false;
}
const argsLen = args.length;
let allowNull = true;
switch (zoom.name) {
case "XYZ":
if (argsLen < 2 || argsLen > 3) {
return false;
}
break;
case "Fit":
case "FitB":
return argsLen === 0;
case "FitH":
case "FitBH":
case "FitV":
case "FitBV":
if (argsLen > 1) {
return false;
}
break;
case "FitR":
if (argsLen !== 4) {
return false;
}
allowNull = false;
break;
default:
return false;
}
for (const arg of args) {
if (typeof arg === "number" || (allowNull && arg === null)) {
continue;
}
return false;
}
return true;
}
// TODO: Remove this once `Uint8Array.prototype.toHex` is generally available.
function toHexUtil(arr) {
if (Uint8Array.prototype.toHex) {
@ -1119,6 +1167,7 @@ if (
}
export {
_isValidExplicitDest,
AbortException,
AnnotationActionEventType,
AnnotationBorderStyleType,

View File

@ -42,6 +42,7 @@ function downloadFile(file, url, redirects = 0) {
.get(url, async function (response) {
if ([301, 302, 307, 308].includes(response.statusCode)) {
if (redirects > 10) {
response.resume();
reject(new Error("Too many redirects"));
return;
}
@ -50,12 +51,14 @@ function downloadFile(file, url, redirects = 0) {
await downloadFile(file, redirectTo, ++redirects);
resolve();
} catch (ex) {
response.resume();
reject(ex);
}
return;
}
if (response.statusCode !== 200) {
response.resume();
reject(new Error(`HTTP ${response.statusCode}`));
return;
}
@ -88,7 +91,7 @@ async function downloadManifestFiles(manifest) {
} catch (ex) {
console.error(`Error during downloading of ${url}:`, ex);
fs.writeFileSync(file, ""); // making it empty file
fs.writeFileSync(`${file}.error`, ex);
fs.writeFileSync(`${file}.error`, ex.toString());
}
}
}

View File

@ -49,10 +49,7 @@ async function initializePDFJS(callback) {
"pdfjs-test/font/font_os2_spec.js",
"pdfjs-test/font/font_post_spec.js",
"pdfjs-test/font/font_fpgm_spec.js",
].map(function (moduleName) {
// eslint-disable-next-line no-unsanitized/method
return import(moduleName);
})
].map(moduleName => import(moduleName)) // eslint-disable-line no-unsanitized/method
);
callback();

View File

@ -13,10 +13,46 @@
* limitations under the License.
*/
import { closePages, createPromise, loadAndWait } from "./test_utils.mjs";
import {
awaitPromise,
closePages,
createPromise,
loadAndWait,
} from "./test_utils.mjs";
function waitForLinkAnnotations(page) {
function waitForLinkAnnotations(page, pageNumber) {
return page.evaluateHandle(
number => [
new Promise(resolve => {
const { eventBus } = window.PDFViewerApplication;
eventBus.on("linkannotationsadded", function listener(e) {
if (number === undefined || e.pageNumber === number) {
resolve();
eventBus.off("linkannotationsadded", listener);
}
});
}),
],
pageNumber
);
}
function recordInitialLinkAnnotationsEvent(eventBus) {
globalThis.initialLinkAnnotationsEventFired = false;
eventBus.on(
"linkannotationsadded",
() => {
globalThis.initialLinkAnnotationsEventFired = true;
},
{ once: true }
);
}
function waitForInitialLinkAnnotations(page) {
return createPromise(page, resolve => {
if (globalThis.initialLinkAnnotationsEventFired) {
resolve();
return;
}
window.PDFViewerApplication.eventBus.on("linkannotationsadded", resolve, {
once: true,
});
@ -178,4 +214,49 @@ describe("autolinker", function () {
);
});
});
describe("when highlighting search results", function () {
let pages;
beforeAll(async () => {
pages = await loadAndWait(
"issue3115r.pdf",
".annotationLayer",
null,
{ eventBusSetup: recordInitialLinkAnnotationsEvent },
{ enableAutoLinking: true }
);
});
afterAll(async () => {
await closePages(pages);
});
it("must find links that overlap with search results", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await awaitPromise(await waitForInitialLinkAnnotations(page));
const linkAnnotationsPromise = await waitForLinkAnnotations(page, 36);
// Search for "rich.edu"
await page.click("#viewFindButton");
await page.waitForSelector("#viewFindButton", { hidden: false });
await page.type("#findInput", "rich.edu");
await page.waitForSelector(".textLayer .highlight");
await awaitPromise(linkAnnotationsPromise);
const urls = await page.$$eval(
".page[data-page-number='36'] > .annotationLayer > .linkAnnotation > a",
annotations => annotations.map(a => a.href)
);
expect(urls)
.withContext(`In ${browserName}`)
.toContain(jasmine.stringContaining("rich.edu"));
})
);
});
});
});

View File

@ -64,9 +64,7 @@ function flatten(stats) {
});
// Use only overall results if not grouped by 'stat'
if (!options.groupBy.includes("stat")) {
rows = rows.filter(function (s) {
return s.stat === "Overall";
});
rows = rows.filter(s => s.stat === "Overall");
}
return rows;
}
@ -129,9 +127,7 @@ function stat(baseline, current) {
}
const rows = [];
// collect rows and measure column widths
const width = labels.map(function (s) {
return s.length;
});
const width = labels.map(s => s.length);
rows.push(labels);
for (const key of keys) {
const baselineMean = mean(baselineGroup[key]);
@ -162,9 +158,7 @@ function stat(baseline, current) {
}
// add horizontal line
const hline = width.map(function (w) {
return new Array(w + 1).join("-");
});
const hline = width.map(w => new Array(w + 1).join("-"));
rows.splice(1, 0, hline);
// print output

View File

@ -1051,9 +1051,7 @@ async function closeSession(browser) {
await session.browser.close();
}
session.closed = true;
const allClosed = sessions.every(function (s) {
return s.closed;
});
const allClosed = sessions.every(s => s.closed);
if (allClosed) {
if (tempDir) {
fs.rmSync(tempDir, { recursive: true, force: true });

View File

@ -4869,9 +4869,9 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)
// Issue 6205 reported an issue with font rendering, so clear the loaded
// fonts so that we can see whether loading PDFs in parallel does not
// cause any issues with the rendered fonts.
const destroyPromises = loadingTasks.map(function (loadingTask) {
return loadingTask.destroy();
});
const destroyPromises = loadingTasks.map(loadingTask =>
loadingTask.destroy()
);
await Promise.all(destroyPromises);
});

View File

@ -100,10 +100,7 @@ async function initializePDFJS(callback) {
"pdfjs-test/unit/xfa_serialize_data_spec.js",
"pdfjs-test/unit/xfa_tohtml_spec.js",
"pdfjs-test/unit/xml_spec.js",
].map(function (moduleName) {
// eslint-disable-next-line no-unsanitized/method
return import(moduleName);
})
].map(moduleName => import(moduleName)) // eslint-disable-line no-unsanitized/method
);
if (isNodeJS) {

View File

@ -36,6 +36,7 @@ import {
import {
build,
getDocument,
isValidExplicitDest,
PDFDataRangeTransport,
PDFWorker,
version,
@ -94,6 +95,7 @@ const expectedAPI = Object.freeze({
InvalidPDFException,
isDataScheme,
isPdfFile,
isValidExplicitDest,
noContextMenu,
normalizeUnicode,
OPS,

View File

@ -69,13 +69,55 @@ function calculateLinkPosition(range, pdfPageView) {
return { quadPoints, rect };
}
/**
* Given a DOM node `container` and an index into its text contents `offset`,
* returns a pair consisting of text node that the `offset` actually points
* to, together with the offset relative to that text node.
* When the offset points at the boundary between two node, the result will
* point to the first text node in depth-first traversal order.
*
* For example, given this DOM:
* <p>abc<span>def</span>ghi</p>
*
* textPosition(p, 0) -> [#text "abc", 0] (before `a`)
* textPosition(p, 2) -> [#text "abc", 2] (between `b` and `c`)
* textPosition(p, 3) -> [#text "abc", 3] (after `c`)
* textPosition(p, 5) -> [#text "def", 2] (between `e` and `f`)
* textPosition(p, 6) -> [#text "def", 3] (after `f`)
*/
function textPosition(container, offset) {
let currentContainer = container;
do {
if (currentContainer.nodeType === Node.TEXT_NODE) {
const currentLength = currentContainer.textContent.length;
if (offset <= currentLength) {
return [currentContainer, offset];
}
offset -= currentLength;
} else if (currentContainer.firstChild) {
currentContainer = currentContainer.firstChild;
continue;
}
while (!currentContainer.nextSibling && currentContainer !== container) {
currentContainer = currentContainer.parentNode;
}
if (currentContainer !== container) {
currentContainer = currentContainer.nextSibling;
}
} while (currentContainer !== container);
throw new Error("Offset is bigger than container's contents length.");
}
function createLinkAnnotation({ url, index, length }, pdfPageView, id) {
const highlighter = pdfPageView._textHighlighter;
const [{ begin, end }] = highlighter._convertMatches([index], [length]);
const range = new Range();
range.setStart(highlighter.textDivs[begin.divIdx].firstChild, begin.offset);
range.setEnd(highlighter.textDivs[end.divIdx].firstChild, end.offset);
range.setStart(
...textPosition(highlighter.textDivs[begin.divIdx], begin.offset)
);
range.setEnd(...textPosition(highlighter.textDivs[end.divIdx], end.offset));
return {
id: `inferred_link_${id}`,

View File

@ -398,9 +398,7 @@ class Stepper {
}
getNextBreakPoint() {
this.breakPoints.sort(function (a, b) {
return a - b;
});
this.breakPoints.sort((a, b) => a - b);
for (const breakPoint of this.breakPoints) {
if (breakPoint > this.currentIdx) {
return breakPoint;
@ -487,9 +485,7 @@ const Stats = (function Stats() {
statsDiv.textContent = stat.toString();
wrapper.append(title, statsDiv);
stats.push({ pageNumber, div: wrapper });
stats.sort(function (a, b) {
return a.pageNumber - b.pageNumber;
});
stats.sort((a, b) => a.pageNumber - b.pageNumber);
clear(this.panel);
for (const entry of stats) {
this.panel.append(entry.div);

View File

@ -53,6 +53,12 @@ class ExternalServices extends BaseExternalServices {
}
class MLManager {
static {
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
this.getFakeMLManager = options => new FakeMLManager(options);
}
}
async isEnabledFor(_name) {
return false;
}
@ -68,83 +74,82 @@ class MLManager {
guess(_data) {}
toggleService(_name, _enabled) {}
static getFakeMLManager(options) {
return new FakeMLManager(options);
}
}
class FakeMLManager {
eventBus = null;
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
// eslint-disable-next-line no-var
var FakeMLManager = class {
eventBus = null;
hasProgress = false;
hasProgress = false;
constructor({ enableGuessAltText, enableAltTextModelDownload }) {
this.enableGuessAltText = enableGuessAltText;
this.enableAltTextModelDownload = enableAltTextModelDownload;
}
constructor({ enableGuessAltText, enableAltTextModelDownload }) {
this.enableGuessAltText = enableGuessAltText;
this.enableAltTextModelDownload = enableAltTextModelDownload;
}
setEventBus(eventBus, abortSignal) {
this.eventBus = eventBus;
}
setEventBus(eventBus, abortSignal) {
this.eventBus = eventBus;
}
async isEnabledFor(_name) {
return this.enableGuessAltText;
}
async isEnabledFor(_name) {
return this.enableGuessAltText;
}
async deleteModel(_name) {
this.enableAltTextModelDownload = false;
return null;
}
async deleteModel(_name) {
this.enableAltTextModelDownload = false;
return null;
}
async loadModel(_name) {}
async loadModel(_name) {}
async downloadModel(_name) {
// Simulate downloading the model but with progress.
// The progress can be seen in the new alt-text dialog.
this.hasProgress = true;
async downloadModel(_name) {
// Simulate downloading the model but with progress.
// The progress can be seen in the new alt-text dialog.
this.hasProgress = true;
const { promise, resolve } = Promise.withResolvers();
const total = 1e8;
const end = 1.5 * total;
const increment = 5e6;
let loaded = 0;
const id = setInterval(() => {
loaded += increment;
if (loaded <= end) {
this.eventBus.dispatch("loadaiengineprogress", {
source: this,
detail: {
total,
totalLoaded: loaded,
finished: loaded + increment >= end,
},
});
return;
}
clearInterval(id);
this.hasProgress = false;
this.enableAltTextModelDownload = true;
resolve(true);
}, 900);
return promise;
}
const { promise, resolve } = Promise.withResolvers();
const total = 1e8;
const end = 1.5 * total;
const increment = 5e6;
let loaded = 0;
const id = setInterval(() => {
loaded += increment;
if (loaded <= end) {
this.eventBus.dispatch("loadaiengineprogress", {
source: this,
detail: {
total,
totalLoaded: loaded,
finished: loaded + increment >= end,
},
});
return;
}
clearInterval(id);
this.hasProgress = false;
this.enableAltTextModelDownload = true;
resolve(true);
}, 900);
return promise;
}
isReady(_name) {
return this.enableAltTextModelDownload;
}
isReady(_name) {
return this.enableAltTextModelDownload;
}
guess({ request: { data } }) {
return new Promise(resolve => {
setTimeout(() => {
resolve(data ? { output: "Fake alt text." } : { error: true });
}, 3000);
});
}
guess({ request: { data } }) {
return new Promise(resolve => {
setTimeout(() => {
resolve(data ? { output: "Fake alt text." } : { error: true });
}, 3000);
});
}
toggleService(_name, enabled) {
this.enableGuessAltText = enabled;
}
toggleService(_name, enabled) {
this.enableGuessAltText = enabled;
}
};
}
export { ExternalServices, initCom, MLManager, Preferences };

View File

@ -16,6 +16,7 @@
/** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
import { isValidExplicitDest } from "pdfjs-lib";
import { parseQueryString } from "./ui_utils.js";
const DEFAULT_LINK_REL = "noopener noreferrer nofollow";
@ -415,7 +416,7 @@ class PDFLinkService {
}
} catch {}
if (typeof dest === "string" || PDFLinkService.#isValidExplicitDest(dest)) {
if (typeof dest === "string" || isValidExplicitDest(dest)) {
this.goToDestination(dest);
return;
}
@ -486,60 +487,6 @@ class PDFLinkService {
optionalContentConfig
);
}
static #isValidExplicitDest(dest) {
if (!Array.isArray(dest) || dest.length < 2) {
return false;
}
const [page, zoom, ...args] = dest;
if (
!(
typeof page === "object" &&
Number.isInteger(page?.num) &&
Number.isInteger(page?.gen)
) &&
!Number.isInteger(page)
) {
return false;
}
if (!(typeof zoom === "object" && typeof zoom?.name === "string")) {
return false;
}
const argsLen = args.length;
let allowNull = true;
switch (zoom.name) {
case "XYZ":
if (argsLen < 2 || argsLen > 3) {
return false;
}
break;
case "Fit":
case "FitB":
return argsLen === 0;
case "FitH":
case "FitBH":
case "FitV":
case "FitBV":
if (argsLen > 1) {
return false;
}
break;
case "FitR":
if (argsLen !== 4) {
return false;
}
allowNull = false;
break;
default:
return false;
}
for (const arg of args) {
if (!(typeof arg === "number" || (allowNull && arg === null))) {
return false;
}
}
return true;
}
}
/**

View File

@ -39,6 +39,7 @@ const {
InvalidPDFException,
isDataScheme,
isPdfFile,
isValidExplicitDest,
noContextMenu,
normalizeUnicode,
OPS,
@ -90,6 +91,7 @@ export {
InvalidPDFException,
isDataScheme,
isPdfFile,
isValidExplicitDest,
noContextMenu,
normalizeUnicode,
OPS,