pdf-raster-cpu 0.0.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/dist/index.cjs +563 -0
- package/dist/index.d.cts +28 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +561 -0
- package/package.json +98 -2
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { PageRasteriser, RasterDrawOp, RasterPageGeometry } from "pdf-codec/raster";
|
|
2
|
+
//#region src/rasteriser.d.ts
|
|
3
|
+
interface RasterCpuDiagnostic {
|
|
4
|
+
readonly code: "raster-cpu/jpeg-image-undecoded";
|
|
5
|
+
readonly message: string;
|
|
6
|
+
}
|
|
7
|
+
interface CpuRasteriserOptions {
|
|
8
|
+
readonly onDiagnostic?: (diagnostic: RasterCpuDiagnostic) => void;
|
|
9
|
+
}
|
|
10
|
+
declare function createCpuRasteriser(options?: CpuRasteriserOptions): PageRasteriser;
|
|
11
|
+
declare class CpuRasteriser implements PageRasteriser {
|
|
12
|
+
private readonly onDiagnostic;
|
|
13
|
+
private buffers;
|
|
14
|
+
private readonly decodedImages;
|
|
15
|
+
constructor(options?: CpuRasteriserOptions);
|
|
16
|
+
beginPage(geometry: RasterPageGeometry): void;
|
|
17
|
+
draw(op: RasterDrawOp): void;
|
|
18
|
+
finish(): Uint8Array<ArrayBuffer>;
|
|
19
|
+
private requirePage;
|
|
20
|
+
private fillRectOp;
|
|
21
|
+
private pathOp;
|
|
22
|
+
private imageOp;
|
|
23
|
+
private decodeCached;
|
|
24
|
+
private blendMask;
|
|
25
|
+
private blendPixel;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { CpuRasteriser, type CpuRasteriserOptions, type RasterCpuDiagnostic, createCpuRasteriser };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { PageRasteriser, RasterDrawOp, RasterPageGeometry } from "pdf-codec/raster";
|
|
2
|
+
//#region src/rasteriser.d.ts
|
|
3
|
+
interface RasterCpuDiagnostic {
|
|
4
|
+
readonly code: "raster-cpu/jpeg-image-undecoded";
|
|
5
|
+
readonly message: string;
|
|
6
|
+
}
|
|
7
|
+
interface CpuRasteriserOptions {
|
|
8
|
+
readonly onDiagnostic?: (diagnostic: RasterCpuDiagnostic) => void;
|
|
9
|
+
}
|
|
10
|
+
declare function createCpuRasteriser(options?: CpuRasteriserOptions): PageRasteriser;
|
|
11
|
+
declare class CpuRasteriser implements PageRasteriser {
|
|
12
|
+
private readonly onDiagnostic;
|
|
13
|
+
private buffers;
|
|
14
|
+
private readonly decodedImages;
|
|
15
|
+
constructor(options?: CpuRasteriserOptions);
|
|
16
|
+
beginPage(geometry: RasterPageGeometry): void;
|
|
17
|
+
draw(op: RasterDrawOp): void;
|
|
18
|
+
finish(): Uint8Array<ArrayBuffer>;
|
|
19
|
+
private requirePage;
|
|
20
|
+
private fillRectOp;
|
|
21
|
+
private pathOp;
|
|
22
|
+
private imageOp;
|
|
23
|
+
private decodeCached;
|
|
24
|
+
private blendMask;
|
|
25
|
+
private blendPixel;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { CpuRasteriser, type CpuRasteriserOptions, type RasterCpuDiagnostic, createCpuRasteriser };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { crc32, decodePng, encodePng } from "byte-codec";
|
|
2
|
+
var CoverageMask = class {
|
|
3
|
+
widthPx;
|
|
4
|
+
heightPx;
|
|
5
|
+
counts;
|
|
6
|
+
constructor(widthPx, heightPx) {
|
|
7
|
+
this.widthPx = widthPx;
|
|
8
|
+
this.heightPx = heightPx;
|
|
9
|
+
this.counts = new Uint16Array(widthPx * heightPx);
|
|
10
|
+
}
|
|
11
|
+
reset() {
|
|
12
|
+
this.counts.fill(0);
|
|
13
|
+
}
|
|
14
|
+
countAt(pixelIndex) {
|
|
15
|
+
return this.counts[pixelIndex] ?? 0;
|
|
16
|
+
}
|
|
17
|
+
fillPolygons(polygons, fillRule) {
|
|
18
|
+
if (polygons.length === 0) return;
|
|
19
|
+
let minY = Infinity;
|
|
20
|
+
let maxY = -Infinity;
|
|
21
|
+
for (const polygon of polygons) {
|
|
22
|
+
if (polygon.length < 3) continue;
|
|
23
|
+
for (const point of polygon) {
|
|
24
|
+
minY = Math.min(minY, point.y);
|
|
25
|
+
maxY = Math.max(maxY, point.y);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (maxY <= minY) return;
|
|
29
|
+
const rowMin = Math.max(0, Math.ceil(minY * 4 - .5));
|
|
30
|
+
const rowMax = Math.min(4 * this.heightPx - 1, Math.floor(maxY * 4 - .5));
|
|
31
|
+
const crossings = [];
|
|
32
|
+
for (let row = rowMin; row <= rowMax; row++) {
|
|
33
|
+
const y = (row + .5) / 4;
|
|
34
|
+
crossings.length = 0;
|
|
35
|
+
for (const polygon of polygons) for (let i = 0; i < polygon.length; i++) {
|
|
36
|
+
const a = polygon[i];
|
|
37
|
+
const b = polygon[(i + 1) % polygon.length];
|
|
38
|
+
if (a === void 0 || b === void 0) continue;
|
|
39
|
+
if (a.y <= y === b.y <= y) continue;
|
|
40
|
+
const t = (y - a.y) / (b.y - a.y);
|
|
41
|
+
crossings.push({
|
|
42
|
+
x: a.x + t * (b.x - a.x),
|
|
43
|
+
dir: b.y > a.y ? 1 : -1
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (crossings.length === 0) continue;
|
|
47
|
+
crossings.sort((p, q) => p.x - q.x);
|
|
48
|
+
const pixelRowBase = Math.floor(row / 4) * this.widthPx;
|
|
49
|
+
let spanStartX;
|
|
50
|
+
let winding = 0;
|
|
51
|
+
for (const crossing of crossings) {
|
|
52
|
+
if (fillRule === "evenodd") {
|
|
53
|
+
if (spanStartX === void 0) spanStartX = crossing.x;
|
|
54
|
+
else {
|
|
55
|
+
this.markSpan(pixelRowBase, spanStartX, crossing.x);
|
|
56
|
+
spanStartX = void 0;
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const previous = winding;
|
|
61
|
+
winding += crossing.dir;
|
|
62
|
+
if (previous === 0 && winding !== 0) spanStartX = crossing.x;
|
|
63
|
+
else if (spanStartX !== void 0 && winding === 0) {
|
|
64
|
+
this.markSpan(pixelRowBase, spanStartX, crossing.x);
|
|
65
|
+
spanStartX = void 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
markSpan(pixelRowBase, startX, endX) {
|
|
71
|
+
if (endX <= startX) return;
|
|
72
|
+
const columnFirst = Math.max(0, Math.ceil(startX * 4 - .5));
|
|
73
|
+
const columnLast = Math.min(4 * this.widthPx - 1, Math.floor(endX * 4 - .5));
|
|
74
|
+
for (let c = columnFirst; c <= columnLast; c++) {
|
|
75
|
+
const cellIndex = pixelRowBase + Math.floor(c / 4);
|
|
76
|
+
const cell = this.counts[cellIndex];
|
|
77
|
+
if (cell !== void 0) this.counts[cellIndex] = cell + 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
function flattenCubic(p0, c1, c2, p1) {
|
|
82
|
+
const points = [];
|
|
83
|
+
const flatten = (a, b, c, d, depth) => {
|
|
84
|
+
const chordX = d.x - a.x;
|
|
85
|
+
const chordY = d.y - a.y;
|
|
86
|
+
const chordLength = Math.hypot(chordX, chordY) || 1;
|
|
87
|
+
const dist1 = Math.abs((b.x - a.x) * chordY - (b.y - a.y) * chordX) / chordLength;
|
|
88
|
+
const dist2 = Math.abs((c.x - a.x) * chordY - (c.y - a.y) * chordX) / chordLength;
|
|
89
|
+
if (depth >= 16 || Math.max(dist1, dist2) <= .05) {
|
|
90
|
+
points.push(d);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const ab = {
|
|
94
|
+
x: (a.x + b.x) / 2,
|
|
95
|
+
y: (a.y + b.y) / 2
|
|
96
|
+
};
|
|
97
|
+
const bc = {
|
|
98
|
+
x: (b.x + c.x) / 2,
|
|
99
|
+
y: (b.y + c.y) / 2
|
|
100
|
+
};
|
|
101
|
+
const cd = {
|
|
102
|
+
x: (c.x + d.x) / 2,
|
|
103
|
+
y: (c.y + d.y) / 2
|
|
104
|
+
};
|
|
105
|
+
const abc = {
|
|
106
|
+
x: (ab.x + bc.x) / 2,
|
|
107
|
+
y: (ab.y + bc.y) / 2
|
|
108
|
+
};
|
|
109
|
+
const bcd = {
|
|
110
|
+
x: (bc.x + cd.x) / 2,
|
|
111
|
+
y: (bc.y + cd.y) / 2
|
|
112
|
+
};
|
|
113
|
+
const mid = {
|
|
114
|
+
x: (abc.x + bcd.x) / 2,
|
|
115
|
+
y: (abc.y + bcd.y) / 2
|
|
116
|
+
};
|
|
117
|
+
flatten(a, ab, abc, mid, depth + 1);
|
|
118
|
+
flatten(mid, bcd, cd, d, depth + 1);
|
|
119
|
+
};
|
|
120
|
+
flatten(p0, c1, c2, p1, 0);
|
|
121
|
+
return points;
|
|
122
|
+
}
|
|
123
|
+
function flattenSubpath(subpath) {
|
|
124
|
+
const points = [{
|
|
125
|
+
x: subpath.startXPx,
|
|
126
|
+
y: subpath.startYPx
|
|
127
|
+
}];
|
|
128
|
+
for (const segment of subpath.segments) {
|
|
129
|
+
if (segment.kind === "line") {
|
|
130
|
+
points.push({
|
|
131
|
+
x: segment.xPx,
|
|
132
|
+
y: segment.yPx
|
|
133
|
+
});
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const previous = points[points.length - 1];
|
|
137
|
+
if (previous === void 0) continue;
|
|
138
|
+
points.push(...flattenCubic(previous, {
|
|
139
|
+
x: segment.c1xPx,
|
|
140
|
+
y: segment.c1yPx
|
|
141
|
+
}, {
|
|
142
|
+
x: segment.c2xPx,
|
|
143
|
+
y: segment.c2yPx
|
|
144
|
+
}, {
|
|
145
|
+
x: segment.xPx,
|
|
146
|
+
y: segment.yPx
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
points,
|
|
151
|
+
closed: subpath.closed
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function strokeOutlinePolygons(subpaths, widthPx, dashPx) {
|
|
155
|
+
const half = Math.max(widthPx, 1) / 2;
|
|
156
|
+
const polygons = [];
|
|
157
|
+
const emitPiece = (piece) => {
|
|
158
|
+
for (let i = 0; i + 1 < piece.length; i++) {
|
|
159
|
+
const p = piece[i];
|
|
160
|
+
const q = piece[i + 1];
|
|
161
|
+
if (p === void 0 || q === void 0) continue;
|
|
162
|
+
const d = unitDirection(p, q);
|
|
163
|
+
if (d === void 0) continue;
|
|
164
|
+
const n = {
|
|
165
|
+
x: -d.y,
|
|
166
|
+
y: d.x
|
|
167
|
+
};
|
|
168
|
+
polygons.push([
|
|
169
|
+
{
|
|
170
|
+
x: p.x + n.x * half,
|
|
171
|
+
y: p.y + n.y * half
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
x: q.x + n.x * half,
|
|
175
|
+
y: q.y + n.y * half
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
x: q.x - n.x * half,
|
|
179
|
+
y: q.y - n.y * half
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
x: p.x - n.x * half,
|
|
183
|
+
y: p.y - n.y * half
|
|
184
|
+
}
|
|
185
|
+
]);
|
|
186
|
+
}
|
|
187
|
+
for (let i = 1; i + 1 < piece.length; i++) {
|
|
188
|
+
const vertex = piece[i];
|
|
189
|
+
const before = piece[i - 1];
|
|
190
|
+
const after = piece[i + 1];
|
|
191
|
+
if (vertex === void 0 || before === void 0 || after === void 0) continue;
|
|
192
|
+
emitJoinWedge(polygons, vertex, before, after, half);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
if (dashPx === void 0) {
|
|
196
|
+
for (const subpath of subpaths) {
|
|
197
|
+
const first = subpath.points[0];
|
|
198
|
+
if (first === void 0) continue;
|
|
199
|
+
emitPiece(subpath.closed ? [...subpath.points, first] : subpath.points);
|
|
200
|
+
if (subpath.closed && subpath.points.length >= 3) {
|
|
201
|
+
const last = subpath.points[subpath.points.length - 1];
|
|
202
|
+
const second = subpath.points[1];
|
|
203
|
+
if (last !== void 0 && second !== void 0) emitJoinWedge(polygons, first, last, second, half);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return polygons;
|
|
207
|
+
}
|
|
208
|
+
for (const subpath of subpaths) for (const piece of dashPolyline(subpath, dashPx)) emitPiece(piece);
|
|
209
|
+
return polygons;
|
|
210
|
+
}
|
|
211
|
+
function emitJoinWedge(polygons, vertex, before, after, half) {
|
|
212
|
+
const d1 = unitDirection(before, vertex);
|
|
213
|
+
const d2 = unitDirection(vertex, after);
|
|
214
|
+
if (d1 === void 0 || d2 === void 0) return;
|
|
215
|
+
const cross = d1.x * d2.y - d1.y * d2.x;
|
|
216
|
+
if (Math.abs(cross) < 1e-12) return;
|
|
217
|
+
const side = cross > 0 ? -1 : 1;
|
|
218
|
+
const n1 = {
|
|
219
|
+
x: -d1.y * side,
|
|
220
|
+
y: d1.x * side
|
|
221
|
+
};
|
|
222
|
+
const n2 = {
|
|
223
|
+
x: -d2.y * side,
|
|
224
|
+
y: d2.x * side
|
|
225
|
+
};
|
|
226
|
+
const a1 = {
|
|
227
|
+
x: vertex.x + n1.x * half,
|
|
228
|
+
y: vertex.y + n1.y * half
|
|
229
|
+
};
|
|
230
|
+
const a2 = {
|
|
231
|
+
x: vertex.x + n2.x * half,
|
|
232
|
+
y: vertex.y + n2.y * half
|
|
233
|
+
};
|
|
234
|
+
const dot = n1.x * n2.x + n1.y * n2.y;
|
|
235
|
+
const denominator = 1 + dot;
|
|
236
|
+
let wedge;
|
|
237
|
+
if (denominator > 1e-12 && miterRatio(dot) <= 10) {
|
|
238
|
+
const scale = half / denominator;
|
|
239
|
+
wedge = [
|
|
240
|
+
a1,
|
|
241
|
+
{
|
|
242
|
+
x: vertex.x + (n1.x + n2.x) * scale,
|
|
243
|
+
y: vertex.y + (n1.y + n2.y) * scale
|
|
244
|
+
},
|
|
245
|
+
a2,
|
|
246
|
+
vertex
|
|
247
|
+
];
|
|
248
|
+
} else wedge = [
|
|
249
|
+
a1,
|
|
250
|
+
vertex,
|
|
251
|
+
a2
|
|
252
|
+
];
|
|
253
|
+
polygons.push(polygonSignedArea(wedge) < 0 ? [...wedge] : [...wedge].reverse());
|
|
254
|
+
}
|
|
255
|
+
function miterRatio(dot) {
|
|
256
|
+
return Math.sqrt(2 + 2 * dot) / (1 + dot);
|
|
257
|
+
}
|
|
258
|
+
function polygonSignedArea(polygon) {
|
|
259
|
+
let area = 0;
|
|
260
|
+
for (let i = 0; i < polygon.length; i++) {
|
|
261
|
+
const a = polygon[i];
|
|
262
|
+
const b = polygon[(i + 1) % polygon.length];
|
|
263
|
+
if (a === void 0 || b === void 0) continue;
|
|
264
|
+
area += a.x * b.y - b.x * a.y;
|
|
265
|
+
}
|
|
266
|
+
return area;
|
|
267
|
+
}
|
|
268
|
+
function unitDirection(p, q) {
|
|
269
|
+
const dx = q.x - p.x;
|
|
270
|
+
const dy = q.y - p.y;
|
|
271
|
+
const length = Math.hypot(dx, dy);
|
|
272
|
+
if (length === 0) return;
|
|
273
|
+
return {
|
|
274
|
+
x: dx / length,
|
|
275
|
+
y: dy / length
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function dashPolyline(subpath, dashPx) {
|
|
279
|
+
if (!dashPx.some((length) => length > 0)) return [];
|
|
280
|
+
const pattern = dashPx.length % 2 === 1 ? [...dashPx, ...dashPx] : dashPx;
|
|
281
|
+
const points = subpath.closed ? [...subpath.points, subpath.points[0]] : subpath.points;
|
|
282
|
+
const start = points[0];
|
|
283
|
+
if (start === void 0) return [];
|
|
284
|
+
const pieces = [];
|
|
285
|
+
const endOnPiece = () => {
|
|
286
|
+
if (current.length >= 2) pieces.push(current);
|
|
287
|
+
current = [];
|
|
288
|
+
};
|
|
289
|
+
const atBoundary = () => {
|
|
290
|
+
if (index % 2 === 0) endOnPiece();
|
|
291
|
+
for (;;) {
|
|
292
|
+
index = (index + 1) % pattern.length;
|
|
293
|
+
const entry = pattern[index];
|
|
294
|
+
if (entry !== void 0 && entry > 0) {
|
|
295
|
+
remaining = entry;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (index % 2 === 0) current = [cursor];
|
|
300
|
+
};
|
|
301
|
+
let index = 0;
|
|
302
|
+
let remaining = 0;
|
|
303
|
+
let cursor = start;
|
|
304
|
+
let current = [];
|
|
305
|
+
const firstEntry = pattern[0];
|
|
306
|
+
if (firstEntry !== void 0 && firstEntry > 0) {
|
|
307
|
+
remaining = firstEntry;
|
|
308
|
+
current = [cursor];
|
|
309
|
+
} else atBoundary();
|
|
310
|
+
for (let i = 0; i + 1 < points.length; i++) {
|
|
311
|
+
const from = points[i];
|
|
312
|
+
const to = points[i + 1];
|
|
313
|
+
if (from === void 0 || to === void 0) continue;
|
|
314
|
+
const direction = unitDirection(from, to);
|
|
315
|
+
if (direction === void 0) continue;
|
|
316
|
+
cursor = from;
|
|
317
|
+
let segmentRemaining = Math.hypot(to.x - from.x, to.y - from.y);
|
|
318
|
+
while (segmentRemaining > 0 && remaining > 0) {
|
|
319
|
+
const step = Math.min(segmentRemaining, remaining);
|
|
320
|
+
cursor = {
|
|
321
|
+
x: cursor.x + direction.x * step,
|
|
322
|
+
y: cursor.y + direction.y * step
|
|
323
|
+
};
|
|
324
|
+
segmentRemaining -= step;
|
|
325
|
+
remaining -= step;
|
|
326
|
+
if (current.length > 0) current.push(cursor);
|
|
327
|
+
if (remaining === 0) atBoundary();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (current.length > 0) endOnPiece();
|
|
331
|
+
return pieces;
|
|
332
|
+
}
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/rasteriser.ts
|
|
335
|
+
function createCpuRasteriser(options = {}) {
|
|
336
|
+
return new CpuRasteriser(options);
|
|
337
|
+
}
|
|
338
|
+
var CpuRasteriser = class {
|
|
339
|
+
onDiagnostic;
|
|
340
|
+
buffers;
|
|
341
|
+
decodedImages = /* @__PURE__ */ new Map();
|
|
342
|
+
constructor(options = {}) {
|
|
343
|
+
this.onDiagnostic = options.onDiagnostic;
|
|
344
|
+
}
|
|
345
|
+
beginPage(geometry) {
|
|
346
|
+
this.buffers = {
|
|
347
|
+
geometry,
|
|
348
|
+
canvas: new Uint8Array(geometry.widthPx * geometry.heightPx * 3).fill(255),
|
|
349
|
+
mask: new CoverageMask(geometry.widthPx, geometry.heightPx)
|
|
350
|
+
};
|
|
351
|
+
this.decodedImages.clear();
|
|
352
|
+
}
|
|
353
|
+
draw(op) {
|
|
354
|
+
if (op.kind === "fillRect") {
|
|
355
|
+
this.fillRectOp(op);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (op.kind === "path") {
|
|
359
|
+
this.pathOp(op);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
this.imageOp(op);
|
|
363
|
+
}
|
|
364
|
+
finish() {
|
|
365
|
+
const { geometry, canvas } = this.requirePage("finish");
|
|
366
|
+
const png = encodePng({
|
|
367
|
+
width: geometry.widthPx,
|
|
368
|
+
height: geometry.heightPx,
|
|
369
|
+
channels: 3,
|
|
370
|
+
data: canvas
|
|
371
|
+
});
|
|
372
|
+
this.buffers = void 0;
|
|
373
|
+
return png;
|
|
374
|
+
}
|
|
375
|
+
requirePage(operation) {
|
|
376
|
+
if (this.buffers === void 0) throw new Error(`CpuRasteriser.${operation}() was called before beginPage(); renderPdfPage (or the caller) drives one page at a time through beginPage, draw..., finish`);
|
|
377
|
+
return this.buffers;
|
|
378
|
+
}
|
|
379
|
+
fillRectOp(op) {
|
|
380
|
+
const { geometry, canvas } = this.requirePage("draw");
|
|
381
|
+
const left = Math.max(op.xPx, 0);
|
|
382
|
+
const right = Math.min(op.xPx + op.widthPx, geometry.widthPx);
|
|
383
|
+
const top = Math.max(op.yPx, 0);
|
|
384
|
+
const bottom = Math.min(op.yPx + op.heightPx, geometry.heightPx);
|
|
385
|
+
if (right <= left || bottom <= top) return;
|
|
386
|
+
const colour = colourBytes(op.color);
|
|
387
|
+
const columnStart = Math.floor(left);
|
|
388
|
+
const columnEnd = Math.ceil(right);
|
|
389
|
+
const rowStart = Math.floor(top);
|
|
390
|
+
const rowEnd = Math.ceil(bottom);
|
|
391
|
+
for (let row = rowStart; row < rowEnd; row++) {
|
|
392
|
+
const coverageY = pixelOverlap(row, top, bottom);
|
|
393
|
+
if (coverageY === 0) continue;
|
|
394
|
+
const rowBase = row * geometry.widthPx;
|
|
395
|
+
for (let column = columnStart; column < columnEnd; column++) {
|
|
396
|
+
const coverage = coverageY * pixelOverlap(column, left, right);
|
|
397
|
+
if (coverage === 0) continue;
|
|
398
|
+
this.blendPixel(canvas, rowBase + column, colour, coverage);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
pathOp(op) {
|
|
403
|
+
const { canvas, mask } = this.requirePage("draw");
|
|
404
|
+
const flattened = op.subpaths.map(flattenSubpath);
|
|
405
|
+
if (op.fill !== void 0) {
|
|
406
|
+
const polygons = flattened.map((subpath) => subpath.points).filter((points) => points.length >= 3);
|
|
407
|
+
mask.reset();
|
|
408
|
+
mask.fillPolygons(polygons, op.fill.fillRule);
|
|
409
|
+
this.blendMask(canvas, mask, op.fill.color);
|
|
410
|
+
}
|
|
411
|
+
if (op.stroke !== void 0) {
|
|
412
|
+
const polygons = strokeOutlinePolygons(flattened, op.stroke.widthPx, op.stroke.dashPx);
|
|
413
|
+
mask.reset();
|
|
414
|
+
mask.fillPolygons(polygons, "nonzero");
|
|
415
|
+
this.blendMask(canvas, mask, op.stroke.color);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
imageOp(op) {
|
|
419
|
+
const { geometry, canvas, mask } = this.requirePage("draw");
|
|
420
|
+
if (op.format === "jpeg") {
|
|
421
|
+
this.onDiagnostic?.({
|
|
422
|
+
code: "raster-cpu/jpeg-image-undecoded",
|
|
423
|
+
message: "a JPEG (DCTDecode) image placement was not drawn: this backend decodes PNG images only (byte-codec has no DCT decoder), and approximating the scan's pixels would misrender it; the op's bytes remain available to the caller through the port"
|
|
424
|
+
});
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const source = this.decodeCached(op.bytes);
|
|
428
|
+
const quad = quadCorners(op.matrix);
|
|
429
|
+
mask.reset();
|
|
430
|
+
mask.fillPolygons([quad], "nonzero");
|
|
431
|
+
const inverse = invertMatrix(op.matrix);
|
|
432
|
+
const rowStart = Math.max(0, Math.floor(Math.min(...quad.map((corner) => corner.y))));
|
|
433
|
+
const rowEnd = Math.min(geometry.heightPx, Math.ceil(Math.max(...quad.map((corner) => corner.y))));
|
|
434
|
+
const columnStart = Math.max(0, Math.floor(Math.min(...quad.map((corner) => corner.x))));
|
|
435
|
+
const columnEnd = Math.min(geometry.widthPx, Math.ceil(Math.max(...quad.map((corner) => corner.x))));
|
|
436
|
+
for (let row = rowStart; row < rowEnd; row++) {
|
|
437
|
+
const rowBase = row * geometry.widthPx;
|
|
438
|
+
for (let column = columnStart; column < columnEnd; column++) {
|
|
439
|
+
const samples = mask.countAt(rowBase + column);
|
|
440
|
+
if (samples === 0) continue;
|
|
441
|
+
const [u, v] = inverse(column + .5, row + .5);
|
|
442
|
+
const [r, g, b, a] = sampleBilinear(source, u, v);
|
|
443
|
+
this.blendPixel(canvas, rowBase + column, {
|
|
444
|
+
r,
|
|
445
|
+
g,
|
|
446
|
+
b
|
|
447
|
+
}, samples / 16 * a);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
decodeCached(bytes) {
|
|
452
|
+
const key = `${bytes.length}:${crc32(bytes)}`;
|
|
453
|
+
const cached = this.decodedImages.get(key);
|
|
454
|
+
if (cached !== void 0) return cached;
|
|
455
|
+
const decoded = decodePng(bytes);
|
|
456
|
+
this.decodedImages.set(key, decoded);
|
|
457
|
+
return decoded;
|
|
458
|
+
}
|
|
459
|
+
blendMask(canvas, mask, color) {
|
|
460
|
+
const colour = colourBytes(color);
|
|
461
|
+
const pixels = mask.widthPx * mask.heightPx;
|
|
462
|
+
for (let i = 0; i < pixels; i++) {
|
|
463
|
+
const samples = mask.countAt(i);
|
|
464
|
+
if (samples === 0) continue;
|
|
465
|
+
this.blendPixel(canvas, i, colour, samples / 16);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
blendPixel(canvas, pixelIndex, colour, alpha) {
|
|
469
|
+
const base = pixelIndex * 3;
|
|
470
|
+
canvas[base] = Math.round((canvas[base] ?? 0) + (colour.r - (canvas[base] ?? 0)) * alpha);
|
|
471
|
+
canvas[base + 1] = Math.round((canvas[base + 1] ?? 0) + (colour.g - (canvas[base + 1] ?? 0)) * alpha);
|
|
472
|
+
canvas[base + 2] = Math.round((canvas[base + 2] ?? 0) + (colour.b - (canvas[base + 2] ?? 0)) * alpha);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
function colourBytes(color) {
|
|
476
|
+
return {
|
|
477
|
+
r: Math.round(color.r * 255),
|
|
478
|
+
g: Math.round(color.g * 255),
|
|
479
|
+
b: Math.round(color.b * 255)
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function pixelOverlap(index, edgeLow, edgeHigh) {
|
|
483
|
+
return Math.max(0, Math.min(index + 1, edgeHigh) - Math.max(index, edgeLow));
|
|
484
|
+
}
|
|
485
|
+
function quadCorners(matrix) {
|
|
486
|
+
const [a, b, c, d, e, f] = matrix;
|
|
487
|
+
const at = (u, v) => ({
|
|
488
|
+
x: a * u + c * v + e,
|
|
489
|
+
y: b * u + d * v + f
|
|
490
|
+
});
|
|
491
|
+
return [
|
|
492
|
+
at(0, 0),
|
|
493
|
+
at(1, 0),
|
|
494
|
+
at(1, 1),
|
|
495
|
+
at(0, 1)
|
|
496
|
+
];
|
|
497
|
+
}
|
|
498
|
+
function invertMatrix(matrix) {
|
|
499
|
+
const [a, b, c, d, e, f] = matrix;
|
|
500
|
+
const determinant = a * d - b * c;
|
|
501
|
+
return (xPx, yPx) => {
|
|
502
|
+
const dx = xPx - e;
|
|
503
|
+
const dy = yPx - f;
|
|
504
|
+
return [(d * dx - c * dy) / determinant, (-b * dx + a * dy) / determinant];
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function sampleBilinear(source, u, v) {
|
|
508
|
+
const clampedU = Math.min(Math.max(u, 0), 1);
|
|
509
|
+
const clampedV = Math.min(Math.max(v, 0), 1);
|
|
510
|
+
const sx = Math.min(Math.max(clampedU * source.width - .5, 0), source.width - 1);
|
|
511
|
+
const sy = Math.min(Math.max(clampedV * source.height - .5, 0), source.height - 1);
|
|
512
|
+
const x0 = Math.floor(sx);
|
|
513
|
+
const y0 = Math.floor(sy);
|
|
514
|
+
const x1 = Math.min(x0 + 1, source.width - 1);
|
|
515
|
+
const y1 = Math.min(y0 + 1, source.height - 1);
|
|
516
|
+
const fx = sx - x0;
|
|
517
|
+
const fy = sy - y0;
|
|
518
|
+
const channelCount = source.channels === 1 ? 1 : 3;
|
|
519
|
+
const sample = (x, y, channel) => {
|
|
520
|
+
const index = (y * source.width + x) * channelCount + channel;
|
|
521
|
+
return source.data[index] ?? 0;
|
|
522
|
+
};
|
|
523
|
+
const lerpChannel = (channel) => {
|
|
524
|
+
const top0 = sample(x0, y0, channel);
|
|
525
|
+
const top1 = sample(x1, y0, channel);
|
|
526
|
+
const bottom0 = sample(x0, y1, channel);
|
|
527
|
+
const bottom1 = sample(x1, y1, channel);
|
|
528
|
+
const top = top0 + (top1 - top0) * fx;
|
|
529
|
+
return top + (bottom0 + (bottom1 - bottom0) * fx - top) * fy;
|
|
530
|
+
};
|
|
531
|
+
let r;
|
|
532
|
+
let g;
|
|
533
|
+
let b;
|
|
534
|
+
if (channelCount === 1) {
|
|
535
|
+
r = lerpChannel(0);
|
|
536
|
+
g = r;
|
|
537
|
+
b = r;
|
|
538
|
+
} else {
|
|
539
|
+
r = lerpChannel(0);
|
|
540
|
+
g = lerpChannel(1);
|
|
541
|
+
b = lerpChannel(2);
|
|
542
|
+
}
|
|
543
|
+
const alpha = source.alpha;
|
|
544
|
+
if (alpha === void 0) return [
|
|
545
|
+
r,
|
|
546
|
+
g,
|
|
547
|
+
b,
|
|
548
|
+
1
|
|
549
|
+
];
|
|
550
|
+
const alphaAt = (x, y) => alpha[y * source.width + x] ?? 0;
|
|
551
|
+
const alphaTop = alphaAt(x0, y0) + (alphaAt(x1, y0) - alphaAt(x0, y0)) * fx;
|
|
552
|
+
const alphaBottom = alphaAt(x0, y1) + (alphaAt(x1, y1) - alphaAt(x0, y1)) * fx;
|
|
553
|
+
return [
|
|
554
|
+
r,
|
|
555
|
+
g,
|
|
556
|
+
b,
|
|
557
|
+
(alphaTop + (alphaBottom - alphaTop) * fy) / 255
|
|
558
|
+
];
|
|
559
|
+
}
|
|
560
|
+
//#endregion
|
|
561
|
+
export { CpuRasteriser, createCpuRasteriser };
|