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