Compare commits

...

7 Commits

Author SHA1 Message Date
Jonas Jenwald
e5fbf52405
Merge pull request #19736 from Snuffleupagus/compileType3Glyph-worker
[api-minor] Move Type3-glyph compilation to the worker-thread
2025-04-01 19:40:30 +02:00
Jonas Jenwald
b33522a208
Merge pull request #19746 from Snuffleupagus/evaluator-img-cache-tweaks
Reduce some code duplication when handling globally cached images
2025-04-01 19:15:10 +02:00
calixteman
25693dc0ee
Merge pull request #19712 from calixteman/optimize_save_construct
Optimize save-transform-constructPath-restore
2025-04-01 18:20:59 +02:00
Jonas Jenwald
9cd5a9658a [api-minor] Move Type3-glyph compilation to the worker-thread
After PR 19731 the format of compiled Type3-glyphs is now simple enough that the compilation can be moved to the worker-thread, without introducing any significant additional complexity.
This allows us to, ever so slightly, simplify the implementation in `src/display/canvas.js` since the Type3 operatorLists will now directly include standard path-rendering operators (using the format introduced in PR 19689).

As part of these changes we also stop caching Type3 image masks since: we've not come across any cases where that actually helps, they're usually fairly small, and it simplifies the code.

Note that one "negative" change introduced in this patch is that we'll now compile Type3-glyphs *eagerly*, whereas previously we'd only do that lazily upon their first use.
However, this doesn't seem to impact performance in any noticeable way since the compilation is fast enough (way below 1 ms/glyph in my testing) and Type3-fonts are also limited to just 256 glyphs. Also, many (or most?) Type3-fonts don't even use image masks and are thus not affected by these changes.
2025-04-01 09:09:00 +02:00
Jonas Jenwald
213830f44f Use, and re-name, the addLocallyCachedImageOps helper for global images too
This avoids having to "manually" set the image operators for globally cached images.
2025-03-31 10:57:04 +02:00
Jonas Jenwald
e0e59eaf01 Define the global cache-data once in buildPaintImageXObject
Currently we duplicate the same identical code three times, which seems both unnecessary and error prone.
2025-03-31 10:29:29 +02:00
Calixte Denizet
6146e5fee7 Optimize save-transform-constructPath-restore
The 4 operations can be replaced with just one in applying the transform to the points coordinates.
2025-03-25 15:31:45 +01:00
5 changed files with 367 additions and 256 deletions

View File

@ -32,6 +32,7 @@ import {
} from "../shared/util.js";
import { CMapFactory, IdentityCMap } from "./cmap.js";
import { Cmd, Dict, EOF, isName, Name, Ref, RefSet } from "./primitives.js";
import { compileType3Glyph, FontFlags } from "./fonts_utils.js";
import { ErrorFont, Font } from "./fonts.js";
import {
fetchBinaryData,
@ -72,7 +73,6 @@ import { bidi } from "./bidi.js";
import { ColorSpace } from "./colorspace.js";
import { ColorSpaceUtils } from "./colorspace_utils.js";
import { DecodeStream } from "./decode_stream.js";
import { FontFlags } from "./fonts_utils.js";
import { getFontSubstitution } from "./font_substitutions.js";
import { getGlyphsUnicode } from "./glyphlist.js";
import { getMetrics } from "./metrics.js";
@ -180,14 +180,17 @@ function normalizeBlendMode(value, parsingArray = false) {
return "source-over";
}
function addLocallyCachedImageOps(opList, data) {
if (data.objId) {
opList.addDependency(data.objId);
function addCachedImageOps(
opList,
{ objId, fn, args, optionalContent, hasMask }
) {
if (objId) {
opList.addDependency(objId);
}
opList.addImageOps(data.fn, data.args, data.optionalContent, data.hasMask);
opList.addImageOps(fn, args, optionalContent, hasMask);
if (data.fn === OPS.paintImageMaskXObject && data.args[0]?.count > 0) {
data.args[0].count++;
if (fn === OPS.paintImageMaskXObject && args[0]?.count > 0) {
args[0].count++;
}
}
@ -608,6 +611,12 @@ class PartialEvaluator {
const decode = dict.getArray("D", "Decode");
if (this.parsingType3Font) {
// NOTE: Compared to other image resources we don't bother caching
// Type3-glyph image masks, since we've not come across any cases
// where that actually helps.
// In Type3-glyphs image masks are "always" inline resources,
// they're usually fairly small and aren't being re-used either.
imgData = PDFImage.createRawMask({
imgArray,
width: w,
@ -616,25 +625,21 @@ class PartialEvaluator {
inverseDecode: decode?.[0] > 0,
interpolate,
});
args = compileType3Glyph(imgData);
imgData.cached = !!cacheKey;
fn = OPS.paintImageMaskXObject;
args = [imgData];
operatorList.addImageOps(fn, args, optionalContent);
if (cacheKey) {
const cacheData = { fn, args, optionalContent };
localImageCache.set(cacheKey, imageRef, cacheData);
if (imageRef) {
this._regionalImageCache.set(
/* name = */ null,
imageRef,
cacheData
);
}
if (args) {
operatorList.addImageOps(OPS.constructPath, args, optionalContent);
return;
}
warn("Cannot compile Type3 glyph.");
// If compilation failed, or was disabled, fallback to using an inline
// image mask; this case should be extremely rare.
operatorList.addImageOps(
OPS.paintImageMaskXObject,
[imgData],
optionalContent
);
return;
}
@ -742,7 +747,8 @@ class PartialEvaluator {
// If there is no imageMask, create the PDFImage and a lot
// of image processing can be done here.
let objId = `img_${this.idFactory.createObjId()}`,
cacheGlobally = false;
cacheGlobally = false,
globalCacheData = null;
if (this.parsingType3Font) {
objId = `${this.idFactory.getDocId()}_type3_${objId}`;
@ -767,15 +773,17 @@ class PartialEvaluator {
operatorList.addImageOps(fn, args, optionalContent, hasMask);
if (cacheGlobally) {
globalCacheData = {
objId,
fn,
args,
optionalContent,
hasMask,
byteSize: 0, // Temporary entry, to avoid `setData` returning early.
};
if (this.globalImageCache.hasDecodeFailed(imageRef)) {
this.globalImageCache.setData(imageRef, {
objId,
fn,
args,
optionalContent,
hasMask,
byteSize: 0, // Data is `null`, since decoding failed previously.
});
this.globalImageCache.setData(imageRef, globalCacheData);
this._sendImgData(objId, /* imgData = */ null, cacheGlobally);
return;
@ -792,14 +800,7 @@ class PartialEvaluator {
]);
if (localLength) {
this.globalImageCache.setData(imageRef, {
objId,
fn,
args,
optionalContent,
hasMask,
byteSize: 0, // Temporary entry, to avoid `setData` returning early.
});
this.globalImageCache.setData(imageRef, globalCacheData);
this.globalImageCache.addByteSize(imageRef, localLength);
return;
}
@ -848,14 +849,8 @@ class PartialEvaluator {
this._regionalImageCache.set(/* name = */ null, imageRef, cacheData);
if (cacheGlobally) {
this.globalImageCache.setData(imageRef, {
objId,
fn,
args,
optionalContent,
hasMask,
byteSize: 0, // Temporary entry, note `addByteSize` above.
});
assert(globalCacheData, "The global cache-data must be available.");
this.globalImageCache.setData(imageRef, globalCacheData);
}
}
}
@ -1779,7 +1774,7 @@ class PartialEvaluator {
if (isValidName) {
const localImage = localImageCache.getByName(name);
if (localImage) {
addLocallyCachedImageOps(operatorList, localImage);
addCachedImageOps(operatorList, localImage);
args = null;
continue;
}
@ -1793,28 +1788,12 @@ class PartialEvaluator {
let xobj = xobjs.getRaw(name);
if (xobj instanceof Ref) {
const localImage =
const cachedImage =
localImageCache.getByRef(xobj) ||
self._regionalImageCache.getByRef(xobj);
if (localImage) {
addLocallyCachedImageOps(operatorList, localImage);
resolveXObject();
return;
}
const globalImage = self.globalImageCache.getData(
xobj,
self.pageIndex
);
if (globalImage) {
operatorList.addDependency(globalImage.objId);
operatorList.addImageOps(
globalImage.fn,
globalImage.args,
globalImage.optionalContent,
globalImage.hasMask
);
self._regionalImageCache.getByRef(xobj) ||
self.globalImageCache.getData(xobj, self.pageIndex);
if (cachedImage) {
addCachedImageOps(operatorList, cachedImage);
resolveXObject();
return;
}
@ -1907,7 +1886,7 @@ class PartialEvaluator {
if (cacheKey) {
const localImage = localImageCache.getByName(cacheKey);
if (localImage) {
addLocallyCachedImageOps(operatorList, localImage);
addCachedImageOps(operatorList, localImage);
args = null;
continue;
}

View File

@ -13,11 +13,11 @@
* limitations under the License.
*/
import { DrawOPS, info, OPS } from "../shared/util.js";
import { getEncoding, StandardEncoding } from "./encodings.js";
import { getGlyphsUnicode } from "./glyphlist.js";
import { getLookupTableFactory } from "./core_utils.js";
import { getUnicodeForGlyph } from "./unicode.js";
import { info } from "../shared/util.js";
// Accented characters have issues on Windows and Linux. When this flag is
// enabled glyphs that use seac and seac style endchar operators are truncated
@ -207,7 +207,177 @@ const getVerticalPresentationForm = getLookupTableFactory(t => {
t[0xff5d] = 0xfe38; // FULLWIDTH RIGHT CURLY BRACKET
});
// To disable Type3 compilation, set the value to `-1`.
const MAX_SIZE_TO_COMPILE = 1000;
function compileType3Glyph({ data: img, width, height }) {
if (width > MAX_SIZE_TO_COMPILE || height > MAX_SIZE_TO_COMPILE) {
return null;
}
const POINT_TO_PROCESS_LIMIT = 1000;
const POINT_TYPES = new Uint8Array([
0, 2, 4, 0, 1, 0, 5, 4, 8, 10, 0, 8, 0, 2, 1, 0,
]);
const width1 = width + 1;
const points = new Uint8Array(width1 * (height + 1));
let i, j, j0;
// decodes bit-packed mask data
const lineSize = (width + 7) & ~7;
const data = new Uint8Array(lineSize * height);
let pos = 0;
for (const elem of img) {
let mask = 128;
while (mask > 0) {
data[pos++] = elem & mask ? 0 : 255;
mask >>= 1;
}
}
// finding interesting points: every point is located between mask pixels,
// so there will be points of the (width + 1)x(height + 1) grid. Every point
// will have flags assigned based on neighboring mask pixels:
// 4 | 8
// --P--
// 2 | 1
// We are interested only in points with the flags:
// - outside corners: 1, 2, 4, 8;
// - inside corners: 7, 11, 13, 14;
// - and, intersections: 5, 10.
let count = 0;
pos = 0;
if (data[pos] !== 0) {
points[0] = 1;
++count;
}
for (j = 1; j < width; j++) {
if (data[pos] !== data[pos + 1]) {
points[j] = data[pos] ? 2 : 1;
++count;
}
pos++;
}
if (data[pos] !== 0) {
points[j] = 2;
++count;
}
for (i = 1; i < height; i++) {
pos = i * lineSize;
j0 = i * width1;
if (data[pos - lineSize] !== data[pos]) {
points[j0] = data[pos] ? 1 : 8;
++count;
}
// 'sum' is the position of the current pixel configuration in the 'TYPES'
// array (in order 8-1-2-4, so we can use '>>2' to shift the column).
let sum = (data[pos] ? 4 : 0) + (data[pos - lineSize] ? 8 : 0);
for (j = 1; j < width; j++) {
sum =
(sum >> 2) +
(data[pos + 1] ? 4 : 0) +
(data[pos - lineSize + 1] ? 8 : 0);
if (POINT_TYPES[sum]) {
points[j0 + j] = POINT_TYPES[sum];
++count;
}
pos++;
}
if (data[pos - lineSize] !== data[pos]) {
points[j0 + j] = data[pos] ? 2 : 4;
++count;
}
if (count > POINT_TO_PROCESS_LIMIT) {
return null;
}
}
pos = lineSize * (height - 1);
j0 = i * width1;
if (data[pos] !== 0) {
points[j0] = 8;
++count;
}
for (j = 1; j < width; j++) {
if (data[pos] !== data[pos + 1]) {
points[j0 + j] = data[pos] ? 4 : 8;
++count;
}
pos++;
}
if (data[pos] !== 0) {
points[j0 + j] = 4;
++count;
}
if (count > POINT_TO_PROCESS_LIMIT) {
return null;
}
// building outlines
const steps = new Int32Array([0, width1, -1, 0, -width1, 0, 0, 0, 1]);
const pathBuf = [];
// the path shall be painted in [0..1]x[0..1] space
const { a, b, c, d, e, f } = new DOMMatrix()
.scaleSelf(1 / width, -1 / height)
.translateSelf(0, -height);
for (i = 0; count && i <= height; i++) {
let p = i * width1;
const end = p + width;
while (p < end && !points[p]) {
p++;
}
if (p === end) {
continue;
}
let x = p % width1;
let y = i;
pathBuf.push(DrawOPS.moveTo, a * x + c * y + e, b * x + d * y + f);
const p0 = p;
let type = points[p];
do {
const step = steps[type];
do {
p += step;
} while (!points[p]);
const pp = points[p];
if (pp !== 5 && pp !== 10) {
// set new direction
type = pp;
// delete mark
points[p] = 0;
} else {
// type is 5 or 10, ie, a crossing
// set new direction
type = pp & ((0x33 * type) >> 4);
// set new type for "future hit"
points[p] &= (type >> 2) | (type << 2);
}
x = p % width1;
y = (p / width1) | 0;
pathBuf.push(DrawOPS.lineTo, a * x + c * y + e, b * x + d * y + f);
if (!points[p]) {
--count;
}
} while (p0 !== p);
--i;
}
return [
OPS.rawFillPath,
[new Float32Array(pathBuf)],
new Float32Array([0, 0, width, height]),
];
}
export {
compileType3Glyph,
FontFlags,
getVerticalPresentationForm,
MacStandardGlyphOrdering,

View File

@ -13,7 +13,14 @@
* limitations under the License.
*/
import { ImageKind, OPS, RenderingIntentFlag, warn } from "../shared/util.js";
import {
DrawOPS,
ImageKind,
OPS,
RenderingIntentFlag,
Util,
warn,
} from "../shared/util.js";
function addState(parentState, pattern, checkFn, iterateFn, processFn) {
let state = parentState;
@ -470,6 +477,70 @@ addState(
}
);
// This replaces (save, transform, constructPath, restore)
// sequences with |constructPath| operation.
addState(
InitialState,
[OPS.save, OPS.transform, OPS.constructPath, OPS.restore],
context => {
const argsArray = context.argsArray;
const iFirstConstructPath = context.iCurr - 1;
const op = argsArray[iFirstConstructPath][0];
// When stroking the transform has to be applied to the line width too.
// So we can only optimize if the transform is an identity.
if (
op !== OPS.stroke &&
op !== OPS.closeStroke &&
op !== OPS.fillStroke &&
op !== OPS.eoFillStroke &&
op !== OPS.closeFillStroke &&
op !== OPS.closeEOFillStroke
) {
return true;
}
const iFirstTransform = context.iCurr - 2;
const transform = argsArray[iFirstTransform];
return (
transform[0] === 1 &&
transform[1] === 0 &&
transform[2] === 0 &&
transform[3] === 1
);
},
() => false,
(context, i) => {
const { fnArray, argsArray } = context;
const curr = context.iCurr;
const iFirstSave = curr - 3;
const iFirstTransform = curr - 2;
const iFirstConstructPath = curr - 1;
const args = argsArray[iFirstConstructPath];
const transform = argsArray[iFirstTransform];
const [, [buffer], minMax] = args;
Util.scaleMinMax(transform, minMax);
for (let k = 0, kk = buffer.length; k < kk; ) {
switch (buffer[k++]) {
case DrawOPS.moveTo:
case DrawOPS.lineTo:
Util.applyTransformInPlace(buffer.subarray(k), transform);
k += 2;
break;
case DrawOPS.curveTo:
Util.applyTransformToBezierInPlace(buffer.subarray(k), transform);
k += 6;
break;
}
}
// Replace queue items.
fnArray.splice(iFirstSave, 4, OPS.constructPath);
argsArray.splice(iFirstSave, 4, args);
return iFirstSave + 1;
}
);
class NullOptimizer {
constructor(queue) {
this.queue = queue;
@ -699,7 +770,7 @@ class OperatorList {
case OPS.paintInlineImageXObjectGroup:
case OPS.paintImageMaskXObject:
const arg = argsArray[i][0]; // First parameter in imgData.
if (!arg.cached && arg.data?.buffer instanceof ArrayBuffer) {
if (arg.data?.buffer instanceof ArrayBuffer) {
transfers.push(arg.data.buffer);
}
break;

View File

@ -54,9 +54,6 @@ const EXECUTION_TIME = 15; // ms
// Defines the number of steps before checking the execution time.
const EXECUTION_STEPS = 10;
// To disable Type3 compilation, set the value to `-1`.
const MAX_SIZE_TO_COMPILE = 1000;
const FULL_CHUNK_HEIGHT = 16;
// Only used in rescaleAndStroke. The goal is to avoid
@ -299,169 +296,6 @@ function drawImageAtIntegerCoords(
return [scaleX * destW, scaleY * destH];
}
function compileType3Glyph(imgData) {
const { width, height } = imgData;
if (width > MAX_SIZE_TO_COMPILE || height > MAX_SIZE_TO_COMPILE) {
return null;
}
const POINT_TO_PROCESS_LIMIT = 1000;
const POINT_TYPES = new Uint8Array([
0, 2, 4, 0, 1, 0, 5, 4, 8, 10, 0, 8, 0, 2, 1, 0,
]);
const width1 = width + 1;
const points = new Uint8Array(width1 * (height + 1));
let i, j, j0;
// decodes bit-packed mask data
const lineSize = (width + 7) & ~7;
const data = new Uint8Array(lineSize * height);
let pos = 0;
for (const elem of imgData.data) {
let mask = 128;
while (mask > 0) {
data[pos++] = elem & mask ? 0 : 255;
mask >>= 1;
}
}
// finding interesting points: every point is located between mask pixels,
// so there will be points of the (width + 1)x(height + 1) grid. Every point
// will have flags assigned based on neighboring mask pixels:
// 4 | 8
// --P--
// 2 | 1
// We are interested only in points with the flags:
// - outside corners: 1, 2, 4, 8;
// - inside corners: 7, 11, 13, 14;
// - and, intersections: 5, 10.
let count = 0;
pos = 0;
if (data[pos] !== 0) {
points[0] = 1;
++count;
}
for (j = 1; j < width; j++) {
if (data[pos] !== data[pos + 1]) {
points[j] = data[pos] ? 2 : 1;
++count;
}
pos++;
}
if (data[pos] !== 0) {
points[j] = 2;
++count;
}
for (i = 1; i < height; i++) {
pos = i * lineSize;
j0 = i * width1;
if (data[pos - lineSize] !== data[pos]) {
points[j0] = data[pos] ? 1 : 8;
++count;
}
// 'sum' is the position of the current pixel configuration in the 'TYPES'
// array (in order 8-1-2-4, so we can use '>>2' to shift the column).
let sum = (data[pos] ? 4 : 0) + (data[pos - lineSize] ? 8 : 0);
for (j = 1; j < width; j++) {
sum =
(sum >> 2) +
(data[pos + 1] ? 4 : 0) +
(data[pos - lineSize + 1] ? 8 : 0);
if (POINT_TYPES[sum]) {
points[j0 + j] = POINT_TYPES[sum];
++count;
}
pos++;
}
if (data[pos - lineSize] !== data[pos]) {
points[j0 + j] = data[pos] ? 2 : 4;
++count;
}
if (count > POINT_TO_PROCESS_LIMIT) {
return null;
}
}
pos = lineSize * (height - 1);
j0 = i * width1;
if (data[pos] !== 0) {
points[j0] = 8;
++count;
}
for (j = 1; j < width; j++) {
if (data[pos] !== data[pos + 1]) {
points[j0 + j] = data[pos] ? 4 : 8;
++count;
}
pos++;
}
if (data[pos] !== 0) {
points[j0 + j] = 4;
++count;
}
if (count > POINT_TO_PROCESS_LIMIT) {
return null;
}
// building outlines
const steps = new Int32Array([0, width1, -1, 0, -width1, 0, 0, 0, 1]);
const path = new Path2D();
// the path shall be painted in [0..1]x[0..1] space
const { a, b, c, d, e, f } = new DOMMatrix()
.scaleSelf(1 / width, -1 / height)
.translateSelf(0, -height);
for (i = 0; count && i <= height; i++) {
let p = i * width1;
const end = p + width;
while (p < end && !points[p]) {
p++;
}
if (p === end) {
continue;
}
let x = p % width1;
let y = i;
path.moveTo(a * x + c * y + e, b * x + d * y + f);
const p0 = p;
let type = points[p];
do {
const step = steps[type];
do {
p += step;
} while (!points[p]);
const pp = points[p];
if (pp !== 5 && pp !== 10) {
// set new direction
type = pp;
// delete mark
points[p] = 0;
} else {
// type is 5 or 10, ie, a crossing
// set new direction
type = pp & ((0x33 * type) >> 4);
// set new type for "future hit"
points[p] &= (type >> 2) | (type << 2);
}
x = p % width1;
y = (p / width1) | 0;
path.lineTo(a * x + c * y + e, b * x + d * y + f);
if (!points[p]) {
--count;
}
} while (p0 !== p);
--i;
}
return path;
}
class CanvasExtraState {
constructor(width, height) {
// Are soft masks and alpha values shapes or opacities?
@ -821,7 +655,6 @@ class CanvasGraphics {
this.canvasFactory = canvasFactory;
this.filterFactory = filterFactory;
this.groupStack = [];
this.processingType3 = null;
// Patterns are painted relative to the initial page/form transform, see
// PDF spec 8.7.2 NOTE 1.
this.baseTransform = null;
@ -1747,6 +1580,10 @@ class CanvasGraphics {
this.consumePath(path);
}
rawFillPath(path) {
this.ctx.fill(path);
}
// Clipping
clip() {
this.pendingClip = NORMAL_CLIP;
@ -2267,7 +2104,6 @@ class CanvasGraphics {
if (!operatorList) {
warn(`Type3 character "${glyph.operatorListId}" is not available.`);
} else if (this.contentVisible) {
this.processingType3 = glyph;
this.save();
ctx.scale(fontSize, fontSize);
ctx.transform(...fontMatrix);
@ -2282,7 +2118,6 @@ class CanvasGraphics {
current.x += width * textHScale;
}
ctx.restore();
this.processingType3 = null;
}
// Type3 fonts
@ -2703,18 +2538,6 @@ class CanvasGraphics {
img.count = count;
const ctx = this.ctx;
const glyph = this.processingType3;
if (glyph) {
if (glyph.compiled === undefined) {
glyph.compiled = compileType3Glyph(img);
}
if (glyph.compiled) {
ctx.fill(glyph.compiled);
return;
}
}
const mask = this._createMaskCanvas(img);
const maskCanvas = mask.canvas;

View File

@ -339,6 +339,7 @@ const OPS = {
constructPath: 91,
setStrokeTransparent: 92,
setFillTransparent: 93,
rawFillPath: 94,
};
// In order to have a switch statement that is fast (i.e. which use a jump
@ -676,6 +677,57 @@ class Util {
return `#${hexNumbers[r]}${hexNumbers[g]}${hexNumbers[b]}`;
}
// Apply a scaling matrix to some min/max values.
// If a scaling factor is negative then min and max must be
// swapped.
static scaleMinMax(transform, minMax) {
let temp;
if (transform[0]) {
if (transform[0] < 0) {
temp = minMax[0];
minMax[0] = minMax[2];
minMax[2] = temp;
}
minMax[0] *= transform[0];
minMax[2] *= transform[0];
if (transform[3] < 0) {
temp = minMax[1];
minMax[1] = minMax[3];
minMax[3] = temp;
}
minMax[1] *= transform[3];
minMax[3] *= transform[3];
} else {
temp = minMax[0];
minMax[0] = minMax[1];
minMax[1] = temp;
temp = minMax[2];
minMax[2] = minMax[3];
minMax[3] = temp;
if (transform[1] < 0) {
temp = minMax[1];
minMax[1] = minMax[3];
minMax[3] = temp;
}
minMax[1] *= transform[1];
minMax[3] *= transform[1];
if (transform[2] < 0) {
temp = minMax[0];
minMax[0] = minMax[2];
minMax[2] = temp;
}
minMax[0] *= transform[2];
minMax[2] *= transform[2];
}
minMax[0] += transform[4];
minMax[1] += transform[5];
minMax[2] += transform[4];
minMax[3] += transform[5];
}
// Concatenates two transformation matrices together and returns the result.
static transform(m1, m2) {
return [
@ -695,6 +747,22 @@ class Util {
return [xt, yt];
}
static applyTransformInPlace(p, m) {
const [p0, p1] = p;
p[0] = p0 * m[0] + p1 * m[2] + m[4];
p[1] = p0 * m[1] + p1 * m[3] + m[5];
}
// For 2d affine transforms
static applyTransformToBezierInPlace(p, [m0, m1, m2, m3, m4, m5]) {
for (let i = 0; i < 6; i += 2) {
const pI = p[i];
const pI1 = p[i + 1];
p[i] = pI * m0 + pI1 * m2 + m4;
p[i + 1] = pI * m1 + pI1 * m3 + m5;
}
}
static applyInverseTransform(p, m) {
const d = m[0] * m[3] - m[1] * m[2];
const xt = (p[0] * m[3] - p[1] * m[2] + m[2] * m[5] - m[4] * m[3]) / d;