mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-04-22 13:14:04 +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,
|
||||
SerializableEmpty,
|
||||
} from "./annotation_storage.js";
|
||||
import {
|
||||
CanvasBBoxTracker,
|
||||
CanvasDependencyTracker,
|
||||
CanvasImagesTracker,
|
||||
} from "./canvas_dependency_tracker.js";
|
||||
import {
|
||||
deprecated,
|
||||
isDataScheme,
|
||||
@ -68,7 +73,6 @@ import {
|
||||
NodeStandardFontDataFactory,
|
||||
NodeWasmFactory,
|
||||
} from "display-node_utils";
|
||||
import { CanvasDependencyTracker } from "./canvas_dependency_tracker.js";
|
||||
import { CanvasGraphics } from "./canvas.js";
|
||||
import { DOMCanvasFactory } from "./canvas_factory.js";
|
||||
import { DOMCMapReaderFactory } from "display-cmap_reader_factory";
|
||||
@ -1273,6 +1277,7 @@ class PDFDocumentProxy {
|
||||
* annotation ids with canvases used to render them.
|
||||
* @property {PrintAnnotationStorage} [printAnnotationStorage]
|
||||
* @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
|
||||
* boxes of all PDF operations that render onto the canvas.
|
||||
* @property {OperationsFilter} [operationsFilter] - If provided, only
|
||||
@ -1357,6 +1362,7 @@ class PDFPageProxy {
|
||||
this.destroyed = false;
|
||||
this.recordedBBoxes = null;
|
||||
this.#pagesMapper = pagesMapper;
|
||||
this.imageCoordinates = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1488,6 +1494,7 @@ class PDFPageProxy {
|
||||
pageColors = null,
|
||||
printAnnotationStorage = null,
|
||||
isEditing = false,
|
||||
recordImages = false,
|
||||
recordOperations = false,
|
||||
operationsFilter = null,
|
||||
}) {
|
||||
@ -1539,6 +1546,7 @@ class PDFPageProxy {
|
||||
);
|
||||
const shouldRecordOperations =
|
||||
!this.recordedBBoxes && (recordOperations || recordForDebugger);
|
||||
const shouldRecordImages = !this.imageCoordinates && recordImages;
|
||||
|
||||
const complete = error => {
|
||||
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
|
||||
// cleanup immediately once rendering has finished.
|
||||
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({
|
||||
callback: complete,
|
||||
// Only include the required properties, and *not* the entire object.
|
||||
params: {
|
||||
canvas,
|
||||
canvasContext,
|
||||
dependencyTracker: shouldRecordOperations
|
||||
? new CanvasDependencyTracker(
|
||||
canvas,
|
||||
intentState.operatorList.length,
|
||||
recordForDebugger
|
||||
)
|
||||
dependencyTracker: dependencyTracker ?? bboxTracker,
|
||||
imagesTracker: shouldRecordImages
|
||||
? new CanvasImagesTracker(canvas)
|
||||
: null,
|
||||
viewport,
|
||||
transform,
|
||||
@ -3288,6 +3312,10 @@ class RenderTask {
|
||||
(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._enableHWA = enableHWA;
|
||||
this._dependencyTracker = params.dependencyTracker;
|
||||
this._imagesTracker = params.imagesTracker;
|
||||
this._operationsFilter = operationsFilter;
|
||||
}
|
||||
|
||||
@ -3375,7 +3404,13 @@ class InternalRenderTask {
|
||||
this.stepper.init(this.operatorList);
|
||||
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
|
||||
// which cannot be created from the canvas itself.
|
||||
@ -3395,7 +3430,8 @@ class InternalRenderTask {
|
||||
{ optionalContentConfig },
|
||||
this.annotationCanvasMap,
|
||||
this.pageColors,
|
||||
dependencyTracker
|
||||
dependencyTracker,
|
||||
imagesTracker
|
||||
);
|
||||
this.gfx.beginDrawing({
|
||||
transform,
|
||||
|
||||
@ -658,7 +658,8 @@ class CanvasGraphics {
|
||||
{ optionalContentConfig, markedContentStack = null },
|
||||
annotationCanvasMap,
|
||||
pageColors,
|
||||
dependencyTracker
|
||||
dependencyTracker,
|
||||
imagesTracker
|
||||
) {
|
||||
this.ctx = canvasCtx;
|
||||
this.current = new CanvasExtraState(
|
||||
@ -698,6 +699,7 @@ class CanvasGraphics {
|
||||
this._cachedBitmapsMap = new Map();
|
||||
|
||||
this.dependencyTracker = dependencyTracker ?? null;
|
||||
this.imagesTracker = imagesTracker ?? null;
|
||||
}
|
||||
|
||||
getObject(opIdx, data, fallback = null) {
|
||||
@ -3064,11 +3066,19 @@ class CanvasGraphics {
|
||||
imgData.interpolate
|
||||
);
|
||||
|
||||
this.dependencyTracker
|
||||
?.resetBBox(opIdx)
|
||||
.recordBBox(opIdx, ctx, 0, width, -height, 0)
|
||||
.recordDependencies(opIdx, Dependencies.imageXObject)
|
||||
.recordOperation(opIdx);
|
||||
if (this.dependencyTracker) {
|
||||
this.dependencyTracker
|
||||
.resetBBox(opIdx)
|
||||
.recordBBox(opIdx, ctx, 0, width, -height, 0)
|
||||
.recordDependencies(opIdx, Dependencies.imageXObject)
|
||||
.recordOperation(opIdx);
|
||||
this.imagesTracker?.record(
|
||||
ctx,
|
||||
width,
|
||||
height,
|
||||
this.dependencyTracker.clipBox
|
||||
);
|
||||
}
|
||||
|
||||
drawImageAtIntegerCoords(
|
||||
ctx,
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Util } from "../shared/util.js";
|
||||
import { FeatureTest, Util } from "../shared/util.js";
|
||||
|
||||
const FORCED_DEPENDENCY_LABEL = "__forcedDependency";
|
||||
|
||||
@ -71,6 +71,323 @@ const ensureDebugMetadata = (map, key) =>
|
||||
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" |
|
||||
* "strokeAlpha" | "fillColor" | "fillAlpha" | "globalCompositeOperation" |
|
||||
@ -100,61 +417,34 @@ class CanvasDependencyTracker {
|
||||
|
||||
#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;
|
||||
|
||||
// Uint8ClampedArray<minX, minY, maxX, maxY>
|
||||
#bboxesCoords;
|
||||
|
||||
#bboxes;
|
||||
|
||||
#debugMetadata;
|
||||
|
||||
constructor(canvas, operationsCount, recordDebugMetadata = false) {
|
||||
this.#canvasWidth = canvas.width;
|
||||
this.#canvasHeight = canvas.height;
|
||||
this.#initializeBBoxes(operationsCount);
|
||||
#recordDebugMetadataDepenencyAfterRestore;
|
||||
|
||||
#bboxTracker;
|
||||
|
||||
constructor(bboxTracker, recordDebugMetadata = false) {
|
||||
this.#bboxTracker = bboxTracker;
|
||||
if (recordDebugMetadata) {
|
||||
this.#debugMetadata = new Map();
|
||||
this.#recordDebugMetadataDepenencyAfterRestore = (lastSave, opIdx) => {
|
||||
ensureDebugMetadata(this.#debugMetadata, opIdx).dependencies.add(
|
||||
lastSave
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
get clipBox() {
|
||||
return this.#bboxTracker.clipBox;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
this.#bboxTracker.growOperationsCount(operationsCount);
|
||||
}
|
||||
|
||||
save(opIdx) {
|
||||
@ -168,13 +458,17 @@ class CanvasDependencyTracker {
|
||||
__proto__: this.#incremental[FORCED_DEPENDENCY_LABEL],
|
||||
},
|
||||
};
|
||||
this.#clipBox = { __proto__: this.#clipBox };
|
||||
this.#savesStack.push(opIdx);
|
||||
this.#bboxTracker.save(opIdx);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
restore(opIdx) {
|
||||
this.#bboxTracker.restore(
|
||||
opIdx,
|
||||
this.#recordDebugMetadataDepenencyAfterRestore
|
||||
);
|
||||
|
||||
const previous = Object.getPrototypeOf(this.#simple);
|
||||
if (previous === null) {
|
||||
// Sometimes we call more .restore() than .save(), for
|
||||
@ -183,77 +477,53 @@ class CanvasDependencyTracker {
|
||||
}
|
||||
this.#simple = previous;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
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()
|
||||
)
|
||||
recordOpenMarker(opIdx) {
|
||||
this.#bboxTracker.recordOpenMarker(
|
||||
opIdx,
|
||||
this.#recordDebugMetadataDepenencyAfterRestore
|
||||
);
|
||||
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() {
|
||||
if (this.#baseTransformStack.length > 1) {
|
||||
this.#baseTransformStack.pop();
|
||||
}
|
||||
this.#bboxTracker.popBaseTransform();
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -323,73 +593,17 @@ class CanvasDependencyTracker {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
this.#bboxTracker.resetBBox(idx);
|
||||
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;
|
||||
}
|
||||
this.#bboxTracker.recordClipBox(idx, ctx, minX, maxX, minY, maxY);
|
||||
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])
|
||||
);
|
||||
this.#bboxTracker.recordBBox(idx, ctx, minX, maxX, minY, maxY);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -471,11 +685,7 @@ class CanvasDependencyTracker {
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
this.#bboxTracker.recordFullPageBBox(idx);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -516,39 +726,25 @@ class CanvasDependencyTracker {
|
||||
const metadata = ensureDebugMetadata(this.#debugMetadata, idx);
|
||||
const { dependencies } = metadata;
|
||||
this.#pendingDependencies.forEach(dependencies.add, dependencies);
|
||||
this.#savesStack.forEach(dependencies.add, dependencies);
|
||||
this.#markedContentStack.forEach(dependencies.add, dependencies);
|
||||
this.#bboxTracker._savesStack.forEach(dependencies.add, dependencies);
|
||||
this.#bboxTracker._markedContentStack.forEach(
|
||||
dependencies.add,
|
||||
dependencies
|
||||
);
|
||||
dependencies.delete(idx);
|
||||
metadata.isRenderingOperation = true;
|
||||
}
|
||||
|
||||
if (this.#pendingBBoxIdx === idx) {
|
||||
const minX = floor((this.#pendingBBox[0] * 256) / this.#canvasWidth);
|
||||
const minY = floor((this.#pendingBBox[1] * 256) / this.#canvasHeight);
|
||||
const maxX = ceil((this.#pendingBBox[2] * 256) / this.#canvasWidth);
|
||||
const maxY = ceil((this.#pendingBBox[3] * 256) / this.#canvasHeight);
|
||||
const needsCleanup = !preserve && idx === this.#bboxTracker._pendingBBoxIdx;
|
||||
|
||||
expandBBox(this.#bboxesCoords, idx, minX, minY, maxX, maxY);
|
||||
for (const depIdx of this.#pendingDependencies) {
|
||||
if (depIdx !== idx) {
|
||||
expandBBox(this.#bboxesCoords, depIdx, minX, minY, maxX, maxY);
|
||||
}
|
||||
}
|
||||
for (const saveIdx of this.#savesStack) {
|
||||
if (saveIdx !== idx) {
|
||||
expandBBox(this.#bboxesCoords, saveIdx, minX, minY, maxX, maxY);
|
||||
}
|
||||
}
|
||||
for (const saveIdx of this.#markedContentStack) {
|
||||
if (saveIdx !== idx) {
|
||||
expandBBox(this.#bboxesCoords, saveIdx, minX, minY, maxX, maxY);
|
||||
}
|
||||
}
|
||||
this.#bboxTracker.recordOperation(idx, preserve, [
|
||||
this.#pendingDependencies,
|
||||
this.#bboxTracker._savesStack,
|
||||
this.#bboxTracker._markedContentStack,
|
||||
]);
|
||||
|
||||
if (!preserve) {
|
||||
this.#pendingDependencies.clear();
|
||||
this.#pendingBBoxIdx = -1;
|
||||
}
|
||||
if (needsCleanup) {
|
||||
this.#pendingDependencies.clear();
|
||||
}
|
||||
|
||||
return this;
|
||||
@ -565,42 +761,17 @@ class CanvasDependencyTracker {
|
||||
}
|
||||
|
||||
bboxToClipBoxDropOperation(idx, preserve = false) {
|
||||
if (this.#pendingBBoxIdx === idx) {
|
||||
this.#pendingBBoxIdx = -1;
|
||||
|
||||
this.#clipBox[0] = Math.max(this.#clipBox[0], this.#pendingBBox[0]);
|
||||
this.#clipBox[1] = Math.max(this.#clipBox[1], this.#pendingBBox[1]);
|
||||
this.#clipBox[2] = Math.min(this.#clipBox[2], this.#pendingBBox[2]);
|
||||
this.#clipBox[3] = Math.min(this.#clipBox[3], this.#pendingBBox[3]);
|
||||
|
||||
if (!preserve) {
|
||||
this.#pendingDependencies.clear();
|
||||
}
|
||||
const needsCleanup = !preserve && idx === this.#bboxTracker._pendingBBoxIdx;
|
||||
this.#bboxTracker.bboxToClipBoxDropOperation(idx);
|
||||
if (needsCleanup) {
|
||||
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 new BBoxReader(this.#bboxes, this.#bboxesCoords);
|
||||
return this.#bboxTracker.take();
|
||||
}
|
||||
|
||||
takeDebugMetadata() {
|
||||
@ -644,6 +815,10 @@ class CanvasNestedDependencyTracker {
|
||||
this.#ignoreBBoxes = !!ignoreBBoxes;
|
||||
}
|
||||
|
||||
get clipBox() {
|
||||
return this.#dependencyTracker.clipBox;
|
||||
}
|
||||
|
||||
growOperationsCount() {
|
||||
throw new Error("Unreachable");
|
||||
}
|
||||
@ -918,4 +1093,156 @@ const Dependencies = {
|
||||
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("./api").TextContent} TextContent */
|
||||
/** @typedef {import("./text_layer_images").TextLayerImages} TextLayerImages */
|
||||
|
||||
import {
|
||||
AbortException,
|
||||
@ -34,6 +35,8 @@ import { OutputScale, setLayerDimensions } from "./display_utils.js";
|
||||
* runs.
|
||||
* @property {PageViewport} viewport - The target viewport to properly layout
|
||||
* 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;
|
||||
|
||||
#imagesHandler = null;
|
||||
|
||||
#lang = null;
|
||||
|
||||
#layoutTextParams = null;
|
||||
@ -97,7 +102,7 @@ class TextLayer {
|
||||
/**
|
||||
* @param {TextLayerParameters} options
|
||||
*/
|
||||
constructor({ textContentSource, container, viewport }) {
|
||||
constructor({ textContentSource, images, container, viewport }) {
|
||||
if (textContentSource instanceof ReadableStream) {
|
||||
this.#textContentSource = textContentSource;
|
||||
} else if (
|
||||
@ -115,6 +120,8 @@ class TextLayer {
|
||||
}
|
||||
this.#container = this.#rootContainer = container;
|
||||
|
||||
this.#imagesHandler = images;
|
||||
|
||||
this.#scale = viewport.scale * OutputScale.pixelRatio;
|
||||
this.#rotation = viewport.rotation;
|
||||
this.#layoutTextParams = {
|
||||
@ -181,6 +188,10 @@ class TextLayer {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
render() {
|
||||
if (this.#imagesHandler) {
|
||||
this.#container.append(this.#imagesHandler.render());
|
||||
}
|
||||
|
||||
const pump = () => {
|
||||
this.#reader.read().then(({ value, 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 { SignatureExtractor } from "./display/editor/drawers/signaturedraw.js";
|
||||
import { TextLayer } from "./display/text_layer.js";
|
||||
import { TextLayerImages } from "./display/text_layer_images.js";
|
||||
import { TouchManager } from "./display/touch_manager.js";
|
||||
import { XfaLayer } from "./display/xfa_layer.js";
|
||||
|
||||
@ -149,6 +150,7 @@ globalThis.pdfjsLib = {
|
||||
stopEvent,
|
||||
SupportedImageMimeTypes,
|
||||
TextLayer,
|
||||
TextLayerImages,
|
||||
TouchManager,
|
||||
updateUrlHash,
|
||||
Util,
|
||||
@ -211,6 +213,7 @@ export {
|
||||
stopEvent,
|
||||
SupportedImageMimeTypes,
|
||||
TextLayer,
|
||||
TextLayerImages,
|
||||
TouchManager,
|
||||
updateUrlHash,
|
||||
Util,
|
||||
|
||||
@ -43,6 +43,7 @@ async function runTests(results) {
|
||||
"stamp_editor_spec.mjs",
|
||||
"text_field_spec.mjs",
|
||||
"text_layer_spec.mjs",
|
||||
"text_layer_images_spec.mjs",
|
||||
"thumbnail_view_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 () => {
|
||||
pages = await loadAndWait(
|
||||
"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 () => {
|
||||
pages = await loadAndWait(
|
||||
"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 () => {
|
||||
pages = await loadAndWait(
|
||||
"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;
|
||||
|
||||
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 () => {
|
||||
|
||||
3
test/pdfs/.gitignore
vendored
3
test/pdfs/.gitignore
vendored
@ -873,6 +873,9 @@
|
||||
!page_with_number.pdf
|
||||
!page_with_number_and_link.pdf
|
||||
!Brotli-Prototype-FileA.pdf
|
||||
!images.pdf
|
||||
!bug_jpx.pdf
|
||||
!image-rotated-black-white-ratio.pdf
|
||||
!bug2013793.pdf
|
||||
!bug2014080.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 { SignatureExtractor } from "../../src/display/editor/drawers/signaturedraw.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 { XfaLayer } from "../../src/display/xfa_layer.js";
|
||||
|
||||
@ -133,6 +134,7 @@ const expectedAPI = Object.freeze({
|
||||
stopEvent,
|
||||
SupportedImageMimeTypes,
|
||||
TextLayer,
|
||||
TextLayerImages,
|
||||
TouchManager,
|
||||
updateUrlHash,
|
||||
Util,
|
||||
|
||||
@ -377,6 +377,7 @@ const PDFViewerApplication = {
|
||||
enableSplitMerge: x => x === "true",
|
||||
enableUpdatedAddImage: x => x === "true",
|
||||
highlightEditorColors: x => x,
|
||||
imagesRightClickMinSize: x => parseInt(x),
|
||||
maxCanvasPixels: x => parseInt(x),
|
||||
spreadModeOnLoad: x => parseInt(x),
|
||||
supportsCaretBrowsingMode: x => x === "true",
|
||||
@ -575,6 +576,7 @@ const PDFViewerApplication = {
|
||||
enableOptimizedPartialRendering: AppOptions.get(
|
||||
"enableOptimizedPartialRendering"
|
||||
),
|
||||
imagesRightClickMinSize: AppOptions.get("imagesRightClickMinSize"),
|
||||
pageColors,
|
||||
mlManager,
|
||||
abortSignal,
|
||||
|
||||
@ -323,6 +323,20 @@ const defaultOptions = {
|
||||
: "./images/",
|
||||
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: {
|
||||
/** @type {number} */
|
||||
value: 2 ** 25,
|
||||
|
||||
@ -36,10 +36,14 @@ class BasePDFPageView extends RenderableView {
|
||||
|
||||
enableOptimizedPartialRendering = false;
|
||||
|
||||
imagesRightClickMinSize = -1;
|
||||
|
||||
eventBus = null;
|
||||
|
||||
id = null;
|
||||
|
||||
imageCoordinates = null;
|
||||
|
||||
pageColors = null;
|
||||
|
||||
recordedBBoxes = null;
|
||||
@ -54,6 +58,7 @@ class BasePDFPageView extends RenderableView {
|
||||
this.renderingQueue = options.renderingQueue;
|
||||
this.enableOptimizedPartialRendering =
|
||||
options.enableOptimizedPartialRendering ?? false;
|
||||
this.imagesRightClickMinSize = options.imagesRightClickMinSize ?? -1;
|
||||
this.minDurationToUpdateCanvas = options.minDurationToUpdateCanvas ?? 500;
|
||||
}
|
||||
|
||||
@ -232,6 +237,9 @@ class BasePDFPageView extends RenderableView {
|
||||
if (this.enableOptimizedPartialRendering) {
|
||||
this.recordedBBoxes ??= renderTask.recordedBBoxes;
|
||||
}
|
||||
if (this.imagesRightClickMinSize !== -1) {
|
||||
this.imageCoordinates ??= this.pdfPage.imageCoordinates;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.renderingState = RenderingStates.FINISHED;
|
||||
|
||||
@ -30,6 +30,7 @@ import {
|
||||
PixelsPerInch,
|
||||
setLayerDimensions,
|
||||
shadow,
|
||||
TextLayerImages,
|
||||
} from "pdfjs-lib";
|
||||
import {
|
||||
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,
|
||||
* that only renders the part of the page that is close to the viewport.
|
||||
* 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
|
||||
* rendering will keep track of which areas of the page each PDF operation
|
||||
* affects. Then, when rendering a partial page (if `enableDetailCanvas` is
|
||||
@ -522,6 +526,14 @@ class PDFPageView extends BasePDFPageView {
|
||||
try {
|
||||
await this.textLayer.render({
|
||||
viewport: this.viewport,
|
||||
images: this.imageCoordinates
|
||||
? new TextLayerImages(
|
||||
this.imagesRightClickMinSize,
|
||||
this.imageCoordinates,
|
||||
this.viewport,
|
||||
() => this.canvas
|
||||
)
|
||||
: null,
|
||||
});
|
||||
} catch (ex) {
|
||||
if (ex instanceof AbortException) {
|
||||
@ -707,6 +719,7 @@ class PDFPageView extends BasePDFPageView {
|
||||
this.detailView ??= new PDFPageDetailView({
|
||||
pageView: this,
|
||||
enableOptimizedPartialRendering: this.enableOptimizedPartialRendering,
|
||||
imagesRightClickMinSize: -1,
|
||||
});
|
||||
this.detailView.update({ visibleArea });
|
||||
} else if (this.detailView) {
|
||||
@ -993,7 +1006,7 @@ class PDFPageView extends BasePDFPageView {
|
||||
return canvasWrapper;
|
||||
}
|
||||
|
||||
_getRenderingContext(canvas, transform, recordOperations) {
|
||||
_getRenderingContext(canvas, transform, recordOperations, recordImages) {
|
||||
return {
|
||||
canvas,
|
||||
transform,
|
||||
@ -1004,6 +1017,7 @@ class PDFPageView extends BasePDFPageView {
|
||||
pageColors: this.pageColors,
|
||||
isEditing: this.#isEditing,
|
||||
recordOperations,
|
||||
recordImages,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1127,12 +1141,15 @@ class PDFPageView extends BasePDFPageView {
|
||||
this.#hasRestrictedScaling &&
|
||||
!this.recordedBBoxes;
|
||||
|
||||
const recordImages =
|
||||
this.imagesRightClickMinSize !== -1 && !this.imageCoordinates;
|
||||
|
||||
// Rendering area
|
||||
const transform = outputScale.scaled
|
||||
? [outputScale.sx, 0, 0, outputScale.sy, 0, 0]
|
||||
: null;
|
||||
const resultPromise = this._drawCanvas(
|
||||
this._getRenderingContext(canvas, transform, recordBBoxes),
|
||||
this._getRenderingContext(canvas, transform, recordBBoxes, recordImages),
|
||||
() => {
|
||||
prevCanvas?.remove();
|
||||
this._resetCanvas();
|
||||
|
||||
@ -132,6 +132,9 @@ function isValidAnnotationEditorMode(mode) {
|
||||
* `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.
|
||||
* 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
|
||||
* rendering will keep track of which areas of the page each PDF operation
|
||||
* affects. Then, when rendering a partial page (if `enableDetailCanvas` is
|
||||
@ -361,6 +364,7 @@ class PDFViewer {
|
||||
this.enableDetailCanvas = options.enableDetailCanvas ?? true;
|
||||
this.enableOptimizedPartialRendering =
|
||||
options.enableOptimizedPartialRendering ?? false;
|
||||
this.imagesRightClickMinSize = options.imagesRightClickMinSize ?? -1;
|
||||
this.l10n = options.l10n;
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
this.l10n ||= new GenericL10n();
|
||||
@ -1062,6 +1066,7 @@ class PDFViewer {
|
||||
enableDetailCanvas: this.enableDetailCanvas,
|
||||
enableOptimizedPartialRendering:
|
||||
this.enableOptimizedPartialRendering,
|
||||
imagesRightClickMinSize: this.imagesRightClickMinSize,
|
||||
pageColors,
|
||||
l10n: this.l10n,
|
||||
layerProperties: this._layerProperties,
|
||||
|
||||
@ -67,6 +67,7 @@ const {
|
||||
stopEvent,
|
||||
SupportedImageMimeTypes,
|
||||
TextLayer,
|
||||
TextLayerImages,
|
||||
TouchManager,
|
||||
updateUrlHash,
|
||||
Util,
|
||||
@ -129,6 +130,7 @@ export {
|
||||
stopEvent,
|
||||
SupportedImageMimeTypes,
|
||||
TextLayer,
|
||||
TextLayerImages,
|
||||
TouchManager,
|
||||
updateUrlHash,
|
||||
Util,
|
||||
|
||||
@ -142,3 +142,14 @@
|
||||
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 */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @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 */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||
@ -36,6 +38,7 @@ import { removeNullCharacters } from "./ui_utils.js";
|
||||
/**
|
||||
* @typedef {Object} TextLayerBuilderRenderOptions
|
||||
* @property {PageViewport} viewport
|
||||
* @property {TextLayerImages} images
|
||||
* @property {Object} [textContentParams]
|
||||
*/
|
||||
|
||||
@ -83,7 +86,7 @@ class TextLayerBuilder {
|
||||
* @param {TextLayerBuilderRenderOptions} options
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async render({ viewport, textContentParams = null }) {
|
||||
async render({ viewport, images, textContentParams = null }) {
|
||||
if (this.#renderingDone && this.#textLayer) {
|
||||
this.#textLayer.update({
|
||||
viewport,
|
||||
@ -101,6 +104,7 @@ class TextLayerBuilder {
|
||||
disableNormalization: true,
|
||||
}
|
||||
),
|
||||
images,
|
||||
container: this.div,
|
||||
viewport,
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user