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 { import {
collectActions, _isValidExplicitDest,
isNumberArray,
MissingDataException,
PDF_VERSION_REGEXP,
recoverJsURL,
toRomanNumerals,
XRefEntryException,
} from "./core_utils.js";
import {
createValidAbsoluteUrl, createValidAbsoluteUrl,
DocumentActionEventType, DocumentActionEventType,
FormatError, FormatError,
@ -34,6 +26,15 @@ import {
stringToUTF8String, stringToUTF8String,
warn, warn,
} from "../shared/util.js"; } from "../shared/util.js";
import {
collectActions,
isNumberArray,
MissingDataException,
PDF_VERSION_REGEXP,
recoverJsURL,
toRomanNumerals,
XRefEntryException,
} from "./core_utils.js";
import { import {
Dict, Dict,
isDict, isDict,
@ -53,52 +54,13 @@ import { FileSpec } from "./file_spec.js";
import { MetadataParser } from "./metadata_parser.js"; import { MetadataParser } from "./metadata_parser.js";
import { StructTreeRoot } from "./struct_tree.js"; import { StructTreeRoot } from "./struct_tree.js";
function isValidExplicitDest(dest) { const isRef = v => v instanceof Ref;
if (!Array.isArray(dest) || dest.length < 2) {
return false; const isValidExplicitDest = _isValidExplicitDest.bind(
} null,
const [page, zoom, ...args] = dest; /* validRef = */ isRef,
if (!(page instanceof Ref) && !Number.isInteger(page)) { /* validName = */ isName
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;
}
function fetchDest(dest) { function fetchDest(dest) {
if (dest instanceof Dict) { if (dest instanceof Dict) {

View File

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

View File

@ -576,9 +576,7 @@ function getRanges(glyphs, toUnicodeExtraMap, numGlyphs) {
if (codes.length === 0) { if (codes.length === 0) {
codes.push({ fontCharCode: 0, glyphId: 0 }); codes.push({ fontCharCode: 0, glyphId: 0 });
} }
codes.sort(function fontGetRangesSort(a, b) { codes.sort((a, b) => a.fontCharCode - b.fontCharCode);
return a.fontCharCode - b.fontCharCode;
});
// Split the sorted codes into ranges. // Split the sorted codes into ranges.
const ranges = []; const ranges = [];
@ -1777,9 +1775,7 @@ class Font {
} }
// removing duplicate entries // removing duplicate entries
mappings.sort(function (a, b) { mappings.sort((a, b) => a.charCode - b.charCode);
return a.charCode - b.charCode;
});
const finalMappings = [], const finalMappings = [],
seenCharCodes = new Set(); seenCharCodes = new Set();
for (const map of mappings) { 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 // Sorting is non-standard, and it is not required. But sorting increases
// the number of template bits that can be reused from the previous // the number of template bits that can be reused from the previous
// contextLabel in the main loop. // contextLabel in the main loop.
template.sort(function (a, b) { template.sort((a, b) => a.y - b.y || a.x - b.x);
return a.y - b.y || a.x - b.x;
});
const templateLength = template.length; const templateLength = template.length;
const templateX = new Int8Array(templateLength); const templateX = new Int8Array(templateLength);

View File

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

View File

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

View File

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

View File

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

View File

@ -1080,6 +1080,54 @@ function getUuid() {
const AnnotationPrefix = "pdfjs_internal_id_"; 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. // TODO: Remove this once `Uint8Array.prototype.toHex` is generally available.
function toHexUtil(arr) { function toHexUtil(arr) {
if (Uint8Array.prototype.toHex) { if (Uint8Array.prototype.toHex) {
@ -1119,6 +1167,7 @@ if (
} }
export { export {
_isValidExplicitDest,
AbortException, AbortException,
AnnotationActionEventType, AnnotationActionEventType,
AnnotationBorderStyleType, AnnotationBorderStyleType,

View File

@ -42,6 +42,7 @@ function downloadFile(file, url, redirects = 0) {
.get(url, async function (response) { .get(url, async function (response) {
if ([301, 302, 307, 308].includes(response.statusCode)) { if ([301, 302, 307, 308].includes(response.statusCode)) {
if (redirects > 10) { if (redirects > 10) {
response.resume();
reject(new Error("Too many redirects")); reject(new Error("Too many redirects"));
return; return;
} }
@ -50,12 +51,14 @@ function downloadFile(file, url, redirects = 0) {
await downloadFile(file, redirectTo, ++redirects); await downloadFile(file, redirectTo, ++redirects);
resolve(); resolve();
} catch (ex) { } catch (ex) {
response.resume();
reject(ex); reject(ex);
} }
return; return;
} }
if (response.statusCode !== 200) { if (response.statusCode !== 200) {
response.resume();
reject(new Error(`HTTP ${response.statusCode}`)); reject(new Error(`HTTP ${response.statusCode}`));
return; return;
} }
@ -88,7 +91,7 @@ async function downloadManifestFiles(manifest) {
} catch (ex) { } catch (ex) {
console.error(`Error during downloading of ${url}:`, ex); console.error(`Error during downloading of ${url}:`, ex);
fs.writeFileSync(file, ""); // making it empty file 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_os2_spec.js",
"pdfjs-test/font/font_post_spec.js", "pdfjs-test/font/font_post_spec.js",
"pdfjs-test/font/font_fpgm_spec.js", "pdfjs-test/font/font_fpgm_spec.js",
].map(function (moduleName) { ].map(moduleName => import(moduleName)) // eslint-disable-line no-unsanitized/method
// eslint-disable-next-line no-unsanitized/method
return import(moduleName);
})
); );
callback(); callback();

View File

@ -13,10 +13,46 @@
* limitations under the License. * 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 => { return createPromise(page, resolve => {
if (globalThis.initialLinkAnnotationsEventFired) {
resolve();
return;
}
window.PDFViewerApplication.eventBus.on("linkannotationsadded", resolve, { window.PDFViewerApplication.eventBus.on("linkannotationsadded", resolve, {
once: true, 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' // Use only overall results if not grouped by 'stat'
if (!options.groupBy.includes("stat")) { if (!options.groupBy.includes("stat")) {
rows = rows.filter(function (s) { rows = rows.filter(s => s.stat === "Overall");
return s.stat === "Overall";
});
} }
return rows; return rows;
} }
@ -129,9 +127,7 @@ function stat(baseline, current) {
} }
const rows = []; const rows = [];
// collect rows and measure column widths // collect rows and measure column widths
const width = labels.map(function (s) { const width = labels.map(s => s.length);
return s.length;
});
rows.push(labels); rows.push(labels);
for (const key of keys) { for (const key of keys) {
const baselineMean = mean(baselineGroup[key]); const baselineMean = mean(baselineGroup[key]);
@ -162,9 +158,7 @@ function stat(baseline, current) {
} }
// add horizontal line // add horizontal line
const hline = width.map(function (w) { const hline = width.map(w => new Array(w + 1).join("-"));
return new Array(w + 1).join("-");
});
rows.splice(1, 0, hline); rows.splice(1, 0, hline);
// print output // print output

View File

@ -1051,9 +1051,7 @@ async function closeSession(browser) {
await session.browser.close(); await session.browser.close();
} }
session.closed = true; session.closed = true;
const allClosed = sessions.every(function (s) { const allClosed = sessions.every(s => s.closed);
return s.closed;
});
if (allClosed) { if (allClosed) {
if (tempDir) { if (tempDir) {
fs.rmSync(tempDir, { recursive: true, force: true }); 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 // 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 // fonts so that we can see whether loading PDFs in parallel does not
// cause any issues with the rendered fonts. // cause any issues with the rendered fonts.
const destroyPromises = loadingTasks.map(function (loadingTask) { const destroyPromises = loadingTasks.map(loadingTask =>
return loadingTask.destroy(); loadingTask.destroy()
}); );
await Promise.all(destroyPromises); 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_serialize_data_spec.js",
"pdfjs-test/unit/xfa_tohtml_spec.js", "pdfjs-test/unit/xfa_tohtml_spec.js",
"pdfjs-test/unit/xml_spec.js", "pdfjs-test/unit/xml_spec.js",
].map(function (moduleName) { ].map(moduleName => import(moduleName)) // eslint-disable-line no-unsanitized/method
// eslint-disable-next-line no-unsanitized/method
return import(moduleName);
})
); );
if (isNodeJS) { if (isNodeJS) {

View File

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

View File

@ -69,13 +69,55 @@ function calculateLinkPosition(range, pdfPageView) {
return { quadPoints, rect }; 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) { function createLinkAnnotation({ url, index, length }, pdfPageView, id) {
const highlighter = pdfPageView._textHighlighter; const highlighter = pdfPageView._textHighlighter;
const [{ begin, end }] = highlighter._convertMatches([index], [length]); const [{ begin, end }] = highlighter._convertMatches([index], [length]);
const range = new Range(); const range = new Range();
range.setStart(highlighter.textDivs[begin.divIdx].firstChild, begin.offset); range.setStart(
range.setEnd(highlighter.textDivs[end.divIdx].firstChild, end.offset); ...textPosition(highlighter.textDivs[begin.divIdx], begin.offset)
);
range.setEnd(...textPosition(highlighter.textDivs[end.divIdx], end.offset));
return { return {
id: `inferred_link_${id}`, id: `inferred_link_${id}`,

View File

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

View File

@ -53,6 +53,12 @@ class ExternalServices extends BaseExternalServices {
} }
class MLManager { class MLManager {
static {
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
this.getFakeMLManager = options => new FakeMLManager(options);
}
}
async isEnabledFor(_name) { async isEnabledFor(_name) {
return false; return false;
} }
@ -68,13 +74,11 @@ class MLManager {
guess(_data) {} guess(_data) {}
toggleService(_name, _enabled) {} toggleService(_name, _enabled) {}
static getFakeMLManager(options) {
return new FakeMLManager(options);
}
} }
class FakeMLManager { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
// eslint-disable-next-line no-var
var FakeMLManager = class {
eventBus = null; eventBus = null;
hasProgress = false; hasProgress = false;
@ -145,6 +149,7 @@ class FakeMLManager {
toggleService(_name, enabled) { toggleService(_name, enabled) {
this.enableGuessAltText = enabled; this.enableGuessAltText = enabled;
} }
};
} }
export { ExternalServices, initCom, MLManager, Preferences }; export { ExternalServices, initCom, MLManager, Preferences };

View File

@ -16,6 +16,7 @@
/** @typedef {import("./event_utils").EventBus} EventBus */ /** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */ /** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
import { isValidExplicitDest } from "pdfjs-lib";
import { parseQueryString } from "./ui_utils.js"; import { parseQueryString } from "./ui_utils.js";
const DEFAULT_LINK_REL = "noopener noreferrer nofollow"; const DEFAULT_LINK_REL = "noopener noreferrer nofollow";
@ -415,7 +416,7 @@ class PDFLinkService {
} }
} catch {} } catch {}
if (typeof dest === "string" || PDFLinkService.#isValidExplicitDest(dest)) { if (typeof dest === "string" || isValidExplicitDest(dest)) {
this.goToDestination(dest); this.goToDestination(dest);
return; return;
} }
@ -486,60 +487,6 @@ class PDFLinkService {
optionalContentConfig 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, InvalidPDFException,
isDataScheme, isDataScheme,
isPdfFile, isPdfFile,
isValidExplicitDest,
noContextMenu, noContextMenu,
normalizeUnicode, normalizeUnicode,
OPS, OPS,
@ -90,6 +91,7 @@ export {
InvalidPDFException, InvalidPDFException,
isDataScheme, isDataScheme,
isPdfFile, isPdfFile,
isValidExplicitDest,
noContextMenu, noContextMenu,
normalizeUnicode, normalizeUnicode,
OPS, OPS,