diff --git a/.eslintrc b/.eslintrc index 537ac0086..40e21f293 100644 --- a/.eslintrc +++ b/.eslintrc @@ -34,7 +34,7 @@ // Plugins "import/extensions": ["error", "always", { "ignorePackages": true, }], "import/no-unresolved": ["error", { - "ignore": ["pdfjs", "pdfjs-lib", "pdfjs-web", "pdfjs-fitCurve", "web"] + "ignore": ["pdfjs", "pdfjs-lib", "pdfjs-web", "web"] }], "mozilla/avoid-removeChild": "error", "mozilla/use-includes-instead-of-indexOf": "error", diff --git a/gulpfile.js b/gulpfile.js index 26e36252d..0c4138db8 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -232,7 +232,6 @@ function createWebpackConfig( pdfjs: "src", "pdfjs-web": "web", "pdfjs-lib": "web/pdfjs", - "pdfjs-fitCurve": "src/display/editor/fit_curve", }; const viewerAlias = { "web-annotation_editor_params": "web/annotation_editor_params.js", @@ -571,26 +570,6 @@ function createImageDecodersBundle(defines) { .pipe(replaceJSRootName(imageDecodersAMDName, "pdfjsImageDecoders")); } -function createFitCurveBundle(defines) { - const fitCurveOutputName = "fit_curve.js"; - - const fitCurveFileConfig = createWebpackConfig( - defines, - { - filename: fitCurveOutputName, - library: { - type: "module", - }, - }, - { - disableVersionInfo: true, - } - ); - return gulp - .src("src/display/editor/fit_curve.js") - .pipe(webpack2Stream(fitCurveFileConfig)); -} - function createCMapBundle() { return gulp.src(["external/bcmaps/*.bcmap", "external/bcmaps/LICENSE"], { base: "external/bcmaps", @@ -1551,7 +1530,6 @@ function buildLibHelper(bundleDefines, inputStream, outputDir) { defines: bundleDefines, map: { "pdfjs-lib": "../pdf", - "pdfjs-fitCurve": "./fit_curve", }, }; const licenseHeaderLibre = fs @@ -1690,90 +1668,54 @@ function setTestEnv(done) { done(); } -gulp.task("dev-fitCurve", function createDevFitCurve() { - console.log(); - console.log("### Building development fitCurve"); - - const defines = builder.merge(DEFINES, { GENERIC: true, TESTING: true }); - const fitCurveDir = BUILD_DIR + "dev-fitCurve/"; - - rimraf.sync(fitCurveDir); - - return createFitCurveBundle(defines).pipe(gulp.dest(fitCurveDir)); -}); - gulp.task( "test", - gulp.series( - setTestEnv, - "generic", - "components", - "dev-fitCurve", - function runTest() { - return streamqueue( - { objectMode: true }, - createTestSource("unit"), - createTestSource("browser"), - createTestSource("integration") - ); - } - ) + gulp.series(setTestEnv, "generic", "components", function runTest() { + return streamqueue( + { objectMode: true }, + createTestSource("unit"), + createTestSource("browser"), + createTestSource("integration") + ); + }) ); gulp.task( "bottest", - gulp.series( - setTestEnv, - "generic", - "components", - "dev-fitCurve", - function runBotTest() { - return streamqueue( - { objectMode: true }, - createTestSource("unit", { bot: true }), - createTestSource("font", { bot: true }), - createTestSource("browser", { bot: true }), - createTestSource("integration") - ); - } - ) + gulp.series(setTestEnv, "generic", "components", function runBotTest() { + return streamqueue( + { objectMode: true }, + createTestSource("unit", { bot: true }), + createTestSource("font", { bot: true }), + createTestSource("browser", { bot: true }), + createTestSource("integration") + ); + }) ); gulp.task( "xfatest", - gulp.series( - setTestEnv, - "generic", - "components", - "dev-fitCurve", - function runXfaTest() { - return streamqueue( - { objectMode: true }, - createTestSource("unit"), - createTestSource("browser", { xfaOnly: true }), - createTestSource("integration") - ); - } - ) + gulp.series(setTestEnv, "generic", "components", function runXfaTest() { + return streamqueue( + { objectMode: true }, + createTestSource("unit"), + createTestSource("browser", { xfaOnly: true }), + createTestSource("integration") + ); + }) ); gulp.task( "botxfatest", - gulp.series( - setTestEnv, - "generic", - "components", - "dev-fitCurve", - function runBotXfaTest() { - return streamqueue( - { objectMode: true }, - createTestSource("unit", { bot: true }), - createTestSource("font", { bot: true }), - createTestSource("browser", { bot: true, xfaOnly: true }), - createTestSource("integration") - ); - } - ) + gulp.series(setTestEnv, "generic", "components", function runBotXfaTest() { + return streamqueue( + { objectMode: true }, + createTestSource("unit", { bot: true }), + createTestSource("font", { bot: true }), + createTestSource("browser", { bot: true, xfaOnly: true }), + createTestSource("integration") + ); + }) ); gulp.task( @@ -1800,7 +1742,7 @@ gulp.task( gulp.task( "unittest", - gulp.series(setTestEnv, "generic", "dev-fitCurve", function runUnitTest() { + gulp.series(setTestEnv, "generic", function runUnitTest() { return createTestSource("unit"); }) ); @@ -2028,13 +1970,6 @@ gulp.task( gulp.task( "server", gulp.parallel( - function watchDevFitCurve() { - gulp.watch( - ["src/display/editor/*"], - { ignoreInitial: false }, - gulp.series("dev-fitCurve") - ); - }, function watchDevSandbox() { gulp.watch( [ diff --git a/package-lock.json b/package-lock.json index ce3be3e3b..13061772a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,6 @@ "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-sort-exports": "^0.8.0", "eslint-plugin-unicorn": "^47.0.0", - "fit-curve": "^0.2.0", "globals": "^13.20.0", "gulp": "^4.0.2", "gulp-postcss": "^9.0.1", @@ -7065,12 +7064,6 @@ "node": ">= 0.10" } }, - "node_modules/fit-curve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/fit-curve/-/fit-curve-0.2.0.tgz", - "integrity": "sha512-op7ofeL13getbqL5J5ACeNxTlLWzusn4/jjEjSVA1sS7PfXWumdtNITvLuTjSobis+jZzMil2rsJ3Vhw7OxQyQ==", - "dev": true - }, "node_modules/flagged-respawn": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", @@ -12732,6 +12725,7 @@ }, "node_modules/npm/node_modules/lodash._baseindexof": { "version": "3.1.0", + "dev": true, "inBundle": true, "license": "MIT" }, @@ -12747,16 +12741,19 @@ }, "node_modules/npm/node_modules/lodash._bindcallback": { "version": "3.0.1", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/lodash._cacheindexof": { "version": "3.0.2", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/lodash._createcache": { "version": "3.1.2", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -12771,6 +12768,7 @@ }, "node_modules/npm/node_modules/lodash._getnative": { "version": "3.9.1", + "dev": true, "inBundle": true, "license": "MIT" }, @@ -12788,6 +12786,7 @@ }, "node_modules/npm/node_modules/lodash.restparam": { "version": "3.6.1", + "dev": true, "inBundle": true, "license": "MIT" }, diff --git a/package.json b/package.json index f37ebafee..f46e243ad 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-sort-exports": "^0.8.0", "eslint-plugin-unicorn": "^47.0.0", - "fit-curve": "^0.2.0", "globals": "^13.20.0", "gulp": "^4.0.2", "gulp-postcss": "^9.0.1", diff --git a/src/display/editor/fit_curve.js b/src/display/editor/fit_curve.js deleted file mode 100644 index 38b29b4ee..000000000 --- a/src/display/editor/fit_curve.js +++ /dev/null @@ -1,20 +0,0 @@ -/* 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. - */ - -const fitCurve = require(PDFJSDev.test("LIB") - ? "fit-curve" - : "fit-curve/src/fit-curve.js"); - -export { fitCurve }; diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index 06af0e73c..2d4514c80 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -19,7 +19,6 @@ import { Util, } 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: @@ -37,6 +36,8 @@ class InkEditor extends AnnotationEditor { #baseWidth = 0; + #boundCanvasContextMenu = this.canvasContextMenu.bind(this); + #boundCanvasPointermove = this.canvasPointermove.bind(this); #boundCanvasPointerleave = this.canvasPointerleave.bind(this); @@ -45,11 +46,13 @@ class InkEditor extends AnnotationEditor { #boundCanvasPointerdown = this.canvasPointerdown.bind(this); + #currentPath2D = new Path2D(); + #disableEditing = false; - #isCanvasInitialized = false; + #hasSomethingToDraw = false; - #lastPoint = null; + #isCanvasInitialized = false; #observer = null; @@ -76,6 +79,7 @@ class InkEditor extends AnnotationEditor { this.opacity = params.opacity || null; this.paths = []; this.bezierPath2D = []; + this.allRawPaths = []; this.currentPath = []; this.scaleFactor = 1; this.translationX = this.translationY = 0; @@ -294,7 +298,6 @@ class InkEditor extends AnnotationEditor { super.enableEditMode(); this.div.draggable = false; this.canvas.addEventListener("pointerdown", this.#boundCanvasPointerdown); - this.canvas.addEventListener("pointerup", this.#boundCanvasPointerup); } /** @inheritdoc */ @@ -311,7 +314,6 @@ class InkEditor extends AnnotationEditor { "pointerdown", this.#boundCanvasPointerdown ); - this.canvas.removeEventListener("pointerup", this.#boundCanvasPointerup); } /** @inheritdoc */ @@ -362,6 +364,15 @@ class InkEditor extends AnnotationEditor { * @param {number} y */ #startDrawing(x, y) { + this.canvas.addEventListener("contextmenu", this.#boundCanvasContextMenu); + this.canvas.addEventListener("pointerleave", this.#boundCanvasPointerleave); + this.canvas.addEventListener("pointermove", this.#boundCanvasPointermove); + this.canvas.addEventListener("pointerup", this.#boundCanvasPointerup); + this.canvas.removeEventListener( + "pointerdown", + this.#boundCanvasPointerdown + ); + this.isEditing = true; if (!this.#isCanvasInitialized) { this.#isCanvasInitialized = true; @@ -372,30 +383,14 @@ class InkEditor extends AnnotationEditor { this.opacity ??= InkEditor._defaultOpacity; } this.currentPath.push([x, y]); - this.#lastPoint = null; + this.#hasSomethingToDraw = false; this.#setStroke(); - this.ctx.beginPath(); - this.ctx.moveTo(x, y); this.#requestFrameCallback = () => { - if (!this.#requestFrameCallback) { - return; + this.#drawPoints(); + if (this.#requestFrameCallback) { + window.requestAnimationFrame(this.#requestFrameCallback); } - - 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); } @@ -407,11 +402,40 @@ class InkEditor extends AnnotationEditor { */ #draw(x, y) { const [lastX, lastY] = this.currentPath.at(-1); - if (x === lastX && y === lastY) { + if (this.currentPath.length > 1 && x === lastX && y === lastY) { return; } - this.currentPath.push([x, y]); - this.#lastPoint = [x, y]; + const currentPath = this.currentPath; + let path2D = this.#currentPath2D; + currentPath.push([x, y]); + this.#hasSomethingToDraw = true; + + if (currentPath.length <= 2) { + path2D.moveTo(...currentPath[0]); + path2D.lineTo(x, y); + return; + } + + if (currentPath.length === 3) { + this.#currentPath2D = path2D = new Path2D(); + path2D.moveTo(...currentPath[0]); + } + + this.#makeBezierCurve( + path2D, + ...currentPath.at(-3), + ...currentPath.at(-2), + x, + y + ); + } + + #endPath() { + if (this.currentPath.length === 0) { + return; + } + const lastPoint = this.currentPath.at(-1); + this.#currentPath2D.lineTo(...lastPoint); } /** @@ -420,38 +444,39 @@ 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); - const [lastX, lastY] = this.currentPath.at(-1); - if (x !== lastX || y !== lastY) { - this.currentPath.push([x, y]); - } + this.#draw(x, y); + this.#endPath(); // 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 !== 1) { - bezier = fitCurve(this.currentPath, 30, null); + bezier = this.#generateBezierPoints(); } else { // We have only one point finally. const xy = [x, y]; bezier = [[xy, xy.slice(), xy.slice(), xy]]; } - const path2D = InkEditor.#buildPath2D(bezier); - this.currentPath.length = 0; + const path2D = this.#currentPath2D; + const currentPath = this.currentPath; + this.currentPath = []; + this.#currentPath2D = new Path2D(); const cmd = () => { + this.allRawPaths.push(currentPath); this.paths.push(bezier); this.bezierPath2D.push(path2D); this.rebuild(); }; const undo = () => { + this.allRawPaths.pop(); this.paths.pop(); this.bezierPath2D.pop(); if (this.paths.length === 0) { @@ -468,6 +493,95 @@ class InkEditor extends AnnotationEditor { this.addCommands({ cmd, undo, mustExec: true }); } + #drawPoints() { + if (!this.#hasSomethingToDraw) { + return; + } + 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; + + const { ctx } = this; + ctx.save(); + + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { + // In Chrome, the clip() method doesn't work as expected. + ctx.clearRect(xMin, yMin, xMax - xMin, yMax - yMin); + ctx.beginPath(); + ctx.rect(xMin, yMin, xMax - xMin, yMax - yMin); + ctx.clip(); + } else { + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + } + + for (const path of this.bezierPath2D) { + ctx.stroke(path); + } + ctx.stroke(this.#currentPath2D); + + ctx.restore(); + } + + #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; + if (path.length <= 2) { + return [[path[0], path[0], path.at(-1), path.at(-1)]]; + } + + const bezierPoints = []; + let i; + let [x0, y0] = path[0]; + 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; + + // The quadratic is: [[x0, y0], [x1, y1], [x3, y3]]. + // Convert the quadratic to a cubic + // (see https://fontforge.org/docs/techref/bezier.html#converting-truetype-to-postscript) + 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]]); + + [x0, y0] = [x3, y3]; + } + + const [x1, y1] = path[i]; + const [x2, y2] = path[i + 1]; + + // The quadratic is: [[x0, y0], [x1, y1], [x2, y2]]. + 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]]); + return bezierPoints; + } + /** * Redraw all the paths. */ @@ -482,6 +596,7 @@ class InkEditor extends AnnotationEditor { ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.clearRect(0, 0, canvas.width, canvas.height); this.#updateTransform(); + for (const path of this.bezierPath2D) { ctx.stroke(path); } @@ -537,24 +652,29 @@ class InkEditor extends AnnotationEditor { // Since it's the last child, there's no need to give it a higher z-index. this.setInForeground(); + event.preventDefault(); + if (event.type !== "mouse") { this.div.focus(); } - event.stopPropagation(); - - this.canvas.addEventListener("pointerleave", this.#boundCanvasPointerleave); - this.canvas.addEventListener("pointermove", this.#boundCanvasPointermove); - this.#startDrawing(event.offsetX, event.offsetY); } + /** + * oncontextmenu callback for the canvas we're drawing on. + * @param {PointerEvent} event + */ + canvasContextMenu(event) { + event.preventDefault(); + } + /** * onpointermove callback for the canvas we're drawing on. * @param {PointerEvent} event */ canvasPointermove(event) { - event.stopPropagation(); + event.preventDefault(); this.#draw(event.offsetX, event.offsetY); } @@ -563,17 +683,8 @@ class InkEditor extends AnnotationEditor { * @param {PointerEvent} event */ canvasPointerup(event) { - if (event.button !== 0) { - return; - } - if (this.isInEditMode() && this.currentPath.length !== 0) { - event.stopPropagation(); - this.#endDrawing(event); - - // Since the ink editor covers all of the page and we want to be able - // to select another editor, we just put this one in the background. - this.setInBackground(); - } + event.preventDefault(); + this.#endDrawing(event); } /** @@ -582,7 +693,6 @@ class InkEditor extends AnnotationEditor { */ canvasPointerleave(event) { this.#endDrawing(event); - this.setInBackground(); } /** @@ -590,8 +700,6 @@ class InkEditor extends AnnotationEditor { * @param {PointerEvent} event */ #endDrawing(event) { - this.#stopDrawing(event.offsetX, event.offsetY); - this.canvas.removeEventListener( "pointerleave", this.#boundCanvasPointerleave @@ -600,8 +708,25 @@ class InkEditor extends AnnotationEditor { "pointermove", this.#boundCanvasPointermove ); + this.canvas.removeEventListener("pointerup", this.#boundCanvasPointerup); + this.canvas.addEventListener("pointerdown", this.#boundCanvasPointerdown); + + // Slight delay to avoid the context menu to appear (it can happen on a long + // tap with a pen). + setTimeout(() => { + this.canvas.removeEventListener( + "contextmenu", + this.#boundCanvasContextMenu + ); + }, 10); + + this.#stopDrawing(event.offsetX, event.offsetY); this.addToAnnotationStorage(); + + // Since the ink editor covers all of the page and we want to be able + // to select another editor, we just put this one in the background. + this.setInBackground(); } /** @@ -762,7 +887,7 @@ class InkEditor extends AnnotationEditor { } /** - * Convert the output of fitCurve in some Path2D. + * Convert into a Path2D. * @param {Arra} bezier * @returns {Path2D} */ @@ -1099,4 +1224,4 @@ class InkEditor extends AnnotationEditor { } } -export { fitCurve, InkEditor }; +export { InkEditor }; diff --git a/test/unit/editor_spec.js b/test/unit/editor_spec.js index ea4cb3b24..7adda9588 100644 --- a/test/unit/editor_spec.js +++ b/test/unit/editor_spec.js @@ -14,7 +14,6 @@ */ import { CommandManager } from "../../src/display/editor/tools.js"; -import { fitCurve } from "../../src/display/editor/ink.js"; describe("editor", function () { describe("Command Manager", function () { @@ -91,29 +90,4 @@ describe("editor", function () { manager.add({ ...makeDoUndo(5), mustExec: true }); expect(x).toEqual(11); }); - - describe("fitCurve", function () { - it("should return a function", function () { - expect(typeof fitCurve).toEqual("function"); - }); - - it("should compute an Array of bezier curves", function () { - const bezier = fitCurve( - [ - [1, 2], - [4, 5], - ], - 30, - null - ); - expect(bezier).toEqual([ - [ - [1, 2], - [2, 3], - [3, 4], - [4, 5], - ], - ]); - }); - }); }); diff --git a/test/unit/unit_test.html b/test/unit/unit_test.html index d9980f18f..622ff00ee 100644 --- a/test/unit/unit_test.html +++ b/test/unit/unit_test.html @@ -15,7 +15,6 @@ "pdfjs/": "../../src/", "pdfjs-lib": "../../src/pdf.js", "pdfjs-web/": "../../web/", - "pdfjs-fitCurve": "../../build/dev-fitCurve/fit_curve.js", "pdfjs-test/": "../", "web-annotation_editor_params": "../../web/annotation_editor_params.js", diff --git a/web/viewer-geckoview.html b/web/viewer-geckoview.html index c9fcaaf1f..3855ba348 100644 --- a/web/viewer-geckoview.html +++ b/web/viewer-geckoview.html @@ -48,7 +48,6 @@ See https://github.com/adobe-type-tools/cmap-resources "pdfjs/": "../src/", "pdfjs-lib": "../src/pdf.js", "pdfjs-web/": "./", - "pdfjs-fitCurve": "../build/dev-fitCurve/fit_curve.js", "web-annotation_editor_params": "./stubs-geckoview.js", "web-com": "./genericcom.js", diff --git a/web/viewer.html b/web/viewer.html index 132595629..38ea691a8 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -59,7 +59,6 @@ See https://github.com/adobe-type-tools/cmap-resources "pdfjs/": "../src/", "pdfjs-lib": "../src/pdf.js", "pdfjs-web/": "./", - "pdfjs-fitCurve": "../build/dev-fitCurve/fit_curve.js", "web-annotation_editor_params": "./annotation_editor_params.js", "web-com": "./genericcom.js",