first commit
Some checks failed
Types tests / Test (lts/*) (push) Has been cancelled
Lint / Lint (lts/*) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CI / Test (20) (push) Has been cancelled
CI / Test (22) (push) Has been cancelled
CI / Test (24) (push) Has been cancelled
3
web/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
locale/
|
||||
cmaps/
|
||||
wasm/
|
||||
312
web/alt_text_manager.js
Normal file
@@ -0,0 +1,312 @@
|
||||
/* Copyright 2023 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 { DOMSVGFactory } from "pdfjs-lib";
|
||||
|
||||
class AltTextManager {
|
||||
#clickAC = null;
|
||||
|
||||
#currentEditor = null;
|
||||
|
||||
#cancelButton;
|
||||
|
||||
#dialog;
|
||||
|
||||
#eventBus;
|
||||
|
||||
#hasUsedPointer = false;
|
||||
|
||||
#optionDescription;
|
||||
|
||||
#optionDecorative;
|
||||
|
||||
#overlayManager;
|
||||
|
||||
#saveButton;
|
||||
|
||||
#textarea;
|
||||
|
||||
#uiManager;
|
||||
|
||||
#previousAltText = null;
|
||||
|
||||
#resizeAC = null;
|
||||
|
||||
#svgElement = null;
|
||||
|
||||
#rectElement = null;
|
||||
|
||||
#container;
|
||||
|
||||
#telemetryData = null;
|
||||
|
||||
constructor(
|
||||
{
|
||||
dialog,
|
||||
optionDescription,
|
||||
optionDecorative,
|
||||
textarea,
|
||||
cancelButton,
|
||||
saveButton,
|
||||
},
|
||||
container,
|
||||
overlayManager,
|
||||
eventBus
|
||||
) {
|
||||
this.#dialog = dialog;
|
||||
this.#optionDescription = optionDescription;
|
||||
this.#optionDecorative = optionDecorative;
|
||||
this.#textarea = textarea;
|
||||
this.#cancelButton = cancelButton;
|
||||
this.#saveButton = saveButton;
|
||||
this.#overlayManager = overlayManager;
|
||||
this.#eventBus = eventBus;
|
||||
this.#container = container;
|
||||
|
||||
const onUpdateUIState = this.#updateUIState.bind(this);
|
||||
|
||||
dialog.addEventListener("close", this.#close.bind(this));
|
||||
dialog.addEventListener("contextmenu", event => {
|
||||
if (event.target !== this.#textarea) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
cancelButton.addEventListener("click", this.#finish.bind(this));
|
||||
saveButton.addEventListener("click", this.#save.bind(this));
|
||||
optionDescription.addEventListener("change", onUpdateUIState);
|
||||
optionDecorative.addEventListener("change", onUpdateUIState);
|
||||
|
||||
this.#overlayManager.register(dialog);
|
||||
}
|
||||
|
||||
#createSVGElement() {
|
||||
if (this.#svgElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We create a mask to add to the dialog backdrop: the idea is to have a
|
||||
// darken background everywhere except on the editor to clearly see the
|
||||
// picture to describe.
|
||||
|
||||
const svgFactory = new DOMSVGFactory();
|
||||
const svg = (this.#svgElement = svgFactory.createElement("svg"));
|
||||
svg.setAttribute("width", "0");
|
||||
svg.setAttribute("height", "0");
|
||||
const defs = svgFactory.createElement("defs");
|
||||
svg.append(defs);
|
||||
const mask = svgFactory.createElement("mask");
|
||||
defs.append(mask);
|
||||
mask.setAttribute("id", "alttext-manager-mask");
|
||||
mask.setAttribute("maskContentUnits", "objectBoundingBox");
|
||||
let rect = svgFactory.createElement("rect");
|
||||
mask.append(rect);
|
||||
rect.setAttribute("fill", "white");
|
||||
rect.setAttribute("width", "1");
|
||||
rect.setAttribute("height", "1");
|
||||
rect.setAttribute("x", "0");
|
||||
rect.setAttribute("y", "0");
|
||||
|
||||
rect = this.#rectElement = svgFactory.createElement("rect");
|
||||
mask.append(rect);
|
||||
rect.setAttribute("fill", "black");
|
||||
this.#dialog.append(svg);
|
||||
}
|
||||
|
||||
async editAltText(uiManager, editor) {
|
||||
if (this.#currentEditor || !editor) {
|
||||
return;
|
||||
}
|
||||
this.#createSVGElement();
|
||||
|
||||
this.#hasUsedPointer = false;
|
||||
|
||||
this.#clickAC = new AbortController();
|
||||
const clickOpts = { signal: this.#clickAC.signal },
|
||||
onClick = this.#onClick.bind(this);
|
||||
for (const element of [
|
||||
this.#optionDescription,
|
||||
this.#optionDecorative,
|
||||
this.#textarea,
|
||||
this.#saveButton,
|
||||
this.#cancelButton,
|
||||
]) {
|
||||
element.addEventListener("click", onClick, clickOpts);
|
||||
}
|
||||
|
||||
const { altText, decorative } = editor.altTextData;
|
||||
if (decorative === true) {
|
||||
this.#optionDecorative.checked = true;
|
||||
this.#optionDescription.checked = false;
|
||||
} else {
|
||||
this.#optionDecorative.checked = false;
|
||||
this.#optionDescription.checked = true;
|
||||
}
|
||||
this.#previousAltText = this.#textarea.value = altText?.trim() || "";
|
||||
this.#updateUIState();
|
||||
|
||||
this.#currentEditor = editor;
|
||||
this.#uiManager = uiManager;
|
||||
this.#uiManager.removeEditListeners();
|
||||
|
||||
this.#resizeAC = new AbortController();
|
||||
this.#eventBus._on("resize", this.#setPosition.bind(this), {
|
||||
signal: this.#resizeAC.signal,
|
||||
});
|
||||
|
||||
try {
|
||||
await this.#overlayManager.open(this.#dialog);
|
||||
this.#setPosition();
|
||||
} catch (ex) {
|
||||
this.#close();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
#setPosition() {
|
||||
if (!this.#currentEditor) {
|
||||
return;
|
||||
}
|
||||
const dialog = this.#dialog;
|
||||
const { style } = dialog;
|
||||
const {
|
||||
x: containerX,
|
||||
y: containerY,
|
||||
width: containerW,
|
||||
height: containerH,
|
||||
} = this.#container.getBoundingClientRect();
|
||||
const { innerWidth: windowW, innerHeight: windowH } = window;
|
||||
const { width: dialogW, height: dialogH } = dialog.getBoundingClientRect();
|
||||
const { x, y, width, height } = this.#currentEditor.getClientDimensions();
|
||||
const MARGIN = 10;
|
||||
const isLTR = this.#uiManager.direction === "ltr";
|
||||
|
||||
const xs = Math.max(x, containerX);
|
||||
const xe = Math.min(x + width, containerX + containerW);
|
||||
const ys = Math.max(y, containerY);
|
||||
const ye = Math.min(y + height, containerY + containerH);
|
||||
this.#rectElement.setAttribute("width", `${(xe - xs) / windowW}`);
|
||||
this.#rectElement.setAttribute("height", `${(ye - ys) / windowH}`);
|
||||
this.#rectElement.setAttribute("x", `${xs / windowW}`);
|
||||
this.#rectElement.setAttribute("y", `${ys / windowH}`);
|
||||
|
||||
let left = null;
|
||||
let top = Math.max(y, 0);
|
||||
top += Math.min(windowH - (top + dialogH), 0);
|
||||
|
||||
if (isLTR) {
|
||||
// Prefer to position the dialog "after" (so on the right) the editor.
|
||||
if (x + width + MARGIN + dialogW < windowW) {
|
||||
left = x + width + MARGIN;
|
||||
} else if (x > dialogW + MARGIN) {
|
||||
left = x - dialogW - MARGIN;
|
||||
}
|
||||
} else if (x > dialogW + MARGIN) {
|
||||
left = x - dialogW - MARGIN;
|
||||
} else if (x + width + MARGIN + dialogW < windowW) {
|
||||
left = x + width + MARGIN;
|
||||
}
|
||||
|
||||
if (left === null) {
|
||||
top = null;
|
||||
left = Math.max(x, 0);
|
||||
left += Math.min(windowW - (left + dialogW), 0);
|
||||
if (y > dialogH + MARGIN) {
|
||||
top = y - dialogH - MARGIN;
|
||||
} else if (y + height + MARGIN + dialogH < windowH) {
|
||||
top = y + height + MARGIN;
|
||||
}
|
||||
}
|
||||
|
||||
if (top !== null) {
|
||||
dialog.classList.add("positioned");
|
||||
if (isLTR) {
|
||||
style.left = `${left}px`;
|
||||
} else {
|
||||
style.right = `${windowW - left - dialogW}px`;
|
||||
}
|
||||
style.top = `${top}px`;
|
||||
} else {
|
||||
dialog.classList.remove("positioned");
|
||||
style.left = "";
|
||||
style.top = "";
|
||||
}
|
||||
}
|
||||
|
||||
#finish() {
|
||||
this.#overlayManager.closeIfActive(this.#dialog);
|
||||
}
|
||||
|
||||
#close() {
|
||||
this.#currentEditor._reportTelemetry(
|
||||
this.#telemetryData || {
|
||||
action: "alt_text_cancel",
|
||||
alt_text_keyboard: !this.#hasUsedPointer,
|
||||
}
|
||||
);
|
||||
this.#telemetryData = null;
|
||||
|
||||
this.#removeOnClickListeners();
|
||||
this.#uiManager?.addEditListeners();
|
||||
this.#resizeAC?.abort();
|
||||
this.#resizeAC = null;
|
||||
this.#currentEditor.altTextFinish();
|
||||
this.#currentEditor = null;
|
||||
this.#uiManager = null;
|
||||
}
|
||||
|
||||
#updateUIState() {
|
||||
this.#textarea.disabled = this.#optionDecorative.checked;
|
||||
}
|
||||
|
||||
#save() {
|
||||
const altText = this.#textarea.value.trim();
|
||||
const decorative = this.#optionDecorative.checked;
|
||||
this.#currentEditor.altTextData = {
|
||||
altText,
|
||||
decorative,
|
||||
};
|
||||
this.#telemetryData = {
|
||||
action: "alt_text_save",
|
||||
alt_text_description: !!altText,
|
||||
alt_text_edit:
|
||||
!!this.#previousAltText && this.#previousAltText !== altText,
|
||||
alt_text_decorative: decorative,
|
||||
alt_text_keyboard: !this.#hasUsedPointer,
|
||||
};
|
||||
this.#finish();
|
||||
}
|
||||
|
||||
#onClick(evt) {
|
||||
if (evt.detail === 0) {
|
||||
return; // The keyboard was used.
|
||||
}
|
||||
this.#hasUsedPointer = true;
|
||||
this.#removeOnClickListeners();
|
||||
}
|
||||
|
||||
#removeOnClickListeners() {
|
||||
this.#clickAC?.abort();
|
||||
this.#clickAC = null;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#uiManager = null; // Avoid re-adding the edit listeners.
|
||||
this.#finish();
|
||||
this.#svgElement?.remove();
|
||||
this.#svgElement = this.#rectElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
export { AltTextManager };
|
||||
1430
web/annotation_editor_layer_builder.css
Normal file
162
web/annotation_editor_layer_builder.js
Normal file
@@ -0,0 +1,162 @@
|
||||
/* Copyright 2022 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.
|
||||
*/
|
||||
|
||||
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||
/** @typedef {import("./interfaces").IL10n} IL10n */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/annotation_layer.js").AnnotationLayer} AnnotationLayer */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/struct_tree_layer_builder.js").StructTreeLayerBuilder} StructTreeLayerBuilder */
|
||||
|
||||
import { AnnotationEditorLayer } from "pdfjs-lib";
|
||||
import { GenericL10n } from "web-null_l10n";
|
||||
|
||||
/**
|
||||
* @typedef {Object} AnnotationEditorLayerBuilderOptions
|
||||
* @property {AnnotationEditorUIManager} [uiManager]
|
||||
* @property {PDFPageProxy} pdfPage
|
||||
* @property {IL10n} [l10n]
|
||||
* @property {StructTreeLayerBuilder} [structTreeLayer]
|
||||
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||
* @property {AnnotationLayer} [annotationLayer]
|
||||
* @property {TextLayer} [textLayer]
|
||||
* @property {DrawLayer} [drawLayer]
|
||||
* @property {function} [onAppend]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} AnnotationEditorLayerBuilderRenderOptions
|
||||
* @property {PageViewport} viewport
|
||||
* @property {string} [intent] - The default value is "display".
|
||||
*/
|
||||
|
||||
class AnnotationEditorLayerBuilder {
|
||||
#annotationLayer = null;
|
||||
|
||||
#drawLayer = null;
|
||||
|
||||
#onAppend = null;
|
||||
|
||||
#structTreeLayer = null;
|
||||
|
||||
#textLayer = null;
|
||||
|
||||
#uiManager;
|
||||
|
||||
/**
|
||||
* @param {AnnotationEditorLayerBuilderOptions} options
|
||||
*/
|
||||
constructor(options) {
|
||||
this.pdfPage = options.pdfPage;
|
||||
this.accessibilityManager = options.accessibilityManager;
|
||||
this.l10n = options.l10n;
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
this.l10n ||= new GenericL10n();
|
||||
}
|
||||
this.annotationEditorLayer = null;
|
||||
this.div = null;
|
||||
this._cancelled = false;
|
||||
this.#uiManager = options.uiManager;
|
||||
this.#annotationLayer = options.annotationLayer || null;
|
||||
this.#textLayer = options.textLayer || null;
|
||||
this.#drawLayer = options.drawLayer || null;
|
||||
this.#onAppend = options.onAppend || null;
|
||||
this.#structTreeLayer = options.structTreeLayer || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AnnotationEditorLayerBuilderRenderOptions} options
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async render({ viewport, intent = "display" }) {
|
||||
if (intent !== "display") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clonedViewport = viewport.clone({ dontFlip: true });
|
||||
if (this.div) {
|
||||
this.annotationEditorLayer.update({ viewport: clonedViewport });
|
||||
this.show();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an AnnotationEditor layer div
|
||||
const div = (this.div = document.createElement("div"));
|
||||
div.className = "annotationEditorLayer";
|
||||
div.hidden = true;
|
||||
div.dir = this.#uiManager.direction;
|
||||
this.#onAppend?.(div);
|
||||
|
||||
this.annotationEditorLayer = new AnnotationEditorLayer({
|
||||
uiManager: this.#uiManager,
|
||||
div,
|
||||
structTreeLayer: this.#structTreeLayer,
|
||||
accessibilityManager: this.accessibilityManager,
|
||||
pageIndex: this.pdfPage.pageNumber - 1,
|
||||
l10n: this.l10n,
|
||||
viewport: clonedViewport,
|
||||
annotationLayer: this.#annotationLayer,
|
||||
textLayer: this.#textLayer,
|
||||
drawLayer: this.#drawLayer,
|
||||
});
|
||||
|
||||
const parameters = {
|
||||
viewport: clonedViewport,
|
||||
div,
|
||||
annotations: null,
|
||||
intent,
|
||||
};
|
||||
|
||||
this.annotationEditorLayer.render(parameters);
|
||||
this.show();
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this._cancelled = true;
|
||||
|
||||
if (!this.div) {
|
||||
return;
|
||||
}
|
||||
this.annotationEditorLayer.destroy();
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!this.div) {
|
||||
return;
|
||||
}
|
||||
this.annotationEditorLayer.pause(/* on */ true);
|
||||
this.div.hidden = true;
|
||||
}
|
||||
|
||||
show() {
|
||||
if (!this.div || this.annotationEditorLayer.isInvisible) {
|
||||
return;
|
||||
}
|
||||
this.div.hidden = false;
|
||||
this.annotationEditorLayer.pause(/* on */ false);
|
||||
}
|
||||
}
|
||||
|
||||
export { AnnotationEditorLayerBuilder };
|
||||
142
web/annotation_editor_params.js
Normal file
@@ -0,0 +1,142 @@
|
||||
/* Copyright 2022 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.
|
||||
*/
|
||||
|
||||
/** @typedef {import("./event_utils.js").EventBus} EventBus */
|
||||
|
||||
import { AnnotationEditorParamsType } from "pdfjs-lib";
|
||||
|
||||
/**
|
||||
* @typedef {Object} AnnotationEditorParamsOptions
|
||||
* @property {HTMLInputElement} editorFreeTextFontSize
|
||||
* @property {HTMLInputElement} editorFreeTextColor
|
||||
* @property {HTMLInputElement} editorInkColor
|
||||
* @property {HTMLInputElement} editorInkThickness
|
||||
* @property {HTMLInputElement} editorInkOpacity
|
||||
* @property {HTMLButtonElement} editorStampAddImage
|
||||
* @property {HTMLInputElement} editorFreeHighlightThickness
|
||||
* @property {HTMLButtonElement} editorHighlightShowAll
|
||||
* @property {HTMLButtonElement} editorSignatureAddSignature
|
||||
*/
|
||||
|
||||
class AnnotationEditorParams {
|
||||
/**
|
||||
* @param {AnnotationEditorParamsOptions} options
|
||||
* @param {EventBus} eventBus
|
||||
*/
|
||||
constructor(options, eventBus) {
|
||||
this.eventBus = eventBus;
|
||||
this.#bindListeners(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AnnotationEditorParamsOptions} options
|
||||
*/
|
||||
#bindListeners({
|
||||
editorFreeTextFontSize,
|
||||
editorFreeTextColor,
|
||||
editorInkColor,
|
||||
editorInkThickness,
|
||||
editorInkOpacity,
|
||||
editorStampAddImage,
|
||||
editorFreeHighlightThickness,
|
||||
editorHighlightShowAll,
|
||||
editorSignatureAddSignature,
|
||||
}) {
|
||||
const { eventBus } = this;
|
||||
|
||||
const dispatchEvent = (typeStr, value) => {
|
||||
eventBus.dispatch("switchannotationeditorparams", {
|
||||
source: this,
|
||||
type: AnnotationEditorParamsType[typeStr],
|
||||
value,
|
||||
});
|
||||
};
|
||||
editorFreeTextFontSize.addEventListener("input", function () {
|
||||
dispatchEvent("FREETEXT_SIZE", this.valueAsNumber);
|
||||
});
|
||||
editorFreeTextColor.addEventListener("input", function () {
|
||||
dispatchEvent("FREETEXT_COLOR", this.value);
|
||||
});
|
||||
editorInkColor.addEventListener("input", function () {
|
||||
dispatchEvent("INK_COLOR", this.value);
|
||||
});
|
||||
editorInkThickness.addEventListener("input", function () {
|
||||
dispatchEvent("INK_THICKNESS", this.valueAsNumber);
|
||||
});
|
||||
editorInkOpacity.addEventListener("input", function () {
|
||||
dispatchEvent("INK_OPACITY", this.valueAsNumber);
|
||||
});
|
||||
editorStampAddImage.addEventListener("click", () => {
|
||||
eventBus.dispatch("reporttelemetry", {
|
||||
source: this,
|
||||
details: {
|
||||
type: "editing",
|
||||
data: { action: "pdfjs.image.add_image_click" },
|
||||
},
|
||||
});
|
||||
dispatchEvent("CREATE");
|
||||
});
|
||||
editorFreeHighlightThickness.addEventListener("input", function () {
|
||||
dispatchEvent("HIGHLIGHT_THICKNESS", this.valueAsNumber);
|
||||
});
|
||||
editorHighlightShowAll.addEventListener("click", function () {
|
||||
const checked = this.getAttribute("aria-pressed") === "true";
|
||||
this.setAttribute("aria-pressed", !checked);
|
||||
dispatchEvent("HIGHLIGHT_SHOW_ALL", !checked);
|
||||
});
|
||||
editorSignatureAddSignature.addEventListener("click", () => {
|
||||
dispatchEvent("CREATE");
|
||||
});
|
||||
|
||||
eventBus._on("annotationeditorparamschanged", evt => {
|
||||
for (const [type, value] of evt.details) {
|
||||
switch (type) {
|
||||
case AnnotationEditorParamsType.FREETEXT_SIZE:
|
||||
editorFreeTextFontSize.value = value;
|
||||
break;
|
||||
case AnnotationEditorParamsType.FREETEXT_COLOR:
|
||||
editorFreeTextColor.value = value;
|
||||
break;
|
||||
case AnnotationEditorParamsType.INK_COLOR:
|
||||
editorInkColor.value = value;
|
||||
break;
|
||||
case AnnotationEditorParamsType.INK_THICKNESS:
|
||||
editorInkThickness.value = value;
|
||||
break;
|
||||
case AnnotationEditorParamsType.INK_OPACITY:
|
||||
editorInkOpacity.value = value;
|
||||
break;
|
||||
case AnnotationEditorParamsType.HIGHLIGHT_COLOR:
|
||||
eventBus.dispatch("mainhighlightcolorpickerupdatecolor", {
|
||||
source: this,
|
||||
value,
|
||||
});
|
||||
break;
|
||||
case AnnotationEditorParamsType.HIGHLIGHT_THICKNESS:
|
||||
editorFreeHighlightThickness.value = value;
|
||||
break;
|
||||
case AnnotationEditorParamsType.HIGHLIGHT_FREE:
|
||||
editorFreeHighlightThickness.disabled = !value;
|
||||
break;
|
||||
case AnnotationEditorParamsType.HIGHLIGHT_SHOW_ALL:
|
||||
editorHighlightShowAll.setAttribute("aria-pressed", value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { AnnotationEditorParams };
|
||||
407
web/annotation_layer_builder.css
Normal file
@@ -0,0 +1,407 @@
|
||||
/* Copyright 2014 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.
|
||||
*/
|
||||
|
||||
.annotationLayer {
|
||||
color-scheme: only light;
|
||||
|
||||
--annotation-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,<svg width='1px' height='1px' xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' style='fill:rgba(0, 54, 255, 0.13);'/></svg>");
|
||||
--input-focus-border-color: Highlight;
|
||||
--input-focus-outline: 1px solid Canvas;
|
||||
--input-unfocused-border-color: transparent;
|
||||
--input-disabled-border-color: transparent;
|
||||
--input-hover-border-color: black;
|
||||
--link-outline: none;
|
||||
|
||||
@media screen and (forced-colors: active) {
|
||||
--input-focus-border-color: CanvasText;
|
||||
--input-unfocused-border-color: ActiveText;
|
||||
--input-disabled-border-color: GrayText;
|
||||
--input-hover-border-color: Highlight;
|
||||
--link-outline: 1.5px solid LinkText;
|
||||
|
||||
.textWidgetAnnotation :is(input, textarea):required,
|
||||
.choiceWidgetAnnotation select:required,
|
||||
.buttonWidgetAnnotation:is(.checkBox, .radioButton) input:required {
|
||||
outline: 1.5px solid selectedItem;
|
||||
}
|
||||
|
||||
.linkAnnotation {
|
||||
outline: var(--link-outline);
|
||||
|
||||
&:hover {
|
||||
backdrop-filter: var(--hcm-highlight-filter);
|
||||
}
|
||||
|
||||
& > a:hover {
|
||||
opacity: 0 !important;
|
||||
background: none !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.popupAnnotation .popup {
|
||||
outline: calc(1.5px * var(--total-scale-factor)) solid CanvasText !important;
|
||||
background-color: ButtonFace !important;
|
||||
color: ButtonText !important;
|
||||
}
|
||||
|
||||
.highlightArea:hover::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backdrop-filter: var(--hcm-highlight-filter);
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.popupAnnotation.focused .popup {
|
||||
outline: calc(3px * var(--total-scale-factor)) solid Highlight !important;
|
||||
}
|
||||
}
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
transform-origin: 0 0;
|
||||
|
||||
&[data-main-rotation="90"] .norotate {
|
||||
transform: rotate(270deg) translateX(-100%);
|
||||
}
|
||||
&[data-main-rotation="180"] .norotate {
|
||||
transform: rotate(180deg) translate(-100%, -100%);
|
||||
}
|
||||
&[data-main-rotation="270"] .norotate {
|
||||
transform: rotate(90deg) translateY(-100%);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
section,
|
||||
.popup {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.annotationContent {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
|
||||
&.freetext {
|
||||
background: transparent;
|
||||
border: none;
|
||||
inset: 0;
|
||||
overflow: visible;
|
||||
white-space: nowrap;
|
||||
font: 10px sans-serif;
|
||||
line-height: 1.35;
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
position: absolute;
|
||||
text-align: initial;
|
||||
pointer-events: auto;
|
||||
box-sizing: border-box;
|
||||
transform-origin: 0 0;
|
||||
user-select: none;
|
||||
|
||||
&:has(div.annotationContent) {
|
||||
canvas.annotationContent {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.overlaidText {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.textLayer.selecting ~ & section {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a {
|
||||
position: absolute;
|
||||
font-size: 1em;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:is(.linkAnnotation, .buttonWidgetAnnotation.pushButton):not(.hasBorder)
|
||||
> a:hover {
|
||||
opacity: 0.2;
|
||||
background-color: rgb(255 255 0);
|
||||
}
|
||||
|
||||
.linkAnnotation.hasBorder:hover {
|
||||
background-color: rgb(255 255 0 / 0.2);
|
||||
}
|
||||
|
||||
.hasBorder {
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.textAnnotation img {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.textWidgetAnnotation :is(input, textarea),
|
||||
.choiceWidgetAnnotation select,
|
||||
.buttonWidgetAnnotation:is(.checkBox, .radioButton) input {
|
||||
background-image: var(--annotation-unfocused-field-background);
|
||||
border: 2px solid var(--input-unfocused-border-color);
|
||||
box-sizing: border-box;
|
||||
font: calc(9px * var(--total-scale-factor)) sans-serif;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
vertical-align: top;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.textWidgetAnnotation :is(input, textarea):required,
|
||||
.choiceWidgetAnnotation select:required,
|
||||
.buttonWidgetAnnotation:is(.checkBox, .radioButton) input:required {
|
||||
outline: 1.5px solid red;
|
||||
}
|
||||
|
||||
.choiceWidgetAnnotation select option {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.buttonWidgetAnnotation.radioButton input {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.textWidgetAnnotation textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.textWidgetAnnotation :is(input, textarea)[disabled],
|
||||
.choiceWidgetAnnotation select[disabled],
|
||||
.buttonWidgetAnnotation:is(.checkBox, .radioButton) input[disabled] {
|
||||
background: none;
|
||||
border: 2px solid var(--input-disabled-border-color);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.textWidgetAnnotation :is(input, textarea):hover,
|
||||
.choiceWidgetAnnotation select:hover,
|
||||
.buttonWidgetAnnotation:is(.checkBox, .radioButton) input:hover {
|
||||
border: 2px solid var(--input-hover-border-color);
|
||||
}
|
||||
.textWidgetAnnotation :is(input, textarea):hover,
|
||||
.choiceWidgetAnnotation select:hover,
|
||||
.buttonWidgetAnnotation.checkBox input:hover {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.textWidgetAnnotation :is(input, textarea):focus,
|
||||
.choiceWidgetAnnotation select:focus {
|
||||
background: none;
|
||||
border: 2px solid var(--input-focus-border-color);
|
||||
border-radius: 2px;
|
||||
outline: var(--input-focus-outline);
|
||||
}
|
||||
|
||||
.buttonWidgetAnnotation:is(.checkBox, .radioButton) :focus {
|
||||
background-image: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.buttonWidgetAnnotation.checkBox :focus {
|
||||
border: 2px solid var(--input-focus-border-color);
|
||||
border-radius: 2px;
|
||||
outline: var(--input-focus-outline);
|
||||
}
|
||||
|
||||
.buttonWidgetAnnotation.radioButton :focus {
|
||||
border: 2px solid var(--input-focus-border-color);
|
||||
outline: var(--input-focus-outline);
|
||||
}
|
||||
|
||||
.buttonWidgetAnnotation.checkBox input:checked::before,
|
||||
.buttonWidgetAnnotation.checkBox input:checked::after,
|
||||
.buttonWidgetAnnotation.radioButton input:checked::before {
|
||||
background-color: CanvasText;
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.buttonWidgetAnnotation.checkBox input:checked::before,
|
||||
.buttonWidgetAnnotation.checkBox input:checked::after {
|
||||
height: 80%;
|
||||
left: 45%;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.buttonWidgetAnnotation.checkBox input:checked::before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.buttonWidgetAnnotation.checkBox input:checked::after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.buttonWidgetAnnotation.radioButton input:checked::before {
|
||||
border-radius: 50%;
|
||||
height: 50%;
|
||||
left: 25%;
|
||||
top: 25%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.textWidgetAnnotation input.comb {
|
||||
font-family: monospace;
|
||||
padding-left: 2px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.textWidgetAnnotation input.comb:focus {
|
||||
/*
|
||||
* Letter spacing is placed on the right side of each character. Hence, the
|
||||
* letter spacing of the last character may be placed outside the visible
|
||||
* area, causing horizontal scrolling. We avoid this by extending the width
|
||||
* when the element has focus and revert this when it loses focus.
|
||||
*/
|
||||
width: 103%;
|
||||
}
|
||||
|
||||
.buttonWidgetAnnotation:is(.checkBox, .radioButton) input {
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.fileAttachmentAnnotation .popupTriggerArea {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.popupAnnotation {
|
||||
position: absolute;
|
||||
font-size: calc(9px * var(--total-scale-factor));
|
||||
pointer-events: none;
|
||||
width: max-content;
|
||||
max-width: 45%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.popup {
|
||||
background-color: rgb(255 255 153);
|
||||
color: black;
|
||||
box-shadow: 0 calc(2px * var(--total-scale-factor))
|
||||
calc(5px * var(--total-scale-factor)) rgb(136 136 136);
|
||||
border-radius: calc(2px * var(--total-scale-factor));
|
||||
outline: 1.5px solid rgb(255 255 74);
|
||||
padding: calc(6px * var(--total-scale-factor));
|
||||
cursor: pointer;
|
||||
font: message-box;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
pointer-events: auto;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.popupAnnotation.focused .popup {
|
||||
outline-width: 3px;
|
||||
}
|
||||
|
||||
.popup * {
|
||||
font-size: calc(9px * var(--total-scale-factor));
|
||||
}
|
||||
|
||||
.popup > .header {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.popup > .header > .title {
|
||||
display: inline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.popup > .header .popupDate {
|
||||
display: inline-block;
|
||||
margin-left: calc(5px * var(--total-scale-factor));
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.popupContent {
|
||||
border-top: 1px solid rgb(51 51 51);
|
||||
margin-top: calc(2px * var(--total-scale-factor));
|
||||
padding-top: calc(2px * var(--total-scale-factor));
|
||||
}
|
||||
|
||||
.richText > * {
|
||||
white-space: pre-wrap;
|
||||
font-size: calc(9px * var(--total-scale-factor));
|
||||
}
|
||||
|
||||
.popupTriggerArea {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
backdrop-filter: var(--hcm-highlight-filter);
|
||||
}
|
||||
}
|
||||
|
||||
section svg {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.annotationTextContent {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
color: transparent;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
|
||||
span {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
svg.quadrilateralsContainer {
|
||||
contain: strict;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
351
web/annotation_layer_builder.js
Normal file
@@ -0,0 +1,351 @@
|
||||
/* Copyright 2014 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.
|
||||
*/
|
||||
|
||||
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/annotation_storage").AnnotationStorage} AnnotationStorage */
|
||||
/** @typedef {import("./interfaces").IDownloadManager} IDownloadManager */
|
||||
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./struct_tree_layer_builder.js").StructTreeLayerBuilder} StructTreeLayerBuilder */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
||||
/** @typedef {import("./comment_manager.js").CommentManager} CommentManager */
|
||||
|
||||
import {
|
||||
AnnotationLayer,
|
||||
AnnotationType,
|
||||
setLayerDimensions,
|
||||
Util,
|
||||
} from "pdfjs-lib";
|
||||
import { PresentationModeState } from "./ui_utils.js";
|
||||
|
||||
/**
|
||||
* @typedef {Object} AnnotationLayerBuilderOptions
|
||||
* @property {PDFPageProxy} pdfPage
|
||||
* @property {AnnotationStorage} [annotationStorage]
|
||||
* @property {string} [imageResourcesPath] - Path for image resources, mainly
|
||||
* for annotation icons. Include trailing slash.
|
||||
* @property {boolean} renderForms
|
||||
* @property {IPDFLinkService} linkService
|
||||
* @property {IDownloadManager} [downloadManager]
|
||||
* @property {boolean} [enableComment]
|
||||
* @property {boolean} [enableScripting]
|
||||
* @property {Promise<boolean>} [hasJSActionsPromise]
|
||||
* @property {Promise<Object<string, Array<Object>> | null>}
|
||||
* [fieldObjectsPromise]
|
||||
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap]
|
||||
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||
* @property {AnnotationEditorUIManager} [annotationEditorUIManager]
|
||||
* @property {function} [onAppend]
|
||||
* @property {CommentManager} [commentManager]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} AnnotationLayerBuilderRenderOptions
|
||||
* @property {PageViewport} viewport
|
||||
* @property {string} [intent] - The default value is "display".
|
||||
* @property {StructTreeLayerBuilder} [structTreeLayer]
|
||||
*/
|
||||
|
||||
class AnnotationLayerBuilder {
|
||||
#annotations = null;
|
||||
|
||||
#commentManager = null;
|
||||
|
||||
#externalHide = false;
|
||||
|
||||
#onAppend = null;
|
||||
|
||||
#eventAbortController = null;
|
||||
|
||||
#linksInjected = false;
|
||||
|
||||
/**
|
||||
* @param {AnnotationLayerBuilderOptions} options
|
||||
*/
|
||||
constructor({
|
||||
pdfPage,
|
||||
linkService,
|
||||
downloadManager,
|
||||
annotationStorage = null,
|
||||
imageResourcesPath = "",
|
||||
renderForms = true,
|
||||
enableComment = false,
|
||||
commentManager = null,
|
||||
enableScripting = false,
|
||||
hasJSActionsPromise = null,
|
||||
fieldObjectsPromise = null,
|
||||
annotationCanvasMap = null,
|
||||
accessibilityManager = null,
|
||||
annotationEditorUIManager = null,
|
||||
onAppend = null,
|
||||
}) {
|
||||
this.pdfPage = pdfPage;
|
||||
this.linkService = linkService;
|
||||
this.downloadManager = downloadManager;
|
||||
this.imageResourcesPath = imageResourcesPath;
|
||||
this.renderForms = renderForms;
|
||||
this.annotationStorage = annotationStorage;
|
||||
this.enableComment = enableComment;
|
||||
this.#commentManager = commentManager;
|
||||
this.enableScripting = enableScripting;
|
||||
this._hasJSActionsPromise = hasJSActionsPromise || Promise.resolve(false);
|
||||
this._fieldObjectsPromise = fieldObjectsPromise || Promise.resolve(null);
|
||||
this._annotationCanvasMap = annotationCanvasMap;
|
||||
this._accessibilityManager = accessibilityManager;
|
||||
this._annotationEditorUIManager = annotationEditorUIManager;
|
||||
this.#onAppend = onAppend;
|
||||
|
||||
this.annotationLayer = null;
|
||||
this.div = null;
|
||||
this._cancelled = false;
|
||||
this._eventBus = linkService.eventBus;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AnnotationLayerBuilderRenderOptions} options
|
||||
* @returns {Promise<void>} A promise that is resolved when rendering of the
|
||||
* annotations is complete.
|
||||
*/
|
||||
async render({ viewport, intent = "display", structTreeLayer = null }) {
|
||||
if (this.div) {
|
||||
if (this._cancelled || !this.annotationLayer) {
|
||||
return;
|
||||
}
|
||||
// If an annotationLayer already exists, refresh its children's
|
||||
// transformation matrices.
|
||||
this.annotationLayer.update({
|
||||
viewport: viewport.clone({ dontFlip: true }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const [annotations, hasJSActions, fieldObjects] = await Promise.all([
|
||||
this.pdfPage.getAnnotations({ intent }),
|
||||
this._hasJSActionsPromise,
|
||||
this._fieldObjectsPromise,
|
||||
]);
|
||||
if (this._cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an annotation layer div and render the annotations
|
||||
// if there is at least one annotation.
|
||||
const div = (this.div = document.createElement("div"));
|
||||
div.className = "annotationLayer";
|
||||
this.#onAppend?.(div);
|
||||
this.#initAnnotationLayer(viewport, structTreeLayer);
|
||||
|
||||
if (annotations.length === 0) {
|
||||
this.#annotations = annotations;
|
||||
setLayerDimensions(this.div, viewport);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.annotationLayer.render({
|
||||
annotations,
|
||||
imageResourcesPath: this.imageResourcesPath,
|
||||
renderForms: this.renderForms,
|
||||
downloadManager: this.downloadManager,
|
||||
enableComment: this.enableComment,
|
||||
enableScripting: this.enableScripting,
|
||||
hasJSActions,
|
||||
fieldObjects,
|
||||
});
|
||||
|
||||
this.#annotations = annotations;
|
||||
|
||||
// Ensure that interactive form elements in the annotationLayer are
|
||||
// disabled while PresentationMode is active (see issue 12232).
|
||||
if (this.linkService.isInPresentationMode) {
|
||||
this.#updatePresentationModeState(PresentationModeState.FULLSCREEN);
|
||||
}
|
||||
if (!this.#eventAbortController) {
|
||||
this.#eventAbortController = new AbortController();
|
||||
|
||||
this._eventBus?._on(
|
||||
"presentationmodechanged",
|
||||
evt => {
|
||||
this.#updatePresentationModeState(evt.state);
|
||||
},
|
||||
{ signal: this.#eventAbortController.signal }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#initAnnotationLayer(viewport, structTreeLayer) {
|
||||
this.annotationLayer = new AnnotationLayer({
|
||||
div: this.div,
|
||||
accessibilityManager: this._accessibilityManager,
|
||||
annotationCanvasMap: this._annotationCanvasMap,
|
||||
annotationEditorUIManager: this._annotationEditorUIManager,
|
||||
annotationStorage: this.annotationStorage,
|
||||
page: this.pdfPage,
|
||||
viewport: viewport.clone({ dontFlip: true }),
|
||||
structTreeLayer,
|
||||
commentManager: this.#commentManager,
|
||||
linkService: this.linkService,
|
||||
});
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this._cancelled = true;
|
||||
|
||||
this.#eventAbortController?.abort();
|
||||
this.#eventAbortController = null;
|
||||
}
|
||||
|
||||
hide(internal = false) {
|
||||
this.#externalHide = !internal;
|
||||
if (!this.div) {
|
||||
return;
|
||||
}
|
||||
this.div.hidden = true;
|
||||
}
|
||||
|
||||
hasEditableAnnotations() {
|
||||
return !!this.annotationLayer?.hasEditableAnnotations();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<Object>} inferredLinks
|
||||
* @returns {Promise<void>} A promise that is resolved when the inferred links
|
||||
* are added to the annotation layer.
|
||||
*/
|
||||
async injectLinkAnnotations(inferredLinks) {
|
||||
if (this.#annotations === null) {
|
||||
throw new Error(
|
||||
"`render` method must be called before `injectLinkAnnotations`."
|
||||
);
|
||||
}
|
||||
if (this._cancelled || this.#linksInjected) {
|
||||
return;
|
||||
}
|
||||
this.#linksInjected = true;
|
||||
|
||||
const newLinks = this.#annotations.length
|
||||
? this.#checkInferredLinks(inferredLinks)
|
||||
: inferredLinks;
|
||||
|
||||
if (!newLinks.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.annotationLayer.addLinkAnnotations(newLinks);
|
||||
// Don't show the annotation layer if it was explicitly hidden previously.
|
||||
if (!this.#externalHide) {
|
||||
this.div.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
#updatePresentationModeState(state) {
|
||||
if (!this.div) {
|
||||
return;
|
||||
}
|
||||
let disableFormElements = false;
|
||||
|
||||
switch (state) {
|
||||
case PresentationModeState.FULLSCREEN:
|
||||
disableFormElements = true;
|
||||
break;
|
||||
case PresentationModeState.NORMAL:
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
for (const section of this.div.childNodes) {
|
||||
if (section.hasAttribute("data-internal-link")) {
|
||||
continue;
|
||||
}
|
||||
section.inert = disableFormElements;
|
||||
}
|
||||
}
|
||||
|
||||
#checkInferredLinks(inferredLinks) {
|
||||
function annotationRects(annot) {
|
||||
if (!annot.quadPoints) {
|
||||
return [annot.rect];
|
||||
}
|
||||
const rects = [];
|
||||
for (let i = 2, ii = annot.quadPoints.length; i < ii; i += 8) {
|
||||
const trX = annot.quadPoints[i];
|
||||
const trY = annot.quadPoints[i + 1];
|
||||
const blX = annot.quadPoints[i + 2];
|
||||
const blY = annot.quadPoints[i + 3];
|
||||
rects.push([blX, blY, trX, trY]);
|
||||
}
|
||||
return rects;
|
||||
}
|
||||
|
||||
function intersectAnnotations(annot1, annot2) {
|
||||
const intersections = [];
|
||||
const annot1Rects = annotationRects(annot1);
|
||||
const annot2Rects = annotationRects(annot2);
|
||||
for (const rect1 of annot1Rects) {
|
||||
for (const rect2 of annot2Rects) {
|
||||
const intersection = Util.intersect(rect1, rect2);
|
||||
if (intersection) {
|
||||
intersections.push(intersection);
|
||||
}
|
||||
}
|
||||
}
|
||||
return intersections;
|
||||
}
|
||||
|
||||
function areaRects(rects) {
|
||||
let totalArea = 0;
|
||||
for (const rect of rects) {
|
||||
totalArea += Math.abs((rect[2] - rect[0]) * (rect[3] - rect[1]));
|
||||
}
|
||||
return totalArea;
|
||||
}
|
||||
|
||||
return inferredLinks.filter(link => {
|
||||
let linkAreaRects;
|
||||
|
||||
for (const annotation of this.#annotations) {
|
||||
if (
|
||||
annotation.annotationType !== AnnotationType.LINK ||
|
||||
!annotation.url
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// TODO: Add a test case to verify that we can find the intersection
|
||||
// between two annotations with quadPoints properly.
|
||||
const intersections = intersectAnnotations(annotation, link);
|
||||
|
||||
if (intersections.length === 0) {
|
||||
continue;
|
||||
}
|
||||
linkAreaRects ??= areaRects(annotationRects(link));
|
||||
|
||||
if (
|
||||
areaRects(intersections) / linkAreaRects >
|
||||
0.5 /* If the overlap is more than 50%. */
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { AnnotationLayerBuilder };
|
||||
3207
web/app.js
Normal file
731
web/app_options.js
Normal file
@@ -0,0 +1,731 @@
|
||||
/* Copyright 2018 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.
|
||||
*/
|
||||
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
// eslint-disable-next-line no-var
|
||||
var compatParams = new Map();
|
||||
if (
|
||||
typeof PDFJSDev !== "undefined" &&
|
||||
PDFJSDev.test("LIB") &&
|
||||
!globalThis.navigator?.language
|
||||
) {
|
||||
globalThis.navigator = {
|
||||
language: "en-US",
|
||||
maxTouchPoints: 1,
|
||||
platform: "",
|
||||
userAgent: "",
|
||||
};
|
||||
}
|
||||
const { maxTouchPoints, platform, userAgent } = navigator;
|
||||
|
||||
const isAndroid = /Android/.test(userAgent);
|
||||
const isIOS =
|
||||
/\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) ||
|
||||
(platform === "MacIntel" && maxTouchPoints > 1);
|
||||
|
||||
// Limit canvas size to 5 mega-pixels on mobile.
|
||||
// Support: Android, iOS
|
||||
(function () {
|
||||
if (isIOS || isAndroid) {
|
||||
compatParams.set("maxCanvasPixels", 5242880);
|
||||
}
|
||||
})();
|
||||
|
||||
// Don't use system fonts on Android (issue 18210).
|
||||
// Support: Android
|
||||
(function () {
|
||||
if (isAndroid) {
|
||||
compatParams.set("useSystemFonts", false);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
const OptionKind = {
|
||||
BROWSER: 0x01,
|
||||
VIEWER: 0x02,
|
||||
API: 0x04,
|
||||
WORKER: 0x08,
|
||||
EVENT_DISPATCH: 0x10,
|
||||
PREFERENCE: 0x80,
|
||||
};
|
||||
|
||||
// Should only be used with options that allow multiple types.
|
||||
const Type = {
|
||||
BOOLEAN: 0x01,
|
||||
NUMBER: 0x02,
|
||||
OBJECT: 0x04,
|
||||
STRING: 0x08,
|
||||
UNDEFINED: 0x10,
|
||||
};
|
||||
|
||||
/**
|
||||
* NOTE: These options are used to generate the `default_preferences.json` file,
|
||||
* see `OptionKind.PREFERENCE`, hence the values below must use only
|
||||
* primitive types and cannot rely on any imported types.
|
||||
*/
|
||||
const defaultOptions = {
|
||||
allowedGlobalEvents: {
|
||||
/** @type {Object} */
|
||||
value: null,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
canvasMaxAreaInBytes: {
|
||||
/** @type {number} */
|
||||
value: -1,
|
||||
kind: OptionKind.BROWSER + OptionKind.API,
|
||||
},
|
||||
isInAutomation: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
localeProperties: {
|
||||
/** @type {Object} */
|
||||
value:
|
||||
typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")
|
||||
? { lang: navigator.language || "en-US" }
|
||||
: null,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
maxCanvasDim: {
|
||||
/** @type {number} */
|
||||
value: 32767,
|
||||
kind: OptionKind.BROWSER + OptionKind.VIEWER,
|
||||
},
|
||||
nimbusDataStr: {
|
||||
/** @type {string} */
|
||||
value: "",
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
supportsCaretBrowsingMode: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
supportsDocumentFonts: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
supportsIntegratedFind: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
supportsMouseWheelZoomCtrlKey: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
supportsMouseWheelZoomMetaKey: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
supportsPinchToZoom: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
supportsPrinting: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
toolbarDensity: {
|
||||
/** @type {number} */
|
||||
value: 0, // 0 = "normal", 1 = "compact", 2 = "touch"
|
||||
kind: OptionKind.BROWSER + OptionKind.EVENT_DISPATCH,
|
||||
},
|
||||
|
||||
altTextLearnMoreUrl: {
|
||||
/** @type {string} */
|
||||
value:
|
||||
typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")
|
||||
? "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/pdf-alt-text"
|
||||
: "",
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
annotationEditorMode: {
|
||||
/** @type {number} */
|
||||
value: 0,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
annotationMode: {
|
||||
/** @type {number} */
|
||||
value: 2,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
capCanvasAreaFactor: {
|
||||
/** @type {number} */
|
||||
value: 200,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
commentLearnMoreUrl: {
|
||||
/** @type {string} */
|
||||
value:
|
||||
typeof PDFJSDev === "undefined" || PDFJSDev.test("MOZCENTRAL")
|
||||
? "https://support.mozilla.org/%LOCALE%/kb/view-pdf-files-firefox-or-choose-another-viewer#w_add-a-comment-to-a-pdf"
|
||||
: "",
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
cursorToolOnLoad: {
|
||||
/** @type {number} */
|
||||
value: 0,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
debuggerSrc: {
|
||||
/** @type {string} */
|
||||
value: "./debugger.mjs",
|
||||
kind: OptionKind.VIEWER,
|
||||
},
|
||||
defaultZoomDelay: {
|
||||
/** @type {number} */
|
||||
value: 400,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
defaultZoomValue: {
|
||||
/** @type {string} */
|
||||
value: "",
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
disableHistory: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER,
|
||||
},
|
||||
disablePageLabels: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enableAltText: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enableAltTextModelDownload: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE + OptionKind.EVENT_DISPATCH,
|
||||
},
|
||||
enableAutoLinking: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enableComment: {
|
||||
/** @type {boolean} */
|
||||
value: typeof PDFJSDev === "undefined",
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enableDetailCanvas: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.VIEWER,
|
||||
},
|
||||
enableGuessAltText: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE + OptionKind.EVENT_DISPATCH,
|
||||
},
|
||||
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,
|
||||
},
|
||||
enableNewAltTextWhenAddingImage: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enableOptimizedPartialRendering: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enablePermissions: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enablePrintAutoRotate: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enableScripting: {
|
||||
/** @type {boolean} */
|
||||
value: typeof PDFJSDev === "undefined" || !PDFJSDev.test("CHROME"),
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enableSignatureEditor: {
|
||||
/** @type {boolean} */
|
||||
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"),
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enableUpdatedAddImage: {
|
||||
// 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: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
externalLinkRel: {
|
||||
/** @type {string} */
|
||||
value: "noopener noreferrer nofollow",
|
||||
kind: OptionKind.VIEWER,
|
||||
},
|
||||
externalLinkTarget: {
|
||||
/** @type {number} */
|
||||
value: 0,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
highlightEditorColors: {
|
||||
/** @type {string} */
|
||||
value:
|
||||
"yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F," +
|
||||
"yellow_HCM=#FFFFCC,green_HCM=#53FFBC,blue_HCM=#80EBFF,pink_HCM=#F6B8FF,red_HCM=#C50043",
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
historyUpdateUrl: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
ignoreDestinationZoom: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
imageResourcesPath: {
|
||||
/** @type {string} */
|
||||
value:
|
||||
typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")
|
||||
? "resource://pdf.js/web/images/"
|
||||
: "./images/",
|
||||
kind: OptionKind.VIEWER,
|
||||
},
|
||||
maxCanvasPixels: {
|
||||
/** @type {number} */
|
||||
value: 2 ** 25,
|
||||
kind: OptionKind.VIEWER,
|
||||
},
|
||||
minDurationToUpdateCanvas: {
|
||||
/** @type {number} */
|
||||
value: 500, // ms
|
||||
kind: OptionKind.VIEWER,
|
||||
},
|
||||
forcePageColors: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
pageColorsBackground: {
|
||||
/** @type {string} */
|
||||
value: "Canvas",
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
pageColorsForeground: {
|
||||
/** @type {string} */
|
||||
value: "CanvasText",
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
pdfBugEnabled: {
|
||||
/** @type {boolean} */
|
||||
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"),
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
printResolution: {
|
||||
/** @type {number} */
|
||||
value: 150,
|
||||
kind: OptionKind.VIEWER,
|
||||
},
|
||||
sidebarViewOnLoad: {
|
||||
/** @type {number} */
|
||||
value: -1,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
scrollModeOnLoad: {
|
||||
/** @type {number} */
|
||||
value: -1,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
spreadModeOnLoad: {
|
||||
/** @type {number} */
|
||||
value: -1,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
textLayerMode: {
|
||||
/** @type {number} */
|
||||
value: 1,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
viewerCssTheme: {
|
||||
/** @type {number} */
|
||||
value: typeof PDFJSDev !== "undefined" && PDFJSDev.test("CHROME") ? 2 : 0,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
viewOnLoad: {
|
||||
/** @type {boolean} */
|
||||
value: 0,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
|
||||
cMapPacked: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.API,
|
||||
},
|
||||
cMapUrl: {
|
||||
/** @type {string} */
|
||||
value:
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
typeof PDFJSDev === "undefined"
|
||||
? "../external/bcmaps/"
|
||||
: PDFJSDev.test("MOZCENTRAL")
|
||||
? "resource://pdf.js/web/cmaps/"
|
||||
: "../web/cmaps/",
|
||||
kind: OptionKind.API,
|
||||
},
|
||||
disableAutoFetch: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.API + OptionKind.PREFERENCE,
|
||||
},
|
||||
disableFontFace: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.API + OptionKind.PREFERENCE,
|
||||
},
|
||||
disableRange: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.API + OptionKind.PREFERENCE,
|
||||
},
|
||||
disableStream: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.API + OptionKind.PREFERENCE,
|
||||
},
|
||||
docBaseUrl: {
|
||||
/** @type {string} */
|
||||
value:
|
||||
typeof PDFJSDev === "undefined"
|
||||
? // NOTE: We cannot use the `updateUrlHash` function here, because of
|
||||
// the default preferences generation (see `gulpfile.mjs`).
|
||||
// However, the following line is *only* used in development mode.
|
||||
document.URL.split("#", 1)[0]
|
||||
: "",
|
||||
kind: OptionKind.API,
|
||||
},
|
||||
enableHWA: {
|
||||
/** @type {boolean} */
|
||||
value: typeof PDFJSDev !== "undefined" && !PDFJSDev.test("MOZCENTRAL"),
|
||||
kind: OptionKind.API + OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enableXfa: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.API + OptionKind.PREFERENCE,
|
||||
},
|
||||
fontExtraProperties: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.API,
|
||||
},
|
||||
iccUrl: {
|
||||
/** @type {string} */
|
||||
value:
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
typeof PDFJSDev === "undefined"
|
||||
? "../external/iccs/"
|
||||
: PDFJSDev.test("MOZCENTRAL")
|
||||
? "resource://pdf.js/web/iccs/"
|
||||
: "../web/iccs/",
|
||||
kind: OptionKind.API,
|
||||
},
|
||||
isEvalSupported: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.API,
|
||||
},
|
||||
isOffscreenCanvasSupported: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.API,
|
||||
},
|
||||
maxImageSize: {
|
||||
/** @type {number} */
|
||||
value: -1,
|
||||
kind: OptionKind.API,
|
||||
},
|
||||
pdfBug: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.API,
|
||||
},
|
||||
standardFontDataUrl: {
|
||||
/** @type {string} */
|
||||
value:
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
typeof PDFJSDev === "undefined"
|
||||
? "../external/standard_fonts/"
|
||||
: PDFJSDev.test("MOZCENTRAL")
|
||||
? "resource://pdf.js/web/standard_fonts/"
|
||||
: "../web/standard_fonts/",
|
||||
kind: OptionKind.API,
|
||||
},
|
||||
useSystemFonts: {
|
||||
// On Android, there is almost no chance to have the font we want so we
|
||||
// don't use the system fonts in this case (bug 1882613).
|
||||
/** @type {boolean|undefined} */
|
||||
value: (
|
||||
typeof PDFJSDev === "undefined"
|
||||
? window.isGECKOVIEW
|
||||
: PDFJSDev.test("GECKOVIEW")
|
||||
)
|
||||
? false
|
||||
: undefined,
|
||||
kind: OptionKind.API,
|
||||
type: Type.BOOLEAN + Type.UNDEFINED,
|
||||
},
|
||||
verbosity: {
|
||||
/** @type {number} */
|
||||
value: 1,
|
||||
kind: OptionKind.API,
|
||||
},
|
||||
wasmUrl: {
|
||||
/** @type {string} */
|
||||
value:
|
||||
typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")
|
||||
? "resource://pdf.js/web/wasm/"
|
||||
: "../web/wasm/",
|
||||
kind: OptionKind.API,
|
||||
},
|
||||
|
||||
workerPort: {
|
||||
/** @type {Object} */
|
||||
value:
|
||||
typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")
|
||||
? globalThis.pdfjsPreloadedWorker || null
|
||||
: null,
|
||||
kind: OptionKind.WORKER,
|
||||
},
|
||||
workerSrc: {
|
||||
/** @type {string} */
|
||||
value:
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
typeof PDFJSDev === "undefined"
|
||||
? "../src/pdf.worker.js"
|
||||
: PDFJSDev.test("MOZCENTRAL")
|
||||
? "resource://pdf.js/build/pdf.worker.mjs"
|
||||
: "../build/pdf.worker.mjs",
|
||||
kind: OptionKind.WORKER,
|
||||
},
|
||||
};
|
||||
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
|
||||
defaultOptions.defaultUrl = {
|
||||
/** @type {string} */
|
||||
value:
|
||||
typeof PDFJSDev !== "undefined" && PDFJSDev.test("CHROME")
|
||||
? ""
|
||||
: "compressed.tracemonkey-pldi-09.pdf",
|
||||
kind: OptionKind.VIEWER,
|
||||
};
|
||||
defaultOptions.sandboxBundleSrc = {
|
||||
/** @type {string} */
|
||||
value:
|
||||
typeof PDFJSDev === "undefined"
|
||||
? "../build/dev-sandbox/pdf.sandbox.mjs"
|
||||
: "../build/pdf.sandbox.mjs",
|
||||
kind: OptionKind.VIEWER,
|
||||
};
|
||||
defaultOptions.enableFakeMLManager = {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.VIEWER,
|
||||
};
|
||||
}
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
defaultOptions.disablePreferences = {
|
||||
/** @type {boolean} */
|
||||
value: typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING"),
|
||||
kind: OptionKind.VIEWER,
|
||||
};
|
||||
} else if (PDFJSDev.test("CHROME")) {
|
||||
defaultOptions.disableTelemetry = {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING || LIB")) {
|
||||
// Ensure that the `defaultOptions` are correctly specified.
|
||||
for (const name in defaultOptions) {
|
||||
const { value, kind, type } = 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 (type !== undefined) {
|
||||
throw new Error(
|
||||
`Cannot have \`type\`-field for "PREFERENCE" kind: ${name}`
|
||||
);
|
||||
}
|
||||
if (typeof compatParams === "object" && compatParams.has(name)) {
|
||||
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}`);
|
||||
}
|
||||
} else if (kind & OptionKind.BROWSER) {
|
||||
if (type !== undefined) {
|
||||
throw new Error(
|
||||
`Cannot have \`type\`-field for "BROWSER" kind: ${name}`
|
||||
);
|
||||
}
|
||||
if (typeof compatParams === "object" && compatParams.has(name)) {
|
||||
throw new Error(
|
||||
`Should not have compatibility-value for "BROWSER" kind: ${name}`
|
||||
);
|
||||
}
|
||||
if (value === undefined) {
|
||||
throw new Error(`Invalid value for "BROWSER" kind: ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppOptions {
|
||||
static eventBus;
|
||||
|
||||
static #opts = new Map();
|
||||
|
||||
static {
|
||||
// Initialize all the user-options.
|
||||
for (const name in defaultOptions) {
|
||||
this.#opts.set(name, defaultOptions[name].value);
|
||||
}
|
||||
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
// Apply any compatibility-values to the user-options.
|
||||
for (const [name, value] of compatParams) {
|
||||
this.#opts.set(name, value);
|
||||
}
|
||||
this._hasInvokedSet = false;
|
||||
|
||||
this._checkDisablePreferences = () => {
|
||||
if (this.get("disablePreferences")) {
|
||||
// Give custom implementations of the default viewer a simpler way to
|
||||
// opt-out of having the `Preferences` override existing `AppOptions`.
|
||||
return true;
|
||||
}
|
||||
if (this._hasInvokedSet) {
|
||||
console.warn(
|
||||
"The Preferences may override manually set AppOptions; " +
|
||||
'please use the "disablePreferences"-option to prevent that.'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
|
||||
throw new Error("Cannot initialize AppOptions.");
|
||||
}
|
||||
}
|
||||
|
||||
static get(name) {
|
||||
return this.#opts.get(name);
|
||||
}
|
||||
|
||||
static getAll(kind = null, defaultOnly = false) {
|
||||
const options = Object.create(null);
|
||||
for (const name in defaultOptions) {
|
||||
const defaultOpt = defaultOptions[name];
|
||||
|
||||
if (kind && !(kind & defaultOpt.kind)) {
|
||||
continue;
|
||||
}
|
||||
options[name] = !defaultOnly ? this.#opts.get(name) : defaultOpt.value;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
static set(name, value) {
|
||||
this.setAll({ [name]: value });
|
||||
}
|
||||
|
||||
static setAll(options, prefs = false) {
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
this._hasInvokedSet ||= true;
|
||||
}
|
||||
let events;
|
||||
|
||||
for (const name in options) {
|
||||
const defaultOpt = defaultOptions[name],
|
||||
userOpt = options[name];
|
||||
|
||||
if (
|
||||
!defaultOpt ||
|
||||
!(
|
||||
typeof userOpt === typeof defaultOpt.value ||
|
||||
Type[(typeof userOpt).toUpperCase()] & defaultOpt.type
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const { kind } = defaultOpt;
|
||||
|
||||
if (
|
||||
prefs &&
|
||||
!(kind & OptionKind.BROWSER || kind & OptionKind.PREFERENCE)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (this.eventBus && kind & OptionKind.EVENT_DISPATCH) {
|
||||
(events ||= new Map()).set(name, userOpt);
|
||||
}
|
||||
this.#opts.set(name, userOpt);
|
||||
}
|
||||
|
||||
if (events) {
|
||||
for (const [name, value] of events) {
|
||||
this.eventBus.dispatch(name.toLowerCase(), { source: this, value });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { AppOptions, OptionKind };
|
||||
180
web/autolinker.js
Normal file
@@ -0,0 +1,180 @@
|
||||
/* Copyright 2025 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 { AnnotationType, createValidAbsoluteUrl, Util } from "pdfjs-lib";
|
||||
import { getOriginalIndex, normalize } from "./pdf_find_controller.js";
|
||||
|
||||
function DOMRectToPDF({ width, height, left, top }, pdfPageView) {
|
||||
if (width === 0 || height === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pageBox = pdfPageView.textLayer.div.getBoundingClientRect();
|
||||
const bottomLeft = pdfPageView.getPagePoint(
|
||||
left - pageBox.left,
|
||||
top - pageBox.top
|
||||
);
|
||||
const topRight = pdfPageView.getPagePoint(
|
||||
left - pageBox.left + width,
|
||||
top - pageBox.top + height
|
||||
);
|
||||
|
||||
return Util.normalizeRect([
|
||||
bottomLeft[0],
|
||||
bottomLeft[1],
|
||||
topRight[0],
|
||||
topRight[1],
|
||||
]);
|
||||
}
|
||||
|
||||
function calculateLinkPosition(range, pdfPageView) {
|
||||
const rangeRects = range.getClientRects();
|
||||
if (rangeRects.length === 1) {
|
||||
return { rect: DOMRectToPDF(rangeRects[0], pdfPageView) };
|
||||
}
|
||||
|
||||
const rect = [Infinity, Infinity, -Infinity, -Infinity];
|
||||
const quadPoints = [];
|
||||
let i = 0;
|
||||
for (const domRect of rangeRects) {
|
||||
const normalized = DOMRectToPDF(domRect, pdfPageView);
|
||||
if (normalized === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
quadPoints[i] = quadPoints[i + 4] = normalized[0];
|
||||
quadPoints[i + 1] = quadPoints[i + 3] = normalized[3];
|
||||
quadPoints[i + 2] = quadPoints[i + 6] = normalized[2];
|
||||
quadPoints[i + 5] = quadPoints[i + 7] = normalized[1];
|
||||
|
||||
Util.rectBoundingBox(...normalized, rect);
|
||||
i += 8;
|
||||
}
|
||||
return { quadPoints, rect };
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a DOM node `container` and an index into its text contents `offset`,
|
||||
* returns a pair consisting of text node that the `offset` actually points
|
||||
* to, together with the offset relative to that text node.
|
||||
* When the offset points at the boundary between two node, the result will
|
||||
* point to the first text node in depth-first traversal order.
|
||||
*
|
||||
* For example, given this DOM:
|
||||
* <p>abc<span>def</span>ghi</p>
|
||||
*
|
||||
* textPosition(p, 0) -> [#text "abc", 0] (before `a`)
|
||||
* textPosition(p, 2) -> [#text "abc", 2] (between `b` and `c`)
|
||||
* textPosition(p, 3) -> [#text "abc", 3] (after `c`)
|
||||
* textPosition(p, 5) -> [#text "def", 2] (between `e` and `f`)
|
||||
* textPosition(p, 6) -> [#text "def", 3] (after `f`)
|
||||
*/
|
||||
function textPosition(container, offset) {
|
||||
let currentContainer = container;
|
||||
do {
|
||||
if (currentContainer.nodeType === Node.TEXT_NODE) {
|
||||
const currentLength = currentContainer.textContent.length;
|
||||
if (offset <= currentLength) {
|
||||
return [currentContainer, offset];
|
||||
}
|
||||
offset -= currentLength;
|
||||
} else if (currentContainer.firstChild) {
|
||||
currentContainer = currentContainer.firstChild;
|
||||
continue;
|
||||
}
|
||||
|
||||
while (!currentContainer.nextSibling && currentContainer !== container) {
|
||||
currentContainer = currentContainer.parentNode;
|
||||
}
|
||||
if (currentContainer !== container) {
|
||||
currentContainer = currentContainer.nextSibling;
|
||||
}
|
||||
} while (currentContainer !== container);
|
||||
throw new Error("Offset is bigger than container's contents length.");
|
||||
}
|
||||
|
||||
function createLinkAnnotation({ url, index, length }, pdfPageView, id) {
|
||||
const highlighter = pdfPageView._textHighlighter;
|
||||
const [{ begin, end }] = highlighter._convertMatches([index], [length]);
|
||||
|
||||
const range = new Range();
|
||||
range.setStart(
|
||||
...textPosition(highlighter.textDivs[begin.divIdx], begin.offset)
|
||||
);
|
||||
range.setEnd(...textPosition(highlighter.textDivs[end.divIdx], end.offset));
|
||||
|
||||
return {
|
||||
id: `inferred_link_${id}`,
|
||||
unsafeUrl: url,
|
||||
url,
|
||||
annotationType: AnnotationType.LINK,
|
||||
rotation: 0,
|
||||
...calculateLinkPosition(range, pdfPageView),
|
||||
// Populated in the annotationLayer to avoid unnecessary object creation,
|
||||
// since most inferred links overlap existing LinkAnnotations:
|
||||
borderStyle: null,
|
||||
};
|
||||
}
|
||||
|
||||
class Autolinker {
|
||||
static #index = 0;
|
||||
|
||||
static #regex;
|
||||
|
||||
static findLinks(text) {
|
||||
// Regex can be tested and verified at https://regex101.com/r/rXoLiT/2.
|
||||
this.#regex ??=
|
||||
/\b(?:https?:\/\/|mailto:|www\.)(?:[\S--[\p{P}<>]]|\/|[\S--[\[\]]]+[\S--[\p{P}<>]])+|\b[\S--[@\p{Ps}\p{Pe}<>]]+@([\S--[\p{P}<>]]+(?:\.[\S--[\p{P}<>]]+)+)/gmv;
|
||||
|
||||
const [normalizedText, diffs] = normalize(text, { ignoreDashEOL: true });
|
||||
const matches = normalizedText.matchAll(this.#regex);
|
||||
const links = [];
|
||||
for (const match of matches) {
|
||||
const [url, emailDomain] = match;
|
||||
let raw;
|
||||
if (
|
||||
url.startsWith("www.") ||
|
||||
url.startsWith("http://") ||
|
||||
url.startsWith("https://")
|
||||
) {
|
||||
raw = url;
|
||||
} else if (URL.canParse(`http://${emailDomain}`)) {
|
||||
raw = url.startsWith("mailto:") ? url : `mailto:${url}`;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
const absoluteURL = createValidAbsoluteUrl(raw, null, {
|
||||
addDefaultProtocol: true,
|
||||
});
|
||||
if (absoluteURL) {
|
||||
const [index, length] = getOriginalIndex(
|
||||
diffs,
|
||||
match.index,
|
||||
url.length
|
||||
);
|
||||
links.push({ url: absoluteURL.href, index, length });
|
||||
}
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
static processLinks(pdfPageView) {
|
||||
return this.findLinks(
|
||||
pdfPageView._textHighlighter.textContentItemsStr.join("\n")
|
||||
).map(link => createLinkAnnotation(link, pdfPageView, this.#index++));
|
||||
}
|
||||
}
|
||||
|
||||
export { Autolinker };
|
||||
278
web/base_pdf_page_view.js
Normal file
@@ -0,0 +1,278 @@
|
||||
/* Copyright 2012 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 { RenderingCancelledException } from "pdfjs-lib";
|
||||
import { RenderingStates } from "./ui_utils.js";
|
||||
|
||||
class BasePDFPageView {
|
||||
#loadingId = null;
|
||||
|
||||
#minDurationToUpdateCanvas = 0;
|
||||
|
||||
#renderError = null;
|
||||
|
||||
#renderingState = RenderingStates.INITIAL;
|
||||
|
||||
#showCanvas = null;
|
||||
|
||||
#startTime = 0;
|
||||
|
||||
#tempCanvas = null;
|
||||
|
||||
canvas = null;
|
||||
|
||||
/** @type {null | HTMLDivElement} */
|
||||
div = null;
|
||||
|
||||
enableOptimizedPartialRendering = false;
|
||||
|
||||
eventBus = null;
|
||||
|
||||
id = null;
|
||||
|
||||
pageColors = null;
|
||||
|
||||
recordedBBoxes = null;
|
||||
|
||||
renderingQueue = null;
|
||||
|
||||
renderTask = null;
|
||||
|
||||
resume = null;
|
||||
|
||||
constructor(options) {
|
||||
this.eventBus = options.eventBus;
|
||||
this.id = options.id;
|
||||
this.pageColors = options.pageColors || null;
|
||||
this.renderingQueue = options.renderingQueue;
|
||||
this.enableOptimizedPartialRendering =
|
||||
options.enableOptimizedPartialRendering ?? false;
|
||||
this.#minDurationToUpdateCanvas = options.minDurationToUpdateCanvas ?? 500;
|
||||
}
|
||||
|
||||
get renderingState() {
|
||||
return this.#renderingState;
|
||||
}
|
||||
|
||||
set renderingState(state) {
|
||||
if (state === this.#renderingState) {
|
||||
return;
|
||||
}
|
||||
this.#renderingState = state;
|
||||
|
||||
if (this.#loadingId) {
|
||||
clearTimeout(this.#loadingId);
|
||||
this.#loadingId = null;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case RenderingStates.PAUSED:
|
||||
this.div.classList.remove("loading");
|
||||
// Display the canvas as it has been drawn.
|
||||
this.#startTime = 0;
|
||||
this.#showCanvas?.(false);
|
||||
break;
|
||||
case RenderingStates.RUNNING:
|
||||
this.div.classList.add("loadingIcon");
|
||||
this.#loadingId = setTimeout(() => {
|
||||
// Adding the loading class is slightly postponed in order to not have
|
||||
// it with loadingIcon.
|
||||
// If we don't do that the visibility of the background is changed but
|
||||
// the transition isn't triggered.
|
||||
this.div.classList.add("loading");
|
||||
this.#loadingId = null;
|
||||
}, 0);
|
||||
this.#startTime = Date.now();
|
||||
break;
|
||||
case RenderingStates.INITIAL:
|
||||
case RenderingStates.FINISHED:
|
||||
this.div.classList.remove("loadingIcon", "loading");
|
||||
this.#startTime = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_createCanvas(onShow, hideUntilComplete = false) {
|
||||
const { pageColors } = this;
|
||||
const hasHCM = !!(pageColors?.background && pageColors?.foreground);
|
||||
const prevCanvas = this.canvas;
|
||||
|
||||
// In HCM, a final filter is applied on the canvas which means that
|
||||
// before it's applied we've normal colors. Consequently, to avoid to
|
||||
// have a final flash we just display it once all the drawing is done.
|
||||
const updateOnFirstShow = !prevCanvas && !hasHCM && !hideUntilComplete;
|
||||
|
||||
let canvas = (this.canvas = document.createElement("canvas"));
|
||||
|
||||
this.#showCanvas = isLastShow => {
|
||||
if (updateOnFirstShow) {
|
||||
let tempCanvas = this.#tempCanvas;
|
||||
if (!isLastShow && this.#minDurationToUpdateCanvas > 0) {
|
||||
// We draw on the canvas at 60fps (in using `requestAnimationFrame`),
|
||||
// so if the canvas is large, updating it at 60fps can be a way too
|
||||
// much and can cause some serious performance issues.
|
||||
// To avoid that we only update the canvas every
|
||||
// `this.#minDurationToUpdateCanvas` ms.
|
||||
|
||||
if (Date.now() - this.#startTime < this.#minDurationToUpdateCanvas) {
|
||||
return;
|
||||
}
|
||||
if (!tempCanvas) {
|
||||
tempCanvas = this.#tempCanvas = canvas;
|
||||
canvas = this.canvas = canvas.cloneNode(false);
|
||||
onShow(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
if (tempCanvas) {
|
||||
const ctx = canvas.getContext("2d", {
|
||||
alpha: false,
|
||||
});
|
||||
ctx.drawImage(tempCanvas, 0, 0);
|
||||
if (isLastShow) {
|
||||
this.#resetTempCanvas();
|
||||
} else {
|
||||
this.#startTime = Date.now();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't add the canvas until the first draw callback, or until
|
||||
// drawing is complete when `!this.renderingQueue`, to prevent black
|
||||
// flickering.
|
||||
onShow(canvas);
|
||||
this.#showCanvas = null;
|
||||
return;
|
||||
}
|
||||
if (!isLastShow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prevCanvas) {
|
||||
prevCanvas.replaceWith(canvas);
|
||||
prevCanvas.width = prevCanvas.height = 0;
|
||||
} else {
|
||||
onShow(canvas);
|
||||
}
|
||||
};
|
||||
|
||||
return { canvas, prevCanvas };
|
||||
}
|
||||
|
||||
#renderContinueCallback = cont => {
|
||||
this.#showCanvas?.(false);
|
||||
if (this.renderingQueue && !this.renderingQueue.isHighestPriority(this)) {
|
||||
this.renderingState = RenderingStates.PAUSED;
|
||||
this.resume = () => {
|
||||
this.renderingState = RenderingStates.RUNNING;
|
||||
cont();
|
||||
};
|
||||
return;
|
||||
}
|
||||
cont();
|
||||
};
|
||||
|
||||
_resetCanvas() {
|
||||
const { canvas } = this;
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
canvas.remove();
|
||||
canvas.width = canvas.height = 0;
|
||||
this.canvas = null;
|
||||
this.#resetTempCanvas();
|
||||
}
|
||||
|
||||
#resetTempCanvas() {
|
||||
if (this.#tempCanvas) {
|
||||
this.#tempCanvas.width = this.#tempCanvas.height = 0;
|
||||
this.#tempCanvas = null;
|
||||
}
|
||||
}
|
||||
|
||||
async _drawCanvas(options, onCancel, onFinish) {
|
||||
const renderTask = (this.renderTask = this.pdfPage.render(options));
|
||||
renderTask.onContinue = this.#renderContinueCallback;
|
||||
renderTask.onError = error => {
|
||||
if (error instanceof RenderingCancelledException) {
|
||||
onCancel();
|
||||
this.#renderError = null;
|
||||
}
|
||||
};
|
||||
|
||||
let error = null;
|
||||
try {
|
||||
await renderTask.promise;
|
||||
this.#showCanvas?.(true);
|
||||
} catch (e) {
|
||||
// When zooming with a `drawingDelay` set, avoid temporarily showing
|
||||
// a black canvas if rendering was cancelled before the `onContinue`-
|
||||
// callback had been invoked at least once.
|
||||
if (e instanceof RenderingCancelledException) {
|
||||
return;
|
||||
}
|
||||
error = e;
|
||||
|
||||
this.#showCanvas?.(true);
|
||||
} finally {
|
||||
this.#renderError = error;
|
||||
|
||||
// The renderTask may have been replaced by a new one, so only remove
|
||||
// the reference to the renderTask if it matches the one that is
|
||||
// triggering this callback.
|
||||
if (renderTask === this.renderTask) {
|
||||
this.renderTask = null;
|
||||
if (this.enableOptimizedPartialRendering) {
|
||||
this.recordedBBoxes ??= renderTask.recordedBBoxes;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.renderingState = RenderingStates.FINISHED;
|
||||
|
||||
onFinish(renderTask);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
cancelRendering({ cancelExtraDelay = 0 } = {}) {
|
||||
if (this.renderTask) {
|
||||
this.renderTask.cancel(cancelExtraDelay);
|
||||
this.renderTask = null;
|
||||
}
|
||||
this.resume = null;
|
||||
}
|
||||
|
||||
dispatchPageRender() {
|
||||
this.eventBus.dispatch("pagerender", {
|
||||
source: this,
|
||||
pageNumber: this.id,
|
||||
});
|
||||
}
|
||||
|
||||
dispatchPageRendered(cssTransform, isDetailView) {
|
||||
this.eventBus.dispatch("pagerendered", {
|
||||
source: this,
|
||||
pageNumber: this.id,
|
||||
cssTransform,
|
||||
isDetailView,
|
||||
timestamp: performance.now(),
|
||||
error: this.#renderError,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { BasePDFPageView };
|
||||
192
web/base_tree_viewer.js
Normal file
@@ -0,0 +1,192 @@
|
||||
/* Copyright 2020 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 { removeNullCharacters } from "./ui_utils.js";
|
||||
|
||||
const TREEITEM_OFFSET_TOP = -100; // px
|
||||
const TREEITEM_SELECTED_CLASS = "selected";
|
||||
|
||||
class BaseTreeViewer {
|
||||
constructor(options) {
|
||||
if (
|
||||
(typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
|
||||
this.constructor === BaseTreeViewer
|
||||
) {
|
||||
throw new Error("Cannot initialize BaseTreeViewer.");
|
||||
}
|
||||
this.container = options.container;
|
||||
this.eventBus = options.eventBus;
|
||||
this._l10n = options.l10n;
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this._pdfDocument = null;
|
||||
this._lastToggleIsShow = true;
|
||||
this._currentTreeItem = null;
|
||||
|
||||
// Remove the tree from the DOM.
|
||||
this.container.textContent = "";
|
||||
// Ensure that the left (right in RTL locales) margin is always reset,
|
||||
// to prevent incorrect tree alignment if a new document is opened.
|
||||
this.container.classList.remove("treeWithDeepNesting");
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
_dispatchEvent(count) {
|
||||
throw new Error("Not implemented: _dispatchEvent");
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
_bindLink(element, params) {
|
||||
throw new Error("Not implemented: _bindLink");
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_normalizeTextContent(str) {
|
||||
// Chars in range [0x01-0x1F] will be replaced with a white space
|
||||
// and 0x00 by "".
|
||||
return (
|
||||
removeNullCharacters(str, /* replaceInvisible */ true) ||
|
||||
/* en dash = */ "\u2013"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepend a button before a tree item which allows the user to collapse or
|
||||
* expand all tree items at that level; see `_toggleTreeItem`.
|
||||
* @param {HTMLDivElement} div
|
||||
* @param {boolean|object} [hidden]
|
||||
* @protected
|
||||
*/
|
||||
_addToggleButton(div, hidden = false) {
|
||||
const toggler = document.createElement("div");
|
||||
toggler.className = "treeItemToggler";
|
||||
if (hidden) {
|
||||
toggler.classList.add("treeItemsHidden");
|
||||
}
|
||||
toggler.onclick = evt => {
|
||||
evt.stopPropagation();
|
||||
toggler.classList.toggle("treeItemsHidden");
|
||||
|
||||
if (evt.shiftKey) {
|
||||
const shouldShowAll = !toggler.classList.contains("treeItemsHidden");
|
||||
this._toggleTreeItem(div, shouldShowAll);
|
||||
}
|
||||
};
|
||||
div.prepend(toggler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse or expand the subtree of a tree item.
|
||||
*
|
||||
* @param {Element} root - the root of the item (sub)tree.
|
||||
* @param {boolean} show - whether to show the item (sub)tree. If false,
|
||||
* the item subtree rooted at `root` will be collapsed.
|
||||
* @private
|
||||
*/
|
||||
_toggleTreeItem(root, show = false) {
|
||||
// Pause translation when collapsing/expanding the subtree.
|
||||
this._l10n.pause();
|
||||
|
||||
this._lastToggleIsShow = show;
|
||||
for (const toggler of root.querySelectorAll(".treeItemToggler")) {
|
||||
toggler.classList.toggle("treeItemsHidden", !show);
|
||||
}
|
||||
this._l10n.resume();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse or expand all subtrees of the `container`.
|
||||
* @private
|
||||
*/
|
||||
_toggleAllTreeItems() {
|
||||
this._toggleTreeItem(this.container, !this._lastToggleIsShow);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_finishRendering(fragment, count, hasAnyNesting = false) {
|
||||
if (hasAnyNesting) {
|
||||
this.container.classList.add("treeWithDeepNesting");
|
||||
|
||||
this._lastToggleIsShow = !fragment.querySelector(".treeItemsHidden");
|
||||
}
|
||||
// Pause translation when inserting the tree into the DOM.
|
||||
this._l10n.pause();
|
||||
this.container.append(fragment);
|
||||
this._l10n.resume();
|
||||
|
||||
this._dispatchEvent(count);
|
||||
}
|
||||
|
||||
render(params) {
|
||||
throw new Error("Not implemented: render");
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_updateCurrentTreeItem(treeItem = null) {
|
||||
if (this._currentTreeItem) {
|
||||
// Ensure that the current treeItem-selection is always removed.
|
||||
this._currentTreeItem.classList.remove(TREEITEM_SELECTED_CLASS);
|
||||
this._currentTreeItem = null;
|
||||
}
|
||||
if (treeItem) {
|
||||
treeItem.classList.add(TREEITEM_SELECTED_CLASS);
|
||||
this._currentTreeItem = treeItem;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_scrollToCurrentTreeItem(treeItem) {
|
||||
if (!treeItem) {
|
||||
return;
|
||||
}
|
||||
// Pause translation when expanding the treeItem.
|
||||
this._l10n.pause();
|
||||
// Ensure that the treeItem is *fully* expanded, such that it will first of
|
||||
// all be visible and secondly that scrolling it into view works correctly.
|
||||
let currentNode = treeItem.parentNode;
|
||||
while (currentNode && currentNode !== this.container) {
|
||||
if (currentNode.classList.contains("treeItem")) {
|
||||
const toggler = currentNode.firstElementChild;
|
||||
toggler?.classList.remove("treeItemsHidden");
|
||||
}
|
||||
currentNode = currentNode.parentNode;
|
||||
}
|
||||
this._l10n.resume();
|
||||
|
||||
this._updateCurrentTreeItem(treeItem);
|
||||
|
||||
this.container.scrollTo(
|
||||
treeItem.offsetLeft,
|
||||
treeItem.offsetTop + TREEITEM_OFFSET_TOP
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { BaseTreeViewer };
|
||||
365
web/caret_browsing.js
Normal file
@@ -0,0 +1,365 @@
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
// Used to compare floats: there is no exact equality due to rounding errors.
|
||||
const PRECISION = 1e-1;
|
||||
|
||||
class CaretBrowsingMode {
|
||||
#mainContainer;
|
||||
|
||||
#toolBarHeight = 0;
|
||||
|
||||
#viewerContainer;
|
||||
|
||||
constructor(abortSignal, mainContainer, viewerContainer, toolbarContainer) {
|
||||
this.#mainContainer = mainContainer;
|
||||
this.#viewerContainer = viewerContainer;
|
||||
|
||||
if (!toolbarContainer) {
|
||||
return;
|
||||
}
|
||||
this.#toolBarHeight = toolbarContainer.getBoundingClientRect().height;
|
||||
|
||||
const toolbarObserver = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
if (entry.target === toolbarContainer) {
|
||||
this.#toolBarHeight = Math.floor(entry.borderBoxSize[0].blockSize);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
toolbarObserver.observe(toolbarContainer);
|
||||
|
||||
abortSignal.addEventListener("abort", () => toolbarObserver.disconnect(), {
|
||||
once: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the two rectangles are on the same line.
|
||||
* @param {DOMRect} rect1
|
||||
* @param {DOMRect} rect2
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#isOnSameLine(rect1, rect2) {
|
||||
const top1 = rect1.y;
|
||||
const bot1 = rect1.bottom;
|
||||
const mid1 = rect1.y + rect1.height / 2;
|
||||
|
||||
const top2 = rect2.y;
|
||||
const bot2 = rect2.bottom;
|
||||
const mid2 = rect2.y + rect2.height / 2;
|
||||
|
||||
return (top1 <= mid2 && mid2 <= bot1) || (top2 <= mid1 && mid1 <= bot2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return `true` if the rectangle is:
|
||||
* - under the caret when `isUp === false`.
|
||||
* - over the caret when `isUp === true`.
|
||||
* @param {DOMRect} rect
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {boolean} isUp
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#isUnderOver(rect, x, y, isUp) {
|
||||
const midY = rect.y + rect.height / 2;
|
||||
return (
|
||||
(isUp ? y >= midY : y <= midY) &&
|
||||
rect.x - PRECISION <= x &&
|
||||
x <= rect.right + PRECISION
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the rectangle is visible.
|
||||
* @param {DOMRect} rect
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#isVisible(rect) {
|
||||
return (
|
||||
rect.top >= this.#toolBarHeight &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <=
|
||||
(window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the position of the caret.
|
||||
* @param {Selection} selection
|
||||
* @param {boolean} isUp
|
||||
* @returns {Array<number>}
|
||||
*/
|
||||
#getCaretPosition(selection, isUp) {
|
||||
const { focusNode, focusOffset } = selection;
|
||||
const range = document.createRange();
|
||||
range.setStart(focusNode, focusOffset);
|
||||
range.setEnd(focusNode, focusOffset);
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
return [rect.x, isUp ? rect.top : rect.bottom];
|
||||
}
|
||||
|
||||
static #caretPositionFromPoint(x, y) {
|
||||
if (
|
||||
(typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) &&
|
||||
!document.caretPositionFromPoint
|
||||
) {
|
||||
const { startContainer: offsetNode, startOffset: offset } =
|
||||
document.caretRangeFromPoint(x, y);
|
||||
return { offsetNode, offset };
|
||||
}
|
||||
return document.caretPositionFromPoint(x, y);
|
||||
}
|
||||
|
||||
#setCaretPositionHelper(selection, caretX, select, element, rect) {
|
||||
rect ||= element.getBoundingClientRect();
|
||||
if (caretX <= rect.x + PRECISION) {
|
||||
if (select) {
|
||||
selection.extend(element.firstChild, 0);
|
||||
} else {
|
||||
selection.setPosition(element.firstChild, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (rect.right - PRECISION <= caretX) {
|
||||
const { lastChild } = element;
|
||||
if (select) {
|
||||
selection.extend(lastChild, lastChild.length);
|
||||
} else {
|
||||
selection.setPosition(lastChild, lastChild.length);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const midY = rect.y + rect.height / 2;
|
||||
let caretPosition = CaretBrowsingMode.#caretPositionFromPoint(caretX, midY);
|
||||
let parentElement = caretPosition.offsetNode?.parentElement;
|
||||
if (parentElement && parentElement !== element) {
|
||||
// There is an element on top of the one in the text layer, so we
|
||||
// need to hide all the elements (except the one in the text layer)
|
||||
// at this position in order to get the correct caret position.
|
||||
const elementsAtPoint = document.elementsFromPoint(caretX, midY);
|
||||
const savedVisibilities = [];
|
||||
for (const el of elementsAtPoint) {
|
||||
if (el === element) {
|
||||
break;
|
||||
}
|
||||
const { style } = el;
|
||||
savedVisibilities.push([el, style.visibility]);
|
||||
style.visibility = "hidden";
|
||||
}
|
||||
caretPosition = CaretBrowsingMode.#caretPositionFromPoint(caretX, midY);
|
||||
parentElement = caretPosition.offsetNode?.parentElement;
|
||||
for (const [el, visibility] of savedVisibilities) {
|
||||
el.style.visibility = visibility;
|
||||
}
|
||||
}
|
||||
if (parentElement !== element) {
|
||||
// The element targeted by caretPositionFromPoint isn't in the text
|
||||
// layer.
|
||||
if (select) {
|
||||
selection.extend(element.firstChild, 0);
|
||||
} else {
|
||||
selection.setPosition(element.firstChild, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (select) {
|
||||
selection.extend(caretPosition.offsetNode, caretPosition.offset);
|
||||
} else {
|
||||
selection.setPosition(caretPosition.offsetNode, caretPosition.offset);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the caret position or extend the selection (it depends on the select
|
||||
* parameter).
|
||||
* @param {boolean} select
|
||||
* @param {Selection} selection
|
||||
* @param {Element} newLineElement
|
||||
* @param {DOMRect} newLineElementRect
|
||||
* @param {number} caretX
|
||||
*/
|
||||
#setCaretPosition(
|
||||
select,
|
||||
selection,
|
||||
newLineElement,
|
||||
newLineElementRect,
|
||||
caretX
|
||||
) {
|
||||
if (this.#isVisible(newLineElementRect)) {
|
||||
this.#setCaretPositionHelper(
|
||||
selection,
|
||||
caretX,
|
||||
select,
|
||||
newLineElement,
|
||||
newLineElementRect
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.#mainContainer.addEventListener(
|
||||
"scrollend",
|
||||
this.#setCaretPositionHelper.bind(
|
||||
this,
|
||||
selection,
|
||||
caretX,
|
||||
select,
|
||||
newLineElement,
|
||||
null
|
||||
),
|
||||
{ once: true }
|
||||
);
|
||||
newLineElement.scrollIntoView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the node on the next page.
|
||||
* @param {Element} textLayer
|
||||
* @param {boolean} isUp
|
||||
* @returns {Node}
|
||||
*/
|
||||
#getNodeOnNextPage(textLayer, isUp) {
|
||||
while (true) {
|
||||
const page = textLayer.closest(".page");
|
||||
const pageNumber = parseInt(page.getAttribute("data-page-number"));
|
||||
const nextPage = isUp ? pageNumber - 1 : pageNumber + 1;
|
||||
textLayer = this.#viewerContainer.querySelector(
|
||||
`.page[data-page-number="${nextPage}"] .textLayer`
|
||||
);
|
||||
if (!textLayer) {
|
||||
return null;
|
||||
}
|
||||
const walker = document.createTreeWalker(textLayer, NodeFilter.SHOW_TEXT);
|
||||
const node = isUp ? walker.lastChild() : walker.firstChild();
|
||||
if (node) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the caret in the given direction.
|
||||
* @param {boolean} isUp
|
||||
* @param {boolean} select
|
||||
*/
|
||||
moveCaret(isUp, select) {
|
||||
const selection = document.getSelection();
|
||||
if (selection.rangeCount === 0) {
|
||||
return;
|
||||
}
|
||||
const { focusNode } = selection;
|
||||
const focusElement =
|
||||
focusNode.nodeType !== Node.ELEMENT_NODE
|
||||
? focusNode.parentElement
|
||||
: focusNode;
|
||||
const root = focusElement.closest(".textLayer");
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
||||
walker.currentNode = focusNode;
|
||||
|
||||
// Move to the next element which is not on the same line as the focus
|
||||
// element.
|
||||
const focusRect = focusElement.getBoundingClientRect();
|
||||
let newLineElement = null;
|
||||
const nodeIterator = (
|
||||
isUp ? walker.previousSibling : walker.nextSibling
|
||||
).bind(walker);
|
||||
while (nodeIterator()) {
|
||||
const element = walker.currentNode.parentElement;
|
||||
if (!this.#isOnSameLine(focusRect, element.getBoundingClientRect())) {
|
||||
newLineElement = element;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!newLineElement) {
|
||||
// Need to find the next line on the next page.
|
||||
const node = this.#getNodeOnNextPage(root, isUp);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
if (select) {
|
||||
const lastNode =
|
||||
(isUp ? walker.firstChild() : walker.lastChild()) || focusNode;
|
||||
selection.extend(lastNode, isUp ? 0 : lastNode.length);
|
||||
const range = document.createRange();
|
||||
range.setStart(node, isUp ? node.length : 0);
|
||||
range.setEnd(node, isUp ? node.length : 0);
|
||||
selection.addRange(range);
|
||||
return;
|
||||
}
|
||||
const [caretX] = this.#getCaretPosition(selection, isUp);
|
||||
const { parentElement } = node;
|
||||
this.#setCaretPosition(
|
||||
select,
|
||||
selection,
|
||||
parentElement,
|
||||
parentElement.getBoundingClientRect(),
|
||||
caretX
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// We've a candidate for the next line now we want to find the first element
|
||||
// which is under/over the caret.
|
||||
const [caretX, caretY] = this.#getCaretPosition(selection, isUp);
|
||||
const newLineElementRect = newLineElement.getBoundingClientRect();
|
||||
|
||||
// Maybe the element on the new line is a valid candidate.
|
||||
if (this.#isUnderOver(newLineElementRect, caretX, caretY, isUp)) {
|
||||
this.#setCaretPosition(
|
||||
select,
|
||||
selection,
|
||||
newLineElement,
|
||||
newLineElementRect,
|
||||
caretX
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
while (nodeIterator()) {
|
||||
// Search an element on the same line as newLineElement
|
||||
// which could be under/over the caret.
|
||||
const element = walker.currentNode.parentElement;
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
if (!this.#isOnSameLine(newLineElementRect, elementRect)) {
|
||||
break;
|
||||
}
|
||||
if (this.#isUnderOver(elementRect, caretX, caretY, isUp)) {
|
||||
// We found the element.
|
||||
this.#setCaretPosition(select, selection, element, elementRect, caretX);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No element has been found so just put the caret on the element on the new
|
||||
// line.
|
||||
this.#setCaretPosition(
|
||||
select,
|
||||
selection,
|
||||
newLineElement,
|
||||
newLineElementRect,
|
||||
caretX
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { CaretBrowsingMode };
|
||||
54
web/chrome-i18n-allow-access-to-file-urls.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"am": "\u1208\u134b\u12ed\u120d \u12e9\u12a0\u122d\u12a4\u120d\u12ce\u127d \u1218\u12f3\u1228\u123b \u134d\u1240\u12f5",
|
||||
"ar": "\u200f\u0627\u0644\u0633\u0645\u0627\u062d \u0628\u0627\u0644\u062f\u062e\u0648\u0644 \u0625\u0644\u0649 \u0639\u0646\u0627\u0648\u064a\u0646 URL \u0644\u0644\u0645\u0644\u0641\u0627\u062a",
|
||||
"bg": "\u0414\u0430 \u0441\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0438 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e URL \u0430\u0434\u0440\u0435\u0441\u0438\u0442\u0435 \u043d\u0430 \u0444\u0430\u0439\u043b\u043e\u0432\u0435\u0442\u0435",
|
||||
"bn": "\u09ab\u09be\u0987\u09b2 URL\u0997\u09c1\u09b2\u09bf\u09a4\u09c7 \u0985\u09cd\u09af\u09be\u0995\u09cd\u09b8\u09c7\u09b8 \u09ae\u099e\u09cd\u099c\u09c1\u09b0 \u0995\u09b0\u09c1\u09a8",
|
||||
"ca": "Permet l'acc\u00e9s als URL de fitxer",
|
||||
"cs": "Umo\u017enit p\u0159\u00edstup k adres\u00e1m URL soubor\u016f",
|
||||
"da": "Tillad adgang til webadresser p\u00e5 filer",
|
||||
"de": "Zugriff auf Datei-URLs zulassen",
|
||||
"el": "\u039d\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03b7 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03b5 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 URL \u03b1\u03c1\u03c7\u03b5\u03af\u03c9\u03bd",
|
||||
"en-GB": "Allow access to file URLs",
|
||||
"es": "Permitir acceso a URL de archivo",
|
||||
"es-419": "Permitir el acceso a las URL del archivo",
|
||||
"et": "Luba juurdep\u00e4\u00e4s failide URL-idele",
|
||||
"fa": "\u200f\u0627\u062c\u0627\u0632\u0647\u0654 \u062f\u0633\u062a\u0631\u0633\u06cc \u0628\u0647 URL \u0647\u0627\u06cc \u0641\u0627\u06cc\u0644",
|
||||
"fi": "Salli tiedostojen URL-osoitteiden k\u00e4ytt\u00f6",
|
||||
"fil": "Payagan ang access na mag-file ng mga URL",
|
||||
"fr": "Autoriser l'acc\u00e8s aux URL de fichier",
|
||||
"gu": "URL \u0aab\u0abe\u0a87\u0ab2 \u0a95\u0ab0\u0ab5\u0abe \u0a8d\u0a95\u0acd\u0ab8\u0ac7\u0ab8\u0aa8\u0ac0 \u0aae\u0a82\u0a9c\u0ac2\u0ab0\u0ac0 \u0a86\u0aaa\u0acb",
|
||||
"hi": "\u092b\u093c\u093e\u0907\u0932 URL \u0924\u0915 \u092a\u0939\u0941\u0902\u091a\u0928\u0947 \u0915\u0940 \u0905\u0928\u0941\u092e\u0924\u093f \u0926\u0947\u0902",
|
||||
"hr": "Dozvoli pristup URL-ovima datoteke",
|
||||
"hu": "F\u00e1jl URL-ekhez val\u00f3 hozz\u00e1f\u00e9r\u00e9s enged\u00e9lyez\u00e9se",
|
||||
"id": "Izinkan akses ke URL file",
|
||||
"it": "Consenti l'accesso agli URL dei file",
|
||||
"iw": "\u05d0\u05e4\u05e9\u05e8 \u05d2\u05d9\u05e9\u05d4 \u05dc\u05db\u05ea\u05d5\u05d1\u05d5\u05ea \u05d0\u05ea\u05e8\u05d9\u05dd \u05e9\u05dc \u05e7\u05d1\u05e6\u05d9\u05dd",
|
||||
"ja": "\u30d5\u30a1\u30a4\u30eb\u306e URL \u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u8a31\u53ef\u3059\u308b",
|
||||
"kn": "URL \u0c97\u0cb3\u0ca8\u0ccd\u0ca8\u0cc1 \u0cab\u0cc8\u0cb2\u0ccd\u200c\u0c97\u0cb3\u0cbf\u0c97\u0cc6 \u0caa\u0ccd\u0cb0\u0cb5\u0cc7\u0cb6\u0cbf\u0cb8\u0cb2\u0cc1 \u0c85\u0ca8\u0cc1\u0cae\u0ca4\u0cbf\u0cb8\u0cbf",
|
||||
"ko": "\ud30c\uc77c URL\uc5d0 \ub300\ud55c \uc561\uc138\uc2a4 \ud5c8\uc6a9",
|
||||
"lt": "Leisti pasiekti failo URL",
|
||||
"lv": "At\u013caut piek\u013cuvi faila vietr\u0101\u017eiem URL",
|
||||
"ml": "URL \u0d15\u0d33\u0d4d\u200d\u200c \u0d2b\u0d2f\u0d32\u0d4d\u200d\u200c \u0d1a\u0d46\u0d2f\u0d4d\u0d2f\u0d41\u0d28\u0d4d\u0d28\u0d24\u0d3f\u0d28\u0d4d \u0d06\u0d15\u0d4d\u200d\u0d38\u0d38\u0d4d\u0d38\u0d4d \u0d05\u0d28\u0d41\u0d35\u0d26\u0d3f\u0d15\u0d4d\u0d15\u0d41\u0d15",
|
||||
"mr": "\u092b\u093e\u0907\u0932 URL \u092e\u0927\u094d\u092f\u0947 \u092a\u094d\u0930\u0935\u0947\u0936\u093e\u0938 \u0905\u0928\u0941\u092e\u0924\u0940 \u0926\u094d\u092f\u093e",
|
||||
"ms": "Membenarkan akses ke URL fail",
|
||||
"nl": "Toegang tot bestand-URL's toestaan",
|
||||
"no": "Tillat tilgang til filnettadresser",
|
||||
"pl": "Zezwalaj na dost\u0119p do adres\u00f3w URL plik\u00f3w",
|
||||
"pt-BR": "Permitir acesso aos URLs do arquivo",
|
||||
"pt-PT": "Permitir acesso a URLs de ficheiro",
|
||||
"ro": "Permite accesul la adresele URL de fi\u0219iere",
|
||||
"ru": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0444\u0430\u0439\u043b\u044b \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0430\u043c",
|
||||
"sk": "Povoli\u0165 pr\u00edstup k webov\u00fdm adres\u00e1m s\u00faboru",
|
||||
"sl": "Dovoli dostop do URL-jev datoteke",
|
||||
"sr": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438 \u043f\u0440\u0438\u0441\u0442\u0443\u043f URL \u0430\u0434\u0440\u0435\u0441\u0430\u043c\u0430 \u0434\u0430\u0442\u043e\u0442\u0435\u043a\u0430",
|
||||
"sv": "Till\u00e5t \u00e5tkomst till webbadresser i filen",
|
||||
"sw": "Ruhusu kufikia URL za faili",
|
||||
"ta": "\u0b95\u0bcb\u0baa\u0bcd\u0baa\u0bc1 URL\u0b95\u0bb3\u0bc1\u0b95\u0bcd\u0b95\u0bc1 \u0b85\u0ba3\u0bc1\u0b95\u0bb2\u0bc8 \u0b85\u0ba9\u0bc1\u0bae\u0ba4\u0bbf",
|
||||
"te": "\u0c2b\u0c48\u0c32\u0c4d URL\u0c32\u0c15\u0c41 \u0c2a\u0c4d\u0c30\u0c3e\u0c2a\u0c4d\u0c24\u0c3f\u0c28\u0c3f \u0c05\u0c28\u0c41\u0c2e\u0c24\u0c3f\u0c02\u0c1a\u0c41",
|
||||
"th": "\u0e2d\u0e19\u0e38\u0e0d\u0e32\u0e15\u0e43\u0e2b\u0e49\u0e40\u0e02\u0e49\u0e32\u0e16\u0e36\u0e07\u0e44\u0e1f\u0e25\u0e4c URL",
|
||||
"tr": "Dosya URL'lerine eri\u015fime izin ver",
|
||||
"uk": "\u041d\u0430\u0434\u0430\u0432\u0430\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e URL-\u0430\u0434\u0440\u0435\u0441 \u0444\u0430\u0439\u043b\u0443",
|
||||
"vi": "Cho ph\u00e9p truy c\u1eadp v\u00e0o c\u00e1c URL c\u1ee7a t\u1ec7p",
|
||||
"zh-CN": "\u5141\u8bb8\u8bbf\u95ee\u6587\u4ef6\u7f51\u5740",
|
||||
"zh-TW": "\u5141\u8a31\u5b58\u53d6\u6a94\u6848\u7db2\u5740"
|
||||
}
|
||||
440
web/chromecom.js
Normal file
@@ -0,0 +1,440 @@
|
||||
/* Copyright 2013 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.
|
||||
*/
|
||||
/* globals chrome */
|
||||
|
||||
import { AppOptions } from "./app_options.js";
|
||||
import { BaseExternalServices } from "./external_services.js";
|
||||
import { BasePreferences } from "./preferences.js";
|
||||
import { GenericL10n } from "./genericl10n.js";
|
||||
import { GenericScripting } from "./generic_scripting.js";
|
||||
import { SignatureStorage } from "./generic_signature_storage.js";
|
||||
|
||||
// These strings are from chrome/app/resources/generated_resources_*.xtb.
|
||||
// eslint-disable-next-line sort-imports
|
||||
import i18nFileAccessLabels from "./chrome-i18n-allow-access-to-file-urls.json" with { type: "json" };
|
||||
|
||||
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("CHROME")) {
|
||||
throw new Error(
|
||||
'Module "pdfjs-web/chromecom" shall not be used outside CHROME build.'
|
||||
);
|
||||
}
|
||||
|
||||
(function rewriteUrlClosure() {
|
||||
// Run this code outside DOMContentLoaded to make sure that the URL
|
||||
// is rewritten as soon as possible.
|
||||
const queryString = document.location.search.slice(1);
|
||||
const m = /(^|&)file=([^&]*)/.exec(queryString);
|
||||
let defaultUrl = m ? decodeURIComponent(m[2]) : "";
|
||||
if (!defaultUrl && queryString.startsWith("DNR:")) {
|
||||
// Redirected via DNR, see registerPdfRedirectRule in pdfHandler.js.
|
||||
defaultUrl = queryString.slice(4);
|
||||
}
|
||||
|
||||
// Example: chrome-extension://.../http://example.com/file.pdf
|
||||
const humanReadableUrl = "/" + defaultUrl + location.hash;
|
||||
history.replaceState(history.state, "", humanReadableUrl);
|
||||
|
||||
AppOptions.set("defaultUrl", defaultUrl);
|
||||
})();
|
||||
|
||||
let viewerApp = { initialized: false };
|
||||
function initCom(app) {
|
||||
viewerApp = app;
|
||||
|
||||
// Ensure that PDFViewerApplication.initialBookmark reflects the current hash,
|
||||
// in case the URL rewrite above results in a different hash.
|
||||
viewerApp.initialBookmark = location.hash.slice(1);
|
||||
}
|
||||
|
||||
const ChromeCom = {
|
||||
/**
|
||||
* Creates an event that the extension is listening for and will
|
||||
* asynchronously respond by calling the callback.
|
||||
*
|
||||
* @param {string} action - The action to trigger.
|
||||
* @param {string} [data] - The data to send.
|
||||
* @param {Function} [callback] - Response callback that will be called with
|
||||
* one data argument. When the request cannot be handled, the callback is
|
||||
* immediately invoked with no arguments.
|
||||
*/
|
||||
request(action, data, callback) {
|
||||
const message = {
|
||||
action,
|
||||
data,
|
||||
};
|
||||
if (!chrome.runtime) {
|
||||
console.error("chrome.runtime is undefined.");
|
||||
callback?.();
|
||||
} else if (callback) {
|
||||
chrome.runtime.sendMessage(message, callback);
|
||||
} else {
|
||||
chrome.runtime.sendMessage(message);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolves a PDF file path and attempts to detects length.
|
||||
*
|
||||
* @param {string} file - Absolute URL of PDF file.
|
||||
* @param {Function} callback - A callback with resolved URL and file length.
|
||||
*/
|
||||
resolvePDFFile(file, callback) {
|
||||
// Expand drive:-URLs to filesystem URLs (Chrome OS)
|
||||
file = file.replace(
|
||||
/^drive:/i,
|
||||
"filesystem:" + location.origin + "/external/"
|
||||
);
|
||||
|
||||
if (/^https?:/.test(file)) {
|
||||
// Assumption: The file being opened is the file that was requested.
|
||||
// There is no UI to input a different URL, so this assumption will hold
|
||||
// for now.
|
||||
setReferer(file, function () {
|
||||
callback(file);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (/^file?:/.test(file)) {
|
||||
getEmbedderOrigin(function (origin) {
|
||||
// If the origin cannot be determined, let Chrome decide whether to
|
||||
// allow embedding files. Otherwise, only allow local files to be
|
||||
// embedded from local files or Chrome extensions.
|
||||
// Even without this check, the file load in frames is still blocked,
|
||||
// but this may change in the future (https://crbug.com/550151).
|
||||
if (origin && !/^file:|^chrome-extension:/.test(origin)) {
|
||||
viewerApp._documentError(null, {
|
||||
message:
|
||||
`Blocked ${origin} from loading ${file}. Refused to load ` +
|
||||
"a local file in a non-local page for security reasons.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
isAllowedFileSchemeAccess(function (isAllowedAccess) {
|
||||
if (isAllowedAccess) {
|
||||
callback(file);
|
||||
} else {
|
||||
requestAccessToLocalFile(file, viewerApp.overlayManager, callback);
|
||||
}
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
callback(file);
|
||||
},
|
||||
};
|
||||
|
||||
function getEmbedderOrigin(callback) {
|
||||
const origin = window === top ? location.origin : location.ancestorOrigins[0];
|
||||
if (origin === "null") {
|
||||
// file:-URLs, data-URLs, sandboxed frames, etc.
|
||||
getParentOrigin(callback);
|
||||
} else {
|
||||
callback(origin);
|
||||
}
|
||||
}
|
||||
|
||||
function getParentOrigin(callback) {
|
||||
ChromeCom.request("getParentOrigin", null, callback);
|
||||
}
|
||||
|
||||
function isAllowedFileSchemeAccess(callback) {
|
||||
ChromeCom.request("isAllowedFileSchemeAccess", null, callback);
|
||||
}
|
||||
|
||||
function isRuntimeAvailable() {
|
||||
try {
|
||||
// When the extension is reloaded, the extension runtime is destroyed and
|
||||
// the extension APIs become unavailable.
|
||||
if (chrome.runtime?.getManifest()) {
|
||||
return true;
|
||||
}
|
||||
} catch {}
|
||||
return false;
|
||||
}
|
||||
|
||||
function reloadIfRuntimeIsUnavailable() {
|
||||
if (!isRuntimeAvailable()) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
let chromeFileAccessOverlayPromise;
|
||||
function requestAccessToLocalFile(fileUrl, overlayManager, callback) {
|
||||
const dialog = document.getElementById("chromeFileAccessDialog");
|
||||
if (top !== window) {
|
||||
// When the extension reloads after receiving new permissions, the pages
|
||||
// have to be reloaded to restore the extension runtime. Auto-reload
|
||||
// frames, because users should not have to reload the whole page just to
|
||||
// update the viewer.
|
||||
// Top-level frames are closed by Chrome upon reload, so there is no need
|
||||
// for detecting unload of the top-level frame. Should this ever change
|
||||
// (crbug.com/511670), then the user can just reload the tab.
|
||||
window.addEventListener("focus", reloadIfRuntimeIsUnavailable);
|
||||
dialog.addEventListener("close", function () {
|
||||
window.removeEventListener("focus", reloadIfRuntimeIsUnavailable);
|
||||
reloadIfRuntimeIsUnavailable();
|
||||
});
|
||||
}
|
||||
chromeFileAccessOverlayPromise ||= overlayManager.register(
|
||||
dialog,
|
||||
/* canForceClose = */ true
|
||||
);
|
||||
|
||||
chromeFileAccessOverlayPromise.then(function () {
|
||||
const iconPath = chrome.runtime.getManifest().icons[48];
|
||||
document.getElementById("chrome-pdfjs-logo-bg").style.backgroundImage =
|
||||
"url(" + chrome.runtime.getURL(iconPath) + ")";
|
||||
|
||||
// Use Chrome's definition of UI language instead of PDF.js's #lang=...,
|
||||
// because the shown string should match the UI at chrome://extensions.
|
||||
const i18nFileAccessLabel =
|
||||
i18nFileAccessLabels[chrome.i18n.getUILanguage?.()];
|
||||
if (i18nFileAccessLabel) {
|
||||
document.getElementById("chrome-file-access-label").textContent =
|
||||
i18nFileAccessLabel;
|
||||
}
|
||||
|
||||
const link = document.getElementById("chrome-link-to-extensions-page");
|
||||
link.href = "chrome://extensions/?id=" + chrome.runtime.id;
|
||||
link.onclick = function (e) {
|
||||
// Direct navigation to chrome:// URLs is blocked by Chrome, so we
|
||||
// have to ask the background page to open chrome://extensions/?id=...
|
||||
e.preventDefault();
|
||||
// Open in the current tab by default, because toggling the file access
|
||||
// checkbox causes the extension to reload, and Chrome will close all
|
||||
// tabs upon reload.
|
||||
ChromeCom.request("openExtensionsPageForFileAccess", {
|
||||
newTab: e.ctrlKey || e.metaKey || e.button === 1 || window !== top,
|
||||
});
|
||||
};
|
||||
|
||||
// Show which file is being opened to help the user with understanding
|
||||
// why this permission request is shown.
|
||||
document.getElementById("chrome-url-of-local-file").textContent = fileUrl;
|
||||
|
||||
document.getElementById("chrome-file-fallback").onchange = function () {
|
||||
const file = this.files[0];
|
||||
if (file) {
|
||||
const originalFilename = decodeURIComponent(fileUrl.split("/").pop());
|
||||
let originalUrl = fileUrl;
|
||||
if (originalFilename !== file.name) {
|
||||
const msg =
|
||||
"The selected file does not match the original file." +
|
||||
"\nOriginal: " +
|
||||
originalFilename +
|
||||
"\nSelected: " +
|
||||
file.name +
|
||||
"\nDo you want to open the selected file?";
|
||||
// eslint-disable-next-line no-alert
|
||||
if (!confirm(msg)) {
|
||||
this.value = "";
|
||||
return;
|
||||
}
|
||||
// There is no way to retrieve the original URL from the File object.
|
||||
// So just generate a fake path.
|
||||
originalUrl = "file:///fakepath/to/" + encodeURIComponent(file.name);
|
||||
}
|
||||
callback(URL.createObjectURL(file), file.size, originalUrl);
|
||||
overlayManager.close(dialog);
|
||||
}
|
||||
};
|
||||
|
||||
overlayManager.open(dialog);
|
||||
});
|
||||
}
|
||||
|
||||
let dnrRequestId;
|
||||
// This port is used for several purposes:
|
||||
// 1. When disconnected, the background page knows that the frame has unload.
|
||||
// 2. When the referrer was saved in history.state.chromecomState, it is sent
|
||||
// to the background page.
|
||||
// 3. When the background page knows the referrer of the page, the referrer is
|
||||
// saved in history.state.chromecomState.
|
||||
let port;
|
||||
// Set the referer for the given URL.
|
||||
// 0. Background: If loaded via a http(s) URL: Save referer.
|
||||
// 1. Page -> background: send URL and referer from history.state
|
||||
// 2. Background: Bind referer to URL (via webRequest).
|
||||
// 3. Background -> page: Send latest referer and save to history.
|
||||
// 4. Page: Invoke callback.
|
||||
function setReferer(url, callback) {
|
||||
dnrRequestId ??= crypto.getRandomValues(new Uint32Array(1))[0] % 0x80000000;
|
||||
if (!port) {
|
||||
// The background page will accept the port, and keep adding the Referer
|
||||
// request header to requests to |url| until the port is disconnected.
|
||||
port = chrome.runtime.connect({ name: "chromecom-referrer" });
|
||||
}
|
||||
port.onDisconnect.addListener(onDisconnect);
|
||||
port.onMessage.addListener(onMessage);
|
||||
// Initiate the information exchange.
|
||||
port.postMessage({
|
||||
dnrRequestId,
|
||||
referer: window.history.state?.chromecomState,
|
||||
requestUrl: url,
|
||||
});
|
||||
|
||||
function onMessage(referer) {
|
||||
if (referer) {
|
||||
// The background extracts the Referer from the initial HTTP request for
|
||||
// the PDF file. When the viewer is reloaded or when the user navigates
|
||||
// back and forward, the background page will not observe a HTTP request
|
||||
// with Referer. To make sure that the Referer is preserved, store it in
|
||||
// history.state, which is preserved across reloads/navigations.
|
||||
const state = window.history.state || {};
|
||||
state.chromecomState = referer;
|
||||
window.history.replaceState(state, "");
|
||||
}
|
||||
onCompleted();
|
||||
}
|
||||
function onDisconnect() {
|
||||
// When the connection fails, ignore the error and call the callback.
|
||||
port = null;
|
||||
callback();
|
||||
}
|
||||
function onCompleted() {
|
||||
port.onDisconnect.removeListener(onDisconnect);
|
||||
port.onMessage.removeListener(onMessage);
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// chrome.storage.sync is not supported in every Chromium-derivate.
|
||||
// Note: The background page takes care of migrating values from
|
||||
// chrome.storage.local to chrome.storage.sync when needed.
|
||||
const storageArea = chrome.storage.sync || chrome.storage.local;
|
||||
|
||||
class Preferences extends BasePreferences {
|
||||
async _writeToStorage(prefObj) {
|
||||
return new Promise(resolve => {
|
||||
if (prefObj === this.defaults) {
|
||||
const keysToRemove = Object.keys(this.defaults);
|
||||
// If the storage is reset, remove the keys so that the values from
|
||||
// managed storage are applied again.
|
||||
storageArea.remove(keysToRemove, function () {
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
storageArea.set(prefObj, function () {
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async _readFromStorage(prefObj) {
|
||||
return new Promise(resolve => {
|
||||
const getPreferences = defaultPrefs => {
|
||||
if (chrome.runtime.lastError) {
|
||||
// Managed storage not supported, e.g. in Opera.
|
||||
defaultPrefs = this.defaults;
|
||||
}
|
||||
storageArea.get(defaultPrefs, function (readPrefs) {
|
||||
resolve({ prefs: readPrefs });
|
||||
});
|
||||
};
|
||||
|
||||
if (chrome.storage.managed) {
|
||||
// Get preferences as set by the system administrator.
|
||||
// See extensions/chromium/preferences_schema.json for more information.
|
||||
// These preferences can be overridden by the user.
|
||||
|
||||
// Deprecated preferences are removed from web/default_preferences.json,
|
||||
// but kept in extensions/chromium/preferences_schema.json for backwards
|
||||
// compatibility with managed preferences.
|
||||
const defaultManagedPrefs = Object.assign(
|
||||
{
|
||||
enableHandToolOnLoad: false,
|
||||
disableTextLayer: false,
|
||||
enhanceTextSelection: false,
|
||||
showPreviousViewOnLoad: true,
|
||||
disablePageMode: false,
|
||||
},
|
||||
this.defaults
|
||||
);
|
||||
|
||||
chrome.storage.managed.get(defaultManagedPrefs, function (items) {
|
||||
items ||= defaultManagedPrefs;
|
||||
// Migration logic for deprecated preferences: If the new preference
|
||||
// is not defined by an administrator (i.e. the value is the same as
|
||||
// the default value), and a deprecated preference is set with a
|
||||
// non-default value, migrate the deprecated preference value to the
|
||||
// new preference value.
|
||||
// Never remove this, because we have no means of modifying managed
|
||||
// preferences.
|
||||
|
||||
// Migration code for https://github.com/mozilla/pdf.js/pull/7635.
|
||||
if (items.enableHandToolOnLoad && !items.cursorToolOnLoad) {
|
||||
items.cursorToolOnLoad = 1;
|
||||
}
|
||||
delete items.enableHandToolOnLoad;
|
||||
|
||||
// Migration code for https://github.com/mozilla/pdf.js/pull/9479.
|
||||
if (items.textLayerMode !== 1 && items.disableTextLayer) {
|
||||
items.textLayerMode = 0;
|
||||
}
|
||||
delete items.disableTextLayer;
|
||||
delete items.enhanceTextSelection;
|
||||
|
||||
// Migration code for https://github.com/mozilla/pdf.js/pull/10502.
|
||||
if (!items.showPreviousViewOnLoad && !items.viewOnLoad) {
|
||||
items.viewOnLoad = 1;
|
||||
}
|
||||
delete items.showPreviousViewOnLoad;
|
||||
delete items.disablePageMode;
|
||||
|
||||
getPreferences(items);
|
||||
});
|
||||
} else {
|
||||
// Managed storage not supported, e.g. in old Chromium versions.
|
||||
getPreferences(this.defaults);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ExternalServices extends BaseExternalServices {
|
||||
initPassiveLoading() {
|
||||
ChromeCom.resolvePDFFile(
|
||||
AppOptions.get("defaultUrl"),
|
||||
function (url, length, originalUrl) {
|
||||
viewerApp.open({ url, length, originalUrl });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async createL10n() {
|
||||
return new GenericL10n(navigator.language);
|
||||
}
|
||||
|
||||
createScripting() {
|
||||
return new GenericScripting(AppOptions.get("sandboxBundleSrc"));
|
||||
}
|
||||
|
||||
createSignatureStorage(eventBus, signal) {
|
||||
return new SignatureStorage(eventBus, signal);
|
||||
}
|
||||
}
|
||||
|
||||
class MLManager {
|
||||
isEnabledFor(_name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
async guess() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export { ExternalServices, initCom, MLManager, Preferences };
|
||||
633
web/comment_manager.css
Normal file
@@ -0,0 +1,633 @@
|
||||
/* Copyright 2025 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.
|
||||
*/
|
||||
|
||||
.commentPopup,
|
||||
#commentManagerDialog {
|
||||
width: 360px;
|
||||
max-width: 100%;
|
||||
min-width: 200px;
|
||||
position: absolute;
|
||||
padding: 8px 16px 16px;
|
||||
margin-left: 0;
|
||||
margin-top: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#commentManagerDialog {
|
||||
--comment-close-button-icon: url(images/comment-closeButton.svg);
|
||||
|
||||
.mainContainer {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
|
||||
#commentManagerToolbar {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
#commentManagerTextInput {
|
||||
width: 100%;
|
||||
min-height: 132px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.annotationLayer.disabled :is(.annotationCommentButton) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:is(.annotationLayer, .annotationEditorLayer) {
|
||||
.annotationCommentButton {
|
||||
color-scheme: light dark;
|
||||
--comment-button-bg: light-dark(white, #1c1b22);
|
||||
--comment-button-fg: light-dark(#5b5b66, #fbfbfe);
|
||||
--comment-button-active-bg: light-dark(#0041a4, #a6ecf4);
|
||||
--comment-button-active-fg: light-dark(white, #15141a);
|
||||
--comment-button-hover-bg: light-dark(#0053cb, #61dce9);
|
||||
--comment-button-hover-fg: light-dark(white, #15141a);
|
||||
--comment-button-selected-bg: light-dark(#0062fa, #00cadb);
|
||||
--comment-button-border-color: light-dark(#8f8f9d, #bfbfc9);
|
||||
--comment-button-active-border-color: var(--comment-button-active-bg);
|
||||
--comment-button-focus-border-color: light-dark(#cfcfd8, #3a3944);
|
||||
--comment-button-hover-border-color: var(--comment-button-hover-bg);
|
||||
--comment-button-selected-border-color: var(--comment-button-selected-bg);
|
||||
--comment-button-selected-fg: light-dark(white, #15141a);
|
||||
--comment-button-dim: 24px;
|
||||
--comment-button-box-shadow:
|
||||
0 0.25px 0.75px 0 light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.2)),
|
||||
0 2px 6px 0 light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.4));
|
||||
--comment-button-focus-outline-color: light-dark(#0062fa, #00cadb);
|
||||
|
||||
@media screen and (forced-colors: active) {
|
||||
--comment-button-bg: ButtonFace;
|
||||
--comment-button-fg: ButtonText;
|
||||
--comment-button-hover-bg: SelectedItemText;
|
||||
--comment-button-hover-fg: SelectedItem;
|
||||
--comment-button-active-bg: SelectedItemText;
|
||||
--comment-button-active-fg: SelectedItem;
|
||||
--comment-button-border-color: ButtonBorder;
|
||||
--comment-button-active-border-color: ButtonBorder;
|
||||
--comment-button-hover-border-color: SelectedItem;
|
||||
--comment-button-box-shadow: none;
|
||||
--comment-button-focus-outline-color: CanvasText;
|
||||
--comment-button-selected-bg: ButtonBorder;
|
||||
--comment-button-selected-fg: ButtonFace;
|
||||
}
|
||||
|
||||
position: absolute;
|
||||
width: var(--comment-button-dim);
|
||||
height: var(--comment-button-dim);
|
||||
background-color: var(--comment-button-bg);
|
||||
border-radius: 6px 6px 6px 0;
|
||||
border: 1px solid var(--comment-button-border-color);
|
||||
box-shadow: var(--comment-button-box-shadow);
|
||||
cursor: auto;
|
||||
z-index: 1;
|
||||
padding: 4px;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
pointer-events: auto;
|
||||
|
||||
&:dir(rtl) {
|
||||
border-radius: 6px 6px 0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: cover;
|
||||
mask-image: var(--comment-edit-button-icon);
|
||||
background-color: var(--comment-button-fg);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
transform: scaleX(var(--dir-factor));
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--comment-button-focus-outline-color);
|
||||
outline-offset: 1px;
|
||||
border-color: var(--comment-button-focus-border-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--comment-button-hover-bg) !important;
|
||||
border-color: var(--comment-button-hover-border-color);
|
||||
|
||||
&::before {
|
||||
background-color: var(--comment-button-hover-fg);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--comment-button-active-bg) !important;
|
||||
border-color: var(--comment-button-active-border-color);
|
||||
|
||||
&::before {
|
||||
background-color: var(--comment-button-active-fg);
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--comment-button-selected-bg) !important;
|
||||
border-color: var(--comment-button-selected-border-color);
|
||||
|
||||
&::before {
|
||||
background-color: var(--comment-button-selected-fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#editorCommentsSidebar,
|
||||
.commentPopup {
|
||||
--comment-close-button-icon: url(images/comment-closeButton.svg);
|
||||
--comment-popup-edit-button-icon: url(images/comment-popup-editButton.svg);
|
||||
--comment-popup-delete-button-icon: url(images/editor-toolbar-delete.svg);
|
||||
|
||||
--comment-date-fg-color: light-dark(
|
||||
rgb(21 20 26 / 0.69),
|
||||
rgb(251 251 254 / 0.69)
|
||||
);
|
||||
--comment-bg-color: light-dark(#f9f9fb, #1c1b22);
|
||||
--comment-hover-bg-color: light-dark(#e0e0e6, #2c2b33);
|
||||
--comment-active-bg-color: light-dark(#d1d1d9, #3a3944);
|
||||
--comment-hover-brightness: 0.89;
|
||||
--comment-hover-filter: brightness(var(--comment-hover-brightness));
|
||||
--comment-active-brightness: 0.825;
|
||||
--comment-active-filter: brightness(var(--comment-active-brightness));
|
||||
--comment-border-color: light-dark(#f0f0f4, #52525e);
|
||||
--comment-focus-outline-color: light-dark(#0062fa, #00cadb);
|
||||
--comment-fg-color: light-dark(#15141a, #fbfbfe);
|
||||
--comment-count-bg-color: light-dark(#e2f7ff, #00317e);
|
||||
--comment-indicator-active-fg-color: light-dark(#0041a4, #a6ecf4);
|
||||
--comment-indicator-active-filter: brightness(
|
||||
calc(1 / var(--comment-active-brightness))
|
||||
);
|
||||
--comment-indicator-focus-fg-color: light-dark(#5b5b66, #fbfbfe);
|
||||
--comment-indicator-hover-fg-color: light-dark(#0053cb, #61dce9);
|
||||
--comment-indicator-hover-filter: brightness(
|
||||
calc(1 / var(--comment-hover-brightness))
|
||||
);
|
||||
--comment-indicator-selected-fg-color: light-dark(#0062fa, #00cadb);
|
||||
|
||||
--button-comment-bg: transparent;
|
||||
--button-comment-color: var(--main-color);
|
||||
--button-comment-active-bg: light-dark(#cfcfd8, #5b5b66);
|
||||
--button-comment-active-border: none;
|
||||
--button-comment-active-color: var(--button-comment-color);
|
||||
--button-comment-border: none;
|
||||
--button-comment-hover-bg: light-dark(#e0e0e6, #52525e);
|
||||
--button-comment-hover-color: var(--button-comment-color);
|
||||
|
||||
@media screen and (forced-colors: active) {
|
||||
--comment-date-fg-color: CanvasText;
|
||||
--comment-bg-color: Canvas;
|
||||
--comment-hover-bg-color: Canvas;
|
||||
--comment-hover-filter: none;
|
||||
--comment-active-bg-color: Canvas;
|
||||
--comment-active-filter: none;
|
||||
--comment-border-color: CanvasText;
|
||||
--comment-fg-color: CanvasText;
|
||||
--comment-count-bg-color: Canvas;
|
||||
--comment-indicator-active-fg-color: SelectedItem;
|
||||
--comment-indicator-focus-fg-color: CanvasText;
|
||||
--comment-indicator-hover-fg-color: CanvasText;
|
||||
--comment-indicator-selected-fg-color: SelectedItem;
|
||||
--button-comment-bg: ButtonFace;
|
||||
--button-comment-color: ButtonText;
|
||||
--button-comment-active-bg: ButtonText;
|
||||
--button-comment-active-color: HighlightText;
|
||||
--button-comment-border: 1px solid ButtonText;
|
||||
--button-comment-hover-bg: Highlight;
|
||||
--button-comment-hover-color: HighlightText;
|
||||
}
|
||||
}
|
||||
|
||||
#editorCommentsSidebar {
|
||||
display: flex;
|
||||
height: auto;
|
||||
padding-bottom: 16px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
#editorCommentsSidebarHeader {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.commentCount {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
user-select: none;
|
||||
|
||||
#editorCommentsSidebarTitle {
|
||||
font: menu;
|
||||
font-style: normal;
|
||||
font-weight: 590;
|
||||
line-height: normal;
|
||||
font-size: 17px;
|
||||
color: var(--comment-fg-color);
|
||||
}
|
||||
|
||||
#editorCommentsSidebarCount {
|
||||
padding: 0 4px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--comment-count-bg-color);
|
||||
|
||||
color: var(--comment-fg-color);
|
||||
text-align: center;
|
||||
|
||||
font: menu;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
#editorCommentsSidebarCloseButton {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-image: var(--comment-close-button-icon);
|
||||
background-color: var(--comment-fg-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--comment-hover-bg-color);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--comment-active-bg-color);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: var(--focus-ring-outline);
|
||||
}
|
||||
|
||||
> span {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#editorCommentsSidebarListContainer {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
|
||||
#editorCommentsSidebarList {
|
||||
display: flex;
|
||||
width: auto;
|
||||
padding: 4px 16px;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
list-style-type: none;
|
||||
|
||||
.sidebarComment {
|
||||
display: flex;
|
||||
width: auto;
|
||||
padding: 8px 16px 16px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
gap: 4px;
|
||||
|
||||
border-radius: 8px;
|
||||
border: 0.5px solid var(--comment-border-color);
|
||||
background-color: var(--comment-bg-color);
|
||||
|
||||
&:not(.noComments) {
|
||||
&:hover {
|
||||
@media screen and (forced-colors: active) {
|
||||
background-color: var(--comment-hover-bg-color);
|
||||
}
|
||||
filter: var(--comment-hover-filter);
|
||||
|
||||
time::after {
|
||||
display: inline-block;
|
||||
background-color: var(--comment-indicator-hover-fg-color);
|
||||
filter: var(--comment-indicator-hover-filter);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
@media screen and (forced-colors: active) {
|
||||
background-color: var(--comment-active-bg-color);
|
||||
}
|
||||
filter: var(--comment-active-filter);
|
||||
|
||||
time::after {
|
||||
display: inline-block;
|
||||
background-color: var(--comment-indicator-active-fg-color);
|
||||
filter: var(--comment-indicator-active-filter);
|
||||
}
|
||||
}
|
||||
|
||||
&:is(:focus, :focus-visible) time::after {
|
||||
display: inline-block;
|
||||
background-color: var(--comment-indicator-focus-fg-color);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--comment-focus-outline-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
.sidebarCommentText {
|
||||
max-height: fit-content;
|
||||
-webkit-line-clamp: unset;
|
||||
}
|
||||
|
||||
time::after {
|
||||
display: inline-block;
|
||||
background-color: var(--comment-indicator-selected-fg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarCommentText {
|
||||
font: menu;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
font-size: 15px;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
max-height: 80px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
.richText {
|
||||
--total-scale-factor: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
&.noComments {
|
||||
.sidebarCommentText {
|
||||
max-height: fit-content;
|
||||
-webkit-line-clamp: unset;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
a {
|
||||
font: menu;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
font-size: 15px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
overflow-wrap: break-word;
|
||||
margin-block-start: 15px;
|
||||
|
||||
&:focus-visible {
|
||||
outline: var(--focus-ring-outline);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
time {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
font: menu;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
font-size: 13px;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-image: var(--comment-edit-button-icon);
|
||||
transform: scaleX(var(--dir-factor));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.commentPopup {
|
||||
color-scheme: light dark;
|
||||
|
||||
--divider-color: light-dark(#cfcfd8, #3a3944);
|
||||
--comment-shadow:
|
||||
0 0.5px 2px 0 light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.2)),
|
||||
0 4px 16px 0 light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.4));
|
||||
|
||||
@media screen and (forced-colors: active) {
|
||||
--divider-color: CanvasText;
|
||||
--comment-shadow: none;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
z-index: 100001; /* above selected annotation editor */
|
||||
pointer-events: auto;
|
||||
margin-top: 2px;
|
||||
|
||||
border: 0.5px solid var(--comment-border-color);
|
||||
background: var(--comment-bg-color);
|
||||
box-shadow: var(--comment-shadow);
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
cursor: move !important;
|
||||
|
||||
* {
|
||||
cursor: move !important;
|
||||
}
|
||||
|
||||
button {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.selected) .commentPopupButtons {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
border: none;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.commentPopupTop {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding-bottom: 4px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
|
||||
.commentPopupTime {
|
||||
font: menu;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
font-size: 13px;
|
||||
color: var(--comment-date-fg-color);
|
||||
}
|
||||
|
||||
.commentPopupButtons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
cursor: default;
|
||||
|
||||
> button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 8px;
|
||||
border: var(--button-comment-border);
|
||||
border-radius: 4px;
|
||||
background-color: var(--button-comment-bg);
|
||||
color: var(--button-comment-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--button-comment-hover-bg);
|
||||
|
||||
&::before {
|
||||
background-color: var(--button-comment-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
border: var(--button-comment-active-border);
|
||||
background-color: var(--button-comment-active-bg);
|
||||
color: var(--button-comment-active-color);
|
||||
|
||||
&::before {
|
||||
background-color: var(--button-comment-active-color);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
background-color: var(--button-comment-hover-bg);
|
||||
outline: 2px solid var(--comment-focus-outline-color);
|
||||
outline-offset: 0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
}
|
||||
|
||||
&.commentPopupEdit::before {
|
||||
mask-image: var(--comment-popup-edit-button-icon);
|
||||
}
|
||||
|
||||
&.commentPopupDelete::before {
|
||||
mask-image: var(--comment-popup-delete-button-icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.commentPopupText {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
font: menu;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
font-size: 15px;
|
||||
color: var(--comment-fg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.commentPopupText,
|
||||
.sidebarCommentText .richText {
|
||||
margin-block: 0;
|
||||
|
||||
p:first-of-type {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
> * {
|
||||
white-space: pre-wrap;
|
||||
font-size: max(15px, calc(10px * var(--total-scale-factor)));
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--comment-fg-color) !important;
|
||||
}
|
||||
}
|
||||
1263
web/comment_manager.js
Normal file
BIN
web/compressed.tracemonkey-pldi-09.pdf
Normal file
145
web/debugger.css
Normal file
@@ -0,0 +1,145 @@
|
||||
/* Copyright 2014 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.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--panel-width: 300px;
|
||||
}
|
||||
|
||||
#PDFBug,
|
||||
#PDFBug :is(input, button, select) {
|
||||
font: message-box;
|
||||
}
|
||||
#PDFBug {
|
||||
color-scheme: only light;
|
||||
|
||||
background-color: white;
|
||||
color: black;
|
||||
border: 1px solid rgb(102 102 102);
|
||||
position: fixed;
|
||||
top: 32px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
font-size: 10px;
|
||||
padding: 0;
|
||||
width: var(--panel-width);
|
||||
}
|
||||
#PDFBug .controls {
|
||||
background: rgb(238 238 238);
|
||||
border-bottom: 1px solid rgb(102 102 102);
|
||||
padding: 3px;
|
||||
}
|
||||
#PDFBug .panels {
|
||||
inset: 27px 0 0;
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
}
|
||||
#PDFBug .panels > div {
|
||||
padding: 5px;
|
||||
}
|
||||
#PDFBug button.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
.debuggerShowText,
|
||||
.debuggerHideText:hover {
|
||||
background-color: rgb(255 255 0 / 0.25);
|
||||
}
|
||||
#PDFBug .stats {
|
||||
font-family: courier;
|
||||
font-size: 10px;
|
||||
white-space: pre;
|
||||
}
|
||||
#PDFBug .stats .title {
|
||||
font-weight: bold;
|
||||
}
|
||||
#PDFBug table {
|
||||
font-size: 10px;
|
||||
white-space: pre;
|
||||
}
|
||||
#PDFBug table.showText {
|
||||
border-collapse: collapse;
|
||||
text-align: center;
|
||||
}
|
||||
#PDFBug table.showText,
|
||||
#PDFBug table.showText :is(tr, td) {
|
||||
border: 1px solid black;
|
||||
padding: 1px;
|
||||
}
|
||||
#PDFBug table.showText td.advance {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
#viewer.textLayer-visible .textLayer {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#viewer.textLayer-visible .canvasWrapper {
|
||||
background-color: rgb(128 255 128);
|
||||
}
|
||||
|
||||
#viewer.textLayer-visible .canvasWrapper canvas {
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
#viewer.textLayer-visible .textLayer span {
|
||||
background-color: rgb(255 255 0 / 0.1);
|
||||
color: rgb(0 0 0);
|
||||
border: solid 1px rgb(255 0 0 / 0.5);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#viewer.textLayer-visible .textLayer span[aria-owns] {
|
||||
background-color: rgb(255 0 0 / 0.3);
|
||||
}
|
||||
|
||||
#viewer.textLayer-hover .textLayer span:hover {
|
||||
background-color: rgb(255 255 255);
|
||||
color: rgb(0 0 0);
|
||||
}
|
||||
|
||||
#viewer.textLayer-shadow .textLayer span {
|
||||
background-color: rgb(255 255 255 / 0.6);
|
||||
color: rgb(0 0 0);
|
||||
}
|
||||
|
||||
.pdfBugGroupsLayer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
|
||||
> * {
|
||||
position: absolute;
|
||||
outline-color: red;
|
||||
outline-width: 2px;
|
||||
|
||||
--hover-outline-style: solid !important;
|
||||
--hover-background-color: rgb(255 0 0 / 0.2);
|
||||
|
||||
&:hover {
|
||||
outline-style: var(--hover-outline-style);
|
||||
background-color: var(--hover-background-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.showDebugBoxes & {
|
||||
outline-style: dashed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.showDebugBoxes {
|
||||
.pdfBugGroupsLayer {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
848
web/debugger.mjs
Normal file
@@ -0,0 +1,848 @@
|
||||
/* Copyright 2012 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.
|
||||
*/
|
||||
|
||||
const { OPS } = globalThis.pdfjsLib || (await import("pdfjs-lib"));
|
||||
|
||||
const opMap = Object.create(null);
|
||||
for (const key in OPS) {
|
||||
opMap[OPS[key]] = key;
|
||||
}
|
||||
|
||||
const FontInspector = (function FontInspectorClosure() {
|
||||
let fonts;
|
||||
let active = false;
|
||||
const fontAttribute = "data-font-name";
|
||||
function removeSelection() {
|
||||
const divs = document.querySelectorAll(`span[${fontAttribute}]`);
|
||||
for (const div of divs) {
|
||||
div.className = "";
|
||||
}
|
||||
}
|
||||
function resetSelection() {
|
||||
const divs = document.querySelectorAll(`span[${fontAttribute}]`);
|
||||
for (const div of divs) {
|
||||
div.className = "debuggerHideText";
|
||||
}
|
||||
}
|
||||
function selectFont(fontName, show) {
|
||||
const divs = document.querySelectorAll(
|
||||
`span[${fontAttribute}=${fontName}]`
|
||||
);
|
||||
for (const div of divs) {
|
||||
div.className = show ? "debuggerShowText" : "debuggerHideText";
|
||||
}
|
||||
}
|
||||
function textLayerClick(e) {
|
||||
if (
|
||||
!e.target.dataset.fontName ||
|
||||
e.target.tagName.toUpperCase() !== "SPAN"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const fontName = e.target.dataset.fontName;
|
||||
const selects = document.getElementsByTagName("input");
|
||||
for (const select of selects) {
|
||||
if (select.dataset.fontName !== fontName) {
|
||||
continue;
|
||||
}
|
||||
select.checked = !select.checked;
|
||||
selectFont(fontName, select.checked);
|
||||
select.scrollIntoView();
|
||||
}
|
||||
}
|
||||
return {
|
||||
// Properties/functions needed by PDFBug.
|
||||
id: "FontInspector",
|
||||
name: "Font Inspector",
|
||||
panel: null,
|
||||
manager: null,
|
||||
init() {
|
||||
const panel = this.panel;
|
||||
const tmp = document.createElement("button");
|
||||
tmp.addEventListener("click", resetSelection);
|
||||
tmp.textContent = "Refresh";
|
||||
panel.append(tmp);
|
||||
|
||||
fonts = document.createElement("div");
|
||||
panel.append(fonts);
|
||||
},
|
||||
cleanup() {
|
||||
fonts.textContent = "";
|
||||
},
|
||||
enabled: false,
|
||||
get active() {
|
||||
return active;
|
||||
},
|
||||
set active(value) {
|
||||
active = value;
|
||||
if (active) {
|
||||
document.body.addEventListener("click", textLayerClick, true);
|
||||
resetSelection();
|
||||
} else {
|
||||
document.body.removeEventListener("click", textLayerClick, true);
|
||||
removeSelection();
|
||||
}
|
||||
},
|
||||
// FontInspector specific functions.
|
||||
fontAdded(fontObj, url) {
|
||||
function properties(obj, list) {
|
||||
const moreInfo = document.createElement("table");
|
||||
for (const entry of list) {
|
||||
const tr = document.createElement("tr");
|
||||
const td1 = document.createElement("td");
|
||||
td1.textContent = entry;
|
||||
tr.append(td1);
|
||||
const td2 = document.createElement("td");
|
||||
td2.textContent = obj[entry].toString();
|
||||
tr.append(td2);
|
||||
moreInfo.append(tr);
|
||||
}
|
||||
return moreInfo;
|
||||
}
|
||||
|
||||
const moreInfo = fontObj.css
|
||||
? properties(fontObj, ["baseFontName"])
|
||||
: properties(fontObj, ["name", "type"]);
|
||||
|
||||
const fontName = fontObj.loadedName;
|
||||
const font = document.createElement("div");
|
||||
const name = document.createElement("span");
|
||||
name.textContent = fontName;
|
||||
let download;
|
||||
if (!fontObj.css) {
|
||||
download = document.createElement("a");
|
||||
if (url) {
|
||||
url = /url\(['"]?([^)"']+)/.exec(url);
|
||||
download.href = url[1];
|
||||
} else if (fontObj.data) {
|
||||
download.href = URL.createObjectURL(
|
||||
new Blob([fontObj.data], { type: fontObj.mimetype })
|
||||
);
|
||||
}
|
||||
download.textContent = "Download";
|
||||
}
|
||||
|
||||
const logIt = document.createElement("a");
|
||||
logIt.href = "";
|
||||
logIt.textContent = "Log";
|
||||
logIt.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
console.log(fontObj);
|
||||
});
|
||||
const select = document.createElement("input");
|
||||
select.setAttribute("type", "checkbox");
|
||||
select.dataset.fontName = fontName;
|
||||
select.addEventListener("click", function () {
|
||||
selectFont(fontName, select.checked);
|
||||
});
|
||||
if (download) {
|
||||
font.append(select, name, " ", download, " ", logIt, moreInfo);
|
||||
} else {
|
||||
font.append(select, name, " ", logIt, moreInfo);
|
||||
}
|
||||
fonts.append(font);
|
||||
// Somewhat of a hack, should probably add a hook for when the text layer
|
||||
// is done rendering.
|
||||
setTimeout(() => {
|
||||
if (this.active) {
|
||||
resetSelection();
|
||||
}
|
||||
}, 2000);
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
// Manages all the page steppers.
|
||||
const StepperManager = (function StepperManagerClosure() {
|
||||
let steppers = [];
|
||||
let stepperDiv = null;
|
||||
let stepperControls = null;
|
||||
let stepperChooser = null;
|
||||
let breakPoints = Object.create(null);
|
||||
return {
|
||||
// Properties/functions needed by PDFBug.
|
||||
id: "Stepper",
|
||||
name: "Stepper",
|
||||
panel: null,
|
||||
manager: null,
|
||||
init() {
|
||||
const self = this;
|
||||
stepperControls = document.createElement("div");
|
||||
stepperChooser = document.createElement("select");
|
||||
stepperChooser.addEventListener("change", function (event) {
|
||||
self.selectStepper(this.value);
|
||||
});
|
||||
stepperControls.append(stepperChooser);
|
||||
stepperDiv = document.createElement("div");
|
||||
this.panel.append(stepperControls, stepperDiv);
|
||||
if (sessionStorage.getItem("pdfjsBreakPoints")) {
|
||||
breakPoints = JSON.parse(sessionStorage.getItem("pdfjsBreakPoints"));
|
||||
}
|
||||
},
|
||||
cleanup() {
|
||||
stepperChooser.textContent = "";
|
||||
stepperDiv.textContent = "";
|
||||
steppers = [];
|
||||
},
|
||||
enabled: false,
|
||||
active: false,
|
||||
// Stepper specific functions.
|
||||
create(pageIndex) {
|
||||
const pageContainer = document.querySelector(
|
||||
`#viewer div[data-page-number="${pageIndex + 1}"]`
|
||||
);
|
||||
|
||||
const debug = document.createElement("div");
|
||||
debug.id = "stepper" + pageIndex;
|
||||
debug.hidden = true;
|
||||
debug.className = "stepper";
|
||||
stepperDiv.append(debug);
|
||||
const b = document.createElement("option");
|
||||
b.textContent = "Page " + (pageIndex + 1);
|
||||
b.value = pageIndex;
|
||||
stepperChooser.append(b);
|
||||
const initBreakPoints = breakPoints[pageIndex] || [];
|
||||
const stepper = new Stepper(
|
||||
debug,
|
||||
pageIndex,
|
||||
initBreakPoints,
|
||||
pageContainer
|
||||
);
|
||||
steppers.push(stepper);
|
||||
if (steppers.length === 1) {
|
||||
this.selectStepper(pageIndex, false);
|
||||
}
|
||||
return stepper;
|
||||
},
|
||||
selectStepper(pageIndex, selectPanel) {
|
||||
pageIndex |= 0;
|
||||
if (selectPanel) {
|
||||
this.manager.selectPanel(this);
|
||||
}
|
||||
for (const stepper of steppers) {
|
||||
stepper.panel.hidden = stepper.pageIndex !== pageIndex;
|
||||
}
|
||||
for (const option of stepperChooser.options) {
|
||||
option.selected = (option.value | 0) === pageIndex;
|
||||
}
|
||||
},
|
||||
saveBreakPoints(pageIndex, bps) {
|
||||
breakPoints[pageIndex] = bps;
|
||||
sessionStorage.setItem("pdfjsBreakPoints", JSON.stringify(breakPoints));
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
// The stepper for each page's operatorList.
|
||||
class Stepper {
|
||||
// Shorter way to create element and optionally set textContent.
|
||||
#c(tag, textContent) {
|
||||
const d = document.createElement(tag);
|
||||
if (textContent) {
|
||||
d.textContent = textContent;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
#simplifyArgs(args) {
|
||||
if (typeof args === "string") {
|
||||
const MAX_STRING_LENGTH = 75;
|
||||
return args.length <= MAX_STRING_LENGTH
|
||||
? args
|
||||
: args.substring(0, MAX_STRING_LENGTH) + "...";
|
||||
}
|
||||
if (typeof args !== "object" || args === null) {
|
||||
return args;
|
||||
}
|
||||
if ("length" in args) {
|
||||
// array
|
||||
const MAX_ITEMS = 10,
|
||||
simpleArgs = [];
|
||||
let i, ii;
|
||||
for (i = 0, ii = Math.min(MAX_ITEMS, args.length); i < ii; i++) {
|
||||
simpleArgs.push(this.#simplifyArgs(args[i]));
|
||||
}
|
||||
if (i < args.length) {
|
||||
simpleArgs.push("...");
|
||||
}
|
||||
return simpleArgs;
|
||||
}
|
||||
const simpleObj = {};
|
||||
for (const key in args) {
|
||||
simpleObj[key] = this.#simplifyArgs(args[key]);
|
||||
}
|
||||
return simpleObj;
|
||||
}
|
||||
|
||||
constructor(panel, pageIndex, initialBreakPoints, pageContainer) {
|
||||
this.panel = panel;
|
||||
this.breakPoint = 0;
|
||||
this.nextBreakPoint = null;
|
||||
this.pageIndex = pageIndex;
|
||||
this.breakPoints = initialBreakPoints;
|
||||
this.currentIdx = -1;
|
||||
this.operatorListIdx = 0;
|
||||
this.indentLevel = 0;
|
||||
this.operatorsGroups = null;
|
||||
this.pageContainer = pageContainer;
|
||||
}
|
||||
|
||||
init(operatorList) {
|
||||
const panel = this.panel;
|
||||
const content = this.#c("div", "c=continue, s=step");
|
||||
|
||||
const showBoxesToggle = this.#c("label", "Show bounding boxes");
|
||||
const showBoxesCheckbox = this.#c("input");
|
||||
showBoxesCheckbox.type = "checkbox";
|
||||
showBoxesToggle.prepend(showBoxesCheckbox);
|
||||
content.append(this.#c("br"), showBoxesToggle);
|
||||
|
||||
const table = this.#c("table");
|
||||
content.append(table);
|
||||
table.cellSpacing = 0;
|
||||
const headerRow = this.#c("tr");
|
||||
table.append(headerRow);
|
||||
headerRow.append(
|
||||
this.#c("th", "Break"),
|
||||
this.#c("th", "Idx"),
|
||||
this.#c("th", "fn"),
|
||||
this.#c("th", "args")
|
||||
);
|
||||
panel.append(content);
|
||||
this.table = table;
|
||||
this.updateOperatorList(operatorList);
|
||||
|
||||
const hoverStyle = this.#c("style");
|
||||
this.hoverStyle = hoverStyle;
|
||||
content.prepend(hoverStyle);
|
||||
table.addEventListener("mouseover", this.#handleStepHover.bind(this));
|
||||
table.addEventListener("mouseleave", e => {
|
||||
hoverStyle.innerText = "";
|
||||
});
|
||||
|
||||
showBoxesCheckbox.addEventListener("change", () => {
|
||||
if (showBoxesCheckbox.checked) {
|
||||
this.pageContainer.classList.add("showDebugBoxes");
|
||||
} else {
|
||||
this.pageContainer.classList.remove("showDebugBoxes");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateOperatorList(operatorList) {
|
||||
const self = this;
|
||||
|
||||
function cboxOnClick() {
|
||||
const x = +this.dataset.idx;
|
||||
if (this.checked) {
|
||||
self.breakPoints.push(x);
|
||||
} else {
|
||||
self.breakPoints.splice(self.breakPoints.indexOf(x), 1);
|
||||
}
|
||||
StepperManager.saveBreakPoints(self.pageIndex, self.breakPoints);
|
||||
}
|
||||
|
||||
const MAX_OPERATORS_COUNT = 15000;
|
||||
if (this.operatorListIdx > MAX_OPERATORS_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chunk = document.createDocumentFragment();
|
||||
const operatorsToDisplay = Math.min(
|
||||
MAX_OPERATORS_COUNT,
|
||||
operatorList.fnArray.length
|
||||
);
|
||||
for (let i = this.operatorListIdx; i < operatorsToDisplay; i++) {
|
||||
const line = this.#c("tr");
|
||||
line.className = "line";
|
||||
line.dataset.idx = i;
|
||||
chunk.append(line);
|
||||
const checked = this.breakPoints.includes(i);
|
||||
const args = operatorList.argsArray[i] || [];
|
||||
|
||||
const breakCell = this.#c("td");
|
||||
const cbox = this.#c("input");
|
||||
cbox.type = "checkbox";
|
||||
cbox.className = "points";
|
||||
cbox.checked = checked;
|
||||
cbox.dataset.idx = i;
|
||||
cbox.onclick = cboxOnClick;
|
||||
|
||||
breakCell.append(cbox);
|
||||
line.append(breakCell, this.#c("td", i.toString()));
|
||||
const fn = opMap[operatorList.fnArray[i]];
|
||||
let decArgs = args;
|
||||
if (fn === "showText") {
|
||||
const glyphs = args[0];
|
||||
const charCodeRow = this.#c("tr");
|
||||
const fontCharRow = this.#c("tr");
|
||||
const unicodeRow = this.#c("tr");
|
||||
for (const glyph of glyphs) {
|
||||
if (typeof glyph === "object" && glyph !== null) {
|
||||
charCodeRow.append(this.#c("td", glyph.originalCharCode));
|
||||
fontCharRow.append(this.#c("td", glyph.fontChar));
|
||||
unicodeRow.append(this.#c("td", glyph.unicode));
|
||||
} else {
|
||||
// null or number
|
||||
const advanceEl = this.#c("td", glyph);
|
||||
advanceEl.classList.add("advance");
|
||||
charCodeRow.append(advanceEl);
|
||||
fontCharRow.append(this.#c("td"));
|
||||
unicodeRow.append(this.#c("td"));
|
||||
}
|
||||
}
|
||||
decArgs = this.#c("td");
|
||||
const table = this.#c("table");
|
||||
table.classList.add("showText");
|
||||
decArgs.append(table);
|
||||
table.append(charCodeRow, fontCharRow, unicodeRow);
|
||||
} else if (fn === "constructPath") {
|
||||
const [op, [path], minMax] = args;
|
||||
decArgs = this.#c("td");
|
||||
decArgs.append(JSON.stringify(this.#simplifyArgs(path)));
|
||||
decArgs.append(this.#c("br"));
|
||||
decArgs.append(`minMax: ${JSON.stringify(this.#simplifyArgs(minMax))}`);
|
||||
decArgs.append(this.#c("br"));
|
||||
decArgs.append(`→ ${opMap[op]}`);
|
||||
} else if (fn === "restore" && this.indentLevel > 0) {
|
||||
this.indentLevel--;
|
||||
}
|
||||
line.append(this.#c("td", " ".repeat(this.indentLevel * 2) + fn));
|
||||
if (fn === "save") {
|
||||
this.indentLevel++;
|
||||
}
|
||||
|
||||
if (decArgs instanceof HTMLElement) {
|
||||
line.append(decArgs);
|
||||
} else {
|
||||
line.append(this.#c("td", JSON.stringify(this.#simplifyArgs(decArgs))));
|
||||
}
|
||||
}
|
||||
if (operatorsToDisplay < operatorList.fnArray.length) {
|
||||
const lastCell = this.#c("td", "...");
|
||||
lastCell.colspan = 4;
|
||||
chunk.append(lastCell);
|
||||
}
|
||||
this.operatorListIdx = operatorList.fnArray.length;
|
||||
this.table.append(chunk);
|
||||
}
|
||||
|
||||
setOperatorBBoxes(bboxes, metadata) {
|
||||
let boxesContainer = this.pageContainer.querySelector(".pdfBugGroupsLayer");
|
||||
if (!boxesContainer) {
|
||||
boxesContainer = this.#c("div");
|
||||
boxesContainer.classList.add("pdfBugGroupsLayer");
|
||||
this.pageContainer.append(boxesContainer);
|
||||
|
||||
boxesContainer.addEventListener(
|
||||
"click",
|
||||
this.#handleDebugBoxClick.bind(this)
|
||||
);
|
||||
boxesContainer.addEventListener(
|
||||
"mouseover",
|
||||
this.#handleDebugBoxHover.bind(this)
|
||||
);
|
||||
}
|
||||
boxesContainer.innerHTML = "";
|
||||
|
||||
const dependents = new Map();
|
||||
for (const [dependentIdx, { dependencies: ownDependencies }] of metadata) {
|
||||
for (const dependencyIdx of ownDependencies) {
|
||||
let ownDependents = dependents.get(dependencyIdx);
|
||||
if (!ownDependents) {
|
||||
dependents.set(dependencyIdx, (ownDependents = new Set()));
|
||||
}
|
||||
ownDependents.add(dependentIdx);
|
||||
}
|
||||
}
|
||||
|
||||
const groups = Array.from({ length: bboxes.length }, (_, i) => {
|
||||
let minX = null;
|
||||
let minY = null;
|
||||
let maxX = null;
|
||||
let maxY = null;
|
||||
if (!bboxes.isEmpty(i)) {
|
||||
minX = bboxes.minX(i);
|
||||
minY = bboxes.minY(i);
|
||||
maxX = bboxes.maxX(i);
|
||||
maxY = bboxes.maxY(i);
|
||||
}
|
||||
|
||||
return {
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
dependencies: Array.from(metadata.get(i)?.dependencies ?? []).sort(),
|
||||
dependents: Array.from(dependents.get(i) ?? []).sort(),
|
||||
isRenderingOperation: metadata.get(i)?.isRenderingOperation ?? false,
|
||||
idx: i,
|
||||
};
|
||||
});
|
||||
this.operatorsGroups = groups;
|
||||
|
||||
const operatorsGroupsByZindex = groups.toSorted((a, b) => {
|
||||
if (a.minX === null) {
|
||||
return b.minX === null ? 0 : 1;
|
||||
}
|
||||
if (b.minX === null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const diffs = [
|
||||
a.minY - b.minY,
|
||||
a.minX - b.minX,
|
||||
b.maxY - a.maxY,
|
||||
b.maxX - a.maxX,
|
||||
];
|
||||
for (const diff of diffs) {
|
||||
if (Math.abs(diff) > 0.01) {
|
||||
return Math.sign(diff);
|
||||
}
|
||||
}
|
||||
for (const diff of diffs) {
|
||||
if (Math.abs(diff) > 0.0001) {
|
||||
return Math.sign(diff);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
for (let i = 0; i < operatorsGroupsByZindex.length; i++) {
|
||||
const group = operatorsGroupsByZindex[i];
|
||||
if (group.minX !== null) {
|
||||
const el = this.#c("div");
|
||||
el.style.left = `${group.minX * 100}%`;
|
||||
el.style.top = `${group.minY * 100}%`;
|
||||
el.style.width = `${(group.maxX - group.minX) * 100}%`;
|
||||
el.style.height = `${(group.maxY - group.minY) * 100}%`;
|
||||
el.dataset.idx = group.idx;
|
||||
boxesContainer.append(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#handleStepHover(e) {
|
||||
const tr = e.target.closest("tr");
|
||||
if (!tr || tr.dataset.idx === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = +tr.dataset.idx;
|
||||
this.#highlightStepsGroup(index);
|
||||
}
|
||||
|
||||
#handleDebugBoxHover(e) {
|
||||
if (e.target.dataset.idx === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = Number(e.target.dataset.idx);
|
||||
this.#highlightStepsGroup(idx);
|
||||
}
|
||||
|
||||
#handleDebugBoxClick(e) {
|
||||
if (e.target.dataset.idx === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = Number(e.target.dataset.idx);
|
||||
|
||||
this.table.childNodes[idx].scrollIntoView();
|
||||
}
|
||||
|
||||
#highlightStepsGroup(index) {
|
||||
const group = this.operatorsGroups?.[index];
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renderingColor = `rgba(0, 0, 0, 0.1)`;
|
||||
const dependencyColor = `rgba(0, 255, 255, 0.1)`;
|
||||
const dependentColor = `rgba(255, 0, 0, 0.1)`;
|
||||
|
||||
const solid = color => `background-color: ${color}`;
|
||||
// Used for operations that have an empty bounding box
|
||||
const striped = color => `
|
||||
background-image: repeating-linear-gradient(
|
||||
-45deg,
|
||||
white,
|
||||
white 10px,
|
||||
${color} 10px,
|
||||
${color} 20px
|
||||
)
|
||||
`;
|
||||
|
||||
const select = idx => `#${this.panel.id} tr[data-idx="${idx}"]`;
|
||||
const selectN = idxs =>
|
||||
idxs.length === 0 ? "q:not(q)" : idxs.map(select).join(", ");
|
||||
|
||||
const isEmpty = idx =>
|
||||
!this.operatorsGroups[idx] || this.operatorsGroups[idx].minX === null;
|
||||
|
||||
const selfColor = group.isRenderingOperation
|
||||
? renderingColor
|
||||
: dependentColor;
|
||||
|
||||
this.hoverStyle.innerText = `${select(index)} {
|
||||
${group.minX === null ? striped(selfColor) : solid(selfColor)}
|
||||
}`;
|
||||
if (group.dependencies.length > 0) {
|
||||
this.hoverStyle.innerText += `
|
||||
${selectN(group.dependencies.filter(idx => !isEmpty(idx)))} {
|
||||
${solid(dependencyColor)}
|
||||
}
|
||||
${selectN(group.dependencies.filter(isEmpty))} {
|
||||
${striped(dependencyColor)}
|
||||
}`;
|
||||
}
|
||||
if (group.dependents.length > 0) {
|
||||
this.hoverStyle.innerText += `
|
||||
${selectN(group.dependents.filter(idx => !isEmpty(idx)))} {
|
||||
${solid(dependentColor)}
|
||||
}
|
||||
${selectN(group.dependents.filter(isEmpty))} {
|
||||
${striped(dependentColor)}
|
||||
}`;
|
||||
}
|
||||
|
||||
this.hoverStyle.innerText += `
|
||||
#viewer [data-page-number="${this.pageIndex + 1}"] .pdfBugGroupsLayer [data-idx="${index}"] {
|
||||
background-color: var(--hover-background-color);
|
||||
outline-style: var(--hover-outline-style);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
getNextBreakPoint() {
|
||||
this.breakPoints.sort((a, b) => a - b);
|
||||
for (const breakPoint of this.breakPoints) {
|
||||
if (breakPoint > this.currentIdx) {
|
||||
return breakPoint;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
breakIt(idx, callback) {
|
||||
StepperManager.selectStepper(this.pageIndex, true);
|
||||
this.currentIdx = idx;
|
||||
|
||||
const listener = evt => {
|
||||
switch (evt.keyCode) {
|
||||
case 83: // step
|
||||
document.removeEventListener("keydown", listener);
|
||||
this.nextBreakPoint = this.currentIdx + 1;
|
||||
this.goTo(-1);
|
||||
callback();
|
||||
break;
|
||||
case 67: // continue
|
||||
document.removeEventListener("keydown", listener);
|
||||
this.nextBreakPoint = this.getNextBreakPoint();
|
||||
this.goTo(-1);
|
||||
callback();
|
||||
break;
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", listener);
|
||||
this.goTo(idx);
|
||||
}
|
||||
|
||||
goTo(idx) {
|
||||
const allRows = this.panel.getElementsByClassName("line");
|
||||
for (const row of allRows) {
|
||||
if ((row.dataset.idx | 0) === idx) {
|
||||
row.style.backgroundColor = "rgb(251,250,207)";
|
||||
row.scrollIntoView();
|
||||
} else {
|
||||
row.style.backgroundColor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Stats = (function Stats() {
|
||||
let stats = [];
|
||||
function clear(node) {
|
||||
node.textContent = ""; // Remove any `node` contents from the DOM.
|
||||
}
|
||||
function getStatIndex(pageNumber) {
|
||||
for (const [i, stat] of stats.entries()) {
|
||||
if (stat.pageNumber === pageNumber) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
// Properties/functions needed by PDFBug.
|
||||
id: "Stats",
|
||||
name: "Stats",
|
||||
panel: null,
|
||||
manager: null,
|
||||
init() {},
|
||||
enabled: false,
|
||||
active: false,
|
||||
// Stats specific functions.
|
||||
add(pageNumber, stat) {
|
||||
if (!stat) {
|
||||
return;
|
||||
}
|
||||
const statsIndex = getStatIndex(pageNumber);
|
||||
if (statsIndex !== false) {
|
||||
stats[statsIndex].div.remove();
|
||||
stats.splice(statsIndex, 1);
|
||||
}
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "stats";
|
||||
const title = document.createElement("div");
|
||||
title.className = "title";
|
||||
title.textContent = "Page: " + pageNumber;
|
||||
const statsDiv = document.createElement("div");
|
||||
statsDiv.textContent = stat.toString();
|
||||
wrapper.append(title, statsDiv);
|
||||
stats.push({ pageNumber, div: wrapper });
|
||||
stats.sort((a, b) => a.pageNumber - b.pageNumber);
|
||||
clear(this.panel);
|
||||
for (const entry of stats) {
|
||||
this.panel.append(entry.div);
|
||||
}
|
||||
},
|
||||
cleanup() {
|
||||
stats = [];
|
||||
clear(this.panel);
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
// Manages all the debugging tools.
|
||||
class PDFBug {
|
||||
static #buttons = [];
|
||||
|
||||
static #activePanel = null;
|
||||
|
||||
static tools = [FontInspector, StepperManager, Stats];
|
||||
|
||||
static enable(ids) {
|
||||
const all = ids.length === 1 && ids[0] === "all";
|
||||
const tools = this.tools;
|
||||
for (const tool of tools) {
|
||||
if (all || ids.includes(tool.id)) {
|
||||
tool.enabled = true;
|
||||
}
|
||||
}
|
||||
if (!all) {
|
||||
// Sort the tools by the order they are enabled.
|
||||
tools.sort(function (a, b) {
|
||||
let indexA = ids.indexOf(a.id);
|
||||
indexA = indexA < 0 ? tools.length : indexA;
|
||||
let indexB = ids.indexOf(b.id);
|
||||
indexB = indexB < 0 ? tools.length : indexB;
|
||||
return indexA - indexB;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static init(container, ids) {
|
||||
this.loadCSS();
|
||||
this.enable(ids);
|
||||
/*
|
||||
* Basic Layout:
|
||||
* PDFBug
|
||||
* Controls
|
||||
* Panels
|
||||
* Panel
|
||||
* Panel
|
||||
* ...
|
||||
*/
|
||||
const ui = document.createElement("div");
|
||||
ui.id = "PDFBug";
|
||||
|
||||
const controls = document.createElement("div");
|
||||
controls.setAttribute("class", "controls");
|
||||
ui.append(controls);
|
||||
|
||||
const panels = document.createElement("div");
|
||||
panels.setAttribute("class", "panels");
|
||||
ui.append(panels);
|
||||
|
||||
container.append(ui);
|
||||
container.style.right = "var(--panel-width)";
|
||||
|
||||
// Initialize all the debugging tools.
|
||||
for (const tool of this.tools) {
|
||||
const panel = document.createElement("div");
|
||||
const panelButton = document.createElement("button");
|
||||
panelButton.textContent = tool.name;
|
||||
panelButton.addEventListener("click", event => {
|
||||
event.preventDefault();
|
||||
this.selectPanel(tool);
|
||||
});
|
||||
controls.append(panelButton);
|
||||
panels.append(panel);
|
||||
tool.panel = panel;
|
||||
tool.manager = this;
|
||||
if (tool.enabled) {
|
||||
tool.init();
|
||||
} else {
|
||||
panel.textContent =
|
||||
`${tool.name} is disabled. To enable add "${tool.id}" to ` +
|
||||
"the pdfBug parameter and refresh (separate multiple by commas).";
|
||||
}
|
||||
this.#buttons.push(panelButton);
|
||||
}
|
||||
this.selectPanel(0);
|
||||
}
|
||||
|
||||
static loadCSS() {
|
||||
const { url } = import.meta;
|
||||
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = url.replace(/\.mjs$/, ".css");
|
||||
|
||||
document.head.append(link);
|
||||
}
|
||||
|
||||
static cleanup() {
|
||||
for (const tool of this.tools) {
|
||||
if (tool.enabled) {
|
||||
tool.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static selectPanel(index) {
|
||||
if (typeof index !== "number") {
|
||||
index = this.tools.indexOf(index);
|
||||
}
|
||||
if (index === this.#activePanel) {
|
||||
return;
|
||||
}
|
||||
this.#activePanel = index;
|
||||
for (const [j, tool] of this.tools.entries()) {
|
||||
const isActive = j === index;
|
||||
this.#buttons[j].classList.toggle("active", isActive);
|
||||
tool.active = isActive;
|
||||
tool.panel.hidden = !isActive;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.FontInspector = FontInspector;
|
||||
globalThis.StepperManager = StepperManager;
|
||||
globalThis.Stats = Stats;
|
||||
|
||||
export { PDFBug };
|
||||
392
web/dialog.css
Normal file
@@ -0,0 +1,392 @@
|
||||
/* Copyright 2022 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.
|
||||
*/
|
||||
|
||||
.dialog {
|
||||
--dialog-bg-color: light-dark(white, #1c1b22);
|
||||
--dialog-border-color: light-dark(white, #1c1b22);
|
||||
--dialog-shadow: 0 2px 14px 0 light-dark(rgb(58 57 68 / 0.2), #15141a);
|
||||
--text-primary-color: light-dark(#15141a, #fbfbfe);
|
||||
--text-secondary-color: light-dark(#5b5b66, #cfcfd8);
|
||||
--hover-filter: brightness(0.9);
|
||||
--link-fg-color: light-dark(#0060df, #0df);
|
||||
--link-hover-fg-color: light-dark(#0250bb, #80ebff);
|
||||
--separator-color: light-dark(#f0f0f4, #52525e);
|
||||
|
||||
--textarea-border-color: #8f8f9d;
|
||||
--textarea-bg-color: light-dark(white, #42414d);
|
||||
--textarea-fg-color: var(--text-secondary-color);
|
||||
|
||||
--radio-bg-color: light-dark(#f0f0f4, #2b2a33);
|
||||
--radio-checked-bg-color: light-dark(#fbfbfe, #15141a);
|
||||
--radio-border-color: #8f8f9d;
|
||||
--radio-checked-border-color: light-dark(#0060df, #0df);
|
||||
|
||||
--button-secondary-bg-color: light-dark(
|
||||
rgb(21 20 26 / 0.07),
|
||||
rgb(251 251 254 / 0.07)
|
||||
);
|
||||
--button-secondary-fg-color: var(--text-primary-color);
|
||||
--button-secondary-border-color: var(--button-secondary-bg-color);
|
||||
--button-secondary-active-bg-color: light-dark(
|
||||
rgb(21 20 26 / 0.21),
|
||||
rgb(251 251 254 / 0.21)
|
||||
);
|
||||
--button-secondary-active-fg-color: var(--button-secondary-fg-color);
|
||||
--button-secondary-active-border-color: var(--button-secondary-bg-color);
|
||||
--button-secondary-hover-bg-color: light-dark(
|
||||
rgb(21 20 26 / 0.14),
|
||||
rgb(251 251 254 / 0.14)
|
||||
);
|
||||
--button-secondary-hover-fg-color: var(--button-secondary-fg-color);
|
||||
--button-secondary-hover-border-color: var(--button-secondary-hover-bg-color);
|
||||
--button-secondary-disabled-bg-color: var(--button-secondary-bg-color);
|
||||
--button-secondary-disabled-border-color: var(
|
||||
--button-secondary-border-color
|
||||
);
|
||||
--button-secondary-disabled-fg-color: var(--button-secondary-fg-color);
|
||||
|
||||
--button-primary-bg-color: light-dark(#0060df, #0df);
|
||||
--button-primary-fg-color: light-dark(#fbfbfe, #15141a);
|
||||
--button-primary-border-color: var(--button-primary-bg-color);
|
||||
--button-primary-active-bg-color: light-dark(#054096, #aaf2ff);
|
||||
--button-primary-active-fg-color: var(--button-primary-fg-color);
|
||||
--button-primary-active-border-color: var(--button-primary-active-bg-color);
|
||||
--button-primary-hover-bg-color: light-dark(#0250bb, #80ebff);
|
||||
--button-primary-hover-fg-color: var(--button-primary-fg-color);
|
||||
--button-primary-hover-border-color: var(--button-primary-hover-bg-color);
|
||||
--button-primary-disabled-bg-color: var(--button-primary-bg-color);
|
||||
--button-primary-disabled-border-color: var(--button-primary-border-color);
|
||||
--button-primary-disabled-fg-color: var(--button-primary-fg-color);
|
||||
--button-disabled-opacity: 0.4;
|
||||
|
||||
--input-text-bg-color: light-dark(white, #42414d);
|
||||
--input-text-fg-color: var(--text-primary-color);
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
--hover-filter: brightness(1.4);
|
||||
--button-disabled-opacity: 0.6;
|
||||
}
|
||||
|
||||
@media screen and (forced-colors: active) {
|
||||
--dialog-bg-color: Canvas;
|
||||
--dialog-border-color: CanvasText;
|
||||
--dialog-shadow: none;
|
||||
--text-primary-color: CanvasText;
|
||||
--text-secondary-color: CanvasText;
|
||||
--hover-filter: none;
|
||||
--link-fg-color: LinkText;
|
||||
--link-hover-fg-color: LinkText;
|
||||
--separator-color: CanvasText;
|
||||
|
||||
--textarea-border-color: ButtonBorder;
|
||||
--textarea-bg-color: Field;
|
||||
--textarea-fg-color: ButtonText;
|
||||
|
||||
--radio-bg-color: ButtonFace;
|
||||
--radio-checked-bg-color: ButtonFace;
|
||||
--radio-border-color: ButtonText;
|
||||
--radio-checked-border-color: ButtonText;
|
||||
|
||||
--button-secondary-bg-color: ButtonFace;
|
||||
--button-secondary-fg-color: ButtonText;
|
||||
--button-secondary-border-color: ButtonText;
|
||||
--button-secondary-active-bg-color: HighlightText;
|
||||
--button-secondary-active-fg-color: SelectedItem;
|
||||
--button-secondary-active-border-color: ButtonText;
|
||||
--button-secondary-hover-bg-color: HighlightText;
|
||||
--button-secondary-hover-fg-color: SelectedItem;
|
||||
--button-secondary-hover-border-color: SelectedItem;
|
||||
--button-secondary-disabled-fg-color: GrayText;
|
||||
--button-secondary-disabled-border-color: GrayText;
|
||||
|
||||
--button-primary-bg-color: ButtonText;
|
||||
--button-primary-fg-color: ButtonFace;
|
||||
--button-primary-border-color: ButtonText;
|
||||
--button-primary-active-bg-color: SelectedItem;
|
||||
--button-primary-active-fg-color: HighlightText;
|
||||
--button-primary-active-border-color: ButtonText;
|
||||
--button-primary-hover-bg-color: SelectedItem;
|
||||
--button-primary-hover-fg-color: HighlightText;
|
||||
--button-primary-hover-border-color: SelectedItem;
|
||||
--button-primary-disabled-bg-color: GrayText;
|
||||
--button-primary-disabled-fg-color: ButtonFace;
|
||||
--button-primary-disabled-border-color: GrayText;
|
||||
--button-disabled-opacity: 1;
|
||||
|
||||
--input-text-bg-color: Field;
|
||||
--input-text-fg-color: FieldText;
|
||||
}
|
||||
|
||||
font: message-box;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 150%;
|
||||
border-radius: 4px;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--dialog-border-color);
|
||||
background: var(--dialog-bg-color);
|
||||
color: var(--text-primary-color);
|
||||
box-shadow: var(--dialog-shadow);
|
||||
|
||||
.mainContainer {
|
||||
*:focus-visible {
|
||||
outline: var(--focus-ring-outline);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
width: auto;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
|
||||
> span {
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 590;
|
||||
line-height: 150%; /* 19.5px */
|
||||
}
|
||||
}
|
||||
|
||||
.dialogSeparator {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
margin-block: 4px;
|
||||
border-top: 1px solid var(--separator-color);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.dialogButtonsGroup {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.radio {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
|
||||
> .radioButton {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
appearance: none;
|
||||
box-sizing: border-box;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--radio-bg-color);
|
||||
border: 1px solid var(--radio-border-color);
|
||||
|
||||
&:hover {
|
||||
filter: var(--hover-filter);
|
||||
}
|
||||
|
||||
&:checked {
|
||||
background-color: var(--radio-checked-bg-color);
|
||||
border: 4px solid var(--radio-checked-border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .radioLabel {
|
||||
display: flex;
|
||||
padding-inline-start: 24px;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
align-self: stretch;
|
||||
|
||||
> span {
|
||||
flex: 1 0 0;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button:not(:is(.toggle-button, .closeButton, .clearInputButton)) {
|
||||
border-radius: 4px;
|
||||
border: 1px solid;
|
||||
font: menu;
|
||||
font-weight: 590;
|
||||
font-size: 13px;
|
||||
padding: 4px 16px;
|
||||
width: auto;
|
||||
height: 32px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
filter: var(--hover-filter);
|
||||
}
|
||||
|
||||
> span {
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
&.secondaryButton {
|
||||
color: var(--button-secondary-fg-color);
|
||||
background-color: var(--button-secondary-bg-color);
|
||||
border-color: var(--button-secondary-border-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--button-secondary-hover-fg-color);
|
||||
background-color: var(--button-secondary-hover-bg-color);
|
||||
border-color: var(--button-secondary-hover-border-color);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--button-secondary-active-fg-color);
|
||||
background-color: var(--button-secondary-active-bg-color);
|
||||
border-color: var(--button-secondary-active-border-color);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--button-secondary-disabled-bg-color);
|
||||
border-color: var(--button-secondary-disabled-border-color);
|
||||
color: var(--button-secondary-disabled-fg-color);
|
||||
opacity: var(--button-disabled-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
&.primaryButton {
|
||||
color: var(--button-primary-fg-color);
|
||||
background-color: var(--button-primary-bg-color);
|
||||
border-color: var(--button-primary-border-color);
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
color: var(--button-primary-hover-fg-color);
|
||||
background-color: var(--button-primary-hover-bg-color);
|
||||
border-color: var(--button-primary-hover-border-color);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--button-primary-active-fg-color);
|
||||
background-color: var(--button-primary-active-bg-color);
|
||||
border-color: var(--button-primary-active-border-color);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--button-primary-disabled-bg-color);
|
||||
border-color: var(--button-primary-disabled-border-color);
|
||||
color: var(--button-primary-disabled-fg-color);
|
||||
opacity: var(--button-disabled-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link-fg-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--link-hover-fg-color);
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
font: inherit;
|
||||
padding: 8px;
|
||||
resize: none;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--textarea-border-color);
|
||||
background: var(--textarea-bg-color);
|
||||
color: var(--textarea-fg-color);
|
||||
|
||||
&:focus {
|
||||
outline-offset: 0;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
background-color: var(--input-text-bg-color);
|
||||
color: var(--input-text-fg-color);
|
||||
}
|
||||
|
||||
.messageBar {
|
||||
--message-bar-bg-color: light-dark(#ffebcd, #5a3100);
|
||||
--message-bar-fg-color: light-dark(#15141a, #fbfbfe);
|
||||
--message-bar-border-color: light-dark(
|
||||
rgb(0 0 0 / 0.08),
|
||||
rgb(255 255 255 / 0.08)
|
||||
);
|
||||
--message-bar-icon: url(images/messageBar_warning.svg);
|
||||
--message-bar-icon-color: light-dark(#cd411e, #e49c49);
|
||||
|
||||
@media screen and (forced-colors: active) {
|
||||
--message-bar-bg-color: HighlightText;
|
||||
--message-bar-fg-color: CanvasText;
|
||||
--message-bar-border-color: CanvasText;
|
||||
--message-bar-icon-color: CanvasText;
|
||||
}
|
||||
|
||||
align-self: stretch;
|
||||
|
||||
> div {
|
||||
&::before,
|
||||
> div {
|
||||
margin-block: 4px;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
flex: 1 0 0;
|
||||
|
||||
.title {
|
||||
font-size: 13px;
|
||||
font-weight: 590;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggler {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
|
||||
> .togglerLabel {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
125
web/download_manager.js
Normal file
@@ -0,0 +1,125 @@
|
||||
/* Copyright 2013 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.
|
||||
*/
|
||||
|
||||
/** @typedef {import("./interfaces").IDownloadManager} IDownloadManager */
|
||||
|
||||
import { createValidAbsoluteUrl, isPdfFile } from "pdfjs-lib";
|
||||
|
||||
if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("CHROME || GENERIC")) {
|
||||
throw new Error(
|
||||
'Module "pdfjs-web/download_manager" shall not be used ' +
|
||||
"outside CHROME and GENERIC builds."
|
||||
);
|
||||
}
|
||||
|
||||
function download(blobUrl, filename) {
|
||||
const a = document.createElement("a");
|
||||
if (!a.click) {
|
||||
throw new Error('DownloadManager: "a.click()" is not supported.');
|
||||
}
|
||||
a.href = blobUrl;
|
||||
a.target = "_parent";
|
||||
// Use a.download if available. This increases the likelihood that
|
||||
// the file is downloaded instead of opened by another PDF plugin.
|
||||
if ("download" in a) {
|
||||
a.download = filename;
|
||||
}
|
||||
// <a> must be in the document for recent Firefox versions,
|
||||
// otherwise .click() is ignored.
|
||||
(document.body || document.documentElement).append(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* @implements {IDownloadManager}
|
||||
*/
|
||||
class DownloadManager {
|
||||
#openBlobUrls = new WeakMap();
|
||||
|
||||
downloadData(data, filename, contentType) {
|
||||
const blobUrl = URL.createObjectURL(
|
||||
new Blob([data], { type: contentType })
|
||||
);
|
||||
download(blobUrl, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} Indicating if the data was opened.
|
||||
*/
|
||||
openOrDownloadData(data, filename, dest = null) {
|
||||
const isPdfData = isPdfFile(filename);
|
||||
const contentType = isPdfData ? "application/pdf" : "";
|
||||
|
||||
if (
|
||||
(typeof PDFJSDev === "undefined" || !PDFJSDev.test("COMPONENTS")) &&
|
||||
isPdfData
|
||||
) {
|
||||
let blobUrl = this.#openBlobUrls.get(data);
|
||||
if (!blobUrl) {
|
||||
blobUrl = URL.createObjectURL(new Blob([data], { type: contentType }));
|
||||
this.#openBlobUrls.set(data, blobUrl);
|
||||
}
|
||||
let viewerUrl;
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
// The current URL is the viewer, let's use it and append the file.
|
||||
viewerUrl = "?file=" + encodeURIComponent(blobUrl + "#" + filename);
|
||||
} else if (PDFJSDev.test("CHROME")) {
|
||||
// In the Chrome extension, the URL is rewritten using the history API
|
||||
// in viewer.js, so an absolute URL must be generated.
|
||||
viewerUrl =
|
||||
// eslint-disable-next-line no-undef
|
||||
chrome.runtime.getURL("/content/web/viewer.html") +
|
||||
"?file=" +
|
||||
encodeURIComponent(blobUrl + "#" + filename);
|
||||
}
|
||||
if (dest) {
|
||||
viewerUrl += `#${escape(dest)}`;
|
||||
}
|
||||
|
||||
try {
|
||||
window.open(viewerUrl);
|
||||
return true;
|
||||
} catch (ex) {
|
||||
console.error("openOrDownloadData:", ex);
|
||||
// Release the `blobUrl`, since opening it failed, and fallback to
|
||||
// downloading the PDF file.
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
this.#openBlobUrls.delete(data);
|
||||
}
|
||||
}
|
||||
|
||||
this.downloadData(data, filename, contentType);
|
||||
return false;
|
||||
}
|
||||
|
||||
download(data, url, filename) {
|
||||
let blobUrl;
|
||||
if (data) {
|
||||
blobUrl = URL.createObjectURL(
|
||||
new Blob([data], { type: "application/pdf" })
|
||||
);
|
||||
} else {
|
||||
if (!createValidAbsoluteUrl(url, "http://example.com")) {
|
||||
console.error(`download - not a valid URL: ${url}`);
|
||||
return;
|
||||
}
|
||||
blobUrl = url + "#pdfjs.action=download";
|
||||
}
|
||||
download(blobUrl, filename);
|
||||
}
|
||||
}
|
||||
|
||||
export { DownloadManager };
|
||||
136
web/draw_layer_builder.css
Normal file
@@ -0,0 +1,136 @@
|
||||
/* Copyright 2014 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.
|
||||
*/
|
||||
|
||||
.canvasWrapper {
|
||||
svg {
|
||||
transform: none;
|
||||
|
||||
&.moving {
|
||||
z-index: 100000;
|
||||
}
|
||||
|
||||
&.highlight,
|
||||
&.highlightOutline {
|
||||
&[data-main-rotation="90"] {
|
||||
mask,
|
||||
use:not(.clip, .mask) {
|
||||
transform: matrix(0, 1, -1, 0, 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-main-rotation="180"] {
|
||||
mask,
|
||||
use:not(.clip, .mask) {
|
||||
transform: matrix(-1, 0, 0, -1, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-main-rotation="270"] {
|
||||
mask,
|
||||
use:not(.clip, .mask) {
|
||||
transform: matrix(0, -1, 1, 0, 0, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.draw {
|
||||
position: absolute;
|
||||
mix-blend-mode: normal;
|
||||
|
||||
&[data-draw-rotation="90"] {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
&[data-draw-rotation="180"] {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
&[data-draw-rotation="270"] {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
}
|
||||
|
||||
&.highlight {
|
||||
--blend-mode: multiply;
|
||||
|
||||
@media screen and (forced-colors: active) {
|
||||
--blend-mode: difference;
|
||||
}
|
||||
|
||||
position: absolute;
|
||||
mix-blend-mode: var(--blend-mode);
|
||||
|
||||
&:not(.free) {
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
}
|
||||
|
||||
&.highlightOutline {
|
||||
position: absolute;
|
||||
mix-blend-mode: normal;
|
||||
fill-rule: evenodd;
|
||||
fill: none;
|
||||
|
||||
&:not(.free) {
|
||||
&.hovered:not(.selected) {
|
||||
stroke: var(--hover-outline-color);
|
||||
stroke-width: var(--outline-width);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
.mainOutline {
|
||||
stroke: var(--outline-around-color);
|
||||
stroke-width: calc(
|
||||
var(--outline-width) + 2 * var(--outline-around-width)
|
||||
);
|
||||
}
|
||||
|
||||
.secondaryOutline {
|
||||
stroke: var(--outline-color);
|
||||
stroke-width: var(--outline-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.free {
|
||||
/*
|
||||
When drawing the outline we use a mask in order to remove the parts
|
||||
that are inside the shape. Unfortunately, this removes the part of the
|
||||
outline that is inside the shape. To "fix" this we increase the width
|
||||
to have what we want to be visible outside the shape.
|
||||
This is not a perfect solution, but it works well enough.
|
||||
*/
|
||||
&.hovered:not(.selected) {
|
||||
stroke: var(--hover-outline-color);
|
||||
stroke-width: calc(2 * var(--outline-width));
|
||||
}
|
||||
|
||||
&.selected {
|
||||
.mainOutline {
|
||||
stroke: var(--outline-around-color);
|
||||
stroke-width: calc(
|
||||
2 * (var(--outline-width) + var(--outline-around-width))
|
||||
);
|
||||
}
|
||||
|
||||
.secondaryOutline {
|
||||
stroke: var(--outline-color);
|
||||
stroke-width: calc(2 * var(--outline-width));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
web/draw_layer_builder.js
Normal file
@@ -0,0 +1,70 @@
|
||||
/* Copyright 2022 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 { DrawLayer } from "pdfjs-lib";
|
||||
|
||||
/**
|
||||
* @typedef {Object} DrawLayerBuilderOptions
|
||||
* @property {number} pageIndex
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} DrawLayerBuilderRenderOptions
|
||||
* @property {string} [intent] - The default value is "display".
|
||||
*/
|
||||
|
||||
class DrawLayerBuilder {
|
||||
#drawLayer = null;
|
||||
|
||||
/**
|
||||
* @param {DrawLayerBuilderOptions} options
|
||||
*/
|
||||
constructor(options) {
|
||||
this.pageIndex = options.pageIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DrawLayerBuilderRenderOptions} options
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async render({ intent = "display" }) {
|
||||
if (intent !== "display" || this.#drawLayer || this._cancelled) {
|
||||
return;
|
||||
}
|
||||
this.#drawLayer = new DrawLayer({
|
||||
pageIndex: this.pageIndex,
|
||||
});
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this._cancelled = true;
|
||||
|
||||
if (!this.#drawLayer) {
|
||||
return;
|
||||
}
|
||||
this.#drawLayer.destroy();
|
||||
this.#drawLayer = null;
|
||||
}
|
||||
|
||||
setParent(parent) {
|
||||
this.#drawLayer?.setParent(parent);
|
||||
}
|
||||
|
||||
getDrawLayer() {
|
||||
return this.#drawLayer;
|
||||
}
|
||||
}
|
||||
|
||||
export { DrawLayerBuilder };
|
||||
129
web/editor_undo_bar.js
Normal file
@@ -0,0 +1,129 @@
|
||||
/* 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 { noContextMenu } from "pdfjs-lib";
|
||||
|
||||
class EditorUndoBar {
|
||||
#closeButton = null;
|
||||
|
||||
#container;
|
||||
|
||||
#eventBus = null;
|
||||
|
||||
#focusTimeout = null;
|
||||
|
||||
#initController = null;
|
||||
|
||||
isOpen = false;
|
||||
|
||||
#message;
|
||||
|
||||
#showController = null;
|
||||
|
||||
#undoButton;
|
||||
|
||||
static #l10nMessages = Object.freeze({
|
||||
highlight: "pdfjs-editor-undo-bar-message-highlight",
|
||||
freetext: "pdfjs-editor-undo-bar-message-freetext",
|
||||
stamp: "pdfjs-editor-undo-bar-message-stamp",
|
||||
ink: "pdfjs-editor-undo-bar-message-ink",
|
||||
signature: "pdfjs-editor-undo-bar-message-signature",
|
||||
_multiple: "pdfjs-editor-undo-bar-message-multiple",
|
||||
});
|
||||
|
||||
constructor({ container, message, undoButton, closeButton }, eventBus) {
|
||||
this.#container = container;
|
||||
this.#message = message;
|
||||
this.#undoButton = undoButton;
|
||||
this.#closeButton = closeButton;
|
||||
this.#eventBus = eventBus;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#initController?.abort();
|
||||
this.#initController = null;
|
||||
|
||||
this.hide();
|
||||
}
|
||||
|
||||
show(undoAction, messageData) {
|
||||
if (!this.#initController) {
|
||||
this.#initController = new AbortController();
|
||||
const opts = { signal: this.#initController.signal };
|
||||
const boundHide = this.hide.bind(this);
|
||||
|
||||
this.#container.addEventListener("contextmenu", noContextMenu, opts);
|
||||
this.#closeButton.addEventListener("click", boundHide, opts);
|
||||
this.#eventBus._on("beforeprint", boundHide, opts);
|
||||
this.#eventBus._on("download", boundHide, opts);
|
||||
}
|
||||
|
||||
this.hide();
|
||||
|
||||
if (typeof messageData === "string") {
|
||||
this.#message.setAttribute(
|
||||
"data-l10n-id",
|
||||
EditorUndoBar.#l10nMessages[messageData]
|
||||
);
|
||||
} else {
|
||||
this.#message.setAttribute(
|
||||
"data-l10n-id",
|
||||
EditorUndoBar.#l10nMessages._multiple
|
||||
);
|
||||
this.#message.setAttribute(
|
||||
"data-l10n-args",
|
||||
JSON.stringify({ count: messageData })
|
||||
);
|
||||
}
|
||||
this.isOpen = true;
|
||||
this.#container.hidden = false;
|
||||
|
||||
this.#showController = new AbortController();
|
||||
|
||||
this.#undoButton.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
undoAction();
|
||||
this.hide();
|
||||
},
|
||||
{ signal: this.#showController.signal }
|
||||
);
|
||||
|
||||
// Without the setTimeout, VoiceOver will read out the document title
|
||||
// instead of the popup label.
|
||||
this.#focusTimeout = setTimeout(() => {
|
||||
this.#container.focus();
|
||||
this.#focusTimeout = null;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
this.isOpen = false;
|
||||
this.#container.hidden = true;
|
||||
|
||||
this.#showController?.abort();
|
||||
this.#showController = null;
|
||||
|
||||
if (this.#focusTimeout) {
|
||||
clearTimeout(this.#focusTimeout);
|
||||
this.#focusTimeout = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { EditorUndoBar };
|
||||
226
web/event_utils.js
Normal file
@@ -0,0 +1,226 @@
|
||||
/* Copyright 2012 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.
|
||||
*/
|
||||
|
||||
const WaitOnType = {
|
||||
EVENT: "event",
|
||||
TIMEOUT: "timeout",
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {Object} WaitOnEventOrTimeoutParameters
|
||||
* @property {Object} target - The event target, can for example be:
|
||||
* `window`, `document`, a DOM element, or an {EventBus} instance.
|
||||
* @property {string} name - The name of the event.
|
||||
* @property {number} delay - The delay, in milliseconds, after which the
|
||||
* timeout occurs (if the event wasn't already dispatched).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Allows waiting for an event or a timeout, whichever occurs first.
|
||||
* Can be used to ensure that an action always occurs, even when an event
|
||||
* arrives late or not at all.
|
||||
*
|
||||
* @param {WaitOnEventOrTimeoutParameters}
|
||||
* @returns {Promise} A promise that is resolved with a {WaitOnType} value.
|
||||
*/
|
||||
async function waitOnEventOrTimeout({ target, name, delay = 0 }) {
|
||||
if (
|
||||
typeof target !== "object" ||
|
||||
!(name && typeof name === "string") ||
|
||||
!(Number.isInteger(delay) && delay >= 0)
|
||||
) {
|
||||
throw new Error("waitOnEventOrTimeout - invalid parameters.");
|
||||
}
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
const ac = new AbortController();
|
||||
|
||||
function handler(type) {
|
||||
ac.abort(); // Remove event listener.
|
||||
clearTimeout(timeout);
|
||||
|
||||
resolve(type);
|
||||
}
|
||||
|
||||
const evtMethod = target instanceof EventBus ? "_on" : "addEventListener";
|
||||
target[evtMethod](name, handler.bind(null, WaitOnType.EVENT), {
|
||||
signal: ac.signal,
|
||||
});
|
||||
|
||||
const timeout = setTimeout(handler.bind(null, WaitOnType.TIMEOUT), delay);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple event bus for an application. Listeners are attached using the `on`
|
||||
* and `off` methods. To raise an event, the `dispatch` method shall be used.
|
||||
*/
|
||||
class EventBus {
|
||||
#listeners = Object.create(null);
|
||||
|
||||
/**
|
||||
* @param {string} eventName
|
||||
* @param {function} listener
|
||||
* @param {Object} [options]
|
||||
*/
|
||||
on(eventName, listener, options = null) {
|
||||
this._on(eventName, listener, {
|
||||
external: true,
|
||||
once: options?.once,
|
||||
signal: options?.signal,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} eventName
|
||||
* @param {function} listener
|
||||
* @param {Object} [options]
|
||||
*/
|
||||
off(eventName, listener, options = null) {
|
||||
this._off(eventName, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} eventName
|
||||
* @param {Object} data
|
||||
*/
|
||||
dispatch(eventName, data) {
|
||||
const eventListeners = this.#listeners[eventName];
|
||||
if (!eventListeners || eventListeners.length === 0) {
|
||||
return;
|
||||
}
|
||||
let externalListeners;
|
||||
// Making copy of the listeners array in case if it will be modified
|
||||
// during dispatch.
|
||||
for (const { listener, external, once } of eventListeners.slice(0)) {
|
||||
if (once) {
|
||||
this._off(eventName, listener);
|
||||
}
|
||||
if (external) {
|
||||
(externalListeners ||= []).push(listener);
|
||||
continue;
|
||||
}
|
||||
listener(data);
|
||||
}
|
||||
// Dispatch any "external" listeners *after* the internal ones, to give the
|
||||
// viewer components time to handle events and update their state first.
|
||||
if (externalListeners) {
|
||||
for (const listener of externalListeners) {
|
||||
listener(data);
|
||||
}
|
||||
externalListeners = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
_on(eventName, listener, options = null) {
|
||||
let rmAbort = null;
|
||||
if (options?.signal instanceof AbortSignal) {
|
||||
const { signal } = options;
|
||||
if (signal.aborted) {
|
||||
console.error("Cannot use an `aborted` signal.");
|
||||
return;
|
||||
}
|
||||
const onAbort = () => this._off(eventName, listener);
|
||||
rmAbort = () => signal.removeEventListener("abort", onAbort);
|
||||
|
||||
signal.addEventListener("abort", onAbort);
|
||||
}
|
||||
|
||||
const eventListeners = (this.#listeners[eventName] ||= []);
|
||||
eventListeners.push({
|
||||
listener,
|
||||
external: options?.external === true,
|
||||
once: options?.once === true,
|
||||
rmAbort,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
_off(eventName, listener, options = null) {
|
||||
const eventListeners = this.#listeners[eventName];
|
||||
if (!eventListeners) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0, ii = eventListeners.length; i < ii; i++) {
|
||||
const evt = eventListeners[i];
|
||||
if (evt.listener === listener) {
|
||||
evt.rmAbort?.(); // Ensure that the `AbortSignal` listener is removed.
|
||||
eventListeners.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: Only used in the Firefox built-in pdf viewer.
|
||||
*/
|
||||
class FirefoxEventBus extends EventBus {
|
||||
#externalServices;
|
||||
|
||||
#globalEventNames;
|
||||
|
||||
#isInAutomation;
|
||||
|
||||
constructor(globalEventNames, externalServices, isInAutomation) {
|
||||
super();
|
||||
this.#globalEventNames = globalEventNames;
|
||||
this.#externalServices = externalServices;
|
||||
this.#isInAutomation = isInAutomation;
|
||||
}
|
||||
|
||||
dispatch(eventName, data) {
|
||||
if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("MOZCENTRAL")) {
|
||||
throw new Error("Not implemented: FirefoxEventBus.dispatch");
|
||||
}
|
||||
super.dispatch(eventName, data);
|
||||
|
||||
if (this.#isInAutomation) {
|
||||
const detail = Object.create(null);
|
||||
if (data) {
|
||||
for (const key in data) {
|
||||
const value = data[key];
|
||||
if (key === "source") {
|
||||
if (value === window || value === document) {
|
||||
return; // No need to re-dispatch (already) global events.
|
||||
}
|
||||
continue; // Ignore the `source` property.
|
||||
}
|
||||
detail[key] = value;
|
||||
}
|
||||
}
|
||||
const event = new CustomEvent(eventName, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
detail,
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
if (this.#globalEventNames?.has(eventName)) {
|
||||
this.#externalServices.dispatchGlobalEvent({
|
||||
eventName,
|
||||
detail: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { EventBus, FirefoxEventBus, waitOnEventOrTimeout, WaitOnType };
|
||||
58
web/external_services.js
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.
|
||||
*/
|
||||
|
||||
/** @typedef {import("./interfaces.js").IL10n} IL10n */
|
||||
|
||||
class BaseExternalServices {
|
||||
constructor() {
|
||||
if (
|
||||
(typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
|
||||
this.constructor === BaseExternalServices
|
||||
) {
|
||||
throw new Error("Cannot initialize BaseExternalServices.");
|
||||
}
|
||||
}
|
||||
|
||||
updateFindControlState(data) {}
|
||||
|
||||
updateFindMatchesCount(data) {}
|
||||
|
||||
initPassiveLoading() {}
|
||||
|
||||
reportTelemetry(data) {}
|
||||
|
||||
/**
|
||||
* @returns {Promise<IL10n>}
|
||||
*/
|
||||
async createL10n() {
|
||||
throw new Error("Not implemented: createL10n");
|
||||
}
|
||||
|
||||
createScripting() {
|
||||
throw new Error("Not implemented: createScripting");
|
||||
}
|
||||
|
||||
createSignatureStorage() {
|
||||
throw new Error("Not implemented: createSignatureStorage");
|
||||
}
|
||||
|
||||
updateEditorStates(data) {
|
||||
throw new Error("Not implemented: updateEditorStates");
|
||||
}
|
||||
|
||||
dispatchGlobalEvent(_event) {}
|
||||
}
|
||||
|
||||
export { BaseExternalServices };
|
||||
211
web/firefox_print_service.js
Normal file
@@ -0,0 +1,211 @@
|
||||
/* Copyright 2016 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 {
|
||||
AnnotationMode,
|
||||
PixelsPerInch,
|
||||
RenderingCancelledException,
|
||||
shadow,
|
||||
} from "pdfjs-lib";
|
||||
import { getXfaHtmlForPrinting } from "./print_utils.js";
|
||||
|
||||
// Creates a placeholder with div and canvas with right size for the page.
|
||||
function composePage(
|
||||
pdfDocument,
|
||||
pageNumber,
|
||||
size,
|
||||
printContainer,
|
||||
printResolution,
|
||||
optionalContentConfigPromise,
|
||||
printAnnotationStoragePromise
|
||||
) {
|
||||
const canvas = document.createElement("canvas");
|
||||
|
||||
// The size of the canvas in pixels for printing.
|
||||
const PRINT_UNITS = printResolution / PixelsPerInch.PDF;
|
||||
canvas.width = Math.floor(size.width * PRINT_UNITS);
|
||||
canvas.height = Math.floor(size.height * PRINT_UNITS);
|
||||
|
||||
const canvasWrapper = document.createElement("div");
|
||||
canvasWrapper.className = "printedPage";
|
||||
canvasWrapper.append(canvas);
|
||||
printContainer.append(canvasWrapper);
|
||||
|
||||
// A callback for a given page may be executed multiple times for different
|
||||
// print operations (think of changing the print settings in the browser).
|
||||
//
|
||||
// Since we don't support queueing multiple render tasks for the same page
|
||||
// (and it'd be racy anyways if painting the page is not done in one go) we
|
||||
// keep track of the last scheduled task in order to properly cancel it before
|
||||
// starting the next one.
|
||||
let currentRenderTask = null;
|
||||
canvas.mozPrintCallback = function (obj) {
|
||||
// Printing/rendering the page.
|
||||
const ctx = obj.context;
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = "rgb(255, 255, 255)";
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.restore();
|
||||
|
||||
let thisRenderTask = null;
|
||||
|
||||
Promise.all([
|
||||
pdfDocument.getPage(pageNumber),
|
||||
printAnnotationStoragePromise,
|
||||
])
|
||||
.then(function ([pdfPage, printAnnotationStorage]) {
|
||||
if (currentRenderTask) {
|
||||
currentRenderTask.cancel();
|
||||
currentRenderTask = null;
|
||||
}
|
||||
const renderContext = {
|
||||
canvasContext: ctx,
|
||||
canvas: null,
|
||||
transform: [PRINT_UNITS, 0, 0, PRINT_UNITS, 0, 0],
|
||||
viewport: pdfPage.getViewport({ scale: 1, rotation: size.rotation }),
|
||||
intent: "print",
|
||||
annotationMode: AnnotationMode.ENABLE_STORAGE,
|
||||
optionalContentConfigPromise,
|
||||
printAnnotationStorage,
|
||||
};
|
||||
currentRenderTask = thisRenderTask = pdfPage.render(renderContext);
|
||||
return thisRenderTask.promise;
|
||||
})
|
||||
.then(
|
||||
function () {
|
||||
// Tell the printEngine that rendering this canvas/page has finished.
|
||||
if (currentRenderTask === thisRenderTask) {
|
||||
currentRenderTask = null;
|
||||
}
|
||||
obj.done();
|
||||
},
|
||||
function (reason) {
|
||||
if (!(reason instanceof RenderingCancelledException)) {
|
||||
console.error(reason);
|
||||
}
|
||||
|
||||
if (currentRenderTask === thisRenderTask) {
|
||||
currentRenderTask.cancel();
|
||||
currentRenderTask = null;
|
||||
}
|
||||
|
||||
// Tell the printEngine that rendering this canvas/page has failed.
|
||||
// This will make the print process stop.
|
||||
if ("abort" in obj) {
|
||||
obj.abort();
|
||||
} else {
|
||||
obj.done();
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
class FirefoxPrintService {
|
||||
constructor({
|
||||
pdfDocument,
|
||||
pagesOverview,
|
||||
printContainer,
|
||||
printResolution,
|
||||
printAnnotationStoragePromise = null,
|
||||
}) {
|
||||
this.pdfDocument = pdfDocument;
|
||||
this.pagesOverview = pagesOverview;
|
||||
this.printContainer = printContainer;
|
||||
this._printResolution = printResolution || 150;
|
||||
this._optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({
|
||||
intent: "print",
|
||||
});
|
||||
this._printAnnotationStoragePromise =
|
||||
printAnnotationStoragePromise || Promise.resolve();
|
||||
}
|
||||
|
||||
layout() {
|
||||
const {
|
||||
pdfDocument,
|
||||
pagesOverview,
|
||||
printContainer,
|
||||
_printResolution,
|
||||
_optionalContentConfigPromise,
|
||||
_printAnnotationStoragePromise,
|
||||
} = this;
|
||||
|
||||
const body = document.querySelector("body");
|
||||
body.setAttribute("data-pdfjsprinting", true);
|
||||
|
||||
const { width, height } = this.pagesOverview[0];
|
||||
const hasEqualPageSizes = this.pagesOverview.every(
|
||||
size => size.width === width && size.height === height
|
||||
);
|
||||
if (!hasEqualPageSizes) {
|
||||
console.warn(
|
||||
"Not all pages have the same size. The printed result may be incorrect!"
|
||||
);
|
||||
}
|
||||
|
||||
// Insert a @page + size rule to make sure that the page size is correctly
|
||||
// set. Note that we assume that all pages have the same size, because
|
||||
// variable-size pages are scaled down to the initial page size in Firefox.
|
||||
this.pageStyleSheet = document.createElement("style");
|
||||
this.pageStyleSheet.textContent = `@page { size: ${width}pt ${height}pt;}`;
|
||||
body.append(this.pageStyleSheet);
|
||||
|
||||
if (pdfDocument.isPureXfa) {
|
||||
getXfaHtmlForPrinting(printContainer, pdfDocument);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0, ii = pagesOverview.length; i < ii; ++i) {
|
||||
composePage(
|
||||
pdfDocument,
|
||||
/* pageNumber = */ i + 1,
|
||||
pagesOverview[i],
|
||||
printContainer,
|
||||
_printResolution,
|
||||
_optionalContentConfigPromise,
|
||||
_printAnnotationStoragePromise
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.printContainer.textContent = "";
|
||||
|
||||
const body = document.querySelector("body");
|
||||
body.removeAttribute("data-pdfjsprinting");
|
||||
|
||||
if (this.pageStyleSheet) {
|
||||
this.pageStyleSheet.remove();
|
||||
this.pageStyleSheet = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @implements {IPDFPrintServiceFactory}
|
||||
*/
|
||||
class PDFPrintServiceFactory {
|
||||
static get supportsPrinting() {
|
||||
const canvas = document.createElement("canvas");
|
||||
return shadow(this, "supportsPrinting", "mozPrintCallback" in canvas);
|
||||
}
|
||||
|
||||
static createPrintService(params) {
|
||||
return new FirefoxPrintService(params);
|
||||
}
|
||||
}
|
||||
|
||||
export { PDFPrintServiceFactory };
|
||||
670
web/firefoxcom.js
Normal file
@@ -0,0 +1,670 @@
|
||||
/* Copyright 2012 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 { isPdfFile, PDFDataRangeTransport } from "pdfjs-lib";
|
||||
import { AppOptions } from "./app_options.js";
|
||||
import { BaseExternalServices } from "./external_services.js";
|
||||
import { BasePreferences } from "./preferences.js";
|
||||
import { DEFAULT_SCALE_VALUE } from "./ui_utils.js";
|
||||
import { L10n } from "./l10n.js";
|
||||
|
||||
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
|
||||
throw new Error(
|
||||
'Module "./firefoxcom.js" shall not be used outside MOZCENTRAL builds.'
|
||||
);
|
||||
}
|
||||
|
||||
let viewerApp = { initialized: false };
|
||||
function initCom(app) {
|
||||
viewerApp = app;
|
||||
}
|
||||
|
||||
class FirefoxCom {
|
||||
/**
|
||||
* Creates an event that the extension is listening for and will
|
||||
* asynchronously respond to.
|
||||
* @param {string} action - The action to trigger.
|
||||
* @param {Object|string} [data] - The data to send.
|
||||
* @returns {Promise<any>} A promise that is resolved with the response data.
|
||||
*/
|
||||
static requestAsync(action, data) {
|
||||
return new Promise(resolve => {
|
||||
this.request(action, data, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event that the extension is listening for and will, optionally,
|
||||
* asynchronously respond to.
|
||||
* @param {string} action - The action to trigger.
|
||||
* @param {Object|string} [data] - The data to send.
|
||||
*/
|
||||
static request(action, data, callback = null) {
|
||||
const request = document.createTextNode("");
|
||||
if (callback) {
|
||||
request.addEventListener(
|
||||
"pdf.js.response",
|
||||
event => {
|
||||
const response = event.detail.response;
|
||||
event.target.remove();
|
||||
|
||||
callback(response);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
document.documentElement.append(request);
|
||||
|
||||
const sender = new CustomEvent("pdf.js.message", {
|
||||
bubbles: true,
|
||||
cancelable: false,
|
||||
detail: {
|
||||
action,
|
||||
data,
|
||||
responseExpected: !!callback,
|
||||
},
|
||||
});
|
||||
request.dispatchEvent(sender);
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadManager {
|
||||
#openBlobUrls = new WeakMap();
|
||||
|
||||
downloadData(data, filename, contentType) {
|
||||
const blobUrl = URL.createObjectURL(
|
||||
new Blob([data], { type: contentType })
|
||||
);
|
||||
|
||||
FirefoxCom.request("download", {
|
||||
blobUrl,
|
||||
originalUrl: blobUrl,
|
||||
filename,
|
||||
isAttachment: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} Indicating if the data was opened.
|
||||
*/
|
||||
openOrDownloadData(data, filename, dest = null) {
|
||||
const isPdfData = isPdfFile(filename);
|
||||
const contentType = isPdfData ? "application/pdf" : "";
|
||||
|
||||
if (isPdfData) {
|
||||
let blobUrl = this.#openBlobUrls.get(data);
|
||||
if (!blobUrl) {
|
||||
blobUrl = URL.createObjectURL(new Blob([data], { type: contentType }));
|
||||
this.#openBlobUrls.set(data, blobUrl);
|
||||
}
|
||||
// Let Firefox's content handler catch the URL and display the PDF.
|
||||
// NOTE: This cannot use a query string for the filename, see
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1632644#c5
|
||||
let viewerUrl = blobUrl + "#filename=" + encodeURIComponent(filename);
|
||||
if (dest) {
|
||||
viewerUrl += `&filedest=${escape(dest)}`;
|
||||
}
|
||||
|
||||
try {
|
||||
window.open(viewerUrl);
|
||||
return true;
|
||||
} catch (ex) {
|
||||
console.error("openOrDownloadData:", ex);
|
||||
// Release the `blobUrl`, since opening it failed, and fallback to
|
||||
// downloading the PDF file.
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
this.#openBlobUrls.delete(data);
|
||||
}
|
||||
}
|
||||
|
||||
this.downloadData(data, filename, contentType);
|
||||
return false;
|
||||
}
|
||||
|
||||
download(data, url, filename) {
|
||||
const blobUrl = data
|
||||
? URL.createObjectURL(new Blob([data], { type: "application/pdf" }))
|
||||
: null;
|
||||
|
||||
FirefoxCom.request("download", {
|
||||
blobUrl,
|
||||
originalUrl: url,
|
||||
filename,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Preferences extends BasePreferences {
|
||||
async _readFromStorage(prefObj) {
|
||||
return FirefoxCom.requestAsync("getPreferences", prefObj);
|
||||
}
|
||||
|
||||
async _writeToStorage(prefObj) {
|
||||
return FirefoxCom.requestAsync("setPreferences", prefObj);
|
||||
}
|
||||
}
|
||||
|
||||
(function listenFindEvents() {
|
||||
const events = [
|
||||
"find",
|
||||
"findagain",
|
||||
"findhighlightallchange",
|
||||
"findcasesensitivitychange",
|
||||
"findentirewordchange",
|
||||
"findbarclose",
|
||||
"finddiacriticmatchingchange",
|
||||
];
|
||||
const findLen = "find".length;
|
||||
|
||||
const handleEvent = function ({ type, detail }) {
|
||||
if (!viewerApp.initialized) {
|
||||
return;
|
||||
}
|
||||
if (type === "findbarclose") {
|
||||
viewerApp.eventBus.dispatch(type, { source: window });
|
||||
return;
|
||||
}
|
||||
viewerApp.eventBus.dispatch("find", {
|
||||
source: window,
|
||||
type: type.substring(findLen),
|
||||
query: detail.query,
|
||||
caseSensitive: !!detail.caseSensitive,
|
||||
entireWord: !!detail.entireWord,
|
||||
highlightAll: !!detail.highlightAll,
|
||||
findPrevious: !!detail.findPrevious,
|
||||
matchDiacritics: !!detail.matchDiacritics,
|
||||
});
|
||||
};
|
||||
|
||||
for (const event of events) {
|
||||
window.addEventListener(event, handleEvent);
|
||||
}
|
||||
})();
|
||||
|
||||
(function listenZoomEvents() {
|
||||
const events = ["zoomin", "zoomout", "zoomreset"];
|
||||
const handleEvent = function ({ type, detail }) {
|
||||
if (!viewerApp.initialized) {
|
||||
return;
|
||||
}
|
||||
// Avoid attempting to needlessly reset the zoom level *twice* in a row,
|
||||
// when using the `Ctrl + 0` keyboard shortcut.
|
||||
if (
|
||||
type === "zoomreset" &&
|
||||
viewerApp.pdfViewer.currentScaleValue === DEFAULT_SCALE_VALUE
|
||||
) {
|
||||
return;
|
||||
}
|
||||
viewerApp.eventBus.dispatch(type, { source: window });
|
||||
};
|
||||
|
||||
for (const event of events) {
|
||||
window.addEventListener(event, handleEvent);
|
||||
}
|
||||
})();
|
||||
|
||||
(function listenSaveEvent() {
|
||||
const handleEvent = function ({ type, detail }) {
|
||||
if (!viewerApp.initialized) {
|
||||
return;
|
||||
}
|
||||
viewerApp.eventBus.dispatch("download", { source: window });
|
||||
};
|
||||
|
||||
window.addEventListener("save", handleEvent);
|
||||
})();
|
||||
|
||||
(function listenEditingEvent() {
|
||||
const handleEvent = function ({ detail }) {
|
||||
if (!viewerApp.initialized) {
|
||||
return;
|
||||
}
|
||||
viewerApp.eventBus.dispatch("editingaction", {
|
||||
source: window,
|
||||
name: detail.name,
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("editingaction", handleEvent);
|
||||
})();
|
||||
|
||||
if (PDFJSDev.test("GECKOVIEW")) {
|
||||
(function listenQueryEvents() {
|
||||
window.addEventListener("pdf.js.query", async ({ detail: { queryId } }) => {
|
||||
let result = null;
|
||||
if (viewerApp.initialized && queryId === "canDownloadInsteadOfPrint") {
|
||||
result = false;
|
||||
const { pdfDocument, pdfViewer } = viewerApp;
|
||||
if (pdfDocument) {
|
||||
try {
|
||||
const hasUnchangedAnnotations =
|
||||
pdfDocument.annotationStorage.size === 0;
|
||||
// WillPrint is called just before printing the document and could
|
||||
// lead to have modified annotations.
|
||||
const hasWillPrint =
|
||||
pdfViewer.enableScripting &&
|
||||
!!(await pdfDocument.getJSActions())?.WillPrint;
|
||||
|
||||
result = hasUnchangedAnnotations && !hasWillPrint;
|
||||
} catch {
|
||||
console.warn("Unable to check if the document can be downloaded.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("pdf.js.query.answer", {
|
||||
bubbles: true,
|
||||
cancelable: false,
|
||||
detail: {
|
||||
queryId,
|
||||
value: result,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
class FirefoxComDataRangeTransport extends PDFDataRangeTransport {
|
||||
requestDataRange(begin, end) {
|
||||
FirefoxCom.request("requestDataRange", { begin, end });
|
||||
}
|
||||
|
||||
// NOTE: This method is currently not invoked in the Firefox PDF Viewer.
|
||||
abort() {
|
||||
FirefoxCom.request("abortLoading", null);
|
||||
}
|
||||
}
|
||||
|
||||
class FirefoxScripting {
|
||||
static async createSandbox(data) {
|
||||
const success = await FirefoxCom.requestAsync("createSandbox", data);
|
||||
if (!success) {
|
||||
throw new Error("Cannot create sandbox.");
|
||||
}
|
||||
}
|
||||
|
||||
static async dispatchEventInSandbox(event) {
|
||||
FirefoxCom.request("dispatchEventInSandbox", event);
|
||||
}
|
||||
|
||||
static async destroySandbox() {
|
||||
FirefoxCom.request("destroySandbox", null);
|
||||
}
|
||||
}
|
||||
|
||||
class MLManager {
|
||||
#abortSignal = null;
|
||||
|
||||
#enabled = null;
|
||||
|
||||
#eventBus = null;
|
||||
|
||||
#ready = null;
|
||||
|
||||
#requestResolvers = null;
|
||||
|
||||
hasProgress = false;
|
||||
|
||||
static #AI_ALT_TEXT_MODEL_NAME = "moz-image-to-text";
|
||||
|
||||
constructor({
|
||||
altTextLearnMoreUrl,
|
||||
enableGuessAltText,
|
||||
enableAltTextModelDownload,
|
||||
}) {
|
||||
// The `altTextLearnMoreUrl` is used to provide a link to the user to learn
|
||||
// more about the "alt text" feature.
|
||||
// The link is used in the Alt Text dialog or in the Image Settings.
|
||||
this.altTextLearnMoreUrl = altTextLearnMoreUrl;
|
||||
this.enableAltTextModelDownload = enableAltTextModelDownload;
|
||||
this.enableGuessAltText = enableGuessAltText;
|
||||
}
|
||||
|
||||
setEventBus(eventBus, abortSignal) {
|
||||
this.#eventBus = eventBus;
|
||||
this.#abortSignal = abortSignal;
|
||||
eventBus._on(
|
||||
"enablealttextmodeldownload",
|
||||
({ value }) => {
|
||||
if (this.enableAltTextModelDownload === value) {
|
||||
return;
|
||||
}
|
||||
if (value) {
|
||||
this.downloadModel("altText");
|
||||
} else {
|
||||
this.deleteModel("altText");
|
||||
}
|
||||
},
|
||||
{ signal: abortSignal }
|
||||
);
|
||||
eventBus._on(
|
||||
"enableguessalttext",
|
||||
({ value }) => {
|
||||
this.toggleService("altText", value);
|
||||
},
|
||||
{ signal: abortSignal }
|
||||
);
|
||||
}
|
||||
|
||||
async isEnabledFor(name) {
|
||||
return this.enableGuessAltText && !!(await this.#enabled?.get(name));
|
||||
}
|
||||
|
||||
isReady(name) {
|
||||
return this.#ready?.has(name) ?? false;
|
||||
}
|
||||
|
||||
async deleteModel(name) {
|
||||
if (name !== "altText" || !this.enableAltTextModelDownload) {
|
||||
return;
|
||||
}
|
||||
this.enableAltTextModelDownload = false;
|
||||
this.#ready?.delete(name);
|
||||
this.#enabled?.delete(name);
|
||||
await this.toggleService("altText", false);
|
||||
await FirefoxCom.requestAsync(
|
||||
"mlDelete",
|
||||
MLManager.#AI_ALT_TEXT_MODEL_NAME
|
||||
);
|
||||
}
|
||||
|
||||
async loadModel(name) {
|
||||
if (name === "altText" && this.enableAltTextModelDownload) {
|
||||
await this.#loadAltTextEngine(false);
|
||||
}
|
||||
}
|
||||
|
||||
async downloadModel(name) {
|
||||
if (name !== "altText" || this.enableAltTextModelDownload) {
|
||||
return null;
|
||||
}
|
||||
this.enableAltTextModelDownload = true;
|
||||
return this.#loadAltTextEngine(true);
|
||||
}
|
||||
|
||||
async guess(data) {
|
||||
if (data?.name !== "altText") {
|
||||
return null;
|
||||
}
|
||||
const resolvers = (this.#requestResolvers ||= new Set());
|
||||
const resolver = Promise.withResolvers();
|
||||
resolvers.add(resolver);
|
||||
|
||||
data.service = MLManager.#AI_ALT_TEXT_MODEL_NAME;
|
||||
|
||||
FirefoxCom.requestAsync("mlGuess", data)
|
||||
.then(response => {
|
||||
if (resolvers.has(resolver)) {
|
||||
resolver.resolve(response);
|
||||
resolvers.delete(resolver);
|
||||
}
|
||||
})
|
||||
.catch(reason => {
|
||||
if (resolvers.has(resolver)) {
|
||||
resolver.reject(reason);
|
||||
resolvers.delete(resolver);
|
||||
}
|
||||
});
|
||||
|
||||
return resolver.promise;
|
||||
}
|
||||
|
||||
async #cancelAllRequests() {
|
||||
if (!this.#requestResolvers) {
|
||||
return;
|
||||
}
|
||||
for (const resolver of this.#requestResolvers) {
|
||||
resolver.resolve({ cancel: true });
|
||||
}
|
||||
this.#requestResolvers.clear();
|
||||
this.#requestResolvers = null;
|
||||
}
|
||||
|
||||
async toggleService(name, enabled) {
|
||||
if (name !== "altText" || this.enableGuessAltText === enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.enableGuessAltText = enabled;
|
||||
if (enabled) {
|
||||
if (this.enableAltTextModelDownload) {
|
||||
await this.#loadAltTextEngine(false);
|
||||
}
|
||||
} else {
|
||||
this.#cancelAllRequests();
|
||||
}
|
||||
}
|
||||
|
||||
async #loadAltTextEngine(listenToProgress) {
|
||||
if (this.#enabled?.has("altText")) {
|
||||
// We already have a promise for the "altText" service.
|
||||
return;
|
||||
}
|
||||
this.#ready ||= new Set();
|
||||
const promise = FirefoxCom.requestAsync("loadAIEngine", {
|
||||
service: MLManager.#AI_ALT_TEXT_MODEL_NAME,
|
||||
listenToProgress,
|
||||
}).then(ok => {
|
||||
if (ok) {
|
||||
this.#ready.add("altText");
|
||||
}
|
||||
return ok;
|
||||
});
|
||||
(this.#enabled ||= new Map()).set("altText", promise);
|
||||
if (listenToProgress) {
|
||||
const ac = new AbortController();
|
||||
const signal = AbortSignal.any([this.#abortSignal, ac.signal]);
|
||||
|
||||
this.hasProgress = true;
|
||||
window.addEventListener(
|
||||
"loadAIEngineProgress",
|
||||
({ detail }) => {
|
||||
this.#eventBus.dispatch("loadaiengineprogress", {
|
||||
source: this,
|
||||
detail,
|
||||
});
|
||||
if (detail.finished) {
|
||||
ac.abort();
|
||||
this.hasProgress = false;
|
||||
}
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
promise.then(ok => {
|
||||
if (!ok) {
|
||||
ac.abort();
|
||||
this.hasProgress = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
await promise;
|
||||
}
|
||||
}
|
||||
|
||||
class SignatureStorage {
|
||||
#eventBus = null;
|
||||
|
||||
#signatures = null;
|
||||
|
||||
#signal = null;
|
||||
|
||||
constructor(eventBus, signal) {
|
||||
this.#eventBus = eventBus;
|
||||
this.#signal = signal;
|
||||
}
|
||||
|
||||
#handleSignature(data) {
|
||||
return FirefoxCom.requestAsync("handleSignature", data);
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
if (this.#signal) {
|
||||
window.addEventListener(
|
||||
"storedSignaturesChanged",
|
||||
() => {
|
||||
this.#signatures = null;
|
||||
this.#eventBus?.dispatch("storedsignatureschanged", { source: this });
|
||||
},
|
||||
{ signal: this.#signal }
|
||||
);
|
||||
this.#signal = null;
|
||||
}
|
||||
if (!this.#signatures) {
|
||||
this.#signatures = new Map();
|
||||
const data = await this.#handleSignature({ action: "get" });
|
||||
if (data) {
|
||||
for (const { uuid, description, signatureData } of data) {
|
||||
this.#signatures.set(uuid, { description, signatureData });
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.#signatures;
|
||||
}
|
||||
|
||||
async isFull() {
|
||||
// We want to store at most 5 signatures.
|
||||
return (await this.size()) === 5;
|
||||
}
|
||||
|
||||
async size() {
|
||||
return (await this.getAll()).size;
|
||||
}
|
||||
|
||||
async create(data) {
|
||||
if (await this.isFull()) {
|
||||
return null;
|
||||
}
|
||||
const uuid = await this.#handleSignature({
|
||||
action: "create",
|
||||
...data,
|
||||
});
|
||||
if (!uuid) {
|
||||
return null;
|
||||
}
|
||||
this.#signatures.set(uuid, data);
|
||||
return uuid;
|
||||
}
|
||||
|
||||
async delete(uuid) {
|
||||
const signatures = await this.getAll();
|
||||
if (!signatures.has(uuid)) {
|
||||
return false;
|
||||
}
|
||||
if (await this.#handleSignature({ action: "delete", uuid })) {
|
||||
signatures.delete(uuid);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class ExternalServices extends BaseExternalServices {
|
||||
updateFindControlState(data) {
|
||||
FirefoxCom.request("updateFindControlState", data);
|
||||
}
|
||||
|
||||
updateFindMatchesCount(data) {
|
||||
FirefoxCom.request("updateFindMatchesCount", data);
|
||||
}
|
||||
|
||||
initPassiveLoading() {
|
||||
let pdfDataRangeTransport;
|
||||
|
||||
window.addEventListener("message", function windowMessage(e) {
|
||||
if (e.source !== null) {
|
||||
// The message MUST originate from Chrome code.
|
||||
console.warn("Rejected untrusted message from " + e.origin);
|
||||
return;
|
||||
}
|
||||
const args = e.data;
|
||||
|
||||
if (typeof args !== "object" || !("pdfjsLoadAction" in args)) {
|
||||
return;
|
||||
}
|
||||
switch (args.pdfjsLoadAction) {
|
||||
case "supportsRangedLoading":
|
||||
if (args.done && !args.data) {
|
||||
viewerApp._documentError(null);
|
||||
break;
|
||||
}
|
||||
pdfDataRangeTransport = new FirefoxComDataRangeTransport(
|
||||
args.length,
|
||||
args.data,
|
||||
args.done,
|
||||
args.filename
|
||||
);
|
||||
|
||||
viewerApp.open({ range: pdfDataRangeTransport });
|
||||
break;
|
||||
case "range":
|
||||
pdfDataRangeTransport.onDataRange(args.begin, args.chunk);
|
||||
break;
|
||||
case "rangeProgress":
|
||||
pdfDataRangeTransport.onDataProgress(args.loaded);
|
||||
break;
|
||||
case "progressiveRead":
|
||||
pdfDataRangeTransport.onDataProgressiveRead(args.chunk);
|
||||
|
||||
// Don't forget to report loading progress as well, since otherwise
|
||||
// the loadingBar won't update when `disableRange=true` is set.
|
||||
pdfDataRangeTransport.onDataProgress(args.loaded, args.total);
|
||||
break;
|
||||
case "progressiveDone":
|
||||
pdfDataRangeTransport?.onDataProgressiveDone();
|
||||
break;
|
||||
case "progress":
|
||||
viewerApp.progress(args.loaded / args.total);
|
||||
break;
|
||||
case "complete":
|
||||
if (!args.data) {
|
||||
viewerApp._documentError(null, { message: args.errorCode });
|
||||
break;
|
||||
}
|
||||
viewerApp.open({ data: args.data, filename: args.filename });
|
||||
break;
|
||||
}
|
||||
});
|
||||
FirefoxCom.request("initPassiveLoading", null);
|
||||
}
|
||||
|
||||
reportTelemetry(data) {
|
||||
FirefoxCom.request("reportTelemetry", data);
|
||||
}
|
||||
|
||||
updateEditorStates(data) {
|
||||
FirefoxCom.request("updateEditorStates", data);
|
||||
}
|
||||
|
||||
async createL10n() {
|
||||
await document.l10n.ready;
|
||||
return new L10n(AppOptions.get("localeProperties"), document.l10n);
|
||||
}
|
||||
|
||||
createScripting() {
|
||||
return FirefoxScripting;
|
||||
}
|
||||
|
||||
createSignatureStorage(eventBus, signal) {
|
||||
return new SignatureStorage(eventBus, signal);
|
||||
}
|
||||
|
||||
dispatchGlobalEvent(event) {
|
||||
FirefoxCom.request("dispatchGlobalEvent", event);
|
||||
}
|
||||
}
|
||||
|
||||
export { DownloadManager, ExternalServices, initCom, MLManager, Preferences };
|
||||
67
web/generic_scripting.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/* Copyright 2020 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 { getPdfFilenameFromUrl } from "pdfjs-lib";
|
||||
|
||||
async function docProperties(pdfDocument) {
|
||||
const url = "",
|
||||
baseUrl = "";
|
||||
const { info, metadata, contentDispositionFilename, contentLength } =
|
||||
await pdfDocument.getMetadata();
|
||||
|
||||
return {
|
||||
...info,
|
||||
baseURL: baseUrl,
|
||||
filesize: contentLength || (await pdfDocument.getDownloadInfo()).length,
|
||||
filename: contentDispositionFilename || getPdfFilenameFromUrl(url),
|
||||
metadata: metadata?.getRaw(),
|
||||
authors: metadata?.get("dc:creator"),
|
||||
numPages: pdfDocument.numPages,
|
||||
URL: url,
|
||||
};
|
||||
}
|
||||
|
||||
class GenericScripting {
|
||||
constructor(sandboxBundleSrc) {
|
||||
this._ready = new Promise((resolve, reject) => {
|
||||
const sandbox =
|
||||
typeof PDFJSDev === "undefined"
|
||||
? import(sandboxBundleSrc) // eslint-disable-line no-unsanitized/method
|
||||
: __raw_import__(sandboxBundleSrc);
|
||||
sandbox
|
||||
.then(pdfjsSandbox => {
|
||||
resolve(pdfjsSandbox.QuickJSSandbox());
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
async createSandbox(data) {
|
||||
const sandbox = await this._ready;
|
||||
sandbox.create(data);
|
||||
}
|
||||
|
||||
async dispatchEventInSandbox(event) {
|
||||
const sandbox = await this._ready;
|
||||
setTimeout(() => sandbox.dispatchEvent(event), 0);
|
||||
}
|
||||
|
||||
async destroySandbox() {
|
||||
const sandbox = await this._ready;
|
||||
sandbox.nukeSandbox();
|
||||
}
|
||||
}
|
||||
|
||||
export { docProperties, GenericScripting };
|
||||
103
web/generic_signature_storage.js
Normal file
@@ -0,0 +1,103 @@
|
||||
/* Copyright 2025 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 { getUuid } from "pdfjs-lib";
|
||||
|
||||
const KEY_STORAGE = "pdfjs.signature";
|
||||
|
||||
class SignatureStorage {
|
||||
// TODO: Encrypt the data in using a password and add a UI for entering it.
|
||||
// We could use the Web Crypto API for this (see https://bradyjoslin.com/blog/encryption-webcrypto/
|
||||
// for an example).
|
||||
|
||||
#eventBus;
|
||||
|
||||
#signatures = null;
|
||||
|
||||
#signal = null;
|
||||
|
||||
constructor(eventBus, signal) {
|
||||
this.#eventBus = eventBus;
|
||||
this.#signal = signal;
|
||||
}
|
||||
|
||||
#save() {
|
||||
localStorage.setItem(
|
||||
KEY_STORAGE,
|
||||
JSON.stringify(Object.fromEntries(this.#signatures))
|
||||
);
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
if (this.#signal) {
|
||||
window.addEventListener(
|
||||
"storage",
|
||||
({ key }) => {
|
||||
if (key === KEY_STORAGE) {
|
||||
this.#signatures = null;
|
||||
this.#eventBus?.dispatch("storedsignatureschanged", {
|
||||
source: this,
|
||||
});
|
||||
}
|
||||
},
|
||||
{ signal: this.#signal }
|
||||
);
|
||||
this.#signal = null;
|
||||
}
|
||||
if (!this.#signatures) {
|
||||
this.#signatures = new Map();
|
||||
const data = localStorage.getItem(KEY_STORAGE);
|
||||
if (data) {
|
||||
for (const [key, value] of Object.entries(JSON.parse(data))) {
|
||||
this.#signatures.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.#signatures;
|
||||
}
|
||||
|
||||
async isFull() {
|
||||
// Only allow 5 signatures to be saved.
|
||||
return (await this.size()) === 5;
|
||||
}
|
||||
|
||||
async size() {
|
||||
return (await this.getAll()).size;
|
||||
}
|
||||
|
||||
async create(data) {
|
||||
if (await this.isFull()) {
|
||||
return null;
|
||||
}
|
||||
const uuid = getUuid();
|
||||
this.#signatures.set(uuid, data);
|
||||
this.#save();
|
||||
|
||||
return uuid;
|
||||
}
|
||||
|
||||
async delete(uuid) {
|
||||
const signatures = await this.getAll();
|
||||
if (!signatures.has(uuid)) {
|
||||
return false;
|
||||
}
|
||||
signatures.delete(uuid);
|
||||
this.#save();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export { SignatureStorage };
|
||||
155
web/genericcom.js
Normal file
@@ -0,0 +1,155 @@
|
||||
/* Copyright 2017 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 } from "./app_options.js";
|
||||
import { BaseExternalServices } from "./external_services.js";
|
||||
import { BasePreferences } from "./preferences.js";
|
||||
import { GenericL10n } from "./genericl10n.js";
|
||||
import { GenericScripting } from "./generic_scripting.js";
|
||||
import { SignatureStorage } from "./generic_signature_storage.js";
|
||||
|
||||
if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("GENERIC")) {
|
||||
throw new Error(
|
||||
'Module "pdfjs-web/genericcom" shall not be used outside GENERIC build.'
|
||||
);
|
||||
}
|
||||
|
||||
function initCom(app) {}
|
||||
|
||||
class Preferences extends BasePreferences {
|
||||
async _writeToStorage(prefObj) {
|
||||
localStorage.setItem("pdfjs.preferences", JSON.stringify(prefObj));
|
||||
}
|
||||
|
||||
async _readFromStorage(prefObj) {
|
||||
return { prefs: JSON.parse(localStorage.getItem("pdfjs.preferences")) };
|
||||
}
|
||||
}
|
||||
|
||||
class ExternalServices extends BaseExternalServices {
|
||||
async createL10n() {
|
||||
return new GenericL10n(AppOptions.get("localeProperties")?.lang);
|
||||
}
|
||||
|
||||
createScripting() {
|
||||
return new GenericScripting(AppOptions.get("sandboxBundleSrc"));
|
||||
}
|
||||
|
||||
createSignatureStorage(eventBus, signal) {
|
||||
return new SignatureStorage(eventBus, signal);
|
||||
}
|
||||
}
|
||||
|
||||
class MLManager {
|
||||
static {
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
|
||||
this.getFakeMLManager = options => new FakeMLManager(options);
|
||||
}
|
||||
}
|
||||
|
||||
async isEnabledFor(_name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
async deleteModel(_service) {
|
||||
return null;
|
||||
}
|
||||
|
||||
isReady(_name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
guess(_data) {}
|
||||
|
||||
toggleService(_name, _enabled) {}
|
||||
}
|
||||
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
|
||||
// eslint-disable-next-line no-var
|
||||
var FakeMLManager = class {
|
||||
eventBus = null;
|
||||
|
||||
hasProgress = false;
|
||||
|
||||
constructor({ enableGuessAltText, enableAltTextModelDownload }) {
|
||||
this.enableGuessAltText = enableGuessAltText;
|
||||
this.enableAltTextModelDownload = enableAltTextModelDownload;
|
||||
}
|
||||
|
||||
setEventBus(eventBus, abortSignal) {
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
async isEnabledFor(_name) {
|
||||
return this.enableGuessAltText;
|
||||
}
|
||||
|
||||
async deleteModel(_name) {
|
||||
this.enableAltTextModelDownload = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
async loadModel(_name) {}
|
||||
|
||||
async downloadModel(_name) {
|
||||
// Simulate downloading the model but with progress.
|
||||
// The progress can be seen in the new alt-text dialog.
|
||||
this.hasProgress = true;
|
||||
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
const total = 1e8;
|
||||
const end = 1.5 * total;
|
||||
const increment = 5e6;
|
||||
let loaded = 0;
|
||||
const id = setInterval(() => {
|
||||
loaded += increment;
|
||||
if (loaded <= end) {
|
||||
this.eventBus.dispatch("loadaiengineprogress", {
|
||||
source: this,
|
||||
detail: {
|
||||
total,
|
||||
totalLoaded: loaded,
|
||||
finished: loaded + increment >= end,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
clearInterval(id);
|
||||
this.hasProgress = false;
|
||||
this.enableAltTextModelDownload = true;
|
||||
resolve(true);
|
||||
}, 900);
|
||||
return promise;
|
||||
}
|
||||
|
||||
isReady(_name) {
|
||||
return this.enableAltTextModelDownload;
|
||||
}
|
||||
|
||||
guess({ request: { data } }) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(data ? { output: "Fake alt text." } : { error: true });
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
toggleService(_name, enabled) {
|
||||
this.enableGuessAltText = enabled;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export { ExternalServices, initCom, MLManager, Preferences };
|
||||
152
web/genericl10n.js
Normal file
@@ -0,0 +1,152 @@
|
||||
/* Copyright 2017 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.
|
||||
*/
|
||||
|
||||
/** @typedef {import("./interfaces").IL10n} IL10n */
|
||||
|
||||
import { FeatureTest, fetchData } from "pdfjs-lib";
|
||||
import { FluentBundle, FluentResource } from "fluent-bundle";
|
||||
import { DOMLocalization } from "fluent-dom";
|
||||
import { L10n } from "./l10n.js";
|
||||
|
||||
function PLATFORM() {
|
||||
const { isAndroid, isLinux, isMac, isWindows } = FeatureTest.platform;
|
||||
if (isLinux) {
|
||||
return "linux";
|
||||
}
|
||||
if (isWindows) {
|
||||
return "windows";
|
||||
}
|
||||
if (isMac) {
|
||||
return "macos";
|
||||
}
|
||||
if (isAndroid) {
|
||||
return "android";
|
||||
}
|
||||
return "other";
|
||||
}
|
||||
|
||||
function createBundle(lang, text) {
|
||||
const resource = new FluentResource(text);
|
||||
const bundle = new FluentBundle(lang, {
|
||||
functions: { PLATFORM },
|
||||
});
|
||||
const errors = bundle.addResource(resource);
|
||||
if (errors.length) {
|
||||
console.error("L10n errors", errors);
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @implements {IL10n}
|
||||
*/
|
||||
class GenericL10n extends L10n {
|
||||
constructor(lang) {
|
||||
super({ lang });
|
||||
|
||||
const generateBundles = !lang
|
||||
? GenericL10n.#generateBundlesFallback.bind(
|
||||
GenericL10n,
|
||||
this.getLanguage()
|
||||
)
|
||||
: GenericL10n.#generateBundles.bind(
|
||||
GenericL10n,
|
||||
"en-us",
|
||||
this.getLanguage()
|
||||
);
|
||||
this._setL10n(new DOMLocalization([], generateBundles));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the bundles for Fluent.
|
||||
* @param {String} defaultLang - The fallback language to use for
|
||||
* translations.
|
||||
* @param {String} baseLang - The base language to use for translations.
|
||||
*/
|
||||
static async *#generateBundles(defaultLang, baseLang) {
|
||||
const { baseURL, paths } = await this.#getPaths();
|
||||
|
||||
const langs = [baseLang];
|
||||
if (defaultLang !== baseLang) {
|
||||
// Also fallback to the short-format of the base language
|
||||
// (see issue 17269).
|
||||
const shortLang = baseLang.split("-", 1)[0];
|
||||
|
||||
if (shortLang !== baseLang) {
|
||||
langs.push(shortLang);
|
||||
}
|
||||
langs.push(defaultLang);
|
||||
}
|
||||
// Trigger fetching of bundles in parallel, to reduce overall load time.
|
||||
const bundles = langs.map(lang => [
|
||||
lang,
|
||||
this.#createBundle(lang, baseURL, paths),
|
||||
]);
|
||||
|
||||
for (const [lang, bundlePromise] of bundles) {
|
||||
const bundle = await bundlePromise;
|
||||
if (bundle) {
|
||||
yield bundle;
|
||||
} else if (lang === "en-us") {
|
||||
yield this.#createBundleFallback(lang);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async #createBundle(lang, baseURL, paths) {
|
||||
const path = paths[lang];
|
||||
if (!path) {
|
||||
return null;
|
||||
}
|
||||
const url = new URL(path, baseURL);
|
||||
const text = await fetchData(url, /* type = */ "text");
|
||||
|
||||
return createBundle(lang, text);
|
||||
}
|
||||
|
||||
static async #getPaths() {
|
||||
try {
|
||||
const { href } = document.querySelector(`link[type="application/l10n"]`);
|
||||
const paths = await fetchData(href, /* type = */ "json");
|
||||
|
||||
return {
|
||||
baseURL: href.substring(0, href.lastIndexOf("/") + 1) || "./",
|
||||
paths,
|
||||
};
|
||||
} catch {}
|
||||
return { baseURL: "./", paths: Object.create(null) };
|
||||
}
|
||||
|
||||
static async *#generateBundlesFallback(lang) {
|
||||
yield this.#createBundleFallback(lang);
|
||||
}
|
||||
|
||||
static async #createBundleFallback(lang) {
|
||||
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
|
||||
throw new Error("Not implemented: #createBundleFallback");
|
||||
}
|
||||
const text =
|
||||
typeof PDFJSDev === "undefined"
|
||||
? await fetchData(
|
||||
new URL("../l10n/en-US/viewer.ftl", window.location.href),
|
||||
/* type = */ "text"
|
||||
)
|
||||
: PDFJSDev.eval("DEFAULT_FTL");
|
||||
|
||||
return createBundle(lang, text);
|
||||
}
|
||||
}
|
||||
|
||||
export { GenericL10n };
|
||||
176
web/grab_to_pan.js
Normal file
@@ -0,0 +1,176 @@
|
||||
/* Copyright 2013 Rob Wu <rob@robwu.nl>
|
||||
* https://github.com/Rob--W/grab-to-pan.js
|
||||
*
|
||||
* 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 { stopEvent } from "pdfjs-lib";
|
||||
|
||||
// Class name of element which can be grabbed.
|
||||
const CSS_CLASS_GRAB = "grab-to-pan-grab";
|
||||
|
||||
/**
|
||||
* @typedef {Object} GrabToPanOptions
|
||||
* @property {HTMLElement} element
|
||||
*/
|
||||
|
||||
class GrabToPan {
|
||||
#activateAC = null;
|
||||
|
||||
#mouseDownAC = null;
|
||||
|
||||
#scrollAC = null;
|
||||
|
||||
/**
|
||||
* Construct a GrabToPan instance for a given HTML element.
|
||||
* @param {GrabToPanOptions} options
|
||||
*/
|
||||
constructor({ element }) {
|
||||
this.element = element;
|
||||
this.document = element.ownerDocument;
|
||||
|
||||
// This overlay will be inserted in the document when the mouse moves during
|
||||
// a grab operation, to ensure that the cursor has the desired appearance.
|
||||
const overlay = (this.overlay = document.createElement("div"));
|
||||
overlay.className = "grab-to-pan-grabbing";
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a mousedown event to the element to enable grab-detection.
|
||||
*/
|
||||
activate() {
|
||||
if (!this.#activateAC) {
|
||||
this.#activateAC = new AbortController();
|
||||
|
||||
this.element.addEventListener("mousedown", this.#onMouseDown.bind(this), {
|
||||
capture: true,
|
||||
signal: this.#activateAC.signal,
|
||||
});
|
||||
this.element.classList.add(CSS_CLASS_GRAB);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all events. Any pending pan session is immediately stopped.
|
||||
*/
|
||||
deactivate() {
|
||||
if (this.#activateAC) {
|
||||
this.#activateAC.abort();
|
||||
this.#activateAC = null;
|
||||
|
||||
this.#endPan();
|
||||
this.element.classList.remove(CSS_CLASS_GRAB);
|
||||
}
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this.#activateAC) {
|
||||
this.deactivate();
|
||||
} else {
|
||||
this.activate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to not pan if the target element is clicked.
|
||||
* Override this method to change the default behaviour.
|
||||
*
|
||||
* @param {Element} node - The target of the event.
|
||||
* @returns {boolean} Whether to not react to the click event.
|
||||
*/
|
||||
ignoreTarget(node) {
|
||||
// Check whether the clicked element is, a child of, an input element/link.
|
||||
return node.matches(
|
||||
"a[href], a[href] *, input, textarea, button, button *, select, option"
|
||||
);
|
||||
}
|
||||
|
||||
#onMouseDown(event) {
|
||||
if (event.button !== 0 || this.ignoreTarget(event.target)) {
|
||||
return;
|
||||
}
|
||||
if (event.originalTarget) {
|
||||
try {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
event.originalTarget.tagName;
|
||||
} catch {
|
||||
// Mozilla-specific: element is a scrollbar (XUL element)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.scrollLeftStart = this.element.scrollLeft;
|
||||
this.scrollTopStart = this.element.scrollTop;
|
||||
this.clientXStart = event.clientX;
|
||||
this.clientYStart = event.clientY;
|
||||
|
||||
this.#mouseDownAC = new AbortController();
|
||||
const boundEndPan = this.#endPan.bind(this),
|
||||
mouseOpts = { capture: true, signal: this.#mouseDownAC.signal };
|
||||
|
||||
this.document.addEventListener(
|
||||
"mousemove",
|
||||
this.#onMouseMove.bind(this),
|
||||
mouseOpts
|
||||
);
|
||||
this.document.addEventListener("mouseup", boundEndPan, mouseOpts);
|
||||
// When a scroll event occurs before a mousemove, assume that the user
|
||||
// dragged a scrollbar (necessary for Opera Presto, Safari and IE)
|
||||
// (not needed for Chrome/Firefox)
|
||||
this.#scrollAC = new AbortController();
|
||||
|
||||
this.element.addEventListener("scroll", boundEndPan, {
|
||||
capture: true,
|
||||
signal: this.#scrollAC.signal,
|
||||
});
|
||||
stopEvent(event);
|
||||
|
||||
const focusedElement = document.activeElement;
|
||||
if (focusedElement && !focusedElement.contains(event.target)) {
|
||||
focusedElement.blur();
|
||||
}
|
||||
}
|
||||
|
||||
#onMouseMove(event) {
|
||||
this.#scrollAC?.abort();
|
||||
this.#scrollAC = null;
|
||||
|
||||
if (!(event.buttons & 1)) {
|
||||
// The left mouse button is released.
|
||||
this.#endPan();
|
||||
return;
|
||||
}
|
||||
const xDiff = event.clientX - this.clientXStart;
|
||||
const yDiff = event.clientY - this.clientYStart;
|
||||
this.element.scrollTo({
|
||||
top: this.scrollTopStart - yDiff,
|
||||
left: this.scrollLeftStart - xDiff,
|
||||
behavior: "instant",
|
||||
});
|
||||
|
||||
if (!this.overlay.parentNode) {
|
||||
document.body.append(this.overlay);
|
||||
}
|
||||
}
|
||||
|
||||
#endPan() {
|
||||
this.#mouseDownAC?.abort();
|
||||
this.#mouseDownAC = null;
|
||||
this.#scrollAC?.abort();
|
||||
this.#scrollAC = null;
|
||||
// Note: ChildNode.remove doesn't throw if the parentNode is undefined.
|
||||
this.overlay.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export { GrabToPan };
|
||||
3
web/images/altText_add.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="13" viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.375 7.625V11.875C5.375 12.0408 5.44085 12.1997 5.55806 12.3169C5.67527 12.4342 5.83424 12.5 6 12.5C6.16576 12.5 6.32473 12.4342 6.44194 12.3169C6.55915 12.1997 6.625 12.0408 6.625 11.875V7.625L7.125 7.125H11.375C11.5408 7.125 11.6997 7.05915 11.8169 6.94194C11.9342 6.82473 12 6.66576 12 6.5C12 6.33424 11.9342 6.17527 11.8169 6.05806C11.6997 5.94085 11.5408 5.875 11.375 5.875H7.125L6.625 5.375V1.125C6.625 0.95924 6.55915 0.800269 6.44194 0.683058C6.32473 0.565848 6.16576 0.5 6 0.5C5.83424 0.5 5.67527 0.565848 5.55806 0.683058C5.44085 0.800269 5.375 0.95924 5.375 1.125V5.375L4.875 5.875H0.625C0.45924 5.875 0.300269 5.94085 0.183058 6.05806C0.065848 6.17527 0 6.33424 0 6.5C0 6.66576 0.065848 6.82473 0.183058 6.94194C0.300269 7.05915 0.45924 7.125 0.625 7.125H4.762L5.375 7.625Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 920 B |
3
web/images/altText_disclaimer.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.49073 1.3015L3.30873 2.1505C3.29349 2.22246 3.25769 2.28844 3.20568 2.34045C3.15368 2.39246 3.08769 2.42826 3.01573 2.4435L2.16673 2.6255C1.76473 2.7125 1.76473 3.2865 2.16673 3.3725L3.01573 3.5555C3.08769 3.57074 3.15368 3.60654 3.20568 3.65855C3.25769 3.71056 3.29349 3.77654 3.30873 3.8485L3.49073 4.6975C3.57773 5.0995 4.15173 5.0995 4.23773 4.6975L4.42073 3.8485C4.43598 3.77654 4.47177 3.71056 4.52378 3.65855C4.57579 3.60654 4.64178 3.57074 4.71373 3.5555L5.56173 3.3725C5.96373 3.2855 5.96373 2.7115 5.56173 2.6255L4.71273 2.4435C4.64083 2.42814 4.57491 2.3923 4.52292 2.34031C4.47093 2.28832 4.43509 2.2224 4.41973 2.1505L4.23773 1.3015C4.15073 0.8995 3.57673 0.8995 3.49073 1.3015ZM10.8647 13.9995C10.4853 14.0056 10.1158 13.8782 9.82067 13.6397C9.52553 13.4013 9.32347 13.0667 9.24973 12.6945L8.89273 11.0275C8.83676 10.7687 8.70738 10.5316 8.52009 10.3445C8.3328 10.1574 8.09554 10.0282 7.83673 9.9725L6.16973 9.6155C5.38873 9.4465 4.86473 8.7975 4.86473 7.9995C4.86473 7.2015 5.38873 6.5525 6.16973 6.3845L7.83673 6.0275C8.09551 5.97135 8.33267 5.84193 8.51992 5.65468C8.70716 5.46744 8.83658 5.23028 8.89273 4.9715L9.25073 3.3045C9.41773 2.5235 10.0667 1.9995 10.8647 1.9995C11.6627 1.9995 12.3117 2.5235 12.4797 3.3045L12.8367 4.9715C12.9507 5.4995 13.3647 5.9135 13.8927 6.0265L15.5597 6.3835C16.3407 6.5525 16.8647 7.2015 16.8647 7.9995C16.8647 8.7975 16.3407 9.4465 15.5597 9.6145L13.8927 9.9715C13.6337 10.0275 13.3963 10.157 13.209 10.3445C13.0217 10.5319 12.8925 10.7694 12.8367 11.0285L12.4787 12.6945C12.4054 13.0667 12.2036 13.4014 11.9086 13.6399C11.6135 13.8784 11.2441 14.0057 10.8647 13.9995ZM10.8647 3.2495C10.7667 3.2495 10.5337 3.2795 10.4727 3.5655L10.1147 5.2335C10.0081 5.72777 9.76116 6.18082 9.40361 6.53837C9.04606 6.89593 8.59301 7.14283 8.09873 7.2495L6.43173 7.6065C6.14573 7.6685 6.11473 7.9015 6.11473 7.9995C6.11473 8.0975 6.14573 8.3305 6.43173 8.3925L8.09873 8.7495C8.59301 8.85617 9.04606 9.10307 9.40361 9.46062C9.76116 9.81817 10.0081 10.2712 10.1147 10.7655L10.4727 12.4335C10.5337 12.7195 10.7667 12.7495 10.8647 12.7495C10.9627 12.7495 11.1957 12.7195 11.2567 12.4335L11.6147 10.7665C11.7212 10.272 11.9681 9.81878 12.3256 9.46103C12.6832 9.10329 13.1363 8.85624 13.6307 8.7495L15.2977 8.3925C15.5837 8.3305 15.6147 8.0975 15.6147 7.9995C15.6147 7.9015 15.5837 7.6685 15.2977 7.6065L13.6307 7.2495C13.1365 7.14283 12.6834 6.89593 12.3259 6.53837C11.9683 6.18082 11.7214 5.72777 11.6147 5.2335L11.2567 3.5655C11.1957 3.2795 10.9627 3.2495 10.8647 3.2495ZM3.30873 12.1505L3.49073 11.3015C3.57673 10.8995 4.15073 10.8995 4.23773 11.3015L4.41973 12.1505C4.43509 12.2224 4.47093 12.2883 4.52292 12.3403C4.57491 12.3923 4.64083 12.4281 4.71273 12.4435L5.56173 12.6255C5.96373 12.7115 5.96373 13.2855 5.56173 13.3725L4.71273 13.5545C4.64083 13.5699 4.57491 13.6057 4.52292 13.6577C4.47093 13.7097 4.43509 13.7756 4.41973 13.8475L4.23773 14.6965C4.15173 15.0985 3.57773 15.0985 3.49073 14.6965L3.30873 13.8475C3.29337 13.7756 3.25754 13.7097 3.20555 13.6577C3.15356 13.6057 3.08764 13.5699 3.01573 13.5545L2.16673 13.3725C1.76473 13.2865 1.76473 12.7125 2.16673 12.6255L3.01573 12.4435C3.08769 12.4283 3.15368 12.3925 3.20568 12.3405C3.25769 12.2884 3.29349 12.2225 3.30873 12.1505Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
3
web/images/altText_done.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="13" viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 0.5C5.21207 0.5 4.43185 0.655195 3.7039 0.956723C2.97595 1.25825 2.31451 1.70021 1.75736 2.25736C1.20021 2.81451 0.758251 3.47595 0.456723 4.2039C0.155195 4.93185 0 5.71207 0 6.5C0 7.28793 0.155195 8.06815 0.456723 8.7961C0.758251 9.52405 1.20021 10.1855 1.75736 10.7426C2.31451 11.2998 2.97595 11.7417 3.7039 12.0433C4.43185 12.3448 5.21207 12.5 6 12.5C7.5913 12.5 9.11742 11.8679 10.2426 10.7426C11.3679 9.61742 12 8.0913 12 6.5C12 4.9087 11.3679 3.38258 10.2426 2.25736C9.11742 1.13214 7.5913 0.5 6 0.5ZM5.06 8.9L2.9464 6.7856C2.85273 6.69171 2.80018 6.56446 2.80033 6.43183C2.80048 6.29921 2.85331 6.17207 2.9472 6.0784C3.04109 5.98473 3.16834 5.93218 3.30097 5.93233C3.43359 5.93248 3.56073 5.98531 3.6544 6.0792L5.3112 7.7368L8.3464 4.7008C8.44109 4.6109 8.56715 4.56153 8.69771 4.56322C8.82827 4.56492 8.95301 4.61754 9.04534 4.70986C9.13766 4.80219 9.19028 4.92693 9.19198 5.05749C9.19367 5.18805 9.1443 5.31411 9.0544 5.4088L5.5624 8.9H5.06Z" fill="#FBFBFE"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
30
web/images/altText_spinner.svg
Normal file
@@ -0,0 +1,30 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
|
||||
<style>
|
||||
@media not (prefers-reduced-motion) {
|
||||
@keyframes loadingRotate {
|
||||
from { rotate: 0; } to { rotate: 360deg }
|
||||
}
|
||||
#circle-arrows {
|
||||
animation: loadingRotate 1.8s linear infinite;
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
#hourglass {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
#circle-arrows {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path id="circle-arrows" d="M9 5.528c0 .42.508.63.804.333l2.528-2.528a.47.47 0 0 0 0-.666L9.805.14A.471.471 0 0 0 9 .472v1.866A5.756 5.756 0 0 0 2.25 8c0 .942.232 1.83.635 2.615l1.143-1.143A4.208 4.208 0 0 1 3.75 8 4.254 4.254 0 0 1 8 3.75c.345 0 .68.042 1 .122v1.656zM7 10.472v1.656c.32.08.655.122 1 .122A4.254 4.254 0 0 0 12.25 8c0-.52-.107-1.013-.279-1.474l1.143-1.143c.404.786.636 1.674.636 2.617A5.756 5.756 0 0 1 7 13.662v1.866a.47.47 0 0 1-.804.333l-2.528-2.528a.47.47 0 0 1 0-.666l2.528-2.528a.47.47 0 0 1 .804.333z"/>
|
||||
<g id="hourglass">
|
||||
<path d="M13,1 C13.5522847,1 14,1.44771525 14,2 C14,2.55228475 13.5522847,3 13,3 L12.9854217,2.99990801 C12.9950817,3.16495885 13,3.33173274 13,3.5 C13,5.24679885 10.9877318,6.01090495 10.9877318,8.0017538 C10.9877318,9.99260264 13,10.7536922 13,12.5 C13,12.6686079 12.9950617,12.8357163 12.985363,13.0010943 L13,13 C13.5522847,13 14,13.4477153 14,14 C14,14.5522847 13.5522847,15 13,15 L3,15 C2.44771525,15 2,14.5522847 2,14 C2,13.4477153 2.44771525,13 3,13 L3.01463704,13.0010943 C3.00493827,12.8357163 3,12.6686079 3,12.5 C3,10.7536922 4.9877318,9.99260264 5,8.0017538 C5.0122682,6.01090495 3,5.24679885 3,3.5 C3,3.33173274 3.00491834,3.16495885 3.01457832,2.99990801 L3,3 C2.44771525,3 2,2.55228475 2,2 C2,1.44771525 2.44771525,1 3,1 L13,1 Z M10.987,3 L5.012,3 L5.00308914,3.24815712 C5.00103707,3.33163368 5,3.4155948 5,3.5 C5,5.36125069 6.99153646,6.01774089 6.99153646,8.0017538 C6.99153646,9.98576671 5,10.6393737 5,12.5 L5.00307746,12.7513676 L5.01222201,12.9998392 L5.60191711,12.9988344 L6.0425138,12.2959826 C7.02362731,10.7653275 7.67612271,10 8,10 C8.37014547,10 9.16950644,10.9996115 10.3980829,12.9988344 L10.987778,12.9998392 C10.9958674,12.8352104 11,12.66849 11,12.5 C11,10.6393737 8.98689779,10.0147381 8.98689779,8.0017538 C8.98689779,5.98876953 11,5.36125069 11,3.5 L10.9969109,3.24815712 L10.987,3 Z"/>
|
||||
<path d="M6,4 L10,4 C8.95166016,6 8.28499349,7 8,7 C7.71500651,7 7.04833984,6 6,4 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
3
web/images/altText_warning.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.78182 2.63903C8.58882 2.28803 8.25782 2.25003 8.12482 2.25003C7.99019 2.24847 7.85771 2.28393 7.74185 2.35253C7.62599 2.42113 7.5312 2.52023 7.46782 2.63903L1.97082 12.639C1.90673 12.7528 1.87406 12.8816 1.87617 13.0122C1.87828 13.1427 1.91509 13.2704 1.98282 13.382C2.04798 13.4951 2.14207 13.5888 2.25543 13.6535C2.36879 13.7182 2.49732 13.7515 2.62782 13.75H13.6218C13.7523 13.7515 13.8809 13.7182 13.9942 13.6535C14.1076 13.5888 14.2017 13.4951 14.2668 13.382C14.3346 13.2704 14.3714 13.1427 14.3735 13.0122C14.3756 12.8816 14.3429 12.7528 14.2788 12.639L8.78182 2.63903ZM6.37282 2.03703C6.75182 1.34603 7.43882 1.00003 8.12482 1.00003C8.48341 0.997985 8.83583 1.09326 9.14454 1.2757C9.45325 1.45814 9.70668 1.72092 9.87782 2.03603L15.3748 12.036C16.1078 13.369 15.1438 15 13.6228 15H2.62782C1.10682 15 0.141823 13.37 0.875823 12.037L6.37282 2.03703ZM8.74982 9.06203C8.74982 9.22779 8.68397 9.38676 8.56676 9.50397C8.44955 9.62118 8.29058 9.68703 8.12482 9.68703C7.95906 9.68703 7.80009 9.62118 7.68288 9.50397C7.56566 9.38676 7.49982 9.22779 7.49982 9.06203V5.62503C7.49982 5.45927 7.56566 5.3003 7.68288 5.18309C7.80009 5.06588 7.95906 5.00003 8.12482 5.00003C8.29058 5.00003 8.44955 5.06588 8.56676 5.18309C8.68397 5.3003 8.74982 5.45927 8.74982 5.62503V9.06203ZM7.74982 12L7.49982 11.75V11L7.74982 10.75H8.49982L8.74982 11V11.75L8.49982 12H7.74982Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
11
web/images/annotation-check.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 40 40">
|
||||
<path
|
||||
d="M 1.5006714,23.536225 6.8925879,18.994244 14.585721,26.037937 34.019683,4.5410479 38.499329,9.2235032 14.585721,35.458952 z"
|
||||
id="path4"
|
||||
style="fill:#ffff00;fill-opacity:1;stroke:#000000;stroke-width:1.25402856;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 415 B |
16
web/images/annotation-comment.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="40"
|
||||
width="40"
|
||||
viewBox="0 0 40 40">
|
||||
<rect
|
||||
style="fill:#ffff00;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
width="33.76017"
|
||||
height="33.76017"
|
||||
x="3.119915"
|
||||
y="3.119915" />
|
||||
<path
|
||||
d="m 20.677967,8.54499 c -7.342801,0 -13.295293,4.954293 -13.295293,11.065751 0,2.088793 0.3647173,3.484376 1.575539,5.150563 L 6.0267418,31.45501 13.560595,29.011117 c 2.221262,1.387962 4.125932,1.665377 7.117372,1.665377 7.3428,0 13.295291,-4.954295 13.295291,-11.065753 0,-6.111458 -5.952491,-11.065751 -13.295291,-11.065751 z"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.93031836;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 883 B |
26
web/images/annotation-help.svg
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 40 40">
|
||||
<g
|
||||
transform="translate(0,-60)"
|
||||
id="layer1">
|
||||
<rect
|
||||
width="36.460953"
|
||||
height="34.805603"
|
||||
x="1.7695236"
|
||||
y="62.597198"
|
||||
style="fill:#ffff00;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.30826771;stroke-opacity:1" />
|
||||
<g
|
||||
transform="matrix(0.88763677,0,0,0.88763677,2.2472646,8.9890584)">
|
||||
<path
|
||||
d="M 20,64.526342 C 11.454135,64.526342 4.5263421,71.454135 4.5263421,80 4.5263421,88.545865 11.454135,95.473658 20,95.473658 28.545865,95.473658 35.473658,88.545865 35.473658,80 35.473658,71.454135 28.545865,64.526342 20,64.526342 z m -0.408738,9.488564 c 3.527079,0 6.393832,2.84061 6.393832,6.335441 0,3.494831 -2.866753,6.335441 -6.393832,6.335441 -3.527079,0 -6.393832,-2.84061 -6.393832,-6.335441 0,-3.494831 2.866753,-6.335441 6.393832,-6.335441 z"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.02768445;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m 7.2335209,71.819938 4.9702591,4.161823 c -1.679956,2.581606 -1.443939,6.069592 0.159325,8.677725 l -5.1263071,3.424463 c 0.67516,1.231452 3.0166401,3.547686 4.2331971,4.194757 l 3.907728,-4.567277 c 2.541952,1.45975 5.730694,1.392161 8.438683,-0.12614 l 3.469517,6.108336 c 1.129779,-0.44367 4.742234,-3.449633 5.416358,-5.003859 l -5.46204,-4.415541 c 1.44319,-2.424098 1.651175,-5.267515 0.557303,-7.748623 l 5.903195,-3.833951 C 33.14257,71.704996 30.616217,69.018606 29.02952,67.99296 l -4.118813,4.981678 C 22.411934,71.205099 18.900853,70.937534 16.041319,72.32916 l -3.595408,-5.322091 c -1.345962,0.579488 -4.1293881,2.921233 -5.2123901,4.812869 z m 8.1010311,3.426672 c 2.75284,-2.446266 6.769149,-2.144694 9.048998,0.420874 2.279848,2.56557 2.113919,6.596919 -0.638924,9.043185 -2.752841,2.446267 -6.775754,2.13726 -9.055604,-0.428308 -2.279851,-2.565568 -2.107313,-6.589485 0.64553,-9.035751 z"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
10
web/images/annotation-insert.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 64 64">
|
||||
<path
|
||||
d="M 32.003143,1.4044602 57.432701,62.632577 6.5672991,62.627924 z"
|
||||
style="fill:#ffff00;fill-opacity:0.94117647;fill-rule:nonzero;stroke:#000000;stroke-width:1.00493038;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
11
web/images/annotation-key.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 64 64">
|
||||
<path
|
||||
d="M 25.470843,9.4933766 C 25.30219,12.141818 30.139101,14.445969 34.704831,13.529144 40.62635,12.541995 41.398833,7.3856498 35.97505,5.777863 31.400921,4.1549155 25.157674,6.5445892 25.470843,9.4933766 z M 4.5246282,17.652051 C 4.068249,11.832873 9.2742983,5.9270407 18.437379,3.0977088 29.751911,-0.87185184 45.495663,1.4008022 53.603953,7.1104009 c 9.275765,6.1889221 7.158128,16.2079421 -3.171076,21.5939521 -1.784316,1.635815 -6.380222,1.21421 -7.068351,3.186186 -1.04003,0.972427 -1.288046,2.050158 -1.232864,3.168203 1.015111,2.000108 -3.831548,1.633216 -3.270553,3.759574 0.589477,5.264544 -0.179276,10.53738 -0.362842,15.806257 -0.492006,2.184998 1.163456,4.574232 -0.734888,6.610642 -2.482919,2.325184 -7.30604,2.189143 -9.193497,-0.274767 -2.733688,-1.740626 -8.254447,-3.615254 -6.104247,-6.339626 3.468112,-1.708686 -2.116197,-3.449897 0.431242,-5.080274 5.058402,-1.39256 -2.393215,-2.304318 -0.146889,-4.334645 3.069198,-0.977415 2.056986,-2.518352 -0.219121,-3.540397 1.876567,-1.807151 1.484149,-4.868919 -2.565455,-5.942205 0.150866,-1.805474 2.905737,-4.136876 -1.679967,-5.20493 C 10.260902,27.882167 4.6872697,22.95045 4.5245945,17.652051 z"
|
||||
id="path604"
|
||||
style="fill:#ffff00;fill-opacity:1;stroke:#000000;stroke-width:1.72665179;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
11
web/images/annotation-newparagraph.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 64 64">
|
||||
<path
|
||||
d="M 32.003143,10.913072 57.432701,53.086929 6.567299,53.083723 z"
|
||||
id="path2985"
|
||||
style="fill:#ffff00;fill-opacity:0.94117647;fill-rule:nonzero;stroke:#000000;stroke-width:0.83403099;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 426 B |
7
web/images/annotation-noicon.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 40 40">
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 158 B |
42
web/images/annotation-note.svg
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 40 40">
|
||||
<rect
|
||||
width="36.075428"
|
||||
height="31.096582"
|
||||
x="1.962286"
|
||||
y="4.4517088"
|
||||
id="rect4"
|
||||
style="fill:#ffff00;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.23004246;stroke-opacity:1" />
|
||||
<rect
|
||||
width="27.96859"
|
||||
height="1.5012145"
|
||||
x="6.0157046"
|
||||
y="10.285"
|
||||
id="rect6"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none" />
|
||||
<rect
|
||||
width="27.96859"
|
||||
height="0.85783684"
|
||||
x="6.0157056"
|
||||
y="23.21689"
|
||||
id="rect8"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none" />
|
||||
<rect
|
||||
width="27.96859"
|
||||
height="0.85783684"
|
||||
x="5.8130345"
|
||||
y="28.964394"
|
||||
id="rect10"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none" />
|
||||
<rect
|
||||
width="27.96859"
|
||||
height="0.85783684"
|
||||
x="6.0157046"
|
||||
y="17.426493"
|
||||
id="rect12"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
6
web/images/annotation-paperclip.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" height="40" width="40">
|
||||
<path d="M9 3.5a1.5 1.5 0 0 0-3-.001v7.95C6 12.83 7.12 14 8.5 14s2.5-1.17 2.5-2.55V5.5a.5.5 0 0 1 1 0v6.03C11.955 13.427 10.405 15 8.5 15S5.044 13.426 5 11.53V3.5a2.5 2.5 0 0 1 5 0v7.003a1.5 1.5 0 0 1-3-.003v-5a.5.5 0 0 1 1 0v5a.5.5 0 0 0 1 0Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 552 B |
16
web/images/annotation-paragraph.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 40 40">
|
||||
<rect
|
||||
width="33.76017"
|
||||
height="33.76017"
|
||||
x="3.119915"
|
||||
y="3.119915"
|
||||
style="fill:#ffff00;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m 17.692678,34.50206 0,-16.182224 c -1.930515,-0.103225 -3.455824,-0.730383 -4.57593,-1.881473 -1.12011,-1.151067 -1.680164,-2.619596 -1.680164,-4.405591 0,-1.992435 0.621995,-3.5796849 1.865988,-4.7617553 1.243989,-1.1820288 3.06352,-1.7730536 5.458598,-1.7730764 l 9.802246,0 0,2.6789711 -2.229895,0 0,26.3251486 -2.632515,0 0,-26.3251486 -3.45324,0 0,26.3251486 z"
|
||||
style="font-size:29.42051125px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.07795751;stroke-opacity:1;font-family:Arial;-inkscape-font-specification:Arial" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
7
web/images/annotation-pushpin.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" height="40" width="40">
|
||||
<path d="M8.156 12.5a.99.99 0 0 0 .707-.294l.523-2.574L10.5 8.499l1.058-1.04 2.65-.601a.996.996 0 0 0 0-1.414l-3.657-3.658a.996.996 0 0 0-1.414 0l-.523 2.576L7.5 5.499 6.442 6.535l-2.65.6a.996.996 0 0 0 0 1.413l3.657 3.658a.999.999 0 0 0 .707.295z"/>
|
||||
<path d="M9.842.996c-.386 0-.77.146-1.06.44a.5.5 0 0 0-.136.251l-.492 2.43-1.008 1.03-.953.933-2.511.566a.5.5 0 0 0-.243.133 1.505 1.505 0 0 0-.002 2.123l1.477 1.477-2.768 2.767a.5.5 0 0 0 0 .707.5.5 0 0 0 .708 0l2.767-2.767 1.475 1.474a1.494 1.494 0 0 0 2.123-.002.5.5 0 0 0 .135-.254l.492-2.427 1.008-1.024.953-.937 2.511-.57a.5.5 0 0 0 .243-.132c.586-.58.583-1.543.002-2.125l-3.659-3.656A1.501 1.501 0 0 0 9.842.996Zm.05 1.025a.394.394 0 0 1 .305.12l3.658 3.657c.18.18.141.432.002.627l-2.41.545a.5.5 0 0 0-.24.131L10.15 8.142a.5.5 0 0 0-.007.006L9.029 9.283a.5.5 0 0 0-.133.25l-.48 2.36c-.082.053-.165.109-.26.109a.492.492 0 0 1-.353-.149L4.145 8.195c-.18-.18-.141-.432-.002-.627l2.41-.545a.5.5 0 0 0 .238-.13L7.85 5.857a.5.5 0 0 0 .007-.008l1.114-1.138a.5.5 0 0 0 .133-.25l.472-2.323a.619.619 0 0 1 .317-.117Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
3
web/images/comment-actionsButton.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.51562 11H6.01562L5.51562 11.5V13L6.01562 13.5H7.51562L8.01562 13V11.5L7.51562 11ZM13.2656 11H11.7656L11.2656 11.5V13L11.7656 13.5H13.2656L13.7656 13V11.5L13.2656 11ZM17.5156 11H19.0156L19.5156 11.5V13L19.0156 13.5H17.5156L17.0156 13V11.5L17.5156 11Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 421 B |
3
web/images/comment-closeButton.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.6241 11.7759L18.3331 7.06694C18.4423 6.94811 18.5015 6.79167 18.4981 6.63028C18.4948 6.46889 18.4292 6.31502 18.3152 6.20081C18.2011 6.0866 18.0473 6.02088 17.8859 6.01736C17.7245 6.01384 17.568 6.0728 17.4491 6.18194L12.7601 10.8709H12.2721L7.58306 6.18294C7.52495 6.12489 7.45598 6.07886 7.38008 6.04747C7.30418 6.01609 7.22284 5.99995 7.14071 6C7.05857 6.00005 6.97725 6.01627 6.90139 6.04774C6.82553 6.07922 6.75661 6.12533 6.69856 6.18344C6.64052 6.24155 6.59449 6.31052 6.5631 6.38642C6.53171 6.46232 6.51558 6.54366 6.51563 6.62579C6.51572 6.79167 6.5817 6.95071 6.69906 7.06794L11.3861 11.7539V12.2449L6.69906 16.9319C6.5898 17.0508 6.53066 17.2072 6.53399 17.3686C6.53732 17.53 6.60288 17.6839 6.71696 17.7981C6.83104 17.9123 6.98483 17.978 7.14622 17.9815C7.3076 17.985 7.46411 17.9261 7.58306 17.8169L12.2701 13.1299H12.7611L17.4481 17.8169C17.5656 17.934 17.7247 17.9997 17.8906 17.9997C18.0564 17.9997 18.2155 17.934 18.3331 17.8169C18.4504 17.6996 18.5163 17.5404 18.5163 17.3744C18.5163 17.2085 18.4504 17.0493 18.3331 16.9319L13.6241 12.2229V11.7759Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
8
web/images/comment-editButton.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path d="M10.75 7H12.25V5.5H10.75V7Z" fill="black"/>
|
||||
<path d="M7.5 7H9V5.5H7.5V7Z" fill="black"/>
|
||||
<path d="M4.25 7H5.75V5.5H4.25V7Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 0C0.895786 0 0 0.895786 0 2V10.5C0 11.6042 0.895786 12.5 2 12.5H3V15.25C3 15.5405 3.16778 15.805 3.43066 15.9287C3.6937 16.0523 4.00473 16.0126 4.22852 15.8271L8.27051 12.4775L13.9941 12.4961C15.1007 12.4991 15.9999 11.6033 16 10.4961V2C16 0.895786 15.1042 0 14 0H2ZM14 1.5C14.2758 1.5 14.5 1.72421 14.5 2V10.4961C14.4999 10.7727 14.2753 10.9969 13.998 10.9961L8.00195 10.9775L7.87207 10.9893C7.74389 11.0115 7.62281 11.0664 7.52148 11.1504L4.5 13.6543V11.75C4.5 11.3358 4.16421 11 3.75 11H2C1.72421 11 1.5 10.7758 1.5 10.5V2C1.5 1.72421 1.72421 1.5 2 1.5H14Z" fill="black"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 908 B |
5
web/images/comment-popup-editButton.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.0189877 13.6645L0.612989 10.4635C0.687989 10.0545 0.884989 9.6805 1.18099 9.3825L9.98199 0.5805C10.756 -0.1925 12.015 -0.1945 12.792 0.5805L14.42 2.2085C15.194 2.9835 15.194 4.2435 14.42 5.0185L5.61599 13.8215C5.31999 14.1165 4.94599 14.3125 4.53799 14.3875L1.33599 14.9815C1.26599 14.9935 1.19799 15.0005 1.12999 15.0005C0.832989 15.0005 0.544988 14.8835 0.330988 14.6695C0.0679874 14.4055 -0.0490122 14.0305 0.0189877 13.6645ZM12.472 5.1965L13.632 4.0365L13.631 3.1885L11.811 1.3675L10.963 1.3685L9.80299 2.5285L12.472 5.1965ZM4.31099 13.1585C4.47099 13.1285 4.61799 13.0515 4.73399 12.9345L11.587 6.0815L8.91899 3.4135L2.06599 10.2655C1.94899 10.3835 1.87199 10.5305 1.84099 10.6915L1.36699 13.2485L1.75199 13.6335L4.31099 13.1585Z" fill="black"/>
|
||||
</svg>
|
||||
|
||||
<!--path d="M0.0189877 14.1645L0.612989 10.9635C0.687989 10.5545 0.884989 10.1805 1.18099 9.8825L9.98199 1.0805C10.756 0.3075 12.015 0.3055 12.792 1.0805L14.42 2.7085C15.194 3.4835 15.194 4.7435 14.42 5.5185L5.61599 14.3215C5.31999 14.6165 4.94599 14.8125 4.53799 14.8875L1.33599 15.4815C1.26599 15.4935 1.19799 15.5005 1.12999 15.5005C0.832989 15.5005 0.544988 15.3835 0.330988 15.1695C0.0679874 14.9055 -0.0490122 14.5305 0.0189877 14.1645ZM12.472 5.6965L13.632 4.5365L13.631 3.6885L11.811 1.8675L10.963 1.8685L9.80299 3.0285L12.472 5.6965ZM4.31099 13.6585C4.47099 13.6285 4.61799 13.5515 4.73399 13.4345L11.587 6.5815L8.91899 3.9135L2.06599 10.7655C1.94899 10.8835 1.87199 11.0305 1.84099 11.1915L1.36699 13.7485L1.75199 14.1335L4.31099 13.6585Z" fill="black"/-->
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
6
web/images/cursor-editorFreeHighlight.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.2 3.09C12.28 3.01 12.43 3 12.43 3C12.48 3 12.58 3.02 12.66 3.1L14.45 4.89C14.58 5.02 14.58 5.22 14.45 5.35L11.7713 8.02872L9.51628 5.77372L12.2 3.09ZM13.2658 5.12L11.7713 6.6145L10.9305 5.77372L12.425 4.27921L13.2658 5.12Z" fill="#FBFBFE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.98 9.32L8.23 11.57L10.7106 9.08938L8.45562 6.83438L5.98 9.31V9.32ZM8.23 10.1558L9.29641 9.08938L8.45562 8.24859L7.38921 9.315L8.23 10.1558Z" fill="#FBFBFE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.1526 13.1816L16.2125 7.1217C16.7576 6.58919 17.05 5.8707 17.05 5.12C17.05 4.36931 16.7576 3.65084 16.2126 3.11834L14.4317 1.33747C13.8992 0.79242 13.1807 0.5 12.43 0.5C11.6643 0.5 10.9529 0.812929 10.4329 1.33289L3.68289 8.08289C3.04127 8.72452 3.00459 9.75075 3.57288 10.4363L1.29187 12.7239C1.09186 12.9245 0.990263 13.1957 1.0007 13.4685L1 14.5C0.447715 14.5 0 14.9477 0 15.5V17.5C0 18.0523 0.447715 18.5 1 18.5H16C16.5523 18.5 17 18.0523 17 17.5V15.5C17 14.9477 16.5523 14.5 16 14.5H10.2325C9.83594 14.5 9.39953 13.9347 10.1526 13.1816ZM4.39 9.85L4.9807 10.4407L2.39762 13.0312H6.63877L7.10501 12.565L7.57125 13.0312H8.88875L15.51 6.41C15.86 6.07 16.05 5.61 16.05 5.12C16.05 4.63 15.86 4.17 15.51 3.83L13.72 2.04C13.38 1.69 12.92 1.5 12.43 1.5C11.94 1.5 11.48 1.7 11.14 2.04L4.39 8.79C4.1 9.08 4.1 9.56 4.39 9.85ZM16 17.5V15.5H1V17.5H16Z" fill="#FBFBFE"/>
|
||||
<path d="M15.1616 6.05136L15.1616 6.05132L15.1564 6.05645L8.40645 12.8064C8.35915 12.8537 8.29589 12.88 8.23 12.88C8.16411 12.88 8.10085 12.8537 8.05355 12.8064L7.45857 12.2115L7.10501 11.8579L6.75146 12.2115L6.03289 12.93H3.20465L5.33477 10.7937L5.6873 10.4402L5.33426 10.0871L4.74355 9.49645C4.64882 9.40171 4.64882 9.23829 4.74355 9.14355L11.4936 2.39355C11.7436 2.14354 12.0779 2 12.43 2C12.7883 2 13.1179 2.13776 13.3614 2.38839L13.3613 2.38843L13.3664 2.39355L15.1564 4.18355L15.1564 4.18359L15.1616 4.18864C15.4122 4.43211 15.55 4.76166 15.55 5.12C15.55 5.47834 15.4122 5.80789 15.1616 6.05136ZM7.87645 11.9236L8.23 12.2771L8.58355 11.9236L11.0642 9.44293L11.4177 9.08938L11.0642 8.73582L8.80918 6.48082L8.45562 6.12727L8.10207 6.48082L5.62645 8.95645L5.48 9.10289V9.31V9.32V9.52711L5.62645 9.67355L7.87645 11.9236ZM11.4177 8.38227L11.7713 8.73582L12.1248 8.38227L14.8036 5.70355C15.1288 5.37829 15.1288 4.86171 14.8036 4.53645L13.0136 2.74645C12.8186 2.55146 12.5792 2.5 12.43 2.5H12.4134L12.3967 2.50111L12.43 3C12.3967 2.50111 12.3966 2.50112 12.3965 2.50112L12.3963 2.50114L12.3957 2.50117L12.3947 2.50125L12.3924 2.50142L12.387 2.50184L12.3732 2.50311C12.3628 2.50416 12.3498 2.50567 12.3346 2.50784C12.3049 2.51208 12.2642 2.51925 12.2178 2.53146C12.1396 2.55202 11.9797 2.60317 11.8464 2.73645L9.16273 5.42016L8.80918 5.77372L9.16273 6.12727L11.4177 8.38227ZM1.5 16H15.5V17H1.5V16Z" stroke="#15141A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
3
web/images/cursor-editorFreeText.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2.75H12.5V2.25V1V0.5H12H10.358C9.91165 0.5 9.47731 0.625661 9.09989 0.860442L9.09886 0.861087L8 1.54837L6.89997 0.860979L6.89911 0.860443C6.5218 0.625734 6.08748 0.5 5.642 0.5H4H3.5V1V2.25V2.75H4H5.642C5.66478 2.75 5.6885 2.75641 5.71008 2.76968C5.71023 2.76977 5.71038 2.76986 5.71053 2.76995L6.817 3.461C6.81704 3.46103 6.81709 3.46105 6.81713 3.46108C6.81713 3.46108 6.81713 3.46108 6.81714 3.46109C6.8552 3.48494 6.876 3.52285 6.876 3.567V8V12.433C6.876 12.4771 6.85523 12.515 6.81722 12.5389C6.81715 12.5389 6.81707 12.539 6.817 12.539L5.70953 13.23C5.70941 13.2301 5.70929 13.2302 5.70917 13.2303C5.68723 13.2438 5.6644 13.25 5.641 13.25H4H3.5V13.75V15V15.5H4H5.642C6.08835 15.5 6.52269 15.3743 6.90011 15.1396L6.90086 15.1391L8 14.4526L9.10003 15.14L9.10089 15.1406C9.47831 15.3753 9.91265 15.501 10.359 15.501H12H12.5V15.001V13.751V13.251H12H10.358C10.3352 13.251 10.3115 13.2446 10.2899 13.2313C10.2897 13.2312 10.2896 13.2311 10.2895 13.231L9.183 12.54C9.18298 12.54 9.18295 12.54 9.18293 12.54C9.18291 12.5399 9.18288 12.5399 9.18286 12.5399C9.14615 12.5169 9.125 12.4797 9.125 12.434V8V3.567C9.125 3.52266 9.14603 3.48441 9.18364 3.4606C9.18377 3.46052 9.1839 3.46043 9.18404 3.46035L10.2895 2.76995C10.2896 2.76985 10.2898 2.76975 10.2899 2.76966C10.3119 2.75619 10.3346 2.75 10.358 2.75H12Z" fill="black" stroke="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
4
web/images/cursor-editorInk.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.0189877 13.6645L0.612989 10.4635C0.687989 10.0545 0.884989 9.6805 1.18099 9.3825L9.98199 0.5805C10.756 -0.1925 12.015 -0.1945 12.792 0.5805L14.42 2.2085C15.194 2.9835 15.194 4.2435 14.42 5.0185L5.61599 13.8215C5.31999 14.1165 4.94599 14.3125 4.53799 14.3875L1.33599 14.9815C1.26599 14.9935 1.19799 15.0005 1.12999 15.0005C0.832989 15.0005 0.544988 14.8835 0.330988 14.6695C0.0679874 14.4055 -0.0490122 14.0305 0.0189877 13.6645Z" fill="white"/>
|
||||
<path d="M0.0189877 13.6645L0.612989 10.4635C0.687989 10.0545 0.884989 9.6805 1.18099 9.3825L9.98199 0.5805C10.756 -0.1925 12.015 -0.1945 12.792 0.5805L14.42 2.2085C15.194 2.9835 15.194 4.2435 14.42 5.0185L5.61599 13.8215C5.31999 14.1165 4.94599 14.3125 4.53799 14.3875L1.33599 14.9815C1.26599 14.9935 1.19799 15.0005 1.12999 15.0005C0.832989 15.0005 0.544988 14.8835 0.330988 14.6695C0.0679874 14.4055 -0.0490122 14.0305 0.0189877 13.6645ZM12.472 5.1965L13.632 4.0365L13.631 3.1885L11.811 1.3675L10.963 1.3685L9.80299 2.5285L12.472 5.1965ZM4.31099 13.1585C4.47099 13.1285 4.61799 13.0515 4.73399 12.9345L11.587 6.0815L8.91899 3.4135L2.06599 10.2655C1.94899 10.3835 1.87199 10.5305 1.84099 10.6915L1.36699 13.2485L1.75199 13.6335L4.31099 13.1585Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
8
web/images/cursor-editorTextHighlight.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="29" height="32" viewBox="0 0 29 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M28 16.75C28.2761 16.75 28.5 16.5261 28.5 16.25V15C28.5 14.7239 28.2761 14.5 28 14.5H26.358C25.9117 14.5 25.4773 14.6257 25.0999 14.8604L25.0989 14.8611L24 15.5484L22.9 14.861L22.8991 14.8604C22.5218 14.6257 22.0875 14.5 21.642 14.5H20C19.7239 14.5 19.5 14.7239 19.5 15V16.25C19.5 16.5261 19.7239 16.75 20 16.75H21.642C21.6648 16.75 21.6885 16.7564 21.7101 16.7697C21.7102 16.7698 21.7104 16.7699 21.7105 16.77L22.817 17.461C22.817 17.461 22.8171 17.4611 22.8171 17.4611C22.8171 17.4611 22.8171 17.4611 22.8171 17.4611C22.8552 17.4849 22.876 17.5229 22.876 17.567V22.625V27.683C22.876 27.7271 22.8552 27.765 22.8172 27.7889C22.8171 27.7889 22.8171 27.789 22.817 27.789L21.7095 28.48C21.7094 28.4801 21.7093 28.4802 21.7092 28.4803C21.6872 28.4938 21.6644 28.5 21.641 28.5H20C19.7239 28.5 19.5 28.7239 19.5 29V30.25C19.5 30.5261 19.7239 30.75 20 30.75H21.642C22.0883 30.75 22.5227 30.6243 22.9001 30.3896L22.9009 30.3891L24 29.7026L25.1 30.39L25.1009 30.3906C25.4783 30.6253 25.9127 30.751 26.359 30.751H28C28.2761 30.751 28.5 30.5271 28.5 30.251V29.001C28.5 28.7249 28.2761 28.501 28 28.501H26.358C26.3352 28.501 26.3115 28.4946 26.2899 28.4813C26.2897 28.4812 26.2896 28.4811 26.2895 28.481L25.183 27.79C25.183 27.79 25.183 27.79 25.1829 27.79C25.1829 27.7899 25.1829 27.7899 25.1829 27.7899C25.1462 27.7669 25.125 27.7297 25.125 27.684V22.625V17.567C25.125 17.5227 25.146 17.4844 25.1836 17.4606C25.1838 17.4605 25.1839 17.4604 25.184 17.4603L26.2895 16.77C26.2896 16.7699 26.2898 16.7698 26.2899 16.7697C26.3119 16.7562 26.3346 16.75 26.358 16.75H28Z" fill="black" stroke="#FBFBFE" stroke-linejoin="round"/>
|
||||
<path d="M24.625 17.567C24.625 17.35 24.735 17.152 24.918 17.037L26.026 16.345C26.126 16.283 26.24 16.25 26.358 16.25H28V15H26.358C26.006 15 25.663 15.099 25.364 15.285L24.256 15.978C24.161 16.037 24.081 16.113 24 16.187C23.918 16.113 23.839 16.037 23.744 15.978L22.635 15.285C22.336 15.099 21.993 15 21.642 15H20V16.25H21.642C21.759 16.25 21.874 16.283 21.974 16.345L23.082 17.037C23.266 17.152 23.376 17.35 23.376 17.567V22.625V27.683C23.376 27.9 23.266 28.098 23.082 28.213L21.973 28.905C21.873 28.967 21.759 29 21.641 29H20V30.25H21.642C21.994 30.25 22.337 30.151 22.636 29.965L23.744 29.273C23.84 29.213 23.919 29.137 24 29.064C24.081 29.137 24.161 29.213 24.256 29.273L25.365 29.966C25.664 30.152 26.007 30.251 26.359 30.251H28V29.001H26.358C26.241 29.001 26.126 28.968 26.026 28.906L24.918 28.214C24.734 28.099 24.625 27.901 24.625 27.684V22.625V17.567Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.2 2.59C12.28 2.51 12.43 2.5 12.43 2.5C12.48 2.5 12.58 2.52 12.66 2.6L14.45 4.39C14.58 4.52 14.58 4.72 14.45 4.85L11.7713 7.52872L9.51628 5.27372L12.2 2.59ZM13.2658 4.62L11.7713 6.1145L10.9305 5.27372L12.425 3.77921L13.2658 4.62Z" fill="#FBFBFE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.98 8.82L8.23 11.07L10.7106 8.58938L8.45562 6.33438L5.98 8.81V8.82ZM8.23 9.65579L9.29641 8.58938L8.45562 7.74859L7.38921 8.815L8.23 9.65579Z" fill="#FBFBFE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.1526 12.6816L16.2125 6.6217C16.7576 6.08919 17.05 5.3707 17.05 4.62C17.05 3.86931 16.7576 3.15084 16.2126 2.61834L14.4317 0.837474C13.8992 0.29242 13.1807 0 12.43 0C11.6643 0 10.9529 0.312929 10.4329 0.832893L3.68289 7.58289C3.04127 8.22452 3.00459 9.25075 3.57288 9.93634L1.29187 12.2239C1.09186 12.4245 0.990263 12.6957 1.0007 12.9685L1 14C0.447715 14 0 14.4477 0 15V17C0 17.5523 0.447715 18 1 18H16C16.5523 18 17 17.5523 17 17V15C17 14.4477 16.5523 14 16 14H10.2325C9.83594 14 9.39953 13.4347 10.1526 12.6816ZM4.39 9.35L4.9807 9.9407L2.39762 12.5312H6.63877L7.10501 12.065L7.57125 12.5312H8.88875L15.51 5.91C15.86 5.57 16.05 5.11 16.05 4.62C16.05 4.13 15.86 3.67 15.51 3.33L13.72 1.54C13.38 1.19 12.92 1 12.43 1C11.94 1 11.48 1.2 11.14 1.54L4.39 8.29C4.1 8.58 4.1 9.06 4.39 9.35ZM16 17V15H1V17H16Z" fill="#FBFBFE"/>
|
||||
<path d="M15.1616 5.55136L15.1616 5.55132L15.1564 5.55645L8.40645 12.3064C8.35915 12.3537 8.29589 12.38 8.23 12.38C8.16411 12.38 8.10085 12.3537 8.05355 12.3064L7.45857 11.7115L7.10501 11.3579L6.75146 11.7115L6.03289 12.43H3.20465L5.33477 10.2937L5.6873 9.94019L5.33426 9.58715L4.74355 8.99645C4.64882 8.90171 4.64882 8.73829 4.74355 8.64355L11.4936 1.89355C11.7436 1.64354 12.0779 1.5 12.43 1.5C12.7883 1.5 13.1179 1.63776 13.3614 1.88839L13.3613 1.88843L13.3664 1.89355L15.1564 3.68355L15.1564 3.68359L15.1616 3.68864C15.4122 3.93211 15.55 4.26166 15.55 4.62C15.55 4.97834 15.4122 5.30789 15.1616 5.55136ZM5.48 8.82V9.02711L5.62645 9.17355L7.87645 11.4236L8.23 11.7771L8.58355 11.4236L11.0642 8.94293L11.4177 8.58938L11.0642 8.23582L8.80918 5.98082L8.45562 5.62727L8.10207 5.98082L5.62645 8.45645L5.48 8.60289V8.81V8.82ZM11.4177 7.88227L11.7713 8.23582L12.1248 7.88227L14.8036 5.20355C15.1288 4.87829 15.1288 4.36171 14.8036 4.03645L13.0136 2.24645C12.8186 2.05146 12.5792 2 12.43 2H12.4134L12.3967 2.00111L12.43 2.5C12.3967 2.00111 12.3966 2.00112 12.3965 2.00112L12.3963 2.00114L12.3957 2.00117L12.3947 2.00125L12.3924 2.00142L12.387 2.00184L12.3732 2.00311C12.3628 2.00416 12.3498 2.00567 12.3346 2.00784C12.3049 2.01208 12.2642 2.01925 12.2178 2.03146C12.1396 2.05202 11.9797 2.10317 11.8464 2.23645L9.16273 4.92016L8.80918 5.27372L9.16273 5.62727L11.4177 7.88227ZM1.5 16.5V15.5H15.5V16.5H1.5Z" stroke="#15141A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
5
web/images/editor-toolbar-delete.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M11 3H13.6C14 3 14.3 3.3 14.3 3.6C14.3 3.9 14 4.2 13.7 4.2H13.3V14C13.3 15.1 12.4 16 11.3 16H4.80005C3.70005 16 2.80005 15.1 2.80005 14V4.2H2.40005C2.00005 4.2 1.80005 4 1.80005 3.6C1.80005 3.2 2.00005 3 2.40005 3H5.00005V2C5.00005 0.9 5.90005 0 7.00005 0H9.00005C10.1 0 11 0.9 11 2V3ZM6.90005 1.2L6.30005 1.8V3H9.80005V1.8L9.20005 1.2H6.90005ZM11.4 14.7L12 14.1V4.2H4.00005V14.1L4.60005 14.7H11.4ZM7.00005 12.4C7.00005 12.7 6.70005 13 6.40005 13C6.10005 13 5.80005 12.7 5.80005 12.4V7.6C5.70005 7.3 6.00005 7 6.40005 7C6.80005 7 7.00005 7.3 7.00005 7.6V12.4ZM10.2001 12.4C10.2001 12.7 9.90006 13 9.60006 13C9.30006 13 9.00006 12.7 9.00006 12.4V7.6C9.00006 7.3 9.30006 7 9.60006 7C9.90006 7 10.2001 7.3 10.2001 7.6V12.4Z"
|
||||
fill="black" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 909 B |
3
web/images/editor-toolbar-edit.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.9815 14.3461L14.3875 11.1451C14.3125 10.7361 14.1155 10.3621 13.8195 10.0641L5.0185 1.26214C4.2445 0.489141 2.9855 0.487141 2.2085 1.26214L0.5805 2.89014C-0.1935 3.66514 -0.1935 4.92514 0.5805 5.70014L9.3845 14.5031C9.6805 14.7981 10.0545 14.9941 10.4625 15.0691L13.6645 15.6631C13.7345 15.6751 13.8025 15.6821 13.8705 15.6821C14.1675 15.6821 14.4555 15.5651 14.6695 15.3511C14.9325 15.0871 15.0495 14.7121 14.9815 14.3461ZM2.5285 5.87814L1.3685 4.71814L1.3695 3.87014L3.1895 2.04914L4.0375 2.05014L5.1975 3.21014L2.5285 5.87814ZM10.6895 13.8401C10.5295 13.8101 10.3825 13.7331 10.2665 13.6161L3.4135 6.76314L6.0815 4.09514L12.9345 10.9471C13.0515 11.0651 13.1285 11.2121 13.1595 11.3731L13.6335 13.9301L13.2485 14.3151L10.6895 13.8401Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 869 B |
3
web/images/findbarButton-next.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.999 8.352L5.534 13.818C5.41551 13.9303 5.25786 13.9918 5.09466 13.9895C4.93146 13.9872 4.77561 13.9212 4.66033 13.8057C4.54505 13.6902 4.47945 13.5342 4.47752 13.3709C4.47559 13.2077 4.53748 13.0502 4.65 12.932L9.585 7.998L4.651 3.067C4.53862 2.94864 4.47691 2.79106 4.47903 2.62786C4.48114 2.46466 4.54692 2.30874 4.66233 2.19333C4.77774 2.07792 4.93366 2.01215 5.09686 2.01003C5.26006 2.00792 5.41763 2.06962 5.536 2.182L11 7.647L10.999 8.352Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 578 B |
3
web/images/findbarButton-previous.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.001 8.352L10.466 13.818C10.5845 13.9303 10.7421 13.9918 10.9053 13.9895C11.0685 13.9872 11.2244 13.9212 11.3397 13.8057C11.4549 13.6902 11.5205 13.5342 11.5225 13.3709C11.5244 13.2077 11.4625 13.0502 11.35 12.932L6.416 7.999L11.349 3.067C11.4614 2.94864 11.5231 2.79106 11.521 2.62786C11.5189 2.46466 11.4531 2.30874 11.3377 2.19333C11.2223 2.07792 11.0663 2.01215 10.9031 2.01003C10.7399 2.00792 10.5824 2.06962 10.464 2.182L5 7.647L5.001 8.352Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 578 B |
3
web/images/gv-toolbarButton-download.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.969 10.059C17.262 9.766 17.737 9.766 18.03 10.059C18.323 10.352 18.323 10.827 18.03 11.12L12.15 17H11.35L5.46896 11.12C5.17596 10.827 5.17596 10.352 5.46896 10.059C5.76196 9.766 6.23696 9.766 6.52996 10.059L11 14.529V2.75C11 2.336 11.336 2 11.75 2C12.164 2 12.5 2.336 12.499 2.75V14.529L16.969 10.059ZM4.98193 19.7L5.78193 20.5H17.7169L18.5169 19.7V17.75C18.5169 17.336 18.8529 17 19.2669 17C19.6809 17 20.0169 17.336 20.0169 17.75V19.5C20.0169 20.881 18.8979 22 17.5169 22H5.98193C4.60093 22 3.48193 20.881 3.48193 19.5V17.75C3.48193 17.336 3.81793 17 4.23193 17C4.64593 17 4.98193 17.336 4.98193 17.75V19.7Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 782 B |
BIN
web/images/loading-icon.gif
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
1
web/images/loading.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" style="animation:spinLoadingIcon 1s steps(12,end) infinite"><style>@keyframes spinLoadingIcon{to{transform:rotate(360deg)}}</style><path d="M7 3V1s0-1 1-1 1 1 1 1v2s0 1-1 1-1-1-1-1z"/><path d="M4.63 4.1l-1-1.73S3.13 1.5 4 1c.87-.5 1.37.37 1.37.37l1 1.73s.5.87-.37 1.37c-.87.57-1.37-.37-1.37-.37z" fill-opacity=".93"/><path d="M3.1 6.37l-1.73-1S.5 4.87 1 4c.5-.87 1.37-.37 1.37-.37l1.73 1s.87.5.37 1.37c-.5.87-1.37.37-1.37.37z" fill-opacity=".86"/><path d="M3 9H1S0 9 0 8s1-1 1-1h2s1 0 1 1-1 1-1 1z" fill-opacity=".79"/><path d="M4.1 11.37l-1.73 1S1.5 12.87 1 12c-.5-.87.37-1.37.37-1.37l1.73-1s.87-.5 1.37.37c.5.87-.37 1.37-.37 1.37z" fill-opacity=".72"/><path d="M3.63 13.56l1-1.73s.5-.87 1.37-.37c.87.5.37 1.37.37 1.37l-1 1.73s-.5.87-1.37.37c-.87-.5-.37-1.37-.37-1.37z" fill-opacity=".65"/><path d="M7 15v-2s0-1 1-1 1 1 1 1v2s0 1-1 1-1-1-1-1z" fill-opacity=".58"/><path d="M10.63 14.56l-1-1.73s-.5-.87.37-1.37c.87-.5 1.37.37 1.37.37l1 1.73s.5.87-.37 1.37c-.87.5-1.37-.37-1.37-.37z" fill-opacity=".51"/><path d="M13.56 12.37l-1.73-1s-.87-.5-.37-1.37c.5-.87 1.37-.37 1.37-.37l1.73 1s.87.5.37 1.37c-.5.87-1.37.37-1.37.37z" fill-opacity=".44"/><path d="M15 9h-2s-1 0-1-1 1-1 1-1h2s1 0 1 1-1 1-1 1z" fill-opacity=".37"/><path d="M14.56 5.37l-1.73 1s-.87.5-1.37-.37c-.5-.87.37-1.37.37-1.37l1.73-1s.87-.5 1.37.37c.5.87-.37 1.37-.37 1.37z" fill-opacity=".3"/><path d="M9.64 3.1l.98-1.66s.5-.874 1.37-.37c.87.5.37 1.37.37 1.37l-1 1.73s-.5.87-1.37.37c-.87-.5-.37-1.37-.37-1.37z" fill-opacity=".23"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
3
web/images/messageBar_closingButton.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.85822 8.84922L4.85322 11.8542C4.75891 11.9453 4.63261 11.9957 4.50151 11.9946C4.37042 11.9934 4.24501 11.9408 4.15231 11.8481C4.0596 11.7554 4.00702 11.63 4.00588 11.4989C4.00474 11.3678 4.05514 11.2415 4.14622 11.1472L7.15122 8.14222V7.85922L4.14622 4.85322C4.05514 4.75891 4.00474 4.63261 4.00588 4.50151C4.00702 4.37042 4.0596 4.24501 4.15231 4.15231C4.24501 4.0596 4.37042 4.00702 4.50151 4.00588C4.63261 4.00474 4.75891 4.05514 4.85322 4.14622L7.85822 7.15122H8.14122L11.1462 4.14622C11.2405 4.05514 11.3668 4.00474 11.4979 4.00588C11.629 4.00702 11.7544 4.0596 11.8471 4.15231C11.9398 4.24501 11.9924 4.37042 11.9936 4.50151C11.9947 4.63261 11.9443 4.75891 11.8532 4.85322L8.84822 7.85922V8.14222L11.8532 11.1472C11.9443 11.2415 11.9947 11.3678 11.9936 11.4989C11.9924 11.63 11.9398 11.7554 11.8471 11.8481C11.7544 11.9408 11.629 11.9934 11.4979 11.9946C11.3668 11.9957 11.2405 11.9453 11.1462 11.8542L8.14122 8.84922L8.14222 8.85022L7.85822 8.84922Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
web/images/messageBar_info.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.625 0.5C6.64009 0.5 5.66482 0.693993 4.75487 1.0709C3.84493 1.44781 3.01814 2.00026 2.3217 2.6967C1.62526 3.39314 1.07281 4.21993 0.695904 5.12987C0.318993 6.03982 0.125 7.01509 0.125 8C0.125 8.98491 0.318993 9.96018 0.695904 10.8701C1.07281 11.7801 1.62526 12.6069 2.3217 13.3033C3.01814 13.9997 3.84493 14.5522 4.75487 14.9291C5.66482 15.306 6.64009 15.5 7.625 15.5C9.61412 15.5 11.5218 14.7098 12.9283 13.3033C14.3348 11.8968 15.125 9.98912 15.125 8C15.125 6.01088 14.3348 4.10322 12.9283 2.6967C11.5218 1.29018 9.61412 0.5 7.625 0.5ZM8.25 11.375C8.25 11.5408 8.18415 11.6997 8.06694 11.8169C7.94973 11.9342 7.79076 12 7.625 12C7.45924 12 7.30027 11.9342 7.18306 11.8169C7.06585 11.6997 7 11.5408 7 11.375V6.938C7 6.77224 7.06585 6.61327 7.18306 6.49606C7.30027 6.37885 7.45924 6.313 7.625 6.313C7.79076 6.313 7.94973 6.37885 8.06694 6.49606C8.18415 6.61327 8.25 6.77224 8.25 6.938V11.375ZM8.25 5L8 5.25H7.25L7 5V4.25L7.25 4H8L8.25 4.25V5Z" fill="black" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
web/images/messageBar_warning.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.8748 12.037L9.37782 2.037C8.99682 1.346 8.31082 1 7.62482 1C6.93882 1 6.25282 1.346 5.87282 2.037L0.375823 12.037C-0.358177 13.37 0.606823 15 2.12782 15H13.1228C14.6428 15 15.6078 13.37 14.8748 12.037ZM8.24982 11.75L7.99982 12H7.24982L6.99982 11.75V11L7.24982 10.75H7.99982L8.24982 11V11.75ZM8.24982 9.062C8.24982 9.22776 8.18398 9.38673 8.06677 9.50394C7.94955 9.62115 7.79058 9.687 7.62482 9.687C7.45906 9.687 7.30009 9.62115 7.18288 9.50394C7.06567 9.38673 6.99982 9.22776 6.99982 9.062V5.625C6.99982 5.45924 7.06567 5.30027 7.18288 5.18306C7.30009 5.06585 7.45906 5 7.62482 5C7.79058 5 7.94955 5.06585 8.06677 5.18306C8.18398 5.30027 8.24982 5.45924 8.24982 5.625V9.062Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 811 B |
3
web/images/secondaryToolbarButton-documentProperties.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 1.5C4.41015 1.5 1.5 4.41015 1.5 8C1.5 11.5899 4.41015 14.5 8 14.5C11.5899 14.5 14.5 11.5899 14.5 8C14.5 4.41015 11.5899 1.5 8 1.5ZM0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8ZM8.75 4V5.5H7.25V4H8.75ZM8.75 12V7H7.25V12H8.75Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 417 B |
3
web/images/secondaryToolbarButton-firstPage.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 3.5H2V5H14V3.5ZM8 8.811L12.939 13.75L14.001 12.689L8.531 7.219C8.238 6.926 7.763 6.926 7.47 7.219L2 12.689L3.061 13.75L8 8.811Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 260 B |
3
web/images/secondaryToolbarButton-handTool.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.75 2.125C7.75 1.78021 8.03021 1.5 8.375 1.5C8.71979 1.5 9 1.78021 9 2.125V3.125V8H10.5V3.125C10.5 2.78021 10.7802 2.5 11.125 2.5C11.4698 2.5 11.75 2.78021 11.75 3.125V4.625V8H13.25V4.625C13.25 4.28021 13.5302 4 13.875 4C14.2198 4 14.5 4.28021 14.5 4.625V12.0188L13.3802 13.6628C13.2954 13.7872 13.25 13.9344 13.25 14.085V16H14.75V14.3162L15.8698 12.6722C15.9546 12.5478 16 12.4006 16 12.25V4.625C16 3.45179 15.0482 2.5 13.875 2.5C13.6346 2.5 13.4035 2.53996 13.188 2.6136C12.959 1.68724 12.1219 1 11.125 1C10.8235 1 10.5366 1.06286 10.2768 1.17618C9.9281 0.478968 9.20726 0 8.375 0C7.54274 0 6.8219 0.478968 6.47323 1.17618C6.21337 1.06286 5.9265 1 5.625 1C4.45179 1 3.5 1.95179 3.5 3.125V7.25317C2.66504 6.54282 1.41035 6.58199 0.621672 7.37067C-0.208221 8.20056 -0.208221 9.54644 0.621672 10.3763L0.62188 10.3765L5.499 15.2498V16H6.999V14.939C6.999 14.74 6.9199 14.5491 6.77912 14.4085L1.68233 9.31567C1.43823 9.07156 1.43823 8.67544 1.68233 8.43133C1.92644 8.18722 2.32257 8.18722 2.56667 8.43133L3.71967 9.58433C3.93417 9.79883 4.25676 9.863 4.53701 9.74691C4.81727 9.63082 5 9.35735 5 9.054V3.125C5 2.78021 5.28022 2.5 5.625 2.5C5.96921 2.5 6.24906 2.77927 6.25 3.12326V8H7.75L7.75 3.125L7.75 3.12178V2.125Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
3
web/images/secondaryToolbarButton-lastPage.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 8.189L12.939 3.25L14 4.311L8.531 9.781C8.238 10.074 7.763 10.074 7.47 9.781L2 4.311L3.061 3.25L8 8.189ZM14 13.5V12H2V13.5H14Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 257 B |
3
web/images/secondaryToolbarButton-rotateCcw.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.4105 4.83612L4.77001 6.19601C5.06701 6.49201 4.85701 7.00001 4.43701 7.00001H0.862006C0.602006 7.00001 0.391006 6.78901 0.391006 6.52901V2.95401C0.391006 2.53401 0.899006 2.32401 1.19601 2.62101L2.32796 3.75328C3.67958 1.78973 5.9401 0.5 8.5 0.5C12.636 0.5 16 3.864 16 8C16 12.136 12.636 15.5 8.5 15.5C4.704 15.5 1.566 12.663 1.075 9H2.59C3.068 11.833 5.532 14 8.5 14C11.809 14 14.5 11.309 14.5 8C14.5 4.691 11.809 2 8.5 2C6.35262 2 4.46893 3.13503 3.4105 4.83612Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 596 B |
3
web/images/secondaryToolbarButton-rotateCw.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5895 4.83613L11.23 6.19601C10.933 6.49201 11.143 7.00001 11.563 7.00001H15.138C15.398 7.00001 15.609 6.78901 15.609 6.52901V2.95401C15.609 2.53401 15.101 2.32401 14.804 2.62101L13.672 3.75328C12.3204 1.78973 10.0599 0.5 7.5 0.5C3.364 0.5 0 3.864 0 8C0 12.136 3.364 15.5 7.5 15.5C11.296 15.5 14.434 12.663 14.925 9H13.41C12.932 11.833 10.468 14 7.5 14C4.191 14 1.5 11.309 1.5 8C1.5 4.691 4.191 2 7.5 2C9.64738 2 11.5311 3.13503 12.5895 4.83613Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 576 B |
3
web/images/secondaryToolbarButton-scrollHorizontal.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 3.78C3 2.7621 2.13279 2.11834 1.25 2.01476V2H1V3.5C1.18133 3.5 1.32279 3.5609 1.40708 3.63029C1.48961 3.69823 1.5 3.75458 1.5 3.78V11.72C1.5 11.7454 1.48961 11.8018 1.40708 11.8697C1.32279 11.9391 1.18133 12 1 12V13.5H1.25V13.4852C2.13279 13.3817 3 12.7379 3 11.72V3.78ZM10.5 4C10.5 3.72386 10.2761 3.5 10 3.5H6.5C6.22386 3.5 6 3.72386 6 4V11.5C6 11.7761 6.22386 12 6.5 12H10C10.2761 12 10.5 11.7761 10.5 11.5V4ZM10 2C11.1046 2 12 2.89543 12 4V11.5C12 12.6046 11.1046 13.5 10 13.5H6.5C5.39543 13.5 4.5 12.6046 4.5 11.5V4C4.5 2.89543 5.39543 2 6.5 2H10ZM15.5 2H15.25V2.01476C14.3672 2.11834 13.5 2.7621 13.5 3.78V11.72C13.5 12.7379 14.3672 13.3817 15.25 13.4852V13.5H15.5V12C15.3187 12 15.1772 11.9391 15.0929 11.8697C15.0104 11.8018 15 11.7454 15 11.72V3.78C15 3.75458 15.0104 3.69823 15.0929 3.63029C15.1772 3.5609 15.3187 3.5 15.5 3.5V2Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 971 B |
3
web/images/secondaryToolbarButton-scrollPage.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.5 2C3.5 1.72421 3.72421 1.5 4 1.5H12C12.2758 1.5 12.5 1.72421 12.5 2V14C12.5 14.2758 12.2758 14.5 12 14.5H4C3.72421 14.5 3.5 14.2758 3.5 14V2ZM4 0C2.89579 0 2 0.895786 2 2V14C2 15.1042 2.89579 16 4 16H12C13.1042 16 14 15.1042 14 14V2C14 0.895786 13.1042 0 12 0H4ZM5.89301 6H7.25V10H5.89301C5.54301 10 5.36801 10.423 5.61501 10.67L7.72101 12.776C7.87401 12.929 8.12301 12.929 8.27601 12.776L10.383 10.669C10.63 10.422 10.455 9.99902 10.105 9.99902H8.75V6H10.106C10.456 6 10.632 5.577 10.383 5.331L8.27601 3.224C8.12301 3.071 7.87401 3.071 7.72101 3.224L5.61501 5.33C5.36801 5.577 5.54301 6 5.89301 6Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 731 B |
3
web/images/secondaryToolbarButton-scrollVertical.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 1V1.25H2.01476C2.11834 2.13279 2.7621 3 3.78 3H11.72C12.7379 3 13.3817 2.13279 13.4852 1.25H13.5V1H12C12 1.18133 11.9391 1.32279 11.8697 1.40708C11.8018 1.48961 11.7454 1.5 11.72 1.5H3.78C3.75458 1.5 3.69823 1.48961 3.63029 1.40708C3.5609 1.32279 3.5 1.18133 3.5 1H2ZM4 6C3.72386 6 3.5 6.22386 3.5 6.5V10C3.5 10.2761 3.72386 10.5 4 10.5H11.5C11.7761 10.5 12 10.2761 12 10V6.5C12 6.22386 11.7761 6 11.5 6H4ZM2 6.5C2 5.39543 2.89543 4.5 4 4.5H11.5C12.6046 4.5 13.5 5.39543 13.5 6.5V10C13.5 11.1046 12.6046 12 11.5 12H4C2.89543 12 2 11.1046 2 10V6.5ZM3.78 13.5C2.7621 13.5 2.11834 14.3672 2.01476 15.25H2V15.5H3.5C3.5 15.3187 3.5609 15.1772 3.63029 15.0929C3.69823 15.0104 3.75458 15 3.78 15H11.72C11.7454 15 11.8018 15.0104 11.8697 15.0929C11.9391 15.1772 12 15.3187 12 15.5H13.5V15.25H13.4852C13.3817 14.3672 12.7379 13.5 11.72 13.5H3.78Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 969 B |
3
web/images/secondaryToolbarButton-scrollWrapped.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.5 1C2.5 1.27579 2.72421 1.5 3 1.5H5C5.27579 1.5 5.5 1.27579 5.5 1H7C7 2.10421 6.10421 3 5 3H3C1.89579 3 1 2.10421 1 1H2.5ZM2.5 6C2.5 5.72421 2.72421 5.5 3 5.5H5C5.27579 5.5 5.5 5.72421 5.5 6V10C5.5 10.2758 5.27579 10.5 5 10.5H3C2.72421 10.5 2.5 10.2758 2.5 10V6ZM3 4C1.89579 4 1 4.89579 1 6V10C1 11.1042 1.89579 12 3 12H5C6.10421 12 7 11.1042 7 10V6C7 4.89579 6.10421 4 5 4H3ZM10 6C10 5.72421 10.2242 5.5 10.5 5.5H12.5C12.7758 5.5 13 5.72421 13 6V10C13 10.2758 12.7758 10.5 12.5 10.5H10.5C10.2242 10.5 10 10.2758 10 10V6ZM10.5 4C9.39579 4 8.5 4.89579 8.5 6V10C8.5 11.1042 9.39579 12 10.5 12H12.5C13.6042 12 14.5 11.1042 14.5 10V6C14.5 4.89579 13.6042 4 12.5 4H10.5ZM3 14.5C2.72421 14.5 2.5 14.7242 2.5 15H1C1 13.8958 1.89579 13 3 13H5C6.10421 13 7 13.8958 7 15H5.5C5.5 14.7242 5.27579 14.5 5 14.5H3ZM10 15C10 14.7242 10.2242 14.5 10.5 14.5H12.5C12.7758 14.5 13 14.7242 13 15H14.5C14.5 13.8958 13.6042 13 12.5 13H10.5C9.39579 13 8.5 13.8958 8.5 15H10ZM10.5 1.5C10.2242 1.5 10 1.27579 10 1H8.5C8.5 2.10421 9.39579 3 10.5 3H12.5C13.6042 3 14.5 2.10421 14.5 1H13C13 1.27579 12.7758 1.5 12.5 1.5H10.5Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
web/images/secondaryToolbarButton-selectTool.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.371588 2.93131C-0.203366 1.33422 1.3342 -0.20335 2.93129 0.371603L2.93263 0.372085L12.0716 3.68171C12.0718 3.68178 12.0714 3.68163 12.0716 3.68171C13.4459 4.17758 13.8478 5.9374 12.8076 6.9776L11.8079 7.97727L14.6876 10.8569C15.4705 11.6398 15.4705 12.9047 14.6876 13.6876L13.6476 14.7276C12.8647 15.5105 11.5998 15.5105 10.8169 14.7276L7.93725 11.8479L6.97758 12.8076C5.93739 13.8478 4.17779 13.4465 3.68192 12.0722C3.68184 12.072 3.682 12.0724 3.68192 12.0722L0.371588 2.93131ZM1.78292 2.42323C1.78298 2.4234 1.78286 2.42305 1.78292 2.42323L5.09281 11.5629C5.21725 11.9082 5.65728 12.0066 5.91692 11.7469L7.93725 9.72661L11.8776 13.6669C12.0747 13.864 12.3898 13.864 12.5869 13.6669L13.6269 12.6269C13.824 12.4298 13.824 12.1147 13.6269 11.9176L9.68659 7.97727L11.7469 5.91694C12.0066 5.65729 11.9081 5.21727 11.5629 5.09283L11.5619 5.09245L2.42321 1.78293C2.42304 1.78287 2.42339 1.783 2.42321 1.78293C2.02067 1.63847 1.63846 2.02069 1.78292 2.42323Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
web/images/secondaryToolbarButton-spreadEven.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M2 3.5C1.72421 3.5 1.5 3.72421 1.5 4V12.5C1.5 12.7758 1.72421 13 2 13H7.25V3.5H2ZM14 13H8.75V3.5H14C14.2758 3.5 14.5 3.72421 14.5 4V12.5C14.5 12.7758 14.2758 13 14 13ZM0 4C0 2.89579 0.895786 2 2 2H14C15.1042 2 16 2.89579 16 4V12.5C16 13.6042 15.1042 14.5 14 14.5H2C0.895786 14.5 0 13.6042 0 12.5V4ZM10 6.5H11.5V7.5H10V9H11.5V10H10V11.5H12.25C12.6642 11.5 13 11.1642 13 10.75V5.75C13 5.33579 12.6642 5 12.25 5H10V6.5ZM4.5 6.5H3V5H5.25C5.66421 5 6 5.33579 6 5.75V7.75C6 8.03408 5.8395 8.29378 5.58541 8.42082L4.5 8.96353V10H6V11.5H3.75C3.33579 11.5 3 11.1642 3 10.75V8.5C3 8.21592 3.1605 7.95622 3.41459 7.82918L4.5 7.28647V6.5Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 775 B |
3
web/images/secondaryToolbarButton-spreadNone.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 1.5C3.72421 1.5 3.5 1.72421 3.5 2V14C3.5 14.2758 3.72421 14.5 4 14.5H12C12.2758 14.5 12.5 14.2758 12.5 14V2C12.5 1.72421 12.2758 1.5 12 1.5H4ZM2 2C2 0.895786 2.89579 0 4 0H12C13.1042 0 14 0.895786 14 2V14C14 15.1042 13.1042 16 12 16H4C2.89579 16 2 15.1042 2 14V2Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 395 B |
3
web/images/secondaryToolbarButton-spreadOdd.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.5 4C1.5 3.72421 1.72421 3.5 2 3.5H7.25V13H2C1.72421 13 1.5 12.7758 1.5 12.5V4ZM8.75 13V3.5H14C14.2758 3.5 14.5 3.72421 14.5 4V12.5C14.5 12.7758 14.2758 13 14 13H8.75ZM2 2C0.895786 2 0 2.89579 0 4V12.5C0 13.6042 0.895786 14.5 2 14.5H14C15.1042 14.5 16 13.6042 16 12.5V4C16 2.89579 15.1042 2 14 2H2ZM4.75 5H3V6.5H4V11.5H5.5V5.75C5.5 5.33579 5.16421 5 4.75 5ZM10 6.5H11.5V7.28647L10.4146 7.82918C10.1605 7.95622 10 8.21592 10 8.5V10.75C10 11.1642 10.3358 11.5 10.75 11.5H13V10H11.5V8.96353L12.5854 8.42082C12.8395 8.29378 13 8.03408 13 7.75V5.75C13 5.33579 12.6642 5 12.25 5H10V6.5Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 711 B |
3
web/images/toolbarButton-bookmark.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 3.5C1.72421 3.5 1.5 3.72421 1.5 4V12C1.5 12.2758 1.72421 12.5 2 12.5H14C14.2758 12.5 14.5 12.2758 14.5 12V4C14.5 3.72421 14.2758 3.5 14 3.5H2ZM0 4C0 2.89579 0.895786 2 2 2H14C15.1042 2 16 2.89579 16 4V12C16 13.1042 15.1042 14 14 14H2C0.895786 14 0 13.1042 0 12V4ZM8.75 8.75H7.25V7.25H8.75V8.75ZM8.00001 4.625C5.91142 4.625 4.14736 5.94291 3.45159 7.77847L3.36761 8L3.45159 8.22153C4.14736 10.0571 5.91142 11.375 8.00001 11.375C10.0886 11.375 11.8527 10.0571 12.5484 8.22153L12.6324 8L12.5484 7.77847C11.8527 5.94291 10.0886 4.625 8.00001 4.625ZM8.00001 10.125C6.53912 10.125 5.28508 9.25455 4.71282 8C5.28508 6.74545 6.53912 5.875 8.00001 5.875C9.4609 5.875 10.7149 6.74545 11.2872 8C10.7149 9.25455 9.4609 10.125 8.00001 10.125Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 861 B |
3
web/images/toolbarButton-currentOutlineItem.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.803 4.74998V6.02436C10.803 6.39302 10.3571 6.57793 10.0967 6.31753L7.87716 4.098C7.71566 3.93649 7.71566 3.67434 7.87716 3.51283L10.0967 1.29329C10.3571 1.0329 10.8036 1.21722 10.8036 1.58588V3.24998H15V4.74998H10.803ZM8 1.24998H3V2.74998H6.5L8 1.24998ZM6.5 5.24998H3V6.74998H8L6.5 5.24998ZM3 13.25H15V14.75H3V13.25ZM6 9.24998H15V10.75H6V9.24998ZM1.5 5.24998H0V6.74998H1.5V5.24998ZM0 13.25H1.5V14.75H0V13.25ZM1.5 1.24998H0V2.74998H1.5V1.24998ZM3 9.24998H4.5V10.75H3V9.24998Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 607 B |
4
web/images/toolbarButton-download.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.79407 7.31811H7.86307C7.41807 7.31811 7.19407 7.85711 7.50907 8.17211L10.1911 10.8541C10.3861 11.0491 10.7031 11.0491 10.8981 10.8541L13.5801 8.17211C13.8951 7.85711 13.6721 7.31811 13.2261 7.31811H11.2941V4.38211H11.2961V3.13211H11.2941V2.30811H9.79407V3.13211H9.79107V4.38211H9.79507V7.31811H9.79407Z" fill="black"/>
|
||||
<path d="M14 3.13208H12.796V4.38208H14C14.345 4.38208 14.625 4.66208 14.625 5.00708V13.0071C14.625 13.3521 14.345 13.6321 14 13.6321H2C1.655 13.6321 1.375 13.3521 1.375 13.0071V3.00708C1.375 2.66208 1.655 2.38208 2 2.38208H5.643C5.82 2.38208 5.989 2.45808 6.108 2.58908L7.536 4.17508C7.654 4.30708 7.823 4.38208 8 4.38208H8.291V3.13208H8.278L7.036 1.75208C6.681 1.35808 6.173 1.13208 5.642 1.13208H2C0.966 1.13208 0.125 1.97308 0.125 3.00708V13.0071C0.125 14.0411 0.966 14.8821 2 14.8821H14C15.034 14.8821 15.875 14.0411 15.875 13.0071V5.00708C15.875 3.97308 15.034 3.13208 14 3.13208Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
5
web/images/toolbarButton-editorFreeText.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 2.5C2.72421 2.5 2.5 2.72421 2.5 3V4.75H1V3C1 1.89579 1.89579 1 3 1H13C14.1042 1 15 1.89579 15 3V4.75H13.5V3C13.5 2.72421 13.2758 2.5 13 2.5H3Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 15H5V13.5H11V15Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.75 2.25V14.25H7.25V2.25H8.75Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 498 B |
6
web/images/toolbarButton-editorHighlight.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.10918 11.66C7.24918 11.8 7.43918 11.88 7.63918 11.88C7.83918 11.88 8.02918 11.8 8.16918 11.66L14.9192 4.91C15.2692 4.57 15.4592 4.11 15.4592 3.62C15.4592 3.13 15.2692 2.67 14.9192 2.33L13.1292 0.54C12.7892 0.19 12.3292 0 11.8392 0C11.3492 0 10.8892 0.2 10.5492 0.54L3.79918 7.29C3.50918 7.58 3.50918 8.06 3.79918 8.35L4.38988 8.9407L1.40918 11.93H5.64918L6.51419 11.065L7.10918 11.66ZM7.63918 10.07L5.38918 7.82V7.81L7.8648 5.33438L10.1198 7.58938L7.63918 10.07ZM11.1805 6.52872L13.8592 3.85C13.9892 3.72 13.9892 3.52 13.8592 3.39L12.0692 1.6C11.9892 1.52 11.8892 1.5 11.8392 1.5C11.8392 1.5 11.6892 1.51 11.6092 1.59L8.92546 4.27372L11.1805 6.52872Z" fill="#000"/>
|
||||
<path d="M0.40918 14H15.4092V16H0.40918V14Z" fill="#000"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 910 B |
4
web/images/toolbarButton-editorInk.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.49913 12.6251C2.61913 12.6251 2.73913 12.6051 2.85713 12.5661L6.29013 11.4201L13.2891 4.4221C14.0191 3.6911 14.0191 2.5011 13.2891 1.7701L12.2291 0.710098C11.4971 -0.0199023 10.3091 -0.0199023 9.57713 0.710098L2.57813 7.7091L1.43313 11.1451C1.29813 11.5511 1.40213 11.9931 1.70513 12.2951C1.92113 12.5101 2.20613 12.6251 2.49913 12.6251ZM10.4611 1.5951C10.7031 1.3511 11.1021 1.3511 11.3441 1.5951L12.4051 2.6561C12.6491 2.8991 12.6491 3.2961 12.4051 3.5391L11.3401 4.6051L9.39513 2.6601L10.4611 1.5951ZM3.67013 8.3851L8.51013 3.5451L10.4541 5.4891L5.61413 10.3301L2.69713 11.3031L3.67013 8.3851Z" fill="black"/>
|
||||
<path d="M14.8169 13.314L13.0229 13.862C12.3309 14.073 11.5909 14.111 10.8859 13.968L8.80391 13.551C7.58491 13.308 6.29791 13.48 5.18491 14.036C3.95291 14.652 2.46691 14.412 1.49191 13.436L1.44091 13.385L0.60791 14.321C1.46291 15.175 2.59991 15.625 3.75291 15.625C4.42891 15.625 5.10991 15.471 5.74391 15.153C6.60891 14.721 7.60891 14.586 8.55891 14.777L10.6409 15.194C11.5509 15.376 12.5009 15.327 13.3879 15.056L15.1819 14.508L14.8169 13.314Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
6
web/images/toolbarButton-editorSignature.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.618 12.868L11.254 13.25H10.423C10.335 13.396 10.182 13.5 10 13.5H2C1.725 13.5 1.5 13.275 1.5 13V2C1.5 1.725 1.725 1.5 2 1.5H6.939L7 1.561V4.5C7 4.776 7.224 5 7.5 5H10.439L10.5 5.061V6.515L12 5.015V4.75C12 4.551 11.921 4.36 11.78 4.22L7.78 0.22C7.64 0.079 7.449 0 7.25 0H2C0.895 0 0 0.895 0 2V13C0 14.105 0.895 15 2 15H10C11.105 15 12 14.105 12 13V12.486L11.618 12.868Z" fill="black"/>
|
||||
<path d="M13.836 4.946C13.962 4.819 14.13 4.75 14.309 4.75C14.487 4.75 14.655 4.819 14.782 4.945L15.805 5.969C15.931 6.094 16 6.262 16 6.441C16 6.621 15.931 6.789 15.805 6.915L14.9397 7.78033L12.9707 5.81134L13.836 4.946Z" fill="black"/>
|
||||
<path d="M12.4403 6.34167L8.84 9.942C8.793 9.989 8.767 10.052 8.767 10.119V11.75C8.767 11.888 8.879 12 9.017 12H10.616L10.793 11.927L14.4093 8.31067L12.4403 6.34167Z" fill="black"/>
|
||||
<path d="M7.517 9.84799V10.016H7.516V11.517C7.49168 11.4944 7.46584 11.4738 7.44007 11.4532C7.39062 11.4138 7.34148 11.3746 7.304 11.322L7.081 11.009C7.071 10.992 7.037 10.946 6.966 10.957C6.93 10.962 6.882 10.982 6.859 11.046C6.705 11.486 6.492 12.087 5.938 12.087H5.921C5.317 12.07 5.009 11.557 4.745 9.98599L4.382 10.954C4.146 11.585 3.535 12.009 2.861 12.009H2.5V10.759H2.861C3.017 10.759 3.158 10.661 3.213 10.515L3.883 8.72799C4.05 8.28199 4.494 8.01899 4.955 8.07299C5.434 8.12799 5.796 8.48599 5.858 8.96399C5.921 9.43699 5.98 9.80399 6.035 10.085C6.24 9.89099 6.504 9.76099 6.795 9.71999C7.048 9.68399 7.291 9.74599 7.517 9.84799Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
8
web/images/toolbarButton-editorStamp.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="black">
|
||||
<path d="M3 1a2 2 0 0 0-2 2l0 10a2 2 0 0 0 2 2l10 0a2 2 0 0 0 2-2l0-10a2 2 0 0 0-2-2L3 1zm10.75 12.15-.6.6-10.3 0-.6-.6 0-10.3.6-.6 10.3 0 .6.6 0 10.3z"/>
|
||||
<path d="m11 12-6 0a1 1 0 0 1-1-1l0-1.321a.75.75 0 0 1 .218-.529L6.35 7.005a.75.75 0 0 1 1.061-.003l2.112 2.102.612-.577a.75.75 0 0 1 1.047.017l.6.605a.75.75 0 0 1 .218.529L12 11a1 1 0 0 1-1 1z"/>
|
||||
<path d="m11.6 5-1.2 0-.4.4 0 1.2.4.4 1.2 0 .4-.4 0-1.2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 734 B |
3
web/images/toolbarButton-menuArrow.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.23336 10.4664L11.8474 6.85339C11.894 6.8071 11.931 6.75203 11.9563 6.69136C11.9816 6.63069 11.9946 6.56562 11.9946 6.49989C11.9946 6.43417 11.9816 6.3691 11.9563 6.30843C11.931 6.24776 11.894 6.19269 11.8474 6.14639C11.7536 6.05266 11.6264 6 11.4939 6C11.3613 6 11.2341 6.05266 11.1404 6.14639L7.99236 9.29339L4.84736 6.14739C4.75305 6.05631 4.62675 6.00592 4.49566 6.00706C4.36456 6.0082 4.23915 6.06078 4.14645 6.15348C4.05374 6.24619 4.00116 6.37159 4.00002 6.50269C3.99888 6.63379 4.04928 6.76009 4.14036 6.85439L7.75236 10.4674L8.23336 10.4664Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 681 B |
3
web/images/toolbarButton-openFile.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.4287 1.08398C10.5111 1.02905 10.608 0.999824 10.707 1H14.7L15 1.3V5.293C15 5.39194 14.9706 5.48864 14.9156 5.57088C14.8606 5.65311 14.7824 5.71718 14.691 5.75498C14.5996 5.79277 14.499 5.80259 14.402 5.78319C14.3049 5.76379 14.2159 5.71605 14.146 5.646L12.973 4.473L12.692 4.192L9.067 7.817C8.94923 7.93347 8.79034 7.99888 8.6247 7.99907C8.45907 7.99925 8.30003 7.93421 8.182 7.818C8.06518 7.70036 7.99962 7.54129 7.99962 7.3755C7.99962 7.20971 8.06518 7.05065 8.182 6.933L11.807 3.308L10.353 1.854C10.2829 1.78407 10.2351 1.6949 10.2158 1.59779C10.1964 1.50068 10.2063 1.40001 10.2442 1.30854C10.2821 1.21707 10.3464 1.13891 10.4287 1.08398ZM7.81694 2.06694C7.69973 2.18415 7.54076 2.25 7.375 2.25H2.85L2.25 2.85V13.15L2.85 13.75H13.15L13.75 13.15V8.625C13.75 8.45924 13.8158 8.30027 13.9331 8.18306C14.0503 8.06585 14.2092 8 14.375 8C14.5408 8 14.6997 8.06585 14.8169 8.18306C14.9342 8.30027 15 8.45924 15 8.625V13C15 13.5304 14.7893 14.0391 14.4142 14.4142C14.0391 14.7893 13.5304 15 13 15H3C2.46957 15 1.96086 14.7893 1.58579 14.4142C1.21071 14.0391 1 13.5304 1 13V3C1 2.46957 1.21071 1.96086 1.58579 1.58579C1.96086 1.21071 2.46957 1 3 1H7.375C7.54076 1 7.69973 1.06585 7.81694 1.18306C7.93415 1.30027 8 1.45924 8 1.625C8 1.79076 7.93415 1.94973 7.81694 2.06694Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
3
web/images/toolbarButton-pageDown.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.35176 10.9989L13.8178 5.53391C13.876 5.47594 13.9222 5.40702 13.9537 5.33113C13.9851 5.25524 14.0013 5.17387 14.0012 5.0917C14.0011 5.00954 13.9848 4.9282 13.9531 4.85238C13.9215 4.77656 13.8751 4.70775 13.8168 4.64991C13.6991 4.53309 13.5401 4.46753 13.3743 4.46753C13.2085 4.46753 13.0494 4.53309 12.9318 4.64991L7.99776 9.58491L3.06776 4.65091C2.9494 4.53853 2.79183 4.47682 2.62863 4.47894C2.46542 4.48106 2.3095 4.54683 2.19409 4.66224C2.07868 4.77765 2.01291 4.93357 2.01079 5.09677C2.00868 5.25997 2.07039 5.41754 2.18276 5.53591L7.64776 10.9999L8.35176 10.9989Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 701 B |
3
web/images/toolbarButton-pageUp.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.35179 5.001L13.8178 10.466C13.876 10.524 13.9222 10.5929 13.9537 10.6688C13.9852 10.7447 14.0013 10.826 14.0012 10.9082C14.0011 10.9904 13.9848 11.0717 13.9531 11.1475C13.9215 11.2234 13.8751 11.2922 13.8168 11.35C13.6991 11.4668 13.5401 11.5324 13.3743 11.5324C13.2085 11.5324 13.0494 11.4668 12.9318 11.35L7.99879 6.416L3.06679 11.349C2.94842 11.4614 2.79085 11.5231 2.62765 11.521C2.46445 11.5189 2.30853 11.4531 2.19312 11.3377C2.07771 11.2223 2.01193 11.0663 2.00982 10.9031C2.0077 10.7399 2.06941 10.5824 2.18179 10.464L7.64779 5L8.35179 5.001Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 682 B |
3
web/images/toolbarButton-presentationMode.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.5 3C1.5 2.72421 1.72421 2.5 2 2.5H14C14.2758 2.5 14.5 2.72421 14.5 3V11C14.5 11.2758 14.2758 11.5 14 11.5H2C1.72421 11.5 1.5 11.2758 1.5 11V3ZM2 1C0.895786 1 0 1.89579 0 3V11C0 12.1042 0.895786 13 2 13H2.64979L1.35052 15.2499L2.64949 16L4.38194 13H11.6391L13.3715 16L14.6705 15.2499L13.3712 13H14C15.1042 13 16 12.1042 16 11V3C16 1.89579 15.1042 1 14 1H2ZM5.79501 4.64401V9.35601C5.79501 9.85001 6.32901 10.159 6.75701 9.91401L10.88 7.55801C11.312 7.31201 11.312 6.68901 10.88 6.44201L6.75701 4.08601C6.32801 3.84101 5.79501 4.15001 5.79501 4.64401Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 681 B |
3
web/images/toolbarButton-print.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 4H12V2C12 1.46957 11.7893 0.960859 11.4142 0.585786C11.0391 0.210714 10.5304 0 10 0L6 0C5.46957 0 4.96086 0.210714 4.58579 0.585786C4.21071 0.960859 4 1.46957 4 2V4H3C2.46957 4 1.96086 4.21071 1.58579 4.58579C1.21071 4.96086 1 5.46957 1 6V11C1 11.5304 1.21071 12.0391 1.58579 12.4142C1.96086 12.7893 2.46957 13 3 13H4V14C4 14.5304 4.21071 15.0391 4.58579 15.4142C4.96086 15.7893 5.46957 16 6 16H10C10.5304 16 11.0391 15.7893 11.4142 15.4142C11.7893 15.0391 12 14.5304 12 14V13H13C13.5304 13 14.0391 12.7893 14.4142 12.4142C14.7893 12.0391 15 11.5304 15 11V6C15 5.46957 14.7893 4.96086 14.4142 4.58579C14.0391 4.21071 13.5304 4 13 4V4ZM10.75 14.15L10.15 14.75H5.85L5.25 14.15V10H10.75V14.15ZM10.75 4H5.25V1.85L5.85 1.25H10.15L10.75 1.85V4V4ZM13 7.6L12.6 8H11.4L11 7.6V6.4L11.4 6H12.6L13 6.4V7.6Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 927 B |
3
web/images/toolbarButton-search.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.089 10.973L13.934 14.817C13.9918 14.8754 14.0605 14.9218 14.1364 14.9534C14.2122 14.9851 14.2936 15.0013 14.3757 15.0012C14.4579 15.0011 14.5392 14.9847 14.6149 14.9529C14.6907 14.9211 14.7594 14.8746 14.817 14.816C14.875 14.7579 14.921 14.6889 14.9523 14.613C14.9836 14.5372 14.9997 14.4559 14.9996 14.3738C14.9995 14.2917 14.9833 14.2104 14.9518 14.1346C14.9203 14.0588 14.8741 13.99 14.816 13.932L10.983 10.1L10.989 9.67299C11.489 8.96674 11.8152 8.15249 11.9413 7.29642C12.0674 6.44034 11.9897 5.5666 11.7145 4.74621C11.4394 3.92582 10.9745 3.18192 10.3578 2.57498C9.74104 1.96804 8.98979 1.51519 8.16509 1.25322C7.34039 0.991255 6.46551 0.927572 5.61157 1.06735C4.75763 1.20712 3.94871 1.54641 3.25057 2.05764C2.55243 2.56887 1.98476 3.23761 1.59371 4.0095C1.20265 4.7814 0.999236 5.63468 1 6.49999C1 7.95868 1.57946 9.35763 2.61091 10.3891C3.64236 11.4205 5.04131 12 6.5 12C7.689 12 8.788 11.62 9.687 10.978L10.089 10.973V10.973ZM6.5 10.75C4.157 10.75 2.25 8.84299 2.25 6.49999C2.25 4.15699 4.157 2.24999 6.5 2.24999C8.843 2.24999 10.75 4.15699 10.75 6.49999C10.75 8.84299 8.843 10.75 6.5 10.75Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
web/images/toolbarButton-secondaryToolbarToggle.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.53406 13.818L7.99906 8.35203L8.00006 7.64703L2.53606 2.18203C2.41769 2.06965 2.26012 2.00795 2.09692 2.01006C1.93372 2.01218 1.7778 2.07795 1.66239 2.19336C1.54698 2.30877 1.48121 2.46469 1.47909 2.62789C1.47697 2.79109 1.53868 2.94867 1.65106 3.06703L6.58506 7.99803L1.65006 12.932C1.53754 13.0503 1.47565 13.2078 1.47758 13.371C1.47951 13.5342 1.54511 13.6902 1.66039 13.8057C1.77567 13.9213 1.93152 13.9872 2.09472 13.9895C2.25792 13.9918 2.41557 13.9303 2.53406 13.818ZM8.53406 13.818L13.9991 8.35203L14.0001 7.64703L8.53606 2.18203C8.4177 2.06965 8.26012 2.00795 8.09692 2.01006C7.93372 2.01218 7.7778 2.07795 7.66239 2.19336C7.54698 2.30877 7.48121 2.46469 7.47909 2.62789C7.47697 2.79109 7.53868 2.94867 7.65106 3.06703L12.5851 7.99803L7.65006 12.932C7.53754 13.0503 7.47565 13.2078 7.47758 13.371C7.47951 13.5342 7.54511 13.6902 7.66039 13.8057C7.77567 13.9213 7.93152 13.9872 8.09472 13.9895C8.25792 13.9918 8.41557 13.9303 8.53406 13.818Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
web/images/toolbarButton-sidebarToggle.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M16 4V12.25C16 12.7804 15.7893 13.2891 15.4142 13.6642C15.0391 14.0393 14.5304 14.25 14 14.25H2C1.46957 14.25 0.960859 14.0393 0.585786 13.6642C0.210714 13.2891 0 12.7804 0 12.25V4C0 3.46957 0.210714 2.96086 0.585786 2.58579C0.960859 2.21071 1.46957 2 2 2H14C14.5304 2 15.0391 2.21071 15.4142 2.58579C15.7893 2.96086 16 3.46957 16 4ZM1.25 3.85V12.4L1.85 13H6.75V3.25H1.85L1.25 3.85ZM14.15 13H8V3.25H14.15L14.75 3.85V12.4L14.15 13ZM5.35355 10.1464C5.44732 10.2402 5.5 10.3674 5.5 10.5C5.5 10.6326 5.44732 10.7598 5.35355 10.8536C5.25979 10.9473 5.13261 11 5 11H3C2.86739 11 2.74021 10.9473 2.64645 10.8536C2.55268 10.7598 2.5 10.6326 2.5 10.5C2.5 10.3674 2.55268 10.2402 2.64645 10.1464C2.74021 10.0527 2.86739 10 3 10H5C5.13261 10 5.25979 10.0527 5.35355 10.1464ZM5.5 8C5.5 7.86739 5.44732 7.74021 5.35355 7.64645C5.25979 7.55268 5.13261 7.5 5 7.5H3C2.86739 7.5 2.74021 7.55268 2.64645 7.64645C2.55268 7.74021 2.5 7.86739 2.5 8C2.5 8.13261 2.55268 8.25979 2.64645 8.35355C2.74021 8.44732 2.86739 8.5 3 8.5H5C5.13261 8.5 5.25979 8.44732 5.35355 8.35355C5.44732 8.25979 5.5 8.13261 5.5 8ZM5.35355 5.14645C5.44732 5.24021 5.5 5.36739 5.5 5.5C5.5 5.63261 5.44732 5.75979 5.35355 5.85355C5.25979 5.94732 5.13261 6 5 6H3C2.86739 6 2.74021 5.94732 2.64645 5.85355C2.55268 5.75979 2.5 5.63261 2.5 5.5C2.5 5.36739 2.55268 5.24021 2.64645 5.14645C2.74021 5.05268 2.86739 5 3 5H5C5.13261 5 5.25979 5.05268 5.35355 5.14645Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
3
web/images/toolbarButton-viewAttachments.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.5 3.75C3.5 1.67879 5.17879 0 7.25 0C9.32121 0 11 1.67879 11 3.75V10.25C11 11.4922 9.99221 12.5 8.75 12.5C7.50779 12.5 6.5 11.4922 6.5 10.25V3.5H8V10.25C8 10.6638 8.33621 11 8.75 11C9.16379 11 9.5 10.6638 9.5 10.25V3.75C9.5 2.50721 8.49279 1.5 7.25 1.5C6.00721 1.5 5 2.50721 5 3.75V10.75C5 12.8208 6.67921 14.5 8.75 14.5C10.8208 14.5 12.5 12.8208 12.5 10.75V3.5H14V10.75C14 13.6492 11.6492 16 8.75 16C5.85079 16 3.5 13.6492 3.5 10.75V3.75Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 570 B |