1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-22 16:18:08 +02:00

[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

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