mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-07-27 17:37:22 +02:00
Merge pull request #20626 from nicolo-ribaudo/images-right-click
Add support for right-clicking on images (bug 1012805)
This commit is contained in:
commit
9d093d9607
@ -38,6 +38,11 @@ import {
|
|||||||
PrintAnnotationStorage,
|
PrintAnnotationStorage,
|
||||||
SerializableEmpty,
|
SerializableEmpty,
|
||||||
} from "./annotation_storage.js";
|
} from "./annotation_storage.js";
|
||||||
|
import {
|
||||||
|
CanvasBBoxTracker,
|
||||||
|
CanvasDependencyTracker,
|
||||||
|
CanvasImagesTracker,
|
||||||
|
} from "./canvas_dependency_tracker.js";
|
||||||
import {
|
import {
|
||||||
deprecated,
|
deprecated,
|
||||||
isDataScheme,
|
isDataScheme,
|
||||||
@ -68,7 +73,6 @@ 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";
|
||||||
@ -1273,6 +1277,7 @@ 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} [recordImages] - Record the location of images in the PDF
|
||||||
* @property {boolean} [recordOperations] - Record the dependencies and bounding
|
* @property {boolean} [recordOperations] - Record the dependencies and bounding
|
||||||
* boxes of all PDF operations that render onto the canvas.
|
* boxes of all PDF operations that render onto the canvas.
|
||||||
* @property {OperationsFilter} [operationsFilter] - If provided, only
|
* @property {OperationsFilter} [operationsFilter] - If provided, only
|
||||||
@ -1357,6 +1362,7 @@ class PDFPageProxy {
|
|||||||
this.destroyed = false;
|
this.destroyed = false;
|
||||||
this.recordedBBoxes = null;
|
this.recordedBBoxes = null;
|
||||||
this.#pagesMapper = pagesMapper;
|
this.#pagesMapper = pagesMapper;
|
||||||
|
this.imageCoordinates = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1488,6 +1494,7 @@ class PDFPageProxy {
|
|||||||
pageColors = null,
|
pageColors = null,
|
||||||
printAnnotationStorage = null,
|
printAnnotationStorage = null,
|
||||||
isEditing = false,
|
isEditing = false,
|
||||||
|
recordImages = false,
|
||||||
recordOperations = false,
|
recordOperations = false,
|
||||||
operationsFilter = null,
|
operationsFilter = null,
|
||||||
}) {
|
}) {
|
||||||
@ -1539,6 +1546,7 @@ class PDFPageProxy {
|
|||||||
);
|
);
|
||||||
const shouldRecordOperations =
|
const shouldRecordOperations =
|
||||||
!this.recordedBBoxes && (recordOperations || recordForDebugger);
|
!this.recordedBBoxes && (recordOperations || recordForDebugger);
|
||||||
|
const shouldRecordImages = !this.imageCoordinates && recordImages;
|
||||||
|
|
||||||
const complete = error => {
|
const complete = error => {
|
||||||
intentState.renderTasks.delete(internalRenderTask);
|
intentState.renderTasks.delete(internalRenderTask);
|
||||||
@ -1557,6 +1565,10 @@ class PDFPageProxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldRecordImages && !error) {
|
||||||
|
this.imageCoordinates = internalRenderTask.gfx?.imagesTracker.take();
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
@ -1585,18 +1597,30 @@ class PDFPageProxy {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let dependencyTracker = null;
|
||||||
|
let bboxTracker = null;
|
||||||
|
if (shouldRecordOperations || shouldRecordImages) {
|
||||||
|
bboxTracker = new CanvasBBoxTracker(
|
||||||
|
canvas,
|
||||||
|
intentState.operatorList.length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (shouldRecordOperations) {
|
||||||
|
dependencyTracker = new CanvasDependencyTracker(
|
||||||
|
bboxTracker,
|
||||||
|
recordForDebugger
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const internalRenderTask = new InternalRenderTask({
|
const internalRenderTask = new InternalRenderTask({
|
||||||
callback: complete,
|
callback: complete,
|
||||||
// Only include the required properties, and *not* the entire object.
|
// Only include the required properties, and *not* the entire object.
|
||||||
params: {
|
params: {
|
||||||
canvas,
|
canvas,
|
||||||
canvasContext,
|
canvasContext,
|
||||||
dependencyTracker: shouldRecordOperations
|
dependencyTracker: dependencyTracker ?? bboxTracker,
|
||||||
? new CanvasDependencyTracker(
|
imagesTracker: shouldRecordImages
|
||||||
canvas,
|
? new CanvasImagesTracker(canvas)
|
||||||
intentState.operatorList.length,
|
|
||||||
recordForDebugger
|
|
||||||
)
|
|
||||||
: null,
|
: null,
|
||||||
viewport,
|
viewport,
|
||||||
transform,
|
transform,
|
||||||
@ -3288,6 +3312,10 @@ class RenderTask {
|
|||||||
(separateAnnots.canvas && annotationCanvasMap?.size > 0)
|
(separateAnnots.canvas && annotationCanvasMap?.size > 0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get imageCoordinates() {
|
||||||
|
return this._internalRenderTask.imageCoordinates || null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3345,6 +3373,7 @@ class InternalRenderTask {
|
|||||||
this._canvasContext = params.canvas ? null : params.canvasContext;
|
this._canvasContext = params.canvas ? null : params.canvasContext;
|
||||||
this._enableHWA = enableHWA;
|
this._enableHWA = enableHWA;
|
||||||
this._dependencyTracker = params.dependencyTracker;
|
this._dependencyTracker = params.dependencyTracker;
|
||||||
|
this._imagesTracker = params.imagesTracker;
|
||||||
this._operationsFilter = operationsFilter;
|
this._operationsFilter = operationsFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3375,7 +3404,13 @@ 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, dependencyTracker } = this.params;
|
const {
|
||||||
|
viewport,
|
||||||
|
transform,
|
||||||
|
background,
|
||||||
|
dependencyTracker,
|
||||||
|
imagesTracker,
|
||||||
|
} = 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.
|
||||||
@ -3395,7 +3430,8 @@ class InternalRenderTask {
|
|||||||
{ optionalContentConfig },
|
{ optionalContentConfig },
|
||||||
this.annotationCanvasMap,
|
this.annotationCanvasMap,
|
||||||
this.pageColors,
|
this.pageColors,
|
||||||
dependencyTracker
|
dependencyTracker,
|
||||||
|
imagesTracker
|
||||||
);
|
);
|
||||||
this.gfx.beginDrawing({
|
this.gfx.beginDrawing({
|
||||||
transform,
|
transform,
|
||||||
|
|||||||
@ -658,7 +658,8 @@ class CanvasGraphics {
|
|||||||
{ optionalContentConfig, markedContentStack = null },
|
{ optionalContentConfig, markedContentStack = null },
|
||||||
annotationCanvasMap,
|
annotationCanvasMap,
|
||||||
pageColors,
|
pageColors,
|
||||||
dependencyTracker
|
dependencyTracker,
|
||||||
|
imagesTracker
|
||||||
) {
|
) {
|
||||||
this.ctx = canvasCtx;
|
this.ctx = canvasCtx;
|
||||||
this.current = new CanvasExtraState(
|
this.current = new CanvasExtraState(
|
||||||
@ -698,6 +699,7 @@ class CanvasGraphics {
|
|||||||
this._cachedBitmapsMap = new Map();
|
this._cachedBitmapsMap = new Map();
|
||||||
|
|
||||||
this.dependencyTracker = dependencyTracker ?? null;
|
this.dependencyTracker = dependencyTracker ?? null;
|
||||||
|
this.imagesTracker = imagesTracker ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getObject(opIdx, data, fallback = null) {
|
getObject(opIdx, data, fallback = null) {
|
||||||
@ -3064,11 +3066,19 @@ class CanvasGraphics {
|
|||||||
imgData.interpolate
|
imgData.interpolate
|
||||||
);
|
);
|
||||||
|
|
||||||
this.dependencyTracker
|
if (this.dependencyTracker) {
|
||||||
?.resetBBox(opIdx)
|
this.dependencyTracker
|
||||||
.recordBBox(opIdx, ctx, 0, width, -height, 0)
|
.resetBBox(opIdx)
|
||||||
.recordDependencies(opIdx, Dependencies.imageXObject)
|
.recordBBox(opIdx, ctx, 0, width, -height, 0)
|
||||||
.recordOperation(opIdx);
|
.recordDependencies(opIdx, Dependencies.imageXObject)
|
||||||
|
.recordOperation(opIdx);
|
||||||
|
this.imagesTracker?.record(
|
||||||
|
ctx,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
this.dependencyTracker.clipBox
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
drawImageAtIntegerCoords(
|
drawImageAtIntegerCoords(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Util } from "../shared/util.js";
|
import { FeatureTest, Util } from "../shared/util.js";
|
||||||
|
|
||||||
const FORCED_DEPENDENCY_LABEL = "__forcedDependency";
|
const FORCED_DEPENDENCY_LABEL = "__forcedDependency";
|
||||||
|
|
||||||
@ -71,6 +71,323 @@ const ensureDebugMetadata = (map, key) =>
|
|||||||
isRenderingOperation: false,
|
isRenderingOperation: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// NOTE: CanvasBBoxTracker, CanvasDependencyTracker and
|
||||||
|
// CanvasNestedDependencyTracker must all have the same interface.
|
||||||
|
|
||||||
|
class CanvasBBoxTracker {
|
||||||
|
#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;
|
||||||
|
|
||||||
|
#canvasWidth;
|
||||||
|
|
||||||
|
#canvasHeight;
|
||||||
|
|
||||||
|
// Uint8ClampedArray<minX, minY, maxX, maxY>
|
||||||
|
#bboxesCoords;
|
||||||
|
|
||||||
|
#bboxes;
|
||||||
|
|
||||||
|
_savesStack = [];
|
||||||
|
|
||||||
|
_markedContentStack = [];
|
||||||
|
|
||||||
|
constructor(canvas, operationsCount) {
|
||||||
|
this.#canvasWidth = canvas.width;
|
||||||
|
this.#canvasHeight = canvas.height;
|
||||||
|
this.#initializeBBoxes(operationsCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
growOperationsCount(operationsCount) {
|
||||||
|
if (operationsCount >= this.#bboxes.length) {
|
||||||
|
this.#initializeBBoxes(operationsCount, this.#bboxes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#initializeBBoxes(operationsCount, oldBBoxes) {
|
||||||
|
const buffer = new ArrayBuffer(operationsCount * 4);
|
||||||
|
this.#bboxesCoords = new Uint8ClampedArray(buffer);
|
||||||
|
this.#bboxes = new Uint32Array(buffer);
|
||||||
|
if (oldBBoxes && oldBBoxes.length > 0) {
|
||||||
|
this.#bboxes.set(oldBBoxes);
|
||||||
|
this.#bboxes.fill(EMPTY_BBOX, oldBBoxes.length);
|
||||||
|
} else {
|
||||||
|
this.#bboxes.fill(EMPTY_BBOX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get clipBox() {
|
||||||
|
return this.#clipBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
save(opIdx) {
|
||||||
|
this.#clipBox = { __proto__: this.#clipBox };
|
||||||
|
this._savesStack.push(opIdx);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
restore(opIdx, onSavePopped) {
|
||||||
|
const previous = Object.getPrototypeOf(this.#clipBox);
|
||||||
|
if (previous === null) {
|
||||||
|
// Sometimes we call more .restore() than .save(), for
|
||||||
|
// example when using CanvasGraphics' #restoreInitialState()
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
this.#clipBox = previous;
|
||||||
|
|
||||||
|
const lastSave = this._savesStack.pop();
|
||||||
|
if (lastSave !== undefined) {
|
||||||
|
onSavePopped?.(lastSave, opIdx);
|
||||||
|
this.#bboxes[opIdx] = this.#bboxes[lastSave];
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} idx
|
||||||
|
*/
|
||||||
|
recordOpenMarker(idx) {
|
||||||
|
this._savesStack.push(idx);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOpenMarker() {
|
||||||
|
if (this._savesStack.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this._savesStack.at(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
recordCloseMarker(opIdx, onSavePopped) {
|
||||||
|
const lastSave = this._savesStack.pop();
|
||||||
|
if (lastSave !== undefined) {
|
||||||
|
onSavePopped?.(lastSave, opIdx);
|
||||||
|
this.#bboxes[opIdx] = this.#bboxes[lastSave];
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marked content needs a separate stack from save/restore, because they
|
||||||
|
// form two independent trees.
|
||||||
|
beginMarkedContent(opIdx) {
|
||||||
|
this._markedContentStack.push(opIdx);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
endMarkedContent(opIdx, onSavePopped) {
|
||||||
|
const lastSave = this._markedContentStack.pop();
|
||||||
|
if (lastSave !== undefined) {
|
||||||
|
onSavePopped?.(lastSave, opIdx);
|
||||||
|
this.#bboxes[opIdx] = this.#bboxes[lastSave];
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetBBox(idx) {
|
||||||
|
if (this._pendingBBoxIdx !== idx) {
|
||||||
|
this._pendingBBoxIdx = idx;
|
||||||
|
this.#pendingBBox[0] = Infinity;
|
||||||
|
this.#pendingBBox[1] = Infinity;
|
||||||
|
this.#pendingBBox[2] = -Infinity;
|
||||||
|
this.#pendingBBox[3] = -Infinity;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} idx
|
||||||
|
*/
|
||||||
|
recordOperation(idx, preserve = false, dependencyLists) {
|
||||||
|
if (this._pendingBBoxIdx !== idx) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minX = floor((this.#pendingBBox[0] * 256) / this.#canvasWidth);
|
||||||
|
const minY = floor((this.#pendingBBox[1] * 256) / this.#canvasHeight);
|
||||||
|
const maxX = ceil((this.#pendingBBox[2] * 256) / this.#canvasWidth);
|
||||||
|
const maxY = ceil((this.#pendingBBox[3] * 256) / this.#canvasHeight);
|
||||||
|
|
||||||
|
expandBBox(this.#bboxesCoords, idx, minX, minY, maxX, maxY);
|
||||||
|
if (dependencyLists) {
|
||||||
|
for (const dependencies of dependencyLists) {
|
||||||
|
for (const depIdx of dependencies) {
|
||||||
|
if (depIdx !== idx) {
|
||||||
|
expandBBox(this.#bboxesCoords, depIdx, minX, minY, maxX, maxY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preserve) {
|
||||||
|
this._pendingBBoxIdx = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
bboxToClipBoxDropOperation(idx) {
|
||||||
|
if (this._pendingBBoxIdx === idx) {
|
||||||
|
this._pendingBBoxIdx = -1;
|
||||||
|
|
||||||
|
this.#clipBox[0] = Math.max(this.#clipBox[0], this.#pendingBBox[0]);
|
||||||
|
this.#clipBox[1] = Math.max(this.#clipBox[1], this.#pendingBBox[1]);
|
||||||
|
this.#clipBox[2] = Math.min(this.#clipBox[2], this.#pendingBBox[2]);
|
||||||
|
this.#clipBox[3] = Math.min(this.#clipBox[3], this.#pendingBBox[3]);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
take() {
|
||||||
|
return new BBoxReader(this.#bboxes, this.#bboxesCoords);
|
||||||
|
}
|
||||||
|
|
||||||
|
takeDebugMetadata() {
|
||||||
|
throw new Error("Unreachable");
|
||||||
|
}
|
||||||
|
|
||||||
|
recordSimpleData(name, idx) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordIncrementalData(name, idx) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetIncrementalData(name, idx) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordNamedData(name, idx) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordSimpleDataFromNamed(name, depName, fallbackIdx) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordFutureForcedDependency(name, idx) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inheritSimpleDataAsFutureForcedDependencies(names) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inheritPendingDependenciesAsFutureForcedDependencies() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordCharacterBBox(idx, ctx, font, scale = 1, x = 0, y = 0, getMeasure) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSimpleIndex(dependencyName) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordDependencies(idx, dependencyNames) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordNamedDependency(idx, name) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordShowTextOperation(idx, preserve = false) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {"lineWidth" | "lineCap" | "lineJoin" | "miterLimit" | "dash" |
|
* @typedef {"lineWidth" | "lineCap" | "lineJoin" | "miterLimit" | "dash" |
|
||||||
* "strokeAlpha" | "fillColor" | "fillAlpha" | "globalCompositeOperation" |
|
* "strokeAlpha" | "fillColor" | "fillAlpha" | "globalCompositeOperation" |
|
||||||
@ -100,61 +417,34 @@ class CanvasDependencyTracker {
|
|||||||
|
|
||||||
#namedDependencies = new Map();
|
#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();
|
#pendingDependencies = new Set();
|
||||||
|
|
||||||
#operations = new Map();
|
|
||||||
|
|
||||||
#fontBBoxTrustworthy = new Map();
|
#fontBBoxTrustworthy = new Map();
|
||||||
|
|
||||||
#canvasWidth;
|
|
||||||
|
|
||||||
#canvasHeight;
|
|
||||||
|
|
||||||
// Uint8ClampedArray<minX, minY, maxX, maxY>
|
|
||||||
#bboxesCoords;
|
|
||||||
|
|
||||||
#bboxes;
|
|
||||||
|
|
||||||
#debugMetadata;
|
#debugMetadata;
|
||||||
|
|
||||||
constructor(canvas, operationsCount, recordDebugMetadata = false) {
|
#recordDebugMetadataDepenencyAfterRestore;
|
||||||
this.#canvasWidth = canvas.width;
|
|
||||||
this.#canvasHeight = canvas.height;
|
#bboxTracker;
|
||||||
this.#initializeBBoxes(operationsCount);
|
|
||||||
|
constructor(bboxTracker, recordDebugMetadata = false) {
|
||||||
|
this.#bboxTracker = bboxTracker;
|
||||||
if (recordDebugMetadata) {
|
if (recordDebugMetadata) {
|
||||||
this.#debugMetadata = new Map();
|
this.#debugMetadata = new Map();
|
||||||
|
this.#recordDebugMetadataDepenencyAfterRestore = (lastSave, opIdx) => {
|
||||||
|
ensureDebugMetadata(this.#debugMetadata, opIdx).dependencies.add(
|
||||||
|
lastSave
|
||||||
|
);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get clipBox() {
|
||||||
|
return this.#bboxTracker.clipBox;
|
||||||
|
}
|
||||||
|
|
||||||
growOperationsCount(operationsCount) {
|
growOperationsCount(operationsCount) {
|
||||||
if (operationsCount >= this.#bboxes.length) {
|
this.#bboxTracker.growOperationsCount(operationsCount);
|
||||||
this.#initializeBBoxes(operationsCount, this.#bboxes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#initializeBBoxes(operationsCount, oldBBoxes) {
|
|
||||||
const buffer = new ArrayBuffer(operationsCount * 4);
|
|
||||||
this.#bboxesCoords = new Uint8ClampedArray(buffer);
|
|
||||||
this.#bboxes = new Uint32Array(buffer);
|
|
||||||
if (oldBBoxes && oldBBoxes.length > 0) {
|
|
||||||
this.#bboxes.set(oldBBoxes);
|
|
||||||
this.#bboxes.fill(EMPTY_BBOX, oldBBoxes.length);
|
|
||||||
} else {
|
|
||||||
this.#bboxes.fill(EMPTY_BBOX);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
save(opIdx) {
|
save(opIdx) {
|
||||||
@ -168,13 +458,17 @@ class CanvasDependencyTracker {
|
|||||||
__proto__: this.#incremental[FORCED_DEPENDENCY_LABEL],
|
__proto__: this.#incremental[FORCED_DEPENDENCY_LABEL],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.#clipBox = { __proto__: this.#clipBox };
|
this.#bboxTracker.save(opIdx);
|
||||||
this.#savesStack.push(opIdx);
|
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
restore(opIdx) {
|
restore(opIdx) {
|
||||||
|
this.#bboxTracker.restore(
|
||||||
|
opIdx,
|
||||||
|
this.#recordDebugMetadataDepenencyAfterRestore
|
||||||
|
);
|
||||||
|
|
||||||
const previous = Object.getPrototypeOf(this.#simple);
|
const previous = Object.getPrototypeOf(this.#simple);
|
||||||
if (previous === null) {
|
if (previous === null) {
|
||||||
// Sometimes we call more .restore() than .save(), for
|
// Sometimes we call more .restore() than .save(), for
|
||||||
@ -183,77 +477,53 @@ class CanvasDependencyTracker {
|
|||||||
}
|
}
|
||||||
this.#simple = previous;
|
this.#simple = previous;
|
||||||
this.#incremental = Object.getPrototypeOf(this.#incremental);
|
this.#incremental = Object.getPrototypeOf(this.#incremental);
|
||||||
this.#clipBox = Object.getPrototypeOf(this.#clipBox);
|
|
||||||
|
|
||||||
const lastSave = this.#savesStack.pop();
|
|
||||||
if (lastSave !== undefined) {
|
|
||||||
ensureDebugMetadata(this.#debugMetadata, opIdx)?.dependencies.add(
|
|
||||||
lastSave
|
|
||||||
);
|
|
||||||
this.#bboxes[opIdx] = this.#bboxes[lastSave];
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
recordOpenMarker(opIdx) {
|
||||||
* @param {number} idx
|
this.#bboxTracker.recordOpenMarker(
|
||||||
*/
|
opIdx,
|
||||||
recordOpenMarker(idx) {
|
this.#recordDebugMetadataDepenencyAfterRestore
|
||||||
this.#savesStack.push(idx);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
getOpenMarker() {
|
|
||||||
if (this.#savesStack.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return this.#savesStack.at(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
recordCloseMarker(opIdx) {
|
|
||||||
const lastSave = this.#savesStack.pop();
|
|
||||||
if (lastSave !== undefined) {
|
|
||||||
ensureDebugMetadata(this.#debugMetadata, opIdx)?.dependencies.add(
|
|
||||||
lastSave
|
|
||||||
);
|
|
||||||
this.#bboxes[opIdx] = this.#bboxes[lastSave];
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marked content needs a separate stack from save/restore, because they
|
|
||||||
// form two independent trees.
|
|
||||||
beginMarkedContent(opIdx) {
|
|
||||||
this.#markedContentStack.push(opIdx);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
endMarkedContent(opIdx) {
|
|
||||||
const lastSave = this.#markedContentStack.pop();
|
|
||||||
if (lastSave !== undefined) {
|
|
||||||
ensureDebugMetadata(this.#debugMetadata, opIdx)?.dependencies.add(
|
|
||||||
lastSave
|
|
||||||
);
|
|
||||||
this.#bboxes[opIdx] = this.#bboxes[lastSave];
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
pushBaseTransform(ctx) {
|
|
||||||
this.#baseTransformStack.push(
|
|
||||||
Util.multiplyByDOMMatrix(
|
|
||||||
this.#baseTransformStack.at(-1),
|
|
||||||
ctx.getTransform()
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOpenMarker() {
|
||||||
|
return this.#bboxTracker.getOpenMarker();
|
||||||
|
}
|
||||||
|
|
||||||
|
recordCloseMarker(opIdx) {
|
||||||
|
this.#bboxTracker.recordCloseMarker(
|
||||||
|
opIdx,
|
||||||
|
this.#recordDebugMetadataDepenencyAfterRestore
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} opIdx
|
||||||
|
*/
|
||||||
|
beginMarkedContent(opIdx) {
|
||||||
|
this.#bboxTracker.beginMarkedContent(opIdx);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
endMarkedContent(opIdx) {
|
||||||
|
this.#bboxTracker.endMarkedContent(
|
||||||
|
opIdx,
|
||||||
|
this.#recordDebugMetadataDepenencyAfterRestore
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushBaseTransform(ctx) {
|
||||||
|
this.#bboxTracker.pushBaseTransform(ctx);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
popBaseTransform() {
|
popBaseTransform() {
|
||||||
if (this.#baseTransformStack.length > 1) {
|
this.#bboxTracker.popBaseTransform();
|
||||||
this.#baseTransformStack.pop();
|
|
||||||
}
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,73 +593,17 @@ class CanvasDependencyTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resetBBox(idx) {
|
resetBBox(idx) {
|
||||||
if (this.#pendingBBoxIdx !== idx) {
|
this.#bboxTracker.resetBBox(idx);
|
||||||
this.#pendingBBoxIdx = idx;
|
|
||||||
this.#pendingBBox[0] = Infinity;
|
|
||||||
this.#pendingBBox[1] = Infinity;
|
|
||||||
this.#pendingBBox[2] = -Infinity;
|
|
||||||
this.#pendingBBox[3] = -Infinity;
|
|
||||||
}
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
recordClipBox(idx, ctx, minX, maxX, minY, maxY) {
|
recordClipBox(idx, ctx, minX, maxX, minY, maxY) {
|
||||||
const transform = Util.multiplyByDOMMatrix(
|
this.#bboxTracker.recordClipBox(idx, ctx, minX, maxX, minY, maxY);
|
||||||
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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
recordBBox(idx, ctx, minX, maxX, minY, maxY) {
|
recordBBox(idx, ctx, minX, maxX, minY, maxY) {
|
||||||
const clipBox = this.#clipBox;
|
this.#bboxTracker.recordBBox(idx, ctx, minX, maxX, minY, maxY);
|
||||||
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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -471,11 +685,7 @@ class CanvasDependencyTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
recordFullPageBBox(idx) {
|
recordFullPageBBox(idx) {
|
||||||
this.#pendingBBox[0] = Math.max(0, this.#clipBox[0]);
|
this.#bboxTracker.recordFullPageBBox(idx);
|
||||||
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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -516,39 +726,25 @@ class CanvasDependencyTracker {
|
|||||||
const metadata = ensureDebugMetadata(this.#debugMetadata, idx);
|
const metadata = ensureDebugMetadata(this.#debugMetadata, idx);
|
||||||
const { dependencies } = metadata;
|
const { dependencies } = metadata;
|
||||||
this.#pendingDependencies.forEach(dependencies.add, dependencies);
|
this.#pendingDependencies.forEach(dependencies.add, dependencies);
|
||||||
this.#savesStack.forEach(dependencies.add, dependencies);
|
this.#bboxTracker._savesStack.forEach(dependencies.add, dependencies);
|
||||||
this.#markedContentStack.forEach(dependencies.add, dependencies);
|
this.#bboxTracker._markedContentStack.forEach(
|
||||||
|
dependencies.add,
|
||||||
|
dependencies
|
||||||
|
);
|
||||||
dependencies.delete(idx);
|
dependencies.delete(idx);
|
||||||
metadata.isRenderingOperation = true;
|
metadata.isRenderingOperation = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.#pendingBBoxIdx === idx) {
|
const needsCleanup = !preserve && idx === this.#bboxTracker._pendingBBoxIdx;
|
||||||
const minX = floor((this.#pendingBBox[0] * 256) / this.#canvasWidth);
|
|
||||||
const minY = floor((this.#pendingBBox[1] * 256) / this.#canvasHeight);
|
|
||||||
const maxX = ceil((this.#pendingBBox[2] * 256) / this.#canvasWidth);
|
|
||||||
const maxY = ceil((this.#pendingBBox[3] * 256) / this.#canvasHeight);
|
|
||||||
|
|
||||||
expandBBox(this.#bboxesCoords, idx, minX, minY, maxX, maxY);
|
this.#bboxTracker.recordOperation(idx, preserve, [
|
||||||
for (const depIdx of this.#pendingDependencies) {
|
this.#pendingDependencies,
|
||||||
if (depIdx !== idx) {
|
this.#bboxTracker._savesStack,
|
||||||
expandBBox(this.#bboxesCoords, depIdx, minX, minY, maxX, maxY);
|
this.#bboxTracker._markedContentStack,
|
||||||
}
|
]);
|
||||||
}
|
|
||||||
for (const saveIdx of this.#savesStack) {
|
|
||||||
if (saveIdx !== idx) {
|
|
||||||
expandBBox(this.#bboxesCoords, saveIdx, minX, minY, maxX, maxY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const saveIdx of this.#markedContentStack) {
|
|
||||||
if (saveIdx !== idx) {
|
|
||||||
expandBBox(this.#bboxesCoords, saveIdx, minX, minY, maxX, maxY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!preserve) {
|
if (needsCleanup) {
|
||||||
this.#pendingDependencies.clear();
|
this.#pendingDependencies.clear();
|
||||||
this.#pendingBBoxIdx = -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
@ -565,42 +761,17 @@ class CanvasDependencyTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bboxToClipBoxDropOperation(idx, preserve = false) {
|
bboxToClipBoxDropOperation(idx, preserve = false) {
|
||||||
if (this.#pendingBBoxIdx === idx) {
|
const needsCleanup = !preserve && idx === this.#bboxTracker._pendingBBoxIdx;
|
||||||
this.#pendingBBoxIdx = -1;
|
this.#bboxTracker.bboxToClipBoxDropOperation(idx);
|
||||||
|
if (needsCleanup) {
|
||||||
this.#clipBox[0] = Math.max(this.#clipBox[0], this.#pendingBBox[0]);
|
this.#pendingDependencies.clear();
|
||||||
this.#clipBox[1] = Math.max(this.#clipBox[1], this.#pendingBBox[1]);
|
|
||||||
this.#clipBox[2] = Math.min(this.#clipBox[2], this.#pendingBBox[2]);
|
|
||||||
this.#clipBox[3] = Math.min(this.#clipBox[3], this.#pendingBBox[3]);
|
|
||||||
|
|
||||||
if (!preserve) {
|
|
||||||
this.#pendingDependencies.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return this;
|
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() {
|
take() {
|
||||||
this.#fontBBoxTrustworthy.clear();
|
this.#fontBBoxTrustworthy.clear();
|
||||||
return new BBoxReader(this.#bboxes, this.#bboxesCoords);
|
return this.#bboxTracker.take();
|
||||||
}
|
}
|
||||||
|
|
||||||
takeDebugMetadata() {
|
takeDebugMetadata() {
|
||||||
@ -644,6 +815,10 @@ class CanvasNestedDependencyTracker {
|
|||||||
this.#ignoreBBoxes = !!ignoreBBoxes;
|
this.#ignoreBBoxes = !!ignoreBBoxes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get clipBox() {
|
||||||
|
return this.#dependencyTracker.clipBox;
|
||||||
|
}
|
||||||
|
|
||||||
growOperationsCount() {
|
growOperationsCount() {
|
||||||
throw new Error("Unreachable");
|
throw new Error("Unreachable");
|
||||||
}
|
}
|
||||||
@ -918,4 +1093,156 @@ const Dependencies = {
|
|||||||
transformAndFill: ["transform", "fillColor"],
|
transformAndFill: ["transform", "fillColor"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export { CanvasDependencyTracker, CanvasNestedDependencyTracker, Dependencies };
|
/**
|
||||||
|
* Track the locations of images in the canvas. For each image it computes
|
||||||
|
* a bounding box as a potentially rotated rectangle, matching the rotation of
|
||||||
|
* the current canvas transform.
|
||||||
|
*/
|
||||||
|
class CanvasImagesTracker {
|
||||||
|
#canvasWidth;
|
||||||
|
|
||||||
|
#canvasHeight;
|
||||||
|
|
||||||
|
#capacity = 4;
|
||||||
|
|
||||||
|
#count = 0;
|
||||||
|
|
||||||
|
// Array of [x1, y1, x2, y2, x3, y3] coordinates.
|
||||||
|
// We need three points to be able to represent a rectangle with a transform
|
||||||
|
// applied.
|
||||||
|
#coords = new CanvasImagesTracker.#CoordsArray(this.#capacity * 6);
|
||||||
|
|
||||||
|
static #CoordsArray =
|
||||||
|
(typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) ||
|
||||||
|
FeatureTest.isFloat16ArraySupported
|
||||||
|
? Float16Array
|
||||||
|
: Float32Array;
|
||||||
|
|
||||||
|
constructor(canvas) {
|
||||||
|
this.#canvasWidth = canvas.width;
|
||||||
|
this.#canvasHeight = canvas.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
record(ctx, width, height, clipBox) {
|
||||||
|
if (this.#count === this.#capacity) {
|
||||||
|
this.#capacity *= 2;
|
||||||
|
const newCoords = new CanvasImagesTracker.#CoordsArray(
|
||||||
|
this.#capacity * 6
|
||||||
|
);
|
||||||
|
newCoords.set(this.#coords);
|
||||||
|
this.#coords = newCoords;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transform = Util.domMatrixToTransform(ctx.getTransform());
|
||||||
|
|
||||||
|
// We want top left, bottom left, top right.
|
||||||
|
// (0, 0) is the bottom left corner.
|
||||||
|
let coords;
|
||||||
|
|
||||||
|
if (clipBox[0] !== Infinity) {
|
||||||
|
const bbox = [Infinity, Infinity, -Infinity, -Infinity];
|
||||||
|
Util.axialAlignedBoundingBox([0, -height, width, 0], transform, bbox);
|
||||||
|
|
||||||
|
const finalBBox = Util.intersect(clipBox, bbox);
|
||||||
|
if (!finalBBox) {
|
||||||
|
// The image is fully clipped out.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [minX, minY, maxX, maxY] = finalBBox;
|
||||||
|
|
||||||
|
if (
|
||||||
|
minX !== bbox[0] ||
|
||||||
|
minY !== bbox[1] ||
|
||||||
|
maxX !== bbox[2] ||
|
||||||
|
maxY !== bbox[3]
|
||||||
|
) {
|
||||||
|
// The clip box affects the image drawing. We need to compute a
|
||||||
|
// transform that takes the image bbox and fits it into the final bbox,
|
||||||
|
// so that we can then apply it to the original image shape (the
|
||||||
|
// non-axially-aligned rectangle).
|
||||||
|
const rotationAngle = Math.atan2(transform[1], transform[0]);
|
||||||
|
|
||||||
|
// Normalize the angle to be between 0 and 90 degrees.
|
||||||
|
const sin = Math.abs(Math.sin(rotationAngle));
|
||||||
|
const cos = Math.abs(Math.cos(rotationAngle));
|
||||||
|
|
||||||
|
if (
|
||||||
|
sin < 1e-6 ||
|
||||||
|
cos < 1e-6 ||
|
||||||
|
// The logic in the `else` case gives more accurate bounding boxes for
|
||||||
|
// rotated images, but the equation it uses does not give a result
|
||||||
|
// when the rotation is exactly 45 degrees, because there are infinite
|
||||||
|
// possible rectangles that can fit into the same bbox with that same
|
||||||
|
// 45deg rotation. Fallback to returning the whole bbox.
|
||||||
|
Math.abs(sin - cos) < 1e-6
|
||||||
|
) {
|
||||||
|
coords = [minX, minY, minX, maxY, maxX, minY];
|
||||||
|
} else {
|
||||||
|
// We cannot just scale the bbox into the original bbox, because that
|
||||||
|
// would not preserve the 90deg corners if they have been rotated.
|
||||||
|
// We instead need to find the transform that maps the original
|
||||||
|
// rectangle into the only rectangle that is rotated by the expected
|
||||||
|
// angle and fits into the final bbox.
|
||||||
|
//
|
||||||
|
// This represents the final bbox, with the top-left corner having
|
||||||
|
// coordinates (minX, minY) and the bottom-right corner having
|
||||||
|
// coordinates (maxX, maxY). Alpha is the rotation angle, and a and b
|
||||||
|
// are helper variables used to compute the effective transform.
|
||||||
|
//
|
||||||
|
// ------------b----------
|
||||||
|
// +-----------------------*----+
|
||||||
|
// | | _ -‾ \ |
|
||||||
|
// a | _ -‾ \ |
|
||||||
|
// | |alpha _ -‾ \ |
|
||||||
|
// | | _ -‾ \|
|
||||||
|
// |\ _ -‾|
|
||||||
|
// | \ _ -‾ |
|
||||||
|
// | \ _ -‾ |
|
||||||
|
// | \ _ -‾ |
|
||||||
|
// +----*-----------------------+
|
||||||
|
|
||||||
|
const finalBBoxWidth = maxX - minX;
|
||||||
|
const finalBBoxHeight = maxY - minY;
|
||||||
|
|
||||||
|
const sin2 = sin * sin;
|
||||||
|
const cos2 = cos * cos;
|
||||||
|
const cosSin = cos * sin;
|
||||||
|
const denom = cos2 - sin2;
|
||||||
|
|
||||||
|
const a = (finalBBoxHeight * cos2 - finalBBoxWidth * cosSin) / denom;
|
||||||
|
const b = (finalBBoxHeight * cosSin - finalBBoxWidth * sin2) / denom;
|
||||||
|
|
||||||
|
coords = [minX + b, minY, minX, minY + a, maxX, maxY - a];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!coords) {
|
||||||
|
coords = [0, -height, 0, 0, width, -height];
|
||||||
|
Util.applyTransform(coords, transform, 0);
|
||||||
|
Util.applyTransform(coords, transform, 2);
|
||||||
|
Util.applyTransform(coords, transform, 4);
|
||||||
|
}
|
||||||
|
coords[0] /= this.#canvasWidth;
|
||||||
|
coords[1] /= this.#canvasHeight;
|
||||||
|
coords[2] /= this.#canvasWidth;
|
||||||
|
coords[3] /= this.#canvasHeight;
|
||||||
|
coords[4] /= this.#canvasWidth;
|
||||||
|
coords[5] /= this.#canvasHeight;
|
||||||
|
this.#coords.set(coords, this.#count * 6);
|
||||||
|
this.#count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
take() {
|
||||||
|
return this.#coords.subarray(0, this.#count * 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
CanvasBBoxTracker,
|
||||||
|
CanvasDependencyTracker,
|
||||||
|
CanvasImagesTracker,
|
||||||
|
CanvasNestedDependencyTracker,
|
||||||
|
Dependencies,
|
||||||
|
};
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
/** @typedef {import("./display_utils").PageViewport} PageViewport */
|
/** @typedef {import("./display_utils").PageViewport} PageViewport */
|
||||||
/** @typedef {import("./api").TextContent} TextContent */
|
/** @typedef {import("./api").TextContent} TextContent */
|
||||||
|
/** @typedef {import("./text_layer_images").TextLayerImages} TextLayerImages */
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AbortException,
|
AbortException,
|
||||||
@ -34,6 +35,8 @@ import { OutputScale, setLayerDimensions } from "./display_utils.js";
|
|||||||
* runs.
|
* runs.
|
||||||
* @property {PageViewport} viewport - The target viewport to properly layout
|
* @property {PageViewport} viewport - The target viewport to properly layout
|
||||||
* the text runs.
|
* the text runs.
|
||||||
|
* @property {TextLayerImages} [images] - An optional TextLayerImages instance
|
||||||
|
* that handles right clicking on images.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,6 +59,8 @@ class TextLayer {
|
|||||||
|
|
||||||
#fontInspectorEnabled = !!globalThis.FontInspector?.enabled;
|
#fontInspectorEnabled = !!globalThis.FontInspector?.enabled;
|
||||||
|
|
||||||
|
#imagesHandler = null;
|
||||||
|
|
||||||
#lang = null;
|
#lang = null;
|
||||||
|
|
||||||
#layoutTextParams = null;
|
#layoutTextParams = null;
|
||||||
@ -97,7 +102,7 @@ class TextLayer {
|
|||||||
/**
|
/**
|
||||||
* @param {TextLayerParameters} options
|
* @param {TextLayerParameters} options
|
||||||
*/
|
*/
|
||||||
constructor({ textContentSource, container, viewport }) {
|
constructor({ textContentSource, images, container, viewport }) {
|
||||||
if (textContentSource instanceof ReadableStream) {
|
if (textContentSource instanceof ReadableStream) {
|
||||||
this.#textContentSource = textContentSource;
|
this.#textContentSource = textContentSource;
|
||||||
} else if (
|
} else if (
|
||||||
@ -115,6 +120,8 @@ class TextLayer {
|
|||||||
}
|
}
|
||||||
this.#container = this.#rootContainer = container;
|
this.#container = this.#rootContainer = container;
|
||||||
|
|
||||||
|
this.#imagesHandler = images;
|
||||||
|
|
||||||
this.#scale = viewport.scale * OutputScale.pixelRatio;
|
this.#scale = viewport.scale * OutputScale.pixelRatio;
|
||||||
this.#rotation = viewport.rotation;
|
this.#rotation = viewport.rotation;
|
||||||
this.#layoutTextParams = {
|
this.#layoutTextParams = {
|
||||||
@ -181,6 +188,10 @@ class TextLayer {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
|
if (this.#imagesHandler) {
|
||||||
|
this.#container.append(this.#imagesHandler.render());
|
||||||
|
}
|
||||||
|
|
||||||
const pump = () => {
|
const pump = () => {
|
||||||
this.#reader.read().then(({ value, done }) => {
|
this.#reader.read().then(({ value, done }) => {
|
||||||
if (done) {
|
if (done) {
|
||||||
|
|||||||
160
src/display/text_layer_images.js
Normal file
160
src/display/text_layer_images.js
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
/* Copyright 2026 Mozilla Foundation
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Util } from "../shared/util.js";
|
||||||
|
|
||||||
|
function percentage(value) {
|
||||||
|
return `${(value * 100).toFixed(2)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to manage paceholder <canvas> elements that, when right-clicked on,
|
||||||
|
* are populated with the corresponding image extracted from the PDF page.
|
||||||
|
*/
|
||||||
|
class TextLayerImages {
|
||||||
|
#coordinates = [];
|
||||||
|
|
||||||
|
#coordinatesByElement = new Map();
|
||||||
|
|
||||||
|
#getPageCanvas = null;
|
||||||
|
|
||||||
|
#minSize = 0;
|
||||||
|
|
||||||
|
#pageWidth = 0;
|
||||||
|
|
||||||
|
#pageHeight = 0;
|
||||||
|
|
||||||
|
static #activeImage = null;
|
||||||
|
|
||||||
|
constructor(minSize, coordinates, viewport, getPageCanvas) {
|
||||||
|
this.#minSize = minSize;
|
||||||
|
this.#coordinates = coordinates;
|
||||||
|
this.#pageWidth = viewport.rawDims.pageWidth;
|
||||||
|
this.#pageHeight = viewport.rawDims.pageHeight;
|
||||||
|
this.#getPageCanvas = getPageCanvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.className = "textLayerImages";
|
||||||
|
|
||||||
|
for (let i = 0; i < this.#coordinates.length; i += 6) {
|
||||||
|
const el = this.#createImagePlaceholder(
|
||||||
|
this.#coordinates.subarray(i, i + 6)
|
||||||
|
);
|
||||||
|
if (el) {
|
||||||
|
container.append(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container.addEventListener("contextmenu", event => {
|
||||||
|
if (!(event.target instanceof HTMLCanvasElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const imgElement = event.target;
|
||||||
|
const coords = this.#coordinatesByElement.get(imgElement);
|
||||||
|
if (!coords) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeImage = TextLayerImages.#activeImage?.deref();
|
||||||
|
if (activeImage === imgElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activeImage) {
|
||||||
|
activeImage.width = 0;
|
||||||
|
activeImage.height = 0;
|
||||||
|
}
|
||||||
|
TextLayerImages.#activeImage = new WeakRef(imgElement);
|
||||||
|
|
||||||
|
const { inverseTransform, x1, y1, width, height } = coords;
|
||||||
|
|
||||||
|
const pageCanvas = this.#getPageCanvas();
|
||||||
|
|
||||||
|
const imageX1 = Math.ceil(x1 * pageCanvas.width);
|
||||||
|
const imageY1 = Math.ceil(y1 * pageCanvas.height);
|
||||||
|
const imageX2 = Math.floor(
|
||||||
|
(x1 + width / this.#pageWidth) * pageCanvas.width
|
||||||
|
);
|
||||||
|
const imageY2 = Math.floor(
|
||||||
|
(y1 + height / this.#pageHeight) * pageCanvas.height
|
||||||
|
);
|
||||||
|
|
||||||
|
imgElement.width = imageX2 - imageX1;
|
||||||
|
imgElement.height = imageY2 - imageY1;
|
||||||
|
|
||||||
|
const ctx = imgElement.getContext("2d");
|
||||||
|
ctx.setTransform(...inverseTransform);
|
||||||
|
ctx.translate(-imageX1, -imageY1);
|
||||||
|
ctx.drawImage(pageCanvas, 0, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
#createImagePlaceholder(
|
||||||
|
[x1, y1, x2, y2, x3, y3] // top left, bottom left, top right
|
||||||
|
) {
|
||||||
|
const width = Math.hypot(
|
||||||
|
(x3 - x1) * this.#pageWidth,
|
||||||
|
(y3 - y1) * this.#pageHeight
|
||||||
|
);
|
||||||
|
const height = Math.hypot(
|
||||||
|
(x2 - x1) * this.#pageWidth,
|
||||||
|
(y2 - y1) * this.#pageHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
if (width < this.#minSize || height < this.#minSize) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transform = [
|
||||||
|
((x3 - x1) * this.#pageWidth) / width,
|
||||||
|
((y3 - y1) * this.#pageHeight) / width,
|
||||||
|
((x2 - x1) * this.#pageWidth) / height,
|
||||||
|
((y2 - y1) * this.#pageHeight) / height,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
];
|
||||||
|
const inverseTransform = Util.inverseTransform(transform);
|
||||||
|
|
||||||
|
const imgElement = document.createElement("canvas");
|
||||||
|
imgElement.className = "textLayerImagePlaceholder";
|
||||||
|
imgElement.width = 0;
|
||||||
|
imgElement.height = 0;
|
||||||
|
Object.assign(imgElement.style, {
|
||||||
|
opacity: 0,
|
||||||
|
position: "absolute",
|
||||||
|
left: percentage(x1),
|
||||||
|
top: percentage(y1),
|
||||||
|
width: percentage(width / this.#pageWidth),
|
||||||
|
height: percentage(height / this.#pageHeight),
|
||||||
|
transformOrigin: "0% 0%",
|
||||||
|
transform: `matrix(${transform.join(",")})`,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#coordinatesByElement.set(imgElement, {
|
||||||
|
inverseTransform,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
x1,
|
||||||
|
y1,
|
||||||
|
});
|
||||||
|
|
||||||
|
return imgElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { TextLayerImages };
|
||||||
@ -86,6 +86,7 @@ import { HighlightOutliner } from "./display/editor/drawers/highlight.js";
|
|||||||
import { isValidExplicitDest } from "./display/api_utils.js";
|
import { isValidExplicitDest } from "./display/api_utils.js";
|
||||||
import { SignatureExtractor } from "./display/editor/drawers/signaturedraw.js";
|
import { SignatureExtractor } from "./display/editor/drawers/signaturedraw.js";
|
||||||
import { TextLayer } from "./display/text_layer.js";
|
import { TextLayer } from "./display/text_layer.js";
|
||||||
|
import { TextLayerImages } from "./display/text_layer_images.js";
|
||||||
import { TouchManager } from "./display/touch_manager.js";
|
import { TouchManager } from "./display/touch_manager.js";
|
||||||
import { XfaLayer } from "./display/xfa_layer.js";
|
import { XfaLayer } from "./display/xfa_layer.js";
|
||||||
|
|
||||||
@ -149,6 +150,7 @@ globalThis.pdfjsLib = {
|
|||||||
stopEvent,
|
stopEvent,
|
||||||
SupportedImageMimeTypes,
|
SupportedImageMimeTypes,
|
||||||
TextLayer,
|
TextLayer,
|
||||||
|
TextLayerImages,
|
||||||
TouchManager,
|
TouchManager,
|
||||||
updateUrlHash,
|
updateUrlHash,
|
||||||
Util,
|
Util,
|
||||||
@ -211,6 +213,7 @@ export {
|
|||||||
stopEvent,
|
stopEvent,
|
||||||
SupportedImageMimeTypes,
|
SupportedImageMimeTypes,
|
||||||
TextLayer,
|
TextLayer,
|
||||||
|
TextLayerImages,
|
||||||
TouchManager,
|
TouchManager,
|
||||||
updateUrlHash,
|
updateUrlHash,
|
||||||
Util,
|
Util,
|
||||||
|
|||||||
@ -43,6 +43,7 @@ async function runTests(results) {
|
|||||||
"stamp_editor_spec.mjs",
|
"stamp_editor_spec.mjs",
|
||||||
"text_field_spec.mjs",
|
"text_field_spec.mjs",
|
||||||
"text_layer_spec.mjs",
|
"text_layer_spec.mjs",
|
||||||
|
"text_layer_images_spec.mjs",
|
||||||
"thumbnail_view_spec.mjs",
|
"thumbnail_view_spec.mjs",
|
||||||
"viewer_spec.mjs",
|
"viewer_spec.mjs",
|
||||||
],
|
],
|
||||||
|
|||||||
289
test/integration/text_layer_images_spec.mjs
Normal file
289
test/integration/text_layer_images_spec.mjs
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
/* Copyright 2026 Mozilla Foundation
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { closePages, loadAndWait } from "./test_utils.mjs";
|
||||||
|
|
||||||
|
describe("Text layer images", () => {
|
||||||
|
describe("basic", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
pages = await loadAndWait(
|
||||||
|
"images.pdf",
|
||||||
|
`.page[data-page-number = "1"] .endOfContent`,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
// When running Firefox with Puppeteer, setting the
|
||||||
|
// devicePixelRatio Puppeteer option does not properly set
|
||||||
|
// the `window.devicePixelRatio` value. Set it manually.
|
||||||
|
earlySetup: `() => { window.devicePixelRatio = 1 }`,
|
||||||
|
},
|
||||||
|
{ imagesRightClickMinSize: 16 },
|
||||||
|
{ width: 800, height: 600, devicePixelRatio: 1 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render images in the text layer", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
const images = await page.$$eval(
|
||||||
|
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`,
|
||||||
|
els => els.map(el => JSON.stringify(el.getBoundingClientRect()))
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(images.length).withContext(`In ${browserName}`).toEqual(5);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when right-clicking an image it should get the contents", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
const imageCanvas = await page.$(
|
||||||
|
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await page.evaluate(el => el.width, imageCanvas))
|
||||||
|
.withContext(`Initial width, in ${browserName}`)
|
||||||
|
.toBe(0);
|
||||||
|
expect(await page.evaluate(el => el.height, imageCanvas))
|
||||||
|
.withContext(`Initial height, in ${browserName}`)
|
||||||
|
.toBe(0);
|
||||||
|
|
||||||
|
await imageCanvas.click({ button: "right" });
|
||||||
|
|
||||||
|
expect(await page.evaluate(el => el.width, imageCanvas))
|
||||||
|
.withContext(`Final width, in ${browserName}`)
|
||||||
|
.toBeGreaterThan(0);
|
||||||
|
expect(await page.evaluate(el => el.height, imageCanvas))
|
||||||
|
.withContext(`Final height, in ${browserName}`)
|
||||||
|
.toBeGreaterThan(0);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await page.evaluate(el => {
|
||||||
|
const ctx = el.getContext("2d");
|
||||||
|
const imageData = ctx.getImageData(0, 0, el.width, el.height);
|
||||||
|
const pixels = new Uint32Array(imageData.data.buffer);
|
||||||
|
const firstPixel = pixels[0];
|
||||||
|
return pixels.some(pixel => pixel !== firstPixel);
|
||||||
|
}, imageCanvas)
|
||||||
|
)
|
||||||
|
.withContext(`Image is not all the same pixel, in ${browserName}`)
|
||||||
|
.toBe(true);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("transforms", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
pages = await loadAndWait(
|
||||||
|
"images.pdf",
|
||||||
|
`.page[data-page-number = "1"] .endOfContent`,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
// When running Firefox with Puppeteer, setting the
|
||||||
|
// devicePixelRatio Puppeteer option does not properly set
|
||||||
|
// the `window.devicePixelRatio` value. Set it manually.
|
||||||
|
earlySetup: `() => { window.devicePixelRatio = 1 }`,
|
||||||
|
},
|
||||||
|
{ imagesRightClickMinSize: 16 },
|
||||||
|
{ width: 800, height: 600, devicePixelRatio: 1 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the three copies of the PDF.js logo have different rotations", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
const getRotation = async nth =>
|
||||||
|
page.evaluate(n => {
|
||||||
|
const canvas = document.querySelectorAll(
|
||||||
|
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`
|
||||||
|
)[n];
|
||||||
|
const cssTransform = getComputedStyle(canvas).transform;
|
||||||
|
if (cssTransform && cssTransform !== "none") {
|
||||||
|
const matrixValues = cssTransform
|
||||||
|
.slice(7, -1)
|
||||||
|
.split(", ")
|
||||||
|
.map(parseFloat);
|
||||||
|
return (
|
||||||
|
Math.atan2(matrixValues[1], matrixValues[0]) * (180 / Math.PI)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}, nth);
|
||||||
|
|
||||||
|
const rotation1 = await getRotation(1);
|
||||||
|
const rotation2 = await getRotation(2);
|
||||||
|
const rotation4 = await getRotation(4);
|
||||||
|
|
||||||
|
expect(Math.abs(rotation1 - rotation2))
|
||||||
|
.withContext(`Rotation between 1 and 2, in ${browserName}`)
|
||||||
|
.toBeGreaterThan(10);
|
||||||
|
expect(Math.abs(rotation1 - rotation4))
|
||||||
|
.withContext(`Rotation between 1 and 4, in ${browserName}`)
|
||||||
|
.toBeGreaterThan(10);
|
||||||
|
expect(Math.abs(rotation2 - rotation4))
|
||||||
|
.withContext(`Rotation between 2 and 4, in ${browserName}`)
|
||||||
|
.toBeGreaterThan(10);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the three copies of the PDF.js logo have the same size", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
const getSize = async nth =>
|
||||||
|
page.evaluate(n => {
|
||||||
|
const canvas = document.querySelectorAll(
|
||||||
|
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`
|
||||||
|
)[n];
|
||||||
|
return { width: canvas.width, height: canvas.height };
|
||||||
|
}, nth);
|
||||||
|
|
||||||
|
const size1 = await getSize(1);
|
||||||
|
const size2 = await getSize(2);
|
||||||
|
const size4 = await getSize(4);
|
||||||
|
|
||||||
|
const EPSILON = 3;
|
||||||
|
|
||||||
|
expect(size1.width)
|
||||||
|
.withContext(`1-2 width, in ${browserName}`)
|
||||||
|
.toBeCloseTo(size2.width, EPSILON);
|
||||||
|
expect(size1.height)
|
||||||
|
.withContext(`1-2 height, in ${browserName}`)
|
||||||
|
.toBeCloseTo(size2.height, EPSILON);
|
||||||
|
|
||||||
|
expect(size1.width)
|
||||||
|
.withContext(`1-4 width, in ${browserName}`)
|
||||||
|
.toBeCloseTo(size4.width, EPSILON);
|
||||||
|
expect(size1.height)
|
||||||
|
.withContext(`1-4 height, in ${browserName}`)
|
||||||
|
.toBeCloseTo(size4.height, EPSILON);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("trimming", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
pages = await loadAndWait(
|
||||||
|
"bug_jpx.pdf",
|
||||||
|
`.page[data-page-number = "1"] .endOfContent`,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
// When running Firefox with Puppeteer, setting the
|
||||||
|
// devicePixelRatio Puppeteer option does not properly set
|
||||||
|
// the `window.devicePixelRatio` value. Set it manually.
|
||||||
|
earlySetup: `() => { window.devicePixelRatio = 1 }`,
|
||||||
|
},
|
||||||
|
{ imagesRightClickMinSize: 16 },
|
||||||
|
{ width: 800, height: 600, devicePixelRatio: 1 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("no white border around black image", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
const canvasHandle = await page.$(
|
||||||
|
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`
|
||||||
|
);
|
||||||
|
|
||||||
|
await canvasHandle.click({ button: "right" });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await page.evaluate(el => {
|
||||||
|
const ctx = el.getContext("2d");
|
||||||
|
const imageData = ctx.getImageData(0, 0, el.width, el.height);
|
||||||
|
const pixels = new Uint32Array(imageData.data.buffer);
|
||||||
|
return Array.from(pixels.filter(pixel => pixel !== 0xff000000));
|
||||||
|
}, canvasHandle)
|
||||||
|
)
|
||||||
|
.withContext(`Image is all black, in ${browserName}`)
|
||||||
|
.toEqual([]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("trimming after rotation", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
pages = await loadAndWait(
|
||||||
|
"image-rotated-black-white-ratio.pdf",
|
||||||
|
`.page[data-page-number = "1"] .endOfContent`,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
// When running Firefox with Puppeteer, setting the
|
||||||
|
// devicePixelRatio Puppeteer option does not properly set
|
||||||
|
// the `window.devicePixelRatio` value. Set it manually.
|
||||||
|
earlySetup: `() => { window.devicePixelRatio = 1 }`,
|
||||||
|
},
|
||||||
|
{ imagesRightClickMinSize: 16 },
|
||||||
|
{ width: 800, height: 600, devicePixelRatio: 1 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("no white extra white around rotated image", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
const canvasHandle = await page.$(
|
||||||
|
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`
|
||||||
|
);
|
||||||
|
|
||||||
|
await canvasHandle.click({ button: "right" });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await page.evaluate(el => {
|
||||||
|
const ctx = el.getContext("2d");
|
||||||
|
const imageData = ctx.getImageData(0, 0, el.width, el.height);
|
||||||
|
const pixels = new Uint32Array(imageData.data.buffer);
|
||||||
|
const blackPixels = pixels.filter(
|
||||||
|
pixel => pixel === 0xff000000
|
||||||
|
).length;
|
||||||
|
const whitePixels = pixels.filter(
|
||||||
|
pixel => pixel === 0xffffffff
|
||||||
|
).length;
|
||||||
|
return blackPixels / (blackPixels + whitePixels);
|
||||||
|
}, canvasHandle)
|
||||||
|
)
|
||||||
|
.withContext(`Image is 75% black, in ${browserName}`)
|
||||||
|
.toBeCloseTo(0.75);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -114,7 +114,15 @@ describe("Text layer", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
pages = await loadAndWait(
|
pages = await loadAndWait(
|
||||||
"tracemonkey.pdf",
|
"tracemonkey.pdf",
|
||||||
`.page[data-page-number = "1"] .endOfContent`
|
`.page[data-page-number = "1"] .endOfContent`,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
(_page, browserName) => ({
|
||||||
|
// Enable images in Firefox, to ensure that they do not interfere
|
||||||
|
// with text selection. We do not test it in Chrome because we
|
||||||
|
// know that they do degrate the text selection experience there.
|
||||||
|
imagesRightClickMinSize: browserName === "firefox" ? 16 : -1,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -224,7 +232,15 @@ describe("Text layer", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
pages = await loadAndWait(
|
pages = await loadAndWait(
|
||||||
"chrome-text-selection-markedContent.pdf",
|
"chrome-text-selection-markedContent.pdf",
|
||||||
`.page[data-page-number = "1"] .endOfContent`
|
`.page[data-page-number = "1"] .endOfContent`,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
(_page, browserName) => ({
|
||||||
|
// Enable images in Firefox, to ensure that they do not interfere
|
||||||
|
// with text selection. We do not test it in Chrome because we
|
||||||
|
// know that they do degrate the text selection experience there.
|
||||||
|
imagesRightClickMinSize: browserName === "firefox" ? 16 : -1,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -314,7 +330,15 @@ describe("Text layer", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
pages = await loadAndWait(
|
pages = await loadAndWait(
|
||||||
"annotation-link-text-popup.pdf",
|
"annotation-link-text-popup.pdf",
|
||||||
`.page[data-page-number = "1"] .endOfContent`
|
`.page[data-page-number = "1"] .endOfContent`,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
(_page, browserName) => ({
|
||||||
|
// Enable images in Firefox, to ensure that they do not interfere
|
||||||
|
// with text selection. We do not test it in Chrome because we
|
||||||
|
// know that they do degrate the text selection experience there.
|
||||||
|
imagesRightClickMinSize: browserName === "firefox" ? 16 : -1,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -437,7 +461,18 @@ describe("Text layer", () => {
|
|||||||
let pages;
|
let pages;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
pages = await loadAndWait("find_all.pdf", ".textLayer", 100);
|
pages = await loadAndWait(
|
||||||
|
"find_all.pdf",
|
||||||
|
".textLayer",
|
||||||
|
100,
|
||||||
|
undefined,
|
||||||
|
(_page, browserName) => ({
|
||||||
|
// Enable images in Firefox, to ensure that they do not interfere
|
||||||
|
// with text selection. We do not test it in Chrome because we
|
||||||
|
// know that they do degrate the text selection experience there.
|
||||||
|
imagesRightClickMinSize: browserName === "firefox" ? 16 : -1,
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
|||||||
3
test/pdfs/.gitignore
vendored
3
test/pdfs/.gitignore
vendored
@ -873,6 +873,9 @@
|
|||||||
!page_with_number.pdf
|
!page_with_number.pdf
|
||||||
!page_with_number_and_link.pdf
|
!page_with_number_and_link.pdf
|
||||||
!Brotli-Prototype-FileA.pdf
|
!Brotli-Prototype-FileA.pdf
|
||||||
|
!images.pdf
|
||||||
|
!bug_jpx.pdf
|
||||||
|
!image-rotated-black-white-ratio.pdf
|
||||||
!bug2013793.pdf
|
!bug2013793.pdf
|
||||||
!bug2014080.pdf
|
!bug2014080.pdf
|
||||||
!two_pages.pdf
|
!two_pages.pdf
|
||||||
|
|||||||
BIN
test/pdfs/bug_jpx.pdf
Normal file
BIN
test/pdfs/bug_jpx.pdf
Normal file
Binary file not shown.
BIN
test/pdfs/image-rotated-black-white-ratio.pdf
Normal file
BIN
test/pdfs/image-rotated-black-white-ratio.pdf
Normal file
Binary file not shown.
BIN
test/pdfs/images.pdf
Normal file
BIN
test/pdfs/images.pdf
Normal file
Binary file not shown.
@ -76,6 +76,7 @@ import { GlobalWorkerOptions } from "../../src/display/worker_options.js";
|
|||||||
import { isValidExplicitDest } from "../../src/display/api_utils.js";
|
import { isValidExplicitDest } from "../../src/display/api_utils.js";
|
||||||
import { SignatureExtractor } from "../../src/display/editor/drawers/signaturedraw.js";
|
import { SignatureExtractor } from "../../src/display/editor/drawers/signaturedraw.js";
|
||||||
import { TextLayer } from "../../src/display/text_layer.js";
|
import { TextLayer } from "../../src/display/text_layer.js";
|
||||||
|
import { TextLayerImages } from "../../src/display/text_layer_images.js";
|
||||||
import { TouchManager } from "../../src/display/touch_manager.js";
|
import { TouchManager } from "../../src/display/touch_manager.js";
|
||||||
import { XfaLayer } from "../../src/display/xfa_layer.js";
|
import { XfaLayer } from "../../src/display/xfa_layer.js";
|
||||||
|
|
||||||
@ -133,6 +134,7 @@ const expectedAPI = Object.freeze({
|
|||||||
stopEvent,
|
stopEvent,
|
||||||
SupportedImageMimeTypes,
|
SupportedImageMimeTypes,
|
||||||
TextLayer,
|
TextLayer,
|
||||||
|
TextLayerImages,
|
||||||
TouchManager,
|
TouchManager,
|
||||||
updateUrlHash,
|
updateUrlHash,
|
||||||
Util,
|
Util,
|
||||||
|
|||||||
@ -377,6 +377,7 @@ const PDFViewerApplication = {
|
|||||||
enableSplitMerge: x => x === "true",
|
enableSplitMerge: x => x === "true",
|
||||||
enableUpdatedAddImage: x => x === "true",
|
enableUpdatedAddImage: x => x === "true",
|
||||||
highlightEditorColors: x => x,
|
highlightEditorColors: x => x,
|
||||||
|
imagesRightClickMinSize: x => parseInt(x),
|
||||||
maxCanvasPixels: x => parseInt(x),
|
maxCanvasPixels: x => parseInt(x),
|
||||||
spreadModeOnLoad: x => parseInt(x),
|
spreadModeOnLoad: x => parseInt(x),
|
||||||
supportsCaretBrowsingMode: x => x === "true",
|
supportsCaretBrowsingMode: x => x === "true",
|
||||||
@ -575,6 +576,7 @@ const PDFViewerApplication = {
|
|||||||
enableOptimizedPartialRendering: AppOptions.get(
|
enableOptimizedPartialRendering: AppOptions.get(
|
||||||
"enableOptimizedPartialRendering"
|
"enableOptimizedPartialRendering"
|
||||||
),
|
),
|
||||||
|
imagesRightClickMinSize: AppOptions.get("imagesRightClickMinSize"),
|
||||||
pageColors,
|
pageColors,
|
||||||
mlManager,
|
mlManager,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
|
|||||||
@ -323,6 +323,20 @@ const defaultOptions = {
|
|||||||
: "./images/",
|
: "./images/",
|
||||||
kind: OptionKind.VIEWER,
|
kind: OptionKind.VIEWER,
|
||||||
},
|
},
|
||||||
|
imagesRightClickMinSize: {
|
||||||
|
/** @type {number} */
|
||||||
|
value:
|
||||||
|
typeof PDFJSDev !== "undefined" &&
|
||||||
|
// Firefox mobile does not support right-clicking on images,
|
||||||
|
// see https://bugzilla.mozilla.org/show_bug.cgi?id=2014081.
|
||||||
|
// This option is disabled by default outside of MOZCENTRAL
|
||||||
|
// because it degrades the text selection experience in Chrome
|
||||||
|
// and Safari.
|
||||||
|
PDFJSDev.test("MOZCENTRAL && !GECKOVIEW")
|
||||||
|
? 16
|
||||||
|
: -1,
|
||||||
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||||
|
},
|
||||||
maxCanvasPixels: {
|
maxCanvasPixels: {
|
||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
value: 2 ** 25,
|
value: 2 ** 25,
|
||||||
|
|||||||
@ -36,10 +36,14 @@ class BasePDFPageView extends RenderableView {
|
|||||||
|
|
||||||
enableOptimizedPartialRendering = false;
|
enableOptimizedPartialRendering = false;
|
||||||
|
|
||||||
|
imagesRightClickMinSize = -1;
|
||||||
|
|
||||||
eventBus = null;
|
eventBus = null;
|
||||||
|
|
||||||
id = null;
|
id = null;
|
||||||
|
|
||||||
|
imageCoordinates = null;
|
||||||
|
|
||||||
pageColors = null;
|
pageColors = null;
|
||||||
|
|
||||||
recordedBBoxes = null;
|
recordedBBoxes = null;
|
||||||
@ -54,6 +58,7 @@ class BasePDFPageView extends RenderableView {
|
|||||||
this.renderingQueue = options.renderingQueue;
|
this.renderingQueue = options.renderingQueue;
|
||||||
this.enableOptimizedPartialRendering =
|
this.enableOptimizedPartialRendering =
|
||||||
options.enableOptimizedPartialRendering ?? false;
|
options.enableOptimizedPartialRendering ?? false;
|
||||||
|
this.imagesRightClickMinSize = options.imagesRightClickMinSize ?? -1;
|
||||||
this.minDurationToUpdateCanvas = options.minDurationToUpdateCanvas ?? 500;
|
this.minDurationToUpdateCanvas = options.minDurationToUpdateCanvas ?? 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,6 +237,9 @@ class BasePDFPageView extends RenderableView {
|
|||||||
if (this.enableOptimizedPartialRendering) {
|
if (this.enableOptimizedPartialRendering) {
|
||||||
this.recordedBBoxes ??= renderTask.recordedBBoxes;
|
this.recordedBBoxes ??= renderTask.recordedBBoxes;
|
||||||
}
|
}
|
||||||
|
if (this.imagesRightClickMinSize !== -1) {
|
||||||
|
this.imageCoordinates ??= this.pdfPage.imageCoordinates;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.renderingState = RenderingStates.FINISHED;
|
this.renderingState = RenderingStates.FINISHED;
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import {
|
|||||||
PixelsPerInch,
|
PixelsPerInch,
|
||||||
setLayerDimensions,
|
setLayerDimensions,
|
||||||
shadow,
|
shadow,
|
||||||
|
TextLayerImages,
|
||||||
} from "pdfjs-lib";
|
} from "pdfjs-lib";
|
||||||
import {
|
import {
|
||||||
approximateFraction,
|
approximateFraction,
|
||||||
@ -89,6 +90,9 @@ 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 {number} [imagesRightClickMinSize] - All images whose width and
|
||||||
|
* height are at least this value (in pixels) will be lazily inserted in the
|
||||||
|
* dom to allow right-clicking and saving them. Use `-1` to disable this.
|
||||||
* @property {boolean} [enableOptimizedPartialRendering] - When enabled, PDF
|
* @property {boolean} [enableOptimizedPartialRendering] - When enabled, PDF
|
||||||
* rendering will keep track of which areas of the page each PDF operation
|
* rendering will keep track of which areas of the page each PDF operation
|
||||||
* affects. Then, when rendering a partial page (if `enableDetailCanvas` is
|
* affects. Then, when rendering a partial page (if `enableDetailCanvas` is
|
||||||
@ -522,6 +526,14 @@ class PDFPageView extends BasePDFPageView {
|
|||||||
try {
|
try {
|
||||||
await this.textLayer.render({
|
await this.textLayer.render({
|
||||||
viewport: this.viewport,
|
viewport: this.viewport,
|
||||||
|
images: this.imageCoordinates
|
||||||
|
? new TextLayerImages(
|
||||||
|
this.imagesRightClickMinSize,
|
||||||
|
this.imageCoordinates,
|
||||||
|
this.viewport,
|
||||||
|
() => this.canvas
|
||||||
|
)
|
||||||
|
: null,
|
||||||
});
|
});
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
if (ex instanceof AbortException) {
|
if (ex instanceof AbortException) {
|
||||||
@ -707,6 +719,7 @@ class PDFPageView extends BasePDFPageView {
|
|||||||
this.detailView ??= new PDFPageDetailView({
|
this.detailView ??= new PDFPageDetailView({
|
||||||
pageView: this,
|
pageView: this,
|
||||||
enableOptimizedPartialRendering: this.enableOptimizedPartialRendering,
|
enableOptimizedPartialRendering: this.enableOptimizedPartialRendering,
|
||||||
|
imagesRightClickMinSize: -1,
|
||||||
});
|
});
|
||||||
this.detailView.update({ visibleArea });
|
this.detailView.update({ visibleArea });
|
||||||
} else if (this.detailView) {
|
} else if (this.detailView) {
|
||||||
@ -993,7 +1006,7 @@ class PDFPageView extends BasePDFPageView {
|
|||||||
return canvasWrapper;
|
return canvasWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
_getRenderingContext(canvas, transform, recordOperations) {
|
_getRenderingContext(canvas, transform, recordOperations, recordImages) {
|
||||||
return {
|
return {
|
||||||
canvas,
|
canvas,
|
||||||
transform,
|
transform,
|
||||||
@ -1004,6 +1017,7 @@ class PDFPageView extends BasePDFPageView {
|
|||||||
pageColors: this.pageColors,
|
pageColors: this.pageColors,
|
||||||
isEditing: this.#isEditing,
|
isEditing: this.#isEditing,
|
||||||
recordOperations,
|
recordOperations,
|
||||||
|
recordImages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1127,12 +1141,15 @@ class PDFPageView extends BasePDFPageView {
|
|||||||
this.#hasRestrictedScaling &&
|
this.#hasRestrictedScaling &&
|
||||||
!this.recordedBBoxes;
|
!this.recordedBBoxes;
|
||||||
|
|
||||||
|
const recordImages =
|
||||||
|
this.imagesRightClickMinSize !== -1 && !this.imageCoordinates;
|
||||||
|
|
||||||
// Rendering area
|
// Rendering area
|
||||||
const transform = outputScale.scaled
|
const transform = outputScale.scaled
|
||||||
? [outputScale.sx, 0, 0, outputScale.sy, 0, 0]
|
? [outputScale.sx, 0, 0, outputScale.sy, 0, 0]
|
||||||
: null;
|
: null;
|
||||||
const resultPromise = this._drawCanvas(
|
const resultPromise = this._drawCanvas(
|
||||||
this._getRenderingContext(canvas, transform, recordBBoxes),
|
this._getRenderingContext(canvas, transform, recordBBoxes, recordImages),
|
||||||
() => {
|
() => {
|
||||||
prevCanvas?.remove();
|
prevCanvas?.remove();
|
||||||
this._resetCanvas();
|
this._resetCanvas();
|
||||||
|
|||||||
@ -132,6 +132,9 @@ 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 {number} [imagesRightClickMinSize] - All images whose width and
|
||||||
|
* height are at least this value (in pixels) will be lazily inserted in the
|
||||||
|
* dom to allow right-clicking and saving them. Use `-1` to disable this.
|
||||||
* @property {boolean} [enableOptimizedPartialRendering] - When enabled, PDF
|
* @property {boolean} [enableOptimizedPartialRendering] - When enabled, PDF
|
||||||
* rendering will keep track of which areas of the page each PDF operation
|
* rendering will keep track of which areas of the page each PDF operation
|
||||||
* affects. Then, when rendering a partial page (if `enableDetailCanvas` is
|
* affects. Then, when rendering a partial page (if `enableDetailCanvas` is
|
||||||
@ -361,6 +364,7 @@ class PDFViewer {
|
|||||||
this.enableDetailCanvas = options.enableDetailCanvas ?? true;
|
this.enableDetailCanvas = options.enableDetailCanvas ?? true;
|
||||||
this.enableOptimizedPartialRendering =
|
this.enableOptimizedPartialRendering =
|
||||||
options.enableOptimizedPartialRendering ?? false;
|
options.enableOptimizedPartialRendering ?? false;
|
||||||
|
this.imagesRightClickMinSize = options.imagesRightClickMinSize ?? -1;
|
||||||
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();
|
||||||
@ -1062,6 +1066,7 @@ class PDFViewer {
|
|||||||
enableDetailCanvas: this.enableDetailCanvas,
|
enableDetailCanvas: this.enableDetailCanvas,
|
||||||
enableOptimizedPartialRendering:
|
enableOptimizedPartialRendering:
|
||||||
this.enableOptimizedPartialRendering,
|
this.enableOptimizedPartialRendering,
|
||||||
|
imagesRightClickMinSize: this.imagesRightClickMinSize,
|
||||||
pageColors,
|
pageColors,
|
||||||
l10n: this.l10n,
|
l10n: this.l10n,
|
||||||
layerProperties: this._layerProperties,
|
layerProperties: this._layerProperties,
|
||||||
|
|||||||
@ -67,6 +67,7 @@ const {
|
|||||||
stopEvent,
|
stopEvent,
|
||||||
SupportedImageMimeTypes,
|
SupportedImageMimeTypes,
|
||||||
TextLayer,
|
TextLayer,
|
||||||
|
TextLayerImages,
|
||||||
TouchManager,
|
TouchManager,
|
||||||
updateUrlHash,
|
updateUrlHash,
|
||||||
Util,
|
Util,
|
||||||
@ -129,6 +130,7 @@ export {
|
|||||||
stopEvent,
|
stopEvent,
|
||||||
SupportedImageMimeTypes,
|
SupportedImageMimeTypes,
|
||||||
TextLayer,
|
TextLayer,
|
||||||
|
TextLayerImages,
|
||||||
TouchManager,
|
TouchManager,
|
||||||
updateUrlHash,
|
updateUrlHash,
|
||||||
Util,
|
Util,
|
||||||
|
|||||||
@ -142,3 +142,14 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.textLayerImages {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
position: absolute;
|
||||||
|
transform-origin: 0% 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -16,6 +16,8 @@
|
|||||||
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
|
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
/** @typedef {import("../src/display/text_layer_images.js").TextLayerImages} TextLayerImages */
|
||||||
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
|
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||||
@ -36,6 +38,7 @@ import { removeNullCharacters } from "./ui_utils.js";
|
|||||||
/**
|
/**
|
||||||
* @typedef {Object} TextLayerBuilderRenderOptions
|
* @typedef {Object} TextLayerBuilderRenderOptions
|
||||||
* @property {PageViewport} viewport
|
* @property {PageViewport} viewport
|
||||||
|
* @property {TextLayerImages} images
|
||||||
* @property {Object} [textContentParams]
|
* @property {Object} [textContentParams]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -83,7 +86,7 @@ class TextLayerBuilder {
|
|||||||
* @param {TextLayerBuilderRenderOptions} options
|
* @param {TextLayerBuilderRenderOptions} options
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async render({ viewport, textContentParams = null }) {
|
async render({ viewport, images, textContentParams = null }) {
|
||||||
if (this.#renderingDone && this.#textLayer) {
|
if (this.#renderingDone && this.#textLayer) {
|
||||||
this.#textLayer.update({
|
this.#textLayer.update({
|
||||||
viewport,
|
viewport,
|
||||||
@ -101,6 +104,7 @@ class TextLayerBuilder {
|
|||||||
disableNormalization: true,
|
disableNormalization: true,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
images,
|
||||||
container: this.div,
|
container: this.div,
|
||||||
viewport,
|
viewport,
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user