Skip to content

Commit

Permalink
Add logic to track rendering area of various PDF ops
Browse files Browse the repository at this point in the history
This commit is a first step towards #6419, and it can also help with
#13287. To support rendering _part_ of a page, we will need to
first compute which ops can affect what is visible in that part of
the page.

This commit adds logic to track "group of ops" with their respective
bounding boxes. Each group eather corresponds to a single op or
to a range, and it can have dependencies earlier in the ops list that
are not contiguous to the range.

Consider the following example:
```
0. setFillRGBColor
1. beginText
2. showText "Hello"
3. endText
4. constructPath [...]
5. eoFill
```
here we have two groups: the text (range 1-3) and the path (range 4-5).
Each of them has a corresponding bounding box, and a dependency
on the op at index 0.

This tracking happens when first rendering a PDF: we wrap the canvas
with a "canvas recorder" that has the same API, but with additional
methods to mark the start/end of a group.
  • Loading branch information
nicolo-ribaudo committed Nov 14, 2024
1 parent 9bf9bbd commit 54be55a
Show file tree
Hide file tree
Showing 2 changed files with 372 additions and 24 deletions.
174 changes: 150 additions & 24 deletions src/display/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
PathType,
TilingPattern,
} from "./pattern_helper.js";
import { CanvasRecorder } from "./canvas_recorder.js";
import { convertBlackAndWhiteToRGBA } from "../shared/image_utils.js";

// <canvas> contexts store most of the state we need natively.
Expand All @@ -57,6 +58,27 @@ const MAX_SIZE_TO_COMPILE = 1000;

const FULL_CHUNK_HEIGHT = 16;

function setter(obj, name) {
return val => {
obj[name] = val;
};
}
function setterMany(obj, names) {
return val => {
for (const name of names) {
obj[name] = val;
}
};
}

function allValues(obj) {
const values = [];
for (const key in obj) {
values.push(obj[key]);
}
return values;
}

/**
* Overrides certain methods on a 2d ctx so that when they are called they
* will also call the same method on the destCtx. The methods that are
Expand Down Expand Up @@ -492,12 +514,15 @@ class CanvasExtraState {
this.activeSMask = null;
this.transferMaps = "none";

this.dependencyIndexes = Object.create(null);

this.startNewPathAndClipBox([0, 0, width, height]);
}

clone() {
const clone = Object.create(this);
clone.clipBox = this.clipBox.slice();
clone.dependencyIndexes = Object.create(this.dependencyIndexes);
return clone;
}

Expand Down Expand Up @@ -933,7 +958,8 @@ class CanvasGraphics {
operatorList,
executionStartIdx,
continueCallback,
stepper
stepper,
filter
) {
const argsArray = operatorList.argsArray;
const fnArray = operatorList.fnArray;
Expand Down Expand Up @@ -965,7 +991,10 @@ class CanvasGraphics {

if (fnId !== OPS.dependency) {
// eslint-disable-next-line prefer-spread
this[fnId].apply(this, argsArray[i]);
const setup = this[fnId].apply(this, argsArray[i]);
if (setup) {
setup(i);
}
} else {
for (const depObjId of argsArray[i]) {
const objsPool = depObjId.startsWith("g_") ? commonObjs : objs;
Expand Down Expand Up @@ -1285,14 +1314,20 @@ class CanvasGraphics {
}
this.current.lineWidth = width;
this.ctx.lineWidth = width;

return setter(this.current.dependencyIndexes, "lineWidth");
}

setLineCap(style) {
this.ctx.lineCap = LINE_CAP_STYLES[style];

return setter(this.current.dependencyIndexes, "lineCap");
}

setLineJoin(style) {
this.ctx.lineJoin = LINE_JOIN_STYLES[style];

return setter(this.current.dependencyIndexes, "lineJoin");
}

setMiterLimit(limit) {
Expand Down Expand Up @@ -1754,6 +1789,8 @@ class CanvasGraphics {
}

current.setCurrentPoint(x, y);

return setter(this, "_pathStartIdx");
}

closePath() {
Expand Down Expand Up @@ -1781,16 +1818,22 @@ class CanvasGraphics {
this.rescaleAndStroke(/* saveRestore */ true);
}
}

let res;
if (consumePath) {
this.consumePath(this.current.getClippedPathBoundingBox());
const bbox = this.current.getClippedPathBoundingBox();
res = this.consumePath(bbox);
}

// Restore the global alpha to the fill alpha
ctx.globalAlpha = this.current.fillAlpha;

return res;
}

closeStroke() {
this.closePath();
this.stroke();
return this.stroke();
}

fill(consumePath = true) {
Expand Down Expand Up @@ -1824,40 +1867,42 @@ class CanvasGraphics {
ctx.restore();
}
if (consumePath) {
this.consumePath(intersect);
return this.consumePath(intersect);
}

return undefined;
}

eoFill() {
this.pendingEOFill = true;
this.fill();
return this.fill();
}

fillStroke() {
this.fill(false);
this.stroke(false);

this.consumePath();
return this.consumePath();
}

eoFillStroke() {
this.pendingEOFill = true;
this.fillStroke();
return this.fillStroke();
}

closeFillStroke() {
this.closePath();
this.fillStroke();
return this.fillStroke();
}

closeEOFillStroke() {
this.pendingEOFill = true;
this.closePath();
this.fillStroke();
return this.fillStroke();
}

endPath() {
this.consumePath();
return this.consumePath();
}

// Clipping
Expand All @@ -1875,27 +1920,38 @@ class CanvasGraphics {
this.current.textMatrixScale = 1;
this.current.x = this.current.lineX = 0;
this.current.y = this.current.lineY = 0;

const group = CanvasRecorder.startGroupRecording(this.ctx, {
type: "text",
startIdx: -1,
endIdx: -1,
dependencies: allValues(this.current.dependencyIndexes),
});

return group ? setter(group.data, "startIdx") : null;
}

endText() {
const paths = this.pendingTextPaths;
const ctx = this.ctx;
if (paths === undefined) {
if (paths !== undefined) {
ctx.save();
ctx.beginPath();
return;
for (const path of paths) {
ctx.setTransform(...path.transform);
ctx.translate(path.x, path.y);
path.addToPath(ctx, path.fontSize);
}
ctx.restore();
ctx.clip();
}

ctx.save();
ctx.beginPath();
for (const path of paths) {
ctx.setTransform(...path.transform);
ctx.translate(path.x, path.y);
path.addToPath(ctx, path.fontSize);
}
ctx.restore();
ctx.clip();
const group = CanvasRecorder.endGroupRecording(this.ctx);

ctx.beginPath();
delete this.pendingTextPaths;

return group ? setter(group.data, "endIdx") : null;
}

setCharSpacing(spacing) {
Expand Down Expand Up @@ -2410,11 +2466,21 @@ class CanvasGraphics {
setStrokeColorN() {
this.current.strokeColor = this.getColorN_Pattern(arguments);
this.current.patternStroke = true;

return setterMany(this.current.dependencyIndexes, [
"strokeColor",
"patternStroke",
]);
}

setFillColorN() {
this.current.fillColor = this.getColorN_Pattern(arguments);
this.current.patternFill = true;

return setterMany(this.current.dependencyIndexes, [
"fillColor",
"patternFill",
]);
}

setStrokeRGBColor(r, g, b) {
Expand All @@ -2424,21 +2490,45 @@ class CanvasGraphics {
b
);
this.current.patternStroke = false;

return setterMany(this.current.dependencyIndexes, [
"strokeStyle",
"strokeColor",
"patternStroke",
]);
}

setStrokeTransparent() {
this.ctx.strokeStyle = this.current.strokeColor = "transparent";
this.current.patternStroke = false;

return setterMany(this.current.dependencyIndexes, [
"strokeStyle",
"strokeColor",
"patternStroke",
]);
}

setFillRGBColor(r, g, b) {
this.ctx.fillStyle = this.current.fillColor = Util.makeHexColor(r, g, b);
this.current.patternFill = false;

return setterMany(this.current.dependencyIndexes, [
"fillStyle",
"fillColor",
"patternFill",
]);
}

setFillTransparent() {
this.ctx.fillStyle = this.current.fillColor = "transparent";
this.current.patternFill = false;

return setterMany(this.current.dependencyIndexes, [
"fillStyle",
"fillColor",
"patternFill",
]);
}

_getPattern(objId, matrix = null) {
Expand Down Expand Up @@ -2901,15 +2991,23 @@ class CanvasGraphics {

paintImageXObject(objId) {
if (!this.contentVisible) {
return;
return undefined;
}
const imgData = this.getObject(objId);
if (!imgData) {
warn("Dependent image isn't ready yet");
return;
return undefined;
}

const group = CanvasRecorder.startGroupRecording(this.ctx, {
type: "image",
idx: -1,
});
this.paintInlineImageXObject(imgData);

CanvasRecorder.endGroupRecording(this.ctx);

return group ? setter(group.data, "idx") : null;
}

paintImageXObjectRepeat(objId, scaleX, scaleY, positions) {
Expand Down Expand Up @@ -3148,8 +3246,36 @@ class CanvasGraphics {
}
this.pendingClip = null;
}

let group = null;
if (clipBox) {
group = CanvasRecorder.pushGroup(
ctx,
clipBox[0],
clipBox[2],
clipBox[1],
clipBox[3],
{
type: "path",
startIdx: this._pathStartIdx,
endIdx: -1,
dependencies: allValues(this.current.dependencyIndexes),
}
);
} else {
// TODO: Get the actual box
group = CanvasRecorder.pushGroup(ctx, 0, Infinity, 0, Infinity, {
type: "path",
startIdx: this._pathStartIdx,
endIdx: -1,
dependencies: allValues(this.current.dependencyIndexes),
});
}

this.current.startNewPathAndClipBox(this.current.clipBox);
ctx.beginPath();

return group ? setter(group.data, "endIdx") : null;
}

getSinglePixelWidth() {
Expand Down
Loading

0 comments on commit 54be55a

Please sign in to comment.