Compare commits

..

16 Commits

Author SHA1 Message Date
calixteman
2a68724b53
Merge pull request #17793 from calixteman/zoom_wheel_ctrl
When zooming the scrollbar can disappear and then no scrollend is triggered
2024-03-15 16:30:39 +01:00
Calixte Denizet
65d618635c When zooming the scrollbar can disappear and then no scrollend is triggered 2024-03-15 15:37:06 +01:00
Jonas Jenwald
30e69956db
Merge pull request #17694 from Snuffleupagus/validate-defaultOptions
Add better validation for the "PREFERENCE" kind `AppOptions`
2024-03-12 18:24:37 +01:00
Jonas Jenwald
e650b95253
Merge pull request #17714 from Snuffleupagus/Node-fs-promise
Use `fs/promises` in the Node.js-specific code in the `src/`-folder
2024-03-12 18:09:33 +01:00
calixteman
a7d47af474
Merge pull request #17732 from calixteman/editor_highlight_floating_button
[Editor] Add a floating button close to the selected text to highlight it (bug 1867742)
2024-03-12 15:43:19 +01:00
Jonas Jenwald
b54887cfab
Merge pull request #17717 from Snuffleupagus/pr-17681-follow-up
Combine a few lines in the `dist-pre` target (PR 17681 follow-up)
2024-03-12 15:15:19 +01:00
Calixte Denizet
b4267cd294 [Editor] Add a floating button close to the selected text to highlight it (bug 1867742)
For now keep this feature behind a pref in order to make some experiments before
deciding to enable it.
2024-03-12 15:06:46 +01:00
Jonas Jenwald
eb160726ee
Merge pull request #17726 from Snuffleupagus/OptionalContent-Usage
[api-minor] Implement basic support for OptionalContent `Usage` dicts (issue 5764, bug 1826783)
2024-03-12 14:09:57 +01:00
Jonas Jenwald
70b6ddc5d9 Move the /SetOCGState handling into the OptionalContentConfig class (PR 15377 follow-up)
This helps ensure that /SetOCGState actions always take the `Usage` dictionary into account as expected.
2024-03-12 13:18:15 +01:00
Jonas Jenwald
3c78ff5fb0 [api-minor] Implement basic support for OptionalContent Usage dicts (issue 5764, bug 1826783)
The following are some highlights of this patch:
 - In the Worker we only extract a *subset* of the potential contents of the `Usage` dictionary, to avoid having to implement/test a bunch of code that'd be completely unused in the viewer.

 - In order to still allow the user to *manually* override the default visible layers in the viewer, the viewable/printable state is purposely *not* enforced during initialization in the `OptionalContentConfig` constructor.

 - Printing will now always use the *default* visible layers, rather than using the same state as the viewer (as was the case previously).
   This ensures that the printing-output will correctly take the `Usage` dictionary into account, and in practice toggling of visible layers rarely seem to be necessary except in the viewer itself (if at all).[1]

---
[1] In the unlikely case that it'd ever be deemed necessary to support fine-grained control of optional content visibility during printing, some new (additional) UI would likely be needed to support that case.
2024-03-12 13:18:15 +01:00
calixteman
e647311a89
Merge pull request #17788 from calixteman/bug1881743
[Editor] Improve the accessibility of the highlight editor (bug 1881743)
2024-03-11 21:57:11 +01:00
Calixte Denizet
f676c2c0c8 [Editor] Improve the accessibility of the highlight editor (bug 1881743) 2024-03-11 14:21:43 +01:00
Jonas Jenwald
dab8a2eaa4 Combine a few lines in the dist-pre target (PR 17681 follow-up)
In PR 17681 I completely missed that we can combine a number of lines in the `dist-pre` target, which helps reduce code size a little bit.
2024-02-23 13:32:04 +01:00
Jonas Jenwald
db2849cc17 Use fs/promises in the Node.js-specific code in the src/-folder
This is available in all Node.js versions that we currently support, and using it allows us to remove callback-functions; please see https://nodejs.org/docs/latest-v18.x/api/fs.html#promises-api
2024-02-22 16:50:13 +01:00
Jonas Jenwald
38004b65b1 Re-factor how the compatibilityParams, in the viewer, are handled
Previously we'd simply export this directly from `web/app_options.js`, which meant that it'd be technically possible to *accidentally* modify the `compatibilityParams` Object when accessing it.
To avoid this we instead introduce a new `AppOptions`-method that is used to lookup data in `compatibilityParams`, which means that we no longer need to export this Object.

Based on these changes, it's now possible to simplify some existing code in `AppOptions` by taking full advantage of the nullish coalescing (`??`) operator.
2024-02-20 18:38:18 +01:00
Jonas Jenwald
90b2664622 Add better validation for the "PREFERENCE" kind AppOptions
Given that the "PREFERENCE" kind is used e.g. to generate the preference-list for the Firefox PDF Viewer, those options need to be carefully validated.
With this patch we'll now check this unconditionally in development mode, during testing, and when creating the preferences in the gulpfile.
2024-02-20 18:38:15 +01:00
34 changed files with 664 additions and 191 deletions

View File

@ -72,6 +72,10 @@
"type": "boolean", "type": "boolean",
"default": false "default": false
}, },
"enableHighlightFloatingButton": {
"type": "boolean",
"default": false
},
"highlightEditorColors": { "highlightEditorColors": {
"type": "string", "type": "string",
"default": "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F" "default": "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F"

View File

@ -863,11 +863,17 @@ async function parseDefaultPreferences(dir) {
"./" + DEFAULT_PREFERENCES_DIR + dir + "app_options.mjs" "./" + DEFAULT_PREFERENCES_DIR + dir + "app_options.mjs"
); );
const browserPrefs = AppOptions.getAll(OptionKind.BROWSER); const browserPrefs = AppOptions.getAll(
OptionKind.BROWSER,
/* defaultOnly = */ true
);
if (Object.keys(browserPrefs).length === 0) { if (Object.keys(browserPrefs).length === 0) {
throw new Error("No browser preferences found."); throw new Error("No browser preferences found.");
} }
const prefs = AppOptions.getAll(OptionKind.PREFERENCE); const prefs = AppOptions.getAll(
OptionKind.PREFERENCE,
/* defaultOnly = */ true
);
if (Object.keys(prefs).length === 0) { if (Object.keys(prefs).length === 0) {
throw new Error("No default preferences found."); throw new Error("No default preferences found.");
} }
@ -2222,25 +2228,15 @@ gulp.task(
]) ])
.pipe(gulp.dest(DIST_DIR + "legacy/build/")), .pipe(gulp.dest(DIST_DIR + "legacy/build/")),
gulp gulp
.src(MINIFIED_DIR + "build/pdf.min.mjs") .src(MINIFIED_DIR + "build/{pdf,pdf.worker,pdf.sandbox}.min.mjs")
.pipe(gulp.dest(DIST_DIR + "build/")),
gulp
.src(MINIFIED_DIR + "build/pdf.worker.min.mjs")
.pipe(gulp.dest(DIST_DIR + "build/")),
gulp
.src(MINIFIED_DIR + "build/pdf.sandbox.min.mjs")
.pipe(gulp.dest(DIST_DIR + "build/")), .pipe(gulp.dest(DIST_DIR + "build/")),
gulp gulp
.src(MINIFIED_DIR + "image_decoders/pdf.image_decoders.min.mjs") .src(MINIFIED_DIR + "image_decoders/pdf.image_decoders.min.mjs")
.pipe(gulp.dest(DIST_DIR + "image_decoders/")), .pipe(gulp.dest(DIST_DIR + "image_decoders/")),
gulp gulp
.src(MINIFIED_LEGACY_DIR + "build/pdf.min.mjs") .src(
.pipe(gulp.dest(DIST_DIR + "legacy/build/")), MINIFIED_LEGACY_DIR + "build/{pdf,pdf.worker,pdf.sandbox}.min.mjs"
gulp )
.src(MINIFIED_LEGACY_DIR + "build/pdf.worker.min.mjs")
.pipe(gulp.dest(DIST_DIR + "legacy/build/")),
gulp
.src(MINIFIED_LEGACY_DIR + "build/pdf.sandbox.min.mjs")
.pipe(gulp.dest(DIST_DIR + "legacy/build/")), .pipe(gulp.dest(DIST_DIR + "legacy/build/")),
gulp gulp
.src( .src(

View File

@ -318,6 +318,8 @@ pdfjs-editor-stamp-button-label = Add or edit images
pdfjs-editor-highlight-button = pdfjs-editor-highlight-button =
.title = Highlight .title = Highlight
pdfjs-editor-highlight-button-label = Highlight pdfjs-editor-highlight-button-label = Highlight
pdfjs-highlight-floating-button =
.title = Highlight
## Remove button for the various kind of editor. ## Remove button for the various kind of editor.

View File

@ -445,20 +445,10 @@ class Catalog {
continue; continue;
} }
groupRefs.put(groupRef); groupRefs.put(groupRef);
const group = this.xref.fetch(groupRef);
groups.push({ groups.push(this.#readOptionalContentGroup(groupRef));
id: groupRef.toString(),
name:
typeof group.get("Name") === "string"
? stringToPDFString(group.get("Name"))
: null,
intent:
typeof group.get("Intent") === "string"
? stringToPDFString(group.get("Intent"))
: null,
});
} }
config = this._readOptionalContentConfig(defaultConfig, groupRefs); config = this.#readOptionalContentConfig(defaultConfig, groupRefs);
config.groups = groups; config.groups = groups;
} catch (ex) { } catch (ex) {
if (ex instanceof MissingDataException) { if (ex instanceof MissingDataException) {
@ -469,7 +459,65 @@ class Catalog {
return shadow(this, "optionalContentConfig", config); return shadow(this, "optionalContentConfig", config);
} }
_readOptionalContentConfig(config, contentGroupRefs) { #readOptionalContentGroup(groupRef) {
const group = this.xref.fetch(groupRef);
const obj = {
id: groupRef.toString(),
name: null,
intent: null,
usage: {
print: null,
view: null,
},
};
const name = group.get("Name");
if (typeof name === "string") {
obj.name = stringToPDFString(name);
}
let intent = group.getArray("Intent");
if (!Array.isArray(intent)) {
intent = [intent];
}
if (intent.every(i => i instanceof Name)) {
obj.intent = intent.map(i => i.name);
}
const usage = group.get("Usage");
if (!(usage instanceof Dict)) {
return obj;
}
const usageObj = obj.usage;
const print = usage.get("Print");
if (print instanceof Dict) {
const printState = print.get("PrintState");
if (printState instanceof Name) {
switch (printState.name) {
case "ON":
case "OFF":
usageObj.print = { printState: printState.name };
}
}
}
const view = usage.get("View");
if (view instanceof Dict) {
const viewState = view.get("ViewState");
if (viewState instanceof Name) {
switch (viewState.name) {
case "ON":
case "OFF":
usageObj.view = { viewState: viewState.name };
}
}
}
return obj;
}
#readOptionalContentConfig(config, contentGroupRefs) {
function parseOnOff(refs) { function parseOnOff(refs) {
const onParsed = []; const onParsed = [];
if (Array.isArray(refs)) { if (Array.isArray(refs)) {

View File

@ -949,12 +949,26 @@ class PDFDocumentProxy {
} }
/** /**
* @typedef {Object} GetOptionalContentConfigParameters
* @property {string} [intent] - Determines the optional content groups that
* are visible by default; valid values are:
* - 'display' (viewable groups).
* - 'print' (printable groups).
* - 'any' (all groups).
* The default value is 'display'.
*/
/**
* @param {GetOptionalContentConfigParameters} [params] - Optional content
* config parameters.
* @returns {Promise<OptionalContentConfig>} A promise that is resolved with * @returns {Promise<OptionalContentConfig>} A promise that is resolved with
* an {@link OptionalContentConfig} that contains all the optional content * an {@link OptionalContentConfig} that contains all the optional content
* groups (assuming that the document has any). * groups (assuming that the document has any).
*/ */
getOptionalContentConfig() { getOptionalContentConfig({ intent = "display" } = {}) {
return this._transport.getOptionalContentConfig(); const { renderingIntent } = this._transport.getRenderingIntent(intent);
return this._transport.getOptionalContentConfig(renderingIntent);
} }
/** /**
@ -1340,17 +1354,14 @@ class PDFPageProxy {
} }
/** /**
* @param {GetAnnotationsParameters} params - Annotation parameters. * @param {GetAnnotationsParameters} [params] - Annotation parameters.
* @returns {Promise<Array<any>>} A promise that is resolved with an * @returns {Promise<Array<any>>} A promise that is resolved with an
* {Array} of the annotation objects. * {Array} of the annotation objects.
*/ */
getAnnotations({ intent = "display" } = {}) { getAnnotations({ intent = "display" } = {}) {
const intentArgs = this._transport.getRenderingIntent(intent); const { renderingIntent } = this._transport.getRenderingIntent(intent);
return this._transport.getAnnotations( return this._transport.getAnnotations(this._pageIndex, renderingIntent);
this._pageIndex,
intentArgs.renderingIntent
);
} }
/** /**
@ -1411,20 +1422,20 @@ class PDFPageProxy {
annotationMode, annotationMode,
printAnnotationStorage printAnnotationStorage
); );
const { renderingIntent, cacheKey } = intentArgs;
// If there was a pending destroy, cancel it so no cleanup happens during // If there was a pending destroy, cancel it so no cleanup happens during
// this call to render... // this call to render...
this.#pendingCleanup = false; this.#pendingCleanup = false;
// ... and ensure that a delayed cleanup is always aborted. // ... and ensure that a delayed cleanup is always aborted.
this.#abortDelayedCleanup(); this.#abortDelayedCleanup();
if (!optionalContentConfigPromise) { optionalContentConfigPromise ||=
optionalContentConfigPromise = this._transport.getOptionalContentConfig(); this._transport.getOptionalContentConfig(renderingIntent);
}
let intentState = this._intentStates.get(intentArgs.cacheKey); let intentState = this._intentStates.get(cacheKey);
if (!intentState) { if (!intentState) {
intentState = Object.create(null); intentState = Object.create(null);
this._intentStates.set(intentArgs.cacheKey, intentState); this._intentStates.set(cacheKey, intentState);
} }
// Ensure that a pending `streamReader` cancel timeout is always aborted. // Ensure that a pending `streamReader` cancel timeout is always aborted.
@ -1433,9 +1444,7 @@ class PDFPageProxy {
intentState.streamReaderCancelTimeout = null; intentState.streamReaderCancelTimeout = null;
} }
const intentPrint = !!( const intentPrint = !!(renderingIntent & RenderingIntentFlag.PRINT);
intentArgs.renderingIntent & RenderingIntentFlag.PRINT
);
// If there's no displayReadyCapability yet, then the operatorList // If there's no displayReadyCapability yet, then the operatorList
// was never requested before. Make the request and create the promise. // was never requested before. Make the request and create the promise.
@ -1512,6 +1521,12 @@ class PDFPageProxy {
} }
this._stats?.time("Rendering"); this._stats?.time("Rendering");
if (!(optionalContentConfig.renderingIntent & renderingIntent)) {
throw new Error(
"Must use the same `intent`-argument when calling the `PDFPageProxy.render` " +
"and `PDFDocumentProxy.getOptionalContentConfig` methods."
);
}
internalRenderTask.initializeGraphics({ internalRenderTask.initializeGraphics({
transparency, transparency,
optionalContentConfig, optionalContentConfig,
@ -2994,10 +3009,10 @@ class WorkerTransport {
return this.messageHandler.sendWithPromise("GetOutline", null); return this.messageHandler.sendWithPromise("GetOutline", null);
} }
getOptionalContentConfig() { getOptionalContentConfig(renderingIntent) {
return this.messageHandler return this.#cacheSimpleMethod("GetOptionalContentConfig").then(
.sendWithPromise("GetOptionalContentConfig", null) data => new OptionalContentConfig(data, renderingIntent)
.then(results => new OptionalContentConfig(results)); );
} }
getPermissions() { getPermissions() {

View File

@ -66,6 +66,7 @@ class DrawLayer {
#createSVG(box) { #createSVG(box) {
const svg = DrawLayer._svgFactory.create(1, 1, /* skipDimensions = */ true); const svg = DrawLayer._svgFactory.create(1, 1, /* skipDimensions = */ true);
this.#parent.append(svg); this.#parent.append(svg);
svg.setAttribute("aria-hidden", true);
DrawLayer.#setBox(svg, box); DrawLayer.#setBox(svg, box);
return svg; return svg;

View File

@ -93,6 +93,7 @@ class ColorPicker {
button.addEventListener("keydown", this.#boundKeyDown); button.addEventListener("keydown", this.#boundKeyDown);
const swatch = (this.#buttonSwatch = document.createElement("span")); const swatch = (this.#buttonSwatch = document.createElement("span"));
swatch.className = "swatch"; swatch.className = "swatch";
swatch.setAttribute("aria-hidden", true);
swatch.style.backgroundColor = this.#defaultColor; swatch.style.backgroundColor = this.#defaultColor;
button.append(swatch); button.append(swatch);
return button; return button;

View File

@ -61,6 +61,8 @@ class HighlightEditor extends AnnotationEditor {
#outlineId = null; #outlineId = null;
#text = "";
#thickness; #thickness;
#methodOfCreation = ""; #methodOfCreation = "";
@ -104,6 +106,7 @@ class HighlightEditor extends AnnotationEditor {
this.#opacity = params.opacity || HighlightEditor._defaultOpacity; this.#opacity = params.opacity || HighlightEditor._defaultOpacity;
this.#boxes = params.boxes || null; this.#boxes = params.boxes || null;
this.#methodOfCreation = params.methodOfCreation || ""; this.#methodOfCreation = params.methodOfCreation || "";
this.#text = params.text || "";
this._isDraggable = false; this._isDraggable = false;
if (params.highlightId > -1) { if (params.highlightId > -1) {
@ -558,6 +561,13 @@ class HighlightEditor extends AnnotationEditor {
} }
const div = super.render(); const div = super.render();
if (this.#text) {
const mark = document.createElement("mark");
div.append(mark);
mark.append(document.createTextNode(this.#text));
// The text is invisible but it's still visible by a screen reader.
mark.className = "visuallyHidden";
}
if (this.#isFreeHighlight) { if (this.#isFreeHighlight) {
div.classList.add("free"); div.classList.add("free");
} else { } else {
@ -565,6 +575,7 @@ class HighlightEditor extends AnnotationEditor {
} }
const highlightDiv = (this.#highlightDiv = document.createElement("div")); const highlightDiv = (this.#highlightDiv = document.createElement("div"));
div.append(highlightDiv); div.append(highlightDiv);
highlightDiv.setAttribute("aria-hidden", "true");
highlightDiv.className = "internal"; highlightDiv.className = "internal";
highlightDiv.style.clipPath = this.#clipPathId; highlightDiv.style.clipPath = this.#clipPathId;
const [parentWidth, parentHeight] = this.parentDimensions; const [parentWidth, parentHeight] = this.parentDimensions;

View File

@ -31,6 +31,7 @@ class EditorToolbar {
render() { render() {
const editToolbar = (this.#toolbar = document.createElement("div")); const editToolbar = (this.#toolbar = document.createElement("div"));
editToolbar.className = "editToolbar"; editToolbar.className = "editToolbar";
editToolbar.setAttribute("role", "toolbar");
editToolbar.addEventListener("contextmenu", noContextMenu); editToolbar.addEventListener("contextmenu", noContextMenu);
editToolbar.addEventListener("pointerdown", EditorToolbar.#pointerDown); editToolbar.addEventListener("pointerdown", EditorToolbar.#pointerDown);
@ -134,4 +135,80 @@ class EditorToolbar {
} }
} }
export { EditorToolbar }; class HighlightToolbar {
#buttons = null;
#toolbar = null;
#uiManager;
constructor(uiManager) {
this.#uiManager = uiManager;
}
#render() {
const editToolbar = (this.#toolbar = document.createElement("div"));
editToolbar.className = "editToolbar";
editToolbar.setAttribute("role", "toolbar");
editToolbar.addEventListener("contextmenu", noContextMenu);
const buttons = (this.#buttons = document.createElement("div"));
buttons.className = "buttons";
editToolbar.append(buttons);
this.#addHighlightButton();
return editToolbar;
}
#getLastPoint(boxes, isLTR) {
let lastY = 0;
let lastX = 0;
for (const box of boxes) {
const y = box.y + box.height;
if (y < lastY) {
continue;
}
const x = box.x + (isLTR ? box.width : 0);
if (y > lastY) {
lastX = x;
lastY = y;
continue;
}
if (isLTR) {
if (x > lastX) {
lastX = x;
}
} else if (x < lastX) {
lastX = x;
}
}
return [isLTR ? 1 - lastX : lastX, lastY];
}
show(parent, boxes, isLTR) {
const [x, y] = this.#getLastPoint(boxes, isLTR);
const { style } = (this.#toolbar ||= this.#render());
parent.append(this.#toolbar);
style.insetInlineEnd = `${100 * x}%`;
style.top = `calc(${100 * y}% + var(--editor-toolbar-vert-offset))`;
}
hide() {
this.#toolbar.remove();
}
#addHighlightButton() {
const button = document.createElement("button");
button.className = "highlightButton";
button.tabIndex = 0;
button.setAttribute("data-l10n-id", `pdfjs-highlight-floating-button`);
button.addEventListener("contextmenu", noContextMenu);
button.addEventListener("click", () => {
this.#uiManager.highlightSelection("floating_button");
});
this.#buttons.append(button);
}
}
export { EditorToolbar, HighlightToolbar };

View File

@ -33,6 +33,7 @@ import {
getRGB, getRGB,
PixelsPerInch, PixelsPerInch,
} from "../display_utils.js"; } from "../display_utils.js";
import { HighlightToolbar } from "./toolbar.js";
function bindEvents(obj, element, names) { function bindEvents(obj, element, names) {
for (const name of names) { for (const name of names) {
@ -555,6 +556,8 @@ class AnnotationEditorUIManager {
#editorsToRescale = new Set(); #editorsToRescale = new Set();
#enableHighlightFloatingButton = false;
#filterFactory = null; #filterFactory = null;
#focusMainContainerTimeoutId = null; #focusMainContainerTimeoutId = null;
@ -563,6 +566,8 @@ class AnnotationEditorUIManager {
#highlightWhenShiftUp = false; #highlightWhenShiftUp = false;
#highlightToolbar = null;
#idManager = new IdManager(); #idManager = new IdManager();
#isEnabled = false; #isEnabled = false;
@ -771,6 +776,7 @@ class AnnotationEditorUIManager {
pdfDocument, pdfDocument,
pageColors, pageColors,
highlightColors, highlightColors,
enableHighlightFloatingButton,
mlManager mlManager
) { ) {
this.#container = container; this.#container = container;
@ -782,10 +788,12 @@ class AnnotationEditorUIManager {
this._eventBus._on("scalechanging", this.#boundOnScaleChanging); this._eventBus._on("scalechanging", this.#boundOnScaleChanging);
this._eventBus._on("rotationchanging", this.#boundOnRotationChanging); this._eventBus._on("rotationchanging", this.#boundOnRotationChanging);
this.#addSelectionListener(); this.#addSelectionListener();
this.#addKeyboardManager();
this.#annotationStorage = pdfDocument.annotationStorage; this.#annotationStorage = pdfDocument.annotationStorage;
this.#filterFactory = pdfDocument.filterFactory; this.#filterFactory = pdfDocument.filterFactory;
this.#pageColors = pageColors; this.#pageColors = pageColors;
this.#highlightColors = highlightColors || null; this.#highlightColors = highlightColors || null;
this.#enableHighlightFloatingButton = enableHighlightFloatingButton;
this.#mlManager = mlManager || null; this.#mlManager = mlManager || null;
this.viewParameters = { this.viewParameters = {
realScale: PixelsPerInch.PDF_TO_CSS_UNITS, realScale: PixelsPerInch.PDF_TO_CSS_UNITS,
@ -821,6 +829,8 @@ class AnnotationEditorUIManager {
this.#selectedEditors.clear(); this.#selectedEditors.clear();
this.#commandManager.destroy(); this.#commandManager.destroy();
this.#altTextManager?.destroy(); this.#altTextManager?.destroy();
this.#highlightToolbar?.hide();
this.#highlightToolbar = null;
if (this.#focusMainContainerTimeoutId) { if (this.#focusMainContainerTimeoutId) {
clearTimeout(this.#focusMainContainerTimeoutId); clearTimeout(this.#focusMainContainerTimeoutId);
this.#focusMainContainerTimeoutId = null; this.#focusMainContainerTimeoutId = null;
@ -946,24 +956,32 @@ class AnnotationEditorUIManager {
this.viewParameters.rotation = pagesRotation; this.viewParameters.rotation = pagesRotation;
} }
#getAnchorElementForSelection({ anchorNode }) {
return anchorNode.nodeType === Node.TEXT_NODE
? anchorNode.parentElement
: anchorNode;
}
highlightSelection(methodOfCreation = "") { highlightSelection(methodOfCreation = "") {
const selection = document.getSelection(); const selection = document.getSelection();
if (!selection || selection.isCollapsed) { if (!selection || selection.isCollapsed) {
return; return;
} }
const { anchorNode, anchorOffset, focusNode, focusOffset } = selection; const { anchorNode, anchorOffset, focusNode, focusOffset } = selection;
const anchorElement = const text = selection.toString();
anchorNode.nodeType === Node.TEXT_NODE const anchorElement = this.#getAnchorElementForSelection(selection);
? anchorNode.parentElement
: anchorNode;
const textLayer = anchorElement.closest(".textLayer"); const textLayer = anchorElement.closest(".textLayer");
const boxes = this.getSelectionBoxes(textLayer); const boxes = this.getSelectionBoxes(textLayer);
if (!boxes) {
return;
}
selection.empty(); selection.empty();
if (this.#mode === AnnotationEditorType.NONE) { if (this.#mode === AnnotationEditorType.NONE) {
this._eventBus.dispatch("showannotationeditorui", { this._eventBus.dispatch("showannotationeditorui", {
source: this, source: this,
mode: AnnotationEditorType.HIGHLIGHT, mode: AnnotationEditorType.HIGHLIGHT,
}); });
this.showAllEditors("highlight", true, /* updateButton = */ true);
} }
for (const layer of this.#allLayers.values()) { for (const layer of this.#allLayers.values()) {
if (layer.hasTextLayer(textLayer)) { if (layer.hasTextLayer(textLayer)) {
@ -974,12 +992,28 @@ class AnnotationEditorUIManager {
anchorOffset, anchorOffset,
focusNode, focusNode,
focusOffset, focusOffset,
text,
}); });
break; break;
} }
} }
} }
#displayHighlightToolbar() {
const selection = document.getSelection();
if (!selection || selection.isCollapsed) {
return;
}
const anchorElement = this.#getAnchorElementForSelection(selection);
const textLayer = anchorElement.closest(".textLayer");
const boxes = this.getSelectionBoxes(textLayer);
if (!boxes) {
return;
}
this.#highlightToolbar ||= new HighlightToolbar(this);
this.#highlightToolbar.show(textLayer, boxes, this.direction === "ltr");
}
/** /**
* Add an editor in the annotation storage. * Add an editor in the annotation storage.
* @param {AnnotationEditor} editor * @param {AnnotationEditor} editor
@ -998,6 +1032,7 @@ class AnnotationEditorUIManager {
const selection = document.getSelection(); const selection = document.getSelection();
if (!selection || selection.isCollapsed) { if (!selection || selection.isCollapsed) {
if (this.#selectedTextNode) { if (this.#selectedTextNode) {
this.#highlightToolbar?.hide();
this.#selectedTextNode = null; this.#selectedTextNode = null;
this.#dispatchUpdateStates({ this.#dispatchUpdateStates({
hasSelectedText: false, hasSelectedText: false,
@ -1010,12 +1045,11 @@ class AnnotationEditorUIManager {
return; return;
} }
const anchorElement = const anchorElement = this.#getAnchorElementForSelection(selection);
anchorNode.nodeType === Node.TEXT_NODE const textLayer = anchorElement.closest(".textLayer");
? anchorNode.parentElement if (!textLayer) {
: anchorNode;
if (!anchorElement.closest(".textLayer")) {
if (this.#selectedTextNode) { if (this.#selectedTextNode) {
this.#highlightToolbar?.hide();
this.#selectedTextNode = null; this.#selectedTextNode = null;
this.#dispatchUpdateStates({ this.#dispatchUpdateStates({
hasSelectedText: false, hasSelectedText: false,
@ -1023,16 +1057,22 @@ class AnnotationEditorUIManager {
} }
return; return;
} }
this.#highlightToolbar?.hide();
this.#selectedTextNode = anchorNode; this.#selectedTextNode = anchorNode;
this.#dispatchUpdateStates({ this.#dispatchUpdateStates({
hasSelectedText: true, hasSelectedText: true,
}); });
if (this.#mode !== AnnotationEditorType.HIGHLIGHT) { if (
this.#mode !== AnnotationEditorType.HIGHLIGHT &&
this.#mode !== AnnotationEditorType.NONE
) {
return; return;
} }
this.showAllEditors("highlight", true, /* updateButton = */ true); if (this.#mode === AnnotationEditorType.HIGHLIGHT) {
this.showAllEditors("highlight", true, /* updateButton = */ true);
}
this.#highlightWhenShiftUp = this.isShiftKeyDown; this.#highlightWhenShiftUp = this.isShiftKeyDown;
if (!this.isShiftKeyDown) { if (!this.isShiftKeyDown) {
@ -1044,7 +1084,7 @@ class AnnotationEditorUIManager {
window.removeEventListener("pointerup", pointerup); window.removeEventListener("pointerup", pointerup);
window.removeEventListener("blur", pointerup); window.removeEventListener("blur", pointerup);
if (e.type === "pointerup") { if (e.type === "pointerup") {
this.highlightSelection("main_toolbar"); this.#onSelectEnd("main_toolbar");
} }
}; };
window.addEventListener("pointerup", pointerup); window.addEventListener("pointerup", pointerup);
@ -1052,6 +1092,14 @@ class AnnotationEditorUIManager {
} }
} }
#onSelectEnd(methodOfCreation = "") {
if (this.#mode === AnnotationEditorType.HIGHLIGHT) {
this.highlightSelection(methodOfCreation);
} else if (this.#enableHighlightFloatingButton) {
this.#displayHighlightToolbar();
}
}
#addSelectionListener() { #addSelectionListener() {
document.addEventListener("selectionchange", this.#boundSelectionChange); document.addEventListener("selectionchange", this.#boundSelectionChange);
} }
@ -1074,7 +1122,7 @@ class AnnotationEditorUIManager {
this.isShiftKeyDown = false; this.isShiftKeyDown = false;
if (this.#highlightWhenShiftUp) { if (this.#highlightWhenShiftUp) {
this.#highlightWhenShiftUp = false; this.#highlightWhenShiftUp = false;
this.highlightSelection("main_toolbar"); this.#onSelectEnd("main_toolbar");
} }
if (!this.hasSelection) { if (!this.hasSelection) {
return; return;
@ -1250,7 +1298,10 @@ class AnnotationEditorUIManager {
if (!this.isShiftKeyDown && event.key === "Shift") { if (!this.isShiftKeyDown && event.key === "Shift") {
this.isShiftKeyDown = true; this.isShiftKeyDown = true;
} }
if (!this.isEditorHandlingKeyboard) { if (
this.#mode !== AnnotationEditorType.NONE &&
!this.isEditorHandlingKeyboard
) {
AnnotationEditorUIManager._keyboardManager.exec(this, event); AnnotationEditorUIManager._keyboardManager.exec(this, event);
} }
} }
@ -1264,7 +1315,7 @@ class AnnotationEditorUIManager {
this.isShiftKeyDown = false; this.isShiftKeyDown = false;
if (this.#highlightWhenShiftUp) { if (this.#highlightWhenShiftUp) {
this.#highlightWhenShiftUp = false; this.#highlightWhenShiftUp = false;
this.highlightSelection("main_toolbar"); this.#onSelectEnd("main_toolbar");
} }
} }
} }
@ -1333,7 +1384,6 @@ class AnnotationEditorUIManager {
setEditingState(isEditing) { setEditingState(isEditing) {
if (isEditing) { if (isEditing) {
this.#addFocusManager(); this.#addFocusManager();
this.#addKeyboardManager();
this.#addCopyPasteListeners(); this.#addCopyPasteListeners();
this.#dispatchUpdateStates({ this.#dispatchUpdateStates({
isEditing: this.#mode !== AnnotationEditorType.NONE, isEditing: this.#mode !== AnnotationEditorType.NONE,
@ -1344,7 +1394,6 @@ class AnnotationEditorUIManager {
}); });
} else { } else {
this.#removeFocusManager(); this.#removeFocusManager();
this.#removeKeyboardManager();
this.#removeCopyPasteListeners(); this.#removeCopyPasteListeners();
this.#dispatchUpdateStates({ this.#dispatchUpdateStates({
isEditing: false, isEditing: false,

View File

@ -424,21 +424,22 @@ class PDFNodeStreamFsFullReader extends BaseFullReader {
path = path.replace(/^\//, ""); path = path.replace(/^\//, "");
} }
fs.lstat(path, (error, stat) => { fs.promises.lstat(path).then(
if (error) { stat => {
// Setting right content length.
this._contentLength = stat.size;
this._setReadableStream(fs.createReadStream(path));
this._headersCapability.resolve();
},
error => {
if (error.code === "ENOENT") { if (error.code === "ENOENT") {
error = new MissingPDFException(`Missing PDF "${path}".`); error = new MissingPDFException(`Missing PDF "${path}".`);
} }
this._storedError = error; this._storedError = error;
this._headersCapability.reject(error); this._headersCapability.reject(error);
return;
} }
// Setting right content length. );
this._contentLength = stat.size;
this._setReadableStream(fs.createReadStream(path));
this._headersCapability.resolve();
});
} }
} }

View File

@ -71,15 +71,7 @@ if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("SKIP_BABEL")) {
} }
const fetchData = function (url) { const fetchData = function (url) {
return new Promise((resolve, reject) => { return fs.promises.readFile(url).then(data => new Uint8Array(data));
fs.readFile(url, (error, data) => {
if (error || !data) {
reject(new Error(error));
return;
}
resolve(new Uint8Array(data));
});
});
}; };
class NodeFilterFactory extends BaseFilterFactory {} class NodeFilterFactory extends BaseFilterFactory {}

View File

@ -13,33 +13,63 @@
* limitations under the License. * limitations under the License.
*/ */
import { info, objectFromMap, unreachable, warn } from "../shared/util.js"; import {
info,
objectFromMap,
RenderingIntentFlag,
unreachable,
warn,
} from "../shared/util.js";
import { MurmurHash3_64 } from "../shared/murmurhash3.js"; import { MurmurHash3_64 } from "../shared/murmurhash3.js";
const INTERNAL = Symbol("INTERNAL"); const INTERNAL = Symbol("INTERNAL");
class OptionalContentGroup { class OptionalContentGroup {
#isDisplay = false;
#isPrint = false;
#userSet = false;
#visible = true; #visible = true;
constructor(name, intent) { constructor(renderingIntent, { name, intent, usage }) {
this.#isDisplay = !!(renderingIntent & RenderingIntentFlag.DISPLAY);
this.#isPrint = !!(renderingIntent & RenderingIntentFlag.PRINT);
this.name = name; this.name = name;
this.intent = intent; this.intent = intent;
this.usage = usage;
} }
/** /**
* @type {boolean} * @type {boolean}
*/ */
get visible() { get visible() {
return this.#visible; if (this.#userSet) {
return this.#visible;
}
if (!this.#visible) {
return false;
}
const { print, view } = this.usage;
if (this.#isDisplay) {
return view?.viewState !== "OFF";
} else if (this.#isPrint) {
return print?.printState !== "OFF";
}
return true;
} }
/** /**
* @ignore * @ignore
*/ */
_setVisible(internal, visible) { _setVisible(internal, visible, userSet = false) {
if (internal !== INTERNAL) { if (internal !== INTERNAL) {
unreachable("Internal method `_setVisible` called."); unreachable("Internal method `_setVisible` called.");
} }
this.#userSet = userSet;
this.#visible = visible; this.#visible = visible;
} }
} }
@ -53,7 +83,9 @@ class OptionalContentConfig {
#order = null; #order = null;
constructor(data) { constructor(data, renderingIntent = RenderingIntentFlag.DISPLAY) {
this.renderingIntent = renderingIntent;
this.name = null; this.name = null;
this.creator = null; this.creator = null;
@ -66,7 +98,7 @@ class OptionalContentConfig {
for (const group of data.groups) { for (const group of data.groups) {
this.#groups.set( this.#groups.set(
group.id, group.id,
new OptionalContentGroup(group.name, group.intent) new OptionalContentGroup(renderingIntent, group)
); );
} }
@ -198,11 +230,44 @@ class OptionalContentConfig {
} }
setVisibility(id, visible = true) { setVisibility(id, visible = true) {
if (!this.#groups.has(id)) { const group = this.#groups.get(id);
if (!group) {
warn(`Optional content group not found: ${id}`); warn(`Optional content group not found: ${id}`);
return; return;
} }
this.#groups.get(id)._setVisible(INTERNAL, !!visible); group._setVisible(INTERNAL, !!visible, /* userSet = */ true);
this.#cachedGetHash = null;
}
setOCGState({ state, preserveRB }) {
let operator;
for (const elem of state) {
switch (elem) {
case "ON":
case "OFF":
case "Toggle":
operator = elem;
continue;
}
const group = this.#groups.get(elem);
if (!group) {
continue;
}
switch (operator) {
case "ON":
group._setVisible(INTERNAL, true);
break;
case "OFF":
group._setVisible(INTERNAL, false);
break;
case "Toggle":
group._setVisible(INTERNAL, !group.visible);
break;
}
}
this.#cachedGetHash = null; this.#cachedGetHash = null;
} }

View File

@ -684,7 +684,9 @@ class Driver {
} }
task.pdfDoc = doc; task.pdfDoc = doc;
task.optionalContentConfigPromise = doc.getOptionalContentConfig(); task.optionalContentConfigPromise = doc.getOptionalContentConfig({
intent: task.print ? "print" : "display",
});
if (task.optionalContent) { if (task.optionalContent) {
const entries = Object.entries(task.optionalContent), const entries = Object.entries(task.optionalContent),

View File

@ -35,6 +35,7 @@ async function runTests(results) {
"scripting_spec.mjs", "scripting_spec.mjs",
"stamp_editor_spec.mjs", "stamp_editor_spec.mjs",
"text_field_spec.mjs", "text_field_spec.mjs",
"viewer_spec.mjs",
], ],
}); });

View File

@ -46,8 +46,11 @@ const getXY = (page, selector) =>
return `${bbox.x}::${bbox.y}`; return `${bbox.x}::${bbox.y}`;
}, selector); }, selector);
const getSpanRectFromText = (page, pageNumber, text) => const getSpanRectFromText = async (page, pageNumber, text) => {
page.evaluate( await page.waitForSelector(
`.page[data-page-number="${pageNumber}"] > .textLayer .endOfContent`
);
return page.evaluate(
(number, content) => { (number, content) => {
for (const el of document.querySelectorAll( for (const el of document.querySelectorAll(
`.page[data-page-number="${number}"] > .textLayer > span` `.page[data-page-number="${number}"] > .textLayer > span`
@ -62,6 +65,7 @@ const getSpanRectFromText = (page, pageNumber, text) =>
pageNumber, pageNumber,
text text
); );
};
describe("Highlight Editor", () => { describe("Highlight Editor", () => {
describe("Editor must be removed without exception", () => { describe("Editor must be removed without exception", () => {
@ -1510,4 +1514,46 @@ describe("Highlight Editor", () => {
); );
}); });
}); });
describe("Highlight from floating highlight button", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".annotationEditorLayer",
null,
null,
{ highlightEditorColors: "red=#AB0000" }
);
});
afterAll(async () => {
await closePages(pages);
});
it("must check that clicking on the highlight floating button triggers an highlight", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const rect = await getSpanRectFromText(page, 1, "Abstract");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(".textLayer .highlightButton");
await page.click(".textLayer .highlightButton");
await page.waitForSelector(getEditorSelector(0));
const usedColor = await page.evaluate(() => {
const highlight = document.querySelector(
`.page[data-page-number = "1"] .canvasWrapper > svg.highlight`
);
return highlight.getAttribute("fill");
});
expect(usedColor).withContext(`In ${browserName}`).toEqual("#AB0000");
})
);
});
});
}); });

View File

@ -0,0 +1,58 @@
/* Copyright 2024 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { closePages, loadAndWait } from "./test_utils.mjs";
describe("PDF viewer", () => {
describe("Zoom with the mouse wheel", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".textLayer .endOfContent", 1000);
});
afterAll(async () => {
await closePages(pages);
});
it("must check that we can zoom with the mouse wheel and pressed control key", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
if (browserName === "firefox") {
// Skip this test for Firefox, as it's not working correctly.
// See https://github.com/puppeteer/puppeteer/issues/12093.
// TODO: Remove this check once the issue is resolved.
return;
}
await page.keyboard.down("Control");
let zoom = 10;
const zoomGetter = () =>
page.evaluate(
() => window.PDFViewerApplication.pdfViewer.currentScale
);
while (zoom > 0.1) {
await page.mouse.wheel({ deltaY: 100 });
zoom = await zoomGetter();
}
while (zoom < 10) {
await page.mouse.wheel({ deltaY: -100 });
zoom = await zoomGetter();
}
await page.keyboard.up("Control");
})
);
});
});
});

View File

@ -0,0 +1 @@
https://bugzilla.mozilla.org/attachment.cgi?id=9327375

View File

@ -4016,6 +4016,23 @@
"lastPage": 5, "lastPage": 5,
"type": "eq" "type": "eq"
}, },
{
"id": "bug1826783-display",
"file": "pdfs/bug1826783.pdf",
"md5": "93e706efee15dd7b32d32d66f15a3ea2",
"rounds": 1,
"link": true,
"type": "eq"
},
{
"id": "bug1826783-print",
"file": "pdfs/bug1826783.pdf",
"md5": "93e706efee15dd7b32d32d66f15a3ea2",
"rounds": 1,
"link": true,
"type": "eq",
"print": true
},
{ {
"id": "issue8586", "id": "issue8586",
"file": "pdfs/issue8586.pdf", "file": "pdfs/issue8586.pdf",

View File

@ -0,0 +1,41 @@
/* Copyright 2024 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AppOptions, OptionKind } from "../../web/app_options.js";
import { objectSize } from "../../src/shared/util.js";
describe("AppOptions", function () {
it("checks that getAll returns data, for every OptionKind", function () {
const KIND_NAMES = ["BROWSER", "VIEWER", "API", "WORKER", "PREFERENCE"];
for (const name of KIND_NAMES) {
const kind = OptionKind[name];
expect(typeof kind).toEqual("number");
const options = AppOptions.getAll(kind);
expect(objectSize(options)).toBeGreaterThan(0);
}
});
it('checks that the number of "PREFERENCE" options does *not* exceed the maximum in mozilla-central', function () {
// If the following constant is updated then you *MUST* make the same change
// in mozilla-central as well to ensure that preference-fetching works; see
// https://searchfox.org/mozilla-central/source/toolkit/components/pdfjs/content/PdfStreamConverter.sys.mjs
const MAX_NUMBER_OF_PREFS = 50;
const options = AppOptions.getAll(OptionKind.PREFERENCE);
expect(objectSize(options)).toBeLessThanOrEqual(MAX_NUMBER_OF_PREFS);
});
});

View File

@ -7,6 +7,7 @@
"annotation_spec.js", "annotation_spec.js",
"annotation_storage_spec.js", "annotation_storage_spec.js",
"api_spec.js", "api_spec.js",
"app_options_spec.js",
"bidi_spec.js", "bidi_spec.js",
"cff_parser_spec.js", "cff_parser_spec.js",
"cmap_spec.js", "cmap_spec.js",

View File

@ -50,6 +50,7 @@ async function initializePDFJS(callback) {
"pdfjs-test/unit/annotation_spec.js", "pdfjs-test/unit/annotation_spec.js",
"pdfjs-test/unit/annotation_storage_spec.js", "pdfjs-test/unit/annotation_storage_spec.js",
"pdfjs-test/unit/api_spec.js", "pdfjs-test/unit/api_spec.js",
"pdfjs-test/unit/app_options_spec.js",
"pdfjs-test/unit/bidi_spec.js", "pdfjs-test/unit/bidi_spec.js",
"pdfjs-test/unit/cff_parser_spec.js", "pdfjs-test/unit/cff_parser_spec.js",
"pdfjs-test/unit/cmap_spec.js", "pdfjs-test/unit/cmap_spec.js",

View File

@ -48,6 +48,19 @@
pointer; pointer;
} }
/* The following class is used to hide an element but keep it available to
* for screen readers. */
.visuallyHidden {
position: absolute;
border: 0;
margin: 0;
padding: 0;
width: 0;
height: 0;
overflow: hidden;
white-space: nowrap;
}
.textLayer.highlighting { .textLayer.highlighting {
cursor: var(--editorFreeHighlight-editing-cursor); cursor: var(--editorFreeHighlight-editing-cursor);
@ -182,10 +195,12 @@
} }
.annotationEditorLayer .annotationEditorLayer
:is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor) { :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),
.textLayer {
.editToolbar { .editToolbar {
--editor-toolbar-delete-image: url(images/editor-toolbar-delete.svg); --editor-toolbar-delete-image: url(images/editor-toolbar-delete.svg);
--editor-toolbar-bg-color: #f0f0f4; --editor-toolbar-bg-color: #f0f0f4;
--editor-toolbar-highlight-image: url(images/toolbarButton-editorHighlight.svg);
--editor-toolbar-fg-color: #2e2e56; --editor-toolbar-fg-color: #2e2e56;
--editor-toolbar-border-color: #8f8f9d; --editor-toolbar-border-color: #8f8f9d;
--editor-toolbar-hover-border-color: var(--editor-toolbar-border-color); --editor-toolbar-hover-border-color: var(--editor-toolbar-border-color);
@ -271,6 +286,25 @@
margin-inline: 2px; margin-inline: 2px;
} }
.highlightButton {
width: var(--editor-toolbar-height);
&::before {
content: "";
mask-image: var(--editor-toolbar-highlight-image);
mask-repeat: no-repeat;
mask-position: center;
display: inline-block;
background-color: var(--editor-toolbar-fg-color);
width: 100%;
height: 100%;
}
&:hover::before {
background-color: var(--editor-toolbar-hover-fg-color);
}
}
.delete { .delete {
width: var(--editor-toolbar-height); width: var(--editor-toolbar-height);

View File

@ -424,6 +424,9 @@ const PDFViewerApplication = {
annotationMode: AppOptions.get("annotationMode"), annotationMode: AppOptions.get("annotationMode"),
annotationEditorMode, annotationEditorMode,
annotationEditorHighlightColors: AppOptions.get("highlightEditorColors"), annotationEditorHighlightColors: AppOptions.get("highlightEditorColors"),
enableHighlightFloatingButton: AppOptions.get(
"enableHighlightFloatingButton"
),
imageResourcesPath: AppOptions.get("imageResourcesPath"), imageResourcesPath: AppOptions.get("imageResourcesPath"),
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"), enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
maxCanvasPixels: AppOptions.get("maxCanvasPixels"), maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
@ -1796,7 +1799,6 @@ const PDFViewerApplication = {
pagesOverview: this.pdfViewer.getPagesOverview(), pagesOverview: this.pdfViewer.getPagesOverview(),
printContainer: this.appConfig.printContainer, printContainer: this.appConfig.printContainer,
printResolution: AppOptions.get("printResolution"), printResolution: AppOptions.get("printResolution"),
optionalContentConfigPromise: this.pdfViewer.optionalContentConfigPromise,
printAnnotationStoragePromise: this._printAnnotationStoragePromise, printAnnotationStoragePromise: this._printAnnotationStoragePromise,
}); });
this.forceRendering(); this.forceRendering();
@ -2022,8 +2024,9 @@ const PDFViewerApplication = {
}); });
const scroll = (_boundEvents.mainContainerScroll = () => { const scroll = (_boundEvents.mainContainerScroll = () => {
if ( if (
this._lastScrollTop === mainContainer.scrollTop && this._isCtrlKeyDown ||
this._lastScrollLeft === mainContainer.scrollLeft (this._lastScrollTop === mainContainer.scrollTop &&
this._lastScrollLeft === mainContainer.scrollLeft)
) { ) {
return; return;
} }

View File

@ -143,6 +143,14 @@ const defaultOptions = {
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"), value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"),
kind: OptionKind.VIEWER + OptionKind.PREFERENCE, kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
}, },
enableHighlightFloatingButton: {
// We'll probably want to make some experiments before enabling this
// in Firefox release, but it has to be temporary.
// TODO: remove it when unnecessary.
/** @type {boolean} */
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"),
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
enableML: { enableML: {
/** @type {boolean} */ /** @type {boolean} */
value: false, value: false,
@ -409,57 +417,64 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
const userOptions = Object.create(null); const userOptions = Object.create(null);
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING || LIB")) {
// Ensure that the `defaultOptions` are correctly specified.
for (const name in defaultOptions) {
const { value, kind } = defaultOptions[name];
if (kind & OptionKind.PREFERENCE) {
if (kind === OptionKind.PREFERENCE) {
throw new Error(`Cannot use only "PREFERENCE" kind: ${name}`);
}
if (kind & OptionKind.BROWSER) {
throw new Error(`Cannot mix "PREFERENCE" and "BROWSER" kind: ${name}`);
}
if (compatibilityParams[name] !== undefined) {
throw new Error(
`Should not have compatibility-value for "PREFERENCE" kind: ${name}`
);
}
// Only "simple" preference-values are allowed.
if (
typeof value !== "boolean" &&
typeof value !== "string" &&
!Number.isInteger(value)
) {
throw new Error(`Invalid value for "PREFERENCE" kind: ${name}`);
}
}
}
}
class AppOptions { class AppOptions {
constructor() { constructor() {
throw new Error("Cannot initialize AppOptions."); throw new Error("Cannot initialize AppOptions.");
} }
static get(name) { static getCompat(name) {
const userOption = userOptions[name]; return compatibilityParams[name] ?? undefined;
if (userOption !== undefined) {
return userOption;
}
const defaultOption = defaultOptions[name];
if (defaultOption !== undefined) {
return compatibilityParams[name] ?? defaultOption.value;
}
return undefined;
} }
static getAll(kind = null) { static get(name) {
return (
userOptions[name] ??
compatibilityParams[name] ??
defaultOptions[name]?.value ??
undefined
);
}
static getAll(kind = null, defaultOnly = false) {
const options = Object.create(null); const options = Object.create(null);
for (const name in defaultOptions) { for (const name in defaultOptions) {
const defaultOption = defaultOptions[name]; const defaultOption = defaultOptions[name];
if (kind) {
if (!(kind & defaultOption.kind)) {
continue;
}
if (
(typeof PDFJSDev === "undefined" || PDFJSDev.test("LIB")) &&
kind === OptionKind.PREFERENCE
) {
if (defaultOption.kind & OptionKind.BROWSER) {
throw new Error(`Invalid kind for preference: ${name}`);
}
const value = defaultOption.value,
valueType = typeof value;
if ( if (kind && !(kind & defaultOption.kind)) {
valueType === "boolean" || continue;
valueType === "string" ||
(valueType === "number" && Number.isInteger(value))
) {
options[name] = value;
continue;
}
throw new Error(`Invalid type for preference: ${name}`);
}
} }
const userOption = userOptions[name]; options[name] = defaultOnly
options[name] = ? defaultOption.value
userOption !== undefined : userOptions[name] ?? compatibilityParams[name] ?? defaultOption.value;
? userOption
: compatibilityParams[name] ?? defaultOption.value;
} }
return options; return options;
} }
@ -493,4 +508,4 @@ class AppOptions {
} }
} }
export { AppOptions, compatibilityParams, OptionKind }; export { AppOptions, OptionKind };

View File

@ -119,15 +119,15 @@ class FirefoxPrintService {
pagesOverview, pagesOverview,
printContainer, printContainer,
printResolution, printResolution,
optionalContentConfigPromise = null,
printAnnotationStoragePromise = null, printAnnotationStoragePromise = null,
}) { }) {
this.pdfDocument = pdfDocument; this.pdfDocument = pdfDocument;
this.pagesOverview = pagesOverview; this.pagesOverview = pagesOverview;
this.printContainer = printContainer; this.printContainer = printContainer;
this._printResolution = printResolution || 150; this._printResolution = printResolution || 150;
this._optionalContentConfigPromise = this._optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({
optionalContentConfigPromise || pdfDocument.getOptionalContentConfig(); intent: "print",
});
this._printAnnotationStoragePromise = this._printAnnotationStoragePromise =
printAnnotationStoragePromise || Promise.resolve(); printAnnotationStoragePromise || Promise.resolve();
} }

View File

@ -258,14 +258,8 @@ if (PDFJSDev.test("GECKOVIEW")) {
const hasWillPrint = const hasWillPrint =
pdfViewer.enableScripting && pdfViewer.enableScripting &&
!!(await pdfDocument.getJSActions())?.WillPrint; !!(await pdfDocument.getJSActions())?.WillPrint;
const hasUnchangedOptionalContent = (
await pdfViewer.optionalContentConfigPromise
).hasInitialVisibility;
result = result = hasUnchangedAnnotations && !hasWillPrint;
hasUnchangedAnnotations &&
!hasWillPrint &&
hasUnchangedOptionalContent;
} catch { } catch {
console.warn("Unable to check if the document can be downloaded."); console.warn("Unable to check if the document can be downloaded.");
} }

View File

@ -182,7 +182,7 @@ class PDFLayerViewer extends BaseTreeViewer {
} }
const pdfDocument = this._pdfDocument; const pdfDocument = this._pdfDocument;
const optionalContentConfig = await (promise || const optionalContentConfig = await (promise ||
pdfDocument.getOptionalContentConfig()); pdfDocument.getOptionalContentConfig({ intent: "display" }));
if (pdfDocument !== this._pdfDocument) { if (pdfDocument !== this._pdfDocument) {
return; // The document was closed while the optional content resolved. return; // The document was closed while the optional content resolved.

View File

@ -517,31 +517,7 @@ class PDFLinkService {
if (pdfDocument !== this.pdfDocument) { if (pdfDocument !== this.pdfDocument) {
return; // The document was closed while the optional content resolved. return; // The document was closed while the optional content resolved.
} }
let operator; optionalContentConfig.setOCGState(action);
for (const elem of action.state) {
switch (elem) {
case "ON":
case "OFF":
case "Toggle":
operator = elem;
continue;
}
switch (operator) {
case "ON":
optionalContentConfig.setVisibility(elem, true);
break;
case "OFF":
optionalContentConfig.setVisibility(elem, false);
break;
case "Toggle":
const group = optionalContentConfig.getGroup(elem);
if (group) {
optionalContentConfig.setVisibility(elem, !group.visible);
}
break;
}
}
this.pdfViewer.optionalContentConfigPromise = Promise.resolve( this.pdfViewer.optionalContentConfigPromise = Promise.resolve(
optionalContentConfig optionalContentConfig

View File

@ -41,7 +41,7 @@ import {
} from "./ui_utils.js"; } from "./ui_utils.js";
import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js"; import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js";
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js"; import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
import { compatibilityParams } from "./app_options.js"; import { AppOptions } from "./app_options.js";
import { DrawLayerBuilder } from "./draw_layer_builder.js"; import { DrawLayerBuilder } from "./draw_layer_builder.js";
import { GenericL10n } from "web-null_l10n"; import { GenericL10n } from "web-null_l10n";
import { SimpleLinkService } from "./pdf_link_service.js"; import { SimpleLinkService } from "./pdf_link_service.js";
@ -83,8 +83,6 @@ import { XfaLayerBuilder } from "./xfa_layer_builder.js";
* the necessary layer-properties. * the necessary layer-properties.
*/ */
const MAX_CANVAS_PIXELS = compatibilityParams.maxCanvasPixels || 16777216;
const DEFAULT_LAYER_PROPERTIES = const DEFAULT_LAYER_PROPERTIES =
typeof PDFJSDev === "undefined" || !PDFJSDev.test("COMPONENTS") typeof PDFJSDev === "undefined" || !PDFJSDev.test("COMPONENTS")
? null ? null
@ -152,7 +150,9 @@ class PDFPageView {
this.#annotationMode = this.#annotationMode =
options.annotationMode ?? AnnotationMode.ENABLE_FORMS; options.annotationMode ?? AnnotationMode.ENABLE_FORMS;
this.imageResourcesPath = options.imageResourcesPath || ""; this.imageResourcesPath = options.imageResourcesPath || "";
this.maxCanvasPixels = options.maxCanvasPixels ?? MAX_CANVAS_PIXELS; this.maxCanvasPixels =
options.maxCanvasPixels ??
(AppOptions.getCompat("maxCanvasPixels") || 16777216);
this.pageColors = options.pageColors || null; this.pageColors = options.pageColors || null;
this.eventBus = options.eventBus; this.eventBus = options.eventBus;

View File

@ -13,7 +13,12 @@
* limitations under the License. * limitations under the License.
*/ */
import { AnnotationMode, PixelsPerInch, shadow } from "pdfjs-lib"; import {
AnnotationMode,
PixelsPerInch,
RenderingCancelledException,
shadow,
} from "pdfjs-lib";
import { getXfaHtmlForPrinting } from "./print_utils.js"; import { getXfaHtmlForPrinting } from "./print_utils.js";
let activeService = null; let activeService = null;
@ -58,7 +63,14 @@ function renderPage(
optionalContentConfigPromise, optionalContentConfigPromise,
printAnnotationStorage, printAnnotationStorage,
}; };
return pdfPage.render(renderContext).promise; const renderTask = pdfPage.render(renderContext);
return renderTask.promise.catch(reason => {
if (!(reason instanceof RenderingCancelledException)) {
console.error(reason);
}
throw reason;
});
}); });
} }
@ -68,15 +80,15 @@ class PDFPrintService {
pagesOverview, pagesOverview,
printContainer, printContainer,
printResolution, printResolution,
optionalContentConfigPromise = null,
printAnnotationStoragePromise = null, printAnnotationStoragePromise = null,
}) { }) {
this.pdfDocument = pdfDocument; this.pdfDocument = pdfDocument;
this.pagesOverview = pagesOverview; this.pagesOverview = pagesOverview;
this.printContainer = printContainer; this.printContainer = printContainer;
this._printResolution = printResolution || 150; this._printResolution = printResolution || 150;
this._optionalContentConfigPromise = this._optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({
optionalContentConfigPromise || pdfDocument.getOptionalContentConfig(); intent: "print",
});
this._printAnnotationStoragePromise = this._printAnnotationStoragePromise =
printAnnotationStoragePromise || Promise.resolve(); printAnnotationStoragePromise || Promise.resolve();
this.currentPage = -1; this.currentPage = -1;

View File

@ -189,7 +189,9 @@ class PDFThumbnailViewer {
return; return;
} }
const firstPagePromise = pdfDocument.getPage(1); const firstPagePromise = pdfDocument.getPage(1);
const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig(); const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({
intent: "display",
});
firstPagePromise firstPagePromise
.then(firstPdfPage => { .then(firstPdfPage => {

View File

@ -214,6 +214,8 @@ class PDFViewer {
#copyCallbackBound = null; #copyCallbackBound = null;
#enableHighlightFloatingButton = false;
#enablePermissions = false; #enablePermissions = false;
#mlManager = null; #mlManager = null;
@ -282,6 +284,8 @@ class PDFViewer {
options.annotationEditorMode ?? AnnotationEditorType.NONE; options.annotationEditorMode ?? AnnotationEditorType.NONE;
this.#annotationEditorHighlightColors = this.#annotationEditorHighlightColors =
options.annotationEditorHighlightColors || null; options.annotationEditorHighlightColors || null;
this.#enableHighlightFloatingButton =
options.enableHighlightFloatingButton === true;
this.imageResourcesPath = options.imageResourcesPath || ""; this.imageResourcesPath = options.imageResourcesPath || "";
this.enablePrintAutoRotate = options.enablePrintAutoRotate || false; this.enablePrintAutoRotate = options.enablePrintAutoRotate || false;
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
@ -781,7 +785,9 @@ class PDFViewer {
const pagesCount = pdfDocument.numPages; const pagesCount = pdfDocument.numPages;
const firstPagePromise = pdfDocument.getPage(1); const firstPagePromise = pdfDocument.getPage(1);
// Rendering (potentially) depends on this, hence fetching it immediately. // Rendering (potentially) depends on this, hence fetching it immediately.
const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig(); const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({
intent: "display",
});
const permissionsPromise = this.#enablePermissions const permissionsPromise = this.#enablePermissions
? pdfDocument.getPermissions() ? pdfDocument.getPermissions()
: Promise.resolve(); : Promise.resolve();
@ -861,6 +867,7 @@ class PDFViewer {
pdfDocument, pdfDocument,
this.pageColors, this.pageColors,
this.#annotationEditorHighlightColors, this.#annotationEditorHighlightColors,
this.#enableHighlightFloatingButton,
this.#mlManager this.#mlManager
); );
this.eventBus.dispatch("annotationeditoruimanager", { this.eventBus.dispatch("annotationeditoruimanager", {
@ -1822,7 +1829,7 @@ class PDFViewer {
console.error("optionalContentConfigPromise: Not initialized yet."); console.error("optionalContentConfigPromise: Not initialized yet.");
// Prevent issues if the getter is accessed *before* the `onePageRendered` // Prevent issues if the getter is accessed *before* the `onePageRendered`
// promise has resolved; won't (normally) happen in the default viewer. // promise has resolved; won't (normally) happen in the default viewer.
return this.pdfDocument.getOptionalContentConfig(); return this.pdfDocument.getOptionalContentConfig({ intent: "display" });
} }
return this._optionalContentConfigPromise; return this._optionalContentConfigPromise;
} }

View File

@ -23,7 +23,7 @@ import { AppOptions, OptionKind } from "./app_options.js";
class BasePreferences { class BasePreferences {
#defaults = Object.freeze( #defaults = Object.freeze(
typeof PDFJSDev === "undefined" typeof PDFJSDev === "undefined"
? AppOptions.getAll(OptionKind.PREFERENCE) ? AppOptions.getAll(OptionKind.PREFERENCE, /* defaultOnly = */ true)
: PDFJSDev.eval("DEFAULT_PREFERENCES") : PDFJSDev.eval("DEFAULT_PREFERENCES")
); );
@ -48,7 +48,7 @@ class BasePreferences {
({ browserPrefs, prefs }) => { ({ browserPrefs, prefs }) => {
const BROWSER_PREFS = const BROWSER_PREFS =
typeof PDFJSDev === "undefined" typeof PDFJSDev === "undefined"
? AppOptions.getAll(OptionKind.BROWSER) ? AppOptions.getAll(OptionKind.BROWSER, /* defaultOnly = */ true)
: PDFJSDev.eval("BROWSER_PREFERENCES"); : PDFJSDev.eval("BROWSER_PREFERENCES");
const options = Object.create(null); const options = Object.create(null);