pdf-raster-cpu 1.0.1 → 1.0.3

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/dist/index.cjs CHANGED
@@ -1,50 +1,62 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let byte_codec = require("byte-codec");
3
+ function firstSampleAtOrAfter(coordinate) {
4
+ return Math.max(0, Math.ceil(coordinate * 4 - .5));
5
+ }
6
+ function lastSampleBefore(coordinate, sizePx) {
7
+ return Math.min(4 * sizePx - 1, Math.floor(coordinate * 4 - .5));
8
+ }
3
9
  var CoverageMask = class {
4
10
  widthPx;
5
11
  heightPx;
6
12
  counts;
13
+ markedFirst;
14
+ markedLast;
7
15
  constructor(widthPx, heightPx) {
8
16
  this.widthPx = widthPx;
9
17
  this.heightPx = heightPx;
10
18
  this.counts = new Uint16Array(widthPx * heightPx);
19
+ this.markedFirst = this.counts.length;
20
+ this.markedLast = 0;
11
21
  }
12
22
  reset() {
13
23
  this.counts.fill(0);
24
+ this.markedFirst = this.counts.length;
25
+ this.markedLast = 0;
14
26
  }
15
27
  countAt(pixelIndex) {
16
28
  return this.counts[pixelIndex] ?? 0;
17
29
  }
30
+ markedRange() {
31
+ return {
32
+ first: this.markedFirst,
33
+ last: this.markedLast
34
+ };
35
+ }
18
36
  fillPolygons(polygons, fillRule) {
19
- if (polygons.length === 0) return;
20
- let minY = Infinity;
21
- let maxY = -Infinity;
37
+ const boundedYs = [];
22
38
  for (const polygon of polygons) {
23
39
  if (polygon.length < 3) continue;
24
- for (const point of polygon) {
25
- minY = Math.min(minY, point.y);
26
- maxY = Math.max(maxY, point.y);
27
- }
40
+ for (const point of polygon) boundedYs.push(point.y);
28
41
  }
29
- if (maxY <= minY) return;
30
- const rowMin = Math.max(0, Math.ceil(minY * 4 - .5));
31
- const rowMax = Math.min(4 * this.heightPx - 1, Math.floor(maxY * 4 - .5));
42
+ if (boundedYs.length === 0) return;
43
+ const rowMin = firstSampleAtOrAfter(boundedYs.reduce((lowest, y) => Math.min(lowest, y)));
44
+ const rowMax = lastSampleBefore(boundedYs.reduce((highest, y) => Math.max(highest, y)), this.heightPx);
32
45
  const crossings = [];
33
46
  for (let row = rowMin; row <= rowMax; row++) {
34
47
  const y = (row + .5) / 4;
35
48
  crossings.length = 0;
36
- for (const polygon of polygons) for (let i = 0; i < polygon.length; i++) {
37
- const a = polygon[i];
49
+ for (const polygon of polygons) for (const [i, a] of polygon.entries()) {
38
50
  const b = polygon[(i + 1) % polygon.length];
39
- if (a === void 0 || b === void 0) continue;
40
- if (a.y <= y === b.y <= y) continue;
51
+ if (b === void 0) continue;
52
+ const aAtOrAboveRow = a.y <= y;
53
+ if (aAtOrAboveRow === b.y <= y) continue;
41
54
  const t = (y - a.y) / (b.y - a.y);
42
55
  crossings.push({
43
56
  x: a.x + t * (b.x - a.x),
44
- dir: b.y > a.y ? 1 : -1
57
+ downward: aAtOrAboveRow
45
58
  });
46
59
  }
47
- if (crossings.length === 0) continue;
48
60
  crossings.sort((p, q) => p.x - q.x);
49
61
  const pixelRowBase = Math.floor(row / 4) * this.widthPx;
50
62
  let spanStartX;
@@ -58,9 +70,10 @@ var CoverageMask = class {
58
70
  }
59
71
  continue;
60
72
  }
61
- const previous = winding;
62
- winding += crossing.dir;
63
- if (previous === 0 && winding !== 0) spanStartX = crossing.x;
73
+ const wasZero = winding === 0;
74
+ if (crossing.downward) winding++;
75
+ else winding--;
76
+ if (wasZero) spanStartX = crossing.x;
64
77
  else if (spanStartX !== void 0 && winding === 0) {
65
78
  this.markSpan(pixelRowBase, spanStartX, crossing.x);
66
79
  spanStartX = void 0;
@@ -70,12 +83,16 @@ var CoverageMask = class {
70
83
  }
71
84
  markSpan(pixelRowBase, startX, endX) {
72
85
  if (endX <= startX) return;
73
- const columnFirst = Math.max(0, Math.ceil(startX * 4 - .5));
74
- const columnLast = Math.min(4 * this.widthPx - 1, Math.floor(endX * 4 - .5));
86
+ const columnFirst = firstSampleAtOrAfter(startX);
87
+ const columnLast = lastSampleBefore(endX, this.widthPx);
75
88
  for (let c = columnFirst; c <= columnLast; c++) {
76
89
  const cellIndex = pixelRowBase + Math.floor(c / 4);
77
90
  const cell = this.counts[cellIndex];
78
- if (cell !== void 0) this.counts[cellIndex] = cell + 1;
91
+ if (cell !== void 0) {
92
+ this.counts[cellIndex] = cell + 1;
93
+ this.markedFirst = Math.min(this.markedFirst, cellIndex);
94
+ this.markedLast = Math.max(this.markedLast, cellIndex);
95
+ }
79
96
  }
80
97
  }
81
98
  };
@@ -156,10 +173,9 @@ function strokeOutlinePolygons(subpaths, widthPx, dashPx) {
156
173
  const half = Math.max(widthPx, 1) / 2;
157
174
  const polygons = [];
158
175
  const emitPiece = (piece) => {
159
- for (let i = 0; i + 1 < piece.length; i++) {
160
- const p = piece[i];
176
+ for (const [i, p] of piece.entries()) {
161
177
  const q = piece[i + 1];
162
- if (p === void 0 || q === void 0) continue;
178
+ if (q === void 0) continue;
163
179
  const d = unitDirection(p, q);
164
180
  if (d === void 0) continue;
165
181
  const n = {
@@ -185,11 +201,10 @@ function strokeOutlinePolygons(subpaths, widthPx, dashPx) {
185
201
  }
186
202
  ]);
187
203
  }
188
- for (let i = 1; i + 1 < piece.length; i++) {
189
- const vertex = piece[i];
204
+ for (const [i, vertex] of piece.entries()) {
190
205
  const before = piece[i - 1];
191
206
  const after = piece[i + 1];
192
- if (vertex === void 0 || before === void 0 || after === void 0) continue;
207
+ if (before === void 0 || after === void 0) continue;
193
208
  emitJoinWedge(polygons, vertex, before, after, half);
194
209
  }
195
210
  };
@@ -198,7 +213,7 @@ function strokeOutlinePolygons(subpaths, widthPx, dashPx) {
198
213
  const first = subpath.points[0];
199
214
  if (first === void 0) continue;
200
215
  emitPiece(subpath.closed ? [...subpath.points, first] : subpath.points);
201
- if (subpath.closed && subpath.points.length >= 3) {
216
+ if (subpath.closed) {
202
217
  const last = subpath.points[subpath.points.length - 1];
203
218
  const second = subpath.points[1];
204
219
  if (last !== void 0 && second !== void 0) emitJoinWedge(polygons, first, last, second, half);
@@ -215,14 +230,20 @@ function emitJoinWedge(polygons, vertex, before, after, half) {
215
230
  if (d1 === void 0 || d2 === void 0) return;
216
231
  const cross = d1.x * d2.y - d1.y * d2.x;
217
232
  if (Math.abs(cross) < 1e-12) return;
218
- const side = cross > 0 ? -1 : 1;
219
- const n1 = {
220
- x: -d1.y * side,
221
- y: d1.x * side
233
+ const outward = turnsOutwardPositive(cross);
234
+ const n1 = outward ? {
235
+ x: d1.y,
236
+ y: -d1.x
237
+ } : {
238
+ x: -d1.y,
239
+ y: d1.x
222
240
  };
223
- const n2 = {
224
- x: -d2.y * side,
225
- y: d2.x * side
241
+ const n2 = outward ? {
242
+ x: d2.y,
243
+ y: -d2.x
244
+ } : {
245
+ x: -d2.y,
246
+ y: d2.x
226
247
  };
227
248
  const a1 = {
228
249
  x: vertex.x + n1.x * half,
@@ -232,10 +253,9 @@ function emitJoinWedge(polygons, vertex, before, after, half) {
232
253
  x: vertex.x + n2.x * half,
233
254
  y: vertex.y + n2.y * half
234
255
  };
235
- const dot = n1.x * n2.x + n1.y * n2.y;
236
- const denominator = 1 + dot;
256
+ const denominator = 1 + (n1.x * n2.x + n1.y * n2.y);
237
257
  let wedge;
238
- if (denominator > 1e-12 && miterRatio(dot) <= 10) {
258
+ if (takesMiterBranch(denominator)) {
239
259
  const scale = half / denominator;
240
260
  wedge = [
241
261
  a1,
@@ -251,17 +271,22 @@ function emitJoinWedge(polygons, vertex, before, after, half) {
251
271
  vertex,
252
272
  a2
253
273
  ];
254
- polygons.push(polygonSignedArea(wedge) < 0 ? [...wedge] : [...wedge].reverse());
274
+ polygons.push(withNegativeWinding(wedge));
275
+ }
276
+ function turnsOutwardPositive(cross) {
277
+ return cross > 0;
278
+ }
279
+ function takesMiterBranch(denominator) {
280
+ return 2 <= 100 * denominator;
255
281
  }
256
- function miterRatio(dot) {
257
- return Math.sqrt(2 + 2 * dot) / (1 + dot);
282
+ function withNegativeWinding(polygon) {
283
+ return polygonSignedArea(polygon) < 0 ? [...polygon] : [...polygon].reverse();
258
284
  }
259
285
  function polygonSignedArea(polygon) {
260
286
  let area = 0;
261
- for (let i = 0; i < polygon.length; i++) {
262
- const a = polygon[i];
287
+ for (const [i, a] of polygon.entries()) {
263
288
  const b = polygon[(i + 1) % polygon.length];
264
- if (a === void 0 || b === void 0) continue;
289
+ if (b === void 0) continue;
265
290
  area += a.x * b.y - b.x * a.y;
266
291
  }
267
292
  return area;
@@ -278,17 +303,17 @@ function unitDirection(p, q) {
278
303
  }
279
304
  function dashPolyline(subpath, dashPx) {
280
305
  if (!dashPx.some((length) => length > 0)) return [];
281
- const pattern = dashPx.length % 2 === 1 ? [...dashPx, ...dashPx] : dashPx;
282
- const points = subpath.closed ? [...subpath.points, subpath.points[0]] : subpath.points;
283
- const start = points[0];
306
+ const pattern = [...dashPx, ...dashPx];
307
+ const start = subpath.points[0];
284
308
  if (start === void 0) return [];
309
+ const points = subpath.closed ? [...subpath.points, start] : subpath.points;
285
310
  const pieces = [];
286
311
  const endOnPiece = () => {
287
312
  if (current.length >= 2) pieces.push(current);
288
313
  current = [];
289
314
  };
290
315
  const atBoundary = () => {
291
- if (index % 2 === 0) endOnPiece();
316
+ endOnPiece();
292
317
  for (;;) {
293
318
  index = (index + 1) % pattern.length;
294
319
  const entry = pattern[index];
@@ -307,16 +332,15 @@ function dashPolyline(subpath, dashPx) {
307
332
  if (firstEntry !== void 0 && firstEntry > 0) {
308
333
  remaining = firstEntry;
309
334
  current = [cursor];
310
- } else atBoundary();
311
- for (let i = 0; i + 1 < points.length; i++) {
312
- const from = points[i];
335
+ }
336
+ for (const [i, from] of points.entries()) {
313
337
  const to = points[i + 1];
314
- if (from === void 0 || to === void 0) continue;
338
+ if (to === void 0) continue;
315
339
  const direction = unitDirection(from, to);
316
340
  if (direction === void 0) continue;
317
341
  cursor = from;
318
342
  let segmentRemaining = Math.hypot(to.x - from.x, to.y - from.y);
319
- while (segmentRemaining > 0 && remaining > 0) {
343
+ while (segmentRemaining > 0) {
320
344
  const step = Math.min(segmentRemaining, remaining);
321
345
  cursor = {
322
346
  x: cursor.x + direction.x * step,
@@ -328,7 +352,7 @@ function dashPolyline(subpath, dashPx) {
328
352
  if (remaining === 0) atBoundary();
329
353
  }
330
354
  }
331
- if (current.length > 0) endOnPiece();
355
+ endOnPiece();
332
356
  return pieces;
333
357
  }
334
358
  //#endregion
@@ -351,6 +375,9 @@ var CpuRasteriser = class {
351
375
  };
352
376
  this.decodedImages.clear();
353
377
  }
378
+ decodedImageForTesting(bytes) {
379
+ return this.decodedImages.get(decodeCacheKey(bytes));
380
+ }
354
381
  draw(op) {
355
382
  if (op.kind === "fillRect") {
356
383
  this.fillRectOp(op);
@@ -380,10 +407,9 @@ var CpuRasteriser = class {
380
407
  fillRectOp(op) {
381
408
  const { geometry, canvas } = this.requirePage("draw");
382
409
  const left = Math.max(op.xPx, 0);
383
- const right = Math.min(op.xPx + op.widthPx, geometry.widthPx);
384
410
  const top = Math.max(op.yPx, 0);
385
- const bottom = Math.min(op.yPx + op.heightPx, geometry.heightPx);
386
- if (right <= left || bottom <= top) return;
411
+ const right = Math.max(Math.min(op.xPx + op.widthPx, geometry.widthPx), left);
412
+ const bottom = Math.max(Math.min(op.yPx + op.heightPx, geometry.heightPx), top);
387
413
  const colour = colourBytes(op.color);
388
414
  const columnStart = Math.floor(left);
389
415
  const columnEnd = Math.ceil(right);
@@ -391,11 +417,9 @@ var CpuRasteriser = class {
391
417
  const rowEnd = Math.ceil(bottom);
392
418
  for (let row = rowStart; row < rowEnd; row++) {
393
419
  const coverageY = pixelOverlap(row, top, bottom);
394
- if (coverageY === 0) continue;
395
420
  const rowBase = row * geometry.widthPx;
396
421
  for (let column = columnStart; column < columnEnd; column++) {
397
422
  const coverage = coverageY * pixelOverlap(column, left, right);
398
- if (coverage === 0) continue;
399
423
  this.blendPixel(canvas, rowBase + column, colour, coverage);
400
424
  }
401
425
  }
@@ -430,27 +454,21 @@ var CpuRasteriser = class {
430
454
  mask.reset();
431
455
  mask.fillPolygons([quad], "nonzero");
432
456
  const inverse = invertMatrix(op.matrix);
433
- const rowStart = Math.max(0, Math.floor(Math.min(...quad.map((corner) => corner.y))));
434
- const rowEnd = Math.min(geometry.heightPx, Math.ceil(Math.max(...quad.map((corner) => corner.y))));
435
- const columnStart = Math.max(0, Math.floor(Math.min(...quad.map((corner) => corner.x))));
436
- const columnEnd = Math.min(geometry.widthPx, Math.ceil(Math.max(...quad.map((corner) => corner.x))));
437
- for (let row = rowStart; row < rowEnd; row++) {
438
- const rowBase = row * geometry.widthPx;
439
- for (let column = columnStart; column < columnEnd; column++) {
440
- const samples = mask.countAt(rowBase + column);
441
- if (samples === 0) continue;
442
- const [u, v] = inverse(column + .5, row + .5);
443
- const [r, g, b, a] = sampleBilinear(source, u, v);
444
- this.blendPixel(canvas, rowBase + column, {
445
- r,
446
- g,
447
- b
448
- }, samples / 16 * a);
449
- }
457
+ const { first, last } = mask.markedRange();
458
+ for (let i = first; i <= last; i++) {
459
+ const samples = mask.countAt(i);
460
+ const row = Math.floor(i / geometry.widthPx);
461
+ const [u, v] = inverse(i % geometry.widthPx + .5, row + .5);
462
+ const [r, g, b, a] = sampleBilinear(source, u, v);
463
+ this.blendPixel(canvas, i, {
464
+ r,
465
+ g,
466
+ b
467
+ }, samples / 16 * a);
450
468
  }
451
469
  }
452
470
  decodeCached(bytes) {
453
- const key = `${bytes.length}:${(0, byte_codec.crc32)(bytes)}`;
471
+ const key = decodeCacheKey(bytes);
454
472
  const cached = this.decodedImages.get(key);
455
473
  if (cached !== void 0) return cached;
456
474
  const decoded = (0, byte_codec.decodePng)(bytes);
@@ -459,10 +477,9 @@ var CpuRasteriser = class {
459
477
  }
460
478
  blendMask(canvas, mask, color) {
461
479
  const colour = colourBytes(color);
462
- const pixels = mask.widthPx * mask.heightPx;
463
- for (let i = 0; i < pixels; i++) {
480
+ const { first, last } = mask.markedRange();
481
+ for (let i = first; i <= last; i++) {
464
482
  const samples = mask.countAt(i);
465
- if (samples === 0) continue;
466
483
  this.blendPixel(canvas, i, colour, samples / 16);
467
484
  }
468
485
  }
@@ -480,8 +497,11 @@ function colourBytes(color) {
480
497
  b: Math.round(color.b * 255)
481
498
  };
482
499
  }
500
+ function decodeCacheKey(bytes) {
501
+ return `${bytes.length}:${(0, byte_codec.crc32)(bytes)}`;
502
+ }
483
503
  function pixelOverlap(index, edgeLow, edgeHigh) {
484
- return Math.max(0, Math.min(index + 1, edgeHigh) - Math.max(index, edgeLow));
504
+ return Math.min(index + 1, edgeHigh) - Math.max(index, edgeLow);
485
505
  }
486
506
  function quadCorners(matrix) {
487
507
  const [a, b, c, d, e, f] = matrix;
@@ -512,8 +532,8 @@ function sampleBilinear(source, u, v) {
512
532
  const sy = Math.min(Math.max(clampedV * source.height - .5, 0), source.height - 1);
513
533
  const x0 = Math.floor(sx);
514
534
  const y0 = Math.floor(sy);
515
- const x1 = Math.min(x0 + 1, source.width - 1);
516
- const y1 = Math.min(y0 + 1, source.height - 1);
535
+ const x1 = x0 + 1;
536
+ const y1 = y0 + 1;
517
537
  const fx = sx - x0;
518
538
  const fy = sy - y0;
519
539
  const channelCount = source.channels === 1 ? 1 : 3;
package/dist/index.d.cts CHANGED
@@ -1,3 +1,4 @@
1
+ import { RawImage } from "byte-codec";
1
2
  import { PageRasteriser, RasterDrawOp, RasterPageGeometry } from "pdf-codec/raster";
2
3
  //#region src/rasteriser.d.ts
3
4
  interface RasterCpuDiagnostic {
@@ -14,6 +15,7 @@ declare class CpuRasteriser implements PageRasteriser {
14
15
  private readonly decodedImages;
15
16
  constructor(options?: CpuRasteriserOptions);
16
17
  beginPage(geometry: RasterPageGeometry): void;
18
+ decodedImageForTesting(bytes: Uint8Array<ArrayBuffer>): RawImage | undefined;
17
19
  draw(op: RasterDrawOp): void;
18
20
  finish(): Uint8Array<ArrayBuffer>;
19
21
  private requirePage;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { RawImage } from "byte-codec";
1
2
  import { PageRasteriser, RasterDrawOp, RasterPageGeometry } from "pdf-codec/raster";
2
3
  //#region src/rasteriser.d.ts
3
4
  interface RasterCpuDiagnostic {
@@ -14,6 +15,7 @@ declare class CpuRasteriser implements PageRasteriser {
14
15
  private readonly decodedImages;
15
16
  constructor(options?: CpuRasteriserOptions);
16
17
  beginPage(geometry: RasterPageGeometry): void;
18
+ decodedImageForTesting(bytes: Uint8Array<ArrayBuffer>): RawImage | undefined;
17
19
  draw(op: RasterDrawOp): void;
18
20
  finish(): Uint8Array<ArrayBuffer>;
19
21
  private requirePage;
package/dist/index.js CHANGED
@@ -1,49 +1,61 @@
1
1
  import { crc32, decodePng, encodePng } from "byte-codec";
2
+ function firstSampleAtOrAfter(coordinate) {
3
+ return Math.max(0, Math.ceil(coordinate * 4 - .5));
4
+ }
5
+ function lastSampleBefore(coordinate, sizePx) {
6
+ return Math.min(4 * sizePx - 1, Math.floor(coordinate * 4 - .5));
7
+ }
2
8
  var CoverageMask = class {
3
9
  widthPx;
4
10
  heightPx;
5
11
  counts;
12
+ markedFirst;
13
+ markedLast;
6
14
  constructor(widthPx, heightPx) {
7
15
  this.widthPx = widthPx;
8
16
  this.heightPx = heightPx;
9
17
  this.counts = new Uint16Array(widthPx * heightPx);
18
+ this.markedFirst = this.counts.length;
19
+ this.markedLast = 0;
10
20
  }
11
21
  reset() {
12
22
  this.counts.fill(0);
23
+ this.markedFirst = this.counts.length;
24
+ this.markedLast = 0;
13
25
  }
14
26
  countAt(pixelIndex) {
15
27
  return this.counts[pixelIndex] ?? 0;
16
28
  }
29
+ markedRange() {
30
+ return {
31
+ first: this.markedFirst,
32
+ last: this.markedLast
33
+ };
34
+ }
17
35
  fillPolygons(polygons, fillRule) {
18
- if (polygons.length === 0) return;
19
- let minY = Infinity;
20
- let maxY = -Infinity;
36
+ const boundedYs = [];
21
37
  for (const polygon of polygons) {
22
38
  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
- }
39
+ for (const point of polygon) boundedYs.push(point.y);
27
40
  }
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));
41
+ if (boundedYs.length === 0) return;
42
+ const rowMin = firstSampleAtOrAfter(boundedYs.reduce((lowest, y) => Math.min(lowest, y)));
43
+ const rowMax = lastSampleBefore(boundedYs.reduce((highest, y) => Math.max(highest, y)), this.heightPx);
31
44
  const crossings = [];
32
45
  for (let row = rowMin; row <= rowMax; row++) {
33
46
  const y = (row + .5) / 4;
34
47
  crossings.length = 0;
35
- for (const polygon of polygons) for (let i = 0; i < polygon.length; i++) {
36
- const a = polygon[i];
48
+ for (const polygon of polygons) for (const [i, a] of polygon.entries()) {
37
49
  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;
50
+ if (b === void 0) continue;
51
+ const aAtOrAboveRow = a.y <= y;
52
+ if (aAtOrAboveRow === b.y <= y) continue;
40
53
  const t = (y - a.y) / (b.y - a.y);
41
54
  crossings.push({
42
55
  x: a.x + t * (b.x - a.x),
43
- dir: b.y > a.y ? 1 : -1
56
+ downward: aAtOrAboveRow
44
57
  });
45
58
  }
46
- if (crossings.length === 0) continue;
47
59
  crossings.sort((p, q) => p.x - q.x);
48
60
  const pixelRowBase = Math.floor(row / 4) * this.widthPx;
49
61
  let spanStartX;
@@ -57,9 +69,10 @@ var CoverageMask = class {
57
69
  }
58
70
  continue;
59
71
  }
60
- const previous = winding;
61
- winding += crossing.dir;
62
- if (previous === 0 && winding !== 0) spanStartX = crossing.x;
72
+ const wasZero = winding === 0;
73
+ if (crossing.downward) winding++;
74
+ else winding--;
75
+ if (wasZero) spanStartX = crossing.x;
63
76
  else if (spanStartX !== void 0 && winding === 0) {
64
77
  this.markSpan(pixelRowBase, spanStartX, crossing.x);
65
78
  spanStartX = void 0;
@@ -69,12 +82,16 @@ var CoverageMask = class {
69
82
  }
70
83
  markSpan(pixelRowBase, startX, endX) {
71
84
  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));
85
+ const columnFirst = firstSampleAtOrAfter(startX);
86
+ const columnLast = lastSampleBefore(endX, this.widthPx);
74
87
  for (let c = columnFirst; c <= columnLast; c++) {
75
88
  const cellIndex = pixelRowBase + Math.floor(c / 4);
76
89
  const cell = this.counts[cellIndex];
77
- if (cell !== void 0) this.counts[cellIndex] = cell + 1;
90
+ if (cell !== void 0) {
91
+ this.counts[cellIndex] = cell + 1;
92
+ this.markedFirst = Math.min(this.markedFirst, cellIndex);
93
+ this.markedLast = Math.max(this.markedLast, cellIndex);
94
+ }
78
95
  }
79
96
  }
80
97
  };
@@ -155,10 +172,9 @@ function strokeOutlinePolygons(subpaths, widthPx, dashPx) {
155
172
  const half = Math.max(widthPx, 1) / 2;
156
173
  const polygons = [];
157
174
  const emitPiece = (piece) => {
158
- for (let i = 0; i + 1 < piece.length; i++) {
159
- const p = piece[i];
175
+ for (const [i, p] of piece.entries()) {
160
176
  const q = piece[i + 1];
161
- if (p === void 0 || q === void 0) continue;
177
+ if (q === void 0) continue;
162
178
  const d = unitDirection(p, q);
163
179
  if (d === void 0) continue;
164
180
  const n = {
@@ -184,11 +200,10 @@ function strokeOutlinePolygons(subpaths, widthPx, dashPx) {
184
200
  }
185
201
  ]);
186
202
  }
187
- for (let i = 1; i + 1 < piece.length; i++) {
188
- const vertex = piece[i];
203
+ for (const [i, vertex] of piece.entries()) {
189
204
  const before = piece[i - 1];
190
205
  const after = piece[i + 1];
191
- if (vertex === void 0 || before === void 0 || after === void 0) continue;
206
+ if (before === void 0 || after === void 0) continue;
192
207
  emitJoinWedge(polygons, vertex, before, after, half);
193
208
  }
194
209
  };
@@ -197,7 +212,7 @@ function strokeOutlinePolygons(subpaths, widthPx, dashPx) {
197
212
  const first = subpath.points[0];
198
213
  if (first === void 0) continue;
199
214
  emitPiece(subpath.closed ? [...subpath.points, first] : subpath.points);
200
- if (subpath.closed && subpath.points.length >= 3) {
215
+ if (subpath.closed) {
201
216
  const last = subpath.points[subpath.points.length - 1];
202
217
  const second = subpath.points[1];
203
218
  if (last !== void 0 && second !== void 0) emitJoinWedge(polygons, first, last, second, half);
@@ -214,14 +229,20 @@ function emitJoinWedge(polygons, vertex, before, after, half) {
214
229
  if (d1 === void 0 || d2 === void 0) return;
215
230
  const cross = d1.x * d2.y - d1.y * d2.x;
216
231
  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
232
+ const outward = turnsOutwardPositive(cross);
233
+ const n1 = outward ? {
234
+ x: d1.y,
235
+ y: -d1.x
236
+ } : {
237
+ x: -d1.y,
238
+ y: d1.x
221
239
  };
222
- const n2 = {
223
- x: -d2.y * side,
224
- y: d2.x * side
240
+ const n2 = outward ? {
241
+ x: d2.y,
242
+ y: -d2.x
243
+ } : {
244
+ x: -d2.y,
245
+ y: d2.x
225
246
  };
226
247
  const a1 = {
227
248
  x: vertex.x + n1.x * half,
@@ -231,10 +252,9 @@ function emitJoinWedge(polygons, vertex, before, after, half) {
231
252
  x: vertex.x + n2.x * half,
232
253
  y: vertex.y + n2.y * half
233
254
  };
234
- const dot = n1.x * n2.x + n1.y * n2.y;
235
- const denominator = 1 + dot;
255
+ const denominator = 1 + (n1.x * n2.x + n1.y * n2.y);
236
256
  let wedge;
237
- if (denominator > 1e-12 && miterRatio(dot) <= 10) {
257
+ if (takesMiterBranch(denominator)) {
238
258
  const scale = half / denominator;
239
259
  wedge = [
240
260
  a1,
@@ -250,17 +270,22 @@ function emitJoinWedge(polygons, vertex, before, after, half) {
250
270
  vertex,
251
271
  a2
252
272
  ];
253
- polygons.push(polygonSignedArea(wedge) < 0 ? [...wedge] : [...wedge].reverse());
273
+ polygons.push(withNegativeWinding(wedge));
274
+ }
275
+ function turnsOutwardPositive(cross) {
276
+ return cross > 0;
277
+ }
278
+ function takesMiterBranch(denominator) {
279
+ return 2 <= 100 * denominator;
254
280
  }
255
- function miterRatio(dot) {
256
- return Math.sqrt(2 + 2 * dot) / (1 + dot);
281
+ function withNegativeWinding(polygon) {
282
+ return polygonSignedArea(polygon) < 0 ? [...polygon] : [...polygon].reverse();
257
283
  }
258
284
  function polygonSignedArea(polygon) {
259
285
  let area = 0;
260
- for (let i = 0; i < polygon.length; i++) {
261
- const a = polygon[i];
286
+ for (const [i, a] of polygon.entries()) {
262
287
  const b = polygon[(i + 1) % polygon.length];
263
- if (a === void 0 || b === void 0) continue;
288
+ if (b === void 0) continue;
264
289
  area += a.x * b.y - b.x * a.y;
265
290
  }
266
291
  return area;
@@ -277,17 +302,17 @@ function unitDirection(p, q) {
277
302
  }
278
303
  function dashPolyline(subpath, dashPx) {
279
304
  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];
305
+ const pattern = [...dashPx, ...dashPx];
306
+ const start = subpath.points[0];
283
307
  if (start === void 0) return [];
308
+ const points = subpath.closed ? [...subpath.points, start] : subpath.points;
284
309
  const pieces = [];
285
310
  const endOnPiece = () => {
286
311
  if (current.length >= 2) pieces.push(current);
287
312
  current = [];
288
313
  };
289
314
  const atBoundary = () => {
290
- if (index % 2 === 0) endOnPiece();
315
+ endOnPiece();
291
316
  for (;;) {
292
317
  index = (index + 1) % pattern.length;
293
318
  const entry = pattern[index];
@@ -306,16 +331,15 @@ function dashPolyline(subpath, dashPx) {
306
331
  if (firstEntry !== void 0 && firstEntry > 0) {
307
332
  remaining = firstEntry;
308
333
  current = [cursor];
309
- } else atBoundary();
310
- for (let i = 0; i + 1 < points.length; i++) {
311
- const from = points[i];
334
+ }
335
+ for (const [i, from] of points.entries()) {
312
336
  const to = points[i + 1];
313
- if (from === void 0 || to === void 0) continue;
337
+ if (to === void 0) continue;
314
338
  const direction = unitDirection(from, to);
315
339
  if (direction === void 0) continue;
316
340
  cursor = from;
317
341
  let segmentRemaining = Math.hypot(to.x - from.x, to.y - from.y);
318
- while (segmentRemaining > 0 && remaining > 0) {
342
+ while (segmentRemaining > 0) {
319
343
  const step = Math.min(segmentRemaining, remaining);
320
344
  cursor = {
321
345
  x: cursor.x + direction.x * step,
@@ -327,7 +351,7 @@ function dashPolyline(subpath, dashPx) {
327
351
  if (remaining === 0) atBoundary();
328
352
  }
329
353
  }
330
- if (current.length > 0) endOnPiece();
354
+ endOnPiece();
331
355
  return pieces;
332
356
  }
333
357
  //#endregion
@@ -350,6 +374,9 @@ var CpuRasteriser = class {
350
374
  };
351
375
  this.decodedImages.clear();
352
376
  }
377
+ decodedImageForTesting(bytes) {
378
+ return this.decodedImages.get(decodeCacheKey(bytes));
379
+ }
353
380
  draw(op) {
354
381
  if (op.kind === "fillRect") {
355
382
  this.fillRectOp(op);
@@ -379,10 +406,9 @@ var CpuRasteriser = class {
379
406
  fillRectOp(op) {
380
407
  const { geometry, canvas } = this.requirePage("draw");
381
408
  const left = Math.max(op.xPx, 0);
382
- const right = Math.min(op.xPx + op.widthPx, geometry.widthPx);
383
409
  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;
410
+ const right = Math.max(Math.min(op.xPx + op.widthPx, geometry.widthPx), left);
411
+ const bottom = Math.max(Math.min(op.yPx + op.heightPx, geometry.heightPx), top);
386
412
  const colour = colourBytes(op.color);
387
413
  const columnStart = Math.floor(left);
388
414
  const columnEnd = Math.ceil(right);
@@ -390,11 +416,9 @@ var CpuRasteriser = class {
390
416
  const rowEnd = Math.ceil(bottom);
391
417
  for (let row = rowStart; row < rowEnd; row++) {
392
418
  const coverageY = pixelOverlap(row, top, bottom);
393
- if (coverageY === 0) continue;
394
419
  const rowBase = row * geometry.widthPx;
395
420
  for (let column = columnStart; column < columnEnd; column++) {
396
421
  const coverage = coverageY * pixelOverlap(column, left, right);
397
- if (coverage === 0) continue;
398
422
  this.blendPixel(canvas, rowBase + column, colour, coverage);
399
423
  }
400
424
  }
@@ -429,27 +453,21 @@ var CpuRasteriser = class {
429
453
  mask.reset();
430
454
  mask.fillPolygons([quad], "nonzero");
431
455
  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
- }
456
+ const { first, last } = mask.markedRange();
457
+ for (let i = first; i <= last; i++) {
458
+ const samples = mask.countAt(i);
459
+ const row = Math.floor(i / geometry.widthPx);
460
+ const [u, v] = inverse(i % geometry.widthPx + .5, row + .5);
461
+ const [r, g, b, a] = sampleBilinear(source, u, v);
462
+ this.blendPixel(canvas, i, {
463
+ r,
464
+ g,
465
+ b
466
+ }, samples / 16 * a);
449
467
  }
450
468
  }
451
469
  decodeCached(bytes) {
452
- const key = `${bytes.length}:${crc32(bytes)}`;
470
+ const key = decodeCacheKey(bytes);
453
471
  const cached = this.decodedImages.get(key);
454
472
  if (cached !== void 0) return cached;
455
473
  const decoded = decodePng(bytes);
@@ -458,10 +476,9 @@ var CpuRasteriser = class {
458
476
  }
459
477
  blendMask(canvas, mask, color) {
460
478
  const colour = colourBytes(color);
461
- const pixels = mask.widthPx * mask.heightPx;
462
- for (let i = 0; i < pixels; i++) {
479
+ const { first, last } = mask.markedRange();
480
+ for (let i = first; i <= last; i++) {
463
481
  const samples = mask.countAt(i);
464
- if (samples === 0) continue;
465
482
  this.blendPixel(canvas, i, colour, samples / 16);
466
483
  }
467
484
  }
@@ -479,8 +496,11 @@ function colourBytes(color) {
479
496
  b: Math.round(color.b * 255)
480
497
  };
481
498
  }
499
+ function decodeCacheKey(bytes) {
500
+ return `${bytes.length}:${crc32(bytes)}`;
501
+ }
482
502
  function pixelOverlap(index, edgeLow, edgeHigh) {
483
- return Math.max(0, Math.min(index + 1, edgeHigh) - Math.max(index, edgeLow));
503
+ return Math.min(index + 1, edgeHigh) - Math.max(index, edgeLow);
484
504
  }
485
505
  function quadCorners(matrix) {
486
506
  const [a, b, c, d, e, f] = matrix;
@@ -511,8 +531,8 @@ function sampleBilinear(source, u, v) {
511
531
  const sy = Math.min(Math.max(clampedV * source.height - .5, 0), source.height - 1);
512
532
  const x0 = Math.floor(sx);
513
533
  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);
534
+ const x1 = x0 + 1;
535
+ const y1 = y0 + 1;
516
536
  const fx = sx - x0;
517
537
  const fy = sy - y0;
518
538
  const channelCount = source.channels === 1 ? 1 : 3;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf-raster-cpu",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Pure-software reference backend for pdf-codec's PageRasteriser port: an RGBA scanline rasteriser that renders a page (or a located figure region) to PNG bytes with no canvas dependency, on any Worker-isomorphic runtime.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -78,8 +78,8 @@
78
78
  "license": "MIT",
79
79
  "packageManager": "pnpm@11.6.0",
80
80
  "dependencies": {
81
- "byte-codec": "1.5.1",
82
- "pdf-codec": "4.8.1"
81
+ "byte-codec": "1.5.2",
82
+ "pdf-codec": "4.8.2"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@arethetypeswrong/cli": "0.18.5",