reamkit 1.25.1 → 1.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +12 -5
  2. package/dist/esm/pdf-reader/annot-draw.d.ts +65 -0
  3. package/dist/esm/pdf-reader/annot-draw.js +374 -0
  4. package/dist/esm/pdf-reader/annots.d.ts +3 -1
  5. package/dist/esm/pdf-reader/annots.js +18 -4
  6. package/dist/esm/pdf-reader/ccitt.d.ts +18 -0
  7. package/dist/esm/pdf-reader/ccitt.js +70 -2
  8. package/dist/esm/pdf-reader/cie-color.d.ts +33 -0
  9. package/dist/esm/pdf-reader/cie-color.js +112 -0
  10. package/dist/esm/pdf-reader/content.d.ts +47 -17
  11. package/dist/esm/pdf-reader/content.js +175 -14
  12. package/dist/esm/pdf-reader/display.d.ts +1 -1
  13. package/dist/esm/pdf-reader/display.js +21 -6
  14. package/dist/esm/pdf-reader/document.d.ts +6 -0
  15. package/dist/esm/pdf-reader/document.js +29 -1
  16. package/dist/esm/pdf-reader/embedded-fonts.d.ts +12 -3
  17. package/dist/esm/pdf-reader/embedded-fonts.js +22 -3
  18. package/dist/esm/pdf-reader/flow-build.d.ts +12 -4
  19. package/dist/esm/pdf-reader/flow-build.js +102 -8
  20. package/dist/esm/pdf-reader/font.js +97 -16
  21. package/dist/esm/pdf-reader/function.d.ts +16 -0
  22. package/dist/esm/pdf-reader/function.js +414 -0
  23. package/dist/esm/pdf-reader/glyph-names.d.ts +6 -0
  24. package/dist/esm/pdf-reader/glyph-names.js +408 -0
  25. package/dist/esm/pdf-reader/image-decode.d.ts +8 -4
  26. package/dist/esm/pdf-reader/image-decode.js +224 -12
  27. package/dist/esm/pdf-reader/images.d.ts +12 -0
  28. package/dist/esm/pdf-reader/images.js +109 -9
  29. package/dist/esm/pdf-reader/jbig2.d.ts +109 -0
  30. package/dist/esm/pdf-reader/jbig2.js +2606 -0
  31. package/dist/esm/pdf-reader/layout.d.ts +32 -0
  32. package/dist/esm/pdf-reader/layout.js +189 -40
  33. package/dist/esm/pdf-reader/lexer.d.ts +2 -0
  34. package/dist/esm/pdf-reader/lexer.js +4 -0
  35. package/dist/esm/pdf-reader/optional-content.d.ts +36 -0
  36. package/dist/esm/pdf-reader/optional-content.js +93 -0
  37. package/dist/esm/pdf-reader/reader.d.ts +5 -2
  38. package/dist/esm/pdf-reader/reader.js +80 -7
  39. package/dist/esm/pdf-reader/shading.d.ts +71 -6
  40. package/dist/esm/pdf-reader/shading.js +185 -15
  41. package/dist/esm/pdf-reader/standard-metrics.d.ts +8 -0
  42. package/dist/esm/pdf-reader/standard-metrics.js +18 -0
  43. package/dist/esm/pdf-reader/standard-widths.d.ts +20 -0
  44. package/dist/esm/pdf-reader/standard-widths.js +62 -0
  45. package/dist/esm/pdf-reader/tagged.js +204 -32
  46. package/dist/esm/pdf-reader/text-rules.d.ts +16 -0
  47. package/dist/esm/pdf-reader/text-rules.js +112 -0
  48. package/dist/esm/pdf-reader/text.js +78 -4
  49. package/dist/esm/pdf-reader/vector.d.ts +5 -0
  50. package/dist/esm/pdf-reader/vector.js +39 -25
  51. package/dist/esm/word/docx-writer.js +213 -23
  52. package/package.json +1 -1
@@ -43,10 +43,6 @@ var RASTER_MEDIA = {
43
43
  ext: "jpeg",
44
44
  contentType: "image/jpeg"
45
45
  },
46
- jpeg2000: {
47
- ext: "jp2",
48
- contentType: "image/jp2"
49
- },
50
46
  gif: {
51
47
  ext: "gif",
52
48
  contentType: "image/gif"
@@ -56,6 +52,18 @@ var RASTER_MEDIA = {
56
52
  contentType: "image/bmp"
57
53
  }
58
54
  };
55
+ /**
56
+ * Why an image was not written, in terms of the image — for the loss report,
57
+ * which used to say "image bytes missing" whatever the reason, including for
58
+ * bytes that were all there and in a format the format does not admit.
59
+ */
60
+ function imageRefusal(resource, state) {
61
+ const bytes = resource === void 0 ? void 0 : state.resources.get(resource);
62
+ if (!bytes) return "image bytes missing";
63
+ const format = detectImageFormat(bytes);
64
+ if (format === "jpeg2000") return "JPEG 2000 picture dropped — §15.2.14 admits no such image part, and no reader of a .docx shows one";
65
+ return `picture dropped — ${format ?? "an unrecognised"} is not an image part a .docx may carry`;
66
+ }
59
67
  function mediaInfo(bytes) {
60
68
  const raster = detectImageFormat(bytes);
61
69
  if (raster) return RASTER_MEDIA[raster];
@@ -137,9 +145,7 @@ function writeDocx(flow) {
137
145
  if (i === sections.length - 1) finalSectPr = sp;
138
146
  else sectPrByClosingIndex.set(sec.endIndex - 1, sp);
139
147
  });
140
- flow.body.forEach((el, idx) => {
141
- emitBlock(body, el, losses, state, docScope, sectPrByClosingIndex.get(idx));
142
- });
148
+ emitBody(body, flow.body, losses, state, docScope, sectPrByClosingIndex);
143
149
  if (finalSectPr) body.push(finalSectPr);
144
150
  const documentXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><w:body>${body.join("")}</w:body></w:document>`;
145
151
  const numberingPart = flow.body.some((el) => el.kind === "paragraph" && el.paragraph.properties.numbering !== void 0) && flow.numbering ? {
@@ -365,6 +371,59 @@ var docxWriter = {
365
371
  supports: new Set([FEATURES.text]),
366
372
  write: (doc) => writeDocx(doc)
367
373
  };
374
+ /**
375
+ * The document body, with consecutive floating drawings gathered into one
376
+ * carrier paragraph.
377
+ *
378
+ * §20.4.2.3 — a floating drawing is placed by its anchor and not by where its
379
+ * paragraph lands, so the paragraph is only somewhere for the anchor to hang
380
+ * from. Given one paragraph EACH, a page of placed artwork is that many
381
+ * paragraphs of flow: 160F-2019.pdf's one-page form is 355 drawings, and Word
382
+ * ran it to sixteen pages of empty lines with the artwork anchored to them.
383
+ * Gathered, the page costs one paragraph however much is placed on it.
384
+ *
385
+ * A block that closes a section keeps its own paragraph — the section break
386
+ * rides that paragraph's properties, and it must be the last one in the
387
+ * section.
388
+ */
389
+ function emitBody(out, blocks, losses, state, scope, sectPrByClosingIndex) {
390
+ let carried = [];
391
+ const flush = () => {
392
+ if (carried.length > 0) out.push(`<w:p>${carried.join("")}</w:p>`);
393
+ carried = [];
394
+ };
395
+ blocks.forEach((el, idx) => {
396
+ const closing = sectPrByClosingIndex.get(idx);
397
+ const anchored = closing === void 0 ? floatingDrawingRun(el, losses, state, scope) : void 0;
398
+ if (anchored !== void 0) {
399
+ carried.push(anchored);
400
+ return;
401
+ }
402
+ flush();
403
+ emitBlock(out, el, losses, state, scope, closing);
404
+ });
405
+ flush();
406
+ }
407
+ /**
408
+ * The run holding one block's drawing, when that block states where on the page
409
+ * it goes — `undefined` for everything that flows, which the ordinary block
410
+ * emitter handles.
411
+ */
412
+ function floatingDrawingRun(el, losses, state, scope) {
413
+ if (el.kind === "image" && el.image.float) {
414
+ const drawing = drawingXml(el.image.resource, el.image.width, el.image.height, el.image.altText, state, scope, el.image.float);
415
+ if (drawing === "") {
416
+ losses.push({
417
+ severity: "dropped",
418
+ feature: FEATURES.images,
419
+ detail: imageRefusal(el.image.resource, state)
420
+ });
421
+ return "";
422
+ }
423
+ return `<w:r>${drawing}</w:r>`;
424
+ }
425
+ if (el.kind === "shape" && el.shape.float) return `<w:r>${shapeDrawingXml(el.shape, losses, state, scope)}</w:r>`;
426
+ }
368
427
  function emitBlock(out, el, losses, state, scope, closingSectPr) {
369
428
  if (el.kind === "paragraph") {
370
429
  out.push(paragraphXml(el.paragraph, state, scope, closingSectPr));
@@ -376,13 +435,13 @@ function emitBlock(out, el, losses, state, scope, closingSectPr) {
376
435
  return;
377
436
  }
378
437
  if (el.kind === "image") {
379
- const drawing = drawingXml(el.image.resource, el.image.width, el.image.height, el.image.altText, state, scope);
438
+ const drawing = drawingXml(el.image.resource, el.image.width, el.image.height, el.image.altText, state, scope, el.image.float);
380
439
  if (drawing) out.push(`<w:p>${pPrWithSect(el.image.paragraphProperties, closingSectPr)}<w:r>${drawing}</w:r></w:p>`);
381
440
  else {
382
441
  losses.push({
383
442
  severity: "dropped",
384
443
  feature: FEATURES.images,
385
- detail: "image bytes missing"
444
+ detail: imageRefusal(el.image.resource, state)
386
445
  });
387
446
  if (closingSectPr) out.push(`<w:p><w:pPr>${closingSectPr}</w:pPr></w:p>`);
388
447
  }
@@ -425,14 +484,82 @@ function chartBlockXml(chart, state, scope, losses) {
425
484
  const id = ++state.drawingSeq;
426
485
  return `<w:drawing><wp:inline xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"><wp:extent cx="${cx}" cy="${cy}"/><wp:docPr id="${id}" name="Chart ${id}"${chart.altText ? ` descr="${escapeAttr(chart.altText)}"` : ""}/><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:id="${relId}"/></a:graphicData></a:graphic></wp:inline></w:drawing>`;
427
486
  }
428
- function drawingXml(resource, widthPt, heightPt, altText, state, scope) {
487
+ /**
488
+ * §20.4.2.3 — the frame a drawing is placed by: `wp:anchor` where the block
489
+ * states where on the page it belongs, `wp:inline` where it belongs in the run
490
+ * of text.
491
+ *
492
+ * Everything used to be inline. The document model has carried the anchor since
493
+ * the reader learned to read one, and the PDF reconstruction fills it in for
494
+ * every line and rule it lifts off a page — and all of it was thrown away here,
495
+ * so a converted page came out as a column of drawings stacked in document
496
+ * order. S2.pdf's two colour wheels marched down the left margin as eighteen
497
+ * separate wedges; 160F-2019.pdf's one-page form ran to sixteen pages, because
498
+ * each of its 355 drawings took a paragraph of its own in the flow.
499
+ *
500
+ * The attribute list is not decoration: CT_Anchor requires `simplePos`,
501
+ * `relativeHeight`, `behindDoc`, `locked`, `layoutInCell` and `allowOverlap`,
502
+ * and the children are a SEQUENCE — position, extent, wrap, docPr, graphic —
503
+ * that Word refuses out of order.
504
+ *
505
+ * @param float The placement, or `undefined` for a drawing that flows.
506
+ * @param cx Width in EMU.
507
+ * @param cy Height in EMU.
508
+ * @param head The `wp:docPr` (and anything else) that precedes the graphic.
509
+ * @param graphic The `a:graphic` element the frame carries.
510
+ */
511
+ function drawingFrame(float, cx, cy, head, graphic, state, rotation60k) {
512
+ const WP_NS = " xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\"";
513
+ if (!float) return `<w:drawing><wp:inline${WP_NS}><wp:extent cx="${cx}" cy="${cy}"/>${head}${graphic}</wp:inline></w:drawing>`;
514
+ const emu = (pt) => Number.isFinite(pt) ? Math.round((pt ?? 0) * EMU_PER_PT) : 0;
515
+ const d = float.wrapDist;
516
+ const dist = ` distT="${String(emu(d?.topPt))}" distB="${String(emu(d?.bottomPt))}" distL="${String(emu(d?.leftPt))}" distR="${String(emu(d?.rightPt))}"`;
517
+ const z = float.zOrder ?? ++state.drawingSeq;
518
+ const attrs = `${dist} simplePos="0" relativeHeight="${String(z)}" behindDoc="${float.behind ? "1" : "0"}" locked="0" layoutInCell="${float.inCell === false ? "0" : "1"}" allowOverlap="1"`;
519
+ const pos = (tag, p, fallback) => {
520
+ const from = p?.relativeFrom ?? fallback;
521
+ const body = p?.align !== void 0 ? `<wp:align>${p.align}</wp:align>` : `<wp:posOffset>${String(emu(p?.offsetPt))}</wp:posOffset>`;
522
+ return `<${tag} relativeFrom="${escapeAttr(from)}">${body}</${tag}>`;
523
+ };
524
+ const side = ` wrapText="${float.wrapSide ?? "bothSides"}"`;
525
+ const wrap = float.wrap === "square" ? `<wp:wrapSquare${side}/>` : float.wrap === "tight" ? `<wp:wrapTight${side}/>` : float.wrap === "through" ? `<wp:wrapThrough${side}/>` : float.wrap === "topAndBottom" || float.wrap === "notBeside" ? "<wp:wrapTopAndBottom/>" : "<wp:wrapNone/>";
526
+ return `<w:drawing><wp:anchor${WP_NS}${attrs}><wp:simplePos x="0" y="0"/>` + pos("wp:positionH", float.posH, "column") + pos("wp:positionV", float.posV, "paragraph") + `<wp:extent cx="${cx}" cy="${cy}"/>` + effectExtentXml(cx, cy, rotation60k) + wrap + head + graphic + "</wp:anchor></w:drawing>";
527
+ }
528
+ /**
529
+ * §20.4.2.3 `wp:effectExtent` — how far the drawing reaches PAST the extent.
530
+ *
531
+ * `wp:extent` is the shape's own size, unrotated: fdo75722-dml.docx states
532
+ * 1964690 × 1240790 there and repeats it in `a:ext`, then carries the turn's
533
+ * overhang here (l 198120, t 433705, r 199390, b 430530). Written as zeroes —
534
+ * which they always were — a renderer reserves the flat box for a shape that
535
+ * draws across the turned one, and 160F-2019.pdf's "Nature", set on its side
536
+ * down the middle of a column, came back lying flat across it.
537
+ *
538
+ * A box `w × h` turned by θ about its centre spans `|w·cosθ| + |h·sinθ|`
539
+ * across and `|w·sinθ| + |h·cosθ|` down; half of each growth hangs off each
540
+ * side. A turn that makes the box no wider hangs off by nothing.
541
+ */
542
+ function effectExtentXml(cx, cy, rotation60k) {
543
+ const none = "<wp:effectExtent l=\"0\" t=\"0\" r=\"0\" b=\"0\"/>";
544
+ if (rotation60k === void 0 || !Number.isFinite(rotation60k) || rotation60k % 216e5 === 0) return none;
545
+ const rad = rotation60k / 6e4 * Math.PI / 180;
546
+ const c = Math.abs(Math.cos(rad));
547
+ const s = Math.abs(Math.sin(rad));
548
+ const overhang = (span, turned) => Math.max(0, Math.round((turned - span) / 2));
549
+ const h = overhang(cx, cx * c + cy * s);
550
+ const v = overhang(cy, cx * s + cy * c);
551
+ return h === 0 && v === 0 ? none : `<wp:effectExtent l="${String(h)}" t="${String(v)}" r="${String(h)}" b="${String(v)}"/>`;
552
+ }
553
+ function drawingXml(resource, widthPt, heightPt, altText, state, scope, float) {
429
554
  if (resource === void 0) return "";
430
555
  const relId = mediaRelId(resource, state, scope);
431
556
  if (relId === void 0) return "";
432
557
  const cx = Math.round(widthPt * EMU_PER_PT);
433
558
  const cy = Math.round(heightPt * EMU_PER_PT);
434
559
  const id = ++state.drawingSeq;
435
- return `<w:drawing><wp:inline xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"><wp:extent cx="${cx}" cy="${cy}"/><wp:docPr id="${id}" name="Image ${id}"${altText ? ` descr="${escapeAttr(altText)}"` : ""}/><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:nvPicPr><pic:cNvPr id="${id}" name="Image ${id}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${relId}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing>`;
560
+ const descr = altText ? ` descr="${escapeAttr(altText)}"` : "";
561
+ const graphic = `<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:nvPicPr><pic:cNvPr id="${id}" name="Image ${id}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${relId}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic>`;
562
+ return drawingFrame(float, cx, cy, `<wp:docPr id="${id}" name="Image ${id}"${descr}/>`, graphic, state);
436
563
  }
437
564
  var WPS_URI = "http://schemas.microsoft.com/office/word/2010/wordprocessingShape";
438
565
  function shapeDrawingXml(shape, losses, state, scope) {
@@ -442,7 +569,8 @@ function shapeDrawingXml(shape, losses, state, scope) {
442
569
  const descr = shape.altText ? ` descr="${escapeAttr(shape.altText)}"` : "";
443
570
  const spPr = `<wps:spPr>${xfrmXml(shape.transform, cx, cy)}${geomXml(shape.geometry)}${fillXml(shape.fill)}${shape.line ? lineXml(shape.line) : ""}</wps:spPr>`;
444
571
  const txbx = shape.text ? txbxXml(shape.text, losses, state, scope) : "";
445
- return `<w:drawing><wp:inline xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"><wp:extent cx="${cx}" cy="${cy}"/><wp:docPr id="${id}" name="Shape ${id}"${descr}/><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="${WPS_URI}"><wps:wsp xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"><wps:cNvSpPr/>` + spPr + txbx + bodyPrXml(shape.text) + "</wps:wsp></a:graphicData></a:graphic></wp:inline></w:drawing>";
572
+ const graphic = `<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="${WPS_URI}"><wps:wsp xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"><wps:cNvSpPr/>` + spPr + txbx + bodyPrXml(shape.text) + "</wps:wsp></a:graphicData></a:graphic>";
573
+ return drawingFrame(shape.float, cx, cy, `<wp:docPr id="${id}" name="Shape ${id}"${descr}/>`, graphic, state, shape.transform?.rotation60k);
446
574
  }
447
575
  function xfrmXml(t, cx, cy) {
448
576
  return `<a:xfrm${t?.rotation60k !== void 0 ? ` rot="${t.rotation60k}"` : ""}${t?.flipH ? " flipH=\"1\"" : ""}${t?.flipV ? " flipV=\"1\"" : ""}><a:off x="0" y="0"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm>`;
@@ -488,8 +616,8 @@ function geomXml(g) {
488
616
  case "close": return "<a:close/>";
489
617
  }
490
618
  }).join("");
491
- const w = c1(g.custom.pathWidth);
492
- const h = c1(g.custom.pathHeight);
619
+ const w = Math.max(1, c1(g.custom.pathWidth));
620
+ const h = Math.max(1, c1(g.custom.pathHeight));
493
621
  return `<a:custGeom><a:avLst/><a:gdLst/><a:rect l="0" t="0" r="${w}" b="${h}"/><a:pathLst><a:path w="${w}" h="${h}">${cmds}</a:path></a:pathLst></a:custGeom>`;
494
622
  }
495
623
  const gds = g.adjust && g.adjust.size > 0 ? [...g.adjust].map(([n, v]) => `<a:gd name="${escapeAttr(n)}" fmla="val ${v}"/>`).join("") : "";
@@ -698,19 +826,27 @@ function runXml(run, state, scope) {
698
826
  }
699
827
  function rPrXml(r) {
700
828
  const out = [];
829
+ const fonts = rFontsXml(r.fontFamily);
830
+ if (fonts) out.push(fonts);
701
831
  if (r.bold !== DEFAULT_RUN.bold) out.push(toggle("w:b", r.bold));
702
832
  if (r.italic !== DEFAULT_RUN.italic) out.push(toggle("w:i", r.italic));
703
833
  if (r.strike !== DEFAULT_RUN.strike) out.push(toggle("w:strike", r.strike));
704
- if (r.underline !== DEFAULT_RUN.underline) out.push(`<w:u w:val="${r.underline}"/>`);
705
- const fonts = rFontsXml(r.fontFamily);
706
- if (fonts) out.push(fonts);
707
- if (r.fontSizePt !== DEFAULT_RUN.fontSizePt) out.push(`<w:sz w:val="${Math.round(r.fontSizePt * 2)}"/>`);
708
834
  if (r.colorHex !== DEFAULT_RUN.colorHex) out.push(`<w:color w:val="${r.colorHex}"/>`);
835
+ if (r.fontSizePt !== DEFAULT_RUN.fontSizePt) out.push(`<w:sz w:val="${Math.round(r.fontSizePt * 2)}"/>`);
836
+ if (r.underline !== DEFAULT_RUN.underline) out.push(underlineXml(r));
837
+ if (r.shadingColorHex !== void 0) out.push(runShdXml(r.shadingColorHex));
709
838
  if (r.verticalAlign !== DEFAULT_RUN.verticalAlign) out.push(`<w:vertAlign w:val="${r.verticalAlign}"/>`);
710
839
  if (r.rtl !== DEFAULT_RUN.rtl) out.push(toggle("w:rtl", r.rtl));
711
840
  if (r.lang !== void 0) out.push(`<w:lang w:val="${escapeAttr(r.lang)}"/>`);
712
841
  return out.length > 0 ? `<w:rPr>${out.join("")}</w:rPr>` : "";
713
842
  }
843
+ function underlineXml(r) {
844
+ const color = r.underlineColorHex !== void 0 ? ` w:color="${r.underlineColorHex}"` : "";
845
+ return `<w:u w:val="${r.underline}"${color}/>`;
846
+ }
847
+ function runShdXml(fill) {
848
+ return `<w:shd w:val="clear" w:color="auto" w:fill="${fill}"/>`;
849
+ }
714
850
  function pPrBody(p) {
715
851
  const out = [];
716
852
  if (p.numbering) out.push(`<w:numPr><w:ilvl w:val="${p.numbering.ilvl}"/><w:numId w:val="${escapeAttr(p.numbering.numId)}"/></w:numPr>`);
@@ -814,14 +950,18 @@ function rawParaPrXml(p) {
814
950
  }
815
951
  function rawRunPrXml(r) {
816
952
  const out = [];
953
+ const fonts = r.fontFamily ? rawRFontsXml(r.fontFamily) : "";
954
+ if (fonts) out.push(fonts);
817
955
  if (r.bold !== void 0) out.push(toggle("w:b", r.bold));
818
956
  if (r.italic !== void 0) out.push(toggle("w:i", r.italic));
819
957
  if (r.strike !== void 0) out.push(toggle("w:strike", r.strike));
820
- if (r.underline !== void 0) out.push(`<w:u w:val="${r.underline}"/>`);
821
- const fonts = r.fontFamily ? rawRFontsXml(r.fontFamily) : "";
822
- if (fonts) out.push(fonts);
823
- if (r.fontSizePt !== void 0) out.push(`<w:sz w:val="${Math.round(r.fontSizePt * 2)}"/>`);
824
958
  if (r.colorHex !== void 0) out.push(`<w:color w:val="${r.colorHex}"/>`);
959
+ if (r.fontSizePt !== void 0) out.push(`<w:sz w:val="${Math.round(r.fontSizePt * 2)}"/>`);
960
+ if (r.underline !== void 0) out.push(underlineXml({
961
+ ...r,
962
+ underline: r.underline
963
+ }));
964
+ if (r.shadingColorHex !== void 0) out.push(runShdXml(r.shadingColorHex));
825
965
  if (r.verticalAlign !== void 0) out.push(`<w:vertAlign w:val="${r.verticalAlign}"/>`);
826
966
  return out.length > 0 ? `<w:rPr>${out.join("")}</w:rPr>` : "";
827
967
  }
@@ -859,8 +999,58 @@ function colsXml(cols) {
859
999
  return `<w:cols w:num="${cols.count}" w:space="${twips(cols.spacePt)}"/>`;
860
1000
  }
861
1001
  var twips = (pt) => Math.round(pt * 20);
1002
+ /**
1003
+ * XML 1.0 §2.2 — the characters a document may contain at all.
1004
+ *
1005
+ * Everything below U+0020 except tab, newline and return is FORBIDDEN, and no
1006
+ * escape exists for them: `&#2;` is as ill-formed as the byte. A package with
1007
+ * one in it is not a document — LibreOffice says "source file could not be
1008
+ * loaded" and Word says nothing useful either.
1009
+ *
1010
+ * They reach here from the PDF reader. A subset font that states neither a
1011
+ * `/ToUnicode` nor an `/Encoding /Differences` says nothing about what its
1012
+ * codes mean, and the codes of a subset start at 1, 2, 3 — so the last-resort
1013
+ * Latin-1 reading turns a page of text into control characters. Reading it
1014
+ * better is `font.ts`'s business; what this must guarantee is that nothing the
1015
+ * reader believes can produce a package that will not open.
1016
+ *
1017
+ * A lone surrogate is the same case: a half of a pair is not a character.
1018
+ */
1019
+ /**
1020
+ * XML 1.0 §2.2 — whether a code point may appear in a document at all.
1021
+ *
1022
+ * Everything below U+0020 except tab, newline and return is forbidden, and no
1023
+ * escape exists for them: `&#2;` is as ill-formed as the byte itself. So is a
1024
+ * lone surrogate — half a pair is not a character — and so are U+FFFE/U+FFFF.
1025
+ */
1026
+ function xmlAllows(cp) {
1027
+ if (cp === 9 || cp === 10 || cp === 13) return true;
1028
+ if (cp < 32) return false;
1029
+ if (cp >= 55296 && cp <= 57343) return false;
1030
+ if (cp === 65534 || cp === 65535) return false;
1031
+ return cp <= 1114111;
1032
+ }
1033
+ /**
1034
+ * Escape text for XML, and drop what XML cannot carry.
1035
+ *
1036
+ * The dropping is not fussiness: a package with one control character in it is
1037
+ * not a document. LibreOffice answers "source file could not be loaded" and
1038
+ * Word refuses it too, so the whole conversion is lost over one byte.
1039
+ *
1040
+ * They reach here from the PDF reader. A subset font that states neither a
1041
+ * `/ToUnicode` nor an `/Encoding /Differences` says nothing about what its
1042
+ * codes mean, and a subset's codes start at 1, 2, 3 — so the last-resort
1043
+ * Latin-1 reading turns a page of prose into control characters. Reading such
1044
+ * a font better is `font.ts`'s business; what this guarantees is that nothing
1045
+ * the reader believes can produce a package that will not open.
1046
+ */
862
1047
  function escapeXml(s) {
863
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1048
+ let out = "";
1049
+ for (const ch of s) {
1050
+ if (!xmlAllows(ch.codePointAt(0) ?? 0)) continue;
1051
+ out += ch === "&" ? "&amp;" : ch === "<" ? "&lt;" : ch === ">" ? "&gt;" : ch;
1052
+ }
1053
+ return out;
864
1054
  }
865
1055
  function escapeAttr(s) {
866
1056
  return escapeXml(s).replace(/"/g, "&quot;");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reamkit",
3
- "version": "1.25.1",
3
+ "version": "1.27.0",
4
4
  "description": "Ream — convert DOCX, XLSX, PPTX and PDF to PDF, SVG, HTML, DOCX and XLSX, built from scratch on the ECMA-376 and ISO 32000 specifications. Parse once, convert anywhere.",
5
5
  "license": "MIT",
6
6
  "author": "Alex Krassavin <info@reamkit.dev>",