From 4f7a025e21060a9f1eae33ecce2f000dd51a8e48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Wed, 25 Feb 2026 15:23:22 +0100 Subject: [PATCH] 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. --- src/display/api.js | 25 +- src/display/canvas_dependency_tracker.js | 626 ++++++++++++++--------- 2 files changed, 414 insertions(+), 237 deletions(-) diff --git a/src/display/api.js b/src/display/api.js index aa7dd97db..16ef2553b 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -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, diff --git a/src/display/canvas_dependency_tracker.js b/src/display/canvas_dependency_tracker.js index 91de981d8..6a5bd5aae 100644 --- a/src/display/canvas_dependency_tracker.js +++ b/src/display/canvas_dependency_tracker.js @@ -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 + #pendingBBox = new Float64Array([Infinity, Infinity, -Infinity, -Infinity]); + + _pendingBBoxIdx = -1; + + #canvasWidth; + + #canvasHeight; + + // Uint8ClampedArray + #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 - #pendingBBox = new Float64Array([Infinity, Infinity, -Infinity, -Infinity]); - - #pendingBBoxIdx = -1; - #pendingDependencies = new Set(); - #operations = new Map(); - #fontBBoxTrustworthy = new Map(); - #canvasWidth; - - #canvasHeight; - - // Uint8ClampedArray - #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,