diff --git a/l10n/en-US/viewer.properties b/l10n/en-US/viewer.properties index d13a8d83d3224..2b0c1bf82394e 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 7cb0033f28e70..b7d203495bb8c 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -3767,7 +3767,7 @@ class InkAnnotation extends MarkupAnnotation { } const appearanceBuffer = [ - `${thickness} w`, + `${thickness} w 1 J 1 j`, `${getPdfColor(color, /* isFill */ false)}`, ]; const buffer = []; diff --git a/src/display/canvas.js b/src/display/canvas.js index 98a6eec1fa9ec..433ffb7a175e0 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 60162f696c4cf..c1fd5a167d611 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.startsWith("#")) { + 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 31f3b3629a2dd..89ebc64b38655 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; @@ -98,14 +100,22 @@ class AnnotationEditorLayer { * @param {number} mode */ updateMode(mode) { - if (mode === AnnotationEditorType.INK) { - // We want to have the ink editor covering all of the page without having - // to click to create it: it must be here when we start to draw. - this.div.addEventListener("mouseover", this.#boundMouseover); - this.div.removeEventListener("click", this.#boundClick); - } else { - this.div.removeEventListener("mouseover", this.#boundMouseover); + switch (mode) { + case AnnotationEditorType.INK: + // We want to have the ink editor covering all of the page without + // having to click to create it: it must be here when we start to draw. + this.div.addEventListener("mouseover", this.#boundMouseover); + this.div.removeEventListener("click", this.#boundClick); + break; + case AnnotationEditorType.FREETEXT: + this.div.removeEventListener("mouseover", this.#boundMouseover); + this.div.addEventListener("click", this.#boundClick); + break; + default: + this.div.removeEventListener("mouseover", this.#boundMouseover); + this.div.removeEventListener("click", this.#boundClick); } + this.setActiveEditor(null); } @@ -130,13 +140,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); } /** @@ -232,7 +239,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); } } @@ -332,7 +342,7 @@ class AnnotationEditorLayer { editor.remove(); }; - this.addCommands(cmd, undo, true); + this.addCommands({ cmd, undo, mustExec: true }); } /** @@ -347,7 +357,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 b89b61c13d57f..3b33f4cf8d7c6 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -372,6 +372,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 3094c238d1950..25a092ad87716 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -14,12 +14,14 @@ */ import { + AnnotationEditorParamsType, AnnotationEditorType, assert, LINE_FACTOR, } 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. @@ -41,10 +43,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_COLOR, + overwriteIfSameType: true, + keepUndo: true, + }); + } + /** @inheritdoc */ getInitialTranslation() { // The start of the base line is where the user clicked. @@ -116,6 +210,7 @@ class FreeTextEditor extends AnnotationEditor { enableEditMode() { super.enableEditMode(); this.overlayDiv.classList.remove("enabled"); + this.editorDiv.contentEditable = true; this.div.draggable = false; } @@ -123,6 +218,7 @@ class FreeTextEditor extends AnnotationEditor { disableEditMode() { super.disableEditMode(); this.overlayDiv.classList.add("enabled"); + this.editorDiv.contentEditable = false; this.div.draggable = true; } @@ -223,7 +319,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); @@ -248,6 +344,7 @@ class FreeTextEditor extends AnnotationEditor { ); // eslint-disable-next-line no-unsanitized/property this.editorDiv.innerHTML = this.#contentHTML; + this.div.draggable = true; } return this.div; @@ -258,9 +355,12 @@ class FreeTextEditor extends AnnotationEditor { const padding = FreeTextEditor._internalPadding * this.parent.scaleFactor; const rect = this.getRect(padding, padding); + // 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 e85bf68863973..ec4a4c13cb852 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. @@ -43,10 +48,14 @@ class InkEditor extends AnnotationEditor { #realHeight = 0; + 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 = []; @@ -89,6 +98,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) { @@ -186,7 +277,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; } @@ -263,7 +354,7 @@ class InkEditor extends AnnotationEditor { } }; - this.parent.addCommands(cmd, undo, true); + this.parent.addCommands({ cmd, undo, mustExec: true }); } /** @@ -755,9 +846,12 @@ class InkEditor extends AnnotationEditor { const height = this.rotation % 180 === 0 ? rect[3] - rect[1] : rect[2] - rect[0]; + // 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 bd0f90922f3c6..bef2d92b2504d 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 8f24d10001a38..8a55278126b26 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 217dcc229c383..d820538901de5 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 25103b058ca1f..ed275087c4aa3 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 76e24b3c0edd9..34d3cf154ad65 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -4217,8 +4217,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" + @@ -4243,8 +4243,8 @@ describe("annotation", function () { annotationType: AnnotationEditorType.INK, rect: [12, 34, 56, 78], rotation: 0, - thickness: 1, - color: [0, 0, 0], + thickness: 3, + color: [0, 255, 0], paths: [ { bezier: [1, 2, 3, 4, 5, 6, 7, 8], @@ -4264,10 +4264,12 @@ describe("annotation", function () { null ); - expect(operatorList.argsArray.length).toEqual(6); + expect(operatorList.argsArray.length).toEqual(8); expect(operatorList.fnArray).toEqual([ OPS.beginAnnotation, OPS.setLineWidth, + OPS.setLineCap, + OPS.setLineJoin, OPS.setStrokeRGBColor, OPS.constructPath, OPS.stroke, @@ -4275,16 +4277,20 @@ describe("annotation", function () { ]); // Linewidth. - expect(operatorList.argsArray[1]).toEqual([1]); + expect(operatorList.argsArray[1]).toEqual([3]); + // LineCap. + expect(operatorList.argsArray[2]).toEqual([1]); + // LineJoin. + expect(operatorList.argsArray[3]).toEqual([1]); // Color. - expect(operatorList.argsArray[2]).toEqual( - new Uint8ClampedArray([0, 0, 0]) + expect(operatorList.argsArray[4]).toEqual( + new Uint8ClampedArray([0, 255, 0]) ); // Path. - expect(operatorList.argsArray[3][0]).toEqual([OPS.moveTo, OPS.curveTo]); - expect(operatorList.argsArray[3][1]).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); + expect(operatorList.argsArray[5][0]).toEqual([OPS.moveTo, OPS.curveTo]); + expect(operatorList.argsArray[5][1]).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); // Min-max. - expect(operatorList.argsArray[3][2]).toEqual([1, 1, 2, 2]); + expect(operatorList.argsArray[5][2]).toEqual([1, 1, 2, 2]); }); }); diff --git a/web/annotation_editor_params.js b/web/annotation_editor_params.js new file mode 100644 index 0000000000000..bcd6af9a81789 --- /dev/null +++ b/web/annotation_editor_params.js @@ -0,0 +1,84 @@ +/* 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({ + 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 a87f2856df3a6..cb5b4f7d9a64a 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, @@ -568,6 +571,10 @@ const PDFViewerApplication = { } if (annotationEditorEnabled) { + this.annotationEditorParams = new AnnotationEditorParams( + appConfig.annotationEditorParams, + eventBus + ); for (const element of [ document.getElementById("editorModeButtons"), document.getElementById("editorModeSeparator"), @@ -1907,6 +1914,10 @@ const PDFViewerApplication = { "switchannotationeditormode", webViewerSwitchAnnotationEditorMode ); + eventBus._on( + "switchannotationeditorparams", + webViewerSwitchAnnotationEditorParams + ); eventBus._on("print", webViewerPrint); eventBus._on("download", webViewerDownload); eventBus._on("firstpage", webViewerFirstPage); @@ -2491,6 +2502,9 @@ function webViewerPresentationMode() { function webViewerSwitchAnnotationEditorMode(evt) { PDFViewerApplication.pdfViewer.annotationEditorMode = evt.mode; } +function webViewerSwitchAnnotationEditorParams(evt) { + PDFViewerApplication.pdfViewer.annotationEditorParams = evt; +} function webViewerPrint() { PDFViewerApplication.triggerPrinting(); } diff --git a/web/base_viewer.js b/web/base_viewer.js index cacb09dbe6d32..d49db80938bf0 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -720,7 +720,9 @@ class BaseViewer { mode: annotationEditorMode, }); - this.#annotationEditorUIManager = new AnnotationEditorUIManager(); + this.#annotationEditorUIManager = new AnnotationEditorUIManager( + this.eventBus + ); } } @@ -2170,6 +2172,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 b78e9476d9862..d0890a3e22e13 100644 --- a/web/toolbar.js +++ b/web/toolbar.js @@ -104,7 +104,9 @@ class Toolbar { zoomOut: options.zoomOut, editorNoneButton: options.editorNoneButton, editorFreeTextButton: options.editorFreeTextButton, + editorFreeTextParamsToolbar: options.editorFreeTextParamsToolbar, editorInkButton: options.editorInkButton, + editorInkParamsToolbar: options.editorInkParamsToolbar, }; this._wasLocalized = false; @@ -212,20 +214,33 @@ class Toolbar { #bindEditorToolsListener({ editorNoneButton, editorFreeTextButton, + editorFreeTextParamsToolbar, editorInkButton, + editorInkParamsToolbar, }) { const editorModeChanged = (evt, disableButtons = false) => { const editorButtons = [ - [AnnotationEditorType.NONE, editorNoneButton], - [AnnotationEditorType.FREETEXT, editorFreeTextButton], - [AnnotationEditorType.INK, editorInkButton], + { mode: AnnotationEditorType.NONE, button: editorNoneButton }, + { + mode: AnnotationEditorType.FREETEXT, + button: editorFreeTextButton, + toolbar: editorFreeTextParamsToolbar, + }, + { + mode: AnnotationEditorType.INK, + button: editorInkButton, + toolbar: 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); button.disabled = disableButtons; + if (toolbar) { + toolbar.classList.toggle("hidden", !checked); + } } }; this.eventBus._on("annotationeditormodechanged", editorModeChanged); diff --git a/web/viewer.css b/web/viewer.css index f269a3e560f2f..865c25541a45c 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,50 @@ select { background-color: var(--doorhanger-bg-color); } +.editorParamsToolbarContainer { + width: 220px; + margin-bottom: -4px; +} + +.editorParamsToolbarContainer > .editorParamsSetter { + min-height: 26px; + display: flex; + align-items: center; + justify-content: space-between; + padding-inline: 10px; +} + +.editorParamsToolbarContainer .editorParamsLabel { + padding-inline-end: 10px; + flex: none; + color: var(--main-color); +} + +.editorParamsToolbarContainer .editorParamsColor { + width: 32px; + height: 32px; + flex: none; +} + +.editorParamsToolbarContainer .editorParamsSlider { + background-color: transparent; + width: 90px; + flex: 0 1 0; +} + +.editorParamsToolbarContainer .editorParamsSlider::-moz-range-progress { + background-color: black; +} +.editorParamsToolbarContainer .editorParamsSlider::-moz-range-track, +.editorParamsToolbarContainer + .editorParamsSlider::-webkit-slider-runnable-track { + background-color: black; +} +.editorParamsToolbarContainer .editorParamsSlider::-moz-range-thumb, +.editorParamsToolbarContainer .editorParamsSlider::-webkit-slider-thumb { + background-color: white; +} + #secondaryToolbarButtonContainer { max-width: 220px; min-height: 26px; @@ -503,6 +550,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; diff --git a/web/viewer.html b/web/viewer.html index 99f88a59ec3e6..e3fa5fc5c3f0a 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -147,6 +147,32 @@ + + + +