pdf.js.mirror/src/display/canvas_dependency_tracker.js
Nicolò Ribaudo 4f7a025e21
Separate bbox tracking from dependencies tracking
When recording bboxes for images, it's enough to record their
clip box / bounding box without needing to run the full bbox
tracking of the image's dependencies.
2026-03-10 14:51:03 +01:00

1249 lines
32 KiB
JavaScript

/* Copyright 2025 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 { FeatureTest, Util } from "../shared/util.js";
const FORCED_DEPENDENCY_LABEL = "__forcedDependency";
const { floor, ceil } = Math;
function expandBBox(array, index, minX, minY, maxX, maxY) {
array[index * 4 + 0] = Math.min(array[index * 4 + 0], minX);
array[index * 4 + 1] = Math.min(array[index * 4 + 1], minY);
array[index * 4 + 2] = Math.max(array[index * 4 + 2], maxX);
array[index * 4 + 3] = Math.max(array[index * 4 + 3], maxY);
}
// This is computed rathter than hard-coded to keep into
// account the platform's endianess.
const EMPTY_BBOX = new Uint32Array(new Uint8Array([255, 255, 0, 0]).buffer)[0];
class BBoxReader {
#bboxes;
#coords;
constructor(bboxes, coords) {
this.#bboxes = bboxes;
this.#coords = coords;
}
get length() {
return this.#bboxes.length;
}
isEmpty(i) {
return this.#bboxes[i] === EMPTY_BBOX;
}
minX(i) {
return this.#coords[i * 4 + 0] / 256;
}
minY(i) {
return this.#coords[i * 4 + 1] / 256;
}
maxX(i) {
return (this.#coords[i * 4 + 2] + 1) / 256;
}
maxY(i) {
return (this.#coords[i * 4 + 3] + 1) / 256;
}
}
const ensureDebugMetadata = (map, key) =>
map?.getOrInsertComputed(key, () => ({
dependencies: new Set(),
isRenderingOperation: false,
}));
// NOTE: CanvasBBoxTracker, CanvasDependencyTracker and
// CanvasNestedDependencyTracker must all have the same interface.
class CanvasBBoxTracker {
#baseTransformStack = [[1, 0, 0, 1, 0, 0]];
#clipBox = [-Infinity, -Infinity, Infinity, Infinity];
// Float32Array<minX, minY, maxX, maxY>
#pendingBBox = new Float64Array([Infinity, Infinity, -Infinity, -Infinity]);
_pendingBBoxIdx = -1;
#canvasWidth;
#canvasHeight;
// Uint8ClampedArray<minX, minY, maxX, maxY>
#bboxesCoords;
#bboxes;
_savesStack = [];
_markedContentStack = [];
constructor(canvas, operationsCount) {
this.#canvasWidth = canvas.width;
this.#canvasHeight = canvas.height;
this.#initializeBBoxes(operationsCount);
}
growOperationsCount(operationsCount) {
if (operationsCount >= this.#bboxes.length) {
this.#initializeBBoxes(operationsCount, this.#bboxes);
}
}
#initializeBBoxes(operationsCount, oldBBoxes) {
const buffer = new ArrayBuffer(operationsCount * 4);
this.#bboxesCoords = new Uint8ClampedArray(buffer);
this.#bboxes = new Uint32Array(buffer);
if (oldBBoxes && oldBBoxes.length > 0) {
this.#bboxes.set(oldBBoxes);
this.#bboxes.fill(EMPTY_BBOX, oldBBoxes.length);
} else {
this.#bboxes.fill(EMPTY_BBOX);
}
}
get clipBox() {
return this.#clipBox;
}
save(opIdx) {
this.#clipBox = { __proto__: this.#clipBox };
this._savesStack.push(opIdx);
return this;
}
restore(opIdx, onSavePopped) {
const previous = Object.getPrototypeOf(this.#clipBox);
if (previous === null) {
// Sometimes we call more .restore() than .save(), for
// example when using CanvasGraphics' #restoreInitialState()
return this;
}
this.#clipBox = previous;
const lastSave = this._savesStack.pop();
if (lastSave !== undefined) {
onSavePopped?.(lastSave, opIdx);
this.#bboxes[opIdx] = this.#bboxes[lastSave];
}
return this;
}
/**
* @param {number} idx
*/
recordOpenMarker(idx) {
this._savesStack.push(idx);
return this;
}
getOpenMarker() {
if (this._savesStack.length === 0) {
return null;
}
return this._savesStack.at(-1);
}
recordCloseMarker(opIdx, onSavePopped) {
const lastSave = this._savesStack.pop();
if (lastSave !== undefined) {
onSavePopped?.(lastSave, opIdx);
this.#bboxes[opIdx] = this.#bboxes[lastSave];
}
return this;
}
// Marked content needs a separate stack from save/restore, because they
// form two independent trees.
beginMarkedContent(opIdx) {
this._markedContentStack.push(opIdx);
return this;
}
endMarkedContent(opIdx, onSavePopped) {
const lastSave = this._markedContentStack.pop();
if (lastSave !== undefined) {
onSavePopped?.(lastSave, opIdx);
this.#bboxes[opIdx] = this.#bboxes[lastSave];
}
return this;
}
pushBaseTransform(ctx) {
this.#baseTransformStack.push(
Util.multiplyByDOMMatrix(
this.#baseTransformStack.at(-1),
ctx.getTransform()
)
);
return this;
}
popBaseTransform() {
if (this.#baseTransformStack.length > 1) {
this.#baseTransformStack.pop();
}
return this;
}
resetBBox(idx) {
if (this._pendingBBoxIdx !== idx) {
this._pendingBBoxIdx = idx;
this.#pendingBBox[0] = Infinity;
this.#pendingBBox[1] = Infinity;
this.#pendingBBox[2] = -Infinity;
this.#pendingBBox[3] = -Infinity;
}
return this;
}
recordClipBox(idx, ctx, minX, maxX, minY, maxY) {
const transform = Util.multiplyByDOMMatrix(
this.#baseTransformStack.at(-1),
ctx.getTransform()
);
const clipBox = [Infinity, Infinity, -Infinity, -Infinity];
Util.axialAlignedBoundingBox([minX, minY, maxX, maxY], transform, clipBox);
const intersection = Util.intersect(this.#clipBox, clipBox);
if (intersection) {
this.#clipBox[0] = intersection[0];
this.#clipBox[1] = intersection[1];
this.#clipBox[2] = intersection[2];
this.#clipBox[3] = intersection[3];
} else {
this.#clipBox[0] = this.#clipBox[1] = Infinity;
this.#clipBox[2] = this.#clipBox[3] = -Infinity;
}
return this;
}
recordBBox(idx, ctx, minX, maxX, minY, maxY) {
const clipBox = this.#clipBox;
if (clipBox[0] === Infinity) {
return this;
}
const transform = Util.multiplyByDOMMatrix(
this.#baseTransformStack.at(-1),
ctx.getTransform()
);
if (clipBox[0] === -Infinity) {
Util.axialAlignedBoundingBox(
[minX, minY, maxX, maxY],
transform,
this.#pendingBBox
);
return this;
}
const bbox = [Infinity, Infinity, -Infinity, -Infinity];
Util.axialAlignedBoundingBox([minX, minY, maxX, maxY], transform, bbox);
this.#pendingBBox[0] = Math.min(
this.#pendingBBox[0],
Math.max(bbox[0], clipBox[0])
);
this.#pendingBBox[1] = Math.min(
this.#pendingBBox[1],
Math.max(bbox[1], clipBox[1])
);
this.#pendingBBox[2] = Math.max(
this.#pendingBBox[2],
Math.min(bbox[2], clipBox[2])
);
this.#pendingBBox[3] = Math.max(
this.#pendingBBox[3],
Math.min(bbox[3], clipBox[3])
);
return this;
}
recordFullPageBBox(idx) {
this.#pendingBBox[0] = Math.max(0, this.#clipBox[0]);
this.#pendingBBox[1] = Math.max(0, this.#clipBox[1]);
this.#pendingBBox[2] = Math.min(this.#canvasWidth, this.#clipBox[2]);
this.#pendingBBox[3] = Math.min(this.#canvasHeight, this.#clipBox[3]);
return this;
}
/**
* @param {number} idx
*/
recordOperation(idx, preserve = false, dependencyLists) {
if (this._pendingBBoxIdx !== idx) {
return this;
}
const minX = floor((this.#pendingBBox[0] * 256) / this.#canvasWidth);
const minY = floor((this.#pendingBBox[1] * 256) / this.#canvasHeight);
const maxX = ceil((this.#pendingBBox[2] * 256) / this.#canvasWidth);
const maxY = ceil((this.#pendingBBox[3] * 256) / this.#canvasHeight);
expandBBox(this.#bboxesCoords, idx, minX, minY, maxX, maxY);
if (dependencyLists) {
for (const dependencies of dependencyLists) {
for (const depIdx of dependencies) {
if (depIdx !== idx) {
expandBBox(this.#bboxesCoords, depIdx, minX, minY, maxX, maxY);
}
}
}
}
if (!preserve) {
this._pendingBBoxIdx = -1;
}
return this;
}
bboxToClipBoxDropOperation(idx) {
if (this._pendingBBoxIdx === idx) {
this._pendingBBoxIdx = -1;
this.#clipBox[0] = Math.max(this.#clipBox[0], this.#pendingBBox[0]);
this.#clipBox[1] = Math.max(this.#clipBox[1], this.#pendingBBox[1]);
this.#clipBox[2] = Math.min(this.#clipBox[2], this.#pendingBBox[2]);
this.#clipBox[3] = Math.min(this.#clipBox[3], this.#pendingBBox[3]);
}
return this;
}
take() {
return new BBoxReader(this.#bboxes, this.#bboxesCoords);
}
takeDebugMetadata() {
throw new Error("Unreachable");
}
recordSimpleData(name, idx) {
return this;
}
recordIncrementalData(name, idx) {
return this;
}
resetIncrementalData(name, idx) {
return this;
}
recordNamedData(name, idx) {
return this;
}
recordSimpleDataFromNamed(name, depName, fallbackIdx) {
return this;
}
recordFutureForcedDependency(name, idx) {
return this;
}
inheritSimpleDataAsFutureForcedDependencies(names) {
return this;
}
inheritPendingDependenciesAsFutureForcedDependencies() {
return this;
}
recordCharacterBBox(idx, ctx, font, scale = 1, x = 0, y = 0, getMeasure) {
return this;
}
getSimpleIndex(dependencyName) {
return undefined;
}
recordDependencies(idx, dependencyNames) {
return this;
}
recordNamedDependency(idx, name) {
return this;
}
recordShowTextOperation(idx, preserve = false) {
return this;
}
}
/**
* @typedef {"lineWidth" | "lineCap" | "lineJoin" | "miterLimit" | "dash" |
* "strokeAlpha" | "fillColor" | "fillAlpha" | "globalCompositeOperation" |
* "path" | "filter" | "font" | "fontObj"} SimpleDependency
*/
/**
* @typedef {"transform" | "moveText" | "sameLineText"} IncrementalDependency
*/
/**
* @typedef {IncrementalDependency |
* typeof FORCED_DEPENDENCY_LABEL} InternalIncrementalDependency
*/
class CanvasDependencyTracker {
/** @type {Record<SimpleDependency, number>} */
#simple = { __proto__: null };
/** @type {Record<InternalIncrementalDependency , number[]>} */
#incremental = {
__proto__: null,
transform: [],
moveText: [],
sameLineText: [],
[FORCED_DEPENDENCY_LABEL]: [],
};
#namedDependencies = new Map();
#pendingDependencies = new Set();
#fontBBoxTrustworthy = new Map();
#debugMetadata;
#recordDebugMetadataDepenencyAfterRestore;
#bboxTracker;
constructor(bboxTracker, recordDebugMetadata = false) {
this.#bboxTracker = bboxTracker;
if (recordDebugMetadata) {
this.#debugMetadata = new Map();
this.#recordDebugMetadataDepenencyAfterRestore = (lastSave, opIdx) => {
ensureDebugMetadata(this.#debugMetadata, opIdx).dependencies.add(
lastSave
);
};
}
}
get clipBox() {
return this.#bboxTracker.clipBox;
}
growOperationsCount(operationsCount) {
this.#bboxTracker.growOperationsCount(operationsCount);
}
save(opIdx) {
this.#simple = { __proto__: this.#simple };
this.#incremental = {
__proto__: this.#incremental,
transform: { __proto__: this.#incremental.transform },
moveText: { __proto__: this.#incremental.moveText },
sameLineText: { __proto__: this.#incremental.sameLineText },
[FORCED_DEPENDENCY_LABEL]: {
__proto__: this.#incremental[FORCED_DEPENDENCY_LABEL],
},
};
this.#bboxTracker.save(opIdx);
return this;
}
restore(opIdx) {
this.#bboxTracker.restore(
opIdx,
this.#recordDebugMetadataDepenencyAfterRestore
);
const previous = Object.getPrototypeOf(this.#simple);
if (previous === null) {
// Sometimes we call more .restore() than .save(), for
// example when using CanvasGraphics' #restoreInitialState()
return this;
}
this.#simple = previous;
this.#incremental = Object.getPrototypeOf(this.#incremental);
return this;
}
recordOpenMarker(opIdx) {
this.#bboxTracker.recordOpenMarker(
opIdx,
this.#recordDebugMetadataDepenencyAfterRestore
);
return this;
}
getOpenMarker() {
return this.#bboxTracker.getOpenMarker();
}
recordCloseMarker(opIdx) {
this.#bboxTracker.recordCloseMarker(
opIdx,
this.#recordDebugMetadataDepenencyAfterRestore
);
return this;
}
/**
* @param {number} opIdx
*/
beginMarkedContent(opIdx) {
this.#bboxTracker.beginMarkedContent(opIdx);
return this;
}
endMarkedContent(opIdx) {
this.#bboxTracker.endMarkedContent(
opIdx,
this.#recordDebugMetadataDepenencyAfterRestore
);
return this;
}
pushBaseTransform(ctx) {
this.#bboxTracker.pushBaseTransform(ctx);
return this;
}
popBaseTransform() {
this.#bboxTracker.popBaseTransform();
return this;
}
/**
* @param {SimpleDependency} name
* @param {number} idx
*/
recordSimpleData(name, idx) {
this.#simple[name] = idx;
return this;
}
/**
* @param {IncrementalDependency} name
* @param {number} idx
*/
recordIncrementalData(name, idx) {
this.#incremental[name].push(idx);
return this;
}
/**
* @param {IncrementalDependency} name
* @param {number} idx
*/
resetIncrementalData(name, idx) {
this.#incremental[name].length = 0;
return this;
}
recordNamedData(name, idx) {
this.#namedDependencies.set(name, idx);
return this;
}
/**
* @param {SimpleDependency} name
* @param {string} depName
* @param {number} fallbackIdx
*/
recordSimpleDataFromNamed(name, depName, fallbackIdx) {
this.#simple[name] = this.#namedDependencies.get(depName) ?? fallbackIdx;
}
// All next operations, until the next .restore(), will depend on this
recordFutureForcedDependency(name, idx) {
this.recordIncrementalData(FORCED_DEPENDENCY_LABEL, idx);
return this;
}
// All next operations, until the next .restore(), will depend on all
// the already recorded data with the given names.
inheritSimpleDataAsFutureForcedDependencies(names) {
for (const name of names) {
if (name in this.#simple) {
this.recordFutureForcedDependency(name, this.#simple[name]);
}
}
return this;
}
inheritPendingDependenciesAsFutureForcedDependencies() {
for (const dep of this.#pendingDependencies) {
this.recordFutureForcedDependency(FORCED_DEPENDENCY_LABEL, dep);
}
return this;
}
resetBBox(idx) {
this.#bboxTracker.resetBBox(idx);
return this;
}
recordClipBox(idx, ctx, minX, maxX, minY, maxY) {
this.#bboxTracker.recordClipBox(idx, ctx, minX, maxX, minY, maxY);
return this;
}
recordBBox(idx, ctx, minX, maxX, minY, maxY) {
this.#bboxTracker.recordBBox(idx, ctx, minX, maxX, minY, maxY);
return this;
}
recordCharacterBBox(idx, ctx, font, scale = 1, x = 0, y = 0, getMeasure) {
const fontBBox = font.bbox;
let isBBoxTrustworthy;
let computedBBox;
if (fontBBox) {
isBBoxTrustworthy =
// Only use the bounding box defined by the font if it
// has a non-empty area.
fontBBox[2] !== fontBBox[0] &&
fontBBox[3] !== fontBBox[1] &&
this.#fontBBoxTrustworthy.get(font);
if (isBBoxTrustworthy !== false) {
computedBBox = [0, 0, 0, 0];
Util.axialAlignedBoundingBox(fontBBox, font.fontMatrix, computedBBox);
if (scale !== 1 || x !== 0 || y !== 0) {
Util.scaleMinMax([scale, 0, 0, -scale, x, y], computedBBox);
}
if (isBBoxTrustworthy) {
return this.recordBBox(
idx,
ctx,
computedBBox[0],
computedBBox[2],
computedBBox[1],
computedBBox[3]
);
}
}
}
if (!getMeasure) {
// We have no way of telling how big this character actually is, record
// a full page bounding box.
return this.recordFullPageBBox(idx);
}
const measure = getMeasure();
if (fontBBox && computedBBox && isBBoxTrustworthy === undefined) {
// If it's the first time we can compare the font bbox with the actual
// bbox measured when drawing it, check if the one recorded in the font
// is large enough to cover the actual bbox. If it is, we assume that the
// font is well-formed and we can use the declared bbox without having to
// measure it again for every character.
isBBoxTrustworthy =
computedBBox[0] <= x - measure.actualBoundingBoxLeft &&
computedBBox[2] >= x + measure.actualBoundingBoxRight &&
computedBBox[1] <= y - measure.actualBoundingBoxAscent &&
computedBBox[3] >= y + measure.actualBoundingBoxDescent;
this.#fontBBoxTrustworthy.set(font, isBBoxTrustworthy);
if (isBBoxTrustworthy) {
return this.recordBBox(
idx,
ctx,
computedBBox[0],
computedBBox[2],
computedBBox[1],
computedBBox[3]
);
}
}
// The font has no bbox or it is not trustworthy, so we need to
// return the bounding box based on .measureText().
return this.recordBBox(
idx,
ctx,
x - measure.actualBoundingBoxLeft,
x + measure.actualBoundingBoxRight,
y - measure.actualBoundingBoxAscent,
y + measure.actualBoundingBoxDescent
);
}
recordFullPageBBox(idx) {
this.#bboxTracker.recordFullPageBBox(idx);
return this;
}
getSimpleIndex(dependencyName) {
return this.#simple[dependencyName];
}
recordDependencies(idx, dependencyNames) {
const pendingDependencies = this.#pendingDependencies;
const simple = this.#simple;
const incremental = this.#incremental;
for (const name of dependencyNames) {
if (name in this.#simple) {
pendingDependencies.add(simple[name]);
} else if (name in incremental) {
incremental[name].forEach(pendingDependencies.add, pendingDependencies);
}
}
return this;
}
recordNamedDependency(idx, name) {
if (this.#namedDependencies.has(name)) {
this.#pendingDependencies.add(this.#namedDependencies.get(name));
}
return this;
}
/**
* @param {number} idx
*/
recordOperation(idx, preserve = false) {
this.recordDependencies(idx, [FORCED_DEPENDENCY_LABEL]);
if (this.#debugMetadata) {
const metadata = ensureDebugMetadata(this.#debugMetadata, idx);
const { dependencies } = metadata;
this.#pendingDependencies.forEach(dependencies.add, dependencies);
this.#bboxTracker._savesStack.forEach(dependencies.add, dependencies);
this.#bboxTracker._markedContentStack.forEach(
dependencies.add,
dependencies
);
dependencies.delete(idx);
metadata.isRenderingOperation = true;
}
const needsCleanup = !preserve && idx === this.#bboxTracker._pendingBBoxIdx;
this.#bboxTracker.recordOperation(idx, preserve, [
this.#pendingDependencies,
this.#bboxTracker._savesStack,
this.#bboxTracker._markedContentStack,
]);
if (needsCleanup) {
this.#pendingDependencies.clear();
}
return this;
}
recordShowTextOperation(idx, preserve = false) {
const deps = Array.from(this.#pendingDependencies);
this.recordOperation(idx, preserve);
this.recordIncrementalData("sameLineText", idx);
for (const dep of deps) {
this.recordIncrementalData("sameLineText", dep);
}
return this;
}
bboxToClipBoxDropOperation(idx, preserve = false) {
const needsCleanup = !preserve && idx === this.#bboxTracker._pendingBBoxIdx;
this.#bboxTracker.bboxToClipBoxDropOperation(idx);
if (needsCleanup) {
this.#pendingDependencies.clear();
}
return this;
}
take() {
this.#fontBBoxTrustworthy.clear();
return this.#bboxTracker.take();
}
takeDebugMetadata() {
return this.#debugMetadata;
}
}
/**
* Used to track dependencies of nested operations list, that
* should actually all map to the index of the operation that
* contains the nested list.
*
* @implements {CanvasDependencyTracker}
*/
class CanvasNestedDependencyTracker {
/** @type {CanvasDependencyTracker} */
#dependencyTracker;
/** @type {number} */
#opIdx;
#ignoreBBoxes;
#nestingLevel = 0;
#savesLevel = 0;
constructor(dependencyTracker, opIdx, ignoreBBoxes) {
if (
dependencyTracker instanceof CanvasNestedDependencyTracker &&
dependencyTracker.#ignoreBBoxes === !!ignoreBBoxes
) {
// The goal of CanvasNestedDependencyTracker is to collapse all operations
// into a single one. If we are already in a
// CanvasNestedDependencyTracker, that is already happening.
return dependencyTracker;
}
this.#dependencyTracker = dependencyTracker;
this.#opIdx = opIdx;
this.#ignoreBBoxes = !!ignoreBBoxes;
}
get clipBox() {
return this.#dependencyTracker.clipBox;
}
growOperationsCount() {
throw new Error("Unreachable");
}
save(opIdx) {
this.#savesLevel++;
this.#dependencyTracker.save(this.#opIdx);
return this;
}
restore(opIdx) {
if (this.#savesLevel > 0) {
this.#dependencyTracker.restore(this.#opIdx);
this.#savesLevel--;
}
return this;
}
recordOpenMarker(idx) {
this.#nestingLevel++;
return this;
}
getOpenMarker() {
return this.#nestingLevel > 0
? this.#opIdx
: this.#dependencyTracker.getOpenMarker();
}
recordCloseMarker(idx) {
this.#nestingLevel--;
return this;
}
beginMarkedContent(opIdx) {
return this;
}
endMarkedContent(opIdx) {
return this;
}
pushBaseTransform(ctx) {
this.#dependencyTracker.pushBaseTransform(ctx);
return this;
}
popBaseTransform() {
this.#dependencyTracker.popBaseTransform();
return this;
}
/**
* @param {SimpleDependency} name
* @param {number} idx
*/
recordSimpleData(name, idx) {
this.#dependencyTracker.recordSimpleData(name, this.#opIdx);
return this;
}
/**
* @param {IncrementalDependency} name
* @param {number} idx
*/
recordIncrementalData(name, idx) {
this.#dependencyTracker.recordIncrementalData(name, this.#opIdx);
return this;
}
/**
* @param {IncrementalDependency} name
* @param {number} idx
*/
resetIncrementalData(name, idx) {
this.#dependencyTracker.resetIncrementalData(name, this.#opIdx);
return this;
}
recordNamedData(name, idx) {
// Nested dependencies are not visible to the outside.
return this;
}
/**
* @param {SimpleDependency} name
* @param {string} depName
* @param {number} fallbackIdx
*/
recordSimpleDataFromNamed(name, depName, fallbackIdx) {
this.#dependencyTracker.recordSimpleDataFromNamed(
name,
depName,
this.#opIdx
);
return this;
}
// All next operations, until the next .restore(), will depend on this
recordFutureForcedDependency(name, idx) {
this.#dependencyTracker.recordFutureForcedDependency(name, this.#opIdx);
return this;
}
// All next operations, until the next .restore(), will depend on all
// the already recorded data with the given names.
inheritSimpleDataAsFutureForcedDependencies(names) {
this.#dependencyTracker.inheritSimpleDataAsFutureForcedDependencies(names);
return this;
}
inheritPendingDependenciesAsFutureForcedDependencies() {
this.#dependencyTracker.inheritPendingDependenciesAsFutureForcedDependencies();
return this;
}
resetBBox(idx) {
if (!this.#ignoreBBoxes) {
this.#dependencyTracker.resetBBox(this.#opIdx);
}
return this;
}
recordClipBox(idx, ctx, minX, maxX, minY, maxY) {
if (!this.#ignoreBBoxes) {
this.#dependencyTracker.recordClipBox(
this.#opIdx,
ctx,
minX,
maxX,
minY,
maxY
);
}
return this;
}
recordBBox(idx, ctx, minX, maxX, minY, maxY) {
if (!this.#ignoreBBoxes) {
this.#dependencyTracker.recordBBox(
this.#opIdx,
ctx,
minX,
maxX,
minY,
maxY
);
}
return this;
}
recordCharacterBBox(idx, ctx, font, scale, x, y, getMeasure) {
if (!this.#ignoreBBoxes) {
this.#dependencyTracker.recordCharacterBBox(
this.#opIdx,
ctx,
font,
scale,
x,
y,
getMeasure
);
}
return this;
}
recordFullPageBBox(idx) {
if (!this.#ignoreBBoxes) {
this.#dependencyTracker.recordFullPageBBox(this.#opIdx);
}
return this;
}
getSimpleIndex(dependencyName) {
return this.#dependencyTracker.getSimpleIndex(dependencyName);
}
recordDependencies(idx, dependencyNames) {
this.#dependencyTracker.recordDependencies(this.#opIdx, dependencyNames);
return this;
}
recordNamedDependency(idx, name) {
this.#dependencyTracker.recordNamedDependency(this.#opIdx, name);
return this;
}
/**
* @param {number} idx
* @param {SimpleDependency[]} dependencyNames
*/
recordOperation(idx) {
this.#dependencyTracker.recordOperation(this.#opIdx, true);
return this;
}
recordShowTextOperation(idx) {
this.#dependencyTracker.recordShowTextOperation(this.#opIdx, true);
return this;
}
bboxToClipBoxDropOperation(idx) {
if (!this.#ignoreBBoxes) {
this.#dependencyTracker.bboxToClipBoxDropOperation(this.#opIdx, true);
}
return this;
}
take() {
throw new Error("Unreachable");
}
takeDebugMetadata() {
throw new Error("Unreachable");
}
}
/** @satisfies {Record<string, SimpleDependency | IncrementalDependency>} */
const Dependencies = {
stroke: [
"path",
"transform",
"filter",
"strokeColor",
"strokeAlpha",
"lineWidth",
"lineCap",
"lineJoin",
"miterLimit",
"dash",
],
fill: [
"path",
"transform",
"filter",
"fillColor",
"fillAlpha",
"globalCompositeOperation",
"SMask",
],
imageXObject: [
"transform",
"SMask",
"filter",
"fillAlpha",
"strokeAlpha",
"globalCompositeOperation",
],
rawFillPath: ["filter", "fillColor", "fillAlpha"],
showText: [
"transform",
"leading",
"charSpacing",
"wordSpacing",
"hScale",
"textRise",
"moveText",
"textMatrix",
"font",
"fontObj",
"filter",
"fillColor",
"textRenderingMode",
"SMask",
"fillAlpha",
"strokeAlpha",
"globalCompositeOperation",
"sameLineText",
],
transform: ["transform"],
transformAndFill: ["transform", "fillColor"],
};
/**
* Track the locations of images in the canvas. For each image it computes
* a bounding box as a potentially rotated rectangle, matching the rotation of
* the current canvas transform.
*/
class CanvasImagesTracker {
#canvasWidth;
#canvasHeight;
#capacity = 4;
#count = 0;
// Array of [x1, y1, x2, y2, x3, y3] coordinates.
// We need three points to be able to represent a rectangle with a transform
// applied.
#coords = new CanvasImagesTracker.#CoordsArray(this.#capacity * 6);
static #CoordsArray =
(typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) ||
FeatureTest.isFloat16ArraySupported
? Float16Array
: Float32Array;
constructor(canvas) {
this.#canvasWidth = canvas.width;
this.#canvasHeight = canvas.height;
}
record(ctx, width, height, clipBox) {
if (this.#count === this.#capacity) {
this.#capacity *= 2;
const newCoords = new CanvasImagesTracker.#CoordsArray(
this.#capacity * 6
);
newCoords.set(this.#coords);
this.#coords = newCoords;
}
const transform = Util.domMatrixToTransform(ctx.getTransform());
// We want top left, bottom left, top right.
// (0, 0) is the bottom left corner.
let coords;
if (clipBox[0] !== Infinity) {
const bbox = [Infinity, Infinity, -Infinity, -Infinity];
Util.axialAlignedBoundingBox([0, -height, width, 0], transform, bbox);
const finalBBox = Util.intersect(clipBox, bbox);
if (!finalBBox) {
// The image is fully clipped out.
return;
}
const [minX, minY, maxX, maxY] = finalBBox;
if (
minX !== bbox[0] ||
minY !== bbox[1] ||
maxX !== bbox[2] ||
maxY !== bbox[3]
) {
// The clip box affects the image drawing. We need to compute a
// transform that takes the image bbox and fits it into the final bbox,
// so that we can then apply it to the original image shape (the
// non-axially-aligned rectangle).
const rotationAngle = Math.atan2(transform[1], transform[0]);
// Normalize the angle to be between 0 and 90 degrees.
const sin = Math.abs(Math.sin(rotationAngle));
const cos = Math.abs(Math.cos(rotationAngle));
if (
sin < 1e-6 ||
cos < 1e-6 ||
// The logic in the `else` case gives more accurate bounding boxes for
// rotated images, but the equation it uses does not give a result
// when the rotation is exactly 45 degrees, because there are infinite
// possible rectangles that can fit into the same bbox with that same
// 45deg rotation. Fallback to returning the whole bbox.
Math.abs(sin - cos) < 1e-6
) {
coords = [minX, minY, minX, maxY, maxX, minY];
} else {
// We cannot just scale the bbox into the original bbox, because that
// would not preserve the 90deg corners if they have been rotated.
// We instead need to find the transform that maps the original
// rectangle into the only rectangle that is rotated by the expected
// angle and fits into the final bbox.
//
// This represents the final bbox, with the top-left corner having
// coordinates (minX, minY) and the bottom-right corner having
// coordinates (maxX, maxY). Alpha is the rotation angle, and a and b
// are helper variables used to compute the effective transform.
//
// ------------b----------
// +-----------------------*----+
// | | _ -‾ \ |
// a | _ -‾ \ |
// | |alpha _ -‾ \ |
// | | _ -‾ \|
// |\ _ -‾|
// | \ _ -‾ |
// | \ _ -‾ |
// | \ _ -‾ |
// +----*-----------------------+
const finalBBoxWidth = maxX - minX;
const finalBBoxHeight = maxY - minY;
const sin2 = sin * sin;
const cos2 = cos * cos;
const cosSin = cos * sin;
const denom = cos2 - sin2;
const a = (finalBBoxHeight * cos2 - finalBBoxWidth * cosSin) / denom;
const b = (finalBBoxHeight * cosSin - finalBBoxWidth * sin2) / denom;
coords = [minX + b, minY, minX, minY + a, maxX, maxY - a];
}
}
}
if (!coords) {
coords = [0, -height, 0, 0, width, -height];
Util.applyTransform(coords, transform, 0);
Util.applyTransform(coords, transform, 2);
Util.applyTransform(coords, transform, 4);
}
coords[0] /= this.#canvasWidth;
coords[1] /= this.#canvasHeight;
coords[2] /= this.#canvasWidth;
coords[3] /= this.#canvasHeight;
coords[4] /= this.#canvasWidth;
coords[5] /= this.#canvasHeight;
this.#coords.set(coords, this.#count * 6);
this.#count++;
}
take() {
return this.#coords.subarray(0, this.#count * 6);
}
}
export {
CanvasBBoxTracker,
CanvasDependencyTracker,
CanvasImagesTracker,
CanvasNestedDependencyTracker,
Dependencies,
};