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

View File

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

View File

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

View File

@ -478,7 +478,7 @@ class TilingPattern {
this.baseTransform = baseTransform; this.baseTransform = baseTransform;
} }
createPatternCanvas(owner) { createPatternCanvas(owner, opIdx) {
const { const {
bbox, bbox,
operatorList, operatorList,
@ -567,7 +567,7 @@ class TilingPattern {
dimy.size dimy.size
); );
const tmpCtx = tmpCanvas.context; const tmpCtx = tmpCanvas.context;
const graphics = canvasGraphicsFactory.createCanvasGraphics(tmpCtx); const graphics = canvasGraphicsFactory.createCanvasGraphics(tmpCtx, opIdx);
graphics.groupLevel = owner.groupLevel; graphics.groupLevel = owner.groupLevel;
this.setFillAndStrokeStyleToContext(graphics, paintType, color); this.setFillAndStrokeStyleToContext(graphics, paintType, color);
@ -600,7 +600,7 @@ class TilingPattern {
graphics.endDrawing(); graphics.endDrawing();
graphics.dependencyTracker?.restore().recordNestedDependencies?.(); graphics.dependencyTracker?.restore();
tmpCtx.restore(); tmpCtx.restore();
if (redrawHorizontally || redrawVertically) { if (redrawHorizontally || redrawVertically) {
@ -726,7 +726,7 @@ class TilingPattern {
return false; 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. // PDF spec 8.7.2 NOTE 1: pattern's matrix is relative to initial matrix.
let matrix = inverse; let matrix = inverse;
if (pathType !== PathType.SHADING) { 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); let domMatrix = new DOMMatrix(matrix);
// Rescale and so that the ctx.createPattern call generates a pattern with // 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"); const baseline = ctx.canvas.toDataURL("image/png");
this._clearCanvas(); this._clearCanvas();
const filteredIndexes = new Set(); const recordedBBoxes = page.recordedBBoxes;
// 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 partialRenderContext = { const partialRenderContext = {
canvasContext: ctx, canvasContext: ctx,
@ -1149,7 +1128,17 @@ class Driver {
pageColors, pageColors,
transform, transform,
recordOperations: false, 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); const partialRenderTask = page.render(partialRenderContext);

View File

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

View File

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

View File

@ -295,7 +295,7 @@ class Stepper {
this.currentIdx = -1; this.currentIdx = -1;
this.operatorListIdx = 0; this.operatorListIdx = 0;
this.indentLevel = 0; this.indentLevel = 0;
this.operatorGroups = null; this.operatorsGroups = null;
this.pageContainer = pageContainer; this.pageContainer = pageContainer;
} }
@ -439,7 +439,7 @@ class Stepper {
this.table.append(chunk); this.table.append(chunk);
} }
setOperatorGroups(groups) { setOperatorBBoxes(bboxes, metadata) {
let boxesContainer = this.pageContainer.querySelector(".pdfBugGroupsLayer"); let boxesContainer = this.pageContainer.querySelector(".pdfBugGroupsLayer");
if (!boxesContainer) { if (!boxesContainer) {
boxesContainer = this.#c("div"); boxesContainer = this.#c("div");
@ -457,12 +457,55 @@ class Stepper {
} }
boxesContainer.innerHTML = ""; 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 = [ const diffs = [
a.minX - b.minX,
a.minY - b.minY, a.minY - b.minY,
b.maxX - a.maxX, a.minX - b.minX,
b.maxY - a.maxY, b.maxY - a.maxY,
b.maxX - a.maxX,
]; ];
for (const diff of diffs) { for (const diff of diffs) {
if (Math.abs(diff) > 0.01) { if (Math.abs(diff) > 0.01) {
@ -476,18 +519,20 @@ class Stepper {
} }
return 0; 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"); const el = this.#c("div");
el.style.left = `${groups[i].minX * 100}%`; el.style.left = `${group.minX * 100}%`;
el.style.top = `${groups[i].minY * 100}%`; el.style.top = `${group.minY * 100}%`;
el.style.width = `${(groups[i].maxX - groups[i].minX) * 100}%`; el.style.width = `${(group.maxX - group.minX) * 100}%`;
el.style.height = `${(groups[i].maxY - groups[i].minY) * 100}%`; el.style.height = `${(group.maxY - group.minY) * 100}%`;
el.dataset.groupIdx = i; el.dataset.idx = group.idx;
boxesContainer.append(el); boxesContainer.append(el);
} }
} }
}
#handleStepHover(e) { #handleStepHover(e) {
const tr = e.target.closest("tr"); const tr = e.target.closest("tr");
@ -496,51 +541,85 @@ class Stepper {
} }
const index = +tr.dataset.idx; const index = +tr.dataset.idx;
this.#highlightStepsGroup(index);
const closestGroupIndex =
this.operatorGroups?.findIndex(({ idx }) => idx === index) ?? -1;
if (closestGroupIndex === -1) {
this.hoverStyle.innerText = "";
return;
}
this.#highlightStepsGroup(closestGroupIndex);
} }
#handleDebugBoxHover(e) { #handleDebugBoxHover(e) {
if (e.target.dataset.groupIdx === undefined) { if (e.target.dataset.idx === undefined) {
return; return;
} }
const groupIdx = Number(e.target.dataset.groupIdx); const idx = Number(e.target.dataset.idx);
this.#highlightStepsGroup(groupIdx); this.#highlightStepsGroup(idx);
} }
#handleDebugBoxClick(e) { #handleDebugBoxClick(e) {
if (e.target.dataset.groupIdx === undefined) { if (e.target.dataset.idx === undefined) {
return; return;
} }
const groupIdx = Number(e.target.dataset.groupIdx); const idx = Number(e.target.dataset.idx);
const group = this.operatorGroups[groupIdx];
this.table.childNodes[group.idx].scrollIntoView(); this.table.childNodes[idx].scrollIntoView();
} }
#highlightStepsGroup(groupIndex) { #highlightStepsGroup(index) {
const group = this.operatorGroups[groupIndex]; 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) { if (group.dependencies.length > 0) {
const selector = group.dependencies this.hoverStyle.innerText += `
.map(idx => `#${this.panel.id} tr[data-idx="${idx}"]`) ${selectN(group.dependencies.filter(idx => !isEmpty(idx)))} {
.join(", "); ${solid(dependencyColor)}
this.hoverStyle.innerText += `${selector} { background-color: rgba(0, 255, 255, 0.1); }`; }
${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 += ` 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); background-color: var(--hover-background-color);
outline-style: var(--hover-outline-style); outline-style: var(--hover-outline-style);
} }

View File

@ -188,18 +188,12 @@ class PDFPageDetailView extends BasePDFPageView {
_getRenderingContext(canvas, transform) { _getRenderingContext(canvas, transform) {
const baseContext = this.pageView._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 }; 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 { const {
viewport: { width: vWidth, height: vHeight }, viewport: { width: vWidth, height: vHeight },
} = this.pageView; } = this.pageView;
@ -215,23 +209,20 @@ class PDFPageDetailView extends BasePDFPageView {
const detailMaxX = (aMinX + aWidth) / vWidth; const detailMaxX = (aMinX + aWidth) / vWidth;
const detailMaxY = (aMinY + aHeight) / vHeight; 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 { return {
...baseContext, ...baseContext,
recordOperations: false, 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, pageColors: this.pageColors,
isEditing: this.#isEditing, isEditing: this.#isEditing,
recordOperations: recordOperations:
this.enableOptimizedPartialRendering && !this.recordedGroups, this.enableOptimizedPartialRendering && !this.recordedBBoxes,
}; };
} }