reamkit 1.6.0 → 1.9.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 (55) hide show
  1. package/README.md +13 -1
  2. package/dist/esm/core/converter/ream.d.ts +1 -0
  3. package/dist/esm/core/converter/ream.js +1 -1
  4. package/dist/esm/core/document-model/types.d.ts +3 -1
  5. package/dist/esm/core/drawingml/chart-serializer.d.ts +2 -0
  6. package/dist/esm/core/drawingml/chart-serializer.js +53 -0
  7. package/dist/esm/core/drawingml/shape-render.d.ts +3 -1
  8. package/dist/esm/core/drawingml/shape-render.js +27 -1
  9. package/dist/esm/core/vector.d.ts +10 -0
  10. package/dist/esm/excel/print-model.js +6 -3
  11. package/dist/esm/excel/xlsx-writer.js +81 -17
  12. package/dist/esm/html/html-writer.js +4 -2
  13. package/dist/esm/layout/styled-layout.js +5 -2
  14. package/dist/esm/pdf/shading.d.ts +12 -0
  15. package/dist/esm/pdf/shading.js +152 -0
  16. package/dist/esm/pdf/styled-page-emitter.js +29 -4
  17. package/dist/esm/pdf/vector-graphics.d.ts +1 -1
  18. package/dist/esm/pdf/vector-graphics.js +5 -3
  19. package/dist/esm/pdf-reader/ccitt.d.ts +15 -0
  20. package/dist/esm/pdf-reader/ccitt.js +394 -0
  21. package/dist/esm/pdf-reader/content.d.ts +43 -1
  22. package/dist/esm/pdf-reader/content.js +173 -4
  23. package/dist/esm/pdf-reader/crypto.d.ts +7 -0
  24. package/dist/esm/pdf-reader/crypto.js +609 -0
  25. package/dist/esm/pdf-reader/decrypt.d.ts +5 -0
  26. package/dist/esm/pdf-reader/decrypt.js +211 -0
  27. package/dist/esm/pdf-reader/document.d.ts +8 -1
  28. package/dist/esm/pdf-reader/document.js +210 -27
  29. package/dist/esm/pdf-reader/flow-build.d.ts +16 -1
  30. package/dist/esm/pdf-reader/flow-build.js +113 -3
  31. package/dist/esm/pdf-reader/image-decode.d.ts +15 -0
  32. package/dist/esm/pdf-reader/image-decode.js +550 -0
  33. package/dist/esm/pdf-reader/images.d.ts +16 -0
  34. package/dist/esm/pdf-reader/images.js +90 -0
  35. package/dist/esm/pdf-reader/layout.d.ts +2 -2
  36. package/dist/esm/pdf-reader/layout.js +86 -14
  37. package/dist/esm/pdf-reader/png-encode.d.ts +2 -0
  38. package/dist/esm/pdf-reader/png-encode.js +98 -0
  39. package/dist/esm/pdf-reader/predictor.d.ts +7 -0
  40. package/dist/esm/pdf-reader/predictor.js +61 -0
  41. package/dist/esm/pdf-reader/reader.d.ts +1 -1
  42. package/dist/esm/pdf-reader/reader.js +14 -7
  43. package/dist/esm/pdf-reader/shading.d.ts +3 -0
  44. package/dist/esm/pdf-reader/shading.js +147 -0
  45. package/dist/esm/pdf-reader/tagged.d.ts +2 -2
  46. package/dist/esm/pdf-reader/tagged.js +60 -7
  47. package/dist/esm/pdf-reader/text.js +91 -10
  48. package/dist/esm/pdf-reader/vector.d.ts +16 -0
  49. package/dist/esm/pdf-reader/vector.js +63 -0
  50. package/dist/esm/svg/svg-writer.js +12 -5
  51. package/dist/esm/word/docx-writer.js +104 -8
  52. package/dist/esm/word/drawing-parser.js +28 -17
  53. package/dist/esm/word/omml-serializer.d.ts +2 -0
  54. package/dist/esm/word/omml-serializer.js +48 -0
  55. package/package.json +1 -1
@@ -1,5 +1,7 @@
1
1
  import { pt } from "../core/ir/units.js";
2
- import { buildFlowDoc, paragraphBlock } from "./flow-build.js";
2
+ import { ResourceStore } from "../core/ir/resources.js";
3
+ import { buildFlowDoc, dedupeLosses, imageBlock, paragraphBlock, paragraphFromRuns } from "./flow-build.js";
4
+ import { collectPageImages } from "./images.js";
3
5
  import { extractPageText } from "./text.js";
4
6
  import { readStructTree } from "./struct-tree.js";
5
7
  //#region src/pdf-reader/tagged.ts
@@ -7,12 +9,45 @@ var ASSUMED_CONTENT_WIDTH_PT = 468;
7
9
  function reconstructTaggedPdf(file) {
8
10
  const root = readStructTree(file);
9
11
  if (!root) return void 0;
10
- const pageText = file.pages().map((page) => {
12
+ const pages = file.pages();
13
+ const pageRuns = pages.map((page) => {
11
14
  const byMcid = /* @__PURE__ */ new Map();
12
- for (const run of extractPageText(file, page)) if (run.mcid !== void 0) byMcid.set(run.mcid, (byMcid.get(run.mcid) ?? "") + run.text);
15
+ for (const run of extractPageText(file, page)) {
16
+ if (run.mcid === void 0) continue;
17
+ const list = byMcid.get(run.mcid);
18
+ if (list) list.push(run);
19
+ else byMcid.set(run.mcid, [run]);
20
+ }
21
+ return byMcid;
22
+ });
23
+ const runsOfMcid = (page, mcid) => pageRuns[page]?.get(mcid) ?? [];
24
+ const resources = new ResourceStore();
25
+ const pageImages = pages.map((page) => collectPageImages(file, page));
26
+ const imageLosses = dedupeLosses(pageImages.flatMap((p) => p.losses));
27
+ const imagesByMcid = pageImages.map((p) => {
28
+ const byMcid = /* @__PURE__ */ new Map();
29
+ for (const img of p.images) {
30
+ if (img.mcid === void 0) continue;
31
+ const list = byMcid.get(img.mcid);
32
+ if (list) list.push(img);
33
+ else byMcid.set(img.mcid, [img]);
34
+ }
13
35
  return byMcid;
14
36
  });
15
- const textOf = (node) => squash(node.mcids.map(({ page, mcid }) => pageText[page]?.get(mcid) ?? "").join(" "));
37
+ const emitted = /* @__PURE__ */ new Set();
38
+ const imagesForNode = (node) => node.mcids.flatMap(({ page, mcid }) => imagesByMcid[page]?.get(mcid) ?? []);
39
+ const textOf = (node) => squash(node.mcids.map(({ page, mcid }) => runsOfMcid(page, mcid).map((r) => r.text).join("")).join(" "));
40
+ const spansOf = (node) => {
41
+ const spans = [];
42
+ node.mcids.forEach(({ page, mcid }, i) => {
43
+ if (i > 0) spans.push({ text: " " });
44
+ for (const run of runsOfMcid(page, mcid)) spans.push(run.href !== void 0 ? {
45
+ text: run.text,
46
+ href: run.href
47
+ } : { text: run.text });
48
+ });
49
+ return spans;
50
+ };
16
51
  const collectText = (node) => squash([textOf(node), ...node.children.map(collectText)].join(" "));
17
52
  function emit(node, out) {
18
53
  if (node.type === "Table") {
@@ -20,14 +55,20 @@ function reconstructTaggedPdf(file) {
20
55
  if (table) out.push(table);
21
56
  return;
22
57
  }
58
+ if (node.type === "Figure") {
59
+ for (const img of imagesForNode(node)) {
60
+ emitted.add(img);
61
+ out.push(imageBlock(img, resources, node.alt));
62
+ }
63
+ return;
64
+ }
23
65
  if (node.type === "LI") {
24
66
  const text = collectText(node);
25
67
  if (text.length > 0) out.push(paragraphBlock(text, void 0));
26
68
  return;
27
69
  }
28
70
  if (node.children.length === 0) {
29
- const text = textOf(node);
30
- if (text.length > 0) out.push(paragraphBlock(text, headingLevel(node.type)));
71
+ if (textOf(node).length > 0) out.push(paragraphFromRuns(spansOf(node), headingLevel(node.type)));
31
72
  return;
32
73
  }
33
74
  for (const child of node.children) emit(child, out);
@@ -74,8 +115,20 @@ function reconstructTaggedPdf(file) {
74
115
  }
75
116
  const body = [];
76
117
  emit(root, body);
118
+ const orphans = [];
119
+ pageImages.forEach((p, page) => {
120
+ for (const img of p.images) if (!emitted.has(img)) orphans.push({
121
+ page,
122
+ img
123
+ });
124
+ });
125
+ orphans.sort((a, b) => a.page - b.page || b.img.y - a.img.y);
126
+ for (const { img } of orphans) body.push(imageBlock(img, resources));
77
127
  if (body.length === 0) return void 0;
78
- return buildFlowDoc(body);
128
+ return {
129
+ doc: buildFlowDoc(body, resources),
130
+ losses: imageLosses
131
+ };
79
132
  }
80
133
  function squash(text) {
81
134
  return text.replace(/\s+/g, " ").trim();
@@ -1,18 +1,99 @@
1
- import { interpretContent } from "./content.js";
1
+ import { PDF_NULL, PdfName, PdfStream } from "../pdf/objects.js";
2
+ import { IDENTITY, interpretContent, multiply } from "./content.js";
2
3
  import { buildContentFont } from "./font.js";
3
4
  //#region src/pdf-reader/text.ts
5
+ var MAX_FORM_DEPTH = 8;
4
6
  function extractPageText(file, page) {
7
+ const runs = [];
8
+ collectRuns(file, page.resources, file.pageContent(page), IDENTITY, 0, /* @__PURE__ */ new Set(), runs);
9
+ const links = collectLinks(file, page);
10
+ if (links.length === 0) return runs;
11
+ return runs.map((run) => {
12
+ const link = links.find((l) => inRect(run.x, run.y, l.rect));
13
+ return link ? {
14
+ ...run,
15
+ href: link.href
16
+ } : run;
17
+ });
18
+ }
19
+ function collectRuns(file, resources, content, baseCtm, depth, visiting, out) {
20
+ const result = interpretContent(content, buildFonts(file, resources), baseCtm);
21
+ out.push(...result.texts);
22
+ if (depth >= MAX_FORM_DEPTH || !resources) return;
23
+ const xobjects = file.get(resources, "XObject");
24
+ if (!(xobjects instanceof Map)) return;
25
+ for (const placement of result.images) {
26
+ const stream = file.resolve(xobjects.get(placement.name) ?? PDF_NULL);
27
+ if (!(stream instanceof PdfStream) || visiting.has(stream)) continue;
28
+ const sub = file.get(stream.dict, "Subtype");
29
+ if (!(sub instanceof PdfName) || sub.value !== "Form") continue;
30
+ visiting.add(stream);
31
+ const formRes = file.get(stream.dict, "Resources");
32
+ collectRuns(file, formRes instanceof Map ? formRes : resources, file.streamData(stream), multiply(matrixOf(file, stream.dict), placement.ctm), depth + 1, visiting, out);
33
+ visiting.delete(stream);
34
+ }
35
+ }
36
+ function buildFonts(file, resources) {
5
37
  const fonts = /* @__PURE__ */ new Map();
6
- if (page.resources) {
7
- const fontContainer = file.get(page.resources, "Font");
8
- if (fontContainer instanceof Map) for (const [fontName, fontRef] of fontContainer) {
9
- const fontDict = file.resolve(fontRef);
10
- if (fontDict instanceof Map) try {
11
- fonts.set(fontName, buildContentFont(file, fontDict));
12
- } catch {}
13
- }
38
+ if (!resources) return fonts;
39
+ const fontContainer = file.get(resources, "Font");
40
+ if (!(fontContainer instanceof Map)) return fonts;
41
+ for (const [fontName, fontRef] of fontContainer) {
42
+ const fontDict = file.resolve(fontRef);
43
+ if (fontDict instanceof Map) try {
44
+ fonts.set(fontName, buildContentFont(file, fontDict));
45
+ } catch {}
46
+ }
47
+ return fonts;
48
+ }
49
+ function matrixOf(file, dict) {
50
+ const m = file.resolve(dict.get("Matrix") ?? PDF_NULL);
51
+ if (Array.isArray(m) && m.length >= 6 && m.every((v) => typeof v === "number")) return [
52
+ m[0],
53
+ m[1],
54
+ m[2],
55
+ m[3],
56
+ m[4],
57
+ m[5]
58
+ ];
59
+ return IDENTITY;
60
+ }
61
+ function collectLinks(file, page) {
62
+ const annots = file.get(page.dict, "Annots");
63
+ if (!Array.isArray(annots)) return [];
64
+ const out = [];
65
+ for (const a of annots) {
66
+ const annot = file.resolve(a);
67
+ if (!(annot instanceof Map)) continue;
68
+ const sub = file.get(annot, "Subtype");
69
+ if (!(sub instanceof PdfName) || sub.value !== "Link") continue;
70
+ const rect = normRect(file.get(annot, "Rect"));
71
+ if (!rect) continue;
72
+ const action = file.get(annot, "A");
73
+ if (!(action instanceof Map)) continue;
74
+ const s = file.get(action, "S");
75
+ if (!(s instanceof PdfName) || s.value !== "URI") continue;
76
+ const uri = file.get(action, "URI");
77
+ if (typeof uri === "string" && uri.length > 0) out.push({
78
+ rect,
79
+ href: uri
80
+ });
14
81
  }
15
- return interpretContent(file.pageContent(page), fonts);
82
+ return out;
83
+ }
84
+ function normRect(v) {
85
+ if (!Array.isArray(v) || v.length < 4) return void 0;
86
+ const n = v.slice(0, 4).map((x) => typeof x === "number" ? x : NaN);
87
+ if (n.some((x) => !Number.isFinite(x))) return void 0;
88
+ return [
89
+ Math.min(n[0], n[2]),
90
+ Math.min(n[1], n[3]),
91
+ Math.max(n[0], n[2]),
92
+ Math.max(n[1], n[3])
93
+ ];
94
+ }
95
+ function inRect(x, y, r) {
96
+ return x >= r[0] - 1 && x <= r[2] + 1 && y >= r[1] - 2 && y <= r[3] + 2;
16
97
  }
17
98
  //#endregion
18
99
  export { extractPageText };
@@ -0,0 +1,16 @@
1
+ import { PathSeg } from './content.js';
2
+ import { ShapeGradient } from '../core/vector.js';
3
+ import { PdfFile, PdfPage } from './document.js';
4
+ export interface PdfVector {
5
+ readonly segs: ReadonlyArray<PathSeg>;
6
+ readonly fillHex?: string;
7
+ readonly gradient?: ShapeGradient;
8
+ readonly strokeHex?: string;
9
+ readonly lineWidth?: number;
10
+ readonly minX: number;
11
+ readonly minY: number;
12
+ readonly maxX: number;
13
+ readonly maxY: number;
14
+ readonly mcid?: number;
15
+ }
16
+ export declare function collectPageVectors(file: PdfFile, page: PdfPage): Array<PdfVector>;
@@ -0,0 +1,63 @@
1
+ import { IDENTITY, interpretContent } from "./content.js";
2
+ import { buildShadingMap } from "./shading.js";
3
+ //#region src/pdf-reader/vector.ts
4
+ var NO_FONTS = /* @__PURE__ */ new Map();
5
+ var MIN_SIDE = 2;
6
+ var MIN_AREA = 16;
7
+ var MIN_STROKE_LEN = 6;
8
+ var MAX_VECTORS = 2e3;
9
+ function collectPageVectors(file, page) {
10
+ const [px0, py0, px1, py1] = page.mediaBox;
11
+ const pageArea = Math.max(1, Math.abs((px1 - px0) * (py1 - py0)));
12
+ const shadings = buildShadingMap(file, page);
13
+ const out = [];
14
+ for (const v of interpretContent(file.pageContent(page), NO_FONTS, IDENTITY, shadings).vectors) {
15
+ if (out.length >= MAX_VECTORS) break;
16
+ const b = bbox(v.segs);
17
+ if (!b) continue;
18
+ const w = b.maxX - b.minX;
19
+ const h = b.maxY - b.minY;
20
+ const area = w * h;
21
+ const solidFill = v.fillHex !== void 0 && v.fillHex !== "FFFFFF";
22
+ const filled = (v.gradient !== void 0 || solidFill) && w >= MIN_SIDE && h >= MIN_SIDE && area >= MIN_AREA && area <= .85 * pageArea;
23
+ const stroked = v.strokeHex !== void 0 && v.strokeHex !== "FFFFFF" && Math.max(w, h) >= MIN_STROKE_LEN && area <= .85 * pageArea;
24
+ if (!filled && !stroked) continue;
25
+ out.push({
26
+ segs: v.segs,
27
+ ...filled ? v.gradient ? { gradient: v.gradient } : v.fillHex !== void 0 ? { fillHex: v.fillHex } : {} : {},
28
+ ...stroked ? {
29
+ strokeHex: v.strokeHex,
30
+ ...v.lineWidth !== void 0 ? { lineWidth: v.lineWidth } : {}
31
+ } : {},
32
+ ...b,
33
+ ...v.mcid !== void 0 ? { mcid: v.mcid } : {}
34
+ });
35
+ }
36
+ return out;
37
+ }
38
+ function bbox(segs) {
39
+ let minX = Infinity;
40
+ let minY = Infinity;
41
+ let maxX = -Infinity;
42
+ let maxY = -Infinity;
43
+ const add = (x, y) => {
44
+ minX = Math.min(minX, x);
45
+ minY = Math.min(minY, y);
46
+ maxX = Math.max(maxX, x);
47
+ maxY = Math.max(maxY, y);
48
+ };
49
+ for (const s of segs) if (s.op === "move" || s.op === "line") add(s.x, s.y);
50
+ else if (s.op === "cubic") {
51
+ add(s.x1, s.y1);
52
+ add(s.x2, s.y2);
53
+ add(s.x, s.y);
54
+ }
55
+ return Number.isFinite(minX) ? {
56
+ minX,
57
+ minY,
58
+ maxX,
59
+ maxY
60
+ } : void 0;
61
+ }
62
+ //#endregion
63
+ export { collectPageVectors };
@@ -1,5 +1,6 @@
1
1
  import { FEATURES } from "../core/ir/features.js";
2
2
  import { svgPathData } from "../core/vector.js";
3
+ import { gradientSvgDef } from "../core/drawingml/shape-render.js";
3
4
  import { paintPlan } from "../layout/page-doc.js";
4
5
  import { toBase64 } from "../core/bytes.js";
5
6
  //#region src/svg/svg-writer.ts
@@ -12,10 +13,11 @@ function writeSvg(laid, opts = {}) {
12
13
  const parts = [];
13
14
  parts.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${fmt(width)}" height="${fmt(height)}" viewBox="0 0 ${fmt(width)} ${fmt(height)}">`);
14
15
  let yOffset = 0;
16
+ const idc = { n: 0 };
15
17
  laid.pages.forEach((page, i) => {
16
18
  parts.push(`<g transform="translate(0 ${fmt(yOffset)})" data-page="${i + 1}">`);
17
19
  parts.push(`<rect x="0" y="0" width="${fmt(page.width)}" height="${fmt(page.height)}" fill="#ffffff" stroke="#cccccc"/>`);
18
- emitPage(parts, page, laid, losses);
20
+ emitPage(parts, page, laid, losses, idc);
19
21
  parts.push("</g>");
20
22
  yOffset += page.height + gap;
21
23
  });
@@ -36,7 +38,7 @@ var svgWriter = {
36
38
  ]),
37
39
  write: (doc, opts) => writeSvg(doc, opts ?? {})
38
40
  };
39
- function emitPage(out, page, laid, losses) {
41
+ function emitPage(out, page, laid, losses, idc) {
40
42
  const plan = paintPlan(page.commands);
41
43
  for (const f of plan.fills) out.push(`<rect x="${fmt(f.x)}" y="${fmt(f.y)}" width="${fmt(f.width)}" height="${fmt(f.height)}" fill="#${f.fillColorHex}"/>`);
42
44
  for (const img of plan.images) {
@@ -71,7 +73,7 @@ function emitPage(out, page, laid, losses) {
71
73
  ];
72
74
  out.push(`<line x1="${fmt(ax)}" y1="${fmt(ay)}" x2="${fmt(bx)}" y2="${fmt(by)}" stroke="#${b.borderColorHex}" stroke-width="${fmt(b.borderSizePt)}"/>`);
73
75
  }
74
- for (const sh of plan.shapes) emitShape(out, sh.shape);
76
+ for (const sh of plan.shapes) emitShape(out, sh.shape, idc);
75
77
  for (const t of plan.lines) emitTextLine(out, t, losses);
76
78
  }
77
79
  function emitTextLine(out, item, losses) {
@@ -95,10 +97,15 @@ function emitTextLine(out, item, losses) {
95
97
  x += tok.widthPt;
96
98
  }
97
99
  }
98
- function emitShape(out, shape) {
100
+ function emitShape(out, shape, idc) {
99
101
  const [a, b, c, d, e, f] = shape.transform;
100
102
  const transform = `matrix(${fmt(a)} ${fmt(b)} ${fmt(c)} ${fmt(d)} ${fmt(e)} ${fmt(f)})`;
101
- const fill = shape.fillColorHex ? `#${shape.fillColorHex}` : "none";
103
+ let fill;
104
+ if (shape.fillGradient) {
105
+ const id = `grad${idc.n++}`;
106
+ out.push(gradientSvgDef(id, shape.fillGradient));
107
+ fill = `url(#${id})`;
108
+ } else fill = shape.fillColorHex ? `#${shape.fillColorHex}` : "none";
102
109
  const stroke = shape.stroke ? ` stroke="#${shape.stroke.colorHex}" stroke-width="${fmt(shape.stroke.widthPt)}"` : "";
103
110
  for (const path of shape.paths) {
104
111
  const d2 = pathData(path.segments);
@@ -4,6 +4,8 @@ import { EMPTY_STYLE_SHEET, resolveParagraphProperties, resolveRunProperties } f
4
4
  import "../core/style-cascade/index.js";
5
5
  import { buildOpcPackage } from "../core/opc/opc-writer.js";
6
6
  import "../core/opc/index.js";
7
+ import { omathXml } from "./omml-serializer.js";
8
+ import { chartSpaceXml } from "../core/drawingml/chart-serializer.js";
7
9
  //#region src/word/docx-writer.ts
8
10
  var encoder = new TextEncoder();
9
11
  var DEFAULT_RUN = resolveRunProperties({}, {}, EMPTY_STYLE_SHEET);
@@ -15,6 +17,14 @@ var REL_NUMBERING = "http://schemas.openxmlformats.org/officeDocument/2006/relat
15
17
  var REL_HYPERLINK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
16
18
  var REL_IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
17
19
  var NUMBERING_PART = "word/numbering.xml";
20
+ var REL_FOOTNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes";
21
+ var REL_ENDNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes";
22
+ var FOOTNOTES_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml";
23
+ var ENDNOTES_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml";
24
+ var FOOTNOTES_PART = "word/footnotes.xml";
25
+ var ENDNOTES_PART = "word/endnotes.xml";
26
+ var REL_CHART = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart";
27
+ var CHART_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml";
18
28
  var EMU_PER_PT = 12700;
19
29
  var RASTER_MEDIA = {
20
30
  png: {
@@ -78,7 +88,10 @@ function writeDocx(flow) {
78
88
  mediaParts: [],
79
89
  mediaFileByResource: /* @__PURE__ */ new Map(),
80
90
  bookmarkSeq: 0,
81
- drawingSeq: 0
91
+ drawingSeq: 0,
92
+ chartParts: [],
93
+ chartSeq: 0,
94
+ ...flow.charts ? { charts: flow.charts } : {}
82
95
  };
83
96
  const docScope = newScope();
84
97
  const extraParts = [];
@@ -112,6 +125,24 @@ function writeDocx(flow) {
112
125
  target: "numbering.xml",
113
126
  targetMode: "Internal"
114
127
  });
128
+ emitNotes(flow.footnotes, {
129
+ noteKind: "footnote",
130
+ partPath: FOOTNOTES_PART,
131
+ rootTag: "w:footnotes",
132
+ noteTag: "w:footnote",
133
+ contentType: FOOTNOTES_CONTENT_TYPE,
134
+ relType: REL_FOOTNOTES,
135
+ target: "footnotes.xml"
136
+ }, state, losses, docScope, extraParts, extraPartRels);
137
+ emitNotes(flow.endnotes, {
138
+ noteKind: "endnote",
139
+ partPath: ENDNOTES_PART,
140
+ rootTag: "w:endnotes",
141
+ noteTag: "w:endnote",
142
+ contentType: ENDNOTES_CONTENT_TYPE,
143
+ relType: REL_ENDNOTES,
144
+ target: "endnotes.xml"
145
+ }, state, losses, docScope, extraParts, extraPartRels);
115
146
  const partRelationships = [...docScope.rels.length > 0 ? [{
116
147
  sourcePart: "word/document.xml",
117
148
  relationships: docScope.rels
@@ -126,6 +157,7 @@ function writeDocx(flow) {
126
157
  },
127
158
  ...numberingPart ? [numberingPart] : [],
128
159
  ...extraParts,
160
+ ...state.chartParts,
129
161
  ...state.mediaParts
130
162
  ],
131
163
  rootRelationships: [{
@@ -139,6 +171,34 @@ function writeDocx(flow) {
139
171
  losses
140
172
  };
141
173
  }
174
+ function emitNotes(notes, cfg, state, losses, docScope, extraParts, extraPartRels) {
175
+ if (!notes || notes.size === 0) return;
176
+ const scope = newScope();
177
+ scope.noteKind = cfg.noteKind;
178
+ const stub = (type, id, mark) => `<${cfg.noteTag} w:type="${type}" w:id="${id}"><w:p><w:r>${mark}</w:r></w:p></${cfg.noteTag}>`;
179
+ const noteXmls = [];
180
+ for (const [id, content] of notes) {
181
+ const inner = [];
182
+ for (const el of content) emitBlock(inner, el, losses, state, scope);
183
+ noteXmls.push(`<${cfg.noteTag} w:id="${escapeAttr(id)}">${inner.join("") || "<w:p/>"}</${cfg.noteTag}>`);
184
+ }
185
+ const xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><${cfg.rootTag} xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">` + stub("separator", -1, "<w:separator/>") + stub("continuationSeparator", 0, "<w:continuationSeparator/>") + noteXmls.join("") + `</${cfg.rootTag}>`;
186
+ extraParts.push({
187
+ path: cfg.partPath,
188
+ data: encoder.encode(xml),
189
+ contentType: cfg.contentType
190
+ });
191
+ if (scope.rels.length > 0) extraPartRels.push({
192
+ sourcePart: cfg.partPath,
193
+ relationships: scope.rels
194
+ });
195
+ docScope.rels.push({
196
+ id: `rId${++docScope.relSeq}`,
197
+ type: cfg.relType,
198
+ target: cfg.target,
199
+ targetMode: "Internal"
200
+ });
201
+ }
142
202
  function emitHeadersFooters(flow, section, state, docScope, extraParts, extraPartRels, hfCache, losses) {
143
203
  const refs = {
144
204
  headers: [],
@@ -226,12 +286,37 @@ function emitBlock(out, el, losses, state, scope, closingSectPr) {
226
286
  out.push(`<w:p>${pPrWithSect(el.shape.paragraphProperties, closingSectPr)}<w:r>${drawing}</w:r></w:p>`);
227
287
  return;
228
288
  }
229
- if (closingSectPr) out.push(`<w:p><w:pPr>${closingSectPr}</w:pPr></w:p>`);
230
- losses.push({
231
- severity: "dropped",
232
- feature: FEATURES.charts,
233
- detail: `${el.kind} not written by the docx writer (v0)`
289
+ const chartDrawing = chartBlockXml(el.chart, state, scope, losses);
290
+ if (chartDrawing) out.push(`<w:p>${pPrWithSect(el.chart.paragraphProperties, closingSectPr)}<w:r>${chartDrawing}</w:r></w:p>`);
291
+ else if (closingSectPr) out.push(`<w:p><w:pPr>${closingSectPr}</w:pPr></w:p>`);
292
+ }
293
+ function chartBlockXml(chart, state, scope, losses) {
294
+ const data = state.charts?.get(chart.chartRelId);
295
+ if (!data) {
296
+ losses.push({
297
+ severity: "dropped",
298
+ feature: FEATURES.charts,
299
+ detail: "chart data missing"
300
+ });
301
+ return "";
302
+ }
303
+ const cid = ++state.chartSeq;
304
+ state.chartParts.push({
305
+ path: `word/charts/chart${cid}.xml`,
306
+ data: encoder.encode(chartSpaceXml(data)),
307
+ contentType: CHART_CONTENT_TYPE
234
308
  });
309
+ const relId = `rId${++scope.relSeq}`;
310
+ scope.rels.push({
311
+ id: relId,
312
+ type: REL_CHART,
313
+ target: `charts/chart${cid}.xml`,
314
+ targetMode: "Internal"
315
+ });
316
+ const cx = Math.round(chart.width * EMU_PER_PT);
317
+ const cy = Math.round(chart.height * EMU_PER_PT);
318
+ const id = ++state.drawingSeq;
319
+ 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>`;
235
320
  }
236
321
  function drawingXml(resource, widthPt, heightPt, altText, state, scope) {
237
322
  if (resource === void 0) return "";
@@ -274,7 +359,14 @@ function geomXml(g) {
274
359
  return `<a:prstGeom prst="${escapeAttr(g.preset ?? "rect")}"><a:avLst>${gds}</a:avLst></a:prstGeom>`;
275
360
  }
276
361
  function fillXml(f) {
277
- return f.kind === "solid" && f.colorHex ? `<a:solidFill><a:srgbClr val="${f.colorHex}"/></a:solidFill>` : "<a:noFill/>";
362
+ if (f.kind === "solid" && f.colorHex) return `<a:solidFill><a:srgbClr val="${f.colorHex}"/></a:solidFill>`;
363
+ if (f.kind === "gradient" && f.gradient) return gradFillXml(f.gradient);
364
+ return "<a:noFill/>";
365
+ }
366
+ function gradFillXml(g) {
367
+ return `<a:gradFill><a:gsLst>${g.stops.map((s) => {
368
+ return `<a:gs pos="${Math.round(Math.max(0, Math.min(1, s.offset)) * 1e5)}"><a:srgbClr val="${s.colorHex}"/></a:gs>`;
369
+ }).join("")}</a:gsLst>${g.kind === "radial" ? "<a:path path=\"circle\"/>" : `<a:lin ang="${Math.round(((g.angle ?? 0) % 360 + 360) % 360 * 6e4)}" scaled="1"/>`}</a:gradFill>`;
278
370
  }
279
371
  function lineXml(l) {
280
372
  const w = l.width !== void 0 ? ` w="${Math.round(l.width * EMU_PER_PT)}"` : "";
@@ -402,7 +494,7 @@ function cellMarginsXml(tag, margins) {
402
494
  return inner ? `<${tag}>${inner}</${tag}>` : "";
403
495
  }
404
496
  function paragraphXml(p, state, scope, closingSectPr) {
405
- const visible = p.runs.filter((run) => !run.listMarker && run.math === void 0 && (run.text !== "" || run.inlineImage !== void 0 || run.pageBreak || run.href !== void 0 || run.anchor !== void 0));
497
+ const visible = p.runs.filter((run) => !run.listMarker && (run.math !== void 0 || run.text !== "" || run.inlineImage !== void 0 || run.pageBreak || run.footnoteRef !== void 0 || run.endnoteRef !== void 0 || run.noteNumber === true || run.href !== void 0 || run.anchor !== void 0));
406
498
  const inner = [];
407
499
  let i = 0;
408
500
  while (i < visible.length) {
@@ -440,6 +532,7 @@ function hyperlinkXml(run, inner, _state, scope) {
440
532
  return `<w:hyperlink w:anchor="${escapeAttr(run.anchor)}">${inner}</w:hyperlink>`;
441
533
  }
442
534
  function runXml(run, state, scope) {
535
+ if (run.math !== void 0) return `<m:oMath xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">${omathXml(run.math)}</m:oMath>`;
443
536
  const rPr = rPrXml(run.properties);
444
537
  if (run.inlineImage !== void 0) {
445
538
  const img = run.inlineImage;
@@ -447,6 +540,9 @@ function runXml(run, state, scope) {
447
540
  if (drawing) return `<w:r>${rPr}${drawing}</w:r>`;
448
541
  if (run.text === "" && !run.pageBreak) return "";
449
542
  }
543
+ if (run.footnoteRef !== void 0) return `<w:r>${rPr}<w:footnoteReference w:id="${escapeAttr(run.footnoteRef)}"/></w:r>`;
544
+ if (run.endnoteRef !== void 0) return `<w:r>${rPr}<w:endnoteReference w:id="${escapeAttr(run.endnoteRef)}"/></w:r>`;
545
+ if (run.noteNumber) return `<w:r>${rPr}<${scope.noteKind === "endnote" ? "w:endnoteRef" : "w:footnoteRef"}/></w:r>`;
450
546
  const brk = run.pageBreak ? "<w:br w:type=\"page\"/>" : "";
451
547
  if (run.text === "") return brk ? `<w:r>${rPr}${brk}</w:r>` : "";
452
548
  return `<w:r>${rPr}<w:t xml:space="preserve">${escapeXml(run.text)}</w:t>${brk}</w:r>`;
@@ -353,10 +353,10 @@ function parseFill(spPr, resolveColor) {
353
353
  } : { kind: "none" };
354
354
  }
355
355
  if (poIs(child, "a:gradFill")) {
356
- const hex = colorFromGradient(child, resolveColor);
357
- return hex ? {
358
- kind: "solid",
359
- colorHex: hex
356
+ const gradient = parseGradient(child, resolveColor);
357
+ return gradient ? {
358
+ kind: "gradient",
359
+ gradient
360
360
  } : { kind: "none" };
361
361
  }
362
362
  }
@@ -404,13 +404,10 @@ function colorFromContainer(parent, resolveColor) {
404
404
  if (hex) return hex;
405
405
  }
406
406
  }
407
- function colorFromGradient(grad, resolveColor) {
407
+ function parseGradient(grad, resolveColor) {
408
408
  const gsLst = poChildren(grad).find((c) => poIs(c, "a:gsLst"));
409
409
  if (!gsLst) return void 0;
410
- let r = 0;
411
- let g = 0;
412
- let b = 0;
413
- let n = 0;
410
+ const stops = [];
414
411
  for (const gs of poChildren(gsLst)) {
415
412
  if (!poIs(gs, "a:gs")) continue;
416
413
  let hex;
@@ -419,15 +416,29 @@ function colorFromGradient(grad, resolveColor) {
419
416
  if (hex) break;
420
417
  }
421
418
  if (!hex) continue;
422
- const num = parseInt(hex, 16);
423
- r += num >> 16 & 255;
424
- g += num >> 8 & 255;
425
- b += num & 255;
426
- n++;
419
+ const pos = poIntAttr(gs, "pos");
420
+ const offset = pos !== void 0 ? clampUnit(pos / 1e5) : stops.length === 0 ? 0 : 1;
421
+ stops.push({
422
+ offset,
423
+ colorHex: hex
424
+ });
427
425
  }
428
- if (n === 0) return void 0;
429
- const toHex = (x) => Math.round(x / n).toString(16).padStart(2, "0");
430
- return (toHex(r) + toHex(g) + toHex(b)).toUpperCase();
426
+ if (stops.length === 0) return void 0;
427
+ stops.sort((a, b) => a.offset - b.offset);
428
+ if (poChildren(grad).some((c) => poIs(c, "a:path"))) return {
429
+ kind: "radial",
430
+ stops
431
+ };
432
+ const lin = poChildren(grad).find((c) => poIs(c, "a:lin"));
433
+ const ang = lin ? poIntAttr(lin, "ang") : void 0;
434
+ return {
435
+ kind: "linear",
436
+ angle: ang !== void 0 ? ang / 6e4 % 360 : 0,
437
+ stops
438
+ };
439
+ }
440
+ function clampUnit(x) {
441
+ return x < 0 ? 0 : x > 1 ? 1 : x;
431
442
  }
432
443
  //#endregion
433
444
  export { expandMcChildren, parseDrawing, parseVmlPicture };
@@ -0,0 +1,2 @@
1
+ import { MathNode } from '../core/document-model/index.js';
2
+ export declare function omathXml(node: MathNode): string;
@@ -0,0 +1,48 @@
1
+ //#region src/word/omml-serializer.ts
2
+ function omathXml(node) {
3
+ return ser(node);
4
+ }
5
+ function ser(node) {
6
+ switch (node.type) {
7
+ case "row": return node.children.map(ser).join("");
8
+ case "run": return `<m:r>${runPr(node)}<m:t xml:space="preserve">${esc(node.text)}</m:t></m:r>`;
9
+ case "fraction": return "<m:f>" + (node.barless ? "<m:fPr><m:type m:val=\"noBar\"/></m:fPr>" : "") + `<m:num>${ser(node.num)}</m:num><m:den>${ser(node.den)}</m:den></m:f>`;
10
+ case "script": return scriptXml(node);
11
+ case "radical": return "<m:rad>" + (node.degree ? "" : "<m:radPr><m:degHide m:val=\"1\"/></m:radPr>") + (node.degree ? `<m:deg>${ser(node.degree)}</m:deg>` : "<m:deg/>") + `<m:e>${ser(node.radicand)}</m:e></m:rad>`;
12
+ case "nary": return `<m:nary><m:naryPr><m:chr m:val="${attr(node.op)}"/>` + (node.limLoc ? `<m:limLoc m:val="${node.limLoc}"/>` : "") + (node.sub ? "" : "<m:subHide m:val=\"1\"/>") + (node.sup ? "" : "<m:supHide m:val=\"1\"/>") + `</m:naryPr><m:sub>${node.sub ? ser(node.sub) : ""}</m:sub><m:sup>${node.sup ? ser(node.sup) : ""}</m:sup><m:e>${ser(node.body)}</m:e></m:nary>`;
13
+ case "func": return `<m:func><m:fName>${ser(node.name)}</m:fName><m:e>${ser(node.body)}</m:e></m:func>`;
14
+ case "limit": return `<m:lim${node.pos === "upp" ? "Upp" : "Low"}><m:e>${ser(node.base)}</m:e><m:lim>${ser(node.lim)}</m:lim></m:lim${node.pos === "upp" ? "Upp" : "Low"}>`;
15
+ case "delimiter": return `<m:d><m:dPr><m:begChr m:val="${attr(node.begChr)}"/>` + (node.sepChr !== void 0 ? `<m:sepChr m:val="${attr(node.sepChr)}"/>` : "") + `<m:endChr m:val="${attr(node.endChr)}"/></m:dPr>` + node.children.map((c) => `<m:e>${ser(c)}</m:e>`).join("") + "</m:d>";
16
+ case "matrix": return "<m:m>" + node.rows.map((row) => `<m:mr>${row.map((c) => `<m:e>${ser(c)}</m:e>`).join("")}</m:mr>`).join("") + "</m:m>";
17
+ case "accent": return `<m:acc><m:accPr><m:chr m:val="${attr(node.char)}"/></m:accPr><m:e>${ser(node.base)}</m:e></m:acc>`;
18
+ case "bar": return `<m:bar><m:barPr><m:pos m:val="${node.pos}"/></m:barPr><m:e>${ser(node.base)}</m:e></m:bar>`;
19
+ case "groupChr": return `<m:groupChr><m:groupChrPr><m:chr m:val="${attr(node.char)}"/><m:pos m:val="${node.pos}"/></m:groupChrPr><m:e>${ser(node.base)}</m:e></m:groupChr>`;
20
+ case "eqArr": return `<m:eqArr>${node.rows.map((r) => `<m:e>${ser(r)}</m:e>`).join("")}</m:eqArr>`;
21
+ }
22
+ }
23
+ function scriptXml(node) {
24
+ const base = `<m:e>${ser(node.base)}</m:e>`;
25
+ const sub = `<m:sub>${node.sub ? ser(node.sub) : ""}</m:sub>`;
26
+ const sup = `<m:sup>${node.sup ? ser(node.sup) : ""}</m:sup>`;
27
+ if (node.pre) return `<m:sPre>${base}${sub}${sup}</m:sPre>`;
28
+ if (node.sub && node.sup) return `<m:sSubSup>${base}${sub}${sup}</m:sSubSup>`;
29
+ if (node.sub) return `<m:sSub>${base}${sub}</m:sSub>`;
30
+ if (node.sup) return `<m:sSup>${base}${sup}</m:sSup>`;
31
+ return ser(node.base);
32
+ }
33
+ function runPr(run) {
34
+ let sty;
35
+ if (run.bold && run.italic) sty = "bi";
36
+ else if (run.bold) sty = "b";
37
+ else if (run.italic) sty = "i";
38
+ else if (run.nor) sty = "p";
39
+ return sty ? `<m:rPr><m:sty m:val="${sty}"/></m:rPr>` : "";
40
+ }
41
+ function esc(s) {
42
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
43
+ }
44
+ function attr(s) {
45
+ return esc(s).replace(/"/g, "&quot;");
46
+ }
47
+ //#endregion
48
+ export { omathXml };