[editor] Add some UI elements in order to set font size & color, and ink thickness & color

This commit is contained in:
Calixte Denizet 2022-06-13 18:23:10 +02:00
parent 4e025e1f08
commit 1a3ef2a0aa
20 changed files with 624 additions and 65 deletions

View File

@ -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

View File

@ -3767,7 +3767,7 @@ class InkAnnotation extends MarkupAnnotation {
}
const appearanceBuffer = [
`${thickness} w`,
`${thickness} w 1 J 1 j`,
`${getPdfColor(color, /* isFill */ false)}`,
];
const buffer = [];

View File

@ -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";
// <canvas> 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;

View File

@ -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,

View File

@ -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 });
}
/**

View File

@ -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 };

View File

@ -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,

View File

@ -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,

View File

@ -64,9 +64,36 @@ class CommandManager {
* @param {function} cmd
* @param {function} undo
* @param {boolean} mustExec
* @param {number} type
* @param {boolean} overwriteIfSameType
* @param {boolean} keepUndo
*/
add(cmd, undo, mustExec) {
const save = [cmd, undo];
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 });
}
/**

View File

@ -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,

View File

@ -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,

View File

@ -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()
);

View File

@ -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]);
});
});

View File

@ -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 };

View File

@ -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();
}

View File

@ -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 };

View File

@ -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);

View File

@ -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;

View File

@ -147,6 +147,32 @@ See https://github.com/adobe-type-tools/cmap-resources
</div>
</div> <!-- findbar -->
<div class="editorParamsToolbar hidden doorHangerRight" id="editorFreeTextParamsToolbar">
<div class="editorParamsToolbarContainer">
<div class="editorParamsSetter">
<label for="editorFreeTextColor" class="editorParamsLabel" data-l10n-id="editor_free_text_font_color">Font Color</label>
<input type="color" id="editorFreeTextColor" name="fontColor" class="editorParamsColor" tabindex="100">
</div>
<div class="editorParamsSetter">
<label for="editorFreeTextFontSize" class="editorParamsLabel" data-l10n-id="editor_free_text_font_size">Font Size</label>
<input type="range" id="editorFreeTextFontSize" class="editorParamsSlider" name="fontSize" value="10" min="5" max="100" step="1" tabindex="101">
</div>
</div>
</div>
<div class="editorParamsToolbar hidden doorHangerRight" id="editorInkParamsToolbar">
<div class="editorParamsToolbarContainer">
<div class="editorParamsSetter">
<label for="editorInkColor" class="editorParamsLabel" data-l10n-id="editor_ink_line_color">Line Color</label>
<input type="color" id="editorInkColor" name="inkColor" class="editorParamsColor" tabindex="102">
</div>
<div class="editorParamsSetter">
<label for="editorInkThickness" class="editorParamsLabel" data-l10n-id="editor_ink_line_thickness">Line Thickness</label>
<input type="range" id="editorInkThickness" class="editorParamsSlider" name="lineThickness" value="1" min="1" max="20" step="1" tabindex="103">
</div>
</div>
</div>
<div id="secondaryToolbar" class="secondaryToolbar hidden doorHangerRight">
<div id="secondaryToolbarButtonContainer">
<button id="secondaryPresentationMode" class="secondaryToolbarButton visibleLargeView" title="Switch to Presentation Mode" tabindex="51" data-l10n-id="presentation_mode">

View File

@ -95,7 +95,11 @@ function getViewerConfiguration() {
print: document.getElementById("print"),
editorNoneButton: document.getElementById("editorNone"),
editorFreeTextButton: document.getElementById("editorFreeText"),
editorFreeTextParamsToolbar: document.getElementById(
"editorFreeTextParamsToolbar"
),
editorInkButton: document.getElementById("editorInk"),
editorInkParamsToolbar: document.getElementById("editorInkParamsToolbar"),
presentationModeButton: document.getElementById("presentationMode"),
download: document.getElementById("download"),
viewBookmark: document.getElementById("viewBookmark"),
@ -193,6 +197,12 @@ function getViewerConfiguration() {
linearized: document.getElementById("linearizedField"),
},
},
annotationEditorParams: {
editorFreeTextFontSize: document.getElementById("editorFreeTextFontSize"),
editorFreeTextColor: document.getElementById("editorFreeTextColor"),
editorInkColor: document.getElementById("editorInkColor"),
editorInkThickness: document.getElementById("editorInkThickness"),
},
errorWrapper,
printContainer: document.getElementById("printContainer"),
openFileInput: