Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 45 additions & 9 deletions src/display/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ import {
PrintAnnotationStorage,
SerializableEmpty,
} from "./annotation_storage.js";
import {
CanvasDependencyTracker,
CanvasImagesTracker,
} from "./canvas_dependency_tracker.js";
import {
deprecated,
isDataScheme,
Expand Down Expand Up @@ -67,7 +71,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";
Expand Down Expand Up @@ -1339,6 +1342,7 @@ class PDFPageProxy {
this._intentStates = new Map();
this.destroyed = false;
this.recordedBBoxes = null;
this.imageCoordinates = null;
}

/**
Expand Down Expand Up @@ -1470,6 +1474,7 @@ class PDFPageProxy {
pageColors = null,
printAnnotationStorage = null,
isEditing = false,
recordImages = true,
recordOperations = false,
operationsFilter = null,
}) {
Expand Down Expand Up @@ -1524,6 +1529,7 @@ class PDFPageProxy {

const shouldRecordOperations =
!this.recordedBBoxes && (recordOperations || recordForDebugger);
const shouldRecordImages = !this.imageCoordinates && recordImages;

const complete = error => {
intentState.renderTasks.delete(internalRenderTask);
Expand All @@ -1543,6 +1549,10 @@ class PDFPageProxy {
}
}

if (shouldRecordImages) {
this.imageCoordinates = internalRenderTask.gfx?.imagesTracker.take();
}

// Attempt to reduce memory usage during *printing*, by always running
// cleanup immediately once rendering has finished.
if (intentPrint) {
Expand Down Expand Up @@ -1577,12 +1587,16 @@ class PDFPageProxy {
params: {
canvas,
canvasContext,
dependencyTracker: shouldRecordOperations
? new CanvasDependencyTracker(
canvas,
intentState.operatorList.length,
recordForDebugger
)
dependencyTracker:
shouldRecordOperations || shouldRecordImages
? new CanvasDependencyTracker(
canvas,
intentState.operatorList.length,
recordForDebugger
)
: null,
imagesTracker: shouldRecordImages
? new CanvasImagesTracker(canvas)
: null,
viewport,
transform,
Expand Down Expand Up @@ -1758,6 +1772,10 @@ class PDFPageProxy {
});
}

getImagesCoordinates() {
return this._transport.getImagesCoordinates(this._pageIndex);
}

/**
* @returns {Promise<StructTreeNode>} A promise that is resolved with a
* {@link StructTreeNode} object that represents the page's structure tree,
Expand Down Expand Up @@ -3067,6 +3085,12 @@ class WorkerTransport {
});
}

getImagesCoordinates(pageIndex) {
return this.messageHandler.sendWithPromise("GetImagesCoordinates", {
pageIndex,
});
}

getStructTree(pageIndex) {
return this.messageHandler.sendWithPromise("GetStructTree", {
pageIndex: this.#pagesMapper.getPageId(pageIndex + 1) - 1,
Expand Down Expand Up @@ -3215,6 +3239,10 @@ class RenderTask {
(separateAnnots.canvas && annotationCanvasMap?.size > 0)
);
}

get imageCoordinates() {
return this._internalRenderTask.imageCoordinates || null;
}
}

/**
Expand Down Expand Up @@ -3272,6 +3300,7 @@ class InternalRenderTask {
this._canvasContext = params.canvas ? null : params.canvasContext;
this._enableHWA = enableHWA;
this._dependencyTracker = params.dependencyTracker;
this._imagesTracker = params.imagesTracker;
this._operationsFilter = operationsFilter;
}

Expand Down Expand Up @@ -3302,7 +3331,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.
Expand All @@ -3322,7 +3357,8 @@ class InternalRenderTask {
{ optionalContentConfig },
this.annotationCanvasMap,
this.pageColors,
dependencyTracker
dependencyTracker,
imagesTracker
);
this.gfx.beginDrawing({
transform,
Expand Down
22 changes: 16 additions & 6 deletions src/display/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,8 @@ class CanvasGraphics {
{ optionalContentConfig, markedContentStack = null },
annotationCanvasMap,
pageColors,
dependencyTracker
dependencyTracker,
imagesTracker
) {
this.ctx = canvasCtx;
this.current = new CanvasExtraState(
Expand Down Expand Up @@ -699,6 +700,7 @@ class CanvasGraphics {
this._cachedBitmapsMap = new Map();

this.dependencyTracker = dependencyTracker ?? null;
this.imagesTracker = imagesTracker ?? null;
}

getObject(opIdx, data, fallback = null) {
Expand Down Expand Up @@ -3068,11 +3070,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,
Expand Down
149 changes: 147 additions & 2 deletions src/display/canvas_dependency_tracker.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Util } from "../shared/util.js";
import { FeatureTest, Util } from "../shared/util.js";

const FORCED_DEPENDENCY_LABEL = "__forcedDependency";

Expand Down Expand Up @@ -130,6 +130,10 @@ class CanvasDependencyTracker {
}
}

get clipBox() {
return this.#clipBox;
}

growOperationsCount(operationsCount) {
if (operationsCount >= this.#bboxes.length) {
this.#initializeBBoxes(operationsCount, this.#bboxes);
Expand Down Expand Up @@ -635,6 +639,10 @@ class CanvasNestedDependencyTracker {
this.#ignoreBBoxes = !!ignoreBBoxes;
}

get clipBox() {
return this.#dependencyTracker.clipBox;
}

growOperationsCount() {
throw new Error("Unreachable");
}
Expand Down Expand Up @@ -909,4 +917,141 @@ const Dependencies = {
transformAndFill: ["transform", "fillColor"],
};

export { CanvasDependencyTracker, CanvasNestedDependencyTracker, Dependencies };
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) {
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 {
CanvasDependencyTracker,
CanvasImagesTracker,
CanvasNestedDependencyTracker,
Dependencies,
};
Loading