[Editor] Add the possibility to change line opacity in Ink editor
This commit is contained in:
parent
45b9e8417d
commit
7831a100b3
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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.
|
||||
|
@ -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: [
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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"),
|
||||
|
Loading…
Reference in New Issue
Block a user