[Editor] Add the possibility to change line opacity in Ink editor

This commit is contained in:
Calixte Denizet 2022-07-24 22:19:09 +02:00
parent 45b9e8417d
commit 7831a100b3
10 changed files with 215 additions and 19 deletions

View File

@ -265,6 +265,7 @@ 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
editor_ink_line_opacity=Line Opacity
# Editor aria
editor_free_text_aria_label=FreeText Editor

View File

@ -3757,7 +3757,7 @@ class InkAnnotation extends MarkupAnnotation {
}
static async createNewAppearanceStream(annotation, xref, params) {
const { color, rect, rotation, paths, thickness } = annotation;
const { color, rect, rotation, paths, thickness, opacity } = annotation;
const [x1, y1, x2, y2] = rect;
let w = x2 - x1;
let h = y2 - y1;
@ -3770,6 +3770,11 @@ class InkAnnotation extends MarkupAnnotation {
`${thickness} w 1 J 1 j`,
`${getPdfColor(color, /* isFill */ false)}`,
];
if (opacity !== 1) {
appearanceBuffer.push("/R0 gs");
}
const buffer = [];
for (const { bezier } of paths) {
buffer.length = 0;
@ -3800,6 +3805,17 @@ class InkAnnotation extends MarkupAnnotation {
appearanceStreamDict.set("Matrix", matrix);
}
if (opacity !== 1) {
const resources = new Dict(xref);
const extGState = new Dict(xref);
const r0 = new Dict(xref);
r0.set("CA", opacity);
r0.set("Type", Name.get("ExtGState"));
extGState.set("R0", r0);
resources.set("ExtGState", extGState);
appearanceStreamDict.set("Resources", resources);
}
const ap = new StringStream(appearance);
ap.dict = appearanceStreamDict;

View File

@ -585,6 +585,14 @@ function getRGB(color) {
.map(x => parseInt(x));
}
if (color.startsWith("rgba(")) {
return color
.slice(/* "rgba(".length */ 5, -1) // Strip out "rgba(" and ")".
.split(",")
.map(x => parseInt(x))
.slice(0, 3);
}
warn(`Not a valid color format: "${color}"`);
return [0, 0, 0];
}

View File

@ -20,6 +20,7 @@ import {
} from "../../shared/util.js";
import { AnnotationEditor } from "./editor.js";
import { fitCurve } from "pdfjs-fitCurve";
import { opacityToHex } from "./tools.js";
// The dimensions of the resizer is 15x15:
// https://searchfox.org/mozilla-central/rev/1ce190047b9556c3c10ab4de70a0e61d893e2954/toolkit/content/minimal-xul.css#136-137
@ -48,14 +49,20 @@ class InkEditor extends AnnotationEditor {
#isCanvasInitialized = false;
#lastPoint = null;
#observer = null;
#realWidth = 0;
#realHeight = 0;
#requestFrameCallback = null;
static _defaultColor = null;
static _defaultOpacity = 1;
static _defaultThickness = 1;
static _l10nPromise;
@ -64,6 +71,7 @@ class InkEditor extends AnnotationEditor {
super({ ...params, name: "inkEditor" });
this.color = params.color || null;
this.thickness = params.thickness || null;
this.opacity = params.opacity || null;
this.paths = [];
this.bezierPath2D = [];
this.currentPath = [];
@ -90,6 +98,9 @@ class InkEditor extends AnnotationEditor {
case AnnotationEditorParamsType.INK_COLOR:
InkEditor._defaultColor = value;
break;
case AnnotationEditorParamsType.INK_OPACITY:
InkEditor._defaultOpacity = value / 100;
break;
}
}
@ -102,6 +113,9 @@ class InkEditor extends AnnotationEditor {
case AnnotationEditorParamsType.INK_COLOR:
this.#updateColor(value);
break;
case AnnotationEditorParamsType.INK_OPACITY:
this.#updateOpacity(value);
break;
}
}
@ -112,6 +126,10 @@ class InkEditor extends AnnotationEditor {
AnnotationEditorParamsType.INK_COLOR,
InkEditor._defaultColor || AnnotationEditor._defaultLineColor,
],
[
AnnotationEditorParamsType.INK_OPACITY,
Math.round(InkEditor._defaultOpacity * 100),
],
];
}
@ -128,6 +146,10 @@ class InkEditor extends AnnotationEditor {
InkEditor._defaultColor ||
AnnotationEditor._defaultLineColor,
],
[
AnnotationEditorParamsType.INK_OPACITY,
Math.round(100 * (this.opacity ?? InkEditor._defaultOpacity)),
],
];
}
@ -175,6 +197,29 @@ class InkEditor extends AnnotationEditor {
});
}
/**
* Update the opacity and make this action undoable.
* @param {number} opacity
*/
#updateOpacity(opacity) {
opacity /= 100;
const savedOpacity = this.opacity;
this.parent.addCommands({
cmd: () => {
this.opacity = opacity;
this.#redraw();
},
undo: () => {
this.opacity = savedOpacity;
this.#redraw();
},
mustExec: true,
type: AnnotationEditorParamsType.INK_OPACITY,
overwriteIfSameType: true,
keepUndo: true,
});
}
/** @inheritdoc */
rebuild() {
super.rebuild();
@ -282,7 +327,7 @@ class InkEditor extends AnnotationEditor {
this.ctx.lineCap = "round";
this.ctx.lineJoin = "round";
this.ctx.miterLimit = 10;
this.ctx.strokeStyle = this.color;
this.ctx.strokeStyle = `${this.color}${opacityToHex(this.opacity)}`;
}
/**
@ -298,11 +343,35 @@ class InkEditor extends AnnotationEditor {
this.thickness ||= InkEditor._defaultThickness;
this.color ||=
InkEditor._defaultColor || AnnotationEditor._defaultLineColor;
this.opacity ??= InkEditor._defaultOpacity;
}
this.currentPath.push([x, y]);
this.#lastPoint = null;
this.#setStroke();
this.ctx.beginPath();
this.ctx.moveTo(x, y);
this.#requestFrameCallback = () => {
if (!this.#requestFrameCallback) {
return;
}
if (this.#lastPoint) {
if (this.isEmpty()) {
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
} else {
this.#redraw();
}
this.ctx.lineTo(...this.#lastPoint);
this.#lastPoint = null;
this.ctx.stroke();
}
window.requestAnimationFrame(this.#requestFrameCallback);
};
window.requestAnimationFrame(this.#requestFrameCallback);
}
/**
@ -311,9 +380,12 @@ class InkEditor extends AnnotationEditor {
* @param {number} y
*/
#draw(x, y) {
const [lastX, lastY] = this.currentPath.at(-1);
if (x === lastX && y === lastY) {
return;
}
this.currentPath.push([x, y]);
this.ctx.lineTo(x, y);
this.ctx.stroke();
this.#lastPoint = [x, y];
}
/**
@ -322,20 +394,22 @@ class InkEditor extends AnnotationEditor {
* @param {number} y
*/
#stopDrawing(x, y) {
this.ctx.closePath();
this.#requestFrameCallback = null;
x = Math.min(Math.max(x, 0), this.canvas.width);
y = Math.min(Math.max(y, 0), this.canvas.height);
this.currentPath.push([x, y]);
const [lastX, lastY] = this.currentPath.at(-1);
if (x !== lastX || y !== lastY) {
this.currentPath.push([x, y]);
}
// Interpolate the path entered by the user with some
// Bezier's curves in order to have a smoother path and
// to reduce the data size used to draw it in the PDF.
let bezier;
if (
this.currentPath.length !== 2 ||
this.currentPath[0][0] !== x ||
this.currentPath[0][1] !== y
) {
if (this.currentPath.length !== 1) {
bezier = fitCurve(this.currentPath, 30, null);
} else {
// We have only one point finally.
@ -372,17 +446,15 @@ class InkEditor extends AnnotationEditor {
* Redraw all the paths.
*/
#redraw() {
this.#setStroke();
if (this.isEmpty()) {
this.#updateTransform();
return;
}
this.#setStroke();
const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
const { ctx, height, width } = this;
const { canvas, ctx } = this;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, width * parentWidth, height * parentHeight);
ctx.clearRect(0, 0, canvas.width, canvas.height);
this.#updateTransform();
for (const path of this.bezierPath2D) {
ctx.stroke(path);
@ -919,6 +991,7 @@ class InkEditor extends AnnotationEditor {
editor.thickness = data.thickness;
editor.color = Util.makeHexColor(...data.color);
editor.opacity = data.opacity;
const [pageWidth, pageHeight] = parent.pageDimensions;
const width = editor.width * pageWidth;
@ -980,6 +1053,7 @@ class InkEditor extends AnnotationEditor {
annotationType: AnnotationEditorType.INK,
color,
thickness: this.thickness,
opacity: this.opacity,
paths: this.#serializePaths(
this.scaleFactor / this.parent.scaleFactor,
this.translationX,

View File

@ -30,6 +30,18 @@ function bindEvents(obj, element, names) {
element.addEventListener(name, obj[name].bind(obj));
}
}
/**
* Convert a number between 0 and 100 into an hex number between 0 and 255.
* @param {number} opacity
* @return {string}
*/
function opacityToHex(opacity) {
return Math.round(Math.min(255, Math.max(1, 255 * opacity)))
.toString(16)
.padStart(2, "0");
}
/**
* Class to create some unique ids for the different editors.
*/
@ -1025,4 +1037,5 @@ export {
ColorManager,
CommandManager,
KeyboardManager,
opacityToHex,
};

View File

@ -62,10 +62,12 @@ const AnnotationEditorType = {
};
const AnnotationEditorParamsType = {
FREETEXT_SIZE: 0,
FREETEXT_COLOR: 1,
INK_COLOR: 2,
INK_THICKNESS: 3,
FREETEXT_SIZE: 1,
FREETEXT_COLOR: 2,
FREETEXT_OPACITY: 3,
INK_COLOR: 11,
INK_THICKNESS: 12,
INK_OPACITY: 13,
};
// Permission flags from Table 22, Section 7.6.3.2 of the PDF specification.

View File

@ -4185,6 +4185,7 @@ describe("annotation", function () {
rect: [12, 34, 56, 78],
rotation: 0,
thickness: 1,
opacity: 1,
color: [0, 0, 0],
paths: [
{
@ -4234,6 +4235,70 @@ describe("annotation", function () {
);
});
it("should create a new Ink annotation with some transparency", async function () {
partialEvaluator.xref = new XRefMock();
const task = new WorkerTask("test Ink creation");
const data = await AnnotationFactory.saveNewAnnotations(
partialEvaluator,
task,
[
{
annotationType: AnnotationEditorType.INK,
rect: [12, 34, 56, 78],
rotation: 0,
thickness: 1,
opacity: 0.12,
color: [0, 0, 0],
paths: [
{
bezier: [
10, 11, 12, 13, 14, 15, 16, 17, 22, 23, 24, 25, 26, 27,
],
points: [1, 2, 3, 4, 5, 6, 7, 8],
},
{
bezier: [
910, 911, 912, 913, 914, 915, 916, 917, 922, 923, 924, 925,
926, 927,
],
points: [91, 92, 93, 94, 95, 96, 97, 98],
},
],
},
]
);
const base = data.annotations[0].data.replace(/\(D:\d+\)/, "(date)");
expect(base).toEqual(
"1 0 obj\n" +
"<< /Type /Annot /Subtype /Ink /CreationDate (date) /Rect [12 34 56 78] " +
"/InkList [[1 2 3 4 5 6 7 8] [91 92 93 94 95 96 97 98]] /F 4 /Border [0 0 0] " +
"/Rotate 0 /AP << /N 2 0 R>>>>\n" +
"endobj\n"
);
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 136 /Resources " +
"<< /ExtGState << /R0 << /CA 0.12 /Type /ExtGState>>>>>>>> stream\n" +
"1 w 1 J 1 j\n" +
"0 G\n" +
"/R0 gs\n" +
"10 11 m\n" +
"12 13 14 15 16 17 c\n" +
"22 23 24 25 26 27 c\n" +
"S\n" +
"910 911 m\n" +
"912 913 914 915 916 917 c\n" +
"922 923 924 925 926 927 c\n" +
"S\n" +
"endstream\n" +
"\n" +
"endobj\n"
);
});
it("should render an added Ink annotation for printing", async function () {
partialEvaluator.xref = new XRefMock();
const task = new WorkerTask("test Ink printing");
@ -4244,6 +4309,7 @@ describe("annotation", function () {
rect: [12, 34, 56, 78],
rotation: 0,
thickness: 3,
opacity: 1,
color: [0, 255, 0],
paths: [
{

View File

@ -30,6 +30,7 @@ class AnnotationEditorParams {
editorFreeTextColor,
editorInkColor,
editorInkThickness,
editorInkOpacity,
}) {
editorFreeTextFontSize.addEventListener("input", evt => {
this.eventBus.dispatch("switchannotationeditorparams", {
@ -59,6 +60,13 @@ class AnnotationEditorParams {
value: editorInkThickness.valueAsNumber,
});
});
editorInkOpacity.addEventListener("input", evt => {
this.eventBus.dispatch("switchannotationeditorparams", {
source: this,
type: AnnotationEditorParamsType.INK_OPACITY,
value: editorInkOpacity.valueAsNumber,
});
});
this.eventBus._on("annotationeditorparamschanged", evt => {
for (const [type, value] of evt.details) {
@ -75,6 +83,9 @@ class AnnotationEditorParams {
case AnnotationEditorParamsType.INK_THICKNESS:
editorInkThickness.value = value;
break;
case AnnotationEditorParamsType.INK_OPACITY:
editorInkOpacity.value = value;
break;
}
}
});

View File

@ -171,6 +171,10 @@ See https://github.com/adobe-type-tools/cmap-resources
<label for="editorInkThickness" class="editorParamsLabel" data-l10n-id="editor_ink_line_thickness">Line Thickness</label>
<input type="range" id="editorInkThickness" class="editorParamsSlider" value="1" min="1" max="20" step="1" tabindex="103">
</div>
<div class="editorParamsSetter">
<label for="editorInkOpacity" class="editorParamsLabel" data-l10n-id="editor_ink_line_opacity">Line Opacity</label>
<input type="range" id="editorInkOpacity" class="editorParamsSlider" value="100" min="1" max="100" step="1" tabindex="104">
</div>
</div>
</div>

View File

@ -202,6 +202,7 @@ function getViewerConfiguration() {
editorFreeTextColor: document.getElementById("editorFreeTextColor"),
editorInkColor: document.getElementById("editorInkColor"),
editorInkThickness: document.getElementById("editorInkThickness"),
editorInkOpacity: document.getElementById("editorInkOpacity"),
},
errorWrapper,
printContainer: document.getElementById("printContainer"),