@reportwright/pdf 0.1.0-beta.3 → 0.1.0-beta.5

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/README.md CHANGED
@@ -322,7 +322,7 @@ PDF/A levels and the reader. The most used:
322
322
  | `createPdf(sink, o)` / `toBytes(build, o)` | `title`, `author`, `creationDate`, `compress`, `metadata`, `pdfa`, `tagged`, `encrypt`, `sign`, `form.flatten`, `signal`, `timeoutMs`, `id` ([table](#createpdfsink-options)) |
323
323
  | `pdf.embedFont(bytes, o)` | `subset`, `shaper`, `textMapping` ([Fonts](#fonts), [Shaping](#shaping)) |
324
324
  | `page.text` / `page.textBox` | `x`, `y`, `font`, `size`, `color`, `width`, `align`, `maxLines`, `fallback`, `direction`, `kerning` ([Text options](#pages)) |
325
- | `pdf.embedImage` / `page.image` | `colorSpace`, `maxDecodedBytes` / `width`, `height`, `fit`, `opacity`, `alt` ([Images](#images)) |
325
+ | `pdf.embedImage` / `page.image` | `colorSpace`, `maxDecodedBytes`, `compression` / `width`, `height`, `fit`, `opacity`, `alt` ([Images](#images)) |
326
326
  | `page.fill` / `stroke` | `fill`, `stroke`, `width`, `dash`, `cap`, `join`, `opacity`, `blendMode`, `rule` ([Vector graphics](#vector-graphics)) |
327
327
  | `encrypt` | `userPassword`, `ownerPassword`, `algorithm`, `permissions`, `encryptMetadata` ([Encryption](#encryption-1)) |
328
328
  | `pdf.sign(field, o)` | `signer`, `reason`, `location`, `name`, `certify`, `timestamp`, `subFilter`, `reserve` ([Signatures](#signatures)) |
@@ -573,10 +573,12 @@ const logo = await pdf.embedImage(fs.readFileSync('logo.png')); // alpha
573
573
  for (const y of [400, 500, 600]) page.image(logo, { x: 40, y, height: 24, opacity: 0.6 }); // written once
574
574
  ```
575
575
 
576
- - `await pdf.embedImage(bytes, { colorSpace, maxDecodedBytes })` — a JPEG or PNG, written to the file at once as an image XObject;
576
+ - `await pdf.embedImage(bytes, { colorSpace, maxDecodedBytes, compression })` — a JPEG or PNG, written to the file at once as an image XObject;
577
577
  returns a handle with `width` and `height` (pixels, after the EXIF orientation). `colorSpace`: an ICC colour space
578
578
  (`pdf.iccColorSpace`) with the image's number of components, instead of the device space. `maxDecodedBytes`: the
579
- decoded size allowed for a PNG that needs decoding (default 64 MB, 67,108,864 bytes).
579
+ decoded size allowed for a PNG that needs decoding (default 64 MB, 67,108,864 bytes). `compression`: the zlib level
580
+ for a PNG that is re-compressed (alpha or interlacing): `'default'` is 6, or 3 above 2 megapixels (about half the
581
+ time for 2–3% more bytes on photos); `'fast'` is 1; or a level 1–9. JPEGs and passed-through PNGs are not affected.
580
582
  - **JPEG**: baseline and progressive, gray, RGB and CMYK, embedded unchanged (DCTDecode, no re-encoding). Adobe's
581
583
  inverted CMYK (an APP14 "Adobe" segment) is drawn with `/Decode [1 0 1 0 1 0 1 0]`. The EXIF orientation (1–8)
582
584
  is honoured. 12-bit, lossless, hierarchical and arithmetic-coded JPEGs are refused.
@@ -596,7 +598,7 @@ for (const y of [400, 500, 600]) page.image(logo, { x: 40, y, height: 24, opacit
596
598
  PDFs: `alt` makes the image a `Figure` with that alternate text; without `alt` it is an artifact (decoration).
597
599
  - Images are deduplicated automatically: `embedImage` called again with the same bytes (and the same options) in the
598
600
  same document returns the same handle and writes nothing new (keyed by a SHA-256 of the bytes; Node's `node:crypto`,
599
- else WebCrypto, else a fast hash confirmed byte for byte). A different option (`colorSpace`, `maxDecodedBytes`)
601
+ else WebCrypto, else a fast hash confirmed byte for byte). A different option (`colorSpace`, `maxDecodedBytes`, `compression`)
600
602
  embeds it again.
601
603
  - An image drawn any number of times, on any pages, is one XObject in the file. Images are always XObjects, never
602
604
  inline images: an inline image would be repeated in every content stream and cannot have a soft mask.
package/dist/index.js CHANGED
@@ -957,29 +957,32 @@ var init_browser = __esm({
957
957
  });
958
958
 
959
959
  // src/core/deflate.js
960
- async function deflate(bytes) {
960
+ async function deflate(bytes, level = 6) {
961
961
  if (slots.free > 0) slots.free--;
962
962
  else await new Promise((ok) => slots.wait.push(
963
963
  /** @type {() => void} */
964
964
  ok
965
965
  ));
966
966
  try {
967
- return await now(bytes);
967
+ return await now(bytes, level);
968
968
  } finally {
969
969
  const next = slots.wait.shift();
970
970
  if (next) next();
971
971
  else slots.free++;
972
972
  }
973
973
  }
974
- async function now(bytes) {
975
- if (NODE_ZLIB) return new Promise((ok, fail2) => NODE_ZLIB.deflate(bytes, { level: 6 }, (e, z) => e ? fail2(e) : ok(z)));
976
- if (typeof CompressionStream === "undefined") return zlibSync(bytes, { level: 6 });
974
+ async function now(bytes, level) {
975
+ if (NODE_ZLIB) return new Promise((ok, fail2) => NODE_ZLIB.deflate(bytes, { level }, (e, z) => e ? fail2(e) : ok(z)));
976
+ if (typeof CompressionStream === "undefined" || level !== 6) return zlibSync(bytes, { level: (
977
+ /** @type {any} */
978
+ level
979
+ ) });
977
980
  return new Uint8Array(await new Response(new Blob([
978
981
  /** @type {any} */
979
982
  bytes
980
983
  ]).stream().pipeThrough(new CompressionStream("deflate"))).arrayBuffer());
981
984
  }
982
- function deflateStream() {
985
+ function deflateStream(level = 6) {
983
986
  const out = [];
984
987
  const join = () => {
985
988
  let n = 0;
@@ -993,7 +996,7 @@ function deflateStream() {
993
996
  return z;
994
997
  };
995
998
  if (NODE_ZLIB) {
996
- const z = NODE_ZLIB.createDeflate({ level: 6 });
999
+ const z = NODE_ZLIB.createDeflate({ level, chunkSize: 1 << 20 });
997
1000
  const ended = new Promise((ok, fail2) => {
998
1001
  z.on("data", (c) => out.push(c));
999
1002
  z.on("end", ok);
@@ -1017,7 +1020,19 @@ function deflateStream() {
1017
1020
  if (typeof CompressionStream === "undefined") return { async push(b) {
1018
1021
  out.push(b.slice());
1019
1022
  }, async end() {
1020
- return zlibSync(join(), { level: 6 });
1023
+ return zlibSync(join(), { level: (
1024
+ /** @type {any} */
1025
+ level
1026
+ ) });
1027
+ }, abort() {
1028
+ } };
1029
+ if (level !== 6) return { async push(b) {
1030
+ out.push(b.slice());
1031
+ }, async end() {
1032
+ return zlibSync(join(), { level: (
1033
+ /** @type {any} */
1034
+ level
1035
+ ) });
1021
1036
  }, abort() {
1022
1037
  } };
1023
1038
  const cs = new CompressionStream("deflate"), w = cs.writable.getWriter();
@@ -3432,6 +3447,7 @@ var init_links = __esm({
3432
3447
 
3433
3448
  // src/images/png.js
3434
3449
  function crc32(b, from, to) {
3450
+ if (NODE_ZLIB2?.crc32) return NODE_ZLIB2.crc32(b.subarray(from, to));
3435
3451
  if (!CRC_TABLE) {
3436
3452
  CRC_TABLE = new Uint32Array(256);
3437
3453
  for (let n = 0; n < 256; n++) {
@@ -3572,16 +3588,24 @@ function unfilterRow(f, c, p, bpp) {
3572
3588
  }
3573
3589
  for (let i2 = 0; i2 < bpp; i2++) c[i2] += p[i2];
3574
3590
  for (let i2 = bpp; i2 < rb; i2++) {
3575
- const a = c[i2 - bpp], u = p[i2], cc = p[i2 - bpp], pp = a + u - cc, pa = Math.abs(pp - a), pb = Math.abs(pp - u), pc = Math.abs(pp - cc);
3576
- c[i2] += pa <= pb && pa <= pc ? a : pb <= pc ? u : cc;
3591
+ const a = c[i2 - bpp], u = p[i2], cc = p[i2 - bpp];
3592
+ let pa = u - cc, pb = a - cc, pc = pa + pb, k;
3593
+ k = pa >> 31;
3594
+ pa = (pa ^ k) - k;
3595
+ k = pb >> 31;
3596
+ pb = (pb ^ k) - k;
3597
+ k = pc >> 31;
3598
+ pc = (pc ^ k) - k;
3599
+ c[i2] += a ^ (a ^ (u ^ (u ^ cc) & pc - pb >> 31)) & (pb - pa | pc - pa) >> 31;
3577
3600
  }
3578
3601
  }
3579
3602
  function filterRow(x2, up, bpp, out, o, t) {
3580
3603
  const rb = x2.length, m = Math.min(bpp, rb);
3581
- let best = 0, sum = 0;
3604
+ let best = 0, sum = 0, s, k;
3582
3605
  for (let i2 = 0; i2 < rb; i2++) {
3583
- const v = x2[i2];
3584
- sum += v < 128 ? v : 256 - v;
3606
+ s = x2[i2] << 24 >> 24;
3607
+ k = s >> 31;
3608
+ sum += (s ^ k) - k;
3585
3609
  }
3586
3610
  out[o] = 0;
3587
3611
  out.set(x2, o + 1);
@@ -3590,12 +3614,15 @@ function filterRow(x2, up, bpp, out, o, t) {
3590
3614
  for (let i2 = 0; i2 < m; i2++) {
3591
3615
  const v = x2[i2];
3592
3616
  t[i2] = v;
3593
- sum += v < 128 ? v : 256 - v;
3617
+ s = v << 24 >> 24;
3618
+ k = s >> 31;
3619
+ sum += (s ^ k) - k;
3594
3620
  }
3595
3621
  for (let i2 = m; i2 < rb; i2++) {
3596
- const v = x2[i2] - x2[i2 - bpp] & 255;
3597
- t[i2] = v;
3598
- sum += v < 128 ? v : 256 - v;
3622
+ s = x2[i2] - x2[i2 - bpp] << 24 >> 24;
3623
+ t[i2] = s;
3624
+ k = s >> 31;
3625
+ sum += (s ^ k) - k;
3599
3626
  }
3600
3627
  if (sum < best) {
3601
3628
  best = sum;
@@ -3605,9 +3632,10 @@ function filterRow(x2, up, bpp, out, o, t) {
3605
3632
  if (!up) return;
3606
3633
  sum = 0;
3607
3634
  for (let i2 = 0; i2 < rb; i2++) {
3608
- const v = x2[i2] - up[i2] & 255;
3609
- t[i2] = v;
3610
- sum += v < 128 ? v : 256 - v;
3635
+ s = x2[i2] - up[i2] << 24 >> 24;
3636
+ t[i2] = s;
3637
+ k = s >> 31;
3638
+ sum += (s ^ k) - k;
3611
3639
  }
3612
3640
  if (sum < best) {
3613
3641
  best = sum;
@@ -3616,15 +3644,25 @@ function filterRow(x2, up, bpp, out, o, t) {
3616
3644
  }
3617
3645
  sum = 0;
3618
3646
  for (let i2 = 0; i2 < m; i2++) {
3619
- const v = x2[i2] - up[i2] & 255;
3620
- t[i2] = v;
3621
- sum += v < 128 ? v : 256 - v;
3647
+ s = x2[i2] - up[i2] << 24 >> 24;
3648
+ t[i2] = s;
3649
+ k = s >> 31;
3650
+ sum += (s ^ k) - k;
3622
3651
  }
3623
3652
  for (let i2 = m; i2 < rb; i2++) {
3624
- const a = x2[i2 - bpp], u = up[i2], c = up[i2 - bpp], pa = Math.abs(u - c), pb = Math.abs(a - c), pc = Math.abs(a + u - c - c);
3625
- const v = x2[i2] - (pa <= pb && pa <= pc ? a : pb <= pc ? u : c) & 255;
3626
- t[i2] = v;
3627
- sum += v < 128 ? v : 256 - v;
3653
+ const a = x2[i2 - bpp], u = up[i2], c = up[i2 - bpp];
3654
+ let pa = u - c, pb = a - c, pc = pa + pb;
3655
+ k = pa >> 31;
3656
+ pa = (pa ^ k) - k;
3657
+ k = pb >> 31;
3658
+ pb = (pb ^ k) - k;
3659
+ k = pc >> 31;
3660
+ pc = (pc ^ k) - k;
3661
+ const notA = (pb - pa | pc - pa) >> 31, uc = u ^ (u ^ c) & pc - pb >> 31;
3662
+ s = x2[i2] - (a ^ (a ^ uc) & notA) << 24 >> 24;
3663
+ t[i2] = s;
3664
+ k = s >> 31;
3665
+ sum += (s ^ k) - k;
3628
3666
  }
3629
3667
  if (sum < best) {
3630
3668
  out[o] = 4;
@@ -3857,13 +3895,14 @@ async function embedImage(doc, bytes, opt = {}) {
3857
3895
  /** @type {any} */
3858
3896
  opt[k2]
3859
3897
  ) : void 0;
3860
- const o = { colorSpace: own2("colorSpace"), maxDecodedBytes: own2("maxDecodedBytes") ?? MAX_DECODED };
3898
+ const o = { colorSpace: own2("colorSpace"), maxDecodedBytes: own2("maxDecodedBytes") ?? MAX_DECODED, compression: own2("compression") ?? "default" };
3899
+ if (!(o.compression === "default" || o.compression === "fast" || Number.isInteger(o.compression) && o.compression >= 1 && o.compression <= 9)) throw new TypeError("embedImage: compression is 'default', 'fast' or a zlib level 1–9");
3861
3900
  if (!(Number.isSafeInteger(o.maxDecodedBytes) && o.maxDecodedBytes > 0)) throw new TypeError("embedImage: maxDecodedBytes is a whole number of bytes > 0");
3862
3901
  if (b.length > MAX_INPUT) throw new RangeError(`embedImage: at most ${MAX_INPUT >> 20} MB`);
3863
3902
  let seen = SEEN.get(doc);
3864
3903
  if (!seen) SEEN.set(doc, seen = /* @__PURE__ */ new Map());
3865
3904
  const { key, copy } = await digest(b);
3866
- const k = `${key} ${o.colorSpace == null ? "-" : idOf(o.colorSpace)} ${o.maxDecodedBytes}`;
3905
+ const k = `${key} ${o.colorSpace == null ? "-" : idOf(o.colorSpace)} ${o.maxDecodedBytes} ${o.compression}`;
3867
3906
  const hit = seen.get(k);
3868
3907
  if (hit && (!hit.copy || same(hit.copy, b))) return hit.img;
3869
3908
  const img = embed(doc, b, o);
@@ -3921,7 +3960,8 @@ async function embed(doc, b, o) {
3921
3960
  let color, a = null;
3922
3961
  if (p.filtered) color = [p.data, parms(ch, p.bpc)];
3923
3962
  else {
3924
- const ce = encoder(h, rb, bpp, parms(ch, p.bpc)), ae = p.alpha ? encoder(h, arb, Math.max(1, abpc >> 3), parms(1, abpc)) : null;
3963
+ const lv = o.compression === "fast" ? 1 : o.compression === "default" ? w * h > 2e6 ? 3 : 6 : o.compression;
3964
+ const ce = encoder(h, rb, bpp, parms(ch, p.bpc), lv), ae = p.alpha ? encoder(h, arb, Math.max(1, abpc >> 3), parms(1, abpc), lv) : null;
3925
3965
  try {
3926
3966
  await p.rows(async (c, al) => {
3927
3967
  await Promise.all([ce.row(c), ae && al ? ae.row(al) : null]);
@@ -3957,8 +3997,8 @@ async function embed(doc, b, o) {
3957
3997
  { ref, w: turned ? h : w, h: turned ? w : h, orient: ORIENT[orientation], alpha }
3958
3998
  );
3959
3999
  }
3960
- function encoder(rows, rb, bpp, dp) {
3961
- const z = deflateStream(), per = Math.max(1, Math.floor((1 << 20) / (rb + 1))), t = new Uint8Array(rb);
4000
+ function encoder(rows, rb, bpp, dp, level) {
4001
+ const z = deflateStream(level), per = Math.max(1, Math.floor((1 << 20) / (rb + 1))), t = new Uint8Array(rb);
3962
4002
  const plain2 = rows * rb <= 256 << 10 ? new Uint8Array(rows * rb) : null;
3963
4003
  const strips = [new Uint8Array(per * (rb + 1)), new Uint8Array(per * (rb + 1))], busy = [Promise.resolve(), Promise.resolve()];
3964
4004
  let s = 0, strip = strips[0], k = 0, y = 0;
@@ -3982,7 +4022,7 @@ function encoder(rows, rb, bpp, dp) {
3982
4022
  await busy[s ^ 1];
3983
4023
  const f = await z.end();
3984
4024
  if (!plain2) return [f, dp];
3985
- const p = await deflate(plain2);
4025
+ const p = await deflate(plain2, level);
3986
4026
  return p.length <= f.length + dp.length ? [p, ""] : [f, dp];
3987
4027
  },
3988
4028
  abort: () => z.abort()
@@ -12364,13 +12404,13 @@ function createForm(doc, opt = {}) {
12364
12404
  if (!Array.isArray(list) || list.length > MAX_OPTIONS) throw new RangeError(`${who}: options is an array of at most ${MAX_OPTIONS} options`);
12365
12405
  const P = placeOf(o, who), L = lookOf(o, who, { border: 1 }), tip = tipOf(o, who);
12366
12406
  const { t, da } = textOf(P.page, o, who), q = qOf(o.align, who);
12367
- let chars2 = 0;
12407
+ let chars3 = 0;
12368
12408
  const index = /* @__PURE__ */ new Map();
12369
12409
  const opts = list.map((it, i2) => {
12370
12410
  const pair = Array.isArray(it) ? it : [it, it];
12371
12411
  if (pair.length !== 2) throw new TypeError(`${who}: an option is a string or [export, display]`);
12372
12412
  const [ex, dp] = pair.map((v) => str2(v, "an option", MAX_TOOLTIP, who));
12373
- if ((chars2 += ex.length + dp.length) > MAX_OPTION_CHARS) throw new RangeError(`${who}: the options have more than ${MAX_OPTION_CHARS} characters in all`);
12413
+ if ((chars3 += ex.length + dp.length) > MAX_OPTION_CHARS) throw new RangeError(`${who}: the options have more than ${MAX_OPTION_CHARS} characters in all`);
12374
12414
  if (index.has(ex)) throw new TypeError(`${who}: two options have the export value ${JSON.stringify(ex.slice(0, 80))}`);
12375
12415
  index.set(ex, i2);
12376
12416
  return { ex, dp };
@@ -13233,8 +13273,9 @@ function indexOf(buf, needle, from, to = buf.length) {
13233
13273
  to = Math.min(to, buf.length);
13234
13274
  if (to - from > 64) {
13235
13275
  if (NodeBuffer) {
13236
- const i2 = NodeBuffer.from(buf.buffer, buf.byteOffset, to).indexOf(needle, from, "latin1");
13237
- return i2;
13276
+ let v = VIEWS.get(buf);
13277
+ if (!v) VIEWS.set(buf, v = NodeBuffer.from(buf.buffer, buf.byteOffset, buf.length));
13278
+ return (to === v.length ? v : v.subarray(0, to)).indexOf(needle, from, "latin1");
13238
13279
  }
13239
13280
  for (let i2 = buf.indexOf(n0, from); i2 >= 0 && i2 + L <= to; i2 = buf.indexOf(n0, i2 + 1)) {
13240
13281
  let j = 1;
@@ -13258,7 +13299,7 @@ function lastIndexOf(buf, needle, to, span = to) {
13258
13299
  }
13259
13300
  return -1;
13260
13301
  }
13261
- var MAX_DEPTH4, MAX_ITEMS, Ref, PdfString, PdfStream, Op, CLASS, T, HEX, Lexer, parseValueEnd, NodeBuffer;
13302
+ var MAX_DEPTH4, MAX_ITEMS, Ref, PdfString, PdfStream, Op, CLASS, T, HEX, Lexer, parseValueEnd, NodeBuffer, VIEWS;
13262
13303
  var init_lexer = __esm({
13263
13304
  "src/reader/lexer.js"() {
13264
13305
  init_budget();
@@ -13429,7 +13470,7 @@ var init_lexer = __esm({
13429
13470
  }
13430
13471
  this.budget.tick(1 + (n >> 10));
13431
13472
  this.pos = i2;
13432
- this.val = new PdfString(out.slice(0, n), false);
13473
+ this.val = new PdfString(n === out.length ? out : out.slice(0, n), false);
13433
13474
  return T.STR;
13434
13475
  }
13435
13476
  /** @param {number} i after "<" */
@@ -13451,7 +13492,7 @@ var init_lexer = __esm({
13451
13492
  if (hi >= 0) out[n++] = hi << 4;
13452
13493
  this.budget.tick(1 + (n >> 10));
13453
13494
  this.pos = j < e ? j + 1 : j;
13454
- this.val = new PdfString(out.slice(0, n), true);
13495
+ this.val = new PdfString(n === out.length ? out : out.slice(0, n), true);
13455
13496
  return T.STR;
13456
13497
  }
13457
13498
  /** @param {number} i after "/" */
@@ -13483,6 +13524,8 @@ var init_lexer = __esm({
13483
13524
  parseValueEnd = () => void 0;
13484
13525
  NodeBuffer = /** @type {any} */
13485
13526
  globalThis.Buffer;
13527
+ VIEWS = /** @type {WeakMap<Uint8Array, any>} */
13528
+ /* @__PURE__ */ new WeakMap();
13486
13529
  }
13487
13530
  });
13488
13531
 
@@ -14644,14 +14687,26 @@ function decodeText(v) {
14644
14687
  if (!(v instanceof PdfString)) return typeof v === "string" ? v : v == null ? "" : String(v);
14645
14688
  const b = v.bytes;
14646
14689
  if (b.length >= 2 && b[0] === 254 && b[1] === 255) {
14647
- let s2 = "";
14648
- for (let i2 = 2; i2 + 1 < b.length; i2 += 2) s2 += String.fromCharCode(b[i2] << 8 | b[i2 + 1]);
14649
- return s2;
14690
+ const u2 = new Uint16Array(b.length - 2 >> 1);
14691
+ for (let i3 = 0; i3 < u2.length; i3++) u2[i3] = b[2 + 2 * i3] << 8 | b[3 + 2 * i3];
14692
+ return chars2(u2);
14650
14693
  }
14651
14694
  if (b.length >= 3 && b[0] === 239 && b[1] === 187 && b[2] === 191) return new TextDecoder().decode(b.subarray(3));
14652
- let s = "";
14653
- for (const x2 of b) s += String.fromCharCode(DOC.get(x2) ?? x2);
14654
- return s;
14695
+ let i2 = 0;
14696
+ while (i2 < b.length && DOC_T[b[i2]] === b[i2]) i2++;
14697
+ if (i2 === b.length) return latin12(b);
14698
+ const u = new Uint16Array(b.length);
14699
+ for (i2 = 0; i2 < b.length; i2++) u[i2] = DOC_T[b[i2]];
14700
+ return chars2(u);
14701
+ }
14702
+ function chars2(u) {
14703
+ const parts = [];
14704
+ for (let i2 = 0; i2 < u.length; i2 += 8192) parts.push(String.fromCharCode.apply(
14705
+ null,
14706
+ /** @type {any} */
14707
+ u.subarray(i2, i2 + 8192)
14708
+ ));
14709
+ return parts.join("");
14655
14710
  }
14656
14711
  function glyphText(n) {
14657
14712
  if (Object.hasOwn(NAMES2, n)) return NAMES2[n];
@@ -14689,11 +14744,11 @@ function parseCMap(data, budget, held = { n: 0 }) {
14689
14744
  for (let i2 = 0; i2 + 1 < b.length; i2 += 2) u.push(b[i2] << 8 | b[i2 + 1]);
14690
14745
  return String.fromCharCode(...u);
14691
14746
  };
14692
- let chars2 = 0;
14747
+ let chars3 = 0;
14693
14748
  const set2 = (c, t) => {
14694
- chars2 += t.length;
14749
+ chars3 += t.length;
14695
14750
  held.n += 8 + t.length;
14696
- if (chars2 > MAX_CMAP_CHARS) fail("E_BUDGET", `A CMap maps codes to more than ${MAX_CMAP_CHARS.toLocaleString("en")} characters`);
14751
+ if (chars3 > MAX_CMAP_CHARS) fail("E_BUDGET", `A CMap maps codes to more than ${MAX_CMAP_CHARS.toLocaleString("en")} characters`);
14697
14752
  if (held.n > MAX_HELD) fail("E_BUDGET", `A page's fonts hold more than ${MAX_HELD.toLocaleString("en")} units of code mappings (an entry 8, a character 1)`);
14698
14753
  map.set(c, t);
14699
14754
  };
@@ -15012,12 +15067,30 @@ function pageText(src, pageDict, resources, raw = false) {
15012
15067
  const PX = [], PY = [], PE = [], PS = [], PG = [];
15013
15068
  const PA = [], PAE = [], PC = [], PD = [];
15014
15069
  const PT = [];
15015
- let chars2 = 0;
15070
+ let chars3 = 0;
15016
15071
  let moved = true;
15017
15072
  const fonts = /* @__PURE__ */ new Map();
15018
- const add2 = (x2, y, end, size, text3, d0, d1, d2, d3) => {
15073
+ let run2 = [];
15074
+ let runs = false;
15075
+ const flush = () => {
15076
+ if (run2.length) {
15077
+ PT[PT.length - 1] += run2.join("");
15078
+ run2 = [];
15079
+ }
15080
+ };
15081
+ const add2 = (x2, y, end, size, text3, d0, d1, d2, d3, glyph = false) => {
15082
+ const n = PT.length - 1;
15083
+ if (glyph && runs && !moved && !(PG[n] & 1) && PD[n] === d3 && PC[n] === d2 && PS[n] === size && d0 >= PA[n] && d0 >= PAE[n] - 0.01 && d0 - PAE[n] <= Math.max(size, 1) * 0.25 && !RTL2.test(text3)) {
15084
+ if ((chars3 += text3.length) > MAX_TEXT_CHARS) throw new PdfReadError("E_BUDGET", `A page makes more than ${MAX_TEXT_CHARS.toLocaleString("en")} characters of text`);
15085
+ run2.push(text3);
15086
+ if (run2.length >= 4096) flush();
15087
+ PAE[n] = Math.max(PAE[n], d1);
15088
+ return;
15089
+ }
15090
+ flush();
15091
+ runs = glyph && !raw && !RTL2.test(text3);
15019
15092
  if (PT.length >= budget.maxTextItems) throw new PdfReadError("E_BUDGET", `A page has more than ${budget.maxTextItems.toLocaleString("en")} pieces of text (budget.maxTextItems)`);
15020
- if ((chars2 += text3.length) > MAX_TEXT_CHARS) throw new PdfReadError("E_BUDGET", `A page makes more than ${MAX_TEXT_CHARS.toLocaleString("en")} characters of text`);
15093
+ if ((chars3 += text3.length) > MAX_TEXT_CHARS) throw new PdfReadError("E_BUDGET", `A page makes more than ${MAX_TEXT_CHARS.toLocaleString("en")} characters of text`);
15021
15094
  if (raw) {
15022
15095
  PX.push(x2);
15023
15096
  PY.push(y);
@@ -15081,7 +15154,7 @@ function pageText(src, pageDict, resources, raw = false) {
15081
15154
  }
15082
15155
  const n = PT.length - 1;
15083
15156
  if (text3.includes("⁠")) text3 = text3.replaceAll("⁠", "");
15084
- if (text3) add2(x2, y, end, size, text3, d0, d1, d2, d3);
15157
+ if (text3) add2(x2, y, end, size, text3, d0, d1, d2, d3, true);
15085
15158
  else if (n >= 0 && PAE[n] < d1 && PD[n] === d3 && Math.abs(PC[n] - d2) < 0.01) {
15086
15159
  if (raw) PE[n] = end;
15087
15160
  PAE[n] = d1;
@@ -15247,6 +15320,7 @@ function pageText(src, pageDict, resources, raw = false) {
15247
15320
  default:
15248
15321
  }
15249
15322
  }
15323
+ flush();
15250
15324
  if (raw) return PT.map((text3, i2) => ({ x: PX[i2], y: PY[i2], end: PE[i2], size: PS[i2], text: text3, gap: !!(PG[i2] & 1) }));
15251
15325
  const out = [];
15252
15326
  for (let i2 = 0; i2 < PT.length; ) {
@@ -15273,7 +15347,7 @@ function pageText(src, pageDict, resources, raw = false) {
15273
15347
  }
15274
15348
  return out.join("");
15275
15349
  }
15276
- var MAX_FORMS, MAX_CMAP, MAX_MARKS, MAX_ACTUAL, MAX_TEXT_CHARS, MAX_DST, MAX_CMAP_CHARS, MAX_HELD, MAX_W, MAX_DO, TEXT_TIMEOUT_MS, DOC, WIN, WIN_T, MAC_HIGH, BASE, STD_HIGH, NAMES2, ACCENT, own, STD_ALIAS, NONE, stops, MAX_CMAP_FONTS, mul6, mul2, ID, matrixOf, C0, G0, G1, RTL2, STRONG_L, NUM, rev2;
15350
+ var MAX_FORMS, MAX_CMAP, MAX_MARKS, MAX_ACTUAL, MAX_TEXT_CHARS, MAX_DST, MAX_CMAP_CHARS, MAX_HELD, MAX_W, MAX_DO, TEXT_TIMEOUT_MS, DOC, DOC_T, WIN, WIN_T, MAC_HIGH, BASE, STD_HIGH, NAMES2, ACCENT, own, STD_ALIAS, NONE, stops, MAX_CMAP_FONTS, mul6, mul2, ID, matrixOf, C0, G0, G1, RTL2, STRONG_L, NUM, rev2;
15277
15351
  var init_text2 = __esm({
15278
15352
  "src/reader/text.js"() {
15279
15353
  init_lexer();
@@ -15333,6 +15407,7 @@ var init_text2 = __esm({
15333
15407
  [158, 382],
15334
15408
  [160, 8364]
15335
15409
  ]);
15410
+ DOC_T = new Uint16Array(256).map((_, c) => DOC.get(c) ?? c);
15336
15411
  WIN = /* @__PURE__ */ new Map();
15337
15412
  {
15338
15413
  const p = ENCODINGS.WinAnsi;
@@ -15732,9 +15807,15 @@ function numText(v) {
15732
15807
  return Math.abs(v) < 1 ? v.toFixed(10).replace(/\.?0+$/, "") || "0" : v.toFixed(0);
15733
15808
  }
15734
15809
  function hexText(b) {
15735
- let s = "<";
15736
- for (const x2 of b) s += HEXD[x2 >> 4] + HEXD[x2 & 15];
15737
- return s + ">";
15810
+ if (NodeBuffer2 && b.length > 64) return `<${NodeBuffer2.from(b.buffer, b.byteOffset, b.length).toString("hex")}>`;
15811
+ const parts = ["<"];
15812
+ for (let i2 = 0; i2 < b.length; i2 += 4096) {
15813
+ let s = "";
15814
+ for (let k = i2; k < Math.min(b.length, i2 + 4096); k++) s += HEXD[b[k] >> 4] + HEXD[b[k] & 15];
15815
+ parts.push(s);
15816
+ }
15817
+ parts.push(">");
15818
+ return parts.join("");
15738
15819
  }
15739
15820
  function createCopier(t) {
15740
15821
  const maps = /* @__PURE__ */ new Map();
@@ -16136,7 +16217,7 @@ function createCopier(t) {
16136
16217
  }
16137
16218
  };
16138
16219
  }
16139
- var SHARE, GRAY, HEXD, Raw, Foreign, AUDIT, OPEN_A, CLOSE_A, OPEN_D, CLOSE_D;
16220
+ var SHARE, GRAY, HEXD, NodeBuffer2, Raw, Foreign, AUDIT, OPEN_A, CLOSE_A, OPEN_D, CLOSE_D;
16140
16221
  var init_copy = __esm({
16141
16222
  "src/reader/copy.js"() {
16142
16223
  init_lexer();
@@ -16147,6 +16228,8 @@ var init_copy = __esm({
16147
16228
  SHARE = /* @__PURE__ */ new Set(["Font", "FontDescriptor", "ExtGState", "Encoding", "Pattern", "Shading", "Halftone", "Function", "XObject", "Mask", "CMap", "OutputIntent", "Filespec", "EmbeddedFile"]);
16148
16229
  GRAY = -2;
16149
16230
  HEXD = "0123456789abcdef";
16231
+ NodeBuffer2 = /** @type {any} */
16232
+ globalThis.Buffer;
16150
16233
  Raw = class {
16151
16234
  /** @param {string} s */
16152
16235
  constructor(s) {
@@ -16438,7 +16521,8 @@ async function run(self, sink, o) {
16438
16521
  if (level && o.encrypt) throw new TypeError("save: PDF/A forbids encryption: pass pdfa: false to drop the PDF/A claim and encrypt");
16439
16522
  const keepStruct = single && o.keepStructure !== false && !self.edits.flatten;
16440
16523
  const info = self.info();
16441
- const edited = !!self.edits.info || level?.conformance !== self.pdfa()?.conformance;
16524
+ const tooBig = ["title", "author", "subject", "keywords"].some((k) => typeof info[k] === "string" && info[k].length > INFO_MAX);
16525
+ const edited = !!self.edits.info || tooBig || level?.conformance !== self.pdfa()?.conformance;
16442
16526
  const uaLost = !keepStruct && /pdfuaid:part/.test(self.xmp() ?? "");
16443
16527
  const srcXmp = self.edits.xmp ?? (edited || uaLost || !level && self.pdfa() ? null : self.xmp());
16444
16528
  const created = o.creationDate ?? (info.creationDate instanceof Date ? info.creationDate : /* @__PURE__ */ new Date());
@@ -16451,6 +16535,13 @@ async function run(self, sink, o) {
16451
16535
  warn("the source's output intent profile was not usable; the bundled sRGB profile was written instead");
16452
16536
  }
16453
16537
  }
16538
+ const infoText = (k) => {
16539
+ const v = info[k];
16540
+ if (v == null) return void 0;
16541
+ if (v.length <= INFO_MAX) return v;
16542
+ warn(`the source's ${k} (${v.length.toLocaleString("en")} characters) was cut to ${INFO_MAX.toLocaleString("en")}`);
16543
+ return v.slice(0, INFO_MAX);
16544
+ };
16454
16545
  const pdf = (
16455
16546
  /** @type {any} */
16456
16547
  createPdf(sink, {
@@ -16461,10 +16552,10 @@ async function run(self, sink, o) {
16461
16552
  creationDate: created,
16462
16553
  metadata: !!(level || self.edits.xmp != null || self.xmp() != null),
16463
16554
  // an XMP packet only when the source had one or PDF/A needs one: a file split into pages is not a kilobyte of XMP each
16464
- title: info.title ?? void 0,
16465
- author: info.author ?? void 0,
16466
- subject: info.subject ?? void 0,
16467
- keywords: info.keywords ?? void 0,
16555
+ title: infoText("title"),
16556
+ author: infoText("author"),
16557
+ subject: infoText("subject"),
16558
+ keywords: infoText("keywords"),
16468
16559
  ...level ? { pdfa: { part: level.part, conformance: "b", ...intentOpt ? { outputIntent: intentOpt } : {} } } : {}
16469
16560
  })
16470
16561
  );
@@ -16775,7 +16866,7 @@ function sameBytes(a, b) {
16775
16866
  for (let i2 = 0; i2 < a.length; i2++) if (a[i2] !== b[i2]) return false;
16776
16867
  return true;
16777
16868
  }
16778
- var MAX_OPEN, KEYS2, OWN;
16869
+ var INFO_MAX, MAX_OPEN, KEYS2, OWN;
16779
16870
  var init_save = __esm({
16780
16871
  "src/reader/save.js"() {
16781
16872
  init_index();
@@ -16789,6 +16880,7 @@ var init_save = __esm({
16789
16880
  init_pdf();
16790
16881
  init_text2();
16791
16882
  init_budget();
16883
+ INFO_MAX = 65536;
16792
16884
  MAX_OPEN = 256;
16793
16885
  KEYS2 = ["compress", "keepActiveContent", "encrypt", "pdfa", "creationDate", "signal", "timeoutMs", "keepStructure"];
16794
16886
  OWN = /* @__PURE__ */ new Set(["Type", "Parent", "Contents", "Resources", "Annots", "MediaBox", "CropBox", "Rotate", "B"]);
@@ -16866,17 +16958,19 @@ async function saveIncremental(self, o = {}) {
16866
16958
  keepStruct: () => true
16867
16959
  });
16868
16960
  const override = async (n, v) => put(n, await copier.text(self, v), null, v instanceof Map ? v : null);
16869
- let present;
16870
- const objects = [];
16871
- try {
16872
- present = findActive(self, (n, v) => {
16961
+ let scan = SCANS.get(src);
16962
+ if (!scan) try {
16963
+ const objects2 = [];
16964
+ const present2 = findActive(self, (n, v) => {
16873
16965
  const d = v instanceof PdfStream ? v.dict : v;
16874
- if (d instanceof Map && (d.has("ByteRange") || d.has("Perms") || d.has("Lock") || R(d.get("FT")) === "Sig" || d.has("V") || nestedSigs(d).length)) objects.push([n, v]);
16966
+ if (d instanceof Map && (d.has("ByteRange") || d.has("Perms") || d.has("Lock") || R(d.get("FT")) === "Sig" || d.has("V") || nestedSigs(d).length)) objects2.push([n, v]);
16875
16967
  });
16968
+ SCANS.set(src, scan = { present: present2, objects: objects2 });
16876
16969
  } catch (e) {
16877
16970
  throw new PdfReadError("E_ACTIVE", `${who}: the file could not be checked for active content (${/** @type {Error} */
16878
16971
  e.message}); use save() to rewrite the file without it (this invalidates existing signatures)`);
16879
16972
  }
16973
+ const { present, objects } = scan;
16880
16974
  checkSigned(self, sig, o, objects, who);
16881
16975
  if (present.length && !o.keepActiveContent) throw new PdfReadError("E_ACTIVE", `${who}: the file has active content (${present.map((p) => `${p.what} ×${p.count}`).join(", ")}), which an incremental update cannot remove (the original bytes stay). Use save() to rewrite the file without it (this invalidates existing signatures), or pass keepActiveContent: true to keep it knowingly`);
16882
16976
  const fills = await fillReplacements(self, { pdfa: !!self.pdfa(), warn: (m) => warnings.push(m) });
@@ -17328,7 +17422,7 @@ function checkSigned(self, sig, o, objects, who) {
17328
17422
  }
17329
17423
  }
17330
17424
  }
17331
- var SIGN_KEYS, MAX_WIDGET_SHARE, TRANSFORMS, isSigDict;
17425
+ var SIGN_KEYS, SCANS, MAX_WIDGET_SHARE, TRANSFORMS, isSigDict;
17332
17426
  var init_incremental = __esm({
17333
17427
  "src/reader/incremental.js"() {
17334
17428
  init_lexer();
@@ -17344,6 +17438,7 @@ var init_incremental = __esm({
17344
17438
  init_serialize();
17345
17439
  init_serialize();
17346
17440
  SIGN_KEYS = ["field", "signer", "reason", "location", "contactInfo", "name", "date", "timestamp", "subFilter", "reserve", "hash"];
17441
+ SCANS = /* @__PURE__ */ new WeakMap();
17347
17442
  MAX_WIDGET_SHARE = 0.4;
17348
17443
  TRANSFORMS = /* @__PURE__ */ new Set(["DocMDP", "FieldMDP", "UR", "UR3"]);
17349
17444
  isSigDict = (d) => d instanceof Map && d.has("ByteRange") && (d.has("Contents") || d.has("Type") || d.has("Filter"));
package/index.d.ts CHANGED
@@ -650,7 +650,7 @@ export interface Pdf {
650
650
  * textMapping: 'carrier' (default: Firefox, pdf.js, search) or 'actualText' (a shaped cluster's text only in its ActualText: MuPDF, AI pipelines) */
651
651
  embedFont(bytes: Uint8Array | ArrayBuffer, o?: { subset?: boolean | Subsetter; shaper?: Shaper; textMapping?: 'carrier' | 'actualText' }): Promise<Font>;
652
652
  /** a JPEG or PNG, written now as an image XObject. colorSpace: an ICC colour space with the image's component count */
653
- embedImage(bytes: Uint8Array | ArrayBuffer, o?: { colorSpace?: IccColorSpace; /** the decoded size a PNG with alpha or interlacing may have; default 64 MB */ maxDecodedBytes?: number }): Promise<Image>;
653
+ embedImage(bytes: Uint8Array | ArrayBuffer, o?: { colorSpace?: IccColorSpace; /** the decoded size a PNG with alpha or interlacing may have; default 64 MB */ maxDecodedBytes?: number; /** zlib level for a re-compressed PNG (alpha, interlacing): 'default' = 6, or 3 above 2 megapixels; 'fast' = 1; or 1–9 */ compression?: 'default' | 'fast' | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 }): Promise<Image>;
654
654
  /** a spot colour: its name and the device colour (gray, RGB, CMYK) shown for it at full tint. One definition per name */
655
655
  spotColor(name: string, alternate: number | string | readonly number[]): SpotColor;
656
656
  /** an ICC-based colour space from a gray, RGB or CMYK profile (its header is checked) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reportwright/pdf",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.5",
4
4
  "description": "A streaming PDF writer for Node and browsers with no runtime dependencies: flat memory, PDF/A-1b to 3a, PDF/UA-1, Factur-X, embedded TrueType with Unicode text.",
5
5
  "type": "module",
6
6
  "license": "MIT",