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.
This commit is contained in:
Nicolò Ribaudo 2026-02-25 15:23:22 +01:00
parent 886c90d1a5
commit 4f7a025e21
No known key found for this signature in database
GPG Key ID: AAFDA9101C58F338
2 changed files with 414 additions and 237 deletions

View File

@ -39,6 +39,7 @@ import {
SerializableEmpty,
} from "./annotation_storage.js";
import {
CanvasBBoxTracker,
CanvasDependencyTracker,
CanvasImagesTracker,
} from "./canvas_dependency_tracker.js";
@ -1594,20 +1595,28 @@ class PDFPageProxy {
}
};
let dependencyTracker = null;
let bboxTracker = null;
if (shouldRecordOperations || shouldRecordImages) {
bboxTracker = new CanvasBBoxTracker(
canvas,
intentState.operatorList.length
);
}
if (shouldRecordOperations) {
dependencyTracker = new CanvasDependencyTracker(
bboxTracker,
recordForDebugger
);
}
const internalRenderTask = new InternalRenderTask({
callback: complete,
// Only include the required properties, and *not* the entire object.
params: {
canvas,
canvasContext,
dependencyTracker:
shouldRecordOperations || shouldRecordImages
? new CanvasDependencyTracker(
canvas,
intentState.operatorList.length,
recordForDebugger
)
: null,
dependencyTracker: dependencyTracker ?? bboxTracker,
imagesTracker: shouldRecordImages
? new CanvasImagesTracker(canvas)
: null,

View File

@ -71,6 +71,323 @@ const ensureDebugMetadata = (map, key) =>
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" |
@ -100,65 +417,34 @@ class CanvasDependencyTracker {
#namedDependencies = new Map();
#savesStack = [];
#markedContentStack = [];
#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;
#pendingDependencies = new Set();
#operations = new Map();
#fontBBoxTrustworthy = new Map();
#canvasWidth;
#canvasHeight;
// Uint8ClampedArray<minX, minY, maxX, maxY>
#bboxesCoords;
#bboxes;
#debugMetadata;
constructor(canvas, operationsCount, recordDebugMetadata = false) {
this.#canvasWidth = canvas.width;
this.#canvasHeight = canvas.height;
this.#initializeBBoxes(operationsCount);
#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.#clipBox;
return this.#bboxTracker.clipBox;
}
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);
}
this.#bboxTracker.growOperationsCount(operationsCount);
}
save(opIdx) {
@ -172,13 +458,17 @@ class CanvasDependencyTracker {
__proto__: this.#incremental[FORCED_DEPENDENCY_LABEL],
},
};
this.#clipBox = { __proto__: this.#clipBox };
this.#savesStack.push(opIdx);
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
@ -187,77 +477,53 @@ class CanvasDependencyTracker {
}
this.#simple = previous;
this.#incremental = Object.getPrototypeOf(this.#incremental);
this.#clipBox = Object.getPrototypeOf(this.#clipBox);
const lastSave = this.#savesStack.pop();
if (lastSave !== undefined) {
ensureDebugMetadata(this.#debugMetadata, opIdx)?.dependencies.add(
lastSave
);
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) {
const lastSave = this.#savesStack.pop();
if (lastSave !== undefined) {
ensureDebugMetadata(this.#debugMetadata, opIdx)?.dependencies.add(
lastSave
);
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) {
const lastSave = this.#markedContentStack.pop();
if (lastSave !== undefined) {
ensureDebugMetadata(this.#debugMetadata, opIdx)?.dependencies.add(
lastSave
);
this.#bboxes[opIdx] = this.#bboxes[lastSave];
}
return this;
}
pushBaseTransform(ctx) {
this.#baseTransformStack.push(
Util.multiplyByDOMMatrix(
this.#baseTransformStack.at(-1),
ctx.getTransform()
)
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() {
if (this.#baseTransformStack.length > 1) {
this.#baseTransformStack.pop();
}
this.#bboxTracker.popBaseTransform();
return this;
}
@ -327,73 +593,17 @@ class CanvasDependencyTracker {
}
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;
}
this.#bboxTracker.resetBBox(idx);
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;
}
this.#bboxTracker.recordClipBox(idx, ctx, minX, maxX, minY, maxY);
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])
);
this.#bboxTracker.recordBBox(idx, ctx, minX, maxX, minY, maxY);
return this;
}
@ -475,11 +685,7 @@ class CanvasDependencyTracker {
}
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]);
this.#bboxTracker.recordFullPageBBox(idx);
return this;
}
@ -520,39 +726,25 @@ class CanvasDependencyTracker {
const metadata = ensureDebugMetadata(this.#debugMetadata, idx);
const { dependencies } = metadata;
this.#pendingDependencies.forEach(dependencies.add, dependencies);
this.#savesStack.forEach(dependencies.add, dependencies);
this.#markedContentStack.forEach(dependencies.add, dependencies);
this.#bboxTracker._savesStack.forEach(dependencies.add, dependencies);
this.#bboxTracker._markedContentStack.forEach(
dependencies.add,
dependencies
);
dependencies.delete(idx);
metadata.isRenderingOperation = true;
}
if (this.#pendingBBoxIdx === idx) {
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);
const needsCleanup = !preserve && idx === this.#bboxTracker._pendingBBoxIdx;
expandBBox(this.#bboxesCoords, idx, minX, minY, maxX, maxY);
for (const depIdx of this.#pendingDependencies) {
if (depIdx !== idx) {
expandBBox(this.#bboxesCoords, depIdx, minX, minY, maxX, maxY);
}
}
for (const saveIdx of this.#savesStack) {
if (saveIdx !== idx) {
expandBBox(this.#bboxesCoords, saveIdx, minX, minY, maxX, maxY);
}
}
for (const saveIdx of this.#markedContentStack) {
if (saveIdx !== idx) {
expandBBox(this.#bboxesCoords, saveIdx, minX, minY, maxX, maxY);
}
}
this.#bboxTracker.recordOperation(idx, preserve, [
this.#pendingDependencies,
this.#bboxTracker._savesStack,
this.#bboxTracker._markedContentStack,
]);
if (!preserve) {
this.#pendingDependencies.clear();
this.#pendingBBoxIdx = -1;
}
if (needsCleanup) {
this.#pendingDependencies.clear();
}
return this;
@ -569,42 +761,17 @@ class CanvasDependencyTracker {
}
bboxToClipBoxDropOperation(idx, preserve = false) {
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]);
if (!preserve) {
this.#pendingDependencies.clear();
}
const needsCleanup = !preserve && idx === this.#bboxTracker._pendingBBoxIdx;
this.#bboxTracker.bboxToClipBoxDropOperation(idx);
if (needsCleanup) {
this.#pendingDependencies.clear();
}
return this;
}
_takePendingDependencies() {
const pendingDependencies = this.#pendingDependencies;
this.#pendingDependencies = new Set();
return pendingDependencies;
}
_extractOperation(idx) {
const operation = this.#operations.get(idx);
this.#operations.delete(idx);
return operation;
}
_pushPendingDependencies(dependencies) {
for (const dep of dependencies) {
this.#pendingDependencies.add(dep);
}
}
take() {
this.#fontBBoxTrustworthy.clear();
return new BBoxReader(this.#bboxes, this.#bboxesCoords);
return this.#bboxTracker.take();
}
takeDebugMetadata() {
@ -1073,6 +1240,7 @@ class CanvasImagesTracker {
}
export {
CanvasBBoxTracker,
CanvasDependencyTracker,
CanvasImagesTracker,
CanvasNestedDependencyTracker,