Merge pull request #20208 from nicolo-ribaudo/bboxes-typedarray

Store ops bboxes in a linear Uint8Array
This commit is contained in:
calixteman 2025-09-12 14:52:42 +02:00 committed by GitHub
commit beb5f5ca85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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,18 +314,16 @@ class CanvasDependencyTracker {
}
resetBBox(idx) {
this.#pendingBBoxIdx = idx;
this.#pendingBBox[0] = Infinity;
this.#pendingBBox[1] = Infinity;
this.#pendingBBox[2] = -Infinity;
this.#pendingBBox[3] = -Infinity;
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;
}
recordClipBox(idx, ctx, minX, maxX, minY, maxY) {
const transform = Util.multiplyByDOMMatrix(
this.#baseTransformStack.at(-1),
@ -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],
}
: null;
this.#operations.set(idx, { bbox, pairs, dependencies });
if (!preserveBbox) {
this.#pendingBBoxIdx = -1;
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;
}
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();
}
}
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,55 +752,59 @@ 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) {
this.#dependencyTracker.recordClipBox(
this.#opIdx,
ctx,
minX,
maxX,
minY,
maxY
);
if (!this.#ignoreBBoxes) {
this.#dependencyTracker.recordClipBox(
this.#opIdx,
ctx,
minX,
maxX,
minY,
maxY
);
}
return this;
}
recordBBox(idx, ctx, minX, maxX, minY, maxY) {
this.#dependencyTracker.recordBBox(
this.#opIdx,
ctx,
minX,
maxX,
minY,
maxY
);
if (!this.#ignoreBBoxes) {
this.#dependencyTracker.recordBBox(
this.#opIdx,
ctx,
minX,
maxX,
minY,
maxY
);
}
return this;
}
recordCharacterBBox(idx, ctx, font, scale, x, y, getMeasure) {
this.#dependencyTracker.recordCharacterBBox(
this.#opIdx,
ctx,
font,
scale,
x,
y,
getMeasure
);
if (!this.#ignoreBBoxes) {
this.#dependencyTracker.recordCharacterBBox(
this.#opIdx,
ctx,
font,
scale,
x,
y,
getMeasure
);
}
return this;
}
recordFullPageBBox(idx) {
this.#dependencyTracker.recordFullPageBBox(this.#opIdx);
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);
}
this.#outerDependencies.delete(this.#opIdx);
this.#outerDependencies.delete(null);
return this;
}
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,16 +519,18 @@ class Stepper {
}
return 0;
});
this.operatorGroups = groups;
for (let i = 0; i < groups.length; i++) {
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;
boxesContainer.append(el);
for (let i = 0; i < operatorsGroupsByZindex.length; i++) {
const group = operatorsGroupsByZindex[i];
if (group.minX !== null) {
const el = this.#c("div");
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);
}
}
}
@ -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

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