diff --git a/src/display/api.js b/src/display/api.js index 54612a5c9..086755f82 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -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} [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; diff --git a/src/display/canvas.js b/src/display/canvas.js index 55054a1bb..245d29127 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -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); diff --git a/src/display/canvas_dependency_tracker.js b/src/display/canvas_dependency_tracker.js index 2b5ff3ef0..0d391c406 100644 --- a/src/display/canvas_dependency_tracker.js +++ b/src/display/canvas_dependency_tracker.js @@ -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 + #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"], diff --git a/src/display/pattern_helper.js b/src/display/pattern_helper.js index 6bb6fe120..7d9b8c9f7 100644 --- a/src/display/pattern_helper.js +++ b/src/display/pattern_helper.js @@ -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 diff --git a/test/driver.js b/test/driver.js index 4a46695e8..6333421f2 100644 --- a/test/driver.js +++ b/test/driver.js @@ -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); diff --git a/test/test_manifest.json b/test/test_manifest.json index b83b54648..90f5883a0 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -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", diff --git a/web/base_pdf_page_view.js b/web/base_pdf_page_view.js index 116ac1a55..4c8ab5e95 100644 --- a/web/base_pdf_page_view.js +++ b/web/base_pdf_page_view.js @@ -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; } } } diff --git a/web/debugger.mjs b/web/debugger.mjs index 91300d624..310ec83a6 100644 --- a/web/debugger.mjs +++ b/web/debugger.mjs @@ -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); } diff --git a/web/pdf_page_detail_view.js b/web/pdf_page_detail_view.js index 52cf13c45..d3fedc6b4 100644 --- a/web/pdf_page_detail_view.js +++ b/web/pdf_page_detail_view.js @@ -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 + ); + }, }; } diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 8a983c0ef..9c0a2643b 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -934,7 +934,7 @@ class PDFPageView extends BasePDFPageView { pageColors: this.pageColors, isEditing: this.#isEditing, recordOperations: - this.enableOptimizedPartialRendering && !this.recordedGroups, + this.enableOptimizedPartialRendering && !this.recordedBBoxes, }; }