@cj-tech-master/excelts 9.6.1 → 10.0.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 (207) hide show
  1. package/README.md +18 -3
  2. package/README_zh.md +18 -3
  3. package/dist/browser/modules/excel/cell.d.ts +4 -0
  4. package/dist/browser/modules/excel/note.js +5 -1
  5. package/dist/browser/modules/excel/row.js +35 -2
  6. package/dist/browser/modules/excel/stream/workbook-writer.browser.d.ts +8 -1
  7. package/dist/browser/modules/excel/stream/workbook-writer.browser.js +22 -2
  8. package/dist/browser/modules/excel/types.d.ts +81 -0
  9. package/dist/browser/modules/excel/utils/drawing-utils.d.ts +8 -0
  10. package/dist/browser/modules/excel/utils/drawing-utils.js +19 -2
  11. package/dist/browser/modules/excel/workbook.browser.d.ts +16 -0
  12. package/dist/browser/modules/excel/workbook.browser.js +32 -2
  13. package/dist/browser/modules/excel/worksheet.d.ts +31 -1
  14. package/dist/browser/modules/excel/worksheet.js +83 -0
  15. package/dist/browser/modules/excel/xlsx/xform/comment/vml-shape-xform.d.ts +7 -0
  16. package/dist/browser/modules/excel/xlsx/xform/comment/vml-shape-xform.js +42 -8
  17. package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +3 -1
  18. package/dist/browser/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +5 -0
  19. package/dist/browser/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +18 -1
  20. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +6 -0
  21. package/dist/browser/modules/excel/xlsx/xform/drawing/blip-xform.js +38 -11
  22. package/dist/browser/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.d.ts +1 -0
  23. package/dist/browser/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.js +5 -0
  24. package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +2 -0
  25. package/dist/browser/modules/excel/xlsx/xform/drawing/pic-xform.js +2 -1
  26. package/dist/browser/modules/excel/xlsx/xform/drawing/shape-xform.d.ts +47 -0
  27. package/dist/browser/modules/excel/xlsx/xform/drawing/shape-xform.js +109 -0
  28. package/dist/browser/modules/excel/xlsx/xform/drawing/two-cell-anchor-xform.js +10 -1
  29. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +64 -1
  30. package/dist/browser/modules/pdf/builder/document-builder.js +22 -49
  31. package/dist/browser/modules/pdf/builder/pdf-editor.js +1 -1
  32. package/dist/browser/modules/pdf/core/pdf-stream.d.ts +28 -1
  33. package/dist/browser/modules/pdf/core/pdf-stream.js +38 -2
  34. package/dist/browser/modules/pdf/font/font-manager.d.ts +26 -0
  35. package/dist/browser/modules/pdf/font/font-manager.js +35 -18
  36. package/dist/browser/modules/pdf/render/page-renderer.d.ts +51 -3
  37. package/dist/browser/modules/pdf/render/page-renderer.js +111 -18
  38. package/dist/browser/modules/word/advanced/field-engine.js +45 -20
  39. package/dist/browser/modules/word/advanced/glossary.d.ts +10 -36
  40. package/dist/browser/modules/word/advanced/glossary.js +8 -9
  41. package/dist/browser/modules/word/advanced/math-convert.js +94 -12
  42. package/dist/browser/modules/word/advanced/ole-objects.d.ts +28 -0
  43. package/dist/browser/modules/word/advanced/ole-objects.js +122 -19
  44. package/dist/browser/modules/word/advanced/style-map.js +31 -10
  45. package/dist/browser/modules/word/builder/run-builders.d.ts +7 -1
  46. package/dist/browser/modules/word/builder/run-builders.js +7 -1
  47. package/dist/browser/modules/word/constants.d.ts +4 -0
  48. package/dist/browser/modules/word/constants.js +5 -1
  49. package/dist/browser/modules/word/convert/docx-to-semantic.d.ts +2 -1
  50. package/dist/browser/modules/word/convert/docx-to-semantic.js +135 -1
  51. package/dist/browser/modules/word/convert/html/html-import.d.ts +32 -1
  52. package/dist/browser/modules/word/convert/html/html-import.js +167 -14
  53. package/dist/browser/modules/word/convert/html/html.d.ts +2 -2
  54. package/dist/browser/modules/word/convert/html/html.js +1 -1
  55. package/dist/browser/modules/word/convert/markdown/markdown-import.d.ts +48 -18
  56. package/dist/browser/modules/word/convert/markdown/markdown-import.js +279 -69
  57. package/dist/browser/modules/word/convert/markdown/markdown.d.ts +1 -1
  58. package/dist/browser/modules/word/convert/odt/odt.js +407 -56
  59. package/dist/browser/modules/word/html.d.ts +2 -2
  60. package/dist/browser/modules/word/html.js +1 -1
  61. package/dist/browser/modules/word/index.base.d.ts +3 -3
  62. package/dist/browser/modules/word/index.base.js +1 -1
  63. package/dist/browser/modules/word/layout/layout-full.js +326 -19
  64. package/dist/browser/modules/word/layout/render-page.js +35 -8
  65. package/dist/browser/modules/word/markdown.d.ts +1 -1
  66. package/dist/browser/modules/word/query/compat.d.ts +10 -2
  67. package/dist/browser/modules/word/query/compat.js +29 -21
  68. package/dist/browser/modules/word/reader/docx-reader.js +105 -2
  69. package/dist/browser/modules/word/reader/math-parser.js +8 -2
  70. package/dist/browser/modules/word/security/cfb-reader.js +5 -5
  71. package/dist/browser/modules/word/types.d.ts +96 -1
  72. package/dist/browser/modules/word/writer/docx-packager.js +108 -2
  73. package/dist/browser/modules/word/writer/glossary-writer.d.ts +28 -0
  74. package/dist/browser/modules/word/writer/glossary-writer.js +121 -0
  75. package/dist/browser/modules/word/writer/header-footer-writer.js +105 -20
  76. package/dist/browser/modules/word/writer/math-writer.js +7 -2
  77. package/dist/browser/utils/font-metrics.d.ts +8 -0
  78. package/dist/browser/utils/font-metrics.js +43 -0
  79. package/dist/browser/utils/theme-colors.js +4 -1
  80. package/dist/cjs/modules/excel/note.js +5 -1
  81. package/dist/cjs/modules/excel/row.js +35 -2
  82. package/dist/cjs/modules/excel/stream/workbook-writer.browser.js +22 -2
  83. package/dist/cjs/modules/excel/utils/drawing-utils.js +19 -2
  84. package/dist/cjs/modules/excel/workbook.browser.js +31 -1
  85. package/dist/cjs/modules/excel/worksheet.js +83 -0
  86. package/dist/cjs/modules/excel/xlsx/xform/comment/vml-shape-xform.js +42 -8
  87. package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +3 -1
  88. package/dist/cjs/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +5 -0
  89. package/dist/cjs/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +18 -1
  90. package/dist/cjs/modules/excel/xlsx/xform/drawing/blip-xform.js +38 -11
  91. package/dist/cjs/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.js +5 -0
  92. package/dist/cjs/modules/excel/xlsx/xform/drawing/pic-xform.js +2 -1
  93. package/dist/cjs/modules/excel/xlsx/xform/drawing/shape-xform.js +112 -0
  94. package/dist/cjs/modules/excel/xlsx/xform/drawing/two-cell-anchor-xform.js +10 -1
  95. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +64 -1
  96. package/dist/cjs/modules/pdf/builder/document-builder.js +21 -48
  97. package/dist/cjs/modules/pdf/builder/pdf-editor.js +1 -1
  98. package/dist/cjs/modules/pdf/core/pdf-stream.js +38 -2
  99. package/dist/cjs/modules/pdf/font/font-manager.js +35 -18
  100. package/dist/cjs/modules/pdf/render/page-renderer.js +112 -18
  101. package/dist/cjs/modules/word/advanced/field-engine.js +45 -20
  102. package/dist/cjs/modules/word/advanced/glossary.js +8 -9
  103. package/dist/cjs/modules/word/advanced/math-convert.js +94 -12
  104. package/dist/cjs/modules/word/advanced/ole-objects.js +123 -19
  105. package/dist/cjs/modules/word/advanced/style-map.js +31 -10
  106. package/dist/cjs/modules/word/builder/run-builders.js +7 -1
  107. package/dist/cjs/modules/word/constants.js +5 -1
  108. package/dist/cjs/modules/word/convert/docx-to-semantic.js +135 -1
  109. package/dist/cjs/modules/word/convert/html/html-import.js +168 -14
  110. package/dist/cjs/modules/word/convert/html/html.js +2 -1
  111. package/dist/cjs/modules/word/convert/markdown/markdown-import.js +279 -69
  112. package/dist/cjs/modules/word/convert/odt/odt.js +407 -56
  113. package/dist/cjs/modules/word/html.js +2 -1
  114. package/dist/cjs/modules/word/index.base.js +4 -3
  115. package/dist/cjs/modules/word/layout/layout-full.js +325 -18
  116. package/dist/cjs/modules/word/layout/render-page.js +35 -8
  117. package/dist/cjs/modules/word/query/compat.js +29 -21
  118. package/dist/cjs/modules/word/reader/docx-reader.js +104 -1
  119. package/dist/cjs/modules/word/reader/math-parser.js +8 -2
  120. package/dist/cjs/modules/word/security/cfb-reader.js +5 -5
  121. package/dist/cjs/modules/word/writer/docx-packager.js +108 -2
  122. package/dist/cjs/modules/word/writer/glossary-writer.js +124 -0
  123. package/dist/cjs/modules/word/writer/header-footer-writer.js +105 -20
  124. package/dist/cjs/modules/word/writer/math-writer.js +7 -2
  125. package/dist/cjs/utils/font-metrics.js +44 -0
  126. package/dist/cjs/utils/theme-colors.js +4 -1
  127. package/dist/esm/modules/excel/note.js +5 -1
  128. package/dist/esm/modules/excel/row.js +35 -2
  129. package/dist/esm/modules/excel/stream/workbook-writer.browser.js +22 -2
  130. package/dist/esm/modules/excel/utils/drawing-utils.js +19 -2
  131. package/dist/esm/modules/excel/workbook.browser.js +32 -2
  132. package/dist/esm/modules/excel/worksheet.js +83 -0
  133. package/dist/esm/modules/excel/xlsx/xform/comment/vml-shape-xform.js +42 -8
  134. package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +3 -1
  135. package/dist/esm/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +5 -0
  136. package/dist/esm/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +18 -1
  137. package/dist/esm/modules/excel/xlsx/xform/drawing/blip-xform.js +38 -11
  138. package/dist/esm/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.js +5 -0
  139. package/dist/esm/modules/excel/xlsx/xform/drawing/pic-xform.js +2 -1
  140. package/dist/esm/modules/excel/xlsx/xform/drawing/shape-xform.js +109 -0
  141. package/dist/esm/modules/excel/xlsx/xform/drawing/two-cell-anchor-xform.js +10 -1
  142. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +64 -1
  143. package/dist/esm/modules/pdf/builder/document-builder.js +22 -49
  144. package/dist/esm/modules/pdf/builder/pdf-editor.js +1 -1
  145. package/dist/esm/modules/pdf/core/pdf-stream.js +38 -2
  146. package/dist/esm/modules/pdf/font/font-manager.js +35 -18
  147. package/dist/esm/modules/pdf/render/page-renderer.js +111 -18
  148. package/dist/esm/modules/word/advanced/field-engine.js +45 -20
  149. package/dist/esm/modules/word/advanced/glossary.js +8 -9
  150. package/dist/esm/modules/word/advanced/math-convert.js +94 -12
  151. package/dist/esm/modules/word/advanced/ole-objects.js +122 -19
  152. package/dist/esm/modules/word/advanced/style-map.js +31 -10
  153. package/dist/esm/modules/word/builder/run-builders.js +7 -1
  154. package/dist/esm/modules/word/constants.js +5 -1
  155. package/dist/esm/modules/word/convert/docx-to-semantic.js +135 -1
  156. package/dist/esm/modules/word/convert/html/html-import.js +167 -14
  157. package/dist/esm/modules/word/convert/html/html.js +1 -1
  158. package/dist/esm/modules/word/convert/markdown/markdown-import.js +279 -69
  159. package/dist/esm/modules/word/convert/odt/odt.js +407 -56
  160. package/dist/esm/modules/word/html.js +1 -1
  161. package/dist/esm/modules/word/index.base.js +1 -1
  162. package/dist/esm/modules/word/layout/layout-full.js +326 -19
  163. package/dist/esm/modules/word/layout/render-page.js +35 -8
  164. package/dist/esm/modules/word/query/compat.js +29 -21
  165. package/dist/esm/modules/word/reader/docx-reader.js +105 -2
  166. package/dist/esm/modules/word/reader/math-parser.js +8 -2
  167. package/dist/esm/modules/word/security/cfb-reader.js +5 -5
  168. package/dist/esm/modules/word/writer/docx-packager.js +108 -2
  169. package/dist/esm/modules/word/writer/glossary-writer.js +121 -0
  170. package/dist/esm/modules/word/writer/header-footer-writer.js +105 -20
  171. package/dist/esm/modules/word/writer/math-writer.js +7 -2
  172. package/dist/esm/utils/font-metrics.js +43 -0
  173. package/dist/esm/utils/theme-colors.js +4 -1
  174. package/dist/iife/excelts.iife.js +496 -59
  175. package/dist/iife/excelts.iife.js.map +1 -1
  176. package/dist/iife/excelts.iife.min.js +39 -39
  177. package/dist/types/modules/excel/cell.d.ts +4 -0
  178. package/dist/types/modules/excel/stream/workbook-writer.browser.d.ts +8 -1
  179. package/dist/types/modules/excel/types.d.ts +81 -0
  180. package/dist/types/modules/excel/utils/drawing-utils.d.ts +8 -0
  181. package/dist/types/modules/excel/workbook.browser.d.ts +16 -0
  182. package/dist/types/modules/excel/worksheet.d.ts +31 -1
  183. package/dist/types/modules/excel/xlsx/xform/comment/vml-shape-xform.d.ts +7 -0
  184. package/dist/types/modules/excel/xlsx/xform/drawing/blip-xform.d.ts +6 -0
  185. package/dist/types/modules/excel/xlsx/xform/drawing/one-cell-anchor-xform.d.ts +1 -0
  186. package/dist/types/modules/excel/xlsx/xform/drawing/pic-xform.d.ts +2 -0
  187. package/dist/types/modules/excel/xlsx/xform/drawing/shape-xform.d.ts +47 -0
  188. package/dist/types/modules/pdf/core/pdf-stream.d.ts +28 -1
  189. package/dist/types/modules/pdf/font/font-manager.d.ts +26 -0
  190. package/dist/types/modules/pdf/render/page-renderer.d.ts +51 -3
  191. package/dist/types/modules/word/advanced/glossary.d.ts +10 -36
  192. package/dist/types/modules/word/advanced/ole-objects.d.ts +28 -0
  193. package/dist/types/modules/word/builder/run-builders.d.ts +7 -1
  194. package/dist/types/modules/word/constants.d.ts +4 -0
  195. package/dist/types/modules/word/convert/docx-to-semantic.d.ts +2 -1
  196. package/dist/types/modules/word/convert/html/html-import.d.ts +32 -1
  197. package/dist/types/modules/word/convert/html/html.d.ts +2 -2
  198. package/dist/types/modules/word/convert/markdown/markdown-import.d.ts +48 -18
  199. package/dist/types/modules/word/convert/markdown/markdown.d.ts +1 -1
  200. package/dist/types/modules/word/html.d.ts +2 -2
  201. package/dist/types/modules/word/index.base.d.ts +3 -3
  202. package/dist/types/modules/word/markdown.d.ts +1 -1
  203. package/dist/types/modules/word/query/compat.d.ts +10 -2
  204. package/dist/types/modules/word/types.d.ts +96 -1
  205. package/dist/types/modules/word/writer/glossary-writer.d.ts +28 -0
  206. package/dist/types/utils/font-metrics.d.ts +8 -0
  207. package/package.json +3 -1
@@ -0,0 +1,109 @@
1
+ import { BaseXform } from "../base-xform.js";
2
+ const EMU_PER_POINT = 12700;
3
+ /**
4
+ * Normalize a user-supplied colour to the bare 6-digit RGB hex that OOXML's
5
+ * `<a:srgbClr val="...">` requires:
6
+ * - strips a leading `#`
7
+ * - accepts 8-digit ARGB (the form excelts uses for cell fills) and drops the
8
+ * leading alpha byte, since `srgbClr` carries no alpha channel
9
+ * - upper-cases
10
+ *
11
+ * Anything that isn't a 6- or 8-digit hex string is passed through unchanged so
12
+ * a caller using a less common form is not silently broken.
13
+ */
14
+ function normalizeColor(color) {
15
+ const hex = color.startsWith("#") ? color.slice(1) : color;
16
+ if (/^[0-9a-fA-F]{8}$/.test(hex)) {
17
+ // ARGB → RGB: drop the alpha byte (srgbClr has no alpha component).
18
+ return hex.slice(2).toUpperCase();
19
+ }
20
+ if (/^[0-9a-fA-F]{6}$/.test(hex)) {
21
+ return hex.toUpperCase();
22
+ }
23
+ return hex;
24
+ }
25
+ /**
26
+ * Renders a user-visible drawing shape. Geometry/position is governed by the
27
+ * enclosing anchor (`xfrm` is written as zero, matching how Excel anchors a
28
+ * shape to a cell range), while preset geometry, fill, outline and text are
29
+ * taken from the model. Write-only: shapes are not parsed back on read (the
30
+ * same limitation that already applies to all non-chart drawing content).
31
+ */
32
+ class ShapeXform extends BaseXform {
33
+ get tag() {
34
+ return "xdr:sp";
35
+ }
36
+ render(xmlStream, model) {
37
+ xmlStream.openNode("xdr:sp", { macro: "", textlink: "" });
38
+ // --- Non-visual shape properties ---
39
+ xmlStream.openNode("xdr:nvSpPr");
40
+ xmlStream.leafNode("xdr:cNvPr", { id: model.cNvPrId, name: model.name });
41
+ xmlStream.leafNode("xdr:cNvSpPr", {});
42
+ xmlStream.closeNode(); // xdr:nvSpPr
43
+ // --- Shape properties ---
44
+ xmlStream.openNode("xdr:spPr");
45
+ // Position/size is driven by the anchor; emit a zero xfrm placeholder.
46
+ xmlStream.openNode("a:xfrm");
47
+ xmlStream.leafNode("a:off", { x: 0, y: 0 });
48
+ xmlStream.leafNode("a:ext", { cx: 0, cy: 0 });
49
+ xmlStream.closeNode(); // a:xfrm
50
+ xmlStream.openNode("a:prstGeom", { prst: model.shapeType });
51
+ xmlStream.leafNode("a:avLst");
52
+ xmlStream.closeNode(); // a:prstGeom
53
+ // Fill: a colour produces a solidFill, otherwise an explicit noFill.
54
+ if (model.fill && model.fill.color) {
55
+ xmlStream.openNode("a:solidFill");
56
+ xmlStream.leafNode("a:srgbClr", { val: normalizeColor(model.fill.color) });
57
+ xmlStream.closeNode(); // a:solidFill
58
+ }
59
+ else {
60
+ xmlStream.leafNode("a:noFill");
61
+ }
62
+ // Line: an `a:ln` with width (pt → EMU) and/or a solid colour. When width
63
+ // is given without a colour, Excel applies its default outline colour at
64
+ // that width. When neither colour nor width is supplied, emit an explicit
65
+ // noFill line (no visible outline).
66
+ if (model.line && (model.line.color || model.line.width !== undefined)) {
67
+ const lnAttrs = {};
68
+ if (model.line.width !== undefined) {
69
+ lnAttrs.w = Math.round(model.line.width * EMU_PER_POINT);
70
+ }
71
+ xmlStream.openNode("a:ln", lnAttrs);
72
+ if (model.line.color) {
73
+ xmlStream.openNode("a:solidFill");
74
+ xmlStream.leafNode("a:srgbClr", { val: normalizeColor(model.line.color) });
75
+ xmlStream.closeNode(); // a:solidFill
76
+ }
77
+ xmlStream.closeNode(); // a:ln
78
+ }
79
+ else {
80
+ xmlStream.openNode("a:ln");
81
+ xmlStream.leafNode("a:noFill");
82
+ xmlStream.closeNode(); // a:ln
83
+ }
84
+ xmlStream.closeNode(); // xdr:spPr
85
+ // --- Text body ---
86
+ xmlStream.openNode("xdr:txBody");
87
+ xmlStream.leafNode("a:bodyPr", { vertOverflow: "clip", wrap: "square", anchor: "ctr" });
88
+ xmlStream.leafNode("a:lstStyle");
89
+ xmlStream.openNode("a:p");
90
+ xmlStream.openNode("a:pPr", { algn: "ctr" });
91
+ xmlStream.closeNode(); // a:pPr
92
+ if (model.text) {
93
+ xmlStream.openNode("a:r");
94
+ xmlStream.openNode("a:rPr", { lang: "en-US" });
95
+ xmlStream.closeNode(); // a:rPr
96
+ xmlStream.openNode("a:t");
97
+ xmlStream.writeText(model.text);
98
+ xmlStream.closeNode(); // a:t
99
+ xmlStream.closeNode(); // a:r
100
+ }
101
+ else {
102
+ xmlStream.leafNode("a:endParaRPr", { lang: "en-US" });
103
+ }
104
+ xmlStream.closeNode(); // a:p
105
+ xmlStream.closeNode(); // xdr:txBody
106
+ xmlStream.closeNode(); // xdr:sp
107
+ }
108
+ }
109
+ export { ShapeXform };
@@ -2,6 +2,7 @@ import { BaseCellAnchorXform } from "./base-cell-anchor-xform.js";
2
2
  import { CellPositionXform } from "./cell-position-xform.js";
3
3
  import { GraphicFrameXform } from "./graphic-frame-xform.js";
4
4
  import { PicXform } from "./pic-xform.js";
5
+ import { ShapeXform } from "./shape-xform.js";
5
6
  import { SpXform } from "./sp-xform.js";
6
7
  import { StaticXform } from "../static-xform.js";
7
8
  class TwoCellAnchorXform extends BaseCellAnchorXform {
@@ -24,6 +25,7 @@ class TwoCellAnchorXform extends BaseCellAnchorXform {
24
25
  "xdr:to": new CellPositionXform({ tag: "xdr:to" }),
25
26
  "xdr:pic": new PicXform(),
26
27
  "xdr:sp": new SpXform(),
28
+ "xdr:userShape": new ShapeXform(),
27
29
  "xdr:graphicFrame": new GraphicFrameXform(),
28
30
  "xdr:clientData": new StaticXform({ tag: "xdr:clientData" })
29
31
  };
@@ -125,7 +127,14 @@ class TwoCellAnchorXform extends BaseCellAnchorXform {
125
127
  this.map["xdr:graphicFrame"].render(xmlStream, model.graphicFrame);
126
128
  }
127
129
  else if (model.shape) {
128
- this.map["xdr:sp"].render(xmlStream, model.shape);
130
+ // A user-visible shape routes to the dedicated ShapeXform; the legacy
131
+ // form-control shape (no `kind`) stays on the SpXform path.
132
+ if (model.shape.kind === "userShape") {
133
+ this.map["xdr:userShape"].render(xmlStream, model.shape);
134
+ }
135
+ else {
136
+ this.map["xdr:sp"].render(xmlStream, model.shape);
137
+ }
129
138
  }
130
139
  this.map["xdr:clientData"].render(xmlStream, {});
131
140
  xmlStream.closeNode(); // xdr:twoCellAnchor
@@ -498,7 +498,70 @@ class WorkSheetXform extends BaseXform {
498
498
  });
499
499
  }
500
500
  }
501
- // Handle header watermark images VML header/footer image
501
+ // Handle user-drawn shapesanchored drawing parts with no media/rel.
502
+ const shapes = model.shapes ?? [];
503
+ if (shapes.length > 0) {
504
+ let { drawing } = model;
505
+ if (!drawing) {
506
+ drawing = model.drawing = {
507
+ rId: nextRid(rels),
508
+ name: `drawing${++options.drawingsCount}`,
509
+ anchors: [],
510
+ rels: []
511
+ };
512
+ options.drawings.push(drawing);
513
+ rels.push({
514
+ Id: drawing.rId,
515
+ Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
516
+ Target: drawingRelTargetFromWorksheet(drawing.name)
517
+ });
518
+ }
519
+ for (const shape of shapes) {
520
+ const anchorRange = shape.anchorRange;
521
+ if (!anchorRange) {
522
+ continue;
523
+ }
524
+ // Mirror the three image anchoring modes. `getAnchorType` (drawing
525
+ // xform) dispatches on `pos`/`br`: absolute when `pos` is present,
526
+ // two-cell when `br` is present, one-cell otherwise (needs `ext`).
527
+ let range;
528
+ if (anchorRange.pos) {
529
+ range = { pos: anchorRange.pos, ext: anchorRange.ext, editAs: "absolute" };
530
+ }
531
+ else if (anchorRange.br) {
532
+ range = {
533
+ tl: anchorRange.tl,
534
+ br: anchorRange.br,
535
+ editAs: anchorRange.editAs ?? "oneCell"
536
+ };
537
+ }
538
+ else {
539
+ range = {
540
+ tl: anchorRange.tl,
541
+ ext: anchorRange.ext,
542
+ editAs: anchorRange.editAs ?? "oneCell"
543
+ };
544
+ }
545
+ // Allocate a cNvPr id from the same monotonic space as the anchor's
546
+ // position in the drawing so it never collides with image/chart ids
547
+ // (which derive from the anchor index).
548
+ const cNvPrId = drawing.anchors.length + 1;
549
+ drawing.anchors.push({
550
+ range,
551
+ shape: {
552
+ kind: "userShape",
553
+ cNvPrId,
554
+ name: shape.name ?? `Shape ${cNvPrId}`,
555
+ shapeType: shape.shapeType,
556
+ fill: shape.fillColor ? { color: shape.fillColor } : undefined,
557
+ line: shape.lineColor !== undefined || shape.lineWidth !== undefined
558
+ ? { color: shape.lineColor, width: shape.lineWidth }
559
+ : undefined,
560
+ text: shape.text
561
+ }
562
+ });
563
+ }
564
+ }
502
565
  if (headerImageMedia.length > 0) {
503
566
  const medium = headerImageMedia[0]; // Only one header image per sheet
504
567
  const bookImage = options.media[medium.imageId];
@@ -26,7 +26,7 @@ import { writePdfAMetadata, writePdfAOutputIntent } from "../core/pdfa.js";
26
26
  import { FontManager } from "../font/font-manager.js";
27
27
  import { iterateSystemFontCandidates } from "../font/system-fonts.js";
28
28
  import { parseTtf } from "../font/ttf-parser.js";
29
- import { wrapTextLines, emitTextWithMatrix, alphaGsName } from "../render/page-renderer.js";
29
+ import { emitTextBlock, alphaGsName } from "../render/page-renderer.js";
30
30
  import { writeImageXObject } from "./image-utils.js";
31
31
  // =============================================================================
32
32
  // Constants
@@ -95,56 +95,29 @@ export class PdfPageBuilder {
95
95
  const bold = options.bold ?? false;
96
96
  const italic = options.italic ?? false;
97
97
  const fontFamily = options.fontFamily ?? "Helvetica";
98
- // Resolve font
98
+ // Resolve the provisional Type1 resource and record the run's code
99
+ // points. The text is emitted as a deferred block so anchor alignment,
100
+ // word wrapping, and glyph encoding are all computed at build time —
101
+ // after fonts are finalised (a non-WinAnsi run may trigger a build-time
102
+ // auto-embed of a system CIDFont). Measuring against the provisional
103
+ // metrics here would misplace anchored text and break lines wrongly.
99
104
  const resourceName = this._fontManager.resolveFont(fontFamily, bold, italic);
100
105
  this._fontManager.trackText(text);
101
- const useType3 = this._fontManager.hasType3Fonts() && !this._fontManager.hasEmbeddedFont();
102
- // Resolve anchor into an adjusted x before any matrix math. We use the
103
- // same font/size combination the stream will render with, so the
104
- // alignment is correct for the actual glyphs (not a fallback
105
- // estimate). Single-line drawing only — wrapped text ignores anchor
106
- // because the wrapper re-splits by the caller's supplied x.
107
- const anchor = options.anchor ?? "start";
108
- const resolvedX = anchor === "start" || options.maxWidth
109
- ? options.x
110
- : options.x -
111
- this._fontManager.measureText(text, resourceName, fontSize) *
112
- (anchor === "middle" ? 0.5 : 1);
113
- if (options.maxWidth) {
114
- // Word-wrap (reuses the shared wrapTextLines from page-renderer)
115
- const measure = (s) => this._fontManager.measureText(s, resourceName, fontSize);
116
- const lines = wrapTextLines(text, measure, options.maxWidth);
117
- const leading = fontSize * lineHeightFactor;
118
- this._stream.save();
119
- this._applyAlpha(color.a);
120
- this._stream.setFillColor(color);
121
- for (let i = 0; i < lines.length; i++) {
122
- const lineY = options.y - i * leading;
123
- emitTextWithMatrix(this._stream, lines[i], 1, 0, 0, 1, options.x, lineY, resourceName, fontSize, this._fontManager, useType3);
124
- }
125
- this._stream.restore();
126
- }
127
- else {
128
- // Single line
129
- this._stream.save();
130
- this._applyAlpha(color.a);
131
- this._stream.setFillColor(color);
132
- const rotation = options.rotation ?? 0;
133
- if (rotation === 0) {
134
- emitTextWithMatrix(this._stream, text, 1, 0, 0, 1, resolvedX, options.y, resourceName, fontSize, this._fontManager, useType3);
135
- }
136
- else {
137
- // Build the rotation matrix around (x, y). `emitTextWithMatrix`
138
- // accepts the full 2×3 text matrix, so we pre-multiply the
139
- // rotation with the translation so one call positions and
140
- // rotates the glyph sequence in a single Tm op.
141
- const theta = (rotation * Math.PI) / 180;
142
- const cos = Math.cos(theta);
143
- const sin = Math.sin(theta);
144
- emitTextWithMatrix(this._stream, text, cos, sin, -sin, cos, resolvedX, options.y, resourceName, fontSize, this._fontManager, useType3);
145
- }
146
- this._stream.restore();
147
- }
106
+ this._stream.save();
107
+ this._applyAlpha(color.a);
108
+ this._stream.setFillColor(color);
109
+ emitTextBlock(this._stream, {
110
+ text,
111
+ x: options.x,
112
+ y: options.y,
113
+ type1ResourceName: resourceName,
114
+ fontSize,
115
+ anchor: options.anchor ?? "start",
116
+ maxWidth: options.maxWidth,
117
+ lineHeightFactor,
118
+ rotation: options.rotation ?? 0
119
+ }, this._fontManager);
120
+ this._stream.restore();
148
121
  return this;
149
122
  }
150
123
  /**
@@ -144,7 +144,7 @@ export class PdfEditorPage {
144
144
  }
145
145
  /** @internal */
146
146
  _hasOverlay() {
147
- return (this._overlay._stream.toString().length > 0 ||
147
+ return (this._overlay._stream.hasContent() ||
148
148
  this._overlay._images.length > 0 ||
149
149
  this._overlay._builderAnnotations.length > 0 ||
150
150
  this._overlay._formFields.length > 0);
@@ -23,6 +23,16 @@ import { pdfNumber } from "./pdf-object.js";
23
23
  */
24
24
  export class PdfContentStream {
25
25
  constructor() {
26
+ /**
27
+ * Content stream fragments in draw order. Most entries are plain operator
28
+ * strings produced eagerly. A function entry is a *deferred* fragment:
29
+ * its body is only evaluated at serialization time. This is required for
30
+ * text whose final byte encoding depends on font decisions (embedded
31
+ * CIDFont vs. Type1/WinAnsi vs. Type3 fallback) that are not finalised
32
+ * until `PdfDocumentBuilder.build()`. Deferring keeps the fragment at its
33
+ * exact draw-order position (preserving z-order) while letting the actual
34
+ * encoding run after the font manager's state is settled.
35
+ */
26
36
  this.parts = [];
27
37
  }
28
38
  // ===========================================================================
@@ -36,6 +46,17 @@ export class PdfContentStream {
36
46
  this.parts.push(operator);
37
47
  return this;
38
48
  }
49
+ /**
50
+ * Append a deferred fragment whose body is evaluated only at serialization
51
+ * time. Used by text drawing so the final byte encoding can be chosen after
52
+ * the document's fonts are resolved at build time. The fragment occupies its
53
+ * draw-order slot immediately, so z-order relative to other operators is
54
+ * preserved.
55
+ */
56
+ deferred(produce) {
57
+ this.parts.push(produce);
58
+ return this;
59
+ }
39
60
  // ===========================================================================
40
61
  // Graphics State
41
62
  // ===========================================================================
@@ -435,10 +456,25 @@ export class PdfContentStream {
435
456
  // Serialization
436
457
  // ===========================================================================
437
458
  /**
438
- * Get the content stream as a string.
459
+ * Whether any fragment has been appended. Unlike `toString().length > 0`,
460
+ * this does NOT evaluate deferred fragments, so it is safe to call before
461
+ * fonts are resolved (e.g. when probing for overlay content during an
462
+ * editor save, prior to `writeFontResources`). A deferred text fragment
463
+ * counts as content even though its bytes are not produced yet.
464
+ */
465
+ hasContent() {
466
+ return this.parts.length > 0;
467
+ }
468
+ /**
469
+ * Get the content stream as a string. Deferred fragments (see `deferred`)
470
+ * are evaluated here, after font resolution has completed at build time.
439
471
  */
440
472
  toString() {
441
- return this.parts.join("\n");
473
+ const out = [];
474
+ for (const part of this.parts) {
475
+ out.push(typeof part === "function" ? part() : part);
476
+ }
477
+ return out.join("\n");
442
478
  }
443
479
  /**
444
480
  * Get the content stream as a Uint8Array (UTF-8 encoded).
@@ -182,30 +182,47 @@ export class FontManager {
182
182
  getEmbeddedResourceName() {
183
183
  return this.embeddedResourceName;
184
184
  }
185
+ /**
186
+ * Resolve the resource name a draw-time-resolved Type1 resource should
187
+ * actually render (and be measured) with, given the font manager's
188
+ * *current* state. If an embedded font exists (possibly auto-discovered
189
+ * at build time, after the text was drawn against a Type1 resource), the
190
+ * embedded resource name is returned so both measurement and encoding go
191
+ * through the CIDFont. Otherwise the original Type1 resource name is kept;
192
+ * `measureText` handles Type3-fallback widths internally from that name.
193
+ *
194
+ * Centralises the routing rule shared by the deferred text renderer and
195
+ * any deferred measurement (anchor alignment, word wrapping) so the two
196
+ * never disagree.
197
+ */
198
+ resolveRenderResourceName(type1ResourceName) {
199
+ return this.embeddedFont ? this.embeddedResourceName : type1ResourceName;
200
+ }
185
201
  /**
186
202
  * Record that a text string will be rendered, tracking its code points.
187
203
  * Must be called for every text string before writing the PDF.
204
+ *
205
+ * Two sets are maintained because font selection may be decided *after*
206
+ * drawing (e.g. `PdfDocumentBuilder.build()` auto-discovers and embeds a
207
+ * system font once it sees the accumulated non-WinAnsi code points):
208
+ *
209
+ * - `usedCodePoints` — every code point seen, always. If an embedded
210
+ * font ends up being used (whether registered up front or
211
+ * auto-discovered at build time), the subset must cover all of these,
212
+ * including plain ASCII, so the CIDFont can encode the full run.
213
+ * - `type3CodePoints` — non-WinAnsi code points only. Drives the
214
+ * build-time decision to auto-embed a system font, and the Type3
215
+ * fallback when none is available.
188
216
  */
189
217
  trackText(text) {
190
- if (this.embeddedFont) {
191
- for (let i = 0; i < text.length; i++) {
192
- const cp = text.codePointAt(i);
193
- this.usedCodePoints.add(cp);
194
- if (cp > 0xffff) {
195
- i++; // skip low surrogate
196
- }
218
+ for (let i = 0; i < text.length; i++) {
219
+ const cp = text.codePointAt(i);
220
+ if (cp > 0xffff) {
221
+ i++; // skip low surrogate
197
222
  }
198
- }
199
- else {
200
- // No embedded font — track non-WinAnsi chars for Type3 fallback
201
- for (let i = 0; i < text.length; i++) {
202
- const cp = text.codePointAt(i);
203
- if (cp > 0xffff) {
204
- i++;
205
- }
206
- if (!isWinAnsiCodePoint(cp)) {
207
- this.type3CodePoints.add(cp);
208
- }
223
+ this.usedCodePoints.add(cp);
224
+ if (!isWinAnsiCodePoint(cp)) {
225
+ this.type3CodePoints.add(cp);
209
226
  }
210
227
  }
211
228
  }
@@ -675,7 +675,7 @@ function drawRotatedGeneral(stream, cell, lines, fontManager, resourceName, font
675
675
  emitTextWithMatrix(stream, line, cos, sin, -sin, cos, tx, ty, resourceName, fontSize, fontManager, useType3);
676
676
  }
677
677
  }
678
- /** Emit a text string with hex encoding if available. */
678
+ /** Emit a text string with hex encoding if available, onto a sink stream. */
679
679
  function emitText(stream, fontManager, text, resourceName) {
680
680
  const hex = fontManager.encodeText(text, resourceName);
681
681
  if (hex) {
@@ -690,40 +690,133 @@ function emitText(stream, fontManager, text, resourceName) {
690
690
  * when needed. For each sub-run the matrix origin is advanced along the
691
691
  * text direction (cos, sin) by the rendered width.
692
692
  *
693
- * When `useType3` is false this collapses to a single BT/ET pair — identical
694
- * to the old `emitText()` path but wrapped in begin/end for convenience.
693
+ * The emitted operators are written as a *deferred* fragment (see
694
+ * `PdfContentStream.deferred`). The fragment is only evaluated at
695
+ * serialization time, by which point `PdfDocumentBuilder.build()` has
696
+ * finalised the document's fonts (auto-discovered embedded CIDFont,
697
+ * Type3 fallback, or plain Type1). This is essential: at draw time the
698
+ * font manager has not yet decided whether a non-WinAnsi code point (e.g.
699
+ * U+2192 →) will be served by an embedded font or a Type3 glyph, so eager
700
+ * encoding would irreversibly degrade those characters to spaces via the
701
+ * WinAnsi fallback. Deferring the encode keeps the fragment at its exact
702
+ * draw-order slot (preserving z-order) while choosing the correct bytes
703
+ * once fonts are known.
704
+ *
705
+ * The `useType3` argument is the caller's *draw-time* guess and is ignored;
706
+ * the deferred body recomputes the routing from the now-settled font
707
+ * manager state.
708
+ */
709
+ export function emitTextWithMatrix(stream, text, a, b, c, d, tx, ty, type1ResourceName, fontSize, fontManager, _useType3) {
710
+ stream.deferred(() => renderTextBlock(text, a, b, c, d, tx, ty, type1ResourceName, fontSize, fontManager));
711
+ }
712
+ /**
713
+ * Emit a text block as a single *deferred* fragment so that anchor
714
+ * alignment, word wrapping, and glyph encoding are all computed at
715
+ * serialization time — after `PdfDocumentBuilder.build()` has finalised the
716
+ * document's fonts.
717
+ *
718
+ * This matters because text measurement (anchor offset, line breaking) must
719
+ * use the *same* font that ultimately renders the glyphs. At draw time the
720
+ * font may still be unresolved (a non-WinAnsi run can trigger a build-time
721
+ * auto-embed of a system CIDFont), so measuring against the provisional
722
+ * Type1/Helvetica metrics would misplace centred/right-aligned text and
723
+ * break lines at the wrong points. Deferring keeps measurement and encoding
724
+ * consistent while preserving the fragment's draw-order slot (z-order).
725
+ */
726
+ export function emitTextBlock(stream, options, fontManager) {
727
+ stream.deferred(() => renderTextBlockLayout(options, fontManager));
728
+ }
729
+ /**
730
+ * Lay out and render a text block from the font manager's *current*
731
+ * (build-time) state. Resolves the render resource name once and uses it for
732
+ * both measurement and encoding so the two never disagree.
733
+ *
734
+ * Layout is computed in the text's *local* coordinate frame — x grows along
735
+ * the baseline, y grows upward — then mapped to page space through the
736
+ * rotation matrix. This makes anchor alignment, multi-line word wrapping, and
737
+ * rotation compose correctly together: each line is offset by its anchor
738
+ * shift (along local x) and its line index (down local y), and a single
739
+ * rotation maps the whole block into place. Upright text (rotation 0) reduces
740
+ * to the identity mapping.
741
+ */
742
+ function renderTextBlockLayout(options, fontManager) {
743
+ const { text, x, y, type1ResourceName, fontSize, anchor, maxWidth, lineHeightFactor, rotation } = options;
744
+ // Resolve the resource name once; measurement and rendering share it so a
745
+ // build-time auto-embedded CIDFont (or Type3 fallback) is measured with the
746
+ // metrics that will actually render the glyphs.
747
+ const measureResource = fontManager.resolveRenderResourceName(type1ResourceName);
748
+ const measure = (s) => fontManager.measureText(s, measureResource, fontSize);
749
+ const lines = maxWidth ? wrapTextLines(text, measure, maxWidth) : [text];
750
+ const leading = fontSize * lineHeightFactor;
751
+ // Rotation matrix [a b; c d] = [cos sin; -sin cos]; identity when upright.
752
+ const theta = (rotation * Math.PI) / 180;
753
+ const cos = rotation === 0 ? 1 : Math.cos(theta);
754
+ const sin = rotation === 0 ? 0 : Math.sin(theta);
755
+ const anchorFactor = anchor === "middle" ? 0.5 : anchor === "end" ? 1 : 0;
756
+ const parts = [];
757
+ for (let i = 0; i < lines.length; i++) {
758
+ // Local-frame origin of this line: anchor shift along x, line index down y.
759
+ const localX = anchorFactor === 0 ? 0 : -measure(lines[i]) * anchorFactor;
760
+ const localY = -i * leading;
761
+ // Map local origin into page space through the rotation matrix.
762
+ const tx = x + localX * cos + localY * -sin;
763
+ const ty = y + localX * sin + localY * cos;
764
+ parts.push(renderTextBlock(lines[i], cos, sin, -sin, cos, tx, ty, type1ResourceName, fontSize, fontManager));
765
+ }
766
+ return parts.join("\n");
767
+ }
768
+ /**
769
+ * Produce the PDF operator string for a positioned text run, choosing the
770
+ * encoding from the font manager's *current* (build-time) state:
771
+ * - embedded font → single BT/ET with CIDFont hex encoding
772
+ * - Type3 fallback → split into WinAnsi (Type1) and per-glyph Type3 runs
773
+ * - neither → single BT/ET with Type1/WinAnsi encoding
774
+ *
775
+ * Must only be called after font resolution (i.e. from a deferred fragment).
695
776
  */
696
- export function emitTextWithMatrix(stream, text, a, b, c, d, tx, ty, type1ResourceName, fontSize, fontManager, useType3) {
777
+ function renderTextBlock(text, a, b, c, d, tx, ty, type1ResourceName, fontSize, fontManager) {
778
+ const sink = new PdfContentStream();
779
+ // Type3 splitting only applies when there is no embedded font but Type3
780
+ // fallback glyphs were generated. Otherwise the run renders as a single
781
+ // BT/ET pair, choosing the resource name from the now-settled state:
782
+ // if an embedded font exists (possibly auto-discovered at build time,
783
+ // after this run was drawn against a Type1 resource), the run must use it
784
+ // so `emitText` → `encodeText` produces CIDFont hex; without that switch
785
+ // the stale Type1 resource name would make `encodeText` return null and
786
+ // non-WinAnsi characters would degrade to spaces via the WinAnsi fallback.
787
+ const useType3 = fontManager.hasType3Fonts() && !fontManager.hasEmbeddedFont();
697
788
  if (!useType3) {
698
- stream.beginText();
699
- stream.setFont(type1ResourceName, fontSize);
700
- stream.setTextMatrix(a, b, c, d, tx, ty);
701
- emitText(stream, fontManager, text, type1ResourceName);
702
- stream.endText();
703
- return;
789
+ const resourceName = fontManager.resolveRenderResourceName(type1ResourceName);
790
+ sink.beginText();
791
+ sink.setFont(resourceName, fontSize);
792
+ sink.setTextMatrix(a, b, c, d, tx, ty);
793
+ emitText(sink, fontManager, text, resourceName);
794
+ sink.endText();
795
+ return sink.toString();
704
796
  }
705
797
  // Type3 path: split into runs and advance origin along text direction
706
798
  const runs = splitTextRuns(text, fontManager);
707
799
  let curTx = tx;
708
800
  let curTy = ty;
709
801
  for (const run of runs) {
710
- stream.beginText();
802
+ sink.beginText();
711
803
  if (run.type3) {
712
- stream.setFont(run.type3.resourceName, fontSize);
713
- stream.setTextMatrix(a, b, c, d, curTx, curTy);
714
- stream.showTextHex(run.type3.hex);
804
+ sink.setFont(run.type3.resourceName, fontSize);
805
+ sink.setTextMatrix(a, b, c, d, curTx, curTy);
806
+ sink.showTextHex(run.type3.hex);
715
807
  }
716
808
  else {
717
- stream.setFont(type1ResourceName, fontSize);
718
- stream.setTextMatrix(a, b, c, d, curTx, curTy);
719
- emitText(stream, fontManager, run.text, type1ResourceName);
809
+ sink.setFont(type1ResourceName, fontSize);
810
+ sink.setTextMatrix(a, b, c, d, curTx, curTy);
811
+ emitText(sink, fontManager, run.text, type1ResourceName);
720
812
  }
721
- stream.endText();
813
+ sink.endText();
722
814
  const w = fontManager.measureText(run.text, run.type3?.resourceName ?? type1ResourceName, fontSize);
723
815
  // Advance along the text direction (first column of the matrix)
724
816
  curTx += a * w;
725
817
  curTy += b * w;
726
818
  }
819
+ return sink.toString();
727
820
  }
728
821
  /**
729
822
  * Split a line of text into runs of consecutive WinAnsi and non-WinAnsi