Merge pull request #15142 from Snuffleupagus/fitCurve
[editor] Use the `fit-curve` package (issue 15004)
This commit is contained in:
commit
7f160d49f9
@ -35,7 +35,7 @@
|
||||
// Plugins
|
||||
"import/extensions": ["error", "always", { "ignorePackages": true, }],
|
||||
"import/no-unresolved": ["error", {
|
||||
"ignore": ["pdfjs", "pdfjs-lib", "pdfjs-web"]
|
||||
"ignore": ["pdfjs", "pdfjs-lib", "pdfjs-web", "pdfjs-fitCurve"]
|
||||
}],
|
||||
"mozilla/avoid-removeChild": "error",
|
||||
"mozilla/use-includes-instead-of-indexOf": "error",
|
||||
|
139
gulpfile.js
139
gulpfile.js
@ -231,11 +231,15 @@ function createWebpackConfig(
|
||||
);
|
||||
}
|
||||
|
||||
const experiments =
|
||||
output.library?.type === "module" ? { outputModule: true } : undefined;
|
||||
|
||||
// Required to expose e.g., the `window` object.
|
||||
output.globalObject = "globalThis";
|
||||
|
||||
return {
|
||||
mode: "none",
|
||||
experiments,
|
||||
output,
|
||||
performance: {
|
||||
hints: false, // Disable messages about larger file sizes.
|
||||
@ -246,6 +250,7 @@ function createWebpackConfig(
|
||||
pdfjs: path.join(__dirname, "src"),
|
||||
"pdfjs-web": path.join(__dirname, "web"),
|
||||
"pdfjs-lib": path.join(__dirname, "web/pdfjs"),
|
||||
"pdfjs-fitCurve": path.join(__dirname, "src/display/editor/fit_curve"),
|
||||
},
|
||||
},
|
||||
devtool: enableSourceMaps ? "source-map" : undefined,
|
||||
@ -511,6 +516,26 @@ 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",
|
||||
@ -1503,6 +1528,7 @@ function buildLibHelper(bundleDefines, inputStream, outputDir) {
|
||||
defines: bundleDefines,
|
||||
map: {
|
||||
"pdfjs-lib": "../pdf",
|
||||
"pdfjs-fitCurve": "./fit_curve",
|
||||
},
|
||||
};
|
||||
const licenseHeaderLibre = fs
|
||||
@ -1643,54 +1669,90 @@ 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", function runTest() {
|
||||
return streamqueue(
|
||||
{ objectMode: true },
|
||||
createTestSource("unit"),
|
||||
createTestSource("browser"),
|
||||
createTestSource("integration")
|
||||
);
|
||||
})
|
||||
gulp.series(
|
||||
setTestEnv,
|
||||
"generic",
|
||||
"components",
|
||||
"dev-fitCurve",
|
||||
function runTest() {
|
||||
return streamqueue(
|
||||
{ objectMode: true },
|
||||
createTestSource("unit"),
|
||||
createTestSource("browser"),
|
||||
createTestSource("integration")
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"bottest",
|
||||
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.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.task(
|
||||
"xfatest",
|
||||
gulp.series(setTestEnv, "generic", "components", function runXfaTest() {
|
||||
return streamqueue(
|
||||
{ objectMode: true },
|
||||
createTestSource("unit"),
|
||||
createTestSource("browser", { xfaOnly: true }),
|
||||
createTestSource("integration")
|
||||
);
|
||||
})
|
||||
gulp.series(
|
||||
setTestEnv,
|
||||
"generic",
|
||||
"components",
|
||||
"dev-fitCurve",
|
||||
function runXfaTest() {
|
||||
return streamqueue(
|
||||
{ objectMode: true },
|
||||
createTestSource("unit"),
|
||||
createTestSource("browser", { xfaOnly: true }),
|
||||
createTestSource("integration")
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"botxfatest",
|
||||
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.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.task(
|
||||
@ -1717,7 +1779,7 @@ gulp.task(
|
||||
|
||||
gulp.task(
|
||||
"unittest",
|
||||
gulp.series(setTestEnv, "generic", function runUnitTest() {
|
||||
gulp.series(setTestEnv, "generic", "dev-fitCurve", function runUnitTest() {
|
||||
return createTestSource("unit");
|
||||
})
|
||||
);
|
||||
@ -1975,6 +2037,13 @@ gulp.task(
|
||||
gulp.series("dev-css")
|
||||
);
|
||||
},
|
||||
function watchDevFitCurve() {
|
||||
gulp.watch(
|
||||
["src/display/editor/*"],
|
||||
{ ignoreInitial: false },
|
||||
gulp.series("dev-fitCurve")
|
||||
);
|
||||
},
|
||||
function watchDevSandbox() {
|
||||
gulp.watch(
|
||||
[
|
||||
|
35
package-lock.json
generated
35
package-lock.json
generated
@ -35,6 +35,7 @@
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-sort-exports": "^0.6.0",
|
||||
"eslint-plugin-unicorn": "^42.0.0",
|
||||
"fit-curve": "0.2.0",
|
||||
"globals": "^13.15.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-postcss": "^9.0.1",
|
||||
@ -6066,6 +6067,12 @@
|
||||
"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",
|
||||
@ -12112,7 +12119,6 @@
|
||||
},
|
||||
"node_modules/npm/node_modules/lodash._baseindexof": {
|
||||
"version": "3.1.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -12128,19 +12134,16 @@
|
||||
},
|
||||
"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": {
|
||||
@ -12155,7 +12158,6 @@
|
||||
},
|
||||
"node_modules/npm/node_modules/lodash._getnative": {
|
||||
"version": "3.9.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -12173,7 +12175,6 @@
|
||||
},
|
||||
"node_modules/npm/node_modules/lodash.restparam": {
|
||||
"version": "3.6.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -23258,6 +23259,12 @@
|
||||
"parse-filepath": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"flagged-respawn": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz",
|
||||
@ -27944,8 +27951,7 @@
|
||||
},
|
||||
"lodash._baseindexof": {
|
||||
"version": "3.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"bundled": true
|
||||
},
|
||||
"lodash._baseuniq": {
|
||||
"version": "4.6.0",
|
||||
@ -27958,18 +27964,15 @@
|
||||
},
|
||||
"lodash._bindcallback": {
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"bundled": true
|
||||
},
|
||||
"lodash._cacheindexof": {
|
||||
"version": "3.0.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"bundled": true
|
||||
},
|
||||
"lodash._createcache": {
|
||||
"version": "3.1.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash._getnative": "^3.0.0"
|
||||
}
|
||||
@ -27981,8 +27984,7 @@
|
||||
},
|
||||
"lodash._getnative": {
|
||||
"version": "3.9.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"bundled": true
|
||||
},
|
||||
"lodash._root": {
|
||||
"version": "3.0.1",
|
||||
@ -27996,8 +27998,7 @@
|
||||
},
|
||||
"lodash.restparam": {
|
||||
"version": "3.6.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"bundled": true
|
||||
},
|
||||
"lodash.union": {
|
||||
"version": "4.6.0",
|
||||
|
@ -28,6 +28,7 @@
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-sort-exports": "^0.6.0",
|
||||
"eslint-plugin-unicorn": "^42.0.0",
|
||||
"fit-curve": "^0.2.0",
|
||||
"globals": "^13.15.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-postcss": "^9.0.1",
|
||||
|
20
src/display/editor/fit_curve.js
Normal file
20
src/display/editor/fit_curve.js
Normal file
@ -0,0 +1,20 @@
|
||||
/* 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 };
|
@ -1,652 +0,0 @@
|
||||
/**
|
||||
* @preserve JavaScript implementation of
|
||||
* Algorithm for Automatically Fitting Digitized Curves
|
||||
* by Philip J. Schneider
|
||||
* "Graphics Gems", Academic Press, 1990
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* https://github.com/soswow/fit-curves
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fit one or more Bezier curves to a set of points.
|
||||
*
|
||||
* @param {Array<Array<Number>>} points - Array of digitized points,
|
||||
* e.g. [[5,5],[5,50],[110,140],[210,160],[320,110]]
|
||||
* @param {Number} maxError - Tolerance, squared error between points and
|
||||
* fitted curve
|
||||
* @returns {Array<Array<Array<Number>>>} Array of Bezier curves, where each
|
||||
* element is
|
||||
* [first-point, control-point-1, control-point-2, second-point]
|
||||
* and points are [x, y]
|
||||
*/
|
||||
function fitCurve(points, maxError, progressCallback) {
|
||||
if (!Array.isArray(points)) {
|
||||
throw new TypeError("First argument should be an array");
|
||||
}
|
||||
points.forEach(point => {
|
||||
if (
|
||||
!Array.isArray(point) ||
|
||||
point.some(item => typeof item !== "number") ||
|
||||
point.length !== points[0].length
|
||||
) {
|
||||
throw Error(
|
||||
"Each point should be an array of numbers. Each point should have the same amount of numbers."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove duplicate points
|
||||
points = points.filter(
|
||||
(point, i) => i === 0 || !point.every((val, j) => val === points[i - 1][j])
|
||||
);
|
||||
|
||||
if (points.length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const len = points.length;
|
||||
const leftTangent = createTangent(points[1], points[0]);
|
||||
const rightTangent = createTangent(points[len - 2], points[len - 1]);
|
||||
|
||||
return fitCubic(
|
||||
points,
|
||||
leftTangent,
|
||||
rightTangent,
|
||||
maxError,
|
||||
progressCallback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fit a Bezier curve to a (sub)set of digitized points.
|
||||
* Your code should not call this function directly.
|
||||
* Use {@link fitCurve} instead.
|
||||
*
|
||||
* @param {Array<Array<Number>>} points - Array of digitized points,
|
||||
* e.g. [[5,5],[5,50],[110,140],[210,160],[320,110]]
|
||||
* @param {Array<Number>} leftTangent - Unit tangent vector at start point
|
||||
* @param {Array<Number>} rightTangent - Unit tangent vector at end point
|
||||
* @param {Number} error - Tolerance, squared error between points and
|
||||
* fitted curve
|
||||
* @returns {Array<Array<Array<Number>>>} Array of Bezier curves, where
|
||||
* each element is
|
||||
* [first-point, control-point-1, control-point-2, second-point]
|
||||
* and points are [x, y]
|
||||
*/
|
||||
function fitCubic(points, leftTangent, rightTangent, error, progressCallback) {
|
||||
const MaxIterations = 20; // Max times to try iterating (to find an acceptable curve)
|
||||
|
||||
let bezCurve, // Control points of fitted Bezier curve
|
||||
uPrime, // Improved parameter values
|
||||
maxError,
|
||||
prevErr, // Maximum fitting error
|
||||
splitPoint,
|
||||
prevSplit, // Point to split point set at if we need more than one curve
|
||||
centerVector,
|
||||
beziers, // Array of fitted Bezier curves if we need more than one curve
|
||||
dist,
|
||||
i;
|
||||
|
||||
// Use heuristic if region only has two points in it
|
||||
if (points.length === 2) {
|
||||
dist = maths.vectorLen(maths.subtract(points[0], points[1])) / 3.0;
|
||||
bezCurve = [
|
||||
points[0],
|
||||
maths.addArrays(points[0], maths.mulItems(leftTangent, dist)),
|
||||
maths.addArrays(points[1], maths.mulItems(rightTangent, dist)),
|
||||
points[1],
|
||||
];
|
||||
return [bezCurve];
|
||||
}
|
||||
|
||||
// Parameterize points, and attempt to fit curve
|
||||
// Parameter values for point
|
||||
const u = chordLengthParameterize(points);
|
||||
[bezCurve, maxError, splitPoint] = generateAndReport(
|
||||
points,
|
||||
u,
|
||||
u,
|
||||
leftTangent,
|
||||
rightTangent,
|
||||
progressCallback
|
||||
);
|
||||
|
||||
if (maxError === 0 || maxError < error) {
|
||||
return [bezCurve];
|
||||
}
|
||||
// If error not too large, try some reparameterization and iteration
|
||||
if (maxError < error * error) {
|
||||
uPrime = u;
|
||||
prevErr = maxError;
|
||||
prevSplit = splitPoint;
|
||||
|
||||
for (i = 0; i < MaxIterations; i++) {
|
||||
uPrime = reparameterize(bezCurve, points, uPrime);
|
||||
[bezCurve, maxError, splitPoint] = generateAndReport(
|
||||
points,
|
||||
u,
|
||||
uPrime,
|
||||
leftTangent,
|
||||
rightTangent,
|
||||
progressCallback
|
||||
);
|
||||
|
||||
if (maxError < error) {
|
||||
return [bezCurve];
|
||||
}
|
||||
// If the development of the fitted curve grinds to a halt,
|
||||
// we abort this attempt (and try a shorter curve):
|
||||
else if (splitPoint === prevSplit) {
|
||||
const errChange = maxError / prevErr;
|
||||
if (errChange > 0.9999 && errChange < 1.0001) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
prevErr = maxError;
|
||||
prevSplit = splitPoint;
|
||||
}
|
||||
}
|
||||
|
||||
// Fitting failed -- split at max error point and fit recursively
|
||||
beziers = [];
|
||||
|
||||
// To create a smooth transition from one curve segment to the next, we
|
||||
// calculate the line between the points directly before and after the
|
||||
// center, and use that as the tangent both to and from the center point.
|
||||
centerVector = maths.subtract(points[splitPoint - 1], points[splitPoint + 1]);
|
||||
// However, this won't work if they're the same point, because the line we
|
||||
// want to use as a tangent would be 0. Instead, we calculate the line from
|
||||
// that "double-point" to the center point, and use its tangent.
|
||||
if (centerVector.every(val => val === 0)) {
|
||||
// [x,y] -> [-y,x]: http://stackoverflow.com/a/4780141/1869660
|
||||
centerVector = maths.subtract(points[splitPoint - 1], points[splitPoint]);
|
||||
[centerVector[0], centerVector[1]] = [-centerVector[1], centerVector[0]];
|
||||
}
|
||||
const toCenterTangent = maths.normalize(centerVector);
|
||||
// To and from need to point in opposite directions:
|
||||
// Unit tangent vector(s) at splitPoint
|
||||
const fromCenterTangent = maths.mulItems(toCenterTangent, -1);
|
||||
|
||||
/*
|
||||
Note:
|
||||
An alternative to this "divide and conquer" recursion could be to always
|
||||
let new curve segments start by trying to go all the way to the end,
|
||||
instead of only to the end of the current subdivided polyline.
|
||||
That might let many segments fit a few points more, reducing the number of
|
||||
total segments.
|
||||
|
||||
However, a few tests have shown that the segment reduction is insignificant
|
||||
(240 pts, 100 err: 25 curves vs 27 curves. 140 pts, 100 err: 17 curves
|
||||
on both), and the results take twice as many steps and milliseconds to
|
||||
finish, without looking any better than what we already have.
|
||||
*/
|
||||
beziers = beziers.concat(
|
||||
fitCubic(
|
||||
points.slice(0, splitPoint + 1),
|
||||
leftTangent,
|
||||
toCenterTangent,
|
||||
error,
|
||||
progressCallback
|
||||
)
|
||||
);
|
||||
beziers = beziers.concat(
|
||||
fitCubic(
|
||||
points.slice(splitPoint),
|
||||
fromCenterTangent,
|
||||
rightTangent,
|
||||
error,
|
||||
progressCallback
|
||||
)
|
||||
);
|
||||
return beziers;
|
||||
}
|
||||
|
||||
function generateAndReport(
|
||||
points,
|
||||
paramsOrig,
|
||||
paramsPrime,
|
||||
leftTangent,
|
||||
rightTangent,
|
||||
progressCallback
|
||||
) {
|
||||
const bezCurve = generateBezier(
|
||||
points,
|
||||
paramsPrime,
|
||||
leftTangent,
|
||||
rightTangent
|
||||
);
|
||||
// Find max deviation of points to fitted curve.
|
||||
// Here we always use the original parameters (from
|
||||
// chordLengthParameterize()), because we need to compare the current
|
||||
// curve to the actual source polyline, and not the currently iterated
|
||||
// parameters which reparameterize() & generateBezier() use, as those
|
||||
// have probably drifted far away and may no longer be in ascending order.
|
||||
const [maxError, splitPoint] = computeMaxError(points, bezCurve, paramsOrig);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback({
|
||||
bez: bezCurve,
|
||||
points,
|
||||
params: paramsOrig,
|
||||
maxErr: maxError,
|
||||
maxPoint: splitPoint,
|
||||
});
|
||||
}
|
||||
|
||||
return [bezCurve, maxError, splitPoint];
|
||||
}
|
||||
|
||||
/**
|
||||
* Use least-squares method to find Bezier control points for region.
|
||||
*
|
||||
* @param {Array<Array<Number>>} points - Array of digitized points
|
||||
* @param {Array<Number>} parameters - Parameter values for region
|
||||
* @param {Array<Number>} leftTangent - Unit tangent vector at start point
|
||||
* @param {Array<Number>} rightTangent - Unit tangent vector at end point
|
||||
* @returns {Array<Array<Number>>} Approximated Bezier curve:
|
||||
* [first-point, control-point-1, control-point-2, second-point]
|
||||
* where points are [x, y]
|
||||
*/
|
||||
function generateBezier(points, parameters, leftTangent, rightTangent) {
|
||||
let a, // Precomputed rhs for eqn
|
||||
tmp,
|
||||
u,
|
||||
ux;
|
||||
|
||||
const firstPoint = points[0];
|
||||
const lastPoint = points.at(-1);
|
||||
|
||||
// Bezier curve ctl pts
|
||||
const bezCurve = [firstPoint, null, null, lastPoint];
|
||||
|
||||
// Compute the A's
|
||||
const A = maths.zeros_Xx2x2(parameters.length);
|
||||
for (let i = 0, len = parameters.length; i < len; i++) {
|
||||
u = parameters[i];
|
||||
ux = 1 - u;
|
||||
a = A[i];
|
||||
|
||||
a[0] = maths.mulItems(leftTangent, 3 * u * (ux * ux));
|
||||
a[1] = maths.mulItems(rightTangent, 3 * ux * (u * u));
|
||||
}
|
||||
|
||||
// Create the C and X matrices
|
||||
const C = [
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
];
|
||||
const X = [0, 0];
|
||||
for (let i = 0, len = points.length; i < len; i++) {
|
||||
u = parameters[i];
|
||||
a = A[i];
|
||||
|
||||
C[0][0] += maths.dot(a[0], a[0]);
|
||||
C[0][1] += maths.dot(a[0], a[1]);
|
||||
C[1][0] += maths.dot(a[0], a[1]);
|
||||
C[1][1] += maths.dot(a[1], a[1]);
|
||||
|
||||
tmp = maths.subtract(
|
||||
points[i],
|
||||
bezier.q([firstPoint, firstPoint, lastPoint, lastPoint], u)
|
||||
);
|
||||
|
||||
X[0] += maths.dot(a[0], tmp);
|
||||
X[1] += maths.dot(a[1], tmp);
|
||||
}
|
||||
|
||||
// Compute the determinants of C and X
|
||||
const det_C0_C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1];
|
||||
const det_C0_X = C[0][0] * X[1] - C[1][0] * X[0];
|
||||
const det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1];
|
||||
|
||||
// Finally, derive alpha values
|
||||
const alpha_l = det_C0_C1 === 0 ? 0 : det_X_C1 / det_C0_C1;
|
||||
const alpha_r = det_C0_C1 === 0 ? 0 : det_C0_X / det_C0_C1;
|
||||
|
||||
// If alpha negative, use the Wu/Barsky heuristic (see text).
|
||||
// If alpha is 0, you get coincident control points that lead to
|
||||
// divide by zero in any subsequent NewtonRaphsonRootFind() call.
|
||||
const segLength = maths.vectorLen(maths.subtract(firstPoint, lastPoint));
|
||||
const epsilon = 1.0e-6 * segLength;
|
||||
if (alpha_l < epsilon || alpha_r < epsilon) {
|
||||
// Fall back on standard (probably inaccurate) formula, and subdivide
|
||||
// further if needed.
|
||||
bezCurve[1] = maths.addArrays(
|
||||
firstPoint,
|
||||
maths.mulItems(leftTangent, segLength / 3.0)
|
||||
);
|
||||
bezCurve[2] = maths.addArrays(
|
||||
lastPoint,
|
||||
maths.mulItems(rightTangent, segLength / 3.0)
|
||||
);
|
||||
} else {
|
||||
// First and last control points of the Bezier curve are
|
||||
// positioned exactly at the first and last data points
|
||||
// Control points 1 and 2 are positioned an alpha distance out
|
||||
// on the tangent vectors, left and right, respectively
|
||||
bezCurve[1] = maths.addArrays(
|
||||
firstPoint,
|
||||
maths.mulItems(leftTangent, alpha_l)
|
||||
);
|
||||
bezCurve[2] = maths.addArrays(
|
||||
lastPoint,
|
||||
maths.mulItems(rightTangent, alpha_r)
|
||||
);
|
||||
}
|
||||
|
||||
return bezCurve;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given set of points and their parameterization, try to find a better
|
||||
* parameterization.
|
||||
*
|
||||
* @param {Array<Array<Number>>} bezier - Current fitted curve
|
||||
* @param {Array<Array<Number>>} points - Array of digitized points
|
||||
* @param {Array<Number>} parameters - Current parameter values
|
||||
* @returns {Array<Number>} New parameter values
|
||||
*/
|
||||
function reparameterize(bezier, points, parameters) {
|
||||
return parameters.map((p, i) => newtonRaphsonRootFind(bezier, points[i], p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Use Newton-Raphson iteration to find better root.
|
||||
*
|
||||
* @param {Array<Array<Number>>} bez - Current fitted curve
|
||||
* @param {Array<Number>} point - Digitized point
|
||||
* @param {Number} u - Parameter value for "P"
|
||||
* @returns {Number} New u
|
||||
*/
|
||||
function newtonRaphsonRootFind(bez, point, u) {
|
||||
/*
|
||||
Newton's root finding algorithm calculates f(x)=0 by reiterating
|
||||
x_n+1 = x_n - f(x_n)/f'(x_n)
|
||||
We are trying to find curve parameter u for some point p that minimizes
|
||||
the distance from that point to the curve. Distance point to curve
|
||||
is d=q(u)-p.
|
||||
At minimum distance the point is perpendicular to the curve.
|
||||
We are solving
|
||||
f = q(u)-p * q'(u) = 0
|
||||
with
|
||||
f' = q'(u) * q'(u) + q(u)-p * q''(u)
|
||||
gives
|
||||
u_n+1 = u_n - |q(u_n)-p * q'(u_n)| / |q'(u_n)**2 + q(u_n)-p * q''(u_n)|
|
||||
*/
|
||||
|
||||
const d = maths.subtract(bezier.q(bez, u), point),
|
||||
qprime = bezier.qprime(bez, u),
|
||||
numerator = maths.mulMatrix(d, qprime),
|
||||
denominator =
|
||||
maths.sum(maths.squareItems(qprime)) +
|
||||
2 * maths.mulMatrix(d, bezier.qprimeprime(bez, u));
|
||||
|
||||
if (denominator === 0) {
|
||||
return u;
|
||||
}
|
||||
return u - numerator / denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign parameter values to digitized points using relative distances
|
||||
* between points.
|
||||
*
|
||||
* @param {Array<Array<Number>>} points - Array of digitized points
|
||||
* @returns {Array<Number>} Parameter values
|
||||
*/
|
||||
function chordLengthParameterize(points) {
|
||||
let u = [],
|
||||
currU,
|
||||
prevU,
|
||||
prevP;
|
||||
|
||||
points.forEach((p, i) => {
|
||||
currU = i ? prevU + maths.vectorLen(maths.subtract(p, prevP)) : 0;
|
||||
u.push(currU);
|
||||
|
||||
prevU = currU;
|
||||
prevP = p;
|
||||
});
|
||||
u = u.map(x => x / prevU);
|
||||
|
||||
return u;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the maximum squared distance of digitized points to fitted curve.
|
||||
*
|
||||
* @param {Array<Array<Number>>} points - Array of digitized points
|
||||
* @param {Array<Array<Number>>} bez - Fitted curve
|
||||
* @param {Array<Number>} parameters - Parameterization of points
|
||||
* @returns {Array<Number>} Maximum error (squared) and point of max error
|
||||
*/
|
||||
function computeMaxError(points, bez, parameters) {
|
||||
let dist, // Current error
|
||||
maxDist, // Maximum error
|
||||
splitPoint, // Point of maximum error
|
||||
v, // Vector from point to curve
|
||||
i,
|
||||
count,
|
||||
point,
|
||||
t;
|
||||
|
||||
maxDist = 0;
|
||||
splitPoint = Math.floor(points.length / 2);
|
||||
|
||||
const t_distMap = mapTtoRelativeDistances(bez, 10);
|
||||
|
||||
for (i = 0, count = points.length; i < count; i++) {
|
||||
point = points[i];
|
||||
// Find 't' for a point on the bez curve that's as close to 'point'
|
||||
// as possible:
|
||||
t = find_t(bez, parameters[i], t_distMap, 10);
|
||||
|
||||
v = maths.subtract(bezier.q(bez, t), point);
|
||||
dist = v[0] * v[0] + v[1] * v[1];
|
||||
|
||||
if (dist > maxDist) {
|
||||
maxDist = dist;
|
||||
splitPoint = i;
|
||||
}
|
||||
}
|
||||
|
||||
return [maxDist, splitPoint];
|
||||
}
|
||||
|
||||
// Sample 't's and map them to relative distances along the curve:
|
||||
function mapTtoRelativeDistances(bez, B_parts) {
|
||||
let B_t_curr;
|
||||
let B_t_dist = [0];
|
||||
let B_t_prev = bez[0];
|
||||
let sumLen = 0;
|
||||
|
||||
for (let i = 1; i <= B_parts; i++) {
|
||||
B_t_curr = bezier.q(bez, i / B_parts);
|
||||
|
||||
sumLen += maths.vectorLen(maths.subtract(B_t_curr, B_t_prev));
|
||||
|
||||
B_t_dist.push(sumLen);
|
||||
B_t_prev = B_t_curr;
|
||||
}
|
||||
|
||||
// Normalize B_length to the same interval as the parameter distances; 0 to 1:
|
||||
B_t_dist = B_t_dist.map(x => x / sumLen);
|
||||
return B_t_dist;
|
||||
}
|
||||
|
||||
function find_t(bez, param, t_distMap, B_parts) {
|
||||
if (param < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (param > 1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
'param' is a value between 0 and 1 telling us the relative position
|
||||
of a point on the source polyline (linearly from the start (0) to the
|
||||
end (1)).
|
||||
To see if a given curve - 'bez' - is a close approximation of the polyline,
|
||||
we compare such a poly-point to the point on the curve that's the same
|
||||
relative distance along the curve's length.
|
||||
|
||||
But finding that curve-point takes a little work:
|
||||
There is a function "B(t)" to find points along a curve from the parametric
|
||||
parameter 't' (also relative from 0 to 1: http://stackoverflow.com/a/32841764/1869660
|
||||
http://pomax.github.io/bezierinfo/#explanation),
|
||||
but 't' isn't linear by length (http://gamedev.stackexchange.com/questions/105230).
|
||||
|
||||
So, we sample some points along the curve using a handful of values for 't'.
|
||||
Then, we calculate the length between those samples via plain euclidean
|
||||
distance; B(t) concentrates the points around sharp turns, so this should
|
||||
give us a good-enough outline of the curve. Thus, for a given relative
|
||||
distance ('param'), we can now find an upper and lower value for the
|
||||
corresponding 't' by searching through those sampled distances. Finally, we
|
||||
just use linear interpolation to find a better value for the exact 't'.
|
||||
|
||||
More info:
|
||||
http://gamedev.stackexchange.com/questions/105230/points-evenly-spaced-along-a-bezier-curve
|
||||
http://stackoverflow.com/questions/29438398/cheap-way-of-calculating-cubic-bezier-length
|
||||
http://steve.hollasch.net/cgindex/curves/cbezarclen.html
|
||||
https://github.com/retuxx/tinyspline
|
||||
*/
|
||||
let lenMax, lenMin, tMax, tMin, t;
|
||||
|
||||
// Find the two t-s that the current param distance lies between,
|
||||
// and then interpolate a somewhat accurate value for the exact t:
|
||||
for (let i = 1; i <= B_parts; i++) {
|
||||
if (param <= t_distMap[i]) {
|
||||
tMin = (i - 1) / B_parts;
|
||||
tMax = i / B_parts;
|
||||
lenMin = t_distMap[i - 1];
|
||||
lenMax = t_distMap[i];
|
||||
|
||||
t = ((param - lenMin) / (lenMax - lenMin)) * (tMax - tMin) + tMin;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a vector of length 1 which shows the direction from B to A
|
||||
*/
|
||||
function createTangent(pointA, pointB) {
|
||||
return maths.normalize(maths.subtract(pointA, pointB));
|
||||
}
|
||||
|
||||
/*
|
||||
Simplified versions of what we need from math.js
|
||||
Optimized for our input, which is only numbers and 1x2 arrays
|
||||
(i.e. [x, y] coordinates).
|
||||
*/
|
||||
class maths {
|
||||
static zeros_Xx2x2(x) {
|
||||
const zs = [];
|
||||
while (x--) {
|
||||
zs.push([0, 0]);
|
||||
}
|
||||
return zs;
|
||||
}
|
||||
|
||||
static mulItems(items, multiplier) {
|
||||
return items.map(x => x * multiplier);
|
||||
}
|
||||
|
||||
static mulMatrix(m1, m2) {
|
||||
// https://en.wikipedia.org/wiki/Matrix_multiplication#Matrix_product_.28two_matrices.29
|
||||
// Simplified to only handle 1-dimensional matrices (i.e. arrays)
|
||||
// of equal length:
|
||||
return m1.reduce((sum, x1, i) => sum + x1 * m2[i], 0);
|
||||
}
|
||||
|
||||
// Only used to subract to points (or at least arrays):
|
||||
static subtract(arr1, arr2) {
|
||||
return arr1.map((x1, i) => x1 - arr2[i]);
|
||||
}
|
||||
|
||||
static addArrays(arr1, arr2) {
|
||||
return arr1.map((x1, i) => x1 + arr2[i]);
|
||||
}
|
||||
|
||||
static addItems(items, addition) {
|
||||
return items.map(x => x + addition);
|
||||
}
|
||||
|
||||
static sum(items) {
|
||||
return items.reduce((sum, x) => sum + x);
|
||||
}
|
||||
|
||||
// Only used on two arrays. The dot product is equal to the matrix product
|
||||
// in this case:
|
||||
static dot(m1, m2) {
|
||||
return maths.mulMatrix(m1, m2);
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/Norm_(mathematics)#Euclidean_norm
|
||||
// var norm = logAndRun(math.norm);
|
||||
static vectorLen(v) {
|
||||
return Math.hypot(...v);
|
||||
}
|
||||
|
||||
static divItems(items, divisor) {
|
||||
return items.map(x => x / divisor);
|
||||
}
|
||||
|
||||
static squareItems(items) {
|
||||
return items.map(x => x * x);
|
||||
}
|
||||
|
||||
static normalize(v) {
|
||||
return this.divItems(v, this.vectorLen(v));
|
||||
}
|
||||
}
|
||||
|
||||
class bezier {
|
||||
// Evaluates cubic bezier at t, return point
|
||||
static q(ctrlPoly, t) {
|
||||
const tx = 1.0 - t;
|
||||
const pA = maths.mulItems(ctrlPoly[0], tx * tx * tx),
|
||||
pB = maths.mulItems(ctrlPoly[1], 3 * tx * tx * t),
|
||||
pC = maths.mulItems(ctrlPoly[2], 3 * tx * t * t),
|
||||
pD = maths.mulItems(ctrlPoly[3], t * t * t);
|
||||
return maths.addArrays(maths.addArrays(pA, pB), maths.addArrays(pC, pD));
|
||||
}
|
||||
|
||||
// Evaluates cubic bezier first derivative at t, return point
|
||||
static qprime(ctrlPoly, t) {
|
||||
const tx = 1.0 - t;
|
||||
const pA = maths.mulItems(
|
||||
maths.subtract(ctrlPoly[1], ctrlPoly[0]),
|
||||
3 * tx * tx
|
||||
),
|
||||
pB = maths.mulItems(maths.subtract(ctrlPoly[2], ctrlPoly[1]), 6 * tx * t),
|
||||
pC = maths.mulItems(maths.subtract(ctrlPoly[3], ctrlPoly[2]), 3 * t * t);
|
||||
return maths.addArrays(maths.addArrays(pA, pB), pC);
|
||||
}
|
||||
|
||||
// Evaluates cubic bezier second derivative at t, return point
|
||||
static qprimeprime(ctrlPoly, t) {
|
||||
return maths.addArrays(
|
||||
maths.mulItems(
|
||||
maths.addArrays(
|
||||
maths.subtract(ctrlPoly[2], maths.mulItems(ctrlPoly[1], 2)),
|
||||
ctrlPoly[0]
|
||||
),
|
||||
6 * (1.0 - t)
|
||||
),
|
||||
maths.mulItems(
|
||||
maths.addArrays(
|
||||
maths.subtract(ctrlPoly[3], maths.mulItems(ctrlPoly[2], 2)),
|
||||
ctrlPoly[1]
|
||||
),
|
||||
6 * t
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { fitCurve };
|
@ -19,7 +19,7 @@ import {
|
||||
Util,
|
||||
} from "../../shared/util.js";
|
||||
import { AnnotationEditor } from "./editor.js";
|
||||
import { fitCurve } from "./fit_curve/fit_curve.js";
|
||||
import { fitCurve } from "pdfjs-fitCurve";
|
||||
|
||||
/**
|
||||
* Basic draw editor in order to generate an Ink annotation.
|
||||
@ -880,4 +880,4 @@ class InkEditor extends AnnotationEditor {
|
||||
}
|
||||
}
|
||||
|
||||
export { InkEditor };
|
||||
export { fitCurve, InkEditor };
|
||||
|
@ -21,6 +21,7 @@
|
||||
"display_svg_spec.js",
|
||||
"display_utils_spec.js",
|
||||
"document_spec.js",
|
||||
"editor_spec.js",
|
||||
"encodings_spec.js",
|
||||
"evaluator_spec.js",
|
||||
"event_utils_spec.js",
|
||||
|
43
test/unit/editor_spec.js
Normal file
43
test/unit/editor_spec.js
Normal file
@ -0,0 +1,43 @@
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
import { fitCurve } from "../../src/display/editor/ink.js";
|
||||
|
||||
describe("editor", function () {
|
||||
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],
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@ -66,6 +66,7 @@ async function initializePDFJS(callback) {
|
||||
"pdfjs-test/unit/display_svg_spec.js",
|
||||
"pdfjs-test/unit/display_utils_spec.js",
|
||||
"pdfjs-test/unit/document_spec.js",
|
||||
"pdfjs-test/unit/editor_spec.js",
|
||||
"pdfjs-test/unit/encodings_spec.js",
|
||||
"pdfjs-test/unit/evaluator_spec.js",
|
||||
"pdfjs-test/unit/event_utils_spec.js",
|
||||
|
@ -15,6 +15,7 @@
|
||||
"pdfjs/": "../../src/",
|
||||
"pdfjs-lib": "../../src/pdf.js",
|
||||
"pdfjs-web/": "../../web/",
|
||||
"pdfjs-fitCurve": "../../build/dev-fitCurve/fit_curve.js",
|
||||
"pdfjs-test/": "../"
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,8 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
"imports": {
|
||||
"pdfjs/": "../src/",
|
||||
"pdfjs-lib": "../src/pdf.js",
|
||||
"pdfjs-web/": "./"
|
||||
"pdfjs-web/": "./",
|
||||
"pdfjs-fitCurve": "../build/dev-fitCurve/fit_curve.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
Loading…
Reference in New Issue
Block a user