docxmlater 10.0.1 → 10.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (395) hide show
  1. package/README.md +3 -2
  2. package/dist/constants/legacyCompatFlags.d.ts.map +1 -1
  3. package/dist/constants/legacyCompatFlags.js.map +1 -1
  4. package/dist/constants/limits.d.ts +0 -27
  5. package/dist/constants/limits.d.ts.map +1 -1
  6. package/dist/constants/limits.js +13 -13
  7. package/dist/constants/limits.js.map +1 -1
  8. package/dist/core/Document.d.ts +24 -19
  9. package/dist/core/Document.d.ts.map +1 -1
  10. package/dist/core/Document.js +272 -71
  11. package/dist/core/Document.js.map +1 -1
  12. package/dist/core/DocumentContent.d.ts.map +1 -1
  13. package/dist/core/DocumentContent.js.map +1 -1
  14. package/dist/core/DocumentGenerator.d.ts.map +1 -1
  15. package/dist/core/DocumentGenerator.js +59 -24
  16. package/dist/core/DocumentGenerator.js.map +1 -1
  17. package/dist/core/DocumentIdManager.d.ts.map +1 -1
  18. package/dist/core/DocumentIdManager.js.map +1 -1
  19. package/dist/core/DocumentParser.d.ts +6 -6
  20. package/dist/core/DocumentParser.d.ts.map +1 -1
  21. package/dist/core/DocumentParser.js +60 -54
  22. package/dist/core/DocumentParser.js.map +1 -1
  23. package/dist/core/DocumentValidator.d.ts.map +1 -1
  24. package/dist/core/DocumentValidator.js.map +1 -1
  25. package/dist/core/Relationship.d.ts.map +1 -1
  26. package/dist/core/Relationship.js +1 -1
  27. package/dist/core/Relationship.js.map +1 -1
  28. package/dist/core/RelationshipManager.js +3 -3
  29. package/dist/core/RelationshipManager.js.map +1 -1
  30. package/dist/elements/AlternateContent.js.map +1 -1
  31. package/dist/elements/Bookmark.d.ts.map +1 -1
  32. package/dist/elements/Bookmark.js.map +1 -1
  33. package/dist/elements/BookmarkManager.d.ts.map +1 -1
  34. package/dist/elements/BookmarkManager.js.map +1 -1
  35. package/dist/elements/Comment.js +1 -1
  36. package/dist/elements/Comment.js.map +1 -1
  37. package/dist/elements/CommentManager.d.ts.map +1 -1
  38. package/dist/elements/CommentManager.js +8 -2
  39. package/dist/elements/CommentManager.js.map +1 -1
  40. package/dist/elements/CommonTypes.d.ts.map +1 -1
  41. package/dist/elements/CommonTypes.js +1 -2
  42. package/dist/elements/CommonTypes.js.map +1 -1
  43. package/dist/elements/CustomXml.js.map +1 -1
  44. package/dist/elements/Endnote.d.ts.map +1 -1
  45. package/dist/elements/Endnote.js.map +1 -1
  46. package/dist/elements/EndnoteManager.d.ts.map +1 -1
  47. package/dist/elements/EndnoteManager.js.map +1 -1
  48. package/dist/elements/Field.d.ts.map +1 -1
  49. package/dist/elements/Field.js +31 -28
  50. package/dist/elements/Field.js.map +1 -1
  51. package/dist/elements/FieldHelpers.d.ts.map +1 -1
  52. package/dist/elements/FieldHelpers.js +6 -6
  53. package/dist/elements/FieldHelpers.js.map +1 -1
  54. package/dist/elements/FontManager.d.ts.map +1 -1
  55. package/dist/elements/FontManager.js.map +1 -1
  56. package/dist/elements/Footer.js.map +1 -1
  57. package/dist/elements/Footnote.d.ts.map +1 -1
  58. package/dist/elements/Footnote.js.map +1 -1
  59. package/dist/elements/FootnoteManager.d.ts.map +1 -1
  60. package/dist/elements/FootnoteManager.js.map +1 -1
  61. package/dist/elements/Header.js.map +1 -1
  62. package/dist/elements/HeaderFooterManager.js.map +1 -1
  63. package/dist/elements/Hyperlink.d.ts.map +1 -1
  64. package/dist/elements/Hyperlink.js +5 -5
  65. package/dist/elements/Hyperlink.js.map +1 -1
  66. package/dist/elements/Image.d.ts +2 -2
  67. package/dist/elements/Image.d.ts.map +1 -1
  68. package/dist/elements/Image.js +21 -5
  69. package/dist/elements/Image.js.map +1 -1
  70. package/dist/elements/ImageManager.d.ts.map +1 -1
  71. package/dist/elements/ImageManager.js +2 -2
  72. package/dist/elements/ImageManager.js.map +1 -1
  73. package/dist/elements/ImageRun.js.map +1 -1
  74. package/dist/elements/MathElement.js.map +1 -1
  75. package/dist/elements/Paragraph.d.ts.map +1 -1
  76. package/dist/elements/Paragraph.js +128 -117
  77. package/dist/elements/Paragraph.js.map +1 -1
  78. package/dist/elements/PreservedElement.js.map +1 -1
  79. package/dist/elements/PropertyChangeTypes.js.map +1 -1
  80. package/dist/elements/RangeMarker.js.map +1 -1
  81. package/dist/elements/Revision.d.ts +1 -0
  82. package/dist/elements/Revision.d.ts.map +1 -1
  83. package/dist/elements/Revision.js +44 -5
  84. package/dist/elements/Revision.js.map +1 -1
  85. package/dist/elements/RevisionContent.js.map +1 -1
  86. package/dist/elements/RevisionManager.d.ts.map +1 -1
  87. package/dist/elements/RevisionManager.js.map +1 -1
  88. package/dist/elements/Run.d.ts.map +1 -1
  89. package/dist/elements/Run.js +1 -3
  90. package/dist/elements/Run.js.map +1 -1
  91. package/dist/elements/Section.d.ts.map +1 -1
  92. package/dist/elements/Section.js +127 -118
  93. package/dist/elements/Section.js.map +1 -1
  94. package/dist/elements/Shape.d.ts.map +1 -1
  95. package/dist/elements/Shape.js +21 -0
  96. package/dist/elements/Shape.js.map +1 -1
  97. package/dist/elements/StructuredDocumentTag.d.ts.map +1 -1
  98. package/dist/elements/StructuredDocumentTag.js +20 -8
  99. package/dist/elements/StructuredDocumentTag.js.map +1 -1
  100. package/dist/elements/Table.d.ts +2 -2
  101. package/dist/elements/Table.d.ts.map +1 -1
  102. package/dist/elements/Table.js +29 -35
  103. package/dist/elements/Table.js.map +1 -1
  104. package/dist/elements/TableCell.d.ts +2 -2
  105. package/dist/elements/TableCell.d.ts.map +1 -1
  106. package/dist/elements/TableCell.js +63 -67
  107. package/dist/elements/TableCell.js.map +1 -1
  108. package/dist/elements/TableGridChange.js.map +1 -1
  109. package/dist/elements/TableOfContents.d.ts +6 -6
  110. package/dist/elements/TableOfContents.d.ts.map +1 -1
  111. package/dist/elements/TableOfContents.js.map +1 -1
  112. package/dist/elements/TableOfContentsElement.js.map +1 -1
  113. package/dist/elements/TableRow.d.ts.map +1 -1
  114. package/dist/elements/TableRow.js +65 -47
  115. package/dist/elements/TableRow.js.map +1 -1
  116. package/dist/elements/TextBox.d.ts.map +1 -1
  117. package/dist/elements/TextBox.js +1 -1
  118. package/dist/elements/TextBox.js.map +1 -1
  119. package/dist/formatting/AbstractNumbering.d.ts +1 -1
  120. package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
  121. package/dist/formatting/AbstractNumbering.js +11 -11
  122. package/dist/formatting/AbstractNumbering.js.map +1 -1
  123. package/dist/formatting/NumberingInstance.d.ts.map +1 -1
  124. package/dist/formatting/NumberingInstance.js +4 -4
  125. package/dist/formatting/NumberingInstance.js.map +1 -1
  126. package/dist/formatting/NumberingLevel.d.ts.map +1 -1
  127. package/dist/formatting/NumberingLevel.js +26 -26
  128. package/dist/formatting/NumberingLevel.js.map +1 -1
  129. package/dist/formatting/NumberingManager.d.ts +1 -1
  130. package/dist/formatting/NumberingManager.d.ts.map +1 -1
  131. package/dist/formatting/NumberingManager.js.map +1 -1
  132. package/dist/formatting/Style.d.ts.map +1 -1
  133. package/dist/formatting/Style.js +87 -95
  134. package/dist/formatting/Style.js.map +1 -1
  135. package/dist/formatting/StylesManager.d.ts +3 -3
  136. package/dist/formatting/StylesManager.d.ts.map +1 -1
  137. package/dist/formatting/StylesManager.js.map +1 -1
  138. package/dist/helpers/CleanupHelper.d.ts.map +1 -1
  139. package/dist/helpers/CleanupHelper.js +1 -7
  140. package/dist/helpers/CleanupHelper.js.map +1 -1
  141. package/dist/images/ImageOptimizer.js.map +1 -1
  142. package/dist/index.js.map +1 -1
  143. package/dist/managers/DrawingManager.d.ts.map +1 -1
  144. package/dist/managers/DrawingManager.js.map +1 -1
  145. package/dist/tracking/DocumentTrackingContext.js.map +1 -1
  146. package/dist/tracking/TrackingContext.js.map +1 -1
  147. package/dist/types/compatibility-types.js.map +1 -1
  148. package/dist/types/formatting.js.map +1 -1
  149. package/dist/types/list-types.d.ts +4 -4
  150. package/dist/types/list-types.d.ts.map +1 -1
  151. package/dist/types/list-types.js.map +1 -1
  152. package/dist/types/settings-types.js.map +1 -1
  153. package/dist/types/styleConfig.js.map +1 -1
  154. package/dist/utils/ChangelogGenerator.d.ts.map +1 -1
  155. package/dist/utils/ChangelogGenerator.js.map +1 -1
  156. package/dist/utils/CompatibilityUpgrader.d.ts.map +1 -1
  157. package/dist/utils/CompatibilityUpgrader.js +7 -7
  158. package/dist/utils/CompatibilityUpgrader.js.map +1 -1
  159. package/dist/utils/InMemoryRevisionAcceptor.js +1 -1
  160. package/dist/utils/InMemoryRevisionAcceptor.js.map +1 -1
  161. package/dist/utils/MoveOperationHelper.js.map +1 -1
  162. package/dist/utils/RevisionAwareProcessor.js.map +1 -1
  163. package/dist/utils/RevisionWalker.js.map +1 -1
  164. package/dist/utils/SelectiveRevisionAcceptor.js.map +1 -1
  165. package/dist/utils/ShadingResolver.js +1 -1
  166. package/dist/utils/ShadingResolver.js.map +1 -1
  167. package/dist/utils/acceptRevisions.d.ts +0 -28
  168. package/dist/utils/acceptRevisions.d.ts.map +1 -1
  169. package/dist/utils/acceptRevisions.js +5 -7
  170. package/dist/utils/acceptRevisions.js.map +1 -1
  171. package/dist/utils/cnfStyleDecoder.js +1 -1
  172. package/dist/utils/cnfStyleDecoder.js.map +1 -1
  173. package/dist/utils/corruptionDetection.js.map +1 -1
  174. package/dist/utils/dateFormatting.js.map +1 -1
  175. package/dist/utils/deepClone.d.ts +0 -1
  176. package/dist/utils/deepClone.d.ts.map +1 -1
  177. package/dist/utils/deepClone.js +0 -7
  178. package/dist/utils/deepClone.js.map +1 -1
  179. package/dist/utils/diagnostics.d.ts +2 -2
  180. package/dist/utils/diagnostics.d.ts.map +1 -1
  181. package/dist/utils/diagnostics.js.map +1 -1
  182. package/dist/utils/errorHandling.js.map +1 -1
  183. package/dist/utils/formatting.js.map +1 -1
  184. package/dist/utils/list-detection.d.ts +2 -2
  185. package/dist/utils/list-detection.d.ts.map +1 -1
  186. package/dist/utils/list-detection.js +3 -3
  187. package/dist/utils/list-detection.js.map +1 -1
  188. package/dist/utils/logger.d.ts +2 -4
  189. package/dist/utils/logger.d.ts.map +1 -1
  190. package/dist/utils/logger.js +0 -2
  191. package/dist/utils/logger.js.map +1 -1
  192. package/dist/utils/parsingHelpers.js.map +1 -1
  193. package/dist/utils/stripTrackedChanges.d.ts +0 -19
  194. package/dist/utils/stripTrackedChanges.d.ts.map +1 -1
  195. package/dist/utils/stripTrackedChanges.js +0 -2
  196. package/dist/utils/stripTrackedChanges.js.map +1 -1
  197. package/dist/utils/textDiff.js.map +1 -1
  198. package/dist/utils/units.js.map +1 -1
  199. package/dist/utils/validation.d.ts.map +1 -1
  200. package/dist/utils/validation.js.map +1 -1
  201. package/dist/utils/xmlSanitization.js.map +1 -1
  202. package/dist/validation/RevisionAutoFixer.js.map +1 -1
  203. package/dist/validation/RevisionValidator.js.map +1 -1
  204. package/dist/validation/ValidationRules.js.map +1 -1
  205. package/dist/validation/index.js.map +1 -1
  206. package/dist/xml/XMLBuilder.d.ts.map +1 -1
  207. package/dist/xml/XMLBuilder.js +10 -0
  208. package/dist/xml/XMLBuilder.js.map +1 -1
  209. package/dist/xml/XMLParser.d.ts.map +1 -1
  210. package/dist/xml/XMLParser.js +4 -5
  211. package/dist/xml/XMLParser.js.map +1 -1
  212. package/dist/zip/ZipHandler.js.map +1 -1
  213. package/dist/zip/ZipReader.js.map +1 -1
  214. package/dist/zip/ZipWriter.js.map +1 -1
  215. package/dist/zip/errors.js.map +1 -1
  216. package/dist/zip/types.js.map +1 -1
  217. package/package.json +34 -4
  218. package/src/__tests__/helper-methods.test.ts +512 -0
  219. package/src/constants/legacyCompatFlags.ts +138 -0
  220. package/src/constants/limits.ts +50 -0
  221. package/src/core/CLAUDE.md +109 -0
  222. package/src/core/Document.ts +15569 -0
  223. package/src/core/DocumentContent.ts +467 -0
  224. package/src/core/DocumentGenerator.ts +1104 -0
  225. package/src/core/DocumentIdManager.ts +158 -0
  226. package/src/core/DocumentParser.ts +10107 -0
  227. package/src/core/DocumentValidator.ts +372 -0
  228. package/src/core/Relationship.ts +367 -0
  229. package/src/core/RelationshipManager.ts +428 -0
  230. package/src/elements/AlternateContent.ts +42 -0
  231. package/src/elements/Bookmark.ts +210 -0
  232. package/src/elements/BookmarkManager.ts +250 -0
  233. package/src/elements/CLAUDE.md +126 -0
  234. package/src/elements/Comment.ts +359 -0
  235. package/src/elements/CommentManager.ts +502 -0
  236. package/src/elements/CommonTypes.ts +549 -0
  237. package/src/elements/CustomXml.ts +36 -0
  238. package/src/elements/Endnote.ts +217 -0
  239. package/src/elements/EndnoteManager.ts +249 -0
  240. package/src/elements/Field.ts +1233 -0
  241. package/src/elements/FieldHelpers.ts +333 -0
  242. package/src/elements/FontManager.ts +339 -0
  243. package/src/elements/Footer.ts +269 -0
  244. package/src/elements/Footnote.ts +217 -0
  245. package/src/elements/FootnoteManager.ts +249 -0
  246. package/src/elements/Header.ts +269 -0
  247. package/src/elements/HeaderFooterManager.ts +219 -0
  248. package/src/elements/Hyperlink.ts +1146 -0
  249. package/src/elements/Image.ts +1756 -0
  250. package/src/elements/ImageManager.ts +432 -0
  251. package/src/elements/ImageRun.ts +59 -0
  252. package/src/elements/MathElement.ts +65 -0
  253. package/src/elements/Paragraph.ts +4227 -0
  254. package/src/elements/PreservedElement.ts +53 -0
  255. package/src/elements/PropertyChangeTypes.ts +442 -0
  256. package/src/elements/RangeMarker.ts +400 -0
  257. package/src/elements/Revision.ts +1217 -0
  258. package/src/elements/RevisionContent.ts +73 -0
  259. package/src/elements/RevisionManager.ts +1070 -0
  260. package/src/elements/Run.ts +3068 -0
  261. package/src/elements/Section.ts +1421 -0
  262. package/src/elements/Shape.ts +873 -0
  263. package/src/elements/StructuredDocumentTag.ts +978 -0
  264. package/src/elements/Table.ts +2524 -0
  265. package/src/elements/TableCell.ts +1586 -0
  266. package/src/elements/TableGridChange.ts +151 -0
  267. package/src/elements/TableOfContents.ts +691 -0
  268. package/src/elements/TableOfContentsElement.ts +89 -0
  269. package/src/elements/TableRow.ts +906 -0
  270. package/src/elements/TextBox.ts +768 -0
  271. package/src/formatting/AbstractNumbering.ts +548 -0
  272. package/src/formatting/CLAUDE.md +74 -0
  273. package/src/formatting/NumberingInstance.ts +212 -0
  274. package/src/formatting/NumberingLevel.ts +1006 -0
  275. package/src/formatting/NumberingManager.ts +827 -0
  276. package/src/formatting/Style.ts +1833 -0
  277. package/src/formatting/StylesManager.ts +1005 -0
  278. package/src/helpers/CleanupHelper.ts +524 -0
  279. package/src/images/ImageOptimizer.ts +274 -0
  280. package/src/index.ts +554 -0
  281. package/src/managers/CLAUDE.md +47 -0
  282. package/src/managers/DrawingManager.ts +319 -0
  283. package/src/tracking/DocumentTrackingContext.ts +643 -0
  284. package/src/tracking/TrackingContext.ts +173 -0
  285. package/src/types/compatibility-types.ts +49 -0
  286. package/src/types/formatting.ts +210 -0
  287. package/src/types/list-types.ts +152 -0
  288. package/src/types/settings-types.ts +59 -0
  289. package/src/types/styleConfig.ts +189 -0
  290. package/src/utils/CLAUDE.md +153 -0
  291. package/src/utils/ChangelogGenerator.ts +1581 -0
  292. package/src/utils/CompatibilityUpgrader.ts +237 -0
  293. package/src/utils/InMemoryRevisionAcceptor.ts +668 -0
  294. package/src/utils/MoveOperationHelper.ts +238 -0
  295. package/src/utils/RevisionAwareProcessor.ts +526 -0
  296. package/src/utils/RevisionWalker.ts +457 -0
  297. package/src/utils/SelectiveRevisionAcceptor.ts +613 -0
  298. package/src/utils/ShadingResolver.ts +107 -0
  299. package/src/utils/acceptRevisions.ts +714 -0
  300. package/src/utils/cnfStyleDecoder.ts +217 -0
  301. package/src/utils/corruptionDetection.ts +345 -0
  302. package/src/utils/dateFormatting.ts +20 -0
  303. package/src/utils/deepClone.ts +78 -0
  304. package/src/utils/diagnostics.ts +129 -0
  305. package/src/utils/errorHandling.ts +80 -0
  306. package/src/utils/formatting.ts +213 -0
  307. package/src/utils/list-detection.ts +274 -0
  308. package/src/utils/logger.ts +404 -0
  309. package/src/utils/parsingHelpers.ts +190 -0
  310. package/src/utils/stripTrackedChanges.ts +353 -0
  311. package/src/utils/textDiff.ts +100 -0
  312. package/src/utils/units.ts +421 -0
  313. package/src/utils/validation.ts +542 -0
  314. package/src/utils/xmlSanitization.ts +182 -0
  315. package/src/validation/RevisionAutoFixer.ts +542 -0
  316. package/src/validation/RevisionValidator.ts +460 -0
  317. package/src/validation/ValidationRules.ts +338 -0
  318. package/src/validation/index.ts +30 -0
  319. package/src/xml/CLAUDE.md +65 -0
  320. package/src/xml/XMLBuilder.ts +871 -0
  321. package/src/xml/XMLParser.ts +919 -0
  322. package/src/zip/CLAUDE.md +55 -0
  323. package/src/zip/ZipHandler.ts +637 -0
  324. package/src/zip/ZipReader.ts +299 -0
  325. package/src/zip/ZipWriter.ts +390 -0
  326. package/src/zip/errors.ts +69 -0
  327. package/src/zip/types.ts +116 -0
  328. package/dist/core/ListNormalizer.d.ts +0 -23
  329. package/dist/core/ListNormalizer.d.ts.map +0 -1
  330. package/dist/core/ListNormalizer.js +0 -624
  331. package/dist/core/ListNormalizer.js.map +0 -1
  332. package/dist/images/index.d.ts +0 -2
  333. package/dist/images/index.d.ts.map +0 -1
  334. package/dist/images/index.js +0 -8
  335. package/dist/images/index.js.map +0 -1
  336. package/dist/ms-doc/cfb/CFBReader.d.ts +0 -35
  337. package/dist/ms-doc/cfb/CFBReader.d.ts.map +0 -1
  338. package/dist/ms-doc/cfb/CFBReader.js +0 -360
  339. package/dist/ms-doc/cfb/CFBReader.js.map +0 -1
  340. package/dist/ms-doc/converter/DocToDocxConverter.d.ts +0 -55
  341. package/dist/ms-doc/converter/DocToDocxConverter.d.ts.map +0 -1
  342. package/dist/ms-doc/converter/DocToDocxConverter.js +0 -324
  343. package/dist/ms-doc/converter/DocToDocxConverter.js.map +0 -1
  344. package/dist/ms-doc/fib/FIB.d.ts +0 -18
  345. package/dist/ms-doc/fib/FIB.d.ts.map +0 -1
  346. package/dist/ms-doc/fib/FIB.js +0 -342
  347. package/dist/ms-doc/fib/FIB.js.map +0 -1
  348. package/dist/ms-doc/fields/FieldParser.d.ts +0 -31
  349. package/dist/ms-doc/fields/FieldParser.d.ts.map +0 -1
  350. package/dist/ms-doc/fields/FieldParser.js +0 -266
  351. package/dist/ms-doc/fields/FieldParser.js.map +0 -1
  352. package/dist/ms-doc/images/PictureExtractor.d.ts +0 -22
  353. package/dist/ms-doc/images/PictureExtractor.d.ts.map +0 -1
  354. package/dist/ms-doc/images/PictureExtractor.js +0 -233
  355. package/dist/ms-doc/images/PictureExtractor.js.map +0 -1
  356. package/dist/ms-doc/index.d.ts +0 -20
  357. package/dist/ms-doc/index.d.ts.map +0 -1
  358. package/dist/ms-doc/index.js +0 -59
  359. package/dist/ms-doc/index.js.map +0 -1
  360. package/dist/ms-doc/properties/SPRM.d.ts +0 -210
  361. package/dist/ms-doc/properties/SPRM.d.ts.map +0 -1
  362. package/dist/ms-doc/properties/SPRM.js +0 -633
  363. package/dist/ms-doc/properties/SPRM.js.map +0 -1
  364. package/dist/ms-doc/sections/SectionParser.d.ts +0 -25
  365. package/dist/ms-doc/sections/SectionParser.d.ts.map +0 -1
  366. package/dist/ms-doc/sections/SectionParser.js +0 -214
  367. package/dist/ms-doc/sections/SectionParser.js.map +0 -1
  368. package/dist/ms-doc/styles/StyleSheet.d.ts +0 -23
  369. package/dist/ms-doc/styles/StyleSheet.d.ts.map +0 -1
  370. package/dist/ms-doc/styles/StyleSheet.js +0 -268
  371. package/dist/ms-doc/styles/StyleSheet.js.map +0 -1
  372. package/dist/ms-doc/subdocuments/SubdocumentParser.d.ts +0 -61
  373. package/dist/ms-doc/subdocuments/SubdocumentParser.d.ts.map +0 -1
  374. package/dist/ms-doc/subdocuments/SubdocumentParser.js +0 -208
  375. package/dist/ms-doc/subdocuments/SubdocumentParser.js.map +0 -1
  376. package/dist/ms-doc/tables/TableParser.d.ts +0 -29
  377. package/dist/ms-doc/tables/TableParser.d.ts.map +0 -1
  378. package/dist/ms-doc/tables/TableParser.js +0 -176
  379. package/dist/ms-doc/tables/TableParser.js.map +0 -1
  380. package/dist/ms-doc/text/PieceTable.d.ts +0 -21
  381. package/dist/ms-doc/text/PieceTable.d.ts.map +0 -1
  382. package/dist/ms-doc/text/PieceTable.js +0 -171
  383. package/dist/ms-doc/text/PieceTable.js.map +0 -1
  384. package/dist/ms-doc/types/Constants.d.ts +0 -99
  385. package/dist/ms-doc/types/Constants.d.ts.map +0 -1
  386. package/dist/ms-doc/types/Constants.js +0 -102
  387. package/dist/ms-doc/types/Constants.js.map +0 -1
  388. package/dist/ms-doc/types/DocTypes.d.ts +0 -368
  389. package/dist/ms-doc/types/DocTypes.d.ts.map +0 -1
  390. package/dist/ms-doc/types/DocTypes.js +0 -3
  391. package/dist/ms-doc/types/DocTypes.js.map +0 -1
  392. package/dist/tracking/index.d.ts +0 -3
  393. package/dist/tracking/index.d.ts.map +0 -1
  394. package/dist/tracking/index.js +0 -6
  395. package/dist/tracking/index.js.map +0 -1
@@ -0,0 +1,1756 @@
1
+ /**
2
+ * Image - Represents an embedded image in a Word document
3
+ *
4
+ * Images use DrawingML (a:) and WordprocessingML Drawing (wp:) namespaces
5
+ * for proper positioning and formatting in Word documents.
6
+ */
7
+
8
+ import { promises as fs } from 'fs';
9
+ import { defaultLogger } from '../utils/logger';
10
+ import { inchesToEmus, UNITS } from '../utils/units';
11
+ import { XMLBuilder, XMLElement } from '../xml/XMLBuilder';
12
+
13
+ /**
14
+ * Supported image formats
15
+ */
16
+ export type ImageFormat = 'png' | 'jpeg' | 'jpg' | 'gif' | 'bmp' | 'tiff' | 'svg' | 'emf' | 'wmf';
17
+
18
+ /**
19
+ * Preset geometry shape type (ECMA-376 §20.1.9.18)
20
+ */
21
+ export type PresetGeometry = 'rect' | 'roundRect' | 'ellipse' | string;
22
+
23
+ /**
24
+ * Blip compression state (ECMA-376 §20.1.8.15)
25
+ */
26
+ export type BlipCompressionState = 'none' | 'print' | 'email' | 'hqprint' | 'screen';
27
+
28
+ /**
29
+ * Picture lock attribute names (ECMA-376 §20.1.2.2.31)
30
+ */
31
+ export type PicLockAttribute = 'noChangeAspect' | 'noChangeArrowheads' | 'noSelect' | 'noMove'
32
+ | 'noResize' | 'noEditPoints' | 'noAdjustHandles' | 'noRot' | 'noChangeShapeType'
33
+ | 'noCrop' | 'noGrp';
34
+
35
+ /**
36
+ * Non-visual picture properties (ECMA-376 §19.3.1.12)
37
+ */
38
+ export interface PicNonVisualProperties {
39
+ id: string;
40
+ name: string;
41
+ descr: string;
42
+ }
43
+
44
+ /**
45
+ * Image border definition (full a:ln support per ECMA-376)
46
+ */
47
+ export interface ImageBorder {
48
+ /** Border width in points */
49
+ width: number;
50
+ /** Line cap style */
51
+ cap?: 'flat' | 'rnd' | 'sq';
52
+ /** Compound line type */
53
+ compound?: 'sng' | 'dbl' | 'thickThin' | 'thinThick' | 'tri';
54
+ /** Alignment relative to shape */
55
+ alignment?: 'ctr' | 'in';
56
+ /** Fill specification */
57
+ fill?: {
58
+ type: 'srgbClr' | 'schemeClr';
59
+ value: string;
60
+ modifiers?: { name: string; val: string }[];
61
+ };
62
+ /** Raw XML for non-solid fills (gradFill, pattFill, etc.) */
63
+ rawFillXml?: string;
64
+ /** Preset dash pattern */
65
+ dashPattern?: string;
66
+ /** Line join style */
67
+ join?: 'round' | 'bevel' | 'miter';
68
+ /** Miter limit (percentage * 1000) */
69
+ miterLimit?: number;
70
+ /** Head end decoration */
71
+ headEnd?: { type?: string; width?: string; length?: string };
72
+ /** Tail end decoration */
73
+ tailEnd?: { type?: string; width?: string; length?: string };
74
+ }
75
+
76
+ /**
77
+ * Image extent (dimensions)
78
+ */
79
+ export interface ImageExtent {
80
+ /** Width in EMUs */
81
+ width: number;
82
+ /** Height in EMUs */
83
+ height: number;
84
+ }
85
+
86
+ /**
87
+ * Effect extent (additional space for shadows, reflections, glows)
88
+ * Specifies additional space to add to each edge to prevent clipping of effects
89
+ */
90
+ export interface EffectExtent {
91
+ /** Left extent in EMUs */
92
+ left: number;
93
+ /** Top extent in EMUs */
94
+ top: number;
95
+ /** Right extent in EMUs */
96
+ right: number;
97
+ /** Bottom extent in EMUs */
98
+ bottom: number;
99
+ }
100
+
101
+ /**
102
+ * Text wrapping type
103
+ */
104
+ export type WrapType = 'square' | 'tight' | 'through' | 'topAndBottom' | 'none';
105
+
106
+ /**
107
+ * Text wrapping side
108
+ */
109
+ export type WrapSide = 'bothSides' | 'left' | 'right' | 'largest';
110
+
111
+ /**
112
+ * Text wrap settings
113
+ */
114
+ export interface TextWrapSettings {
115
+ /** Wrap type */
116
+ type: WrapType;
117
+ /** Which side(s) to wrap text */
118
+ side?: WrapSide;
119
+ /** Distance from top in EMUs */
120
+ distanceTop?: number;
121
+ /** Distance from bottom in EMUs */
122
+ distanceBottom?: number;
123
+ /** Distance from left in EMUs */
124
+ distanceLeft?: number;
125
+ /** Distance from right in EMUs */
126
+ distanceRight?: number;
127
+ }
128
+
129
+ /**
130
+ * Position anchor type (what to position relative to)
131
+ */
132
+ export type PositionAnchor = 'page' | 'margin' | 'column' | 'character' | 'paragraph';
133
+
134
+ /**
135
+ * Horizontal alignment options
136
+ */
137
+ export type HorizontalAlignment = 'left' | 'center' | 'right' | 'inside' | 'outside';
138
+
139
+ /**
140
+ * Vertical alignment options
141
+ */
142
+ export type VerticalAlignment = 'top' | 'center' | 'bottom' | 'inside' | 'outside';
143
+
144
+ /**
145
+ * Image position configuration
146
+ */
147
+ export interface ImagePosition {
148
+ /** Horizontal positioning */
149
+ horizontal: {
150
+ /** Anchor point */
151
+ anchor: PositionAnchor;
152
+ /** Offset in EMUs (absolute positioning) */
153
+ offset?: number;
154
+ /** Alignment (relative positioning) */
155
+ alignment?: HorizontalAlignment;
156
+ };
157
+ /** Vertical positioning */
158
+ vertical: {
159
+ /** Anchor point */
160
+ anchor: PositionAnchor;
161
+ /** Offset in EMUs (absolute positioning) */
162
+ offset?: number;
163
+ /** Alignment (relative positioning) */
164
+ alignment?: VerticalAlignment;
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Image anchor configuration (floating images)
170
+ */
171
+ export interface ImageAnchor {
172
+ /** Position behind text */
173
+ behindDoc: boolean;
174
+ /** Lock anchor (prevent movement) */
175
+ locked: boolean;
176
+ /** Layout in table cell */
177
+ layoutInCell: boolean;
178
+ /** Allow overlap with other objects */
179
+ allowOverlap: boolean;
180
+ /** Z-order (higher = in front) */
181
+ relativeHeight: number;
182
+ /** Use simple positioning (wp:simplePos coordinates) */
183
+ simplePos?: boolean;
184
+ /** Distance from text - top (EMUs) */
185
+ distT?: number;
186
+ /** Distance from text - bottom (EMUs) */
187
+ distB?: number;
188
+ /** Distance from text - left (EMUs) */
189
+ distL?: number;
190
+ /** Distance from text - right (EMUs) */
191
+ distR?: number;
192
+ }
193
+
194
+ /**
195
+ * Image crop settings (percentage-based)
196
+ */
197
+ export interface ImageCrop {
198
+ /** Left crop percentage (0-100) */
199
+ left: number;
200
+ /** Top crop percentage (0-100) */
201
+ top: number;
202
+ /** Right crop percentage (0-100) */
203
+ right: number;
204
+ /** Bottom crop percentage (0-100) */
205
+ bottom: number;
206
+ }
207
+
208
+ /**
209
+ * Image visual effects
210
+ */
211
+ export interface ImageEffects {
212
+ /** Brightness adjustment (-100 to +100) */
213
+ brightness?: number;
214
+ /** Contrast adjustment (-100 to +100) */
215
+ contrast?: number;
216
+ /** Convert to grayscale */
217
+ grayscale?: boolean;
218
+ /** Transparency (0-100, percentage) via a:alphaModFix */
219
+ transparency?: number;
220
+ }
221
+
222
+ /**
223
+ * Image properties
224
+ */
225
+ export interface ImageProperties {
226
+ /** Image source (file path or buffer) */
227
+ source: string | Buffer;
228
+ /** Image width in EMUs (optional - will auto-detect) */
229
+ width?: number;
230
+ /** Image height in EMUs (optional - will auto-detect) */
231
+ height?: number;
232
+ /** Maintain aspect ratio when resizing */
233
+ maintainAspectRatio?: boolean;
234
+ /** Alt text / description */
235
+ description?: string;
236
+ /** Image name/title */
237
+ name?: string;
238
+ /** Image title (wp:docPr title attribute for accessibility) */
239
+ title?: string;
240
+ /** Relationship ID (will be set by ImageManager) */
241
+ relationshipId?: string;
242
+ /** Effect extent (space for shadows/glows) */
243
+ effectExtent?: EffectExtent;
244
+ /** Text wrapping configuration */
245
+ wrap?: TextWrapSettings;
246
+ /** Position configuration (floating images) */
247
+ position?: ImagePosition;
248
+ /** Anchor configuration (floating images) */
249
+ anchor?: ImageAnchor;
250
+ /** Crop settings */
251
+ crop?: ImageCrop;
252
+ /** Visual effects */
253
+ effects?: ImageEffects;
254
+ /** Border settings */
255
+ border?: ImageBorder | { width: number };
256
+ /** Rotation angle in degrees (0-360) */
257
+ rotation?: number;
258
+ /** Horizontal flip (ECMA-376 §20.1.7.6) */
259
+ flipH?: boolean;
260
+ /** Vertical flip (ECMA-376 §20.1.7.6) */
261
+ flipV?: boolean;
262
+ /** Preset geometry shape (ECMA-376 §20.1.9.18) */
263
+ presetGeometry?: PresetGeometry;
264
+ /** Blip compression state (ECMA-376 §20.1.8.15) */
265
+ compressionState?: BlipCompressionState;
266
+ /** Black-and-white mode (ECMA-376 §20.1.2.2.35) */
267
+ bwMode?: string;
268
+ /** Inline distance from text - top (EMUs, ECMA-376 §20.4.2.8) */
269
+ inlineDistT?: number;
270
+ /** Inline distance from text - bottom (EMUs) */
271
+ inlineDistB?: number;
272
+ /** Inline distance from text - left (EMUs) */
273
+ inlineDistL?: number;
274
+ /** Inline distance from text - right (EMUs) */
275
+ inlineDistR?: number;
276
+ /** Whether aspect ratio lock is enabled (ECMA-376 §20.4.2.4) */
277
+ noChangeAspect?: boolean;
278
+ /** Hidden attribute on docPr (ECMA-376 §20.4.2.3) */
279
+ hidden?: boolean;
280
+ /** BlipFill DPI override (ECMA-376 §20.1.8.14) */
281
+ blipFillDpi?: number;
282
+ /** BlipFill rotate with shape flag (ECMA-376 §20.1.8.14) */
283
+ blipFillRotWithShape?: boolean;
284
+ /** Picture locks (ECMA-376 §20.1.2.2.31) */
285
+ picLocks?: Partial<Record<PicLockAttribute, boolean>>;
286
+ /** Non-visual picture properties (ECMA-376 §19.3.1.12) */
287
+ picNonVisualProps?: PicNonVisualProperties;
288
+ /** Whether image is linked (r:link) vs embedded (r:embed) */
289
+ isLinked?: boolean;
290
+ /** SVG relationship ID for Word 365 dual-relationship approach */
291
+ svgRelationshipId?: string;
292
+ }
293
+
294
+ /**
295
+ * Image validation result
296
+ */
297
+ export interface ValidationResult {
298
+ valid: boolean;
299
+ error?: string;
300
+ }
301
+
302
+ export class Image {
303
+ private source: string | Buffer;
304
+ private width: number;
305
+ private height: number;
306
+ private description: string;
307
+ private name: string;
308
+ private title?: string;
309
+ private relationshipId?: string;
310
+ private imageData?: Buffer;
311
+ private extension: string;
312
+ private docPrId = 1;
313
+ private dpi = 96; // Default DPI
314
+
315
+ // Advanced image properties
316
+ private effectExtent?: EffectExtent;
317
+ private wrap?: TextWrapSettings;
318
+ private position?: ImagePosition;
319
+ private anchor?: ImageAnchor;
320
+ private crop?: ImageCrop;
321
+ private effects?: ImageEffects;
322
+ private rotation = 0;
323
+ private flipH = false;
324
+ private flipV = false;
325
+ private border?: ImageBorder;
326
+
327
+ // Group A: Simple attribute preservation (ECMA-376 compliance)
328
+ private presetGeometry: PresetGeometry = 'rect';
329
+ private compressionState: BlipCompressionState = 'none';
330
+ private bwMode = 'auto';
331
+ private inlineDistT = 0;
332
+ private inlineDistB = 0;
333
+ private inlineDistL = 0;
334
+ private inlineDistR = 0;
335
+ private noChangeAspect = true;
336
+ private hidden = false;
337
+ private blipFillDpi?: number;
338
+ private blipFillRotWithShape?: boolean;
339
+ private picLocks: Partial<Record<PicLockAttribute, boolean>> = {
340
+ noChangeAspect: true,
341
+ noChangeArrowheads: true,
342
+ };
343
+ private picNonVisualProps: PicNonVisualProperties = { id: '0', name: '', descr: '' };
344
+ private isLinked = false;
345
+ private svgRelationshipId?: string;
346
+
347
+ // Group B: Raw XML passthrough for complex subtrees
348
+ private _rawPassthrough = new Map<string, string>();
349
+
350
+ /**
351
+ * Creates a new image from file path (async factory)
352
+ * @param path File path
353
+ * @param properties Additional properties
354
+ * @returns Promise<Image>
355
+ */
356
+ static async fromFile(path: string, properties: Partial<ImageProperties> = {}): Promise<Image> {
357
+ const image = new Image({ source: path, ...properties });
358
+ await image.loadImageDataForDimensions();
359
+ return image;
360
+ }
361
+
362
+ /**
363
+ * Creates a new image from buffer (async factory)
364
+ * Supports both modern and legacy API signatures
365
+ *
366
+ * @param buffer Image buffer
367
+ * @param mimeTypeOrProperties MIME type string ('png', 'jpeg', etc.) or properties object
368
+ * @param width Optional width in EMUs (legacy API)
369
+ * @param height Optional height in EMUs (legacy API)
370
+ * @returns Promise<Image>
371
+ *
372
+ * @example
373
+ * // Modern API (recommended)
374
+ * const img = await Image.fromBuffer(buffer, { mimeType: 'png', width: 914400, height: 914400 });
375
+ *
376
+ * // Legacy API (still supported)
377
+ * const img = await Image.fromBuffer(buffer, 'png', 914400, 914400);
378
+ */
379
+ static async fromBuffer(
380
+ buffer: Buffer,
381
+ mimeTypeOrProperties?: string | Partial<ImageProperties>,
382
+ width?: number,
383
+ height?: number
384
+ ): Promise<Image> {
385
+ let properties: Partial<ImageProperties>;
386
+
387
+ // Detect API signature
388
+ if (typeof mimeTypeOrProperties === 'string') {
389
+ // Legacy 4-parameter signature: fromBuffer(buffer, 'png', 914400, 914400)
390
+ // Note: mimeType is ignored - extension is auto-detected from buffer
391
+ properties = {
392
+ width: width,
393
+ height: height
394
+ };
395
+ } else {
396
+ // Modern API: fromBuffer(buffer, { width: 914400, height: 914400 })
397
+ properties = mimeTypeOrProperties || {};
398
+ }
399
+
400
+ const image = new Image({ source: buffer, ...properties });
401
+ await image.loadImageDataForDimensions();
402
+ return image;
403
+ }
404
+
405
+ /**
406
+ * Unified create method for images (async factory)
407
+ * @param properties Image properties including source (path or buffer)
408
+ * @returns Promise<Image>
409
+ */
410
+ static async create(properties: ImageProperties): Promise<Image> {
411
+ if (Buffer.isBuffer(properties.source)) {
412
+ return Image.fromBuffer(properties.source, properties);
413
+ } else if (typeof properties.source === 'string') {
414
+ return Image.fromFile(properties.source, properties);
415
+ } else {
416
+ throw new Error('Invalid source: must be file path or Buffer');
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Private constructor
422
+ * @param properties Image properties
423
+ */
424
+ private constructor(properties: ImageProperties) {
425
+ this.source = properties.source;
426
+ this.description = properties.description || 'Image';
427
+ this.name = properties.name || 'image';
428
+ this.title = properties.title;
429
+ this.relationshipId = properties.relationshipId;
430
+
431
+ // Detect image extension
432
+ this.extension = this.detectExtension();
433
+
434
+ // Set default dimensions (6 inches x 4 inches) if not provided
435
+ this.width = properties.width || inchesToEmus(6);
436
+ this.height = properties.height || inchesToEmus(4);
437
+
438
+ // Initialize advanced properties
439
+ this.effectExtent = properties.effectExtent;
440
+ this.wrap = properties.wrap;
441
+ this.position = properties.position;
442
+ this.anchor = properties.anchor;
443
+ this.crop = properties.crop;
444
+ this.effects = properties.effects;
445
+ // Border: accept both legacy { width } and full ImageBorder
446
+ if (properties.border) {
447
+ this.border = properties.border as ImageBorder;
448
+ }
449
+ // Apply rotation if provided (normalize to 0-360)
450
+ if (properties.rotation !== undefined && properties.rotation !== 0) {
451
+ this.rotation = ((properties.rotation % 360) + 360) % 360;
452
+ }
453
+ // Apply flip attributes (ECMA-376 §20.1.7.6)
454
+ this.flipH = properties.flipH || false;
455
+ this.flipV = properties.flipV || false;
456
+
457
+ // Group A: Simple attribute preservation
458
+ if (properties.presetGeometry !== undefined) this.presetGeometry = properties.presetGeometry;
459
+ if (properties.compressionState !== undefined) this.compressionState = properties.compressionState;
460
+ if (properties.bwMode !== undefined) this.bwMode = properties.bwMode;
461
+ if (properties.inlineDistT !== undefined) this.inlineDistT = properties.inlineDistT;
462
+ if (properties.inlineDistB !== undefined) this.inlineDistB = properties.inlineDistB;
463
+ if (properties.inlineDistL !== undefined) this.inlineDistL = properties.inlineDistL;
464
+ if (properties.inlineDistR !== undefined) this.inlineDistR = properties.inlineDistR;
465
+ if (properties.noChangeAspect !== undefined) this.noChangeAspect = properties.noChangeAspect;
466
+ if (properties.hidden !== undefined) this.hidden = properties.hidden;
467
+ if (properties.blipFillDpi !== undefined) this.blipFillDpi = properties.blipFillDpi;
468
+ if (properties.blipFillRotWithShape !== undefined) this.blipFillRotWithShape = properties.blipFillRotWithShape;
469
+ if (properties.picLocks !== undefined) this.picLocks = properties.picLocks;
470
+ if (properties.picNonVisualProps !== undefined) this.picNonVisualProps = properties.picNonVisualProps;
471
+ if (properties.isLinked !== undefined) this.isLinked = properties.isLinked;
472
+ if (properties.svgRelationshipId !== undefined) this.svgRelationshipId = properties.svgRelationshipId;
473
+
474
+ // Set default DPI
475
+ this.dpi = 96;
476
+ }
477
+
478
+ /**
479
+ * Loads image data temporarily for dimension detection only
480
+ * Data is released after detection to save memory
481
+ * @private
482
+ */
483
+ private async loadImageDataForDimensions(): Promise<void> {
484
+ let tempData: Buffer | undefined;
485
+
486
+ try {
487
+ if (Buffer.isBuffer(this.source)) {
488
+ tempData = this.source;
489
+ } else if (typeof this.source === 'string') {
490
+ await fs.access(this.source);
491
+ tempData = await fs.readFile(this.source);
492
+ }
493
+
494
+ if (tempData) {
495
+ this.imageData = tempData; // Temporarily store
496
+
497
+ // Only auto-detect dimensions if they weren't explicitly provided
498
+ // This preserves wp:extent values from parsed documents
499
+ const defaultWidth = inchesToEmus(6);
500
+ const defaultHeight = inchesToEmus(4);
501
+ const hasExplicitDimensions = this.width !== defaultWidth || this.height !== defaultHeight;
502
+
503
+ if (!hasExplicitDimensions) {
504
+ const dimensions = this.detectDimensions();
505
+ if (dimensions) {
506
+ this.dpi = this.detectDPI() || 96;
507
+ const emuPerInch = 914400;
508
+ const pixelsPerInch = this.dpi;
509
+ this.width = Math.round((dimensions.width / pixelsPerInch) * emuPerInch);
510
+ this.height = Math.round((dimensions.height / pixelsPerInch) * emuPerInch);
511
+ }
512
+ }
513
+
514
+ if (typeof this.source === 'string') {
515
+ this.imageData = undefined; // Release
516
+ }
517
+ }
518
+ } catch (error: unknown) {
519
+ const message = error instanceof Error ? error.message : String(error);
520
+ defaultLogger.error(`Failed to load image for dimensions: ${message}`);
521
+ throw new Error(`Image loading failed: ${message}`);
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Ensures image data is loaded (lazy loading)
527
+ */
528
+ async ensureDataLoaded(): Promise<void> {
529
+ if (this.imageData) return;
530
+
531
+ try {
532
+ if (Buffer.isBuffer(this.source)) {
533
+ this.imageData = this.source;
534
+ } else if (typeof this.source === 'string') {
535
+ await fs.access(this.source);
536
+ this.imageData = await fs.readFile(this.source);
537
+ } else {
538
+ throw new Error('Invalid image source');
539
+ }
540
+ } catch (error: unknown) {
541
+ const message = error instanceof Error ? error.message : String(error);
542
+ defaultLogger.error(`Failed to load image data: ${message}`);
543
+ throw new Error(`Image data loading failed: ${message}`);
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Releases image data from memory
549
+ */
550
+ releaseData(): void {
551
+ if (typeof this.source === 'string') {
552
+ this.imageData = undefined;
553
+ }
554
+ }
555
+
556
+ /**
557
+ * Validates the image data integrity
558
+ */
559
+ validateImageData(): ValidationResult {
560
+ // Skip validation for linked images (no data in package)
561
+ if (this.isLinked) {
562
+ return { valid: true };
563
+ }
564
+
565
+ if (!this.imageData || this.imageData.length === 0) {
566
+ return { valid: false, error: 'Empty image data' };
567
+ }
568
+
569
+ // Skip signature validation for text-based formats (SVG)
570
+ if (this.extension === 'svg') {
571
+ return { valid: true };
572
+ }
573
+
574
+ const signatures: Record<string, number[]> = {
575
+ png: [0x89, 0x50, 0x4E, 0x47],
576
+ jpg: [0xFF, 0xD8],
577
+ jpeg: [0xFF, 0xD8],
578
+ gif: [0x47, 0x49, 0x46],
579
+ bmp: [0x42, 0x4D],
580
+ tiff: [0x49, 0x49, 0x2A, 0x00],
581
+ tif: [0x49, 0x49, 0x2A, 0x00]
582
+ };
583
+
584
+ // EMF: check for ENHMETAHEADER signature at offset 40
585
+ if (this.extension === 'emf') {
586
+ if (this.imageData.length >= 44 &&
587
+ this.imageData[40] === 0x20 && this.imageData[41] === 0x45 &&
588
+ this.imageData[42] === 0x4D && this.imageData[43] === 0x46) {
589
+ return { valid: true };
590
+ }
591
+ return { valid: false, error: 'Invalid EMF signature' };
592
+ }
593
+
594
+ // WMF: check for placeable or standard header
595
+ if (this.extension === 'wmf') {
596
+ if (this.imageData.length >= 4) {
597
+ // Placeable WMF
598
+ if (this.imageData[0] === 0xD7 && this.imageData[1] === 0xCD &&
599
+ this.imageData[2] === 0xC6 && this.imageData[3] === 0x9A) {
600
+ return { valid: true };
601
+ }
602
+ // Standard WMF
603
+ if (this.imageData[0] === 0x01 && this.imageData[1] === 0x00 &&
604
+ this.imageData[2] === 0x09 && this.imageData[3] === 0x00) {
605
+ return { valid: true };
606
+ }
607
+ }
608
+ return { valid: false, error: 'Invalid WMF signature' };
609
+ }
610
+
611
+ const sig = signatures[this.extension];
612
+ if (sig) {
613
+ for (let i = 0; i < sig.length; i++) {
614
+ if (this.imageData[i] !== sig[i]) {
615
+ return { valid: false, error: `Invalid ${this.extension.toUpperCase()} signature` };
616
+ }
617
+ }
618
+ }
619
+
620
+ return { valid: true };
621
+ }
622
+
623
+ /**
624
+ * Detects image extension from source (path or buffer)
625
+ */
626
+ private detectExtension(): string {
627
+ // Try path-based detection first
628
+ if (typeof this.source === 'string') {
629
+ const match = /\.([a-z]+)$/i.exec(this.source);
630
+ if (match?.[1]) {
631
+ return match[1].toLowerCase();
632
+ }
633
+ }
634
+
635
+ // Buffer-based detection using magic bytes
636
+ if (Buffer.isBuffer(this.source) && this.source.length >= 4) {
637
+ const buf = this.source;
638
+ // PNG
639
+ if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4E && buf[3] === 0x47) return 'png';
640
+ // JPEG
641
+ if (buf[0] === 0xFF && buf[1] === 0xD8) return 'jpeg';
642
+ // GIF
643
+ if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) return 'gif';
644
+ // BMP
645
+ if (buf[0] === 0x42 && buf[1] === 0x4D) return 'bmp';
646
+ // TIFF LE
647
+ if (buf[0] === 0x49 && buf[1] === 0x49 && buf[2] === 0x2A && buf[3] === 0x00) return 'tiff';
648
+ // TIFF BE
649
+ if (buf[0] === 0x4D && buf[1] === 0x4D && buf[2] === 0x00 && buf[3] === 0x2A) return 'tiff';
650
+ // EMF: byte 0 = 0x01,0x00,0x00,0x00 AND ' EMF' at offset 40
651
+ if (buf.length >= 44 && buf[0] === 0x01 && buf[1] === 0x00 && buf[2] === 0x00 && buf[3] === 0x00 &&
652
+ buf[40] === 0x20 && buf[41] === 0x45 && buf[42] === 0x4D && buf[43] === 0x46) return 'emf';
653
+ // WMF placeable
654
+ if (buf[0] === 0xD7 && buf[1] === 0xCD && buf[2] === 0xC6 && buf[3] === 0x9A) return 'wmf';
655
+ // WMF standard
656
+ if (buf[0] === 0x01 && buf[1] === 0x00 && buf[2] === 0x09 && buf[3] === 0x00) return 'wmf';
657
+ // SVG: starts with '<' or UTF-8 BOM + '<'
658
+ if (buf[0] === 0x3C || (buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF && buf.length > 3 && buf[3] === 0x3C)) return 'svg';
659
+ }
660
+
661
+ return 'png';
662
+ }
663
+
664
+ /**
665
+ * Attempts to detect image dimensions from buffer
666
+ */
667
+ private detectDimensions(): { width: number; height: number } | null {
668
+ if (!this.imageData || this.imageData.length < 24) return null;
669
+
670
+ if (this.imageData[0] === 0x89 && this.imageData[1] === 0x50 && this.imageData[2] === 0x4e && this.imageData[3] === 0x47) {
671
+ return this.detectPngDimensions();
672
+ }
673
+ if (this.imageData[0] === 0xff && this.imageData[1] === 0xd8) {
674
+ return this.detectJpegDimensions();
675
+ }
676
+ if (this.imageData[0] === 0x47 && this.imageData[1] === 0x49 && this.imageData[2] === 0x46) {
677
+ return this.detectGifDimensions();
678
+ }
679
+ if (this.imageData[0] === 0x42 && this.imageData[1] === 0x4d) {
680
+ return this.detectBmpDimensions();
681
+ }
682
+ if ((this.imageData[0] === 0x49 && this.imageData[1] === 0x49 && this.imageData[2] === 0x2a) ||
683
+ (this.imageData[0] === 0x4d && this.imageData[1] === 0x4d && this.imageData[2] === 0x00)) {
684
+ return this.detectTiffDimensions();
685
+ }
686
+ // EMF: ENHMETAHEADER has ' EMF' at offset 40
687
+ if (this.imageData.length >= 44 &&
688
+ this.imageData[40] === 0x20 && this.imageData[41] === 0x45 &&
689
+ this.imageData[42] === 0x4D && this.imageData[43] === 0x46) {
690
+ return this.detectEmfDimensions();
691
+ }
692
+ // WMF placeable
693
+ if (this.imageData[0] === 0xD7 && this.imageData[1] === 0xCD &&
694
+ this.imageData[2] === 0xC6 && this.imageData[3] === 0x9A) {
695
+ return this.detectWmfDimensions();
696
+ }
697
+ // SVG (text-based)
698
+ if (this.imageData[0] === 0x3C ||
699
+ (this.imageData[0] === 0xEF && this.imageData[1] === 0xBB && this.imageData[2] === 0xBF)) {
700
+ return this.detectSvgDimensions();
701
+ }
702
+ return null;
703
+ }
704
+
705
+ // Dimension detection helpers (as before, keeping them the same)
706
+
707
+ private detectPngDimensions(): { width: number; height: number } | null {
708
+ if (!this.imageData || this.imageData.length < 24) return null;
709
+ const width = this.imageData.readUInt32BE(16);
710
+ const height = this.imageData.readUInt32BE(20);
711
+ return { width, height };
712
+ }
713
+
714
+ private detectGifDimensions(): { width: number; height: number } | null {
715
+ if (!this.imageData || this.imageData.length < 10) return null;
716
+ const width = this.imageData.readUInt16LE(6);
717
+ const height = this.imageData.readUInt16LE(8);
718
+ if (width > 0 && height > 0) return { width, height };
719
+ return null;
720
+ }
721
+
722
+ private detectBmpDimensions(): { width: number; height: number } | null {
723
+ if (!this.imageData || this.imageData.length < 26) return null;
724
+ const width = this.imageData.readInt32LE(18);
725
+ const height = Math.abs(this.imageData.readInt32LE(22));
726
+ if (width > 0 && height > 0) return { width, height };
727
+ return null;
728
+ }
729
+
730
+ private detectTiffDimensions(): { width: number; height: number } | null {
731
+ // Implementation as before
732
+ if (!this.imageData || this.imageData.length < 14) return null;
733
+ const isLittleEndian = this.imageData[0] === 0x49;
734
+ const ifdOffset = isLittleEndian ? this.imageData.readUInt32LE(4) : this.imageData.readUInt32BE(4);
735
+ if (ifdOffset + 14 > this.imageData.length) return null;
736
+ const numEntries = isLittleEndian ? this.imageData.readUInt16LE(ifdOffset) : this.imageData.readUInt16BE(ifdOffset);
737
+ let width = 0;
738
+ let height = 0;
739
+ for (let i = 0; i < numEntries; i++) {
740
+ const entryOffset = ifdOffset + 2 + i * 12;
741
+ if (entryOffset + 12 > this.imageData.length) break;
742
+ const tag = isLittleEndian ? this.imageData.readUInt16LE(entryOffset) : this.imageData.readUInt16BE(entryOffset);
743
+ const value = isLittleEndian ? this.imageData.readUInt32LE(entryOffset + 8) : this.imageData.readUInt32BE(entryOffset + 8);
744
+ if (tag === 256) width = value;
745
+ if (tag === 257) height = value;
746
+ if (width > 0 && height > 0) break;
747
+ }
748
+ if (width > 0 && height > 0) return { width, height };
749
+ return null;
750
+ }
751
+
752
+ private detectJpegDimensions(): { width: number; height: number } | null {
753
+ // Implementation as before
754
+ if (!this.imageData || this.imageData.length < 12) return null;
755
+ let offset = 2;
756
+ while (offset < this.imageData.length - 1) {
757
+ if (this.imageData[offset] !== 0xff) break;
758
+ const marker = this.imageData[offset + 1];
759
+ if (marker === undefined) break;
760
+ if (marker === 0x00 || marker === 0xff) {
761
+ offset++;
762
+ continue;
763
+ }
764
+ const isSOF = (marker >= 0xc0 && marker <= 0xcf) && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
765
+ if (isSOF) {
766
+ if (offset + 9 > this.imageData.length) break;
767
+ const height = this.imageData.readUInt16BE(offset + 5);
768
+ const width = this.imageData.readUInt16BE(offset + 7);
769
+ if (width > 0 && height > 0) return { width, height };
770
+ }
771
+ if (marker === 0xda || marker === 0xd9) break;
772
+ const segmentLength = this.imageData.readUInt16BE(offset + 2);
773
+ if (segmentLength < 2 || offset + 2 + segmentLength > this.imageData.length) break;
774
+ offset += 2 + segmentLength;
775
+ }
776
+ return null;
777
+ }
778
+
779
+ /**
780
+ * Detects SVG dimensions from width/height attributes or viewBox
781
+ */
782
+ private detectSvgDimensions(): { width: number; height: number } | null {
783
+ if (!this.imageData) return null;
784
+ try {
785
+ const svgText = this.imageData.toString('utf-8').substring(0, 2000);
786
+ // Try width/height attributes on <svg> element
787
+ const widthMatch = /<svg[^>]*\bwidth\s*=\s*["']?(\d+(?:\.\d+)?)/i.exec(svgText);
788
+ const heightMatch = /<svg[^>]*\bheight\s*=\s*["']?(\d+(?:\.\d+)?)/i.exec(svgText);
789
+ if (widthMatch?.[1] && heightMatch?.[1]) {
790
+ return { width: Math.round(parseFloat(widthMatch[1])), height: Math.round(parseFloat(heightMatch[1])) };
791
+ }
792
+ // Try viewBox attribute
793
+ const viewBoxMatch = /<svg[^>]*\bviewBox\s*=\s*["']?\s*[\d.]+\s+[\d.]+\s+([\d.]+)\s+([\d.]+)/i.exec(svgText);
794
+ if (viewBoxMatch?.[1] && viewBoxMatch?.[2]) {
795
+ return { width: Math.round(parseFloat(viewBoxMatch[1])), height: Math.round(parseFloat(viewBoxMatch[2])) };
796
+ }
797
+ } catch {
798
+ // SVG parsing failed
799
+ }
800
+ return null;
801
+ }
802
+
803
+ /**
804
+ * Detects EMF dimensions from ENHMETAHEADER rclFrame (offsets 24-36, 0.01mm units)
805
+ */
806
+ private detectEmfDimensions(): { width: number; height: number } | null {
807
+ if (!this.imageData || this.imageData.length < 40) return null;
808
+ try {
809
+ // rclFrame: left(24), top(28), right(32), bottom(36) in 0.01mm units
810
+ const left = this.imageData.readInt32LE(24);
811
+ const top = this.imageData.readInt32LE(28);
812
+ const right = this.imageData.readInt32LE(32);
813
+ const bottom = this.imageData.readInt32LE(36);
814
+ const widthMm = (right - left) / 100;
815
+ const heightMm = (bottom - top) / 100;
816
+ // Convert mm to pixels at 96 DPI (1 inch = 25.4mm)
817
+ const width = Math.round((widthMm / 25.4) * 96);
818
+ const height = Math.round((heightMm / 25.4) * 96);
819
+ if (width > 0 && height > 0) return { width, height };
820
+ } catch {
821
+ // EMF header parsing failed
822
+ }
823
+ return null;
824
+ }
825
+
826
+ /**
827
+ * Detects WMF dimensions from placeable WMF header bounding box (offsets 6-14)
828
+ */
829
+ private detectWmfDimensions(): { width: number; height: number } | null {
830
+ if (!this.imageData || this.imageData.length < 22) return null;
831
+ try {
832
+ // Placeable WMF header: left(6), top(8), right(10), bottom(12), inch(14)
833
+ const left = this.imageData.readInt16LE(6);
834
+ const top = this.imageData.readInt16LE(8);
835
+ const right = this.imageData.readInt16LE(10);
836
+ const bottom = this.imageData.readInt16LE(12);
837
+ const unitsPerInch = this.imageData.readUInt16LE(14);
838
+ if (unitsPerInch > 0) {
839
+ const width = Math.round(((right - left) / unitsPerInch) * 96);
840
+ const height = Math.round(((bottom - top) / unitsPerInch) * 96);
841
+ if (width > 0 && height > 0) return { width, height };
842
+ }
843
+ } catch {
844
+ // WMF header parsing failed
845
+ }
846
+ return null;
847
+ }
848
+
849
+ /**
850
+ * Gets the image data buffer asynchronously
851
+ */
852
+ async getImageDataAsync(): Promise<Buffer> {
853
+ await this.ensureDataLoaded();
854
+ if (!this.imageData) throw new Error('Failed to load image data');
855
+ return this.imageData;
856
+ }
857
+
858
+ /**
859
+ * Gets the image data buffer synchronously
860
+ */
861
+ getImageData(): Buffer {
862
+ if (!this.imageData) throw new Error('Image data not loaded. Call ensureDataLoaded first.');
863
+ return this.imageData;
864
+ }
865
+
866
+ getExtension(): string {
867
+ return this.extension;
868
+ }
869
+
870
+ getDPI(): number {
871
+ return this.dpi;
872
+ }
873
+
874
+ getWidth(): number {
875
+ return this.width;
876
+ }
877
+
878
+ getHeight(): number {
879
+ return this.height;
880
+ }
881
+
882
+ getImageDataSafe(): Buffer | null {
883
+ return this.imageData ?? null;
884
+ }
885
+
886
+ setWidth(width: number, maintainAspectRatio = true): this {
887
+ if (maintainAspectRatio && this.height > 0) {
888
+ const ratio = this.height / this.width;
889
+ this.height = Math.round(width * ratio);
890
+ }
891
+ this.width = width;
892
+ return this;
893
+ }
894
+
895
+ setHeight(height: number, maintainAspectRatio = true): this {
896
+ if (maintainAspectRatio && this.width > 0) {
897
+ const ratio = this.width / this.height;
898
+ this.width = Math.round(height * ratio);
899
+ }
900
+ this.height = height;
901
+ return this;
902
+ }
903
+
904
+ setSize(width: number, height: number): this {
905
+ this.width = width;
906
+ this.height = height;
907
+ return this;
908
+ }
909
+
910
+ async updateImageData(newSource: string | Buffer): Promise<void> {
911
+ this.source = newSource;
912
+ this.imageData = undefined;
913
+ await this.loadImageDataForDimensions();
914
+ this.extension = this.detectExtension();
915
+ this.dpi = this.detectDPI() || 96;
916
+ }
917
+
918
+ setRelationshipId(relationshipId: string): this {
919
+ this.relationshipId = relationshipId;
920
+ return this;
921
+ }
922
+
923
+ getRelationshipId(): string | undefined {
924
+ return this.relationshipId;
925
+ }
926
+
927
+ setDocPrId(id: number): this {
928
+ this.docPrId = id;
929
+ return this;
930
+ }
931
+
932
+ setAltText(altText: string): this {
933
+ this.description = altText;
934
+ return this;
935
+ }
936
+
937
+ getAltText(): string {
938
+ return this.description;
939
+ }
940
+
941
+ setTitle(title: string): this {
942
+ this.title = title;
943
+ return this;
944
+ }
945
+
946
+ getTitle(): string | undefined {
947
+ return this.title;
948
+ }
949
+
950
+ rotate(degrees: number): this {
951
+ this.rotation = ((degrees % 360) + 360) % 360;
952
+ if (this.rotation === 90 || this.rotation === 270) {
953
+ [this.width, this.height] = [this.height, this.width];
954
+ }
955
+ return this;
956
+ }
957
+
958
+ getRotation(): number {
959
+ return this.rotation;
960
+ }
961
+
962
+ setFlipH(flip: boolean): this {
963
+ this.flipH = flip;
964
+ return this;
965
+ }
966
+
967
+ getFlipH(): boolean {
968
+ return this.flipH;
969
+ }
970
+
971
+ setFlipV(flip: boolean): this {
972
+ this.flipV = flip;
973
+ return this;
974
+ }
975
+
976
+ getFlipV(): boolean {
977
+ return this.flipV;
978
+ }
979
+
980
+ // --- Group A: Simple attribute getters/setters ---
981
+
982
+ getPresetGeometry(): PresetGeometry { return this.presetGeometry; }
983
+ setPresetGeometry(geom: PresetGeometry): this { this.presetGeometry = geom; return this; }
984
+
985
+ getCompressionState(): BlipCompressionState { return this.compressionState; }
986
+ setCompressionState(state: BlipCompressionState): this { this.compressionState = state; return this; }
987
+
988
+ getBwMode(): string { return this.bwMode; }
989
+ setBwMode(mode: string): this { this.bwMode = mode; return this; }
990
+
991
+ getInlineDistT(): number { return this.inlineDistT; }
992
+ getInlineDistB(): number { return this.inlineDistB; }
993
+ getInlineDistL(): number { return this.inlineDistL; }
994
+ getInlineDistR(): number { return this.inlineDistR; }
995
+ setInlineDist(distT: number, distB: number, distL: number, distR: number): this {
996
+ this.inlineDistT = distT;
997
+ this.inlineDistB = distB;
998
+ this.inlineDistL = distL;
999
+ this.inlineDistR = distR;
1000
+ return this;
1001
+ }
1002
+
1003
+ getNoChangeAspect(): boolean { return this.noChangeAspect; }
1004
+ setNoChangeAspect(val: boolean): this { this.noChangeAspect = val; return this; }
1005
+
1006
+ getHidden(): boolean { return this.hidden; }
1007
+ setHidden(val: boolean): this { this.hidden = val; return this; }
1008
+
1009
+ getBlipFillDpi(): number | undefined { return this.blipFillDpi; }
1010
+ setBlipFillDpi(dpi: number | undefined): this { this.blipFillDpi = dpi; return this; }
1011
+
1012
+ getBlipFillRotWithShape(): boolean | undefined { return this.blipFillRotWithShape; }
1013
+ setBlipFillRotWithShape(val: boolean | undefined): this { this.blipFillRotWithShape = val; return this; }
1014
+
1015
+ getPicLocks(): Partial<Record<PicLockAttribute, boolean>> { return { ...this.picLocks }; }
1016
+ setPicLocks(locks: Partial<Record<PicLockAttribute, boolean>>): this { this.picLocks = locks; return this; }
1017
+
1018
+ getPicNonVisualProps(): PicNonVisualProperties { return { ...this.picNonVisualProps }; }
1019
+ setPicNonVisualProps(props: PicNonVisualProperties): this { this.picNonVisualProps = props; return this; }
1020
+
1021
+ getIsLinked(): boolean { return this.isLinked; }
1022
+ setIsLinked(val: boolean): this { this.isLinked = val; return this; }
1023
+
1024
+ getSvgRelationshipId(): string | undefined { return this.svgRelationshipId; }
1025
+ setSvgRelationshipId(id: string | undefined): this { this.svgRelationshipId = id; return this; }
1026
+
1027
+ // --- Group B: Raw passthrough storage ---
1028
+
1029
+ /** @internal */
1030
+ _setRawPassthrough(slot: string, xml: string): void {
1031
+ this._rawPassthrough.set(slot, xml);
1032
+ }
1033
+
1034
+ /** @internal */
1035
+ _getRawPassthrough(slot: string): string | undefined {
1036
+ return this._rawPassthrough.get(slot);
1037
+ }
1038
+
1039
+ /** @internal */
1040
+ _hasRawPassthrough(slot: string): boolean {
1041
+ return this._rawPassthrough.has(slot);
1042
+ }
1043
+
1044
+ // --- Group C: Enhanced border ---
1045
+
1046
+ getBorder(): ImageBorder | undefined { return this.border; }
1047
+
1048
+ setEffectExtent(left: number, top: number, right: number, bottom: number): this {
1049
+ this.effectExtent = { left, top, right, bottom };
1050
+ return this;
1051
+ }
1052
+
1053
+ getEffectExtent(): EffectExtent | undefined {
1054
+ return this.effectExtent;
1055
+ }
1056
+
1057
+ setWrap(type: WrapType, side?: WrapSide, distances?: { top?: number; bottom?: number; left?: number; right?: number }): this {
1058
+ this.wrap = {
1059
+ type,
1060
+ side,
1061
+ distanceTop: distances?.top,
1062
+ distanceBottom: distances?.bottom,
1063
+ distanceLeft: distances?.left,
1064
+ distanceRight: distances?.right,
1065
+ };
1066
+ return this;
1067
+ }
1068
+
1069
+ getWrap(): TextWrapSettings | undefined {
1070
+ return this.wrap;
1071
+ }
1072
+
1073
+ /**
1074
+ * Validates a position offset value
1075
+ * @param offset - Offset value in EMUs
1076
+ * @param axis - 'horizontal' or 'vertical' for error messages
1077
+ * @throws {Error} If offset exceeds maximum reasonable value
1078
+ * @private
1079
+ */
1080
+ private validatePositionOffset(offset: number | undefined, axis: string): void {
1081
+ if (offset === undefined) return;
1082
+
1083
+ // Maximum reasonable offset: 50 inches = 45,720,000 EMUs
1084
+ const MAX_OFFSET_EMUS = 45720000;
1085
+ if (Math.abs(offset) > MAX_OFFSET_EMUS) {
1086
+ throw new Error(
1087
+ `Invalid ${axis} position offset: ${offset} EMUs exceeds maximum of ${MAX_OFFSET_EMUS} EMUs (50 inches).`
1088
+ );
1089
+ }
1090
+ }
1091
+
1092
+ /**
1093
+ * Sets the position for a floating image
1094
+ *
1095
+ * Position can be specified using either:
1096
+ * - Absolute offset (in EMUs from the anchor point)
1097
+ * - Relative alignment (left, center, right / top, center, bottom)
1098
+ *
1099
+ * @param horizontal - Horizontal positioning configuration
1100
+ * @param vertical - Vertical positioning configuration
1101
+ * @returns This image for chaining
1102
+ * @throws {Error} If offset values exceed maximum
1103
+ *
1104
+ * @example
1105
+ * ```typescript
1106
+ * // Absolute positioning (100,000 EMUs from page edges)
1107
+ * image.setPosition(
1108
+ * { anchor: 'page', offset: 100000 },
1109
+ * { anchor: 'page', offset: 100000 }
1110
+ * );
1111
+ *
1112
+ * // Relative alignment (centered on page)
1113
+ * image.setPosition(
1114
+ * { anchor: 'page', alignment: 'center' },
1115
+ * { anchor: 'page', alignment: 'center' }
1116
+ * );
1117
+ * ```
1118
+ */
1119
+ setPosition(horizontal: ImagePosition['horizontal'], vertical: ImagePosition['vertical']): this {
1120
+ // Validate offset values
1121
+ this.validatePositionOffset(horizontal.offset, 'horizontal');
1122
+ this.validatePositionOffset(vertical.offset, 'vertical');
1123
+
1124
+ this.position = { horizontal, vertical };
1125
+ return this;
1126
+ }
1127
+
1128
+ getPosition(): ImagePosition | undefined {
1129
+ return this.position;
1130
+ }
1131
+
1132
+ /**
1133
+ * Validates the current image position configuration
1134
+ *
1135
+ * Checks for common configuration issues:
1136
+ * - Missing anchor when offset is used
1137
+ * - Conflicting offset and alignment values
1138
+ * - Invalid combinations
1139
+ *
1140
+ * @returns Validation result with details
1141
+ *
1142
+ * @example
1143
+ * ```typescript
1144
+ * const result = image.validatePosition();
1145
+ * if (!result.isValid) {
1146
+ * console.log(result.warnings); // Array of warning messages
1147
+ * }
1148
+ * ```
1149
+ */
1150
+ validatePosition(): {
1151
+ isValid: boolean;
1152
+ warnings: string[];
1153
+ } {
1154
+ const warnings: string[] = [];
1155
+
1156
+ if (!this.position) {
1157
+ return { isValid: true, warnings };
1158
+ }
1159
+
1160
+ // Check if both offset and alignment are specified (unusual but not invalid)
1161
+ if (this.position.horizontal.offset !== undefined && this.position.horizontal.alignment) {
1162
+ warnings.push(
1163
+ 'Horizontal position has both offset and alignment. Word will use alignment and ignore offset.'
1164
+ );
1165
+ }
1166
+
1167
+ if (this.position.vertical.offset !== undefined && this.position.vertical.alignment) {
1168
+ warnings.push(
1169
+ 'Vertical position has both offset and alignment. Word will use alignment and ignore offset.'
1170
+ );
1171
+ }
1172
+
1173
+ // Check for floating image without anchor settings
1174
+ if (this.position && !this.anchor) {
1175
+ warnings.push(
1176
+ 'Position is set but anchor is not. Consider setting anchor properties for proper floating behavior.'
1177
+ );
1178
+ }
1179
+
1180
+ return {
1181
+ isValid: warnings.length === 0,
1182
+ warnings,
1183
+ };
1184
+ }
1185
+
1186
+ setAnchor(options: ImageAnchor): this {
1187
+ this.anchor = options;
1188
+ return this;
1189
+ }
1190
+
1191
+ getAnchor(): ImageAnchor | undefined {
1192
+ return this.anchor;
1193
+ }
1194
+
1195
+ setCrop(left: number, top: number, right: number, bottom: number): this {
1196
+ const clamp = (val: number) => Math.max(0, Math.min(100, val));
1197
+ this.crop = { left: clamp(left), top: clamp(top), right: clamp(right), bottom: clamp(bottom) };
1198
+ return this;
1199
+ }
1200
+
1201
+ getCrop(): ImageCrop | undefined {
1202
+ return this.crop;
1203
+ }
1204
+
1205
+ setEffects(options: ImageEffects): this {
1206
+ const clamp = (val?: number) => val !== undefined ? Math.max(-100, Math.min(100, val)) : undefined;
1207
+ this.effects = { brightness: clamp(options.brightness), contrast: clamp(options.contrast), grayscale: options.grayscale, transparency: options.transparency !== undefined ? Math.max(0, Math.min(100, options.transparency)) : undefined };
1208
+ return this;
1209
+ }
1210
+
1211
+ getEffects(): ImageEffects | undefined {
1212
+ return this.effects;
1213
+ }
1214
+
1215
+ private detectDPI(): number | undefined {
1216
+ if (!this.imageData) return undefined;
1217
+
1218
+ try {
1219
+ if (this.extension === 'png') {
1220
+ const physIndex = this.imageData.indexOf(Buffer.from([0x70, 0x48, 0x59, 0x73]));
1221
+ if (physIndex !== -1 && physIndex + 12 < this.imageData.length) {
1222
+ const xPixelsPerMeter = this.imageData.readUInt32BE(physIndex + 4);
1223
+ const yPixelsPerMeter = this.imageData.readUInt32BE(physIndex + 8);
1224
+ const unit = this.imageData[physIndex + 12];
1225
+ if (unit === 1) {
1226
+ const dpiX = Math.round(xPixelsPerMeter * 0.0254);
1227
+ const dpiY = Math.round(yPixelsPerMeter * 0.0254);
1228
+ return Math.min(dpiX, dpiY);
1229
+ }
1230
+ }
1231
+ } else if (this.extension === 'jpg' || this.extension === 'jpeg') {
1232
+ let offset = 2;
1233
+ while (offset < this.imageData.length) {
1234
+ if (this.imageData[offset] !== 0xFF) break;
1235
+ const marker = this.imageData[offset + 1];
1236
+ if (marker === 0xE0) {
1237
+ const length = this.imageData.readUInt16BE(offset + 2);
1238
+ if (length >= 16 && this.imageData.slice(offset + 4, offset + 9).toString('ascii') === 'JFIF\0') {
1239
+ const units = this.imageData[offset + 11];
1240
+ const xDensity = this.imageData.readUInt16BE(offset + 12);
1241
+ const yDensity = this.imageData.readUInt16BE(offset + 14);
1242
+ if (units === 1) return Math.min(xDensity, yDensity);
1243
+ if (units === 2) return Math.min(Math.round(xDensity * 2.54), Math.round(yDensity * 2.54));
1244
+ }
1245
+ offset += 2 + length;
1246
+ continue;
1247
+ }
1248
+ offset += 2 + this.imageData.readUInt16BE(offset + 2);
1249
+ }
1250
+ }
1251
+ } catch (error: unknown) {
1252
+ const message = error instanceof Error ? error.message : String(error);
1253
+ defaultLogger.warn(`DPI detection failed: ${message}`);
1254
+ }
1255
+ return undefined;
1256
+ }
1257
+
1258
+ isFloating(): boolean {
1259
+ return !!this.anchor || !!this.position;
1260
+ }
1261
+
1262
+ floatTopLeft(marginTop = 0, marginLeft = 0): this {
1263
+ this.setPosition(
1264
+ { anchor: 'page', offset: marginLeft },
1265
+ { anchor: 'page', offset: marginTop }
1266
+ );
1267
+ this.setAnchor({
1268
+ behindDoc: false,
1269
+ locked: false,
1270
+ layoutInCell: true,
1271
+ allowOverlap: true,
1272
+ relativeHeight: 251658240
1273
+ });
1274
+ this.setWrap('square', 'bothSides');
1275
+ return this;
1276
+ }
1277
+
1278
+ floatTopRight(marginTop = 0, marginRight = 0): this {
1279
+ this.setPosition(
1280
+ { anchor: 'page', alignment: 'right', offset: -marginRight },
1281
+ { anchor: 'page', offset: marginTop }
1282
+ );
1283
+ this.setAnchor({
1284
+ behindDoc: false,
1285
+ locked: false,
1286
+ layoutInCell: true,
1287
+ allowOverlap: true,
1288
+ relativeHeight: 251658240
1289
+ });
1290
+ this.setWrap('square', 'bothSides');
1291
+ return this;
1292
+ }
1293
+
1294
+ floatCenter(): this {
1295
+ this.setPosition(
1296
+ { anchor: 'page', alignment: 'center' },
1297
+ { anchor: 'page', alignment: 'center' }
1298
+ );
1299
+ this.setAnchor({
1300
+ behindDoc: false,
1301
+ locked: false,
1302
+ layoutInCell: true,
1303
+ allowOverlap: true,
1304
+ relativeHeight: 251658240
1305
+ });
1306
+ this.setWrap('square', 'bothSides');
1307
+ return this;
1308
+ }
1309
+
1310
+ setBehindText(behind = true): this {
1311
+ if (this.anchor) {
1312
+ this.anchor.behindDoc = behind;
1313
+ } else {
1314
+ this.setAnchor({
1315
+ behindDoc: behind,
1316
+ locked: false,
1317
+ layoutInCell: true,
1318
+ allowOverlap: true,
1319
+ relativeHeight: 251658240
1320
+ });
1321
+ }
1322
+ return this;
1323
+ }
1324
+
1325
+ /**
1326
+ * Applies a border around the image
1327
+ * @param thicknessOrOptions Border thickness in points (number) or full ImageBorder options
1328
+ * @returns This image for chaining
1329
+ *
1330
+ * Note: effectExtent is set to accommodate the border width so it renders
1331
+ * properly without being clipped. The border is drawn centered on the image
1332
+ * edge, so half the border width extends outside the image bounds.
1333
+ */
1334
+ setBorder(thicknessOrOptions: number | ImageBorder = 2): this {
1335
+ if (typeof thicknessOrOptions === 'number') {
1336
+ this.border = { width: thicknessOrOptions };
1337
+ } else {
1338
+ this.border = thicknessOrOptions;
1339
+ }
1340
+
1341
+ // Calculate space needed for border (half-width on each side)
1342
+ // Border is drawn centered on the edge
1343
+ const borderEmu = this.border.width * UNITS.EMUS_PER_POINT;
1344
+ const halfBorderEmu = Math.ceil(borderEmu / 2);
1345
+
1346
+ // Ensure effectExtent has at least enough space for the border
1347
+ if (!this.effectExtent) {
1348
+ this.effectExtent = { left: 0, top: 0, right: 0, bottom: 0 };
1349
+ }
1350
+ this.effectExtent.left = Math.max(this.effectExtent.left, halfBorderEmu);
1351
+ this.effectExtent.top = Math.max(this.effectExtent.top, halfBorderEmu);
1352
+ this.effectExtent.right = Math.max(this.effectExtent.right, halfBorderEmu);
1353
+ this.effectExtent.bottom = Math.max(this.effectExtent.bottom, halfBorderEmu);
1354
+
1355
+ return this;
1356
+ }
1357
+
1358
+ /**
1359
+ * Removes the border from the image
1360
+ * @returns This image for chaining
1361
+ */
1362
+ removeBorder(): this {
1363
+ this.border = undefined;
1364
+ return this;
1365
+ }
1366
+
1367
+ /**
1368
+ * @deprecated Use setBorder() instead. This method will be removed in a future version.
1369
+ * Applies a 2-point black border around the image.
1370
+ * @returns This image for chaining
1371
+ */
1372
+ applyTwoPixelBlackBorder(): this {
1373
+ return this.setBorder(2);
1374
+ }
1375
+
1376
+ toXML(): XMLElement {
1377
+ const isFloating = this.isFloating();
1378
+
1379
+ // Common elements - must include wp: namespace prefix
1380
+ const extent = XMLBuilder.wp('extent', { cx: this.width.toString(), cy: this.height.toString() });
1381
+
1382
+ // --- Build blip element with effects ---
1383
+ const blipChildren: XMLElement[] = [];
1384
+
1385
+ // Add luminance effect for brightness/contrast (per ECMA-376 §20.1.8.43)
1386
+ if (this.effects?.brightness !== undefined || this.effects?.contrast !== undefined) {
1387
+ const lumAttrs: Record<string, string> = {};
1388
+ if (this.effects.brightness !== undefined) {
1389
+ lumAttrs.bright = Math.round(this.effects.brightness * 1000).toString();
1390
+ }
1391
+ if (this.effects.contrast !== undefined) {
1392
+ lumAttrs.contrast = Math.round(this.effects.contrast * 1000).toString();
1393
+ }
1394
+ blipChildren.push(XMLBuilder.aSelf('lum', lumAttrs));
1395
+ }
1396
+
1397
+ // Add grayscale effect (per ECMA-376 §20.1.8.37)
1398
+ if (this.effects?.grayscale) {
1399
+ blipChildren.push(XMLBuilder.aSelf('grayscl'));
1400
+ }
1401
+
1402
+ // Add transparency effect via a:alphaModFix (ECMA-376 §20.1.8.4)
1403
+ if (this.effects?.transparency !== undefined && this.effects.transparency > 0) {
1404
+ // transparency is 0-100%, alphaModFix amt is in 1/1000ths of percent
1405
+ // e.g., 50% transparency = 50000 amt (= 50% opacity)
1406
+ const amt = Math.round((100 - this.effects.transparency) * 1000);
1407
+ blipChildren.push(XMLBuilder.aSelf('alphaModFix', { amt: amt.toString() }));
1408
+ }
1409
+
1410
+ // Group B: Inject raw blip effects passthrough (a:clrChange, a:duotone, etc.)
1411
+ if (this._rawPassthrough.has('blip-effects')) {
1412
+ blipChildren.push({ name: '__rawXml', rawXml: this._rawPassthrough.get('blip-effects')! } as XMLElement);
1413
+ }
1414
+
1415
+ // Group B: Inject raw blip extLst passthrough (must come last per schema)
1416
+ if (this._rawPassthrough.has('blip-extLst')) {
1417
+ blipChildren.push({ name: '__rawXml', rawXml: this._rawPassthrough.get('blip-extLst')! } as XMLElement);
1418
+ } else if (this.svgRelationshipId) {
1419
+ // SVG dual-relationship: add asvg:svgBlip reference in extLst
1420
+ blipChildren.push({
1421
+ name: '__rawXml',
1422
+ rawXml: `<a:extLst><a:ext uri="{96DAC541-7B7A-43D3-8B79-37D633B846F1}"><asvg:svgBlip xmlns:asvg="http://schemas.microsoft.com/office/drawing/2016/SVG/main" r:embed="${this.svgRelationshipId}"/></a:ext></a:extLst>`,
1423
+ } as XMLElement);
1424
+ }
1425
+
1426
+ // Build blip attributes: r:embed or r:link, cstate
1427
+ const blipAttrs: Record<string, string | undefined> = {
1428
+ cstate: this.compressionState,
1429
+ };
1430
+ if (this.isLinked) {
1431
+ blipAttrs['r:link'] = this.relationshipId;
1432
+ } else {
1433
+ blipAttrs['r:embed'] = this.relationshipId;
1434
+ }
1435
+
1436
+ const blip = blipChildren.length > 0
1437
+ ? XMLBuilder.a('blip', blipAttrs, blipChildren)
1438
+ : XMLBuilder.a('blip', blipAttrs);
1439
+
1440
+ // --- Build transform (a:xfrm) ---
1441
+ const xfrmAttrs: Record<string, string> | undefined = (() => {
1442
+ const attrs: Record<string, string> = {};
1443
+ if (this.rotation > 0) attrs.rot = Math.round(this.rotation * 60000).toString();
1444
+ if (this.flipH) attrs.flipH = '1';
1445
+ if (this.flipV) attrs.flipV = '1';
1446
+ return Object.keys(attrs).length > 0 ? attrs : undefined;
1447
+ })();
1448
+ const xfrm = XMLBuilder.a('xfrm', xfrmAttrs, [
1449
+ XMLBuilder.a('off', { x: '0', y: '0' }),
1450
+ XMLBuilder.a('ext', { cx: this.width.toString(), cy: this.height.toString() })
1451
+ ]);
1452
+
1453
+ // --- Build shape properties (pic:spPr) ---
1454
+ const spPrChildren: XMLElement[] = [xfrm];
1455
+
1456
+ // Geometry: use passthrough for custGeom or prstGeom with avLst, otherwise default
1457
+ if (this._rawPassthrough.has('geometry')) {
1458
+ spPrChildren.push({ name: '__rawXml', rawXml: this._rawPassthrough.get('geometry')! } as XMLElement);
1459
+ } else {
1460
+ spPrChildren.push(XMLBuilder.a('prstGeom', { prst: this.presetGeometry }, [
1461
+ XMLBuilder.a('avLst')
1462
+ ]));
1463
+ }
1464
+
1465
+ // Border (a:ln) - full model (Group C)
1466
+ if (this.border) {
1467
+ // Add noFill element before the border line (required by Word)
1468
+ spPrChildren.push(XMLBuilder.a('noFill'));
1469
+
1470
+ const ptToEmu = 12700;
1471
+ const widthEmu = this.border.width * ptToEmu;
1472
+ const lnAttrs: Record<string, string> = { w: widthEmu.toString() };
1473
+ if (this.border.cap) lnAttrs.cap = this.border.cap;
1474
+ if (this.border.compound) lnAttrs.cmpd = this.border.compound;
1475
+ if (this.border.alignment) lnAttrs.algn = this.border.alignment;
1476
+
1477
+ const lnChildren: XMLElement[] = [];
1478
+
1479
+ // Fill
1480
+ if (this.border.rawFillXml) {
1481
+ lnChildren.push({ name: '__rawXml', rawXml: this.border.rawFillXml } as XMLElement);
1482
+ } else if (this.border.fill) {
1483
+ const colorChildren: XMLElement[] = [];
1484
+ if (this.border.fill.modifiers) {
1485
+ for (const mod of this.border.fill.modifiers) {
1486
+ colorChildren.push(XMLBuilder.aSelf(mod.name, { val: mod.val }));
1487
+ }
1488
+ }
1489
+ const colorEl = colorChildren.length > 0
1490
+ ? XMLBuilder.a(this.border.fill.type, { val: this.border.fill.value }, colorChildren)
1491
+ : XMLBuilder.a(this.border.fill.type, { val: this.border.fill.value });
1492
+ lnChildren.push(XMLBuilder.a('solidFill', undefined, [colorEl]));
1493
+ } else {
1494
+ // Default: scheme color tx1 (backward compat)
1495
+ lnChildren.push(XMLBuilder.a('solidFill', undefined, [
1496
+ XMLBuilder.a('schemeClr', { val: 'tx1' })
1497
+ ]));
1498
+ }
1499
+
1500
+ // Dash pattern
1501
+ if (this.border.dashPattern) {
1502
+ lnChildren.push(XMLBuilder.aSelf('prstDash', { val: this.border.dashPattern }));
1503
+ }
1504
+
1505
+ // Join
1506
+ if (this.border.join === 'round') {
1507
+ lnChildren.push(XMLBuilder.aSelf('round'));
1508
+ } else if (this.border.join === 'bevel') {
1509
+ lnChildren.push(XMLBuilder.aSelf('bevel'));
1510
+ } else if (this.border.join === 'miter') {
1511
+ const miterAttrs: Record<string, string> = {};
1512
+ if (this.border.miterLimit) miterAttrs.lim = this.border.miterLimit.toString();
1513
+ lnChildren.push(XMLBuilder.aSelf('miter', miterAttrs));
1514
+ }
1515
+
1516
+ // Head/tail end
1517
+ if (this.border.headEnd) {
1518
+ lnChildren.push(XMLBuilder.aSelf('headEnd', this.border.headEnd as Record<string, string>));
1519
+ }
1520
+ if (this.border.tailEnd) {
1521
+ lnChildren.push(XMLBuilder.aSelf('tailEnd', this.border.tailEnd as Record<string, string>));
1522
+ }
1523
+
1524
+ spPrChildren.push(XMLBuilder.a('ln', lnAttrs, lnChildren));
1525
+ }
1526
+
1527
+ // Group B: Inject raw spPr effects passthrough (effectLst, scene3d, sp3d, etc.)
1528
+ if (this._rawPassthrough.has('spPr-effects')) {
1529
+ spPrChildren.push({ name: '__rawXml', rawXml: this._rawPassthrough.get('spPr-effects')! } as XMLElement);
1530
+ }
1531
+
1532
+ // --- Build pic:cNvPr with passthrough ---
1533
+ const cNvPrChildren: XMLElement[] = [];
1534
+ if (this._rawPassthrough.has('cNvPr-extra')) {
1535
+ cNvPrChildren.push({ name: '__rawXml', rawXml: this._rawPassthrough.get('cNvPr-extra')! } as XMLElement);
1536
+ }
1537
+ const cNvPr = cNvPrChildren.length > 0
1538
+ ? XMLBuilder.pic('cNvPr', {
1539
+ id: this.picNonVisualProps.id,
1540
+ name: this.picNonVisualProps.name,
1541
+ descr: this.picNonVisualProps.descr,
1542
+ }, cNvPrChildren)
1543
+ : XMLBuilder.pic('cNvPr', {
1544
+ id: this.picNonVisualProps.id,
1545
+ name: this.picNonVisualProps.name,
1546
+ descr: this.picNonVisualProps.descr,
1547
+ });
1548
+
1549
+ // --- Build picLocks from map ---
1550
+ const picLocksAttrs: Record<string, string> = {};
1551
+ for (const [key, val] of Object.entries(this.picLocks)) {
1552
+ if (val) picLocksAttrs[key] = '1';
1553
+ }
1554
+
1555
+ // --- Build blipFill ---
1556
+ const blipFillAttrs: Record<string, string> = {};
1557
+ if (this.blipFillDpi !== undefined) blipFillAttrs.dpi = this.blipFillDpi.toString();
1558
+ if (this.blipFillRotWithShape !== undefined) blipFillAttrs.rotWithShape = this.blipFillRotWithShape ? '1' : '0';
1559
+
1560
+ const blipFillChildren: XMLElement[] = [blip];
1561
+ // Crop values are stored as percentages (0-100), serialized as per-mille (0-100000)
1562
+ if (this.crop) {
1563
+ blipFillChildren.push(XMLBuilder.a('srcRect', {
1564
+ l: Math.round(this.crop.left * 1000).toString(),
1565
+ t: Math.round(this.crop.top * 1000).toString(),
1566
+ r: Math.round(this.crop.right * 1000).toString(),
1567
+ b: Math.round(this.crop.bottom * 1000).toString(),
1568
+ }));
1569
+ }
1570
+ // Group B: Use tile passthrough instead of stretch when present
1571
+ if (this._rawPassthrough.has('blipFill-extra')) {
1572
+ blipFillChildren.push({ name: '__rawXml', rawXml: this._rawPassthrough.get('blipFill-extra')! } as XMLElement);
1573
+ } else {
1574
+ blipFillChildren.push(XMLBuilder.a('stretch', undefined, [XMLBuilder.a('fillRect')]));
1575
+ }
1576
+
1577
+ const blipFillAttrsObj = Object.keys(blipFillAttrs).length > 0 ? blipFillAttrs : undefined;
1578
+
1579
+ const graphicData = XMLBuilder.a('graphicData', { uri: 'http://schemas.openxmlformats.org/drawingml/2006/picture' }, [
1580
+ XMLBuilder.pic('pic', undefined, [
1581
+ XMLBuilder.pic('nvPicPr', undefined, [
1582
+ cNvPr,
1583
+ XMLBuilder.pic('cNvPicPr', undefined, [
1584
+ XMLBuilder.a('picLocks', Object.keys(picLocksAttrs).length > 0 ? picLocksAttrs : undefined)
1585
+ ])
1586
+ ]),
1587
+ XMLBuilder.pic('blipFill', blipFillAttrsObj, blipFillChildren),
1588
+ XMLBuilder.pic('spPr', { bwMode: this.bwMode }, spPrChildren)
1589
+ ])
1590
+ ]);
1591
+
1592
+ const graphic = XMLBuilder.a('graphic', undefined, [graphicData]);
1593
+
1594
+ // --- Build docPr element (shared between inline and floating) ---
1595
+ const buildDocPr = (idVal: string | number): XMLElement => {
1596
+ const attrs: Record<string, any> = { id: idVal, name: this.name, descr: this.description };
1597
+ if (this.title) attrs.title = this.title;
1598
+ if (this.hidden) attrs.hidden = '1';
1599
+ const children: XMLElement[] = [];
1600
+ if (this._rawPassthrough.has('docPr-extra')) {
1601
+ children.push({ name: '__rawXml', rawXml: this._rawPassthrough.get('docPr-extra')! } as XMLElement);
1602
+ }
1603
+ return children.length > 0
1604
+ ? XMLBuilder.wp('docPr', attrs, children)
1605
+ : XMLBuilder.wp('docPr', attrs);
1606
+ };
1607
+
1608
+ // --- Build cNvGraphicFramePr element (shared between inline and floating) ---
1609
+ const buildCNvGraphicFramePr = (): XMLElement => {
1610
+ return XMLBuilder.wp('cNvGraphicFramePr', undefined, [
1611
+ XMLBuilder.a('graphicFrameLocks', {
1612
+ 'xmlns:a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
1613
+ noChangeAspect: this.noChangeAspect ? '1' : '0',
1614
+ })
1615
+ ]);
1616
+ };
1617
+
1618
+ if (isFloating) {
1619
+ // Floating image (anchor)
1620
+ const positionHChildren: XMLElement[] = [];
1621
+ if (this.position?.horizontal.alignment) {
1622
+ positionHChildren.push(XMLBuilder.wp('align', undefined, [this.position.horizontal.alignment]));
1623
+ } else {
1624
+ positionHChildren.push(XMLBuilder.wp('posOffset', undefined, [(this.position?.horizontal.offset || 0).toString()]));
1625
+ }
1626
+ const positionH = XMLBuilder.wp('positionH', { relativeFrom: this.position?.horizontal.anchor || 'page' }, positionHChildren);
1627
+
1628
+ const positionVChildren: XMLElement[] = [];
1629
+ if (this.position?.vertical.alignment) {
1630
+ positionVChildren.push(XMLBuilder.wp('align', undefined, [this.position.vertical.alignment]));
1631
+ } else {
1632
+ positionVChildren.push(XMLBuilder.wp('posOffset', undefined, [(this.position?.vertical.offset || 0).toString()]));
1633
+ }
1634
+ const positionV = XMLBuilder.wp('positionV', { relativeFrom: this.position?.vertical.anchor || 'page' }, positionVChildren);
1635
+
1636
+ // Effect extent for floating images (required by Word)
1637
+ const floatEffectExt = this.effectExtent || { left: 0, top: 0, right: 0, bottom: 0 };
1638
+ const effectExtentElement = XMLBuilder.wp('effectExtent', {
1639
+ t: floatEffectExt.top.toString(),
1640
+ r: floatEffectExt.right.toString(),
1641
+ b: floatEffectExt.bottom.toString(),
1642
+ l: floatEffectExt.left.toString()
1643
+ });
1644
+
1645
+ const anchorChildren: XMLElement[] = [
1646
+ positionH,
1647
+ positionV,
1648
+ extent,
1649
+ effectExtentElement
1650
+ ];
1651
+
1652
+ // Wrap element (required by CT_Anchor per ECMA-376 — defaults to wrapNone)
1653
+ if (this.wrap) {
1654
+ const wrapAttrs: Record<string, any> = {};
1655
+ if (this.wrap.distanceTop !== undefined) wrapAttrs.distT = this.wrap.distanceTop;
1656
+ if (this.wrap.distanceBottom !== undefined) wrapAttrs.distB = this.wrap.distanceBottom;
1657
+ if (this.wrap.distanceLeft !== undefined) wrapAttrs.distL = this.wrap.distanceLeft;
1658
+ if (this.wrap.distanceRight !== undefined) wrapAttrs.distR = this.wrap.distanceRight;
1659
+ if (this.wrap.side) wrapAttrs.wrapText = this.wrap.side;
1660
+
1661
+ let wrapElementName: string;
1662
+ switch (this.wrap.type) {
1663
+ case 'square': wrapElementName = 'wrapSquare'; break;
1664
+ case 'tight': wrapElementName = 'wrapTight'; break;
1665
+ case 'through': wrapElementName = 'wrapThrough'; break;
1666
+ case 'topAndBottom': wrapElementName = 'wrapTopAndBottom'; break;
1667
+ case 'none': wrapElementName = 'wrapNone'; break;
1668
+ default: wrapElementName = 'wrapSquare';
1669
+ }
1670
+
1671
+ // Group B: Include wrap polygon passthrough as children
1672
+ const wrapChildren: XMLElement[] = [];
1673
+ if (this._rawPassthrough.has('wrap-polygon')) {
1674
+ wrapChildren.push({ name: '__rawXml', rawXml: this._rawPassthrough.get('wrap-polygon')! } as XMLElement);
1675
+ } else if (wrapElementName === 'wrapTight' || wrapElementName === 'wrapThrough') {
1676
+ // CT_WrapTight/CT_WrapThrough require a wp:wrapPolygon child.
1677
+ // Generate a default rectangular polygon covering the full image extents.
1678
+ wrapChildren.push({
1679
+ name: 'wp:wrapPolygon',
1680
+ attributes: { edited: '0' },
1681
+ children: [
1682
+ { name: 'wp:start', attributes: { x: '0', y: '0' }, selfClosing: true },
1683
+ { name: 'wp:lineTo', attributes: { x: '21600', y: '0' }, selfClosing: true },
1684
+ { name: 'wp:lineTo', attributes: { x: '21600', y: '21600' }, selfClosing: true },
1685
+ { name: 'wp:lineTo', attributes: { x: '0', y: '21600' }, selfClosing: true },
1686
+ { name: 'wp:lineTo', attributes: { x: '0', y: '0' }, selfClosing: true },
1687
+ ],
1688
+ });
1689
+ }
1690
+
1691
+ anchorChildren.push(
1692
+ wrapChildren.length > 0
1693
+ ? XMLBuilder.wp(wrapElementName, wrapAttrs, wrapChildren)
1694
+ : XMLBuilder.wp(wrapElementName, wrapAttrs)
1695
+ );
1696
+ } else {
1697
+ // Default: wrapNone (required choice element per CT_Anchor)
1698
+ anchorChildren.push(XMLBuilder.wp('wrapNone', {}));
1699
+ }
1700
+
1701
+ anchorChildren.push(buildDocPr(this.docPrId));
1702
+ anchorChildren.push(buildCNvGraphicFramePr());
1703
+ anchorChildren.push(graphic);
1704
+
1705
+ // Group B: Inject anchor extras (wp14:sizeRelH, wp14:sizeRelV)
1706
+ if (this._rawPassthrough.has('anchor-extra')) {
1707
+ anchorChildren.push({ name: '__rawXml', rawXml: this._rawPassthrough.get('anchor-extra')! } as XMLElement);
1708
+ }
1709
+
1710
+ // Build anchor attributes including simplePos and distance from text
1711
+ const anchorAttrs: Record<string, any> = {
1712
+ behindDoc: this.anchor?.behindDoc ? 1 : 0,
1713
+ locked: this.anchor?.locked ? 1 : 0,
1714
+ layoutInCell: this.anchor?.layoutInCell ? 1 : 0,
1715
+ allowOverlap: this.anchor?.allowOverlap ? 1 : 0,
1716
+ relativeHeight: this.anchor?.relativeHeight,
1717
+ simplePos: this.anchor?.simplePos ? '1' : '0',
1718
+ distT: (this.anchor?.distT ?? 0).toString(),
1719
+ distB: (this.anchor?.distB ?? 0).toString(),
1720
+ distL: (this.anchor?.distL ?? 0).toString(),
1721
+ distR: (this.anchor?.distR ?? 0).toString(),
1722
+ };
1723
+ if (this.hidden) anchorAttrs.hidden = '1';
1724
+
1725
+ // Add wp:simplePos child element (required by ECMA-376 even when simplePos="0")
1726
+ anchorChildren.unshift(XMLBuilder.wp('simplePos', { x: '0', y: '0' }));
1727
+
1728
+ return XMLBuilder.w('drawing', undefined, [
1729
+ XMLBuilder.wp('anchor', anchorAttrs, anchorChildren)
1730
+ ]);
1731
+ } else {
1732
+ // Inline image
1733
+ const effectExt = this.effectExtent || { left: 0, top: 0, right: 0, bottom: 0 };
1734
+
1735
+ return XMLBuilder.w('drawing', undefined, [
1736
+ XMLBuilder.wp('inline', {
1737
+ distT: this.inlineDistT.toString(),
1738
+ distB: this.inlineDistB.toString(),
1739
+ distL: this.inlineDistL.toString(),
1740
+ distR: this.inlineDistR.toString(),
1741
+ }, [
1742
+ extent,
1743
+ XMLBuilder.wp('effectExtent', {
1744
+ t: effectExt.top.toString(),
1745
+ r: effectExt.right.toString(),
1746
+ b: effectExt.bottom.toString(),
1747
+ l: effectExt.left.toString()
1748
+ }),
1749
+ buildDocPr(this.docPrId.toString()),
1750
+ buildCNvGraphicFramePr(),
1751
+ graphic
1752
+ ])
1753
+ ]);
1754
+ }
1755
+ }
1756
+ }