Add logic to track rendering area of various PDF ops
This commit is a first step towards #6419, and it can also help with first compute which ops can affect what is visible in that part of the page. This commit adds logic to track operations with their respective bounding boxes. Only operations that actually cause something to be rendered have a bounding box and dependencies. Consider the following example: ``` 0. setFillRGBColor 1. beginText 2. showText "Hello" 3. endText 4. constructPath [...] -> eoFill ``` here we have three rendering operations: the showText op (2) and the path (4). (2) depends on (0), (1) and (3), while (4) only depends on (0). Both (2) and (4) have a bounding box. This tracking happens when first rendering a PDF: we then use the recorded information to optimize future partial renderings of a PDF, so that we can skip operations that do not affected the PDF area on the canvas. All this logic only runs when the new `enableOptimizedPartialRendering` preference, disabled by default, is enabled. The bounding boxes and dependencies are also shown in the pdfBug stepper. When hovering over a step now: - it highlights the steps that they depend on - it highlights on the PDF itself the bounding box
This commit is contained in:
parent
e20ee99580
commit
6a22da9c2e
@ -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