diff --git a/l10n/en-US/viewer.properties b/l10n/en-US/viewer.properties index d13a8d83d32240..2b0c1bf82394e1 100644 --- a/l10n/en-US/viewer.properties +++ b/l10n/en-US/viewer.properties @@ -259,3 +259,9 @@ editor_ink.title=Add Ink Annotation editor_ink_label=Ink Annotation freetext_default_content=Enter some text… + +# Editor Parameters +editor_free_text_font_color=Font Color +editor_free_text_font_size=Font Size +editor_ink_line_color=Line Color +editor_ink_line_thickness=Line Thickness diff --git a/src/core/annotation.js b/src/core/annotation.js index bd9dc692abfafa..60df46df44b245 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -3458,7 +3458,7 @@ class InkAnnotation extends MarkupAnnotation { const h = y2 - y1; const appearanceBuffer = [ - `${annotation.thickness} w`, + `${annotation.thickness} w 1 J 1 j`, `${getPdfColor(annotation.color, /* isFill */ false)}`, ]; const buffer = []; diff --git a/src/display/canvas.js b/src/display/canvas.js index 98a6eec1fa9ecd..433ffb7a175e0e 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -26,6 +26,7 @@ import { Util, warn, } from "../shared/util.js"; +import { getRGB, PixelsPerInch } from "./display_utils.js"; import { getShadingPattern, PathType, @@ -33,7 +34,6 @@ import { } from "./pattern_helper.js"; import { applyMaskImageData } from "../shared/image_utils.js"; import { isNodeJS } from "../shared/is_node.js"; -import { PixelsPerInch } from "./display_utils.js"; // contexts store most of the state we need natively. // However, PDF needs a bit more state, which we store here. @@ -1326,10 +1326,7 @@ class CanvasGraphics { // Then for every color in the pdf, if its rounded luminance is the // same as the background one then it's replaced by the new // background color else by the foreground one. - const cB = parseInt(defaultBg.slice(1), 16); - const rB = (cB && 0xff0000) >> 16; - const gB = (cB && 0x00ff00) >> 8; - const bB = cB && 0x0000ff; + const [rB, gB, bB] = getRGB(defaultBg); const newComp = x => { x /= 255; return x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4; diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 60162f696c4cf2..934adefe3bc21d 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -567,6 +567,28 @@ function getXfaPageViewport(xfaPage, { scale = 1, rotation = 0 }) { }); } +function getRGB(color) { + if (color.charAt(0) === "#") { + const colorRGB = parseInt(color.slice(1), 16); + return [ + (colorRGB & 0xff0000) >> 16, + (colorRGB & 0x00ff00) >> 8, + colorRGB & 0x0000ff, + ]; + } + + if (color.startsWith("rgb(")) { + // getComputedStyle(...).color returns a `rgb(R, G, B)` color. + return color + .slice(/* "rgb(".length */ 4, -1) // Strip out "rgb(" and ")". + .split(",") + .map(x => parseInt(x)); + } + + warn(`Not a valid color format: "${color}"`); + return [0, 0, 0]; +} + export { deprecated, DOMCanvasFactory, @@ -575,6 +597,7 @@ export { DOMSVGFactory, getFilenameFromUrl, getPdfFilenameFromUrl, + getRGB, getXfaPageViewport, isDataScheme, isPdfFile, diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index 98e0038e25d5a7..6ef0691fa81f03 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -78,6 +78,8 @@ class AnnotationEditorLayer { if (!AnnotationEditorLayer._initialized) { AnnotationEditorLayer._initialized = true; FreeTextEditor.initialize(options.l10n); + + options.uiManager.registerEditorTypes([FreeTextEditor, InkEditor]); } this.#uiManager = options.uiManager; this.annotationStorage = options.annotationStorage; @@ -129,13 +131,10 @@ class AnnotationEditorLayer { /** * Add some commands into the CommandManager (undo/redo stuff). - * @param {function} cmd - * @param {function} undo - * @param {boolean} mustExec - If true the command is executed after having - * been added. + * @param {Object} params */ - addCommands(cmd, undo, mustExec) { - this.#uiManager.addCommands(cmd, undo, mustExec); + addCommands(params) { + this.#uiManager.addCommands(params); } /** @@ -231,7 +230,10 @@ class AnnotationEditorLayer { this.unselectAll(); this.div.removeEventListener("click", this.#boundClick); } else { - this.#uiManager.allowClick = false; + // When in Ink mode, setting the editor to null allows the + // user to have to make one click in order to start drawing. + this.#uiManager.allowClick = + this.#uiManager.getMode() === AnnotationEditorType.INK; this.div.addEventListener("click", this.#boundClick); } } @@ -326,7 +328,7 @@ class AnnotationEditorLayer { editor.remove(); }; - this.addCommands(cmd, undo, true); + this.addCommands({ cmd, undo, mustExec: true }); } /** @@ -341,7 +343,7 @@ class AnnotationEditorLayer { editor.remove(); }; - this.addCommands(cmd, undo, false); + this.addCommands({ cmd, undo, mustExec: false }); } /** diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 6de6c7cedc999e..658d3500b92588 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -330,6 +330,21 @@ class AnnotationEditor { this.div.classList.remove("selectedEditor"); } } + + /** + * Update some parameters which have been changed through the UI. + * @param {number} type + * @param {*} value + */ + updateParams(type, value) {} + + /** + * Get some properties to update in the UI. + * @returns {Object} + */ + get propertiesToUpdate() { + return {}; + } } export { AnnotationEditor }; diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js index d657f2e3dd9174..3abbc88738c818 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -14,6 +14,7 @@ */ import { + AnnotationEditorParamsType, AnnotationEditorType, assert, LINE_FACTOR, @@ -21,6 +22,7 @@ import { } from "../../shared/util.js"; import { AnnotationEditor } from "./editor.js"; import { bindEvents } from "./tools.js"; +import { getRGB } from "../display_utils.js"; /** * Basic text editor in order to create a FreeTex annotation. @@ -42,10 +44,14 @@ class FreeTextEditor extends AnnotationEditor { static _internalPadding = 0; + static _defaultFontSize = 10; + + static _defaultColor = "CanvasText"; + constructor(params) { super({ ...params, name: "freeTextEditor" }); - this.#color = params.color || "CanvasText"; - this.#fontSize = params.fontSize || 10; + this.#color = params.color || FreeTextEditor._defaultColor; + this.#fontSize = params.fontSize || FreeTextEditor._defaultFontSize; } static initialize(l10n) { @@ -89,6 +95,94 @@ class FreeTextEditor extends AnnotationEditor { return editor; } + static updateDefaultParams(type, value) { + switch (type) { + case AnnotationEditorParamsType.FREETEXT_SIZE: + FreeTextEditor._defaultFontSize = value; + break; + case AnnotationEditorParamsType.FREETEXT_COLOR: + FreeTextEditor._defaultColor = value; + break; + } + } + + /** @inheritdoc */ + updateParams(type, value) { + switch (type) { + case AnnotationEditorParamsType.FREETEXT_SIZE: + this.#updateFontSize(value); + break; + case AnnotationEditorParamsType.FREETEXT_COLOR: + this.#updateColor(value); + break; + } + } + + static get defaultPropertiesToUpdate() { + return [ + [ + AnnotationEditorParamsType.FREETEXT_SIZE, + FreeTextEditor._defaultFontSize, + ], + [AnnotationEditorParamsType.FREETEXT_COLOR, FreeTextEditor._defaultColor], + ]; + } + + /** @inheritdoc */ + get propertiesToUpdate() { + return [ + [AnnotationEditorParamsType.FREETEXT_SIZE, this.#fontSize], + [AnnotationEditorParamsType.FREETEXT_COLOR, this.#color], + ]; + } + + /** + * Update the font size and make this action as undoable. + * @param {number} fontSize + */ + #updateFontSize(fontSize) { + const setFontsize = size => { + this.editorDiv.style.fontSize = `calc(${size}px * var(--scale-factor))`; + this.translate(0, -(size - this.#fontSize) * this.parent.scaleFactor); + this.#fontSize = size; + }; + const savedFontsize = this.#fontSize; + this.parent.addCommands({ + cmd: () => { + setFontsize(fontSize); + }, + undo: () => { + setFontsize(savedFontsize); + }, + mustExec: true, + type: AnnotationEditorParamsType.FREETEXT_SIZE, + overwriteIfSameType: true, + keepUndo: true, + }); + } + + /** + * Update the color and make this action undoable. + * @param {string} color + */ + #updateColor(color) { + const savedColor = this.#color; + this.parent.addCommands({ + cmd: () => { + this.#color = color; + this.editorDiv.style.color = color; + }, + undo: () => { + this.#color = savedColor; + this.editorDiv.style.color = savedColor; + }, + mustExec: true, + type: AnnotationEditorParamsType.FREETEXT_SIZE, + overwriteIfSameType: true, + keepUndo: true, + }); + } + /** @inheritdoc */ getInitialTranslation() { // The start of the base line is where the user clicked. @@ -216,7 +310,7 @@ class FreeTextEditor extends AnnotationEditor { this.editorDiv.contentEditable = true; const { style } = this.editorDiv; - style.fontSize = `${this.#fontSize}%`; + style.fontSize = `calc(${this.#fontSize}px * var(--scale-factor))`; style.color = this.#color; this.div.append(this.editorDiv); @@ -232,9 +326,11 @@ class FreeTextEditor extends AnnotationEditor { if (this.width) { // This editor was created in using copy (ctrl+c). - this.setAt(this.x + this.width, this.y + this.height); + const [tx, ty] = this.getInitialTranslation(); + this.setAt(this.x + this.width - tx, this.y + this.height - ty); // eslint-disable-next-line no-unsanitized/property this.editorDiv.innerHTML = this.#contentHTML; + this.div.draggable = true; } return this.div; @@ -253,9 +349,14 @@ class FreeTextEditor extends AnnotationEditor { [this.x + padding + rect.width, this.y + padding], this.parent.inverseViewportTransform ); + + // We don't use this.#color directly because it can + // be CanvasText. + const color = getRGB(getComputedStyle(this.editorDiv).color); + return { annotationType: AnnotationEditorType.FREETEXT, - color: [0, 0, 0], + color, fontSize: this.#fontSize, value: this.#content, pageIndex: this.parent.pageIndex, diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index e9bc81232e6a3c..8f5aefc2f3e35f 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -13,9 +13,14 @@ * limitations under the License. */ -import { AnnotationEditorType, Util } from "../../shared/util.js"; +import { + AnnotationEditorParamsType, + AnnotationEditorType, + Util, +} from "../../shared/util.js"; import { AnnotationEditor } from "./editor.js"; import { fitCurve } from "./fit_curve/fit_curve.js"; +import { getRGB } from "../display_utils.js"; /** * Basic draw editor in order to generate an Ink annotation. @@ -39,10 +44,14 @@ class InkEditor extends AnnotationEditor { #observer = null; + static _defaultThickness = 1; + + static _defaultColor = "CanvasText"; + constructor(params) { super({ ...params, name: "inkEditor" }); - this.color = params.color || "CanvasText"; - this.thickness = params.thickness || 1; + this.color = params.color || InkEditor._defaultColor; + this.thickness = params.thickness || InkEditor._defaultThickness; this.paths = []; this.bezierPath2D = []; this.currentPath = []; @@ -83,6 +92,88 @@ class InkEditor extends AnnotationEditor { return editor; } + static updateDefaultParams(type, value) { + switch (type) { + case AnnotationEditorParamsType.INK_THICKNESS: + InkEditor._defaultThickness = value; + break; + case AnnotationEditorParamsType.INK_COLOR: + InkEditor._defaultColor = value; + break; + } + } + + /** @inheritdoc */ + updateParams(type, value) { + switch (type) { + case AnnotationEditorParamsType.INK_THICKNESS: + this.#updateThickness(value); + break; + case AnnotationEditorParamsType.INK_COLOR: + this.#updateColor(value); + break; + } + } + + static get defaultPropertiesToUpdate() { + return [ + [AnnotationEditorParamsType.INK_THICKNESS, InkEditor._defaultThickness], + [AnnotationEditorParamsType.INK_COLOR, InkEditor._defaultColor], + ]; + } + + /** @inheritdoc */ + get propertiesToUpdate() { + return [ + [AnnotationEditorParamsType.INK_THICKNESS, this.thickness], + [AnnotationEditorParamsType.INK_COLOR, this.color], + ]; + } + + /** + * Update the thickness and make this action undoable. + * @param {number} thickness + */ + #updateThickness(thickness) { + const savedThickness = this.thickness; + this.parent.addCommands({ + cmd: () => { + this.thickness = thickness; + this.#fitToContent(); + }, + undo: () => { + this.thickness = savedThickness; + this.#fitToContent(); + }, + mustExec: true, + type: AnnotationEditorParamsType.INK_THICKNESS, + overwriteIfSameType: true, + keepUndo: true, + }); + } + + /** + * Update the color and make this action undoable. + * @param {string} color + */ + #updateColor(color) { + const savedColor = this.color; + this.parent.addCommands({ + cmd: () => { + this.color = color; + this.#redraw(); + }, + undo: () => { + this.color = savedColor; + this.#redraw(); + }, + mustExec: true, + type: AnnotationEditorParamsType.INK_COLOR, + overwriteIfSameType: true, + keepUndo: true, + }); + } + /** @inheritdoc */ rebuild() { if (this.div === null) { @@ -166,7 +257,7 @@ class InkEditor extends AnnotationEditor { this.ctx.lineWidth = (this.thickness * this.parent.scaleFactor) / this.scaleFactor; this.ctx.lineCap = "round"; - this.ctx.lineJoin = "miter"; + this.ctx.lineJoin = "round"; this.ctx.miterLimit = 10; this.ctx.strokeStyle = this.color; } @@ -243,7 +334,7 @@ class InkEditor extends AnnotationEditor { } }; - this.parent.addCommands(cmd, undo, true); + this.parent.addCommands({ cmd, undo, mustExec: true }); } /** @@ -715,9 +806,13 @@ class InkEditor extends AnnotationEditor { this.parent.inverseViewportTransform ); + // We don't use this.color directly because it can + // be CanvasText. + const color = getRGB(this.ctx.strokeStyle); + return { annotationType: AnnotationEditorType.INK, - color: [0, 0, 0], + color, thickness: this.thickness, paths: this.#serializePaths( this.scaleFactor / this.parent.scaleFactor, diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index bd0f90922f3c6f..bef2d92b2504df 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -64,9 +64,36 @@ class CommandManager { * @param {function} cmd * @param {function} undo * @param {boolean} mustExec - */ - add(cmd, undo, mustExec) { - const save = [cmd, undo]; + * @param {number} type + * @param {boolean} overwriteIfSameType + * @param {boolean} keepUndo + */ + add({ + cmd, + undo, + mustExec, + type = NaN, + overwriteIfSameType = false, + keepUndo = false, + }) { + const save = { cmd, undo, type }; + if ( + overwriteIfSameType && + !isNaN(this.#position) && + this.#commands[this.#position].type === type + ) { + // For example when we change a color we don't want to + // be able to undo all the steps, hence we only want to + // keep the last undoable action in this sequence of actions. + if (keepUndo) { + save.undo = this.#commands[this.#position].undo; + } + this.#commands[this.#position] = save; + if (mustExec) { + cmd(); + } + return; + } const next = (this.#position + 1) % this.#maxSize; if (next !== this.#start) { if (this.#start < next) { @@ -94,7 +121,7 @@ class CommandManager { // Nothing to undo. return; } - this.#commands[this.#position][1](); + this.#commands[this.#position].undo(); if (this.#position === this.#start) { this.#position = NaN; } else { @@ -108,7 +135,7 @@ class CommandManager { redo() { if (isNaN(this.#position)) { if (this.#start < this.#commands.length) { - this.#commands[this.#start][0](); + this.#commands[this.#start].cmd(); this.#position = this.#start; } return; @@ -116,7 +143,7 @@ class CommandManager { const next = (this.#position + 1) % this.#maxSize; if (next !== this.#start && next < this.#commands.length) { - this.#commands[next][0](); + this.#commands[next].cmd(); this.#position = next; } } @@ -273,6 +300,10 @@ class AnnotationEditorUIManager { #commandManager = new CommandManager(); + #editorTypes = null; + + #eventBus = null; + #idManager = new IdManager(); #isAllSelected = false; @@ -281,6 +312,26 @@ class AnnotationEditorUIManager { #mode = AnnotationEditorType.NONE; + #previousActiveEditor = null; + + constructor(eventBus) { + this.#eventBus = eventBus; + } + + #dispatchUpdateUI(details) { + this.#eventBus?.dispatch("annotationeditorparamschanged", { + source: this, + details, + }); + } + + registerEditorTypes(types) { + this.#editorTypes = types; + for (const editorType of this.#editorTypes) { + this.#dispatchUpdateUI(editorType.defaultPropertiesToUpdate); + } + } + /** * Get an id. * @returns {string} @@ -326,6 +377,21 @@ class AnnotationEditorUIManager { } } + /** + * Update a parameter in the current editor or globally. + * @param {number} type + * @param {*} value + */ + updateParams(type, value) { + (this.#activeEditor || this.#previousActiveEditor)?.updateParams( + type, + value + ); + for (const editorType of this.#editorTypes) { + editorType.updateDefaultParams(type, value); + } + } + /** * Enable all the layers. */ @@ -395,7 +461,24 @@ class AnnotationEditorUIManager { * @param {AnnotationEditor} editor */ setActiveEditor(editor) { + if (this.#activeEditor === editor) { + return; + } + + this.#previousActiveEditor = this.#activeEditor; + this.#activeEditor = editor; + if (editor) { + this.#dispatchUpdateUI(editor.propertiesToUpdate); + } else { + if (this.#previousActiveEditor) { + this.#dispatchUpdateUI(this.#previousActiveEditor.propertiesToUpdate); + } else { + for (const editorType of this.#editorTypes) { + this.#dispatchUpdateUI(editorType.defaultPropertiesToUpdate); + } + } + } } /** @@ -414,12 +497,10 @@ class AnnotationEditorUIManager { /** * Add a command to execute (cmd) and another one to undo it. - * @param {function} cmd - * @param {function} undo - * @param {boolean} mustExec + * @param {Object} params */ - addCommands(cmd, undo, mustExec) { - this.#commandManager.add(cmd, undo, mustExec); + addCommands(params) { + this.#commandManager.add(params); } /** @@ -468,7 +549,7 @@ class AnnotationEditorUIManager { } }; - this.addCommands(cmd, undo, true); + this.addCommands({ cmd, undo, mustExec: true }); } else { if (!this.#activeEditor) { return; @@ -482,7 +563,7 @@ class AnnotationEditorUIManager { }; } - this.addCommands(cmd, undo, true); + this.addCommands({ cmd, undo, mustExec: true }); } /** @@ -509,7 +590,7 @@ class AnnotationEditorUIManager { layer.addOrRebuild(editor); }; - this.addCommands(cmd, undo, true); + this.addCommands({ cmd, undo, mustExec: true }); } } @@ -530,7 +611,7 @@ class AnnotationEditorUIManager { editor.remove(); }; - this.addCommands(cmd, undo, true); + this.addCommands({ cmd, undo, mustExec: true }); } /** diff --git a/src/pdf.js b/src/pdf.js index 8f24d10001a38f..8a55278126b263 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -23,6 +23,7 @@ /** @typedef {import("./display/text_layer").TextLayerRenderTask} TextLayerRenderTask */ import { + AnnotationEditorParamsType, AnnotationEditorType, AnnotationMode, CMapCompressionType, @@ -110,6 +111,7 @@ if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION")) { export { AnnotationEditorLayer, + AnnotationEditorParamsType, AnnotationEditorType, AnnotationEditorUIManager, AnnotationLayer, diff --git a/src/shared/util.js b/src/shared/util.js index 217dcc229c3834..d820538901de52 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -60,6 +60,13 @@ const AnnotationEditorType = { INK: 15, }; +const AnnotationEditorParamsType = { + FREETEXT_SIZE: 0, + FREETEXT_COLOR: 1, + INK_COLOR: 2, + INK_THICKNESS: 3, +}; + // Permission flags from Table 22, Section 7.6.3.2 of the PDF specification. const PermissionFlag = { PRINT: 0x04, @@ -1146,6 +1153,7 @@ export { AbortException, AnnotationActionEventType, AnnotationBorderStyleType, + AnnotationEditorParamsType, AnnotationEditorPrefix, AnnotationEditorType, AnnotationFieldFlag, diff --git a/test/integration/freetext_editor_spec.js b/test/integration/freetext_editor_spec.js index 25103b058ca1f1..ed275087c4aa3a 100644 --- a/test/integration/freetext_editor_spec.js +++ b/test/integration/freetext_editor_spec.js @@ -94,6 +94,7 @@ describe("Editor", () => { const content = await page.$eval(`${editorPrefix}0`, el => el.innerText.trimEnd() ); + let pastedContent = await page.$eval(`${editorPrefix}2`, el => el.innerText.trimEnd() ); diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index e4d96e15f9923b..6b5f7053993348 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -4012,8 +4012,8 @@ describe("annotation", function () { const appearance = data.dependencies[0].data; expect(appearance).toEqual( "2 0 obj\n" + - "<< /FormType 1 /Subtype /Form /Type /XObject /BBox [0 0 44 44] /Length 121>> stream\n" + - "1 w\n" + + "<< /FormType 1 /Subtype /Form /Type /XObject /BBox [0 0 44 44] /Length 129>> stream\n" + + "1 w 1 J 1 j\n" + "0 G\n" + "10 11 m\n" + "12 13 14 15 16 17 c\n" + diff --git a/web/annotation_editor_params.js b/web/annotation_editor_params.js new file mode 100644 index 00000000000000..e6dde312434e10 --- /dev/null +++ b/web/annotation_editor_params.js @@ -0,0 +1,85 @@ +/* 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 { AnnotationEditorParamsType } from "pdfjs-lib"; + +class AnnotationEditorParams { + /** + * @param {AnnotationEditorParamsOptions} options + * @param {EventBus} eventBus + */ + constructor(options, eventBus) { + this.eventBus = eventBus; + this.#bindListeners(options); + } + + #bindListeners({ + editorParamsButton, + editorFreeTextFontSize, + editorFreeTextColor, + editorInkColor, + editorInkThickness, + }) { + editorFreeTextFontSize.addEventListener("input", evt => { + this.eventBus.dispatch("switchannotationeditorparams", { + source: this, + type: AnnotationEditorParamsType.FREETEXT_SIZE, + value: editorFreeTextFontSize.valueAsNumber, + }); + }); + editorFreeTextColor.addEventListener("input", evt => { + this.eventBus.dispatch("switchannotationeditorparams", { + source: this, + type: AnnotationEditorParamsType.FREETEXT_COLOR, + value: editorFreeTextColor.value, + }); + }); + editorInkColor.addEventListener("input", evt => { + this.eventBus.dispatch("switchannotationeditorparams", { + source: this, + type: AnnotationEditorParamsType.INK_COLOR, + value: editorInkColor.value, + }); + }); + editorInkThickness.addEventListener("input", evt => { + this.eventBus.dispatch("switchannotationeditorparams", { + source: this, + type: AnnotationEditorParamsType.INK_THICKNESS, + value: editorInkThickness.valueAsNumber, + }); + }); + + this.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; + } + } + }); + } +} + +export { AnnotationEditorParams }; diff --git a/web/app.js b/web/app.js index 390ebd426776db..78e72bd6193b08 100644 --- a/web/app.js +++ b/web/app.js @@ -56,6 +56,7 @@ import { } from "pdfjs-lib"; import { CursorTool, PDFCursorTools } from "./pdf_cursor_tools.js"; import { LinkTarget, PDFLinkService } from "./pdf_link_service.js"; +import { AnnotationEditorParams } from "./annotation_editor_params.js"; import { OverlayManager } from "./overlay_manager.js"; import { PasswordPrompt } from "./password_prompt.js"; import { PDFAttachmentViewer } from "./pdf_attachment_viewer.js"; @@ -237,6 +238,8 @@ const PDFViewerApplication = { eventBus: null, /** @type {IL10n} */ l10n: null, + /** @type {AnnotationEditorParams} */ + annotationEditorParams: null, isInitialViewSet: false, downloadComplete: false, isViewerEmbedded: window.parent !== window, @@ -588,6 +591,11 @@ const PDFViewerApplication = { eventBus ); + this.annotationEditorParams = new AnnotationEditorParams( + appConfig.annotationEditorParams, + eventBus + ); + if (this.supportsFullscreen) { this.pdfPresentationMode = new PDFPresentationMode({ container, @@ -1892,6 +1900,10 @@ const PDFViewerApplication = { "switchannotationeditormode", webViewerSwitchAnnotationEditorMode ); + eventBus._on( + "switchannotationeditorparams", + webViewerSwitchAnnotationEditorParams + ); eventBus._on("print", webViewerPrint); eventBus._on("download", webViewerDownload); eventBus._on("firstpage", webViewerFirstPage); @@ -2480,6 +2492,12 @@ function webViewerPresentationMode() { function webViewerSwitchAnnotationEditorMode(evt) { PDFViewerApplication.pdfViewer.annotationEditorMode = evt.mode; } +function webViewerSwitchAnnotationEditorParams(evt) { + PDFViewerApplication.pdfViewer.annotationEditorParams = { + type: evt.type, + value: evt.value, + }; +} function webViewerPrint() { PDFViewerApplication.triggerPrinting(); } diff --git a/web/base_viewer.js b/web/base_viewer.js index 12837f1d210e08..a8c8adf068c261 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -285,7 +285,9 @@ class BaseViewer { this.pageColors = options.pageColors || null; if (options.annotationEditorEnabled === true) { - this.#annotationEditorUIManager = new AnnotationEditorUIManager(); + this.#annotationEditorUIManager = new AnnotationEditorUIManager( + this.eventBus + ); } if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { @@ -2150,6 +2152,14 @@ class BaseViewer { this.#annotationEditorUIManager.updateMode(mode); } + + // eslint-disable-next-line accessor-pairs + set annotationEditorParams({ type, value }) { + if (!this.#annotationEditorUIManager) { + throw new Error(`The AnnotationEditor is not enabled.`); + } + this.#annotationEditorUIManager.updateParams(type, value); + } } export { BaseViewer, PagesCountLimit, PDFPageViewBuffer }; diff --git a/web/toolbar.js b/web/toolbar.js index 5da90c8f8d97e5..36c77e02ffad41 100644 --- a/web/toolbar.js +++ b/web/toolbar.js @@ -104,14 +104,16 @@ class Toolbar { zoomOut: options.zoomOut, editorNoneButton: options.editorNoneButton, editorFreeTextButton: options.editorFreeTextButton, + editorFreeTextParamsToolbar: options.editorFreeTextParamsToolbar, editorInkButton: options.editorInkButton, + editorInkParamsToolbar: options.editorInkParamsToolbar, }; this._wasLocalized = false; this.reset(); // Bind the event listeners for click and various other actions. - this._bindListeners(options); + this._bindListeners(); } setPageNumber(pageNumber, pageLabel) { @@ -207,22 +209,31 @@ class Toolbar { this.#bindEditorToolsListener(options); } - #bindEditorToolsListener({ - editorNoneButton, - editorFreeTextButton, - editorInkButton, - }) { + #bindEditorToolsListener() { + const { items } = this; + this.eventBus._on("annotationeditormodechanged", evt => { const editorButtons = [ - [AnnotationEditorType.NONE, editorNoneButton], - [AnnotationEditorType.FREETEXT, editorFreeTextButton], - [AnnotationEditorType.INK, editorInkButton], + { mode: AnnotationEditorType.NONE, button: items.editorNoneButton }, + { + mode: AnnotationEditorType.FREETEXT, + button: items.editorFreeTextButton, + toolbar: items.editorFreeTextParamsToolbar, + }, + { + mode: AnnotationEditorType.INK, + button: items.editorInkButton, + toolbar: items.editorInkParamsToolbar, + }, ]; - for (const [mode, button] of editorButtons) { + for (const { mode, button, toolbar } of editorButtons) { const checked = mode === evt.mode; button.classList.toggle("toggled", checked); button.setAttribute("aria-checked", checked); + if (toolbar) { + toolbar.classList.toggle("hidden", !checked); + } } }); } diff --git a/web/viewer.css b/web/viewer.css index f5bc4176f7f088..2f89bd7114f6ea 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -335,7 +335,8 @@ select { #toolbarContainer, .findbar, -.secondaryToolbar { +.secondaryToolbar, +.editorParamsToolbar { position: relative; height: 32px; background-color: var(--toolbar-bg-color); @@ -415,7 +416,8 @@ select { } .findbar, -.secondaryToolbar { +.secondaryToolbar, +.editorParamsToolbar { top: 32px; position: absolute; z-index: 10000; @@ -487,7 +489,8 @@ select { background-color: rgba(255, 102, 102, 1); } -.secondaryToolbar { +.secondaryToolbar, +.editorParamsToolbar { padding: 6px 0 10px; inset-inline-end: 4px; height: auto; @@ -495,6 +498,45 @@ select { background-color: var(--doorhanger-bg-color); } +#editorParamsToolbarContainer > .editorParamsSetter { + max-width: 220px; + min-height: 26px; + display: flex; + align-items: center; + justify-content: space-between; + padding-inline-start: 12px; + padding-inline-end: 12px; +} + +#editorParamsToolbarContainer .editorParamsLabel { + padding-inline-start: 10px; + flex: none; +} + +#editorParamsToolbarContainer .editorParamsColor { + width: 32px; + height: 32px; + flex: none; +} + +#editorParamsToolbarContainer .editorParamsSlider { + background-color: transparent; + width: 90px; + flex: 0 1 0; +} + +/*#if MOZCENTRAL*/ +#editorParamsToolbarContainer .editorParamsSlider::-moz-range-progress { + background-color: black; +} +#editorParamsToolbarContainer .editorParamsSlider::-moz-range-track { + background-color: black; +} +#editorParamsToolbarContainer .editorParamsSlider::-moz-range-thumb { + background-color: white; +} +/*#endif*/ + #secondaryToolbarButtonContainer { max-width: 220px; min-height: 26px; @@ -503,6 +545,16 @@ select { margin-bottom: -4px; } +#editorInkParamsToolbar { + inset-inline-end: 40px; + background-color: var(--toolbar-bg-color); +} + +#editorFreeTextParamsToolbar { + inset-inline-end: 68px; + background-color: var(--toolbar-bg-color); +} + .doorHanger, .doorHangerRight { border-radius: 2px; @@ -999,6 +1051,10 @@ a.secondaryToolbarButton[href="#"] { background-color: var(--doorhanger-separator-color); } +#editorModeButtons .verticalToolbarSeparator { + display: inline-block; +} + .toolbarField { padding: 4px 7px; margin: 3px 0; diff --git a/web/viewer.html b/web/viewer.html index 86e979455f3406..22be986594157d 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -147,6 +147,32 @@ + + + +