Compare commits
16 Commits
b14f696071
...
2a68724b53
Author | SHA1 | Date | |
---|---|---|---|
|
2a68724b53 | ||
|
65d618635c | ||
|
30e69956db | ||
|
e650b95253 | ||
|
a7d47af474 | ||
|
b54887cfab | ||
|
b4267cd294 | ||
|
eb160726ee | ||
|
70b6ddc5d9 | ||
|
3c78ff5fb0 | ||
|
e647311a89 | ||
|
f676c2c0c8 | ||
|
dab8a2eaa4 | ||
|
db2849cc17 | ||
|
38004b65b1 | ||
|
90b2664622 |
@ -72,6 +72,10 @@
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"enableHighlightFloatingButton": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"highlightEditorColors": {
|
||||
"type": "string",
|
||||
"default": "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F"
|
||||
|
28
gulpfile.mjs
28
gulpfile.mjs
@ -863,11 +863,17 @@ async function parseDefaultPreferences(dir) {
|
||||
"./" + 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) {
|
||||
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) {
|
||||
throw new Error("No default preferences found.");
|
||||
}
|
||||
@ -2222,25 +2228,15 @@ gulp.task(
|
||||
])
|
||||
.pipe(gulp.dest(DIST_DIR + "legacy/build/")),
|
||||
gulp
|
||||
.src(MINIFIED_DIR + "build/pdf.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")
|
||||
.src(MINIFIED_DIR + "build/{pdf,pdf.worker,pdf.sandbox}.min.mjs")
|
||||
.pipe(gulp.dest(DIST_DIR + "build/")),
|
||||
gulp
|
||||
.src(MINIFIED_DIR + "image_decoders/pdf.image_decoders.min.mjs")
|
||||
.pipe(gulp.dest(DIST_DIR + "image_decoders/")),
|
||||
gulp
|
||||
.src(MINIFIED_LEGACY_DIR + "build/pdf.min.mjs")
|
||||
.pipe(gulp.dest(DIST_DIR + "legacy/build/")),
|
||||
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")
|
||||
.src(
|
||||
MINIFIED_LEGACY_DIR + "build/{pdf,pdf.worker,pdf.sandbox}.min.mjs"
|
||||
)
|
||||
.pipe(gulp.dest(DIST_DIR + "legacy/build/")),
|
||||
gulp
|
||||
.src(
|
||||
|
@ -318,6 +318,8 @@ pdfjs-editor-stamp-button-label = Add or edit images
|
||||
pdfjs-editor-highlight-button =
|
||||
.title = Highlight
|
||||
pdfjs-editor-highlight-button-label = Highlight
|
||||
pdfjs-highlight-floating-button =
|
||||
.title = Highlight
|
||||
|
||||
## Remove button for the various kind of editor.
|
||||
|
||||
|
@ -445,20 +445,10 @@ class Catalog {
|
||||
continue;
|
||||
}
|
||||
groupRefs.put(groupRef);
|
||||
const group = this.xref.fetch(groupRef);
|
||||
groups.push({
|
||||
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,
|
||||
});
|
||||
|
||||
groups.push(this.#readOptionalContentGroup(groupRef));
|
||||
}
|
||||
config = this._readOptionalContentConfig(defaultConfig, groupRefs);
|
||||
config = this.#readOptionalContentConfig(defaultConfig, groupRefs);
|
||||
config.groups = groups;
|
||||
} catch (ex) {
|
||||
if (ex instanceof MissingDataException) {
|
||||
@ -469,7 +459,65 @@ class Catalog {
|
||||
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) {
|
||||
const onParsed = [];
|
||||
if (Array.isArray(refs)) {
|
||||
|
@ -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
|
||||
* an {@link OptionalContentConfig} that contains all the optional content
|
||||
* groups (assuming that the document has any).
|
||||
*/
|
||||
getOptionalContentConfig() {
|
||||
return this._transport.getOptionalContentConfig();
|
||||
getOptionalContentConfig({ intent = "display" } = {}) {
|
||||
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
|
||||
* {Array} of the annotation objects.
|
||||
*/
|
||||
getAnnotations({ intent = "display" } = {}) {
|
||||
const intentArgs = this._transport.getRenderingIntent(intent);
|
||||
const { renderingIntent } = this._transport.getRenderingIntent(intent);
|
||||
|
||||
return this._transport.getAnnotations(
|
||||
this._pageIndex,
|
||||
intentArgs.renderingIntent
|
||||
);
|
||||
return this._transport.getAnnotations(this._pageIndex, renderingIntent);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1411,20 +1422,20 @@ class PDFPageProxy {
|
||||
annotationMode,
|
||||
printAnnotationStorage
|
||||
);
|
||||
const { renderingIntent, cacheKey } = intentArgs;
|
||||
// If there was a pending destroy, cancel it so no cleanup happens during
|
||||
// this call to render...
|
||||
this.#pendingCleanup = false;
|
||||
// ... and ensure that a delayed cleanup is always aborted.
|
||||
this.#abortDelayedCleanup();
|
||||
|
||||
if (!optionalContentConfigPromise) {
|
||||
optionalContentConfigPromise = this._transport.getOptionalContentConfig();
|
||||
}
|
||||
optionalContentConfigPromise ||=
|
||||
this._transport.getOptionalContentConfig(renderingIntent);
|
||||
|
||||
let intentState = this._intentStates.get(intentArgs.cacheKey);
|
||||
let intentState = this._intentStates.get(cacheKey);
|
||||
if (!intentState) {
|
||||
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.
|
||||
@ -1433,9 +1444,7 @@ class PDFPageProxy {
|
||||
intentState.streamReaderCancelTimeout = null;
|
||||
}
|
||||
|
||||
const intentPrint = !!(
|
||||
intentArgs.renderingIntent & RenderingIntentFlag.PRINT
|
||||
);
|
||||
const intentPrint = !!(renderingIntent & RenderingIntentFlag.PRINT);
|
||||
|
||||
// If there's no displayReadyCapability yet, then the operatorList
|
||||
// was never requested before. Make the request and create the promise.
|
||||
@ -1512,6 +1521,12 @@ class PDFPageProxy {
|
||||
}
|
||||
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({
|
||||
transparency,
|
||||
optionalContentConfig,
|
||||
@ -2994,10 +3009,10 @@ class WorkerTransport {
|
||||
return this.messageHandler.sendWithPromise("GetOutline", null);
|
||||
}
|
||||
|
||||
getOptionalContentConfig() {
|
||||
return this.messageHandler
|
||||
.sendWithPromise("GetOptionalContentConfig", null)
|
||||
.then(results => new OptionalContentConfig(results));
|
||||
getOptionalContentConfig(renderingIntent) {
|
||||
return this.#cacheSimpleMethod("GetOptionalContentConfig").then(
|
||||
data => new OptionalContentConfig(data, renderingIntent)
|
||||
);
|
||||
}
|
||||
|
||||
getPermissions() {
|
||||
|
@ -66,6 +66,7 @@ class DrawLayer {
|
||||
#createSVG(box) {
|
||||
const svg = DrawLayer._svgFactory.create(1, 1, /* skipDimensions = */ true);
|
||||
this.#parent.append(svg);
|
||||
svg.setAttribute("aria-hidden", true);
|
||||
DrawLayer.#setBox(svg, box);
|
||||
|
||||
return svg;
|
||||
|
@ -93,6 +93,7 @@ class ColorPicker {
|
||||
button.addEventListener("keydown", this.#boundKeyDown);
|
||||
const swatch = (this.#buttonSwatch = document.createElement("span"));
|
||||
swatch.className = "swatch";
|
||||
swatch.setAttribute("aria-hidden", true);
|
||||
swatch.style.backgroundColor = this.#defaultColor;
|
||||
button.append(swatch);
|
||||
return button;
|
||||
|
@ -61,6 +61,8 @@ class HighlightEditor extends AnnotationEditor {
|
||||
|
||||
#outlineId = null;
|
||||
|
||||
#text = "";
|
||||
|
||||
#thickness;
|
||||
|
||||
#methodOfCreation = "";
|
||||
@ -104,6 +106,7 @@ class HighlightEditor extends AnnotationEditor {
|
||||
this.#opacity = params.opacity || HighlightEditor._defaultOpacity;
|
||||
this.#boxes = params.boxes || null;
|
||||
this.#methodOfCreation = params.methodOfCreation || "";
|
||||
this.#text = params.text || "";
|
||||
this._isDraggable = false;
|
||||
|
||||
if (params.highlightId > -1) {
|
||||
@ -558,6 +561,13 @@ class HighlightEditor extends AnnotationEditor {
|
||||
}
|
||||
|
||||
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) {
|
||||
div.classList.add("free");
|
||||
} else {
|
||||
@ -565,6 +575,7 @@ class HighlightEditor extends AnnotationEditor {
|
||||
}
|
||||
const highlightDiv = (this.#highlightDiv = document.createElement("div"));
|
||||
div.append(highlightDiv);
|
||||
highlightDiv.setAttribute("aria-hidden", "true");
|
||||
highlightDiv.className = "internal";
|
||||
highlightDiv.style.clipPath = this.#clipPathId;
|
||||
const [parentWidth, parentHeight] = this.parentDimensions;
|
||||
|
@ -31,6 +31,7 @@ class EditorToolbar {
|
||||
render() {
|
||||
const editToolbar = (this.#toolbar = document.createElement("div"));
|
||||
editToolbar.className = "editToolbar";
|
||||
editToolbar.setAttribute("role", "toolbar");
|
||||
editToolbar.addEventListener("contextmenu", noContextMenu);
|
||||
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 };
|
||||
|
@ -33,6 +33,7 @@ import {
|
||||
getRGB,
|
||||
PixelsPerInch,
|
||||
} from "../display_utils.js";
|
||||
import { HighlightToolbar } from "./toolbar.js";
|
||||
|
||||
function bindEvents(obj, element, names) {
|
||||
for (const name of names) {
|
||||
@ -555,6 +556,8 @@ class AnnotationEditorUIManager {
|
||||
|
||||
#editorsToRescale = new Set();
|
||||
|
||||
#enableHighlightFloatingButton = false;
|
||||
|
||||
#filterFactory = null;
|
||||
|
||||
#focusMainContainerTimeoutId = null;
|
||||
@ -563,6 +566,8 @@ class AnnotationEditorUIManager {
|
||||
|
||||
#highlightWhenShiftUp = false;
|
||||
|
||||
#highlightToolbar = null;
|
||||
|
||||
#idManager = new IdManager();
|
||||
|
||||
#isEnabled = false;
|
||||
@ -771,6 +776,7 @@ class AnnotationEditorUIManager {
|
||||
pdfDocument,
|
||||
pageColors,
|
||||
highlightColors,
|
||||
enableHighlightFloatingButton,
|
||||
mlManager
|
||||
) {
|
||||
this.#container = container;
|
||||
@ -782,10 +788,12 @@ class AnnotationEditorUIManager {
|
||||
this._eventBus._on("scalechanging", this.#boundOnScaleChanging);
|
||||
this._eventBus._on("rotationchanging", this.#boundOnRotationChanging);
|
||||
this.#addSelectionListener();
|
||||
this.#addKeyboardManager();
|
||||
this.#annotationStorage = pdfDocument.annotationStorage;
|
||||
this.#filterFactory = pdfDocument.filterFactory;
|
||||
this.#pageColors = pageColors;
|
||||
this.#highlightColors = highlightColors || null;
|
||||
this.#enableHighlightFloatingButton = enableHighlightFloatingButton;
|
||||
this.#mlManager = mlManager || null;
|
||||
this.viewParameters = {
|
||||
realScale: PixelsPerInch.PDF_TO_CSS_UNITS,
|
||||
@ -821,6 +829,8 @@ class AnnotationEditorUIManager {
|
||||
this.#selectedEditors.clear();
|
||||
this.#commandManager.destroy();
|
||||
this.#altTextManager?.destroy();
|
||||
this.#highlightToolbar?.hide();
|
||||
this.#highlightToolbar = null;
|
||||
if (this.#focusMainContainerTimeoutId) {
|
||||
clearTimeout(this.#focusMainContainerTimeoutId);
|
||||
this.#focusMainContainerTimeoutId = null;
|
||||
@ -946,24 +956,32 @@ class AnnotationEditorUIManager {
|
||||
this.viewParameters.rotation = pagesRotation;
|
||||
}
|
||||
|
||||
#getAnchorElementForSelection({ anchorNode }) {
|
||||
return anchorNode.nodeType === Node.TEXT_NODE
|
||||
? anchorNode.parentElement
|
||||
: anchorNode;
|
||||
}
|
||||
|
||||
highlightSelection(methodOfCreation = "") {
|
||||
const selection = document.getSelection();
|
||||
if (!selection || selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
const { anchorNode, anchorOffset, focusNode, focusOffset } = selection;
|
||||
const anchorElement =
|
||||
anchorNode.nodeType === Node.TEXT_NODE
|
||||
? anchorNode.parentElement
|
||||
: anchorNode;
|
||||
const text = selection.toString();
|
||||
const anchorElement = this.#getAnchorElementForSelection(selection);
|
||||
const textLayer = anchorElement.closest(".textLayer");
|
||||
const boxes = this.getSelectionBoxes(textLayer);
|
||||
if (!boxes) {
|
||||
return;
|
||||
}
|
||||
selection.empty();
|
||||
if (this.#mode === AnnotationEditorType.NONE) {
|
||||
this._eventBus.dispatch("showannotationeditorui", {
|
||||
source: this,
|
||||
mode: AnnotationEditorType.HIGHLIGHT,
|
||||
});
|
||||
this.showAllEditors("highlight", true, /* updateButton = */ true);
|
||||
}
|
||||
for (const layer of this.#allLayers.values()) {
|
||||
if (layer.hasTextLayer(textLayer)) {
|
||||
@ -974,12 +992,28 @@ class AnnotationEditorUIManager {
|
||||
anchorOffset,
|
||||
focusNode,
|
||||
focusOffset,
|
||||
text,
|
||||
});
|
||||
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.
|
||||
* @param {AnnotationEditor} editor
|
||||
@ -998,6 +1032,7 @@ class AnnotationEditorUIManager {
|
||||
const selection = document.getSelection();
|
||||
if (!selection || selection.isCollapsed) {
|
||||
if (this.#selectedTextNode) {
|
||||
this.#highlightToolbar?.hide();
|
||||
this.#selectedTextNode = null;
|
||||
this.#dispatchUpdateStates({
|
||||
hasSelectedText: false,
|
||||
@ -1010,12 +1045,11 @@ class AnnotationEditorUIManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const anchorElement =
|
||||
anchorNode.nodeType === Node.TEXT_NODE
|
||||
? anchorNode.parentElement
|
||||
: anchorNode;
|
||||
if (!anchorElement.closest(".textLayer")) {
|
||||
const anchorElement = this.#getAnchorElementForSelection(selection);
|
||||
const textLayer = anchorElement.closest(".textLayer");
|
||||
if (!textLayer) {
|
||||
if (this.#selectedTextNode) {
|
||||
this.#highlightToolbar?.hide();
|
||||
this.#selectedTextNode = null;
|
||||
this.#dispatchUpdateStates({
|
||||
hasSelectedText: false,
|
||||
@ -1023,16 +1057,22 @@ class AnnotationEditorUIManager {
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.#highlightToolbar?.hide();
|
||||
this.#selectedTextNode = anchorNode;
|
||||
this.#dispatchUpdateStates({
|
||||
hasSelectedText: true,
|
||||
});
|
||||
|
||||
if (this.#mode !== AnnotationEditorType.HIGHLIGHT) {
|
||||
if (
|
||||
this.#mode !== AnnotationEditorType.HIGHLIGHT &&
|
||||
this.#mode !== AnnotationEditorType.NONE
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showAllEditors("highlight", true, /* updateButton = */ true);
|
||||
if (this.#mode === AnnotationEditorType.HIGHLIGHT) {
|
||||
this.showAllEditors("highlight", true, /* updateButton = */ true);
|
||||
}
|
||||
|
||||
this.#highlightWhenShiftUp = this.isShiftKeyDown;
|
||||
if (!this.isShiftKeyDown) {
|
||||
@ -1044,7 +1084,7 @@ class AnnotationEditorUIManager {
|
||||
window.removeEventListener("pointerup", pointerup);
|
||||
window.removeEventListener("blur", pointerup);
|
||||
if (e.type === "pointerup") {
|
||||
this.highlightSelection("main_toolbar");
|
||||
this.#onSelectEnd("main_toolbar");
|
||||
}
|
||||
};
|
||||
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() {
|
||||
document.addEventListener("selectionchange", this.#boundSelectionChange);
|
||||
}
|
||||
@ -1074,7 +1122,7 @@ class AnnotationEditorUIManager {
|
||||
this.isShiftKeyDown = false;
|
||||
if (this.#highlightWhenShiftUp) {
|
||||
this.#highlightWhenShiftUp = false;
|
||||
this.highlightSelection("main_toolbar");
|
||||
this.#onSelectEnd("main_toolbar");
|
||||
}
|
||||
if (!this.hasSelection) {
|
||||
return;
|
||||
@ -1250,7 +1298,10 @@ class AnnotationEditorUIManager {
|
||||
if (!this.isShiftKeyDown && event.key === "Shift") {
|
||||
this.isShiftKeyDown = true;
|
||||
}
|
||||
if (!this.isEditorHandlingKeyboard) {
|
||||
if (
|
||||
this.#mode !== AnnotationEditorType.NONE &&
|
||||
!this.isEditorHandlingKeyboard
|
||||
) {
|
||||
AnnotationEditorUIManager._keyboardManager.exec(this, event);
|
||||
}
|
||||
}
|
||||
@ -1264,7 +1315,7 @@ class AnnotationEditorUIManager {
|
||||
this.isShiftKeyDown = false;
|
||||
if (this.#highlightWhenShiftUp) {
|
||||
this.#highlightWhenShiftUp = false;
|
||||
this.highlightSelection("main_toolbar");
|
||||
this.#onSelectEnd("main_toolbar");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1333,7 +1384,6 @@ class AnnotationEditorUIManager {
|
||||
setEditingState(isEditing) {
|
||||
if (isEditing) {
|
||||
this.#addFocusManager();
|
||||
this.#addKeyboardManager();
|
||||
this.#addCopyPasteListeners();
|
||||
this.#dispatchUpdateStates({
|
||||
isEditing: this.#mode !== AnnotationEditorType.NONE,
|
||||
@ -1344,7 +1394,6 @@ class AnnotationEditorUIManager {
|
||||
});
|
||||
} else {
|
||||
this.#removeFocusManager();
|
||||
this.#removeKeyboardManager();
|
||||
this.#removeCopyPasteListeners();
|
||||
this.#dispatchUpdateStates({
|
||||
isEditing: false,
|
||||
|
@ -424,21 +424,22 @@ class PDFNodeStreamFsFullReader extends BaseFullReader {
|
||||
path = path.replace(/^\//, "");
|
||||
}
|
||||
|
||||
fs.lstat(path, (error, stat) => {
|
||||
if (error) {
|
||||
fs.promises.lstat(path).then(
|
||||
stat => {
|
||||
// Setting right content length.
|
||||
this._contentLength = stat.size;
|
||||
|
||||
this._setReadableStream(fs.createReadStream(path));
|
||||
this._headersCapability.resolve();
|
||||
},
|
||||
error => {
|
||||
if (error.code === "ENOENT") {
|
||||
error = new MissingPDFException(`Missing PDF "${path}".`);
|
||||
}
|
||||
this._storedError = error;
|
||||
this._headersCapability.reject(error);
|
||||
return;
|
||||
}
|
||||
// Setting right content length.
|
||||
this._contentLength = stat.size;
|
||||
|
||||
this._setReadableStream(fs.createReadStream(path));
|
||||
this._headersCapability.resolve();
|
||||
});
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,15 +71,7 @@ if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("SKIP_BABEL")) {
|
||||
}
|
||||
|
||||
const fetchData = function (url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(url, (error, data) => {
|
||||
if (error || !data) {
|
||||
reject(new Error(error));
|
||||
return;
|
||||
}
|
||||
resolve(new Uint8Array(data));
|
||||
});
|
||||
});
|
||||
return fs.promises.readFile(url).then(data => new Uint8Array(data));
|
||||
};
|
||||
|
||||
class NodeFilterFactory extends BaseFilterFactory {}
|
||||
|
@ -13,33 +13,63 @@
|
||||
* 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";
|
||||
|
||||
const INTERNAL = Symbol("INTERNAL");
|
||||
|
||||
class OptionalContentGroup {
|
||||
#isDisplay = false;
|
||||
|
||||
#isPrint = false;
|
||||
|
||||
#userSet = false;
|
||||
|
||||
#visible = true;
|
||||
|
||||
constructor(name, intent) {
|
||||
constructor(renderingIntent, { name, intent, usage }) {
|
||||
this.#isDisplay = !!(renderingIntent & RenderingIntentFlag.DISPLAY);
|
||||
this.#isPrint = !!(renderingIntent & RenderingIntentFlag.PRINT);
|
||||
|
||||
this.name = name;
|
||||
this.intent = intent;
|
||||
this.usage = usage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
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
|
||||
*/
|
||||
_setVisible(internal, visible) {
|
||||
_setVisible(internal, visible, userSet = false) {
|
||||
if (internal !== INTERNAL) {
|
||||
unreachable("Internal method `_setVisible` called.");
|
||||
}
|
||||
this.#userSet = userSet;
|
||||
this.#visible = visible;
|
||||
}
|
||||
}
|
||||
@ -53,7 +83,9 @@ class OptionalContentConfig {
|
||||
|
||||
#order = null;
|
||||
|
||||
constructor(data) {
|
||||
constructor(data, renderingIntent = RenderingIntentFlag.DISPLAY) {
|
||||
this.renderingIntent = renderingIntent;
|
||||
|
||||
this.name = null;
|
||||
this.creator = null;
|
||||
|
||||
@ -66,7 +98,7 @@ class OptionalContentConfig {
|
||||
for (const group of data.groups) {
|
||||
this.#groups.set(
|
||||
group.id,
|
||||
new OptionalContentGroup(group.name, group.intent)
|
||||
new OptionalContentGroup(renderingIntent, group)
|
||||
);
|
||||
}
|
||||
|
||||
@ -198,11 +230,44 @@ class OptionalContentConfig {
|
||||
}
|
||||
|
||||
setVisibility(id, visible = true) {
|
||||
if (!this.#groups.has(id)) {
|
||||
const group = this.#groups.get(id);
|
||||
if (!group) {
|
||||
warn(`Optional content group not found: ${id}`);
|
||||
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;
|
||||
}
|
||||
|
@ -684,7 +684,9 @@ class Driver {
|
||||
}
|
||||
|
||||
task.pdfDoc = doc;
|
||||
task.optionalContentConfigPromise = doc.getOptionalContentConfig();
|
||||
task.optionalContentConfigPromise = doc.getOptionalContentConfig({
|
||||
intent: task.print ? "print" : "display",
|
||||
});
|
||||
|
||||
if (task.optionalContent) {
|
||||
const entries = Object.entries(task.optionalContent),
|
||||
|
@ -35,6 +35,7 @@ async function runTests(results) {
|
||||
"scripting_spec.mjs",
|
||||
"stamp_editor_spec.mjs",
|
||||
"text_field_spec.mjs",
|
||||
"viewer_spec.mjs",
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -46,8 +46,11 @@ const getXY = (page, selector) =>
|
||||
return `${bbox.x}::${bbox.y}`;
|
||||
}, selector);
|
||||
|
||||
const getSpanRectFromText = (page, pageNumber, text) =>
|
||||
page.evaluate(
|
||||
const getSpanRectFromText = async (page, pageNumber, text) => {
|
||||
await page.waitForSelector(
|
||||
`.page[data-page-number="${pageNumber}"] > .textLayer .endOfContent`
|
||||
);
|
||||
return page.evaluate(
|
||||
(number, content) => {
|
||||
for (const el of document.querySelectorAll(
|
||||
`.page[data-page-number="${number}"] > .textLayer > span`
|
||||
@ -62,6 +65,7 @@ const getSpanRectFromText = (page, pageNumber, text) =>
|
||||
pageNumber,
|
||||
text
|
||||
);
|
||||
};
|
||||
|
||||
describe("Highlight Editor", () => {
|
||||
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");
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
58
test/integration/viewer_spec.mjs
Normal file
58
test/integration/viewer_spec.mjs
Normal 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");
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
1
test/pdfs/bug1826783.pdf.link
Normal file
1
test/pdfs/bug1826783.pdf.link
Normal file
@ -0,0 +1 @@
|
||||
https://bugzilla.mozilla.org/attachment.cgi?id=9327375
|
@ -4016,6 +4016,23 @@
|
||||
"lastPage": 5,
|
||||
"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",
|
||||
"file": "pdfs/issue8586.pdf",
|
||||
|
41
test/unit/app_options_spec.js
Normal file
41
test/unit/app_options_spec.js
Normal 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);
|
||||
});
|
||||
});
|
@ -7,6 +7,7 @@
|
||||
"annotation_spec.js",
|
||||
"annotation_storage_spec.js",
|
||||
"api_spec.js",
|
||||
"app_options_spec.js",
|
||||
"bidi_spec.js",
|
||||
"cff_parser_spec.js",
|
||||
"cmap_spec.js",
|
||||
|
@ -50,6 +50,7 @@ async function initializePDFJS(callback) {
|
||||
"pdfjs-test/unit/annotation_spec.js",
|
||||
"pdfjs-test/unit/annotation_storage_spec.js",
|
||||
"pdfjs-test/unit/api_spec.js",
|
||||
"pdfjs-test/unit/app_options_spec.js",
|
||||
"pdfjs-test/unit/bidi_spec.js",
|
||||
"pdfjs-test/unit/cff_parser_spec.js",
|
||||
"pdfjs-test/unit/cmap_spec.js",
|
||||
|
@ -48,6 +48,19 @@
|
||||
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 {
|
||||
cursor: var(--editorFreeHighlight-editing-cursor);
|
||||
|
||||
@ -182,10 +195,12 @@
|
||||
}
|
||||
|
||||
.annotationEditorLayer
|
||||
:is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor) {
|
||||
:is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),
|
||||
.textLayer {
|
||||
.editToolbar {
|
||||
--editor-toolbar-delete-image: url(images/editor-toolbar-delete.svg);
|
||||
--editor-toolbar-bg-color: #f0f0f4;
|
||||
--editor-toolbar-highlight-image: url(images/toolbarButton-editorHighlight.svg);
|
||||
--editor-toolbar-fg-color: #2e2e56;
|
||||
--editor-toolbar-border-color: #8f8f9d;
|
||||
--editor-toolbar-hover-border-color: var(--editor-toolbar-border-color);
|
||||
@ -271,6 +286,25 @@
|
||||
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 {
|
||||
width: var(--editor-toolbar-height);
|
||||
|
||||
|
@ -424,6 +424,9 @@ const PDFViewerApplication = {
|
||||
annotationMode: AppOptions.get("annotationMode"),
|
||||
annotationEditorMode,
|
||||
annotationEditorHighlightColors: AppOptions.get("highlightEditorColors"),
|
||||
enableHighlightFloatingButton: AppOptions.get(
|
||||
"enableHighlightFloatingButton"
|
||||
),
|
||||
imageResourcesPath: AppOptions.get("imageResourcesPath"),
|
||||
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
|
||||
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
|
||||
@ -1796,7 +1799,6 @@ const PDFViewerApplication = {
|
||||
pagesOverview: this.pdfViewer.getPagesOverview(),
|
||||
printContainer: this.appConfig.printContainer,
|
||||
printResolution: AppOptions.get("printResolution"),
|
||||
optionalContentConfigPromise: this.pdfViewer.optionalContentConfigPromise,
|
||||
printAnnotationStoragePromise: this._printAnnotationStoragePromise,
|
||||
});
|
||||
this.forceRendering();
|
||||
@ -2022,8 +2024,9 @@ const PDFViewerApplication = {
|
||||
});
|
||||
const scroll = (_boundEvents.mainContainerScroll = () => {
|
||||
if (
|
||||
this._lastScrollTop === mainContainer.scrollTop &&
|
||||
this._lastScrollLeft === mainContainer.scrollLeft
|
||||
this._isCtrlKeyDown ||
|
||||
(this._lastScrollTop === mainContainer.scrollTop &&
|
||||
this._lastScrollLeft === mainContainer.scrollLeft)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
@ -143,6 +143,14 @@ const defaultOptions = {
|
||||
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"),
|
||||
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: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
@ -409,57 +417,64 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
|
||||
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 {
|
||||
constructor() {
|
||||
throw new Error("Cannot initialize AppOptions.");
|
||||
}
|
||||
|
||||
static get(name) {
|
||||
const userOption = userOptions[name];
|
||||
if (userOption !== undefined) {
|
||||
return userOption;
|
||||
}
|
||||
const defaultOption = defaultOptions[name];
|
||||
if (defaultOption !== undefined) {
|
||||
return compatibilityParams[name] ?? defaultOption.value;
|
||||
}
|
||||
return undefined;
|
||||
static getCompat(name) {
|
||||
return compatibilityParams[name] ?? 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);
|
||||
for (const name in defaultOptions) {
|
||||
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 (
|
||||
valueType === "boolean" ||
|
||||
valueType === "string" ||
|
||||
(valueType === "number" && Number.isInteger(value))
|
||||
) {
|
||||
options[name] = value;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Invalid type for preference: ${name}`);
|
||||
}
|
||||
if (kind && !(kind & defaultOption.kind)) {
|
||||
continue;
|
||||
}
|
||||
const userOption = userOptions[name];
|
||||
options[name] =
|
||||
userOption !== undefined
|
||||
? userOption
|
||||
: compatibilityParams[name] ?? defaultOption.value;
|
||||
options[name] = defaultOnly
|
||||
? defaultOption.value
|
||||
: userOptions[name] ?? compatibilityParams[name] ?? defaultOption.value;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
@ -493,4 +508,4 @@ class AppOptions {
|
||||
}
|
||||
}
|
||||
|
||||
export { AppOptions, compatibilityParams, OptionKind };
|
||||
export { AppOptions, OptionKind };
|
||||
|
@ -119,15 +119,15 @@ class FirefoxPrintService {
|
||||
pagesOverview,
|
||||
printContainer,
|
||||
printResolution,
|
||||
optionalContentConfigPromise = null,
|
||||
printAnnotationStoragePromise = null,
|
||||
}) {
|
||||
this.pdfDocument = pdfDocument;
|
||||
this.pagesOverview = pagesOverview;
|
||||
this.printContainer = printContainer;
|
||||
this._printResolution = printResolution || 150;
|
||||
this._optionalContentConfigPromise =
|
||||
optionalContentConfigPromise || pdfDocument.getOptionalContentConfig();
|
||||
this._optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({
|
||||
intent: "print",
|
||||
});
|
||||
this._printAnnotationStoragePromise =
|
||||
printAnnotationStoragePromise || Promise.resolve();
|
||||
}
|
||||
|
@ -258,14 +258,8 @@ if (PDFJSDev.test("GECKOVIEW")) {
|
||||
const hasWillPrint =
|
||||
pdfViewer.enableScripting &&
|
||||
!!(await pdfDocument.getJSActions())?.WillPrint;
|
||||
const hasUnchangedOptionalContent = (
|
||||
await pdfViewer.optionalContentConfigPromise
|
||||
).hasInitialVisibility;
|
||||
|
||||
result =
|
||||
hasUnchangedAnnotations &&
|
||||
!hasWillPrint &&
|
||||
hasUnchangedOptionalContent;
|
||||
result = hasUnchangedAnnotations && !hasWillPrint;
|
||||
} catch {
|
||||
console.warn("Unable to check if the document can be downloaded.");
|
||||
}
|
||||
|
@ -182,7 +182,7 @@ class PDFLayerViewer extends BaseTreeViewer {
|
||||
}
|
||||
const pdfDocument = this._pdfDocument;
|
||||
const optionalContentConfig = await (promise ||
|
||||
pdfDocument.getOptionalContentConfig());
|
||||
pdfDocument.getOptionalContentConfig({ intent: "display" }));
|
||||
|
||||
if (pdfDocument !== this._pdfDocument) {
|
||||
return; // The document was closed while the optional content resolved.
|
||||
|
@ -517,31 +517,7 @@ class PDFLinkService {
|
||||
if (pdfDocument !== this.pdfDocument) {
|
||||
return; // The document was closed while the optional content resolved.
|
||||
}
|
||||
let operator;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
optionalContentConfig.setOCGState(action);
|
||||
|
||||
this.pdfViewer.optionalContentConfigPromise = Promise.resolve(
|
||||
optionalContentConfig
|
||||
|
@ -41,7 +41,7 @@ import {
|
||||
} from "./ui_utils.js";
|
||||
import { AnnotationEditorLayerBuilder } from "./annotation_editor_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 { GenericL10n } from "web-null_l10n";
|
||||
import { SimpleLinkService } from "./pdf_link_service.js";
|
||||
@ -83,8 +83,6 @@ import { XfaLayerBuilder } from "./xfa_layer_builder.js";
|
||||
* the necessary layer-properties.
|
||||
*/
|
||||
|
||||
const MAX_CANVAS_PIXELS = compatibilityParams.maxCanvasPixels || 16777216;
|
||||
|
||||
const DEFAULT_LAYER_PROPERTIES =
|
||||
typeof PDFJSDev === "undefined" || !PDFJSDev.test("COMPONENTS")
|
||||
? null
|
||||
@ -152,7 +150,9 @@ class PDFPageView {
|
||||
this.#annotationMode =
|
||||
options.annotationMode ?? AnnotationMode.ENABLE_FORMS;
|
||||
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.eventBus = options.eventBus;
|
||||
|
@ -13,7 +13,12 @@
|
||||
* 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";
|
||||
|
||||
let activeService = null;
|
||||
@ -58,7 +63,14 @@ function renderPage(
|
||||
optionalContentConfigPromise,
|
||||
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,
|
||||
printContainer,
|
||||
printResolution,
|
||||
optionalContentConfigPromise = null,
|
||||
printAnnotationStoragePromise = null,
|
||||
}) {
|
||||
this.pdfDocument = pdfDocument;
|
||||
this.pagesOverview = pagesOverview;
|
||||
this.printContainer = printContainer;
|
||||
this._printResolution = printResolution || 150;
|
||||
this._optionalContentConfigPromise =
|
||||
optionalContentConfigPromise || pdfDocument.getOptionalContentConfig();
|
||||
this._optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({
|
||||
intent: "print",
|
||||
});
|
||||
this._printAnnotationStoragePromise =
|
||||
printAnnotationStoragePromise || Promise.resolve();
|
||||
this.currentPage = -1;
|
||||
|
@ -189,7 +189,9 @@ class PDFThumbnailViewer {
|
||||
return;
|
||||
}
|
||||
const firstPagePromise = pdfDocument.getPage(1);
|
||||
const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig();
|
||||
const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({
|
||||
intent: "display",
|
||||
});
|
||||
|
||||
firstPagePromise
|
||||
.then(firstPdfPage => {
|
||||
|
@ -214,6 +214,8 @@ class PDFViewer {
|
||||
|
||||
#copyCallbackBound = null;
|
||||
|
||||
#enableHighlightFloatingButton = false;
|
||||
|
||||
#enablePermissions = false;
|
||||
|
||||
#mlManager = null;
|
||||
@ -282,6 +284,8 @@ class PDFViewer {
|
||||
options.annotationEditorMode ?? AnnotationEditorType.NONE;
|
||||
this.#annotationEditorHighlightColors =
|
||||
options.annotationEditorHighlightColors || null;
|
||||
this.#enableHighlightFloatingButton =
|
||||
options.enableHighlightFloatingButton === true;
|
||||
this.imageResourcesPath = options.imageResourcesPath || "";
|
||||
this.enablePrintAutoRotate = options.enablePrintAutoRotate || false;
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
@ -781,7 +785,9 @@ class PDFViewer {
|
||||
const pagesCount = pdfDocument.numPages;
|
||||
const firstPagePromise = pdfDocument.getPage(1);
|
||||
// Rendering (potentially) depends on this, hence fetching it immediately.
|
||||
const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig();
|
||||
const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({
|
||||
intent: "display",
|
||||
});
|
||||
const permissionsPromise = this.#enablePermissions
|
||||
? pdfDocument.getPermissions()
|
||||
: Promise.resolve();
|
||||
@ -861,6 +867,7 @@ class PDFViewer {
|
||||
pdfDocument,
|
||||
this.pageColors,
|
||||
this.#annotationEditorHighlightColors,
|
||||
this.#enableHighlightFloatingButton,
|
||||
this.#mlManager
|
||||
);
|
||||
this.eventBus.dispatch("annotationeditoruimanager", {
|
||||
@ -1822,7 +1829,7 @@ class PDFViewer {
|
||||
console.error("optionalContentConfigPromise: Not initialized yet.");
|
||||
// Prevent issues if the getter is accessed *before* the `onePageRendered`
|
||||
// promise has resolved; won't (normally) happen in the default viewer.
|
||||
return this.pdfDocument.getOptionalContentConfig();
|
||||
return this.pdfDocument.getOptionalContentConfig({ intent: "display" });
|
||||
}
|
||||
return this._optionalContentConfigPromise;
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ import { AppOptions, OptionKind } from "./app_options.js";
|
||||
class BasePreferences {
|
||||
#defaults = Object.freeze(
|
||||
typeof PDFJSDev === "undefined"
|
||||
? AppOptions.getAll(OptionKind.PREFERENCE)
|
||||
? AppOptions.getAll(OptionKind.PREFERENCE, /* defaultOnly = */ true)
|
||||
: PDFJSDev.eval("DEFAULT_PREFERENCES")
|
||||
);
|
||||
|
||||
@ -48,7 +48,7 @@ class BasePreferences {
|
||||
({ browserPrefs, prefs }) => {
|
||||
const BROWSER_PREFS =
|
||||
typeof PDFJSDev === "undefined"
|
||||
? AppOptions.getAll(OptionKind.BROWSER)
|
||||
? AppOptions.getAll(OptionKind.BROWSER, /* defaultOnly = */ true)
|
||||
: PDFJSDev.eval("BROWSER_PREFERENCES");
|
||||
const options = Object.create(null);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user