: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
window.requestAnimationFrame(this.#requestFrameCallback);
const [lastX, lastY] = this.currentPath.at(-1);
if (this.currentPath.length > 1 && x === lastX && y === lastY) {
const currentPath = this.currentPath;
let path2D = this.#currentPath2D;
currentPath.push([x, y]);
this.#hasSomethingToDraw = true;
if (currentPath.length <= 2) {
path2D.moveTo(...currentPath[0]);
if (currentPath.length === 3) {
this.#currentPath2D = path2D = new Path2D();
path2D.moveTo(...currentPath[0]);
this.#makeBezierCurve(path2D, ...currentPath.at(-3), ...currentPath.at(-2), x, y);
if (this.currentPath.length === 0) {
const lastPoint = this.currentPath.at(-1);
this.#currentPath2D.lineTo(...lastPoint);
this.#requestFrameCallback = null;
x = Math.min(Math.max(x, 0), this.canvas.width);
y = Math.min(Math.max(y, 0), this.canvas.height);
if (this.currentPath.length !== 1) {
bezier = this.#generateBezierPoints();
bezier = [[xy, xy.slice(), xy.slice(), xy]];
const path2D = this.#currentPath2D;
const currentPath = this.currentPath;
this.#currentPath2D = new Path2D();
this.allRawPaths.push(currentPath);
this.bezierPath2D.push(path2D);
this._uiManager.rebuild(this);
if (this.paths.length === 0) {
if (!this.#hasSomethingToDraw) {
this.#hasSomethingToDraw = false;
const thickness = Math.ceil(this.thickness * this.parentScale);
const lastPoints = this.currentPath.slice(-3);
const x = lastPoints.map(xy => xy[0]);
const y = lastPoints.map(xy => xy[1]);
const xMin = Math.min(...x) - thickness;
const xMax = Math.max(...x) + thickness;
const yMin = Math.min(...y) - thickness;
const yMax = Math.max(...y) + thickness;
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
for (const path of this.bezierPath2D) {
ctx.stroke(this.#currentPath2D);
#makeBezierCurve(path2D, x0, y0, x1, y1, x2, y2) {
const prevX = (x0 + x1) / 2;
const prevY = (y0 + y1) / 2;
const x3 = (x1 + x2) / 2;
const y3 = (y1 + y2) / 2;
path2D.bezierCurveTo(prevX + 2 * (x1 - prevX) / 3, prevY + 2 * (y1 - prevY) / 3, x3 + 2 * (x1 - x3) / 3, y3 + 2 * (y1 - y3) / 3, x3, y3);
#generateBezierPoints() {
const path = this.currentPath;
return [[path[0], path[0], path.at(-1), path.at(-1)]];
for (i = 1; i < path.length - 2; i++) {
const [x1, y1] = path[i];
const [x2, y2] = path[i + 1];
const x3 = (x1 + x2) / 2;
const y3 = (y1 + y2) / 2;
const control1 = [x0 + 2 * (x1 - x0) / 3, y0 + 2 * (y1 - y0) / 3];
const control2 = [x3 + 2 * (x1 - x3) / 3, y3 + 2 * (y1 - y3) / 3];
bezierPoints.push([[x0, y0], control1, control2, [x3, y3]]);
const [x1, y1] = path[i];
const [x2, y2] = path[i + 1];
const control1 = [x0 + 2 * (x1 - x0) / 3, y0 + 2 * (y1 - y0) / 3];
const control2 = [x2 + 2 * (x1 - x2) / 3, y2 + 2 * (y1 - y2) / 3];
bezierPoints.push([[x0, y0], control1, control2, [x2, y2]]);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const path of this.bezierPath2D) {
if (this.#disableEditing) {
this.#disableEditing = true;
this.div.classList.add("disabled");
this.#fitToContent(true);
this.parent.addInkEditorIfNeeded(true);
if (!this._focusEventsAllowed) {
canvasPointerdown(event) {
if (event.button !== 0 || !this.isInEditMode() || this.#disableEditing) {
if (!this.div.contains(document.activeElement)) {
this.#startDrawing(event.offsetX, event.offsetY);
canvasPointermove(event) {
this.#draw(event.offsetX, event.offsetY);
canvasPointerleave(event) {
this.canvas.removeEventListener("pointerleave", this.#boundCanvasPointerleave);
this.canvas.removeEventListener("pointermove", this.#boundCanvasPointermove);
this.canvas.removeEventListener("pointerup", this.#boundCanvasPointerup);
this.canvas.addEventListener("pointerdown", this.#boundCanvasPointerdown);
if (this.#canvasContextMenuTimeoutId) {
clearTimeout(this.#canvasContextMenuTimeoutId);
this.#canvasContextMenuTimeoutId = setTimeout(() => {
this.#canvasContextMenuTimeoutId = null;
this.canvas.removeEventListener("contextmenu", noContextMenu);
this.#stopDrawing(event.offsetX, event.offsetY);
this.addToAnnotationStorage();
this.canvas = document.createElement("canvas");
this.canvas.width = this.canvas.height = 0;
this.canvas.className = "inkEditorCanvas";
this.canvas.setAttribute("data-l10n-id", "pdfjs-ink-canvas");
this.div.append(this.canvas);
this.ctx = this.canvas.getContext("2d");
this.#observer = new ResizeObserver(entries => {
const rect = entries[0].contentRect;
if (rect.width && rect.height) {
this.setDimensions(rect.width, rect.height);
this.#observer.observe(this.div);
return !this.isEmpty() && this.#disableEditing;
this.div.setAttribute("data-l10n-id", "pdfjs-ink");
const [x, y, w, h] = this.#getInitialBBox();
const [parentWidth, parentHeight] = this.parentDimensions;
this.setAspectRatio(this.width * parentWidth, this.height * parentHeight);
this.setAt(baseX * parentWidth, baseY * parentHeight, this.width * parentWidth, this.height * parentHeight);
this.#isCanvasInitialized = true;
this.setDims(this.width * parentWidth, this.height * parentHeight);
this.div.classList.add("disabled");
this.div.classList.add("editing");
if (!this.#isCanvasInitialized) {
const [parentWidth, parentHeight] = this.parentDimensions;
this.canvas.width = Math.ceil(this.width * parentWidth);
this.canvas.height = Math.ceil(this.height * parentHeight);
setDimensions(width, height) {
const roundedWidth = Math.round(width);
const roundedHeight = Math.round(height);
if (this.#realWidth === roundedWidth && this.#realHeight === roundedHeight) {
this.#realWidth = roundedWidth;
this.#realHeight = roundedHeight;
this.canvas.style.visibility = "hidden";
const [parentWidth, parentHeight] = this.parentDimensions;
this.width = width / parentWidth;
this.height = height / parentHeight;
this.fixAndSetPosition();
if (this.#disableEditing) {
this.#setScaleFactor(width, height);
this.canvas.style.visibility = "visible";
#setScaleFactor(width, height) {
const padding = this.#getPadding();
const scaleFactorW = (width - padding) / this.#baseWidth;
const scaleFactorH = (height - padding) / this.#baseHeight;
this.scaleFactor = Math.min(scaleFactorW, scaleFactorH);
const padding = this.#getPadding() / 2;
this.ctx.setTransform(this.scaleFactor, 0, 0, this.scaleFactor, this.translationX * this.scaleFactor + padding, this.translationY * this.scaleFactor + padding);
static #buildPath2D(bezier) {
const path2D = new Path2D();
for (let i = 0, ii = bezier.length; i < ii; i++) {
const [first, control1, control2, second] = bezier[i];
path2D.bezierCurveTo(control1[0], control1[1], control2[0], control2[1], second[0], second[1]);
static #toPDFCoordinates(points, rect, rotation) {
const [blX, blY, trX, trY] = rect;
for (let i = 0, ii = points.length; i < ii; i += 2) {
points[i + 1] = trY - points[i + 1];
for (let i = 0, ii = points.length; i < ii; i += 2) {
points[i] = points[i + 1] + blX;
for (let i = 0, ii = points.length; i < ii; i += 2) {
points[i] = trX - points[i];
for (let i = 0, ii = points.length; i < ii; i += 2) {
points[i] = trX - points[i + 1];
throw new Error("Invalid rotation");
static #fromPDFCoordinates(points, rect, rotation) {
const [blX, blY, trX, trY] = rect;
for (let i = 0, ii = points.length; i < ii; i += 2) {
points[i + 1] = trY - points[i + 1];
for (let i = 0, ii = points.length; i < ii; i += 2) {
points[i] = points[i + 1] - blY;
for (let i = 0, ii = points.length; i < ii; i += 2) {
points[i] = trX - points[i];
for (let i = 0, ii = points.length; i < ii; i += 2) {
points[i] = trY - points[i + 1];
throw new Error("Invalid rotation");
#serializePaths(s, tx, ty, rect) {
const padding = this.thickness / 2;
const shiftX = s * tx + padding;
const shiftY = s * ty + padding;
for (const bezier of this.paths) {
for (let j = 0, jj = bezier.length; j < jj; j++) {
const [first, control1, control2, second] = bezier[j];
if (first[0] === second[0] && first[1] === second[1] && jj === 1) {
const p0 = s * first[0] + shiftX;
const p1 = s * first[1] + shiftY;
const p10 = s * first[0] + shiftX;
const p11 = s * first[1] + shiftY;
const p20 = s * control1[0] + shiftX;
const p21 = s * control1[1] + shiftY;
const p30 = s * control2[0] + shiftX;
const p31 = s * control2[1] + shiftY;
const p40 = s * second[0] + shiftX;
const p41 = s * second[1] + shiftY;
buffer.push(p20, p21, p30, p31, p40, p41);
bezier: InkEditor.#toPDFCoordinates(buffer, rect, this.rotation),
points: InkEditor.#toPDFCoordinates(points, rect, this.rotation)
for (const path of this.paths) {
for (const [first, control1, control2, second] of path) {
const bbox = Util.bezierBoundingBox(...first, ...control1, ...control2, ...second);
xMin = Math.min(xMin, bbox[0]);
yMin = Math.min(yMin, bbox[1]);
xMax = Math.max(xMax, bbox[2]);
yMax = Math.max(yMax, bbox[3]);
return [xMin, yMin, xMax, yMax];
return this.#disableEditing ? Math.ceil(this.thickness * this.parentScale) : 0;
#fitToContent(firstTime = false) {
if (!this.#disableEditing) {
const bbox = this.#getBbox();
const padding = this.#getPadding();
this.#baseWidth = Math.max(AnnotationEditor.MIN_SIZE, bbox[2] - bbox[0]);
this.#baseHeight = Math.max(AnnotationEditor.MIN_SIZE, bbox[3] - bbox[1]);
const width = Math.ceil(padding + this.#baseWidth * this.scaleFactor);
const height = Math.ceil(padding + this.#baseHeight * this.scaleFactor);
const [parentWidth, parentHeight] = this.parentDimensions;
this.width = width / parentWidth;
this.height = height / parentHeight;
this.setAspectRatio(width, height);
const prevTranslationX = this.translationX;
const prevTranslationY = this.translationY;
this.translationX = -bbox[0];
this.translationY = -bbox[1];
this.#realHeight = height;
this.setDims(width, height);
const unscaledPadding = firstTime ? padding / this.scaleFactor / 2 : 0;
this.translate(prevTranslationX - this.translationX - unscaledPadding, prevTranslationY - this.translationY - unscaledPadding);
static deserialize(data, parent, uiManager) {
if (data instanceof InkAnnotationElement) {
const editor = super.deserialize(data, parent, uiManager);
editor.thickness = data.thickness;
editor.color = Util.makeHexColor(...data.color);
editor.opacity = data.opacity;
const [pageWidth, pageHeight] = editor.pageDimensions;
const width = editor.width * pageWidth;
const height = editor.height * pageHeight;
const scaleFactor = editor.parentScale;
const padding = data.thickness / 2;
editor.#disableEditing = true;
editor.#realWidth = Math.round(width);
editor.#realHeight = Math.round(height);
bezier = InkEditor.#fromPDFCoordinates(bezier, rect, rotation);