Store ops bboxes in a linear Uint8Array

This PR changes the way we store bounding boxes so that they use less
memory and can be more easily shared across threads in the future.

Instead of storing the bounding box and list of dependencies for each
operation that renders _something_, we now only store the bounding box
of _every_ operation and no dependencies list. The bounding box of
each operation covers the bounding box of all the operations affected
by it that render something. For example, the bounding box of a
`setFont` operation will be the bounding box of all the `showText`
operations that use that font.

This affects the debugging experience in pdfBug, since now the bounding
box of an operation may be larger than what it renders itself. To help
with this, now when hovering on an operation we also highlight (in red)
all its dependents. We highlight with white stripes operations that do
not affect any part of the page (i.e. with an empty bbox).

To save memory, we now save bounding box x/y coordinates as uint8
rather than float64. This effectively gives us a 256x256 uniform grid
that covers the page, which is high enough resolution for the usecase.
This commit is contained in:
Nicolò Ribaudo 2025-08-27 11:04:34 +02:00
parent e37a58f978
commit e4ea2e0c79
No known key found for this signature in database
GPG Key ID: AAFDA9101C58F338
10 changed files with 479 additions and 276 deletions

View File

@ -1242,8 +1242,8 @@ class PDFDocumentProxy {
* @property {boolean} [isEditing] - Render the page in editing mode.
* @property {boolean} [recordOperations] - Record the dependencies and bounding
* boxes of all PDF operations that render onto the canvas.
* @property {Set<number>} [filteredOperationIndexes] - If provided, only run
* the PDF operations that are included in this set.
* @property {(index: number) => boolean} [operationsFilter] - If provided, only
* run for which this function returns `true`.
*/
/**
@ -1314,7 +1314,7 @@ class PDFPageProxy {
this._intentStates = new Map();
this.destroyed = false;
this.recordedGroups = null;
this.recordedBBoxes = null;
}
/**
@ -1440,7 +1440,7 @@ class PDFPageProxy {
printAnnotationStorage = null,
isEditing = false,
recordOperations = false,
filteredOperationIndexes = null,
operationsFilter = null,
}) {
this._stats?.time("Overall");
@ -1487,23 +1487,28 @@ class PDFPageProxy {
this._pumpOperatorList(intentArgs);
}
const recordForDebugger = Boolean(
this._pdfBug && globalThis.StepperManager?.enabled
);
const shouldRecordOperations =
!this.recordedGroups &&
(recordOperations ||
(this._pdfBug && globalThis.StepperManager?.enabled));
!this.recordedBBoxes && (recordOperations || recordForDebugger);
const complete = error => {
intentState.renderTasks.delete(internalRenderTask);
if (shouldRecordOperations) {
const recordedGroups = internalRenderTask.gfx?.dependencyTracker.take();
if (recordedGroups) {
internalRenderTask.stepper?.setOperatorGroups(recordedGroups);
if (recordOperations) {
this.recordedGroups = recordedGroups;
const recordedBBoxes = internalRenderTask.gfx?.dependencyTracker.take();
if (recordedBBoxes) {
if (internalRenderTask.stepper) {
internalRenderTask.stepper.setOperatorBBoxes(
recordedBBoxes,
internalRenderTask.gfx.dependencyTracker.takeDebugMetadata()
);
}
if (recordOperations) {
this.recordedBBoxes = recordedBBoxes;
}
} else if (recordOperations) {
this.recordedGroups = [];
}
}
@ -1542,7 +1547,11 @@ class PDFPageProxy {
canvas,
canvasContext,
dependencyTracker: shouldRecordOperations
? new CanvasDependencyTracker(canvas)
? new CanvasDependencyTracker(
canvas,
intentState.operatorList.length,
recordForDebugger
)
: null,
viewport,
transform,
@ -1559,7 +1568,7 @@ class PDFPageProxy {
pdfBug: this._pdfBug,
pageColors,
enableHWA: this._transport.enableHWA,
filteredOperationIndexes,
operationsFilter,
});
(intentState.renderTasks ||= new Set()).add(internalRenderTask);
@ -3169,7 +3178,7 @@ class InternalRenderTask {
pdfBug = false,
pageColors = null,
enableHWA = false,
filteredOperationIndexes = null,
operationsFilter = null,
}) {
this.callback = callback;
this.params = params;
@ -3201,7 +3210,7 @@ class InternalRenderTask {
this._canvasContext = params.canvas ? null : params.canvasContext;
this._enableHWA = enableHWA;
this._dependencyTracker = params.dependencyTracker;
this._filteredOperationIndexes = filteredOperationIndexes;
this._operationsFilter = operationsFilter;
}
get completed() {
@ -3288,6 +3297,9 @@ class InternalRenderTask {
this.graphicsReadyCallback ||= this._continueBound;
return;
}
this.gfx.dependencyTracker?.growOperationsCount(
this.operatorList.fnArray.length
);
this.stepper?.updateOperatorList(this.operatorList);
if (this.running) {
@ -3328,7 +3340,7 @@ class InternalRenderTask {
this.operatorListIdx,
this._continueBound,
this.stepper,
this._filteredOperationIndexes
this._operationsFilter
);
if (this.operatorListIdx === this.operatorList.argsArray.length) {
this.running = false;

View File

@ -763,7 +763,7 @@ class CanvasGraphics {
executionStartIdx,
continueCallback,
stepper,
filteredOperationIndexes
operationsFilter
) {
const argsArray = operatorList.argsArray;
const fnArray = operatorList.fnArray;
@ -791,7 +791,7 @@ class CanvasGraphics {
return i;
}
if (!filteredOperationIndexes || filteredOperationIndexes.has(i)) {
if (!operationsFilter || operationsFilter(i)) {
fnId = fnArray[i];
// TODO: There is a `undefined` coming from somewhere.
fnArgs = argsArray[i] ?? null;
@ -1100,7 +1100,7 @@ class CanvasGraphics {
-offsetY,
]);
fillCtx.fillStyle = isPatternFill
? fillColor.getPattern(ctx, this, inverse, PathType.FILL)
? fillColor.getPattern(ctx, this, inverse, PathType.FILL, opIdx)
: fillColor;
fillCtx.fillRect(0, 0, width, height);
@ -1549,7 +1549,8 @@ class CanvasGraphics {
ctx,
this,
getCurrentTransformInverse(ctx),
PathType.STROKE
PathType.STROKE,
opIdx
);
if (baseTransform) {
const newPath = new Path2D();
@ -1603,7 +1604,8 @@ class CanvasGraphics {
ctx,
this,
getCurrentTransformInverse(ctx),
PathType.FILL
PathType.FILL,
opIdx
);
if (baseTransform) {
const newPath = new Path2D();
@ -1759,7 +1761,7 @@ class CanvasGraphics {
setFont(opIdx, fontRefName, size) {
this.dependencyTracker
?.recordSimpleData("font", opIdx)
.recordNamedDependency(opIdx, fontRefName);
.recordSimpleDataFromNamed("fontObj", fontRefName, opIdx);
const fontObj = this.commonObjs.get(fontRefName);
const current = this.current;
@ -2034,7 +2036,6 @@ class CanvasGraphics {
if (this.dependencyTracker) {
this.dependencyTracker
.recordDependencies(opIdx, Dependencies.showText)
.copyDependenciesFromIncrementalOperation(opIdx, "sameLineText")
.resetBBox(opIdx);
if (this.current.textRenderingMode & TextRenderingMode.ADD_TO_PATH_FLAG) {
this.dependencyTracker
@ -2047,9 +2048,7 @@ class CanvasGraphics {
const font = current.font;
if (font.isType3Font) {
this.showType3Text(opIdx, glyphs);
this.dependencyTracker
?.recordOperation(opIdx)
.recordIncrementalData("sameLineText", opIdx);
this.dependencyTracker?.recordShowTextOperation(opIdx);
return undefined;
}
@ -2095,7 +2094,8 @@ class CanvasGraphics {
ctx,
this,
getCurrentTransformInverse(ctx),
PathType.FILL
PathType.FILL,
opIdx
);
patternFillTransform = getCurrentTransform(ctx);
ctx.restore();
@ -2108,7 +2108,8 @@ class CanvasGraphics {
ctx,
this,
getCurrentTransformInverse(ctx),
PathType.STROKE
PathType.STROKE,
opIdx
);
patternStrokeTransform = getCurrentTransform(ctx);
ctx.restore();
@ -2157,8 +2158,7 @@ class CanvasGraphics {
-measure.actualBoundingBoxAscent,
measure.actualBoundingBoxDescent
)
.recordOperation(opIdx)
.recordIncrementalData("sameLineText", opIdx);
.recordShowTextOperation(opIdx);
}
current.x += width * widthAdvanceScale * textHScale;
ctx.restore();
@ -2277,9 +2277,7 @@ class CanvasGraphics {
ctx.restore();
this.compose();
this.dependencyTracker
?.recordOperation(opIdx)
.recordIncrementalData("sameLineText", opIdx);
this.dependencyTracker?.recordShowTextOperation(opIdx);
return undefined;
}
@ -2351,7 +2349,6 @@ class CanvasGraphics {
}
ctx.restore();
if (dependencyTracker) {
this.dependencyTracker.recordNestedDependencies();
this.dependencyTracker = dependencyTracker;
}
}
@ -2378,7 +2375,7 @@ class CanvasGraphics {
if (IR[0] === "TilingPattern") {
const baseTransform = this.baseTransform || getCurrentTransform(this.ctx);
const canvasGraphicsFactory = {
createCanvasGraphics: ctx =>
createCanvasGraphics: (ctx, renderingOpIdx) =>
new CanvasGraphics(
ctx,
this.commonObjs,
@ -2392,7 +2389,11 @@ class CanvasGraphics {
undefined,
undefined,
this.dependencyTracker
? new CanvasNestedDependencyTracker(this.dependencyTracker, opIdx)
? new CanvasNestedDependencyTracker(
this.dependencyTracker,
renderingOpIdx,
/* ignoreBBoxes */ true
)
: null
),
};
@ -2470,7 +2471,8 @@ class CanvasGraphics {
ctx,
this,
getCurrentTransformInverse(ctx),
PathType.SHADING
PathType.SHADING,
opIdx
);
const inv = getCurrentTransformInverse(ctx);
@ -2937,7 +2939,8 @@ class CanvasGraphics {
maskCtx,
this,
getCurrentTransformInverse(ctx),
PathType.FILL
PathType.FILL,
opIdx
)
: fillColor;
maskCtx.fillRect(0, 0, width, height);

View File

@ -2,10 +2,70 @@ import { 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) => {
if (!map) {
return undefined;
}
let value = map.get(key);
if (!value) {
value = { dependencies: new Set(), isRenderingOperation: false };
map.set(key, value);
}
return value;
};
/**
* @typedef {"lineWidth" | "lineCap" | "lineJoin" | "miterLimit" | "dash" |
* "strokeAlpha" | "fillColor" | "fillAlpha" | "globalCompositeOperation" |
* "path" | "filter"} SimpleDependency
* "path" | "filter" | "font" | "fontObj"} SimpleDependency
*/
/**
@ -54,9 +114,38 @@ class CanvasDependencyTracker {
#canvasHeight;
constructor(canvas) {
// Uint8ClampedArray<minX, minY, maxX, maxY>
#bboxesCoords;
#bboxes;
#debugMetadata;
constructor(canvas, operationsCount, recordDebugMetadata = false) {
this.#canvasWidth = canvas.width;
this.#canvasHeight = canvas.height;
this.#initializeBBoxes(operationsCount);
if (recordDebugMetadata) {
this.#debugMetadata = new Map();
}
}
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);
}
}
save(opIdx) {
@ -71,7 +160,7 @@ class CanvasDependencyTracker {
},
};
this.#clipBox = { __proto__: this.#clipBox };
this.#savesStack.push([opIdx, null]);
this.#savesStack.push(opIdx);
return this;
}
@ -87,9 +176,12 @@ class CanvasDependencyTracker {
this.#incremental = Object.getPrototypeOf(this.#incremental);
this.#clipBox = Object.getPrototypeOf(this.#clipBox);
const lastPair = this.#savesStack.pop();
if (lastPair !== undefined) {
lastPair[1] = opIdx;
const lastSave = this.#savesStack.pop();
if (lastSave !== undefined) {
ensureDebugMetadata(this.#debugMetadata, opIdx)?.dependencies.add(
lastSave
);
this.#bboxes[opIdx] = this.#bboxes[lastSave];
}
return this;
@ -99,7 +191,7 @@ class CanvasDependencyTracker {
* @param {number} idx
*/
recordOpenMarker(idx) {
this.#savesStack.push([idx, null]);
this.#savesStack.push(idx);
return this;
}
@ -107,13 +199,16 @@ class CanvasDependencyTracker {
if (this.#savesStack.length === 0) {
return null;
}
return this.#savesStack.at(-1)[0];
return this.#savesStack.at(-1);
}
recordCloseMarker(idx) {
const lastPair = this.#savesStack.pop();
if (lastPair !== undefined) {
lastPair[1] = idx;
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;
}
@ -121,14 +216,17 @@ class CanvasDependencyTracker {
// Marked content needs a separate stack from save/restore, because they
// form two independent trees.
beginMarkedContent(opIdx) {
this.#markedContentStack.push([opIdx, null]);
this.#markedContentStack.push(opIdx);
return this;
}
endMarkedContent(opIdx) {
const lastPair = this.#markedContentStack.pop();
if (lastPair !== undefined) {
lastPair[1] = opIdx;
const lastSave = this.#markedContentStack.pop();
if (lastSave !== undefined) {
ensureDebugMetadata(this.#debugMetadata, opIdx)?.dependencies.add(
lastSave
);
this.#bboxes[opIdx] = this.#bboxes[lastSave];
}
return this;
}
@ -182,6 +280,15 @@ class CanvasDependencyTracker {
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);
@ -207,16 +314,14 @@ 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;
return this;
}
get hasPendingBBox() {
return this.#pendingBBoxIdx !== -1;
return this;
}
recordClipBox(idx, ctx, minX, maxX, minY, maxY) {
@ -384,20 +489,6 @@ class CanvasDependencyTracker {
return this;
}
copyDependenciesFromIncrementalOperation(idx, name) {
const operations = this.#operations;
const pendingDependencies = this.#pendingDependencies;
for (const depIdx of this.#incremental[name]) {
operations
.get(depIdx)
.dependencies.forEach(
pendingDependencies.add,
pendingDependencies.add(depIdx)
);
}
return this;
}
recordNamedDependency(idx, name) {
if (this.#namedDependencies.has(name)) {
this.#pendingDependencies.add(this.#namedDependencies.get(name));
@ -409,38 +500,74 @@ class CanvasDependencyTracker {
/**
* @param {number} idx
*/
recordOperation(idx, preserveBbox = false) {
recordOperation(idx, preserve = false) {
this.recordDependencies(idx, [FORCED_DEPENDENCY_LABEL]);
const dependencies = new Set(this.#pendingDependencies);
const pairs = this.#savesStack.concat(this.#markedContentStack);
const bbox =
this.#pendingBBoxIdx === idx
? {
minX: this.#pendingBBox[0],
minY: this.#pendingBBox[1],
maxX: this.#pendingBBox[2],
maxY: this.#pendingBBox[3],
if (this.#debugMetadata) {
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);
dependencies.delete(idx);
metadata.isRenderingOperation = true;
}
: null;
this.#operations.set(idx, { bbox, pairs, dependencies });
if (!preserveBbox) {
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);
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);
}
}
if (!preserve) {
this.#pendingDependencies.clear();
this.#pendingBBoxIdx = -1;
}
this.#pendingDependencies.clear();
}
return this;
}
bboxToClipBoxDropOperation(idx) {
if (this.#pendingBBoxIdx !== -1) {
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) {
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();
}
}
return this;
}
@ -464,21 +591,11 @@ class CanvasDependencyTracker {
take() {
this.#fontBBoxTrustworthy.clear();
return Array.from(
this.#operations,
([idx, { bbox, pairs, dependencies }]) => {
pairs.forEach(pair => pair.forEach(dependencies.add, dependencies));
dependencies.delete(idx);
return {
minX: (bbox?.minX ?? 0) / this.#canvasWidth,
maxX: (bbox?.maxX ?? this.#canvasWidth) / this.#canvasWidth,
minY: (bbox?.minY ?? 0) / this.#canvasHeight,
maxY: (bbox?.maxY ?? this.#canvasHeight) / this.#canvasHeight,
dependencies: Array.from(dependencies).sort((a, b) => a - b),
idx,
};
return new BBoxReader(this.#bboxes, this.#bboxesCoords);
}
);
takeDebugMetadata() {
return this.#debugMetadata;
}
}
@ -496,14 +613,17 @@ class CanvasNestedDependencyTracker {
/** @type {number} */
#opIdx;
#nestingLevel = 0;
#ignoreBBoxes;
#outerDependencies;
#nestingLevel = 0;
#savesLevel = 0;
constructor(dependencyTracker, opIdx) {
if (dependencyTracker instanceof CanvasNestedDependencyTracker) {
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.
@ -511,8 +631,12 @@ class CanvasNestedDependencyTracker {
}
this.#dependencyTracker = dependencyTracker;
this.#outerDependencies = dependencyTracker._takePendingDependencies();
this.#opIdx = opIdx;
this.#ignoreBBoxes = !!ignoreBBoxes;
}
growOperationsCount() {
throw new Error("Unreachable");
}
save(opIdx) {
@ -595,6 +719,20 @@ class CanvasNestedDependencyTracker {
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);
@ -614,17 +752,14 @@ class CanvasNestedDependencyTracker {
}
resetBBox(idx) {
if (!this.#dependencyTracker.hasPendingBBox) {
if (!this.#ignoreBBoxes) {
this.#dependencyTracker.resetBBox(this.#opIdx);
}
return this;
}
get hasPendingBBox() {
return this.#dependencyTracker.hasPendingBBox;
}
recordClipBox(idx, ctx, minX, maxX, minY, maxY) {
if (!this.#ignoreBBoxes) {
this.#dependencyTracker.recordClipBox(
this.#opIdx,
ctx,
@ -633,10 +768,12 @@ class CanvasNestedDependencyTracker {
minY,
maxY
);
}
return this;
}
recordBBox(idx, ctx, minX, maxX, minY, maxY) {
if (!this.#ignoreBBoxes) {
this.#dependencyTracker.recordBBox(
this.#opIdx,
ctx,
@ -645,10 +782,12 @@ class CanvasNestedDependencyTracker {
minY,
maxY
);
}
return this;
}
recordCharacterBBox(idx, ctx, font, scale, x, y, getMeasure) {
if (!this.#ignoreBBoxes) {
this.#dependencyTracker.recordCharacterBBox(
this.#opIdx,
ctx,
@ -658,11 +797,14 @@ class CanvasNestedDependencyTracker {
y,
getMeasure
);
}
return this;
}
recordFullPageBBox(idx) {
if (!this.#ignoreBBoxes) {
this.#dependencyTracker.recordFullPageBBox(this.#opIdx);
}
return this;
}
@ -675,14 +817,6 @@ class CanvasNestedDependencyTracker {
return this;
}
copyDependenciesFromIncrementalOperation(idx, name) {
this.#dependencyTracker.copyDependenciesFromIncrementalOperation(
this.#opIdx,
name
);
return this;
}
recordNamedDependency(idx, name) {
this.#dependencyTracker.recordNamedDependency(this.#opIdx, name);
return this;
@ -694,25 +828,26 @@ class CanvasNestedDependencyTracker {
*/
recordOperation(idx) {
this.#dependencyTracker.recordOperation(this.#opIdx, true);
const operation = this.#dependencyTracker._extractOperation(this.#opIdx);
for (const depIdx of operation.dependencies) {
this.#outerDependencies.add(depIdx);
return this;
}
this.#outerDependencies.delete(this.#opIdx);
this.#outerDependencies.delete(null);
recordShowTextOperation(idx) {
this.#dependencyTracker.recordShowTextOperation(this.#opIdx, true);
return this;
}
bboxToClipBoxDropOperation(idx) {
this.#dependencyTracker.bboxToClipBoxDropOperation(this.#opIdx);
if (!this.#ignoreBBoxes) {
this.#dependencyTracker.bboxToClipBoxDropOperation(this.#opIdx, true);
}
return this;
}
recordNestedDependencies() {
this.#dependencyTracker._pushPendingDependencies(this.#outerDependencies);
take() {
throw new Error("Unreachable");
}
take() {
takeDebugMetadata() {
throw new Error("Unreachable");
}
}
@ -759,6 +894,7 @@ const Dependencies = {
"moveText",
"textMatrix",
"font",
"fontObj",
"filter",
"fillColor",
"textRenderingMode",
@ -766,7 +902,8 @@ const Dependencies = {
"fillAlpha",
"strokeAlpha",
"globalCompositeOperation",
// TODO: More
"sameLineText",
],
transform: ["transform"],
transformAndFill: ["transform", "fillColor"],

View File

@ -478,7 +478,7 @@ class TilingPattern {
this.baseTransform = baseTransform;
}
createPatternCanvas(owner) {
createPatternCanvas(owner, opIdx) {
const {
bbox,
operatorList,
@ -567,7 +567,7 @@ class TilingPattern {
dimy.size
);
const tmpCtx = tmpCanvas.context;
const graphics = canvasGraphicsFactory.createCanvasGraphics(tmpCtx);
const graphics = canvasGraphicsFactory.createCanvasGraphics(tmpCtx, opIdx);
graphics.groupLevel = owner.groupLevel;
this.setFillAndStrokeStyleToContext(graphics, paintType, color);
@ -600,7 +600,7 @@ class TilingPattern {
graphics.endDrawing();
graphics.dependencyTracker?.restore().recordNestedDependencies?.();
graphics.dependencyTracker?.restore();
tmpCtx.restore();
if (redrawHorizontally || redrawVertically) {
@ -726,7 +726,7 @@ class TilingPattern {
return false;
}
getPattern(ctx, owner, inverse, pathType) {
getPattern(ctx, owner, inverse, pathType, opIdx) {
// PDF spec 8.7.2 NOTE 1: pattern's matrix is relative to initial matrix.
let matrix = inverse;
if (pathType !== PathType.SHADING) {
@ -736,7 +736,7 @@ class TilingPattern {
}
}
const temporaryPatternCanvas = this.createPatternCanvas(owner);
const temporaryPatternCanvas = this.createPatternCanvas(owner, opIdx);
let domMatrix = new DOMMatrix(matrix);
// Rescale and so that the ctx.createPattern call generates a pattern with

View File

@ -1117,28 +1117,7 @@ class Driver {
const baseline = ctx.canvas.toDataURL("image/png");
this._clearCanvas();
const filteredIndexes = new Set();
// TODO: This logic is copy-psated from PDFPageDetailView.
// We should export it instead, because even though it's
// not the core logic of partial rendering it is still
// relevant
const recordedGroups = page.recordedGroups;
for (let i = 0, ii = recordedGroups.length; i < ii; i++) {
const group = recordedGroups[i];
if (
group.minX <= partialCrop.maxX &&
group.maxX >= partialCrop.minX &&
group.minY <= partialCrop.maxY &&
group.maxY >= partialCrop.minY
) {
filteredIndexes.add(group.idx);
group.dependencies.forEach(
filteredIndexes.add,
filteredIndexes
);
}
}
const recordedBBoxes = page.recordedBBoxes;
const partialRenderContext = {
canvasContext: ctx,
@ -1149,7 +1128,17 @@ class Driver {
pageColors,
transform,
recordOperations: false,
filteredOperationIndexes: filteredIndexes,
operationsFilter(index) {
if (recordedBBoxes.isEmpty(index)) {
return false;
}
return (
recordedBBoxes.minX(index) <= partialCrop.maxX &&
recordedBBoxes.maxX(index) >= partialCrop.minX &&
recordedBBoxes.minY(index) <= partialCrop.maxY &&
recordedBBoxes.maxY(index) >= partialCrop.minY
);
},
};
const partialRenderTask = page.render(partialRenderContext);

View File

@ -541,8 +541,7 @@
"maxX": 0.5,
"minY": 0.25,
"maxY": 0.5
},
"knownPartialMismatch": true
}
},
{
"id": "bug1753983",
@ -673,8 +672,7 @@
"maxX": 0.5,
"minY": 0.25,
"maxY": 0.5
},
"knownPartialMismatch": true
}
},
{
"id": "bug1473809",
@ -780,8 +778,7 @@
"maxX": 0.5,
"minY": 0.25,
"maxY": 0.5
},
"knownPartialMismatch": "Gives slightly different results in Firefox when running in headless mode"
}
},
{
"id": "issue2128",
@ -1556,8 +1553,7 @@
"maxX": 0.5,
"minY": 0.25,
"maxY": 0.5
},
"knownPartialMismatch": "Gives slightly different results in Firefox when running in headless mode"
}
},
{
"id": "bug1811668",
@ -2904,8 +2900,7 @@
"maxX": 0.5,
"minY": 0.25,
"maxY": 0.5
},
"knownPartialMismatch": true
}
},
{
"id": "bug1245391-text",
@ -3250,8 +3245,7 @@
"maxX": 0.5,
"minY": 0.25,
"maxY": 0.5
},
"knownPartialMismatch": true
}
},
{
"id": "bug1337429",
@ -8080,8 +8074,7 @@
"maxX": 0.5,
"minY": 0.25,
"maxY": 0.5
},
"knownPartialMismatch": "Half pixel rendering at the edge of the image"
}
},
{
"id": "issue16454",
@ -11270,8 +11263,7 @@
"maxX": 0.5,
"minY": 0.25,
"maxY": 0.5
},
"knownPartialMismatch": "One-bit difference in two pixels at the edge"
}
},
{
"id": "issue14724",

View File

@ -44,7 +44,7 @@ class BasePDFPageView {
pageColors = null;
recordedGroups = null;
recordedBBoxes = null;
renderingQueue = null;
@ -235,7 +235,7 @@ class BasePDFPageView {
if (renderTask === this.renderTask) {
this.renderTask = null;
if (this.enableOptimizedPartialRendering) {
this.recordedGroups ??= renderTask.recordedGroups;
this.recordedBBoxes ??= renderTask.recordedBBoxes;
}
}
}

View File

@ -295,7 +295,7 @@ class Stepper {
this.currentIdx = -1;
this.operatorListIdx = 0;
this.indentLevel = 0;
this.operatorGroups = null;
this.operatorsGroups = null;
this.pageContainer = pageContainer;
}
@ -439,7 +439,7 @@ class Stepper {
this.table.append(chunk);
}
setOperatorGroups(groups) {
setOperatorBBoxes(bboxes, metadata) {
let boxesContainer = this.pageContainer.querySelector(".pdfBugGroupsLayer");
if (!boxesContainer) {
boxesContainer = this.#c("div");
@ -457,12 +457,55 @@ class Stepper {
}
boxesContainer.innerHTML = "";
groups = groups.toSorted((a, b) => {
const dependents = new Map();
for (const [dependentIdx, { dependencies: ownDependencies }] of metadata) {
for (const dependencyIdx of ownDependencies) {
let ownDependents = dependents.get(dependencyIdx);
if (!ownDependents) {
dependents.set(dependencyIdx, (ownDependents = new Set()));
}
ownDependents.add(dependentIdx);
}
}
const groups = Array.from({ length: bboxes.length }, (_, i) => {
let minX = null;
let minY = null;
let maxX = null;
let maxY = null;
if (!bboxes.isEmpty(i)) {
minX = bboxes.minX(i);
minY = bboxes.minY(i);
maxX = bboxes.maxX(i);
maxY = bboxes.maxY(i);
}
return {
minX,
minY,
maxX,
maxY,
dependencies: Array.from(metadata.get(i)?.dependencies ?? []).sort(),
dependents: Array.from(dependents.get(i) ?? []).sort(),
isRenderingOperation: metadata.get(i)?.isRenderingOperation ?? false,
idx: i,
};
});
this.operatorsGroups = groups;
const operatorsGroupsByZindex = groups.toSorted((a, b) => {
if (a.minX === null) {
return b.minX === null ? 0 : 1;
}
if (b.minX === null) {
return -1;
}
const diffs = [
a.minX - b.minX,
a.minY - b.minY,
b.maxX - a.maxX,
a.minX - b.minX,
b.maxY - a.maxY,
b.maxX - a.maxX,
];
for (const diff of diffs) {
if (Math.abs(diff) > 0.01) {
@ -476,18 +519,20 @@ class Stepper {
}
return 0;
});
this.operatorGroups = groups;
for (let i = 0; i < groups.length; i++) {
for (let i = 0; i < operatorsGroupsByZindex.length; i++) {
const group = operatorsGroupsByZindex[i];
if (group.minX !== null) {
const el = this.#c("div");
el.style.left = `${groups[i].minX * 100}%`;
el.style.top = `${groups[i].minY * 100}%`;
el.style.width = `${(groups[i].maxX - groups[i].minX) * 100}%`;
el.style.height = `${(groups[i].maxY - groups[i].minY) * 100}%`;
el.dataset.groupIdx = i;
el.style.left = `${group.minX * 100}%`;
el.style.top = `${group.minY * 100}%`;
el.style.width = `${(group.maxX - group.minX) * 100}%`;
el.style.height = `${(group.maxY - group.minY) * 100}%`;
el.dataset.idx = group.idx;
boxesContainer.append(el);
}
}
}
#handleStepHover(e) {
const tr = e.target.closest("tr");
@ -496,51 +541,85 @@ class Stepper {
}
const index = +tr.dataset.idx;
const closestGroupIndex =
this.operatorGroups?.findIndex(({ idx }) => idx === index) ?? -1;
if (closestGroupIndex === -1) {
this.hoverStyle.innerText = "";
return;
}
this.#highlightStepsGroup(closestGroupIndex);
this.#highlightStepsGroup(index);
}
#handleDebugBoxHover(e) {
if (e.target.dataset.groupIdx === undefined) {
if (e.target.dataset.idx === undefined) {
return;
}
const groupIdx = Number(e.target.dataset.groupIdx);
this.#highlightStepsGroup(groupIdx);
const idx = Number(e.target.dataset.idx);
this.#highlightStepsGroup(idx);
}
#handleDebugBoxClick(e) {
if (e.target.dataset.groupIdx === undefined) {
if (e.target.dataset.idx === undefined) {
return;
}
const groupIdx = Number(e.target.dataset.groupIdx);
const group = this.operatorGroups[groupIdx];
const idx = Number(e.target.dataset.idx);
this.table.childNodes[group.idx].scrollIntoView();
this.table.childNodes[idx].scrollIntoView();
}
#highlightStepsGroup(groupIndex) {
const group = this.operatorGroups[groupIndex];
#highlightStepsGroup(index) {
const group = this.operatorsGroups?.[index];
if (!group) {
return;
}
this.hoverStyle.innerText = `#${this.panel.id} tr[data-idx="${group.idx}"] { background-color: rgba(0, 0, 0, 0.1); }`;
const renderingColor = `rgba(0, 0, 0, 0.1)`;
const dependencyColor = `rgba(0, 255, 255, 0.1)`;
const dependentColor = `rgba(255, 0, 0, 0.1)`;
const solid = color => `background-color: ${color}`;
// Used for operations that have an empty bounding box
const striped = color => `
background-image: repeating-linear-gradient(
-45deg,
white,
white 10px,
${color} 10px,
${color} 20px
)
`;
const select = idx => `#${this.panel.id} tr[data-idx="${idx}"]`;
const selectN = idxs =>
idxs.length === 0 ? "q:not(q)" : idxs.map(select).join(", ");
const isEmpty = idx =>
!this.operatorsGroups[idx] || this.operatorsGroups[idx].minX === null;
const selfColor = group.isRenderingOperation
? renderingColor
: dependentColor;
this.hoverStyle.innerText = `${select(index)} {
${group.minX === null ? striped(selfColor) : solid(selfColor)}
}`;
if (group.dependencies.length > 0) {
const selector = group.dependencies
.map(idx => `#${this.panel.id} tr[data-idx="${idx}"]`)
.join(", ");
this.hoverStyle.innerText += `${selector} { background-color: rgba(0, 255, 255, 0.1); }`;
this.hoverStyle.innerText += `
${selectN(group.dependencies.filter(idx => !isEmpty(idx)))} {
${solid(dependencyColor)}
}
${selectN(group.dependencies.filter(isEmpty))} {
${striped(dependencyColor)}
}`;
}
if (group.dependents.length > 0) {
this.hoverStyle.innerText += `
${selectN(group.dependents.filter(idx => !isEmpty(idx)))} {
${solid(dependentColor)}
}
${selectN(group.dependents.filter(isEmpty))} {
${striped(dependentColor)}
}`;
}
this.hoverStyle.innerText += `
#viewer [data-page-number="${this.pageIndex + 1}"] .pdfBugGroupsLayer :nth-child(${groupIndex + 1}) {
#viewer [data-page-number="${this.pageIndex + 1}"] .pdfBugGroupsLayer [data-idx="${index}"] {
background-color: var(--hover-background-color);
outline-style: var(--hover-outline-style);
}

View File

@ -188,18 +188,12 @@ class PDFPageDetailView extends BasePDFPageView {
_getRenderingContext(canvas, transform) {
const baseContext = this.pageView._getRenderingContext(canvas, transform);
const recordedGroups = this.pdfPage.recordedGroups;
const recordedBBoxes = this.pdfPage.recordedBBoxes;
if (!recordedGroups || !this.enableOptimizedPartialRendering) {
if (!recordedBBoxes || !this.enableOptimizedPartialRendering) {
return { ...baseContext, recordOperations: false };
}
// TODO: There is probably a better data structure for this.
// The indexes are always checked in increasing order, so we can just try
// to build a pre-sorted array which should have faster lookups.
// Needs benchmarking.
const filteredIndexes = new Set();
const {
viewport: { width: vWidth, height: vHeight },
} = this.pageView;
@ -215,23 +209,20 @@ class PDFPageDetailView extends BasePDFPageView {
const detailMaxX = (aMinX + aWidth) / vWidth;
const detailMaxY = (aMinY + aHeight) / vHeight;
for (let i = 0, ii = recordedGroups.length; i < ii; i++) {
const group = recordedGroups[i];
if (
group.minX <= detailMaxX &&
group.maxX >= detailMinX &&
group.minY <= detailMaxY &&
group.maxY >= detailMinY
) {
filteredIndexes.add(group.idx);
group.dependencies.forEach(filteredIndexes.add, filteredIndexes);
}
}
return {
...baseContext,
recordOperations: false,
filteredOperationIndexes: filteredIndexes,
operationsFilter(index) {
if (recordedBBoxes.isEmpty(index)) {
return false;
}
return (
recordedBBoxes.minX(index) <= detailMaxX &&
recordedBBoxes.maxX(index) >= detailMinX &&
recordedBBoxes.minY(index) <= detailMaxY &&
recordedBBoxes.maxY(index) >= detailMinY
);
},
};
}

View File

@ -934,7 +934,7 @@ class PDFPageView extends BasePDFPageView {
pageColors: this.pageColors,
isEditing: this.#isEditing,
recordOperations:
this.enableOptimizedPartialRendering && !this.recordedGroups,
this.enableOptimizedPartialRendering && !this.recordedBBoxes,
};
}