Merge pull request #19043 from nicolo-ribaudo/compute-bounding-boxes
Add logic to track rendering area of various PDF ops
This commit is contained in:
commit
673f19bc2b
@ -238,6 +238,11 @@
|
|||||||
"description": "Enable creation of comment annotations.",
|
"description": "Enable creation of comment annotations.",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false
|
"default": false
|
||||||
|
},
|
||||||
|
"enableOptimizedPartialRendering": {
|
||||||
|
"description": "Enable tracking of PDF operations to optimize partial rendering.",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,6 +60,7 @@ import {
|
|||||||
NodeStandardFontDataFactory,
|
NodeStandardFontDataFactory,
|
||||||
NodeWasmFactory,
|
NodeWasmFactory,
|
||||||
} from "display-node_utils";
|
} from "display-node_utils";
|
||||||
|
import { CanvasDependencyTracker } from "./canvas_dependency_tracker.js";
|
||||||
import { CanvasGraphics } from "./canvas.js";
|
import { CanvasGraphics } from "./canvas.js";
|
||||||
import { DOMCanvasFactory } from "./canvas_factory.js";
|
import { DOMCanvasFactory } from "./canvas_factory.js";
|
||||||
import { DOMCMapReaderFactory } from "display-cmap_reader_factory";
|
import { DOMCMapReaderFactory } from "display-cmap_reader_factory";
|
||||||
@ -1239,6 +1240,10 @@ class PDFDocumentProxy {
|
|||||||
* annotation ids with canvases used to render them.
|
* annotation ids with canvases used to render them.
|
||||||
* @property {PrintAnnotationStorage} [printAnnotationStorage]
|
* @property {PrintAnnotationStorage} [printAnnotationStorage]
|
||||||
* @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
|
||||||
|
* 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.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1309,6 +1314,7 @@ class PDFPageProxy {
|
|||||||
|
|
||||||
this._intentStates = new Map();
|
this._intentStates = new Map();
|
||||||
this.destroyed = false;
|
this.destroyed = false;
|
||||||
|
this.recordedGroups = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1433,6 +1439,8 @@ class PDFPageProxy {
|
|||||||
pageColors = null,
|
pageColors = null,
|
||||||
printAnnotationStorage = null,
|
printAnnotationStorage = null,
|
||||||
isEditing = false,
|
isEditing = false,
|
||||||
|
recordOperations = false,
|
||||||
|
filteredOperationIndexes = null,
|
||||||
}) {
|
}) {
|
||||||
this._stats?.time("Overall");
|
this._stats?.time("Overall");
|
||||||
|
|
||||||
@ -1479,9 +1487,26 @@ class PDFPageProxy {
|
|||||||
this._pumpOperatorList(intentArgs);
|
this._pumpOperatorList(intentArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shouldRecordOperations =
|
||||||
|
!this.recordedGroups &&
|
||||||
|
(recordOperations ||
|
||||||
|
(this._pdfBug && globalThis.StepperManager?.enabled));
|
||||||
|
|
||||||
const complete = error => {
|
const complete = error => {
|
||||||
intentState.renderTasks.delete(internalRenderTask);
|
intentState.renderTasks.delete(internalRenderTask);
|
||||||
|
|
||||||
|
if (shouldRecordOperations) {
|
||||||
|
const recordedGroups = internalRenderTask.gfx?.dependencyTracker.take();
|
||||||
|
if (recordedGroups) {
|
||||||
|
internalRenderTask.stepper?.setOperatorGroups(recordedGroups);
|
||||||
|
if (recordOperations) {
|
||||||
|
this.recordedGroups = recordedGroups;
|
||||||
|
}
|
||||||
|
} else if (recordOperations) {
|
||||||
|
this.recordedGroups = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Attempt to reduce memory usage during *printing*, by always running
|
// Attempt to reduce memory usage during *printing*, by always running
|
||||||
// cleanup immediately once rendering has finished.
|
// cleanup immediately once rendering has finished.
|
||||||
if (intentPrint) {
|
if (intentPrint) {
|
||||||
@ -1516,6 +1541,9 @@ class PDFPageProxy {
|
|||||||
params: {
|
params: {
|
||||||
canvas,
|
canvas,
|
||||||
canvasContext,
|
canvasContext,
|
||||||
|
dependencyTracker: shouldRecordOperations
|
||||||
|
? new CanvasDependencyTracker(canvas)
|
||||||
|
: null,
|
||||||
viewport,
|
viewport,
|
||||||
transform,
|
transform,
|
||||||
background,
|
background,
|
||||||
@ -1531,6 +1559,7 @@ class PDFPageProxy {
|
|||||||
pdfBug: this._pdfBug,
|
pdfBug: this._pdfBug,
|
||||||
pageColors,
|
pageColors,
|
||||||
enableHWA: this._transport.enableHWA,
|
enableHWA: this._transport.enableHWA,
|
||||||
|
filteredOperationIndexes,
|
||||||
});
|
});
|
||||||
|
|
||||||
(intentState.renderTasks ||= new Set()).add(internalRenderTask);
|
(intentState.renderTasks ||= new Set()).add(internalRenderTask);
|
||||||
@ -3140,6 +3169,7 @@ class InternalRenderTask {
|
|||||||
pdfBug = false,
|
pdfBug = false,
|
||||||
pageColors = null,
|
pageColors = null,
|
||||||
enableHWA = false,
|
enableHWA = false,
|
||||||
|
filteredOperationIndexes = null,
|
||||||
}) {
|
}) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.params = params;
|
this.params = params;
|
||||||
@ -3170,6 +3200,8 @@ class InternalRenderTask {
|
|||||||
this._canvas = params.canvas;
|
this._canvas = params.canvas;
|
||||||
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._filteredOperationIndexes = filteredOperationIndexes;
|
||||||
}
|
}
|
||||||
|
|
||||||
get completed() {
|
get completed() {
|
||||||
@ -3199,7 +3231,7 @@ class InternalRenderTask {
|
|||||||
this.stepper.init(this.operatorList);
|
this.stepper.init(this.operatorList);
|
||||||
this.stepper.nextBreakPoint = this.stepper.getNextBreakPoint();
|
this.stepper.nextBreakPoint = this.stepper.getNextBreakPoint();
|
||||||
}
|
}
|
||||||
const { viewport, transform, background } = this.params;
|
const { viewport, transform, background, dependencyTracker } = this.params;
|
||||||
|
|
||||||
// When printing in Firefox, we get a specific context in mozPrintCallback
|
// When printing in Firefox, we get a specific context in mozPrintCallback
|
||||||
// which cannot be created from the canvas itself.
|
// which cannot be created from the canvas itself.
|
||||||
@ -3218,7 +3250,8 @@ class InternalRenderTask {
|
|||||||
this.filterFactory,
|
this.filterFactory,
|
||||||
{ optionalContentConfig },
|
{ optionalContentConfig },
|
||||||
this.annotationCanvasMap,
|
this.annotationCanvasMap,
|
||||||
this.pageColors
|
this.pageColors,
|
||||||
|
dependencyTracker
|
||||||
);
|
);
|
||||||
this.gfx.beginDrawing({
|
this.gfx.beginDrawing({
|
||||||
transform,
|
transform,
|
||||||
@ -3294,7 +3327,8 @@ class InternalRenderTask {
|
|||||||
this.operatorList,
|
this.operatorList,
|
||||||
this.operatorListIdx,
|
this.operatorListIdx,
|
||||||
this._continueBound,
|
this._continueBound,
|
||||||
this.stepper
|
this.stepper,
|
||||||
|
this._filteredOperationIndexes
|
||||||
);
|
);
|
||||||
if (this.operatorListIdx === this.operatorList.argsArray.length) {
|
if (this.operatorListIdx === this.operatorList.argsArray.length) {
|
||||||
this.running = false;
|
this.running = false;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
775
src/display/canvas_dependency_tracker.js
Normal file
775
src/display/canvas_dependency_tracker.js
Normal file
@ -0,0 +1,775 @@
|
|||||||
|
import { Util } from "../shared/util.js";
|
||||||
|
|
||||||
|
const FORCED_DEPENDENCY_LABEL = "__forcedDependency";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {"lineWidth" | "lineCap" | "lineJoin" | "miterLimit" | "dash" |
|
||||||
|
* "strokeAlpha" | "fillColor" | "fillAlpha" | "globalCompositeOperation" |
|
||||||
|
* "path" | "filter"} SimpleDependency
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {"transform" | "moveText" | "sameLineText"} IncrementalDependency
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {IncrementalDependency |
|
||||||
|
* typeof FORCED_DEPENDENCY_LABEL} InternalIncrementalDependency
|
||||||
|
*/
|
||||||
|
class CanvasDependencyTracker {
|
||||||
|
/** @type {Record<SimpleDependency, number>} */
|
||||||
|
#simple = { __proto__: null };
|
||||||
|
|
||||||
|
/** @type {Record<InternalIncrementalDependency , number[]>} */
|
||||||
|
#incremental = {
|
||||||
|
__proto__: null,
|
||||||
|
transform: [],
|
||||||
|
moveText: [],
|
||||||
|
sameLineText: [],
|
||||||
|
[FORCED_DEPENDENCY_LABEL]: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
#namedDependencies = new Map();
|
||||||
|
|
||||||
|
#savesStack = [];
|
||||||
|
|
||||||
|
#markedContentStack = [];
|
||||||
|
|
||||||
|
#baseTransformStack = [[1, 0, 0, 1, 0, 0]];
|
||||||
|
|
||||||
|
#clipBox = [-Infinity, -Infinity, Infinity, Infinity];
|
||||||
|
|
||||||
|
// Float32Array<minX, minY, maxX, maxY>
|
||||||
|
#pendingBBox = new Float64Array([Infinity, Infinity, -Infinity, -Infinity]);
|
||||||
|
|
||||||
|
#pendingBBoxIdx = -1;
|
||||||
|
|
||||||
|
#pendingDependencies = new Set();
|
||||||
|
|
||||||
|
#operations = new Map();
|
||||||
|
|
||||||
|
#fontBBoxTrustworthy = new Map();
|
||||||
|
|
||||||
|
#canvasWidth;
|
||||||
|
|
||||||
|
#canvasHeight;
|
||||||
|
|
||||||
|
constructor(canvas) {
|
||||||
|
this.#canvasWidth = canvas.width;
|
||||||
|
this.#canvasHeight = canvas.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
save(opIdx) {
|
||||||
|
this.#simple = { __proto__: this.#simple };
|
||||||
|
this.#incremental = {
|
||||||
|
__proto__: this.#incremental,
|
||||||
|
transform: { __proto__: this.#incremental.transform },
|
||||||
|
moveText: { __proto__: this.#incremental.moveText },
|
||||||
|
sameLineText: { __proto__: this.#incremental.sameLineText },
|
||||||
|
[FORCED_DEPENDENCY_LABEL]: {
|
||||||
|
__proto__: this.#incremental[FORCED_DEPENDENCY_LABEL],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.#clipBox = { __proto__: this.#clipBox };
|
||||||
|
this.#savesStack.push([opIdx, null]);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
restore(opIdx) {
|
||||||
|
const previous = Object.getPrototypeOf(this.#simple);
|
||||||
|
if (previous === null) {
|
||||||
|
// Sometimes we call more .restore() than .save(), for
|
||||||
|
// example when using CanvasGraphics' #restoreInitialState()
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
this.#simple = previous;
|
||||||
|
this.#incremental = Object.getPrototypeOf(this.#incremental);
|
||||||
|
this.#clipBox = Object.getPrototypeOf(this.#clipBox);
|
||||||
|
|
||||||
|
const lastPair = this.#savesStack.pop();
|
||||||
|
if (lastPair !== undefined) {
|
||||||
|
lastPair[1] = opIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} idx
|
||||||
|
*/
|
||||||
|
recordOpenMarker(idx) {
|
||||||
|
this.#savesStack.push([idx, null]);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOpenMarker() {
|
||||||
|
if (this.#savesStack.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.#savesStack.at(-1)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
recordCloseMarker(idx) {
|
||||||
|
const lastPair = this.#savesStack.pop();
|
||||||
|
if (lastPair !== undefined) {
|
||||||
|
lastPair[1] = idx;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marked content needs a separate stack from save/restore, because they
|
||||||
|
// form two independent trees.
|
||||||
|
beginMarkedContent(opIdx) {
|
||||||
|
this.#markedContentStack.push([opIdx, null]);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
endMarkedContent(opIdx) {
|
||||||
|
const lastPair = this.#markedContentStack.pop();
|
||||||
|
if (lastPair !== undefined) {
|
||||||
|
lastPair[1] = opIdx;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushBaseTransform(ctx) {
|
||||||
|
this.#baseTransformStack.push(
|
||||||
|
Util.multiplyByDOMMatrix(
|
||||||
|
this.#baseTransformStack.at(-1),
|
||||||
|
ctx.getTransform()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
popBaseTransform() {
|
||||||
|
if (this.#baseTransformStack.length > 1) {
|
||||||
|
this.#baseTransformStack.pop();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SimpleDependency} name
|
||||||
|
* @param {number} idx
|
||||||
|
*/
|
||||||
|
recordSimpleData(name, idx) {
|
||||||
|
this.#simple[name] = idx;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {IncrementalDependency} name
|
||||||
|
* @param {number} idx
|
||||||
|
*/
|
||||||
|
recordIncrementalData(name, idx) {
|
||||||
|
this.#incremental[name].push(idx);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {IncrementalDependency} name
|
||||||
|
* @param {number} idx
|
||||||
|
*/
|
||||||
|
resetIncrementalData(name, idx) {
|
||||||
|
this.#incremental[name].length = 0;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordNamedData(name, idx) {
|
||||||
|
this.#namedDependencies.set(name, idx);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All next operations, until the next .restore(), will depend on this
|
||||||
|
recordFutureForcedDependency(name, idx) {
|
||||||
|
this.recordIncrementalData(FORCED_DEPENDENCY_LABEL, idx);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All next operations, until the next .restore(), will depend on all
|
||||||
|
// the already recorded data with the given names.
|
||||||
|
inheritSimpleDataAsFutureForcedDependencies(names) {
|
||||||
|
for (const name of names) {
|
||||||
|
if (name in this.#simple) {
|
||||||
|
this.recordFutureForcedDependency(name, this.#simple[name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inheritPendingDependenciesAsFutureForcedDependencies() {
|
||||||
|
for (const dep of this.#pendingDependencies) {
|
||||||
|
this.recordFutureForcedDependency(FORCED_DEPENDENCY_LABEL, dep);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetBBox(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),
|
||||||
|
ctx.getTransform()
|
||||||
|
);
|
||||||
|
const clipBox = [Infinity, Infinity, -Infinity, -Infinity];
|
||||||
|
Util.axialAlignedBoundingBox([minX, minY, maxX, maxY], transform, clipBox);
|
||||||
|
const intersection = Util.intersect(this.#clipBox, clipBox);
|
||||||
|
if (intersection) {
|
||||||
|
this.#clipBox[0] = intersection[0];
|
||||||
|
this.#clipBox[1] = intersection[1];
|
||||||
|
this.#clipBox[2] = intersection[2];
|
||||||
|
this.#clipBox[3] = intersection[3];
|
||||||
|
} else {
|
||||||
|
this.#clipBox[0] = this.#clipBox[1] = Infinity;
|
||||||
|
this.#clipBox[2] = this.#clipBox[3] = -Infinity;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordBBox(idx, ctx, minX, maxX, minY, maxY) {
|
||||||
|
const clipBox = this.#clipBox;
|
||||||
|
if (clipBox[0] === Infinity) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transform = Util.multiplyByDOMMatrix(
|
||||||
|
this.#baseTransformStack.at(-1),
|
||||||
|
ctx.getTransform()
|
||||||
|
);
|
||||||
|
if (clipBox[0] === -Infinity) {
|
||||||
|
Util.axialAlignedBoundingBox(
|
||||||
|
[minX, minY, maxX, maxY],
|
||||||
|
transform,
|
||||||
|
this.#pendingBBox
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bbox = [Infinity, Infinity, -Infinity, -Infinity];
|
||||||
|
Util.axialAlignedBoundingBox([minX, minY, maxX, maxY], transform, bbox);
|
||||||
|
this.#pendingBBox[0] = Math.min(
|
||||||
|
this.#pendingBBox[0],
|
||||||
|
Math.max(bbox[0], clipBox[0])
|
||||||
|
);
|
||||||
|
this.#pendingBBox[1] = Math.min(
|
||||||
|
this.#pendingBBox[1],
|
||||||
|
Math.max(bbox[1], clipBox[1])
|
||||||
|
);
|
||||||
|
this.#pendingBBox[2] = Math.max(
|
||||||
|
this.#pendingBBox[2],
|
||||||
|
Math.min(bbox[2], clipBox[2])
|
||||||
|
);
|
||||||
|
this.#pendingBBox[3] = Math.max(
|
||||||
|
this.#pendingBBox[3],
|
||||||
|
Math.min(bbox[3], clipBox[3])
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordCharacterBBox(idx, ctx, font, scale = 1, x = 0, y = 0, getMeasure) {
|
||||||
|
const fontBBox = font.bbox;
|
||||||
|
let isBBoxTrustworthy;
|
||||||
|
let computedBBox;
|
||||||
|
|
||||||
|
if (fontBBox) {
|
||||||
|
isBBoxTrustworthy =
|
||||||
|
// Only use the bounding box defined by the font if it
|
||||||
|
// has a non-empty area.
|
||||||
|
fontBBox[2] !== fontBBox[0] &&
|
||||||
|
fontBBox[3] !== fontBBox[1] &&
|
||||||
|
this.#fontBBoxTrustworthy.get(font);
|
||||||
|
|
||||||
|
if (isBBoxTrustworthy !== false) {
|
||||||
|
computedBBox = [0, 0, 0, 0];
|
||||||
|
Util.axialAlignedBoundingBox(fontBBox, font.fontMatrix, computedBBox);
|
||||||
|
if (scale !== 1 || x !== 0 || y !== 0) {
|
||||||
|
Util.scaleMinMax([scale, 0, 0, -scale, x, y], computedBBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBBoxTrustworthy) {
|
||||||
|
return this.recordBBox(
|
||||||
|
idx,
|
||||||
|
ctx,
|
||||||
|
computedBBox[0],
|
||||||
|
computedBBox[2],
|
||||||
|
computedBBox[1],
|
||||||
|
computedBBox[3]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getMeasure) {
|
||||||
|
// We have no way of telling how big this character actually is, record
|
||||||
|
// a full page bounding box.
|
||||||
|
return this.recordFullPageBBox(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
const measure = getMeasure();
|
||||||
|
|
||||||
|
if (fontBBox && computedBBox && isBBoxTrustworthy === undefined) {
|
||||||
|
// If it's the first time we can compare the font bbox with the actual
|
||||||
|
// bbox measured when drawing it, check if the one recorded in the font
|
||||||
|
// is large enough to cover the actual bbox. If it is, we assume that the
|
||||||
|
// font is well-formed and we can use the declared bbox without having to
|
||||||
|
// measure it again for every character.
|
||||||
|
isBBoxTrustworthy =
|
||||||
|
computedBBox[0] <= x - measure.actualBoundingBoxLeft &&
|
||||||
|
computedBBox[2] >= x + measure.actualBoundingBoxRight &&
|
||||||
|
computedBBox[1] <= y - measure.actualBoundingBoxAscent &&
|
||||||
|
computedBBox[3] >= y + measure.actualBoundingBoxDescent;
|
||||||
|
this.#fontBBoxTrustworthy.set(font, isBBoxTrustworthy);
|
||||||
|
if (isBBoxTrustworthy) {
|
||||||
|
return this.recordBBox(
|
||||||
|
idx,
|
||||||
|
ctx,
|
||||||
|
computedBBox[0],
|
||||||
|
computedBBox[2],
|
||||||
|
computedBBox[1],
|
||||||
|
computedBBox[3]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The font has no bbox or it is not trustworthy, so we need to
|
||||||
|
// return the bounding box based on .measureText().
|
||||||
|
return this.recordBBox(
|
||||||
|
idx,
|
||||||
|
ctx,
|
||||||
|
x - measure.actualBoundingBoxLeft,
|
||||||
|
x + measure.actualBoundingBoxRight,
|
||||||
|
y - measure.actualBoundingBoxAscent,
|
||||||
|
y + measure.actualBoundingBoxDescent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
recordFullPageBBox(idx) {
|
||||||
|
this.#pendingBBox[0] = Math.max(0, this.#clipBox[0]);
|
||||||
|
this.#pendingBBox[1] = Math.max(0, this.#clipBox[1]);
|
||||||
|
this.#pendingBBox[2] = Math.min(this.#canvasWidth, this.#clipBox[2]);
|
||||||
|
this.#pendingBBox[3] = Math.min(this.#canvasHeight, this.#clipBox[3]);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSimpleIndex(dependencyName) {
|
||||||
|
return this.#simple[dependencyName];
|
||||||
|
}
|
||||||
|
|
||||||
|
recordDependencies(idx, dependencyNames) {
|
||||||
|
const pendingDependencies = this.#pendingDependencies;
|
||||||
|
const simple = this.#simple;
|
||||||
|
const incremental = this.#incremental;
|
||||||
|
for (const name of dependencyNames) {
|
||||||
|
if (name in this.#simple) {
|
||||||
|
pendingDependencies.add(simple[name]);
|
||||||
|
} else if (name in incremental) {
|
||||||
|
incremental[name].forEach(pendingDependencies.add, pendingDependencies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} idx
|
||||||
|
*/
|
||||||
|
recordOperation(idx, preserveBbox = 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;
|
||||||
|
}
|
||||||
|
this.#pendingDependencies.clear();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
bboxToClipBoxDropOperation(idx) {
|
||||||
|
if (this.#pendingBBoxIdx !== -1) {
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
this.#pendingDependencies.clear();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
_takePendingDependencies() {
|
||||||
|
const pendingDependencies = this.#pendingDependencies;
|
||||||
|
this.#pendingDependencies = new Set();
|
||||||
|
return pendingDependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
_extractOperation(idx) {
|
||||||
|
const operation = this.#operations.get(idx);
|
||||||
|
this.#operations.delete(idx);
|
||||||
|
return operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pushPendingDependencies(dependencies) {
|
||||||
|
for (const dep of dependencies) {
|
||||||
|
this.#pendingDependencies.add(dep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
take() {
|
||||||
|
this.#fontBBoxTrustworthy.clear();
|
||||||
|
return 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to track dependencies of nested operations list, that
|
||||||
|
* should actually all map to the index of the operation that
|
||||||
|
* contains the nested list.
|
||||||
|
*
|
||||||
|
* @implements {CanvasDependencyTracker}
|
||||||
|
*/
|
||||||
|
class CanvasNestedDependencyTracker {
|
||||||
|
/** @type {CanvasDependencyTracker} */
|
||||||
|
#dependencyTracker;
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
#opIdx;
|
||||||
|
|
||||||
|
#nestingLevel = 0;
|
||||||
|
|
||||||
|
#outerDependencies;
|
||||||
|
|
||||||
|
#savesLevel = 0;
|
||||||
|
|
||||||
|
constructor(dependencyTracker, opIdx) {
|
||||||
|
if (dependencyTracker instanceof CanvasNestedDependencyTracker) {
|
||||||
|
// The goal of CanvasNestedDependencyTracker is to collapse all operations
|
||||||
|
// into a single one. If we are already in a
|
||||||
|
// CanvasNestedDependencyTracker, that is already happening.
|
||||||
|
return dependencyTracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#dependencyTracker = dependencyTracker;
|
||||||
|
this.#outerDependencies = dependencyTracker._takePendingDependencies();
|
||||||
|
this.#opIdx = opIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
save(opIdx) {
|
||||||
|
this.#savesLevel++;
|
||||||
|
this.#dependencyTracker.save(this.#opIdx);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
restore(opIdx) {
|
||||||
|
if (this.#savesLevel > 0) {
|
||||||
|
this.#dependencyTracker.restore(this.#opIdx);
|
||||||
|
this.#savesLevel--;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordOpenMarker(idx) {
|
||||||
|
this.#nestingLevel++;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOpenMarker() {
|
||||||
|
return this.#nestingLevel > 0
|
||||||
|
? this.#opIdx
|
||||||
|
: this.#dependencyTracker.getOpenMarker();
|
||||||
|
}
|
||||||
|
|
||||||
|
recordCloseMarker(idx) {
|
||||||
|
this.#nestingLevel--;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
beginMarkedContent(opIdx) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
endMarkedContent(opIdx) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushBaseTransform(ctx) {
|
||||||
|
this.#dependencyTracker.pushBaseTransform(ctx);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
popBaseTransform() {
|
||||||
|
this.#dependencyTracker.popBaseTransform();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SimpleDependency} name
|
||||||
|
* @param {number} idx
|
||||||
|
*/
|
||||||
|
recordSimpleData(name, idx) {
|
||||||
|
this.#dependencyTracker.recordSimpleData(name, this.#opIdx);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {IncrementalDependency} name
|
||||||
|
* @param {number} idx
|
||||||
|
*/
|
||||||
|
recordIncrementalData(name, idx) {
|
||||||
|
this.#dependencyTracker.recordIncrementalData(name, this.#opIdx);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {IncrementalDependency} name
|
||||||
|
* @param {number} idx
|
||||||
|
*/
|
||||||
|
resetIncrementalData(name, idx) {
|
||||||
|
this.#dependencyTracker.resetIncrementalData(name, this.#opIdx);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordNamedData(name, idx) {
|
||||||
|
// Nested dependencies are not visible to the outside.
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All next operations, until the next .restore(), will depend on this
|
||||||
|
recordFutureForcedDependency(name, idx) {
|
||||||
|
this.#dependencyTracker.recordFutureForcedDependency(name, this.#opIdx);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All next operations, until the next .restore(), will depend on all
|
||||||
|
// the already recorded data with the given names.
|
||||||
|
inheritSimpleDataAsFutureForcedDependencies(names) {
|
||||||
|
this.#dependencyTracker.inheritSimpleDataAsFutureForcedDependencies(names);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inheritPendingDependenciesAsFutureForcedDependencies() {
|
||||||
|
this.#dependencyTracker.inheritPendingDependenciesAsFutureForcedDependencies();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetBBox(idx) {
|
||||||
|
if (!this.#dependencyTracker.hasPendingBBox) {
|
||||||
|
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
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordBBox(idx, ctx, minX, maxX, minY, maxY) {
|
||||||
|
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
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordFullPageBBox(idx) {
|
||||||
|
this.#dependencyTracker.recordFullPageBBox(this.#opIdx);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSimpleIndex(dependencyName) {
|
||||||
|
return this.#dependencyTracker.getSimpleIndex(dependencyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
recordDependencies(idx, dependencyNames) {
|
||||||
|
this.#dependencyTracker.recordDependencies(this.#opIdx, dependencyNames);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
copyDependenciesFromIncrementalOperation(idx, name) {
|
||||||
|
this.#dependencyTracker.copyDependenciesFromIncrementalOperation(
|
||||||
|
this.#opIdx,
|
||||||
|
name
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordNamedDependency(idx, name) {
|
||||||
|
this.#dependencyTracker.recordNamedDependency(this.#opIdx, name);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} idx
|
||||||
|
* @param {SimpleDependency[]} dependencyNames
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
bboxToClipBoxDropOperation(idx) {
|
||||||
|
this.#dependencyTracker.bboxToClipBoxDropOperation(this.#opIdx);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordNestedDependencies() {
|
||||||
|
this.#dependencyTracker._pushPendingDependencies(this.#outerDependencies);
|
||||||
|
}
|
||||||
|
|
||||||
|
take() {
|
||||||
|
throw new Error("Unreachable");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @satisfies {Record<string, SimpleDependency | IncrementalDependency>} */
|
||||||
|
const Dependencies = {
|
||||||
|
stroke: [
|
||||||
|
"path",
|
||||||
|
"transform",
|
||||||
|
"filter",
|
||||||
|
"strokeColor",
|
||||||
|
"strokeAlpha",
|
||||||
|
"lineWidth",
|
||||||
|
"lineCap",
|
||||||
|
"lineJoin",
|
||||||
|
"miterLimit",
|
||||||
|
"dash",
|
||||||
|
],
|
||||||
|
fill: [
|
||||||
|
"path",
|
||||||
|
"transform",
|
||||||
|
"filter",
|
||||||
|
"fillColor",
|
||||||
|
"fillAlpha",
|
||||||
|
"globalCompositeOperation",
|
||||||
|
"SMask",
|
||||||
|
],
|
||||||
|
imageXObject: [
|
||||||
|
"transform",
|
||||||
|
"SMask",
|
||||||
|
"filter",
|
||||||
|
"fillAlpha",
|
||||||
|
"strokeAlpha",
|
||||||
|
"globalCompositeOperation",
|
||||||
|
],
|
||||||
|
rawFillPath: ["filter", "fillColor", "fillAlpha"],
|
||||||
|
showText: [
|
||||||
|
"transform",
|
||||||
|
"leading",
|
||||||
|
"charSpacing",
|
||||||
|
"wordSpacing",
|
||||||
|
"hScale",
|
||||||
|
"textRise",
|
||||||
|
"moveText",
|
||||||
|
"textMatrix",
|
||||||
|
"font",
|
||||||
|
"filter",
|
||||||
|
"fillColor",
|
||||||
|
"textRenderingMode",
|
||||||
|
"SMask",
|
||||||
|
"fillAlpha",
|
||||||
|
"strokeAlpha",
|
||||||
|
"globalCompositeOperation",
|
||||||
|
// TODO: More
|
||||||
|
],
|
||||||
|
transform: ["transform"],
|
||||||
|
transformAndFill: ["transform", "fillColor"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export { CanvasDependencyTracker, CanvasNestedDependencyTracker, Dependencies };
|
||||||
@ -573,11 +573,24 @@ class TilingPattern {
|
|||||||
this.setFillAndStrokeStyleToContext(graphics, paintType, color);
|
this.setFillAndStrokeStyleToContext(graphics, paintType, color);
|
||||||
|
|
||||||
tmpCtx.translate(-dimx.scale * x0, -dimy.scale * y0);
|
tmpCtx.translate(-dimx.scale * x0, -dimy.scale * y0);
|
||||||
graphics.transform(dimx.scale, 0, 0, dimy.scale, 0, 0);
|
graphics.transform(
|
||||||
|
// We pass 0 as the 'opIdx' argument, but the value is irrelevant.
|
||||||
|
// We know that we are in a 'CanvasNestedDependencyTracker' that captures
|
||||||
|
// all the sub-operations needed to create this pattern canvas and uses
|
||||||
|
// the top-level operation index as their index.
|
||||||
|
0,
|
||||||
|
dimx.scale,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
dimy.scale,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
// To match CanvasGraphics beginDrawing we must save the context here or
|
// To match CanvasGraphics beginDrawing we must save the context here or
|
||||||
// else we end up with unbalanced save/restores.
|
// else we end up with unbalanced save/restores.
|
||||||
tmpCtx.save();
|
tmpCtx.save();
|
||||||
|
graphics.dependencyTracker?.save();
|
||||||
|
|
||||||
this.clipBbox(graphics, x0, y0, x1, y1);
|
this.clipBbox(graphics, x0, y0, x1, y1);
|
||||||
|
|
||||||
@ -587,6 +600,7 @@ class TilingPattern {
|
|||||||
|
|
||||||
graphics.endDrawing();
|
graphics.endDrawing();
|
||||||
|
|
||||||
|
graphics.dependencyTracker?.restore().recordNestedDependencies?.();
|
||||||
tmpCtx.restore();
|
tmpCtx.restore();
|
||||||
|
|
||||||
if (redrawHorizontally || redrawVertically) {
|
if (redrawHorizontally || redrawVertically) {
|
||||||
|
|||||||
@ -674,6 +674,10 @@ class Util {
|
|||||||
return `#${hexNumbers[r]}${hexNumbers[g]}${hexNumbers[b]}`;
|
return `#${hexNumbers[r]}${hexNumbers[g]}${hexNumbers[b]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static domMatrixToTransform(dm) {
|
||||||
|
return [dm.a, dm.b, dm.c, dm.d, dm.e, dm.f];
|
||||||
|
}
|
||||||
|
|
||||||
// Apply a scaling matrix to some min/max values.
|
// Apply a scaling matrix to some min/max values.
|
||||||
// If a scaling factor is negative then min and max must be
|
// If a scaling factor is negative then min and max must be
|
||||||
// swapped.
|
// swapped.
|
||||||
@ -737,6 +741,18 @@ class Util {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multiplies m (an array-based transform) by md (a DOMMatrix transform).
|
||||||
|
static multiplyByDOMMatrix(m, md) {
|
||||||
|
return [
|
||||||
|
m[0] * md.a + m[2] * md.b,
|
||||||
|
m[1] * md.a + m[3] * md.b,
|
||||||
|
m[0] * md.c + m[2] * md.d,
|
||||||
|
m[1] * md.c + m[3] * md.d,
|
||||||
|
m[0] * md.e + m[2] * md.f + m[4],
|
||||||
|
m[1] * md.e + m[3] * md.f + m[5],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// For 2d affine transforms
|
// For 2d affine transforms
|
||||||
static applyTransform(p, m, pos = 0) {
|
static applyTransform(p, m, pos = 0) {
|
||||||
const p0 = p[pos];
|
const p0 = p[pos];
|
||||||
|
|||||||
166
test/driver.js
166
test/driver.js
@ -920,7 +920,8 @@ class Driver {
|
|||||||
renderPrint = false,
|
renderPrint = false,
|
||||||
renderXfa = false,
|
renderXfa = false,
|
||||||
annotationCanvasMap = null,
|
annotationCanvasMap = null,
|
||||||
pageColors = null;
|
pageColors = null,
|
||||||
|
partialCrop = null;
|
||||||
|
|
||||||
if (task.annotationStorage) {
|
if (task.annotationStorage) {
|
||||||
task.pdfDoc.annotationStorage._setValues(task.annotationStorage);
|
task.pdfDoc.annotationStorage._setValues(task.annotationStorage);
|
||||||
@ -968,10 +969,14 @@ class Driver {
|
|||||||
textLayerCanvas = null;
|
textLayerCanvas = null;
|
||||||
// We fetch the `eq` specific test subtypes here, to avoid
|
// We fetch the `eq` specific test subtypes here, to avoid
|
||||||
// accidentally changing the behaviour for other types of tests.
|
// accidentally changing the behaviour for other types of tests.
|
||||||
renderAnnotations = !!task.annotations;
|
|
||||||
renderForms = !!task.forms;
|
partialCrop = task.partial;
|
||||||
renderPrint = !!task.print;
|
if (!partialCrop) {
|
||||||
renderXfa = !!task.enableXfa;
|
renderAnnotations = !!task.annotations;
|
||||||
|
renderForms = !!task.forms;
|
||||||
|
renderPrint = !!task.print;
|
||||||
|
renderXfa = !!task.enableXfa;
|
||||||
|
}
|
||||||
pageColors = task.pageColors || null;
|
pageColors = task.pageColors || null;
|
||||||
|
|
||||||
// Render the annotation layer if necessary.
|
// Render the annotation layer if necessary.
|
||||||
@ -1031,6 +1036,9 @@ class Driver {
|
|||||||
}
|
}
|
||||||
renderContext.intent = "print";
|
renderContext.intent = "print";
|
||||||
}
|
}
|
||||||
|
if (partialCrop) {
|
||||||
|
renderContext.recordOperations = true;
|
||||||
|
}
|
||||||
|
|
||||||
const completeRender = error => {
|
const completeRender = error => {
|
||||||
// if text layer is present, compose it on top of the page
|
// if text layer is present, compose it on top of the page
|
||||||
@ -1061,7 +1069,7 @@ class Driver {
|
|||||||
this._snapshot(task, error);
|
this._snapshot(task, error);
|
||||||
};
|
};
|
||||||
initPromise
|
initPromise
|
||||||
.then(data => {
|
.then(async data => {
|
||||||
const renderTask = page.render(renderContext);
|
const renderTask = page.render(renderContext);
|
||||||
|
|
||||||
if (task.renderTaskOnContinue) {
|
if (task.renderTaskOnContinue) {
|
||||||
@ -1070,26 +1078,122 @@ class Driver {
|
|||||||
setTimeout(cont, RENDER_TASK_ON_CONTINUE_DELAY);
|
setTimeout(cont, RENDER_TASK_ON_CONTINUE_DELAY);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return renderTask.promise.then(() => {
|
await renderTask.promise;
|
||||||
if (annotationCanvasMap) {
|
|
||||||
Rasterize.annotationLayer(
|
if (partialCrop) {
|
||||||
annotationLayerContext,
|
const clearOutsidePartial = () => {
|
||||||
viewport,
|
const { width, height } = ctx.canvas;
|
||||||
outputScale,
|
// Everything above the partial area
|
||||||
data,
|
ctx.clearRect(
|
||||||
annotationCanvasMap,
|
0,
|
||||||
task.pdfDoc.annotationStorage,
|
0,
|
||||||
task.fieldObjects,
|
width,
|
||||||
page,
|
Math.ceil(partialCrop.minY * height)
|
||||||
IMAGE_RESOURCES_PATH,
|
);
|
||||||
renderForms
|
// Everything below the partial area
|
||||||
).then(() => {
|
ctx.clearRect(
|
||||||
completeRender(false);
|
0,
|
||||||
});
|
Math.floor(partialCrop.maxY * height),
|
||||||
} else {
|
width,
|
||||||
completeRender(false);
|
height
|
||||||
|
);
|
||||||
|
// Everything to the left of the partial area
|
||||||
|
ctx.clearRect(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
Math.ceil(partialCrop.minX * width),
|
||||||
|
height
|
||||||
|
);
|
||||||
|
// Everything to the right of the partial area
|
||||||
|
ctx.clearRect(
|
||||||
|
Math.floor(partialCrop.maxX * width),
|
||||||
|
0,
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
clearOutsidePartial();
|
||||||
|
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 partialRenderContext = {
|
||||||
|
canvasContext: ctx,
|
||||||
|
viewport,
|
||||||
|
optionalContentConfigPromise:
|
||||||
|
task.optionalContentConfigPromise,
|
||||||
|
annotationCanvasMap,
|
||||||
|
pageColors,
|
||||||
|
transform,
|
||||||
|
recordOperations: false,
|
||||||
|
filteredOperationIndexes: filteredIndexes,
|
||||||
|
};
|
||||||
|
|
||||||
|
const partialRenderTask = page.render(partialRenderContext);
|
||||||
|
await partialRenderTask.promise;
|
||||||
|
|
||||||
|
clearOutsidePartial();
|
||||||
|
|
||||||
|
if (page.stats) {
|
||||||
|
// Get the page stats *before* running cleanup.
|
||||||
|
task.stats = page.stats;
|
||||||
|
}
|
||||||
|
page.cleanup(/* resetStats = */ true);
|
||||||
|
this._snapshot(
|
||||||
|
task,
|
||||||
|
false,
|
||||||
|
// Sometimes the optimized version does not match the
|
||||||
|
// baseline. Tests marked as "knownPartialMismatch" have
|
||||||
|
// been manually verified to be good enough (e.g. there is
|
||||||
|
// one pixel of a very slightly different shade), so we
|
||||||
|
// avoid compating them to the non-optimized version and
|
||||||
|
// instead use the optimized version also for makeref.
|
||||||
|
task.knownPartialMismatch ? null : baseline
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (annotationCanvasMap) {
|
||||||
|
Rasterize.annotationLayer(
|
||||||
|
annotationLayerContext,
|
||||||
|
viewport,
|
||||||
|
outputScale,
|
||||||
|
data,
|
||||||
|
annotationCanvasMap,
|
||||||
|
task.pdfDoc.annotationStorage,
|
||||||
|
task.fieldObjects,
|
||||||
|
page,
|
||||||
|
IMAGE_RESOURCES_PATH,
|
||||||
|
renderForms
|
||||||
|
).then(() => {
|
||||||
|
completeRender(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
completeRender(false);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
completeRender("render : " + error);
|
completeRender("render : " + error);
|
||||||
@ -1112,11 +1216,16 @@ class Driver {
|
|||||||
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
_snapshot(task, failure) {
|
_snapshot(task, failure, baselineDataUrl = null) {
|
||||||
this._log("Snapshotting... ");
|
this._log("Snapshotting... ");
|
||||||
|
|
||||||
const dataUrl = this.canvas.toDataURL("image/png");
|
const dataUrl = this.canvas.toDataURL("image/png");
|
||||||
this._sendResult(dataUrl, task, failure).then(() => {
|
|
||||||
|
if (baselineDataUrl && baselineDataUrl !== dataUrl) {
|
||||||
|
failure ||= "Optimized rendering differs from full rendering.";
|
||||||
|
}
|
||||||
|
|
||||||
|
this._sendResult(dataUrl, task, failure, baselineDataUrl).then(() => {
|
||||||
this._log(
|
this._log(
|
||||||
"done" + (failure ? " (failed !: " + failure + ")" : "") + "\n"
|
"done" + (failure ? " (failed !: " + failure + ")" : "") + "\n"
|
||||||
);
|
);
|
||||||
@ -1170,7 +1279,7 @@ class Driver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_sendResult(snapshot, task, failure) {
|
_sendResult(snapshot, task, failure, baselineSnapshot = null) {
|
||||||
const result = JSON.stringify({
|
const result = JSON.stringify({
|
||||||
browser: this.browser,
|
browser: this.browser,
|
||||||
id: task.id,
|
id: task.id,
|
||||||
@ -1181,6 +1290,7 @@ class Driver {
|
|||||||
round: task.round,
|
round: task.round,
|
||||||
page: task.pageNum,
|
page: task.pageNum,
|
||||||
snapshot,
|
snapshot,
|
||||||
|
baselineSnapshot,
|
||||||
stats: task.stats.times,
|
stats: task.stats.times,
|
||||||
viewportWidth: task.viewportWidth,
|
viewportWidth: task.viewportWidth,
|
||||||
viewportHeight: task.viewportHeight,
|
viewportHeight: task.viewportHeight,
|
||||||
|
|||||||
@ -460,6 +460,13 @@ function checkEq(task, results, browser, masterMode) {
|
|||||||
} else {
|
} else {
|
||||||
console.error("Valid snapshot was not found.");
|
console.error("Valid snapshot was not found.");
|
||||||
}
|
}
|
||||||
|
let unoptimizedSnapshot = pageResult.baselineSnapshot;
|
||||||
|
if (unoptimizedSnapshot?.startsWith("data:image/png;base64,")) {
|
||||||
|
unoptimizedSnapshot = Buffer.from(
|
||||||
|
unoptimizedSnapshot.substring(22),
|
||||||
|
"base64"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
var refSnapshot = null;
|
var refSnapshot = null;
|
||||||
var eq = false;
|
var eq = false;
|
||||||
@ -526,7 +533,7 @@ function checkEq(task, results, browser, masterMode) {
|
|||||||
ensureDirSync(tmpSnapshotDir);
|
ensureDirSync(tmpSnapshotDir);
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(tmpSnapshotDir, page + 1 + ".png"),
|
path.join(tmpSnapshotDir, page + 1 + ".png"),
|
||||||
testSnapshot
|
unoptimizedSnapshot ?? testSnapshot
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -616,7 +623,14 @@ function checkRefTestResults(browser, id, results) {
|
|||||||
return; // no results
|
return; // no results
|
||||||
}
|
}
|
||||||
if (pageResult.failure) {
|
if (pageResult.failure) {
|
||||||
failed = true;
|
// If the test failes due to a difference between the optimized and
|
||||||
|
// unoptimized rendering, we don't set `failed` to true so that we will
|
||||||
|
// still compute the differences between them. In master mode, this
|
||||||
|
// means that we will save the reference image from the unoptimized
|
||||||
|
// rendering even if the optimized rendering is wrong.
|
||||||
|
if (!pageResult.failure.includes("Optimized rendering differs")) {
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
if (fs.existsSync(task.file + ".error")) {
|
if (fs.existsSync(task.file + ".error")) {
|
||||||
console.log(
|
console.log(
|
||||||
"TEST-SKIPPED | PDF was not downloaded " +
|
"TEST-SKIPPED | PDF was not downloaded " +
|
||||||
@ -631,7 +645,9 @@ function checkRefTestResults(browser, id, results) {
|
|||||||
pageResult.failure
|
pageResult.failure
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
session.numErrors++;
|
if (failed) {
|
||||||
|
session.numErrors++;
|
||||||
|
}
|
||||||
console.log(
|
console.log(
|
||||||
"TEST-UNEXPECTED-FAIL | test failed " +
|
"TEST-UNEXPECTED-FAIL | test failed " +
|
||||||
id +
|
id +
|
||||||
@ -653,6 +669,7 @@ function checkRefTestResults(browser, id, results) {
|
|||||||
}
|
}
|
||||||
switch (task.type) {
|
switch (task.type) {
|
||||||
case "eq":
|
case "eq":
|
||||||
|
case "partial":
|
||||||
case "text":
|
case "text":
|
||||||
case "highlight":
|
case "highlight":
|
||||||
checkEq(task, results, browser, session.masterMode);
|
checkEq(task, results, browser, session.masterMode);
|
||||||
@ -712,6 +729,7 @@ function refTestPostHandler(parsedUrl, req, res) {
|
|||||||
var page = data.page - 1;
|
var page = data.page - 1;
|
||||||
var failure = data.failure;
|
var failure = data.failure;
|
||||||
var snapshot = data.snapshot;
|
var snapshot = data.snapshot;
|
||||||
|
var baselineSnapshot = data.baselineSnapshot;
|
||||||
var lastPageNum = data.lastPageNum;
|
var lastPageNum = data.lastPageNum;
|
||||||
|
|
||||||
session = getSession(browser);
|
session = getSession(browser);
|
||||||
@ -740,6 +758,7 @@ function refTestPostHandler(parsedUrl, req, res) {
|
|||||||
taskResults[round][page] = {
|
taskResults[round][page] = {
|
||||||
failure,
|
failure,
|
||||||
snapshot,
|
snapshot,
|
||||||
|
baselineSnapshot,
|
||||||
viewportWidth: data.viewportWidth,
|
viewportWidth: data.viewportWidth,
|
||||||
viewportHeight: data.viewportHeight,
|
viewportHeight: data.viewportHeight,
|
||||||
outputScale: data.outputScale,
|
outputScale: data.outputScale,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -534,6 +534,9 @@ const PDFViewerApplication = {
|
|||||||
capCanvasAreaFactor,
|
capCanvasAreaFactor,
|
||||||
enableDetailCanvas: AppOptions.get("enableDetailCanvas"),
|
enableDetailCanvas: AppOptions.get("enableDetailCanvas"),
|
||||||
enablePermissions: AppOptions.get("enablePermissions"),
|
enablePermissions: AppOptions.get("enablePermissions"),
|
||||||
|
enableOptimizedPartialRendering: AppOptions.get(
|
||||||
|
"enableOptimizedPartialRendering"
|
||||||
|
),
|
||||||
pageColors,
|
pageColors,
|
||||||
mlManager,
|
mlManager,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
|
|||||||
@ -246,6 +246,11 @@ const defaultOptions = {
|
|||||||
value: true,
|
value: true,
|
||||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||||
},
|
},
|
||||||
|
enableOptimizedPartialRendering: {
|
||||||
|
/** @type {boolean} */
|
||||||
|
value: false,
|
||||||
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||||
|
},
|
||||||
enablePermissions: {
|
enablePermissions: {
|
||||||
/** @type {boolean} */
|
/** @type {boolean} */
|
||||||
value: false,
|
value: false,
|
||||||
|
|||||||
@ -36,12 +36,16 @@ class BasePDFPageView {
|
|||||||
/** @type {null | HTMLDivElement} */
|
/** @type {null | HTMLDivElement} */
|
||||||
div = null;
|
div = null;
|
||||||
|
|
||||||
|
enableOptimizedPartialRendering = false;
|
||||||
|
|
||||||
eventBus = null;
|
eventBus = null;
|
||||||
|
|
||||||
id = null;
|
id = null;
|
||||||
|
|
||||||
pageColors = null;
|
pageColors = null;
|
||||||
|
|
||||||
|
recordedGroups = null;
|
||||||
|
|
||||||
renderingQueue = null;
|
renderingQueue = null;
|
||||||
|
|
||||||
renderTask = null;
|
renderTask = null;
|
||||||
@ -53,6 +57,8 @@ class BasePDFPageView {
|
|||||||
this.id = options.id;
|
this.id = options.id;
|
||||||
this.pageColors = options.pageColors || null;
|
this.pageColors = options.pageColors || null;
|
||||||
this.renderingQueue = options.renderingQueue;
|
this.renderingQueue = options.renderingQueue;
|
||||||
|
this.enableOptimizedPartialRendering =
|
||||||
|
options.enableOptimizedPartialRendering ?? false;
|
||||||
this.#minDurationToUpdateCanvas = options.minDurationToUpdateCanvas ?? 500;
|
this.#minDurationToUpdateCanvas = options.minDurationToUpdateCanvas ?? 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,6 +234,9 @@ class BasePDFPageView {
|
|||||||
// triggering this callback.
|
// triggering this callback.
|
||||||
if (renderTask === this.renderTask) {
|
if (renderTask === this.renderTask) {
|
||||||
this.renderTask = null;
|
this.renderTask = null;
|
||||||
|
if (this.enableOptimizedPartialRendering) {
|
||||||
|
this.recordedGroups ??= renderTask.recordedGroups;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.renderingState = RenderingStates.FINISHED;
|
this.renderingState = RenderingStates.FINISHED;
|
||||||
|
|||||||
@ -112,3 +112,34 @@
|
|||||||
background-color: rgb(255 255 255 / 0.6);
|
background-color: rgb(255 255 255 / 0.6);
|
||||||
color: rgb(0 0 0);
|
color: rgb(0 0 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pdfBugGroupsLayer {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
position: absolute;
|
||||||
|
outline-color: red;
|
||||||
|
outline-width: 2px;
|
||||||
|
|
||||||
|
--hover-outline-style: solid !important;
|
||||||
|
--hover-background-color: rgb(255 0 0 / 0.2);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
outline-style: var(--hover-outline-style);
|
||||||
|
background-color: var(--hover-background-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showDebugBoxes & {
|
||||||
|
outline-style: dashed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.showDebugBoxes {
|
||||||
|
.pdfBugGroupsLayer {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
146
web/debugger.mjs
146
web/debugger.mjs
@ -200,6 +200,10 @@ const StepperManager = (function StepperManagerClosure() {
|
|||||||
active: false,
|
active: false,
|
||||||
// Stepper specific functions.
|
// Stepper specific functions.
|
||||||
create(pageIndex) {
|
create(pageIndex) {
|
||||||
|
const pageContainer = document.querySelector(
|
||||||
|
`#viewer div[data-page-number="${pageIndex + 1}"]`
|
||||||
|
);
|
||||||
|
|
||||||
const debug = document.createElement("div");
|
const debug = document.createElement("div");
|
||||||
debug.id = "stepper" + pageIndex;
|
debug.id = "stepper" + pageIndex;
|
||||||
debug.hidden = true;
|
debug.hidden = true;
|
||||||
@ -210,7 +214,12 @@ const StepperManager = (function StepperManagerClosure() {
|
|||||||
b.value = pageIndex;
|
b.value = pageIndex;
|
||||||
stepperChooser.append(b);
|
stepperChooser.append(b);
|
||||||
const initBreakPoints = breakPoints[pageIndex] || [];
|
const initBreakPoints = breakPoints[pageIndex] || [];
|
||||||
const stepper = new Stepper(debug, pageIndex, initBreakPoints);
|
const stepper = new Stepper(
|
||||||
|
debug,
|
||||||
|
pageIndex,
|
||||||
|
initBreakPoints,
|
||||||
|
pageContainer
|
||||||
|
);
|
||||||
steppers.push(stepper);
|
steppers.push(stepper);
|
||||||
if (steppers.length === 1) {
|
if (steppers.length === 1) {
|
||||||
this.selectStepper(pageIndex, false);
|
this.selectStepper(pageIndex, false);
|
||||||
@ -277,7 +286,7 @@ class Stepper {
|
|||||||
return simpleObj;
|
return simpleObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(panel, pageIndex, initialBreakPoints) {
|
constructor(panel, pageIndex, initialBreakPoints, pageContainer) {
|
||||||
this.panel = panel;
|
this.panel = panel;
|
||||||
this.breakPoint = 0;
|
this.breakPoint = 0;
|
||||||
this.nextBreakPoint = null;
|
this.nextBreakPoint = null;
|
||||||
@ -286,11 +295,20 @@ class Stepper {
|
|||||||
this.currentIdx = -1;
|
this.currentIdx = -1;
|
||||||
this.operatorListIdx = 0;
|
this.operatorListIdx = 0;
|
||||||
this.indentLevel = 0;
|
this.indentLevel = 0;
|
||||||
|
this.operatorGroups = null;
|
||||||
|
this.pageContainer = pageContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
init(operatorList) {
|
init(operatorList) {
|
||||||
const panel = this.panel;
|
const panel = this.panel;
|
||||||
const content = this.#c("div", "c=continue, s=step");
|
const content = this.#c("div", "c=continue, s=step");
|
||||||
|
|
||||||
|
const showBoxesToggle = this.#c("label", "Show bounding boxes");
|
||||||
|
const showBoxesCheckbox = this.#c("input");
|
||||||
|
showBoxesCheckbox.type = "checkbox";
|
||||||
|
showBoxesToggle.prepend(showBoxesCheckbox);
|
||||||
|
content.append(this.#c("br"), showBoxesToggle);
|
||||||
|
|
||||||
const table = this.#c("table");
|
const table = this.#c("table");
|
||||||
content.append(table);
|
content.append(table);
|
||||||
table.cellSpacing = 0;
|
table.cellSpacing = 0;
|
||||||
@ -305,6 +323,22 @@ class Stepper {
|
|||||||
panel.append(content);
|
panel.append(content);
|
||||||
this.table = table;
|
this.table = table;
|
||||||
this.updateOperatorList(operatorList);
|
this.updateOperatorList(operatorList);
|
||||||
|
|
||||||
|
const hoverStyle = this.#c("style");
|
||||||
|
this.hoverStyle = hoverStyle;
|
||||||
|
content.prepend(hoverStyle);
|
||||||
|
table.addEventListener("mouseover", this.#handleStepHover.bind(this));
|
||||||
|
table.addEventListener("mouseleave", e => {
|
||||||
|
hoverStyle.innerText = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
showBoxesCheckbox.addEventListener("change", () => {
|
||||||
|
if (showBoxesCheckbox.checked) {
|
||||||
|
this.pageContainer.classList.add("showDebugBoxes");
|
||||||
|
} else {
|
||||||
|
this.pageContainer.classList.remove("showDebugBoxes");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOperatorList(operatorList) {
|
updateOperatorList(operatorList) {
|
||||||
@ -405,6 +439,114 @@ class Stepper {
|
|||||||
this.table.append(chunk);
|
this.table.append(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setOperatorGroups(groups) {
|
||||||
|
let boxesContainer = this.pageContainer.querySelector(".pdfBugGroupsLayer");
|
||||||
|
if (!boxesContainer) {
|
||||||
|
boxesContainer = this.#c("div");
|
||||||
|
boxesContainer.classList.add("pdfBugGroupsLayer");
|
||||||
|
this.pageContainer.append(boxesContainer);
|
||||||
|
|
||||||
|
boxesContainer.addEventListener(
|
||||||
|
"click",
|
||||||
|
this.#handleDebugBoxClick.bind(this)
|
||||||
|
);
|
||||||
|
boxesContainer.addEventListener(
|
||||||
|
"mouseover",
|
||||||
|
this.#handleDebugBoxHover.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
boxesContainer.innerHTML = "";
|
||||||
|
|
||||||
|
groups = groups.toSorted((a, b) => {
|
||||||
|
const diffs = [
|
||||||
|
a.minX - b.minX,
|
||||||
|
a.minY - b.minY,
|
||||||
|
b.maxX - a.maxX,
|
||||||
|
b.maxY - a.maxY,
|
||||||
|
];
|
||||||
|
for (const diff of diffs) {
|
||||||
|
if (Math.abs(diff) > 0.01) {
|
||||||
|
return Math.sign(diff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const diff of diffs) {
|
||||||
|
if (Math.abs(diff) > 0.0001) {
|
||||||
|
return Math.sign(diff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleStepHover(e) {
|
||||||
|
const tr = e.target.closest("tr");
|
||||||
|
if (!tr || tr.dataset.idx === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = +tr.dataset.idx;
|
||||||
|
|
||||||
|
const closestGroupIndex =
|
||||||
|
this.operatorGroups?.findIndex(({ idx }) => idx === index) ?? -1;
|
||||||
|
if (closestGroupIndex === -1) {
|
||||||
|
this.hoverStyle.innerText = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#highlightStepsGroup(closestGroupIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleDebugBoxHover(e) {
|
||||||
|
if (e.target.dataset.groupIdx === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupIdx = Number(e.target.dataset.groupIdx);
|
||||||
|
this.#highlightStepsGroup(groupIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleDebugBoxClick(e) {
|
||||||
|
if (e.target.dataset.groupIdx === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupIdx = Number(e.target.dataset.groupIdx);
|
||||||
|
const group = this.operatorGroups[groupIdx];
|
||||||
|
|
||||||
|
this.table.childNodes[group.idx].scrollIntoView();
|
||||||
|
}
|
||||||
|
|
||||||
|
#highlightStepsGroup(groupIndex) {
|
||||||
|
const group = this.operatorGroups[groupIndex];
|
||||||
|
|
||||||
|
this.hoverStyle.innerText = `#${this.panel.id} tr[data-idx="${group.idx}"] { background-color: rgba(0, 0, 0, 0.1); }`;
|
||||||
|
|
||||||
|
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 += `
|
||||||
|
#viewer [data-page-number="${this.pageIndex + 1}"] .pdfBugGroupsLayer :nth-child(${groupIndex + 1}) {
|
||||||
|
background-color: var(--hover-background-color);
|
||||||
|
outline-style: var(--hover-outline-style);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
getNextBreakPoint() {
|
getNextBreakPoint() {
|
||||||
this.breakPoints.sort((a, b) => a - b);
|
this.breakPoints.sort((a, b) => a - b);
|
||||||
for (const breakPoint of this.breakPoints) {
|
for (const breakPoint of this.breakPoints) {
|
||||||
|
|||||||
@ -186,6 +186,55 @@ class PDFPageDetailView extends BasePDFPageView {
|
|||||||
this.reset({ keepCanvas: true });
|
this.reset({ keepCanvas: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getRenderingContext(canvas, transform) {
|
||||||
|
const baseContext = this.pageView._getRenderingContext(canvas, transform);
|
||||||
|
const recordedGroups = this.pdfPage.recordedGroups;
|
||||||
|
|
||||||
|
if (!recordedGroups || !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;
|
||||||
|
const {
|
||||||
|
width: aWidth,
|
||||||
|
height: aHeight,
|
||||||
|
minX: aMinX,
|
||||||
|
minY: aMinY,
|
||||||
|
} = this.#detailArea;
|
||||||
|
|
||||||
|
const detailMinX = aMinX / vWidth;
|
||||||
|
const detailMinY = aMinY / vHeight;
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async draw() {
|
async draw() {
|
||||||
// The PDFPageView might have already dropped this PDFPageDetailView. In
|
// The PDFPageView might have already dropped this PDFPageDetailView. In
|
||||||
// that case, simply do nothing.
|
// that case, simply do nothing.
|
||||||
@ -249,7 +298,7 @@ class PDFPageDetailView extends BasePDFPageView {
|
|||||||
style.left = `${(area.minX * 100) / width}%`;
|
style.left = `${(area.minX * 100) / width}%`;
|
||||||
|
|
||||||
const renderingPromise = this._drawCanvas(
|
const renderingPromise = this._drawCanvas(
|
||||||
this.pageView._getRenderingContext(canvas, transform),
|
this._getRenderingContext(canvas, transform),
|
||||||
() => {
|
() => {
|
||||||
// If the rendering is cancelled, keep the old canvas visible.
|
// If the rendering is cancelled, keep the old canvas visible.
|
||||||
this.canvas?.remove();
|
this.canvas?.remove();
|
||||||
|
|||||||
@ -89,6 +89,11 @@ import { XfaLayerBuilder } from "./xfa_layer_builder.js";
|
|||||||
* `maxCanvasDim`, it will draw a second canvas on top of the CSS-zoomed one,
|
* `maxCanvasDim`, it will draw a second canvas on top of the CSS-zoomed one,
|
||||||
* that only renders the part of the page that is close to the viewport.
|
* that only renders the part of the page that is close to the viewport.
|
||||||
* The default value is `true`.
|
* The default value is `true`.
|
||||||
|
* @property {boolean} [enableOptimizedPartialRendering] - When enabled, PDF
|
||||||
|
* rendering will keep track of which areas of the page each PDF operation
|
||||||
|
* affects. Then, when rendering a partial page (if `enableDetailCanvas` is
|
||||||
|
* enabled), it will only run through the operations that affect that portion.
|
||||||
|
* The default value is `false`.
|
||||||
* @property {Object} [pageColors] - Overwrites background and foreground colors
|
* @property {Object} [pageColors] - Overwrites background and foreground colors
|
||||||
* with user defined ones in order to improve readability in high contrast
|
* with user defined ones in order to improve readability in high contrast
|
||||||
* mode.
|
* mode.
|
||||||
@ -538,6 +543,8 @@ class PDFPageView extends BasePDFPageView {
|
|||||||
keepCanvasWrapper = false,
|
keepCanvasWrapper = false,
|
||||||
preserveDetailViewState = false,
|
preserveDetailViewState = false,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
|
const keepPdfBugGroups = this.pdfPage?._pdfBug ?? false;
|
||||||
|
|
||||||
this.cancelRendering({
|
this.cancelRendering({
|
||||||
keepAnnotationLayer,
|
keepAnnotationLayer,
|
||||||
keepAnnotationEditorLayer,
|
keepAnnotationEditorLayer,
|
||||||
@ -566,6 +573,9 @@ class PDFPageView extends BasePDFPageView {
|
|||||||
case canvasWrapperNode:
|
case canvasWrapperNode:
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (keepPdfBugGroups && node.classList.contains("pdfBugGroupsLayer")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
node.remove();
|
node.remove();
|
||||||
const layerIndex = this.#layers.indexOf(node);
|
const layerIndex = this.#layers.indexOf(node);
|
||||||
if (layerIndex >= 0) {
|
if (layerIndex >= 0) {
|
||||||
@ -634,7 +644,10 @@ class PDFPageView extends BasePDFPageView {
|
|||||||
this.maxCanvasPixels > 0 &&
|
this.maxCanvasPixels > 0 &&
|
||||||
visibleArea
|
visibleArea
|
||||||
) {
|
) {
|
||||||
this.detailView ??= new PDFPageDetailView({ pageView: this });
|
this.detailView ??= new PDFPageDetailView({
|
||||||
|
pageView: this,
|
||||||
|
enableOptimizedPartialRendering: this.enableOptimizedPartialRendering,
|
||||||
|
});
|
||||||
this.detailView.update({ visibleArea });
|
this.detailView.update({ visibleArea });
|
||||||
} else if (this.detailView) {
|
} else if (this.detailView) {
|
||||||
this.detailView.reset();
|
this.detailView.reset();
|
||||||
@ -920,6 +933,8 @@ class PDFPageView extends BasePDFPageView {
|
|||||||
annotationCanvasMap: this._annotationCanvasMap,
|
annotationCanvasMap: this._annotationCanvasMap,
|
||||||
pageColors: this.pageColors,
|
pageColors: this.pageColors,
|
||||||
isEditing: this.#isEditing,
|
isEditing: this.#isEditing,
|
||||||
|
recordOperations:
|
||||||
|
this.enableOptimizedPartialRendering && !this.recordedGroups,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -130,6 +130,10 @@ function isValidAnnotationEditorMode(mode) {
|
|||||||
* `maxCanvasDim`, it will draw a second canvas on top of the CSS-zoomed one,
|
* `maxCanvasDim`, it will draw a second canvas on top of the CSS-zoomed one,
|
||||||
* that only renders the part of the page that is close to the viewport.
|
* that only renders the part of the page that is close to the viewport.
|
||||||
* The default value is `true`.
|
* The default value is `true`.
|
||||||
|
* @property {boolean} [enableOptimizedPartialRendering] - When enabled, PDF
|
||||||
|
* rendering will keep track of which areas of the page each PDF operation
|
||||||
|
* affects. Then, when rendering a partial page (if `enableDetailCanvas` is
|
||||||
|
* enabled), it will only run through the operations that affect that portion.
|
||||||
* @property {IL10n} [l10n] - Localization service.
|
* @property {IL10n} [l10n] - Localization service.
|
||||||
* @property {boolean} [enablePermissions] - Enables PDF document permissions,
|
* @property {boolean} [enablePermissions] - Enables PDF document permissions,
|
||||||
* when they exist. The default value is `false`.
|
* when they exist. The default value is `false`.
|
||||||
@ -348,6 +352,8 @@ class PDFViewer {
|
|||||||
this.maxCanvasDim = options.maxCanvasDim;
|
this.maxCanvasDim = options.maxCanvasDim;
|
||||||
this.capCanvasAreaFactor = options.capCanvasAreaFactor;
|
this.capCanvasAreaFactor = options.capCanvasAreaFactor;
|
||||||
this.enableDetailCanvas = options.enableDetailCanvas ?? true;
|
this.enableDetailCanvas = options.enableDetailCanvas ?? true;
|
||||||
|
this.enableOptimizedPartialRendering =
|
||||||
|
options.enableOptimizedPartialRendering ?? false;
|
||||||
this.l10n = options.l10n;
|
this.l10n = options.l10n;
|
||||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||||
this.l10n ||= new GenericL10n();
|
this.l10n ||= new GenericL10n();
|
||||||
@ -1038,6 +1044,8 @@ class PDFViewer {
|
|||||||
maxCanvasDim: this.maxCanvasDim,
|
maxCanvasDim: this.maxCanvasDim,
|
||||||
capCanvasAreaFactor: this.capCanvasAreaFactor,
|
capCanvasAreaFactor: this.capCanvasAreaFactor,
|
||||||
enableDetailCanvas: this.enableDetailCanvas,
|
enableDetailCanvas: this.enableDetailCanvas,
|
||||||
|
enableOptimizedPartialRendering:
|
||||||
|
this.enableOptimizedPartialRendering,
|
||||||
pageColors,
|
pageColors,
|
||||||
l10n: this.l10n,
|
l10n: this.l10n,
|
||||||
layerProperties: this._layerProperties,
|
layerProperties: this._layerProperties,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user