omni-viewer-core 0.1.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 (387) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/THIRD_PARTY_NOTICES.md +49 -0
  4. package/dist/assets/audio-engine/audio_engine.mjs +2 -0
  5. package/dist/assets/audio-engine/audio_engine.wasm +0 -0
  6. package/dist/assets/pdfjs/pdf.worker.min.mjs +21 -0
  7. package/dist/host/index.d.ts +133 -0
  8. package/dist/host/index.js +10 -0
  9. package/dist/i18n/catalog.en.d.ts +1 -0
  10. package/dist/i18n/catalog.en.js +411 -0
  11. package/dist/i18n/catalog.ko.d.ts +1 -0
  12. package/dist/i18n/catalog.ko.js +68 -0
  13. package/dist/i18n/index.d.ts +18 -0
  14. package/dist/i18n/index.js +41 -0
  15. package/dist/licenses/emscripten.txt +101 -0
  16. package/dist/licenses/kissfft.txt +28 -0
  17. package/dist/licenses/pdfjs-dist.txt +177 -0
  18. package/dist/parsers/a2l/index.d.ts +1 -0
  19. package/dist/parsers/a2l/index.js +1 -0
  20. package/dist/parsers/archive/index.d.ts +22 -0
  21. package/dist/parsers/archive/index.js +46 -0
  22. package/dist/parsers/archive/model.d.ts +29 -0
  23. package/dist/parsers/archive/model.js +1 -0
  24. package/dist/parsers/archive/worker.d.ts +21 -0
  25. package/dist/parsers/archive/worker.js +1 -0
  26. package/dist/parsers/arxml/index.d.ts +1 -0
  27. package/dist/parsers/arxml/index.js +1 -0
  28. package/dist/parsers/asc/index.d.ts +1 -0
  29. package/dist/parsers/asc/index.js +1 -0
  30. package/dist/parsers/audio/index.d.ts +9 -0
  31. package/dist/parsers/audio/index.js +46 -0
  32. package/dist/parsers/automotive/index.d.ts +33 -0
  33. package/dist/parsers/automotive/index.js +973 -0
  34. package/dist/parsers/blf/index.d.ts +1 -0
  35. package/dist/parsers/blf/index.js +1 -0
  36. package/dist/parsers/csv/delimiter.d.ts +31 -0
  37. package/dist/parsers/csv/delimiter.js +151 -0
  38. package/dist/parsers/csv/index.d.ts +30 -0
  39. package/dist/parsers/csv/index.js +108 -0
  40. package/dist/parsers/csv/parseText.d.ts +20 -0
  41. package/dist/parsers/csv/parseText.js +108 -0
  42. package/dist/parsers/csv/sort.d.ts +26 -0
  43. package/dist/parsers/csv/sort.js +224 -0
  44. package/dist/parsers/csv/statistics.d.ts +49 -0
  45. package/dist/parsers/csv/statistics.js +150 -0
  46. package/dist/parsers/dbc/index.d.ts +56 -0
  47. package/dist/parsers/dbc/index.js +94 -0
  48. package/dist/parsers/doc-binary/index.d.ts +35 -0
  49. package/dist/parsers/doc-binary/index.js +267 -0
  50. package/dist/parsers/doc-binary/vscode-parser.d.ts +142 -0
  51. package/dist/parsers/doc-binary/vscode-parser.js +3747 -0
  52. package/dist/parsers/excel/index.d.ts +17 -0
  53. package/dist/parsers/excel/index.js +406 -0
  54. package/dist/parsers/excel/model.d.ts +80 -0
  55. package/dist/parsers/excel/model.js +48 -0
  56. package/dist/parsers/excel/serialize.d.ts +3 -0
  57. package/dist/parsers/excel/serialize.js +78 -0
  58. package/dist/parsers/hdf5/index.d.ts +75 -0
  59. package/dist/parsers/hdf5/index.js +658 -0
  60. package/dist/parsers/hdf5/node.d.ts +21 -0
  61. package/dist/parsers/hdf5/node.js +95 -0
  62. package/dist/parsers/json/index.d.ts +15 -0
  63. package/dist/parsers/json/index.js +102 -0
  64. package/dist/parsers/json/model.d.ts +42 -0
  65. package/dist/parsers/json/model.js +140 -0
  66. package/dist/parsers/json/parse.d.ts +22 -0
  67. package/dist/parsers/json/parse.js +279 -0
  68. package/dist/parsers/json/tokenizer.d.ts +17 -0
  69. package/dist/parsers/json/tokenizer.js +175 -0
  70. package/dist/parsers/jsonl/index.d.ts +14 -0
  71. package/dist/parsers/jsonl/index.js +18 -0
  72. package/dist/parsers/jsonl/model.d.ts +23 -0
  73. package/dist/parsers/jsonl/model.js +1 -0
  74. package/dist/parsers/jsonl/parse.d.ts +7 -0
  75. package/dist/parsers/jsonl/parse.js +34 -0
  76. package/dist/parsers/markdown/index.d.ts +37 -0
  77. package/dist/parsers/markdown/index.js +59 -0
  78. package/dist/parsers/mat/index.d.ts +31 -0
  79. package/dist/parsers/mat/index.js +426 -0
  80. package/dist/parsers/mermaid/index.d.ts +5 -0
  81. package/dist/parsers/mermaid/index.js +3 -0
  82. package/dist/parsers/mf4/index.d.ts +1 -0
  83. package/dist/parsers/mf4/index.js +1 -0
  84. package/dist/parsers/parquet/index.d.ts +21 -0
  85. package/dist/parsers/parquet/index.js +68 -0
  86. package/dist/parsers/pcap/index.d.ts +1 -0
  87. package/dist/parsers/pcap/index.js +1 -0
  88. package/dist/parsers/pcapng/index.d.ts +1 -0
  89. package/dist/parsers/pcapng/index.js +1 -0
  90. package/dist/parsers/plantuml/index.d.ts +5 -0
  91. package/dist/parsers/plantuml/index.js +4 -0
  92. package/dist/parsers/ppt-binary/index.d.ts +5 -0
  93. package/dist/parsers/ppt-binary/index.js +62 -0
  94. package/dist/parsers/ppt-binary/pptBinaryBuildContext.d.ts +12 -0
  95. package/dist/parsers/ppt-binary/pptBinaryBuildContext.js +40 -0
  96. package/dist/parsers/ppt-binary/pptBinaryContainer.d.ts +4 -0
  97. package/dist/parsers/ppt-binary/pptBinaryContainer.js +174 -0
  98. package/dist/parsers/ppt-binary/pptBinaryTypes.d.ts +125 -0
  99. package/dist/parsers/ppt-binary/pptBinaryTypes.js +1 -0
  100. package/dist/parsers/ppt-binary/pptSlideLayouts.d.ts +9 -0
  101. package/dist/parsers/ppt-binary/pptSlideLayouts.js +510 -0
  102. package/dist/parsers/ppt-binary/vscode-parser.d.ts +116 -0
  103. package/dist/parsers/ppt-binary/vscode-parser.js +2853 -0
  104. package/dist/parsers/ppt-binary/vscode.d.ts +4 -0
  105. package/dist/parsers/ppt-binary/vscode.js +12 -0
  106. package/dist/parsers/pptx/index.d.ts +15 -0
  107. package/dist/parsers/pptx/index.js +88 -0
  108. package/dist/parsers/pptx/vscode-parser.d.ts +167 -0
  109. package/dist/parsers/pptx/vscode-parser.js +1503 -0
  110. package/dist/parsers/pptx/vscode.d.ts +4 -0
  111. package/dist/parsers/pptx/vscode.js +18 -0
  112. package/dist/parsers/pptx/zip-reader.d.ts +3 -0
  113. package/dist/parsers/pptx/zip-reader.js +20 -0
  114. package/dist/parsers/proto/index.d.ts +90 -0
  115. package/dist/parsers/proto/index.js +197 -0
  116. package/dist/parsers/psd/index.d.ts +16 -0
  117. package/dist/parsers/psd/index.js +10 -0
  118. package/dist/parsers/shapefile/dbf.d.ts +23 -0
  119. package/dist/parsers/shapefile/dbf.js +112 -0
  120. package/dist/parsers/shapefile/index.d.ts +44 -0
  121. package/dist/parsers/shapefile/index.js +119 -0
  122. package/dist/parsers/shapefile/reproject.d.ts +10 -0
  123. package/dist/parsers/shapefile/reproject.js +41 -0
  124. package/dist/parsers/slide-model.d.ts +106 -0
  125. package/dist/parsers/slide-model.js +1 -0
  126. package/dist/parsers/toml/index.d.ts +15 -0
  127. package/dist/parsers/toml/index.js +18 -0
  128. package/dist/parsers/toml/model.d.ts +15 -0
  129. package/dist/parsers/toml/model.js +1 -0
  130. package/dist/parsers/toml/parse.d.ts +16 -0
  131. package/dist/parsers/toml/parse.js +266 -0
  132. package/dist/parsers/types.d.ts +103 -0
  133. package/dist/parsers/types.js +106 -0
  134. package/dist/parsers/video/index.d.ts +6 -0
  135. package/dist/parsers/video/index.js +2 -0
  136. package/dist/parsers/yaml/index.d.ts +16 -0
  137. package/dist/parsers/yaml/index.js +58 -0
  138. package/dist/parsers/yaml/model.d.ts +22 -0
  139. package/dist/parsers/yaml/model.js +1 -0
  140. package/dist/parsers/yaml/self-loading.d.ts +3 -0
  141. package/dist/parsers/yaml/self-loading.js +19 -0
  142. package/dist/parsers/zip-scan.d.ts +8 -0
  143. package/dist/parsers/zip-scan.js +80 -0
  144. package/dist/registry/container.d.ts +17 -0
  145. package/dist/registry/container.js +49 -0
  146. package/dist/registry/index.d.ts +133 -0
  147. package/dist/registry/index.js +348 -0
  148. package/dist/registry/probe.d.ts +3 -0
  149. package/dist/registry/probe.js +177 -0
  150. package/dist/registry/sniff.d.ts +17 -0
  151. package/dist/registry/sniff.js +57 -0
  152. package/dist/styles/archive.css +10 -0
  153. package/dist/styles/audio.css +66 -0
  154. package/dist/styles/automotive.css +1 -0
  155. package/dist/styles/csv.css +262 -0
  156. package/dist/styles/excel.css +212 -0
  157. package/dist/styles/fallback.css +48 -0
  158. package/dist/styles/hdf5.css +3 -0
  159. package/dist/styles/hwp.css +12 -0
  160. package/dist/styles/image.css +115 -0
  161. package/dist/styles/json.css +342 -0
  162. package/dist/styles/jsonl.css +1 -0
  163. package/dist/styles/markdown.css +10 -0
  164. package/dist/styles/mat.css +2 -0
  165. package/dist/styles/mermaid.css +1 -0
  166. package/dist/styles/parquet.css +2 -0
  167. package/dist/styles/pdf.css +551 -0
  168. package/dist/styles/plantuml.css +1 -0
  169. package/dist/styles/ppt.css +1 -0
  170. package/dist/styles/proto.css +2 -0
  171. package/dist/styles/psd.css +1 -0
  172. package/dist/styles/shapefile.css +25 -0
  173. package/dist/styles/toml.css +1 -0
  174. package/dist/styles/video.css +74 -0
  175. package/dist/styles/word.css +685 -0
  176. package/dist/styles/yaml.css +1 -0
  177. package/dist/viewers/a2l/index.d.ts +15 -0
  178. package/dist/viewers/a2l/index.js +5 -0
  179. package/dist/viewers/archive/archive-preview-decoder.d.ts +4 -0
  180. package/dist/viewers/archive/archive-preview-decoder.js +223 -0
  181. package/dist/viewers/archive/controller.d.ts +24 -0
  182. package/dist/viewers/archive/controller.js +18 -0
  183. package/dist/viewers/archive/index.d.ts +25 -0
  184. package/dist/viewers/archive/index.js +230 -0
  185. package/dist/viewers/archive/preview.d.ts +2 -0
  186. package/dist/viewers/archive/preview.js +2 -0
  187. package/dist/viewers/archive/styles.d.ts +1 -0
  188. package/dist/viewers/archive/styles.js +11 -0
  189. package/dist/viewers/arxml/index.d.ts +15 -0
  190. package/dist/viewers/arxml/index.js +5 -0
  191. package/dist/viewers/asc/index.d.ts +15 -0
  192. package/dist/viewers/asc/index.js +5 -0
  193. package/dist/viewers/audio/controller.d.ts +33 -0
  194. package/dist/viewers/audio/controller.js +49 -0
  195. package/dist/viewers/audio/engine.d.ts +44 -0
  196. package/dist/viewers/audio/engine.js +108 -0
  197. package/dist/viewers/audio/index.d.ts +92 -0
  198. package/dist/viewers/audio/index.js +448 -0
  199. package/dist/viewers/audio/self-loading.d.ts +7 -0
  200. package/dist/viewers/audio/self-loading.js +42 -0
  201. package/dist/viewers/audio/styles.d.ts +1 -0
  202. package/dist/viewers/audio/styles.js +73 -0
  203. package/dist/viewers/automotive/index.d.ts +8 -0
  204. package/dist/viewers/automotive/index.js +152 -0
  205. package/dist/viewers/automotive/styles.d.ts +1 -0
  206. package/dist/viewers/automotive/styles.js +1 -0
  207. package/dist/viewers/avro/index.d.ts +15 -0
  208. package/dist/viewers/avro/index.js +5 -0
  209. package/dist/viewers/bag/index.d.ts +15 -0
  210. package/dist/viewers/bag/index.js +5 -0
  211. package/dist/viewers/blf/index.d.ts +15 -0
  212. package/dist/viewers/blf/index.js +5 -0
  213. package/dist/viewers/csv/controller.d.ts +126 -0
  214. package/dist/viewers/csv/controller.js +598 -0
  215. package/dist/viewers/csv/index.d.ts +48 -0
  216. package/dist/viewers/csv/index.js +846 -0
  217. package/dist/viewers/csv/styles.d.ts +1 -0
  218. package/dist/viewers/csv/styles.js +267 -0
  219. package/dist/viewers/db3/index.d.ts +15 -0
  220. package/dist/viewers/db3/index.js +5 -0
  221. package/dist/viewers/dbc/index.d.ts +15 -0
  222. package/dist/viewers/dbc/index.js +5 -0
  223. package/dist/viewers/diagram.d.ts +14 -0
  224. package/dist/viewers/diagram.js +95 -0
  225. package/dist/viewers/excel/controller.d.ts +108 -0
  226. package/dist/viewers/excel/controller.js +548 -0
  227. package/dist/viewers/excel/grid.d.ts +28 -0
  228. package/dist/viewers/excel/grid.js +99 -0
  229. package/dist/viewers/excel/index.d.ts +37 -0
  230. package/dist/viewers/excel/index.js +620 -0
  231. package/dist/viewers/excel/self-loading.d.ts +6 -0
  232. package/dist/viewers/excel/self-loading.js +13 -0
  233. package/dist/viewers/excel/styles.d.ts +1 -0
  234. package/dist/viewers/excel/styles.js +216 -0
  235. package/dist/viewers/fallback/index.d.ts +17 -0
  236. package/dist/viewers/fallback/index.js +130 -0
  237. package/dist/viewers/fallback/styles.d.ts +1 -0
  238. package/dist/viewers/fallback/styles.js +49 -0
  239. package/dist/viewers/hdf5/index.d.ts +19 -0
  240. package/dist/viewers/hdf5/index.js +170 -0
  241. package/dist/viewers/hdf5/styles.d.ts +1 -0
  242. package/dist/viewers/hdf5/styles.js +4 -0
  243. package/dist/viewers/hwp/controller.d.ts +19 -0
  244. package/dist/viewers/hwp/controller.js +22 -0
  245. package/dist/viewers/hwp/index.d.ts +39 -0
  246. package/dist/viewers/hwp/index.js +271 -0
  247. package/dist/viewers/hwp/self-loading.d.ts +2 -0
  248. package/dist/viewers/hwp/self-loading.js +6 -0
  249. package/dist/viewers/hwp/styles.d.ts +1 -0
  250. package/dist/viewers/hwp/styles.js +13 -0
  251. package/dist/viewers/image/controller.d.ts +210 -0
  252. package/dist/viewers/image/controller.js +336 -0
  253. package/dist/viewers/image/decode.d.ts +54 -0
  254. package/dist/viewers/image/decode.js +150 -0
  255. package/dist/viewers/image/index.d.ts +38 -0
  256. package/dist/viewers/image/index.js +749 -0
  257. package/dist/viewers/image/limits.d.ts +18 -0
  258. package/dist/viewers/image/limits.js +20 -0
  259. package/dist/viewers/image/styles.d.ts +1 -0
  260. package/dist/viewers/image/styles.js +119 -0
  261. package/dist/viewers/json/controller.d.ts +89 -0
  262. package/dist/viewers/json/controller.js +411 -0
  263. package/dist/viewers/json/index.d.ts +25 -0
  264. package/dist/viewers/json/index.js +503 -0
  265. package/dist/viewers/json/styles.d.ts +1 -0
  266. package/dist/viewers/json/styles.js +346 -0
  267. package/dist/viewers/json/transforms.d.ts +27 -0
  268. package/dist/viewers/json/transforms.js +253 -0
  269. package/dist/viewers/jsonl/controller.d.ts +48 -0
  270. package/dist/viewers/jsonl/controller.js +136 -0
  271. package/dist/viewers/jsonl/index.d.ts +22 -0
  272. package/dist/viewers/jsonl/index.js +184 -0
  273. package/dist/viewers/jsonl/styles.d.ts +1 -0
  274. package/dist/viewers/jsonl/styles.js +1 -0
  275. package/dist/viewers/markdown/controller.d.ts +50 -0
  276. package/dist/viewers/markdown/controller.js +62 -0
  277. package/dist/viewers/markdown/index.d.ts +64 -0
  278. package/dist/viewers/markdown/index.js +425 -0
  279. package/dist/viewers/markdown/math.d.ts +21 -0
  280. package/dist/viewers/markdown/math.js +100 -0
  281. package/dist/viewers/markdown/self-loading.d.ts +5 -0
  282. package/dist/viewers/markdown/self-loading.js +48 -0
  283. package/dist/viewers/markdown/styles.d.ts +1 -0
  284. package/dist/viewers/markdown/styles.js +11 -0
  285. package/dist/viewers/mat/index.d.ts +19 -0
  286. package/dist/viewers/mat/index.js +113 -0
  287. package/dist/viewers/mat/styles.d.ts +1 -0
  288. package/dist/viewers/mat/styles.js +3 -0
  289. package/dist/viewers/media.d.ts +9 -0
  290. package/dist/viewers/media.js +77 -0
  291. package/dist/viewers/mermaid/index.d.ts +17 -0
  292. package/dist/viewers/mermaid/index.js +6 -0
  293. package/dist/viewers/mermaid/self-loading.d.ts +2 -0
  294. package/dist/viewers/mermaid/self-loading.js +2 -0
  295. package/dist/viewers/mf4/index.d.ts +15 -0
  296. package/dist/viewers/mf4/index.js +5 -0
  297. package/dist/viewers/parquet/controller.d.ts +37 -0
  298. package/dist/viewers/parquet/controller.js +26 -0
  299. package/dist/viewers/parquet/index.d.ts +23 -0
  300. package/dist/viewers/parquet/index.js +134 -0
  301. package/dist/viewers/parquet/styles.d.ts +1 -0
  302. package/dist/viewers/parquet/styles.js +3 -0
  303. package/dist/viewers/pcap/index.d.ts +15 -0
  304. package/dist/viewers/pcap/index.js +5 -0
  305. package/dist/viewers/pcapng/index.d.ts +15 -0
  306. package/dist/viewers/pcapng/index.js +5 -0
  307. package/dist/viewers/pdf/controller.d.ts +120 -0
  308. package/dist/viewers/pdf/controller.js +191 -0
  309. package/dist/viewers/pdf/editing.d.ts +26 -0
  310. package/dist/viewers/pdf/editing.js +265 -0
  311. package/dist/viewers/pdf/index.d.ts +88 -0
  312. package/dist/viewers/pdf/index.js +1400 -0
  313. package/dist/viewers/pdf/self-loading.d.ts +6 -0
  314. package/dist/viewers/pdf/self-loading.js +14 -0
  315. package/dist/viewers/pdf/styles.d.ts +1 -0
  316. package/dist/viewers/pdf/styles.js +555 -0
  317. package/dist/viewers/plantuml/index.d.ts +17 -0
  318. package/dist/viewers/plantuml/index.js +6 -0
  319. package/dist/viewers/plantuml/self-loading.d.ts +2 -0
  320. package/dist/viewers/plantuml/self-loading.js +2 -0
  321. package/dist/viewers/ppt/chart.d.ts +8 -0
  322. package/dist/viewers/ppt/chart.js +83 -0
  323. package/dist/viewers/ppt/controller.d.ts +34 -0
  324. package/dist/viewers/ppt/controller.js +27 -0
  325. package/dist/viewers/ppt/index.d.ts +33 -0
  326. package/dist/viewers/ppt/index.js +101 -0
  327. package/dist/viewers/ppt/render.d.ts +5 -0
  328. package/dist/viewers/ppt/render.js +144 -0
  329. package/dist/viewers/ppt/self-loading.d.ts +9 -0
  330. package/dist/viewers/ppt/self-loading.js +4 -0
  331. package/dist/viewers/ppt/styles.d.ts +1 -0
  332. package/dist/viewers/ppt/styles.js +1 -0
  333. package/dist/viewers/proto/index.d.ts +17 -0
  334. package/dist/viewers/proto/index.js +170 -0
  335. package/dist/viewers/proto/styles.d.ts +1 -0
  336. package/dist/viewers/proto/styles.js +2 -0
  337. package/dist/viewers/psd/index.d.ts +27 -0
  338. package/dist/viewers/psd/index.js +39 -0
  339. package/dist/viewers/psd/self-loading.d.ts +10 -0
  340. package/dist/viewers/psd/self-loading.js +20 -0
  341. package/dist/viewers/psd/styles.d.ts +1 -0
  342. package/dist/viewers/psd/styles.js +1 -0
  343. package/dist/viewers/reqif/index.d.ts +15 -0
  344. package/dist/viewers/reqif/index.js +5 -0
  345. package/dist/viewers/shapefile/index.d.ts +35 -0
  346. package/dist/viewers/shapefile/index.js +336 -0
  347. package/dist/viewers/shapefile/self-loading.d.ts +7 -0
  348. package/dist/viewers/shapefile/self-loading.js +32 -0
  349. package/dist/viewers/shapefile/styles.d.ts +1 -0
  350. package/dist/viewers/shapefile/styles.js +28 -0
  351. package/dist/viewers/stp/index.d.ts +15 -0
  352. package/dist/viewers/stp/index.js +5 -0
  353. package/dist/viewers/structured-styles.d.ts +1 -0
  354. package/dist/viewers/structured-styles.js +1 -0
  355. package/dist/viewers/structured.d.ts +36 -0
  356. package/dist/viewers/structured.js +138 -0
  357. package/dist/viewers/toml/controller.d.ts +38 -0
  358. package/dist/viewers/toml/controller.js +61 -0
  359. package/dist/viewers/toml/index.d.ts +18 -0
  360. package/dist/viewers/toml/index.js +34 -0
  361. package/dist/viewers/types.d.ts +36 -0
  362. package/dist/viewers/types.js +9 -0
  363. package/dist/viewers/video/controller.d.ts +29 -0
  364. package/dist/viewers/video/controller.js +54 -0
  365. package/dist/viewers/video/index.d.ts +18 -0
  366. package/dist/viewers/video/index.js +305 -0
  367. package/dist/viewers/video/styles.d.ts +1 -0
  368. package/dist/viewers/video/styles.js +79 -0
  369. package/dist/viewers/word/controller.d.ts +19 -0
  370. package/dist/viewers/word/controller.js +22 -0
  371. package/dist/viewers/word/docx-preprocess.d.ts +63 -0
  372. package/dist/viewers/word/docx-preprocess.js +136 -0
  373. package/dist/viewers/word/index.d.ts +32 -0
  374. package/dist/viewers/word/index.js +187 -0
  375. package/dist/viewers/word/normalize-docx.d.ts +2 -0
  376. package/dist/viewers/word/normalize-docx.js +50 -0
  377. package/dist/viewers/word/paginate.d.ts +1 -0
  378. package/dist/viewers/word/paginate.js +1035 -0
  379. package/dist/viewers/word/self-loading.d.ts +2 -0
  380. package/dist/viewers/word/self-loading.js +7 -0
  381. package/dist/viewers/word/styles.d.ts +1 -0
  382. package/dist/viewers/word/styles.js +686 -0
  383. package/dist/viewers/yaml/controller.d.ts +42 -0
  384. package/dist/viewers/yaml/controller.js +41 -0
  385. package/dist/viewers/yaml/index.d.ts +21 -0
  386. package/dist/viewers/yaml/index.js +20 -0
  387. package/package.json +375 -0
@@ -0,0 +1,2853 @@
1
+ // @ts-nocheck -- upstream VS Code parser predates exactOptionalPropertyTypes.
2
+ import { Buffer } from 'buffer';
3
+ import { normalizeBuildSlidesArgs } from './pptBinaryBuildContext.js';
4
+ import { parseCfb as parseCfbContainer, parseRecords as parseRecordTree } from './pptBinaryContainer.js';
5
+ /**
6
+ * Standalone legacy .ppt parser.
7
+ *
8
+ * Scope (incremental roadmap):
9
+ * 1) CFB/OLE container parsing
10
+ * 2) Slide container indexing
11
+ * 3) Text extraction
12
+ * 4) Basic layout placement
13
+ * 5) Pictures stream extraction (JPEG/PNG)
14
+ * 6) Minimal style hints (title/bullets)
15
+ * 7) Extension-friendly internal pipeline
16
+ */
17
+ export class PptBinaryParser {
18
+ static POINTS_TO_PX = 96 / 72;
19
+ static async parse(input) {
20
+ const buffer = Buffer.from(input.buffer, input.byteOffset, input.byteLength);
21
+ const cfb = this.parseCfb(buffer);
22
+ const docStream = cfb.getStream('PowerPoint Document');
23
+ if (!docStream) {
24
+ throw new Error('Invalid .ppt file: missing "PowerPoint Document" stream.');
25
+ }
26
+ const records = this.parseRecords(docStream, 0, docStream.length);
27
+ const slideRecords = this.collectSlideContainers(records);
28
+ const picturesStream = cfb.getStream('Pictures');
29
+ const pictures = this.extractPictures(picturesStream);
30
+ const picturesById = this.extractPicturesByBlipId(records, picturesStream, pictures);
31
+ const outlineTextByPersistId = this.extractOutlineTextByPersistId(records);
32
+ const defaultColorScheme = this.extractDocumentColorScheme(records);
33
+ const masterRecord = this.collectPrimaryMasterContainer(records);
34
+ const presentationMetrics = this.extractPresentationMetrics(records);
35
+ const widthPx = presentationMetrics?.widthPx ?? 960;
36
+ const heightPx = presentationMetrics?.heightPx ?? 720;
37
+ const slides = this.buildSlides(slideRecords, pictures, outlineTextByPersistId, defaultColorScheme, masterRecord, presentationMetrics, widthPx, heightPx, picturesById);
38
+ if (slides.length === 0) {
39
+ const fallbackTexts = this.extractLooseTexts(docStream).slice(0, 40);
40
+ if (fallbackTexts.length > 0) {
41
+ return {
42
+ slides: [{
43
+ slideNumber: 1,
44
+ widthPx,
45
+ heightPx,
46
+ backgroundColor: '#ffffff',
47
+ elements: fallbackTexts.map((text, idx) => ({
48
+ type: 'text',
49
+ x: 72,
50
+ y: 80 + idx * 34,
51
+ width: 816,
52
+ height: 30,
53
+ zIndex: idx,
54
+ isTitle: idx === 0,
55
+ paragraphs: [{
56
+ text,
57
+ level: 0,
58
+ bullet: idx > 0
59
+ }]
60
+ }))
61
+ }],
62
+ totalSlides: 1
63
+ };
64
+ }
65
+ }
66
+ return {
67
+ slides,
68
+ totalSlides: slides.length
69
+ };
70
+ }
71
+ static extractPresentationMetrics(records) {
72
+ for (const record of records) {
73
+ if (record.recType === 1000 && record.children && record.children.length > 0) {
74
+ const documentAtom = record.children.find((child) => child.recType === 1001 && child.payload.length >= 8);
75
+ if (!documentAtom) {
76
+ continue;
77
+ }
78
+ const rawWidth = documentAtom.payload.readUInt32LE(0);
79
+ const rawHeight = documentAtom.payload.readUInt32LE(4);
80
+ const widthPx = this.legacyCoordToPixels(rawWidth);
81
+ const heightPx = this.legacyCoordToPixels(rawHeight);
82
+ if (widthPx && heightPx) {
83
+ return { widthPx, heightPx, rawWidth, rawHeight };
84
+ }
85
+ }
86
+ if (record.children && record.children.length > 0) {
87
+ const nestedSize = this.extractPresentationMetrics(record.children);
88
+ if (nestedSize) {
89
+ return nestedSize;
90
+ }
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+ static legacyCoordToPixels(value) {
96
+ if (!Number.isFinite(value) || value <= 0) {
97
+ return null;
98
+ }
99
+ // Many legacy .ppt decks store slide geometry in PowerPoint master units
100
+ // (for example 5760 x 4320 for a 10 x 7.5 inch slide), not points.
101
+ if (value >= 2000) {
102
+ return Math.max(1, Math.round(value / 6));
103
+ }
104
+ return Math.max(1, Math.round(value * this.POINTS_TO_PX));
105
+ }
106
+ static parseCfb(file) {
107
+ return parseCfbContainer(file);
108
+ }
109
+ static parseRecords(buffer, start, end) {
110
+ return parseRecordTree(buffer, start, end);
111
+ }
112
+ static collectSlideContainers(records) {
113
+ const discoveredSlides = [];
114
+ const visit = (list) => {
115
+ list.forEach((r) => {
116
+ // RT_Slide (commonly 1006)
117
+ if (r.recType === 1006) {
118
+ discoveredSlides.push(r);
119
+ }
120
+ if (r.children && r.children.length > 0) {
121
+ visit(r.children);
122
+ }
123
+ });
124
+ };
125
+ visit(records);
126
+ const orderedPersistRefs = this.extractOrderedSlidePersistRefs(records);
127
+ if (orderedPersistRefs.length === 0) {
128
+ return discoveredSlides;
129
+ }
130
+ const slideByPersistRef = new Map();
131
+ discoveredSlides.forEach((slide) => {
132
+ if (!slideByPersistRef.has(slide.recInstance)) {
133
+ slideByPersistRef.set(slide.recInstance, slide);
134
+ }
135
+ });
136
+ const orderedSlides = [];
137
+ const seen = new Set();
138
+ orderedPersistRefs.forEach((persistRef) => {
139
+ const slide = slideByPersistRef.get(persistRef);
140
+ if (!slide || seen.has(slide)) {
141
+ return;
142
+ }
143
+ seen.add(slide);
144
+ orderedSlides.push(slide);
145
+ });
146
+ return orderedSlides.length > 0 ? orderedSlides : discoveredSlides;
147
+ }
148
+ static collectPrimaryMasterContainer(records) {
149
+ let found = null;
150
+ const visit = (list) => {
151
+ for (const record of list) {
152
+ if (record.recType === 1016 && !found) {
153
+ found = record;
154
+ return;
155
+ }
156
+ if (record.children && record.children.length > 0) {
157
+ visit(record.children);
158
+ if (found) {
159
+ return;
160
+ }
161
+ }
162
+ }
163
+ };
164
+ visit(records);
165
+ return found;
166
+ }
167
+ static extractOrderedSlidePersistRefs(records) {
168
+ const refs = [];
169
+ const visit = (list) => {
170
+ list.forEach((record) => {
171
+ if (record.recType === 1000 && record.children && record.children.length > 0) {
172
+ record.children.forEach((child) => {
173
+ if (child.recType !== 4080 || !child.children || child.children.length === 0) {
174
+ return;
175
+ }
176
+ child.children.forEach((entry) => {
177
+ if (entry.recType !== 1011 || entry.payload.length < 4) {
178
+ return;
179
+ }
180
+ refs.push(entry.payload.readUInt32LE(0));
181
+ });
182
+ });
183
+ }
184
+ if (record.children && record.children.length > 0) {
185
+ visit(record.children);
186
+ }
187
+ });
188
+ };
189
+ visit(records);
190
+ return refs.filter((value) => Number.isFinite(value) && value > 0);
191
+ }
192
+ static buildSlides(slideRecords, pictures, outlineTextByPersistId, defaultColorScheme, masterRecordOrPresentationMetrics, presentationMetricsOrWidth, widthPxOrHeight, heightPxOrPicturesById, picturesByIdMaybe) {
193
+ const { masterRecord, presentationMetrics, widthPx, heightPx, picturesById } = normalizeBuildSlidesArgs(slideRecords, pictures, outlineTextByPersistId, defaultColorScheme, masterRecordOrPresentationMetrics, presentationMetricsOrWidth, widthPxOrHeight, heightPxOrPicturesById, picturesByIdMaybe);
194
+ const slides = [];
195
+ let sequentialPictureIndex = 0;
196
+ for (let i = 0; i < slideRecords.length; i++) {
197
+ const slideRecord = slideRecords[i];
198
+ const outlineBlocks = outlineTextByPersistId.get(slideRecord.recInstance) ?? [];
199
+ const colorScheme = this.extractSlideColorScheme(slideRecord) ?? defaultColorScheme;
200
+ const layout = this.extractSlideLayoutInfo(slideRecord);
201
+ const spContainerCounter = { value: 0 };
202
+ const visualSlots = this.decorateVisualSlotsWithPlaceholders(this.extractVisualSlotsFromRecord(slideRecord, spContainerCounter), layout);
203
+ // Reset counter so both extractions share the same DFS index space
204
+ spContainerCounter.value = 0;
205
+ const shapeTextGroups = this.decorateTextGroupsWithPlaceholders(this.extractShapeTextGroupsFromRecord(slideRecord, spContainerCounter), layout);
206
+ const directBlocks = this.extractTypedTextBlocksFromRecord(slideRecord);
207
+ const styledShapeBlocks = this.extractStyledTextBlocksFromShapes(slideRecord);
208
+ const styledBounds = styledShapeBlocks
209
+ .map((block) => block.bounds)
210
+ .filter((bounds) => !!bounds);
211
+ const shouldPreferGroupedShapeText = this.shouldPreferShapeTextGroups(outlineBlocks, shapeTextGroups);
212
+ const groupedShapeTextBlocks = shapeTextGroups.length > 0
213
+ ? this.flattenTextGroups(shapeTextGroups)
214
+ : [];
215
+ // Enrich outline blocks with SpContainer bounds by matching content
216
+ const normalizedOutline = outlineBlocks.length > 0 ? this.normalizeTextBlocks(outlineBlocks) : [];
217
+ if (normalizedOutline.length > 0 && groupedShapeTextBlocks.length > 0) {
218
+ const usedGroupIndices = new Set();
219
+ normalizedOutline.forEach((block) => {
220
+ if (block.bounds) {
221
+ return;
222
+ }
223
+ const blockText = block.text.replace(/\s+/g, '');
224
+ const match = { block: null, index: -1, score: 0 };
225
+ groupedShapeTextBlocks.forEach((grouped, groupIndex) => {
226
+ if (usedGroupIndices.has(groupIndex)) {
227
+ return;
228
+ }
229
+ const groupedText = grouped.text.replace(/\s+/g, '');
230
+ if (blockText === groupedText) {
231
+ match.block = grouped;
232
+ match.index = groupIndex;
233
+ match.score = 1;
234
+ }
235
+ else if (match.score < 0.5 && blockText.length > 4 && groupedText.length > 4) {
236
+ if (blockText.includes(groupedText) || groupedText.includes(blockText)) {
237
+ match.block = grouped;
238
+ match.index = groupIndex;
239
+ match.score = 0.5;
240
+ }
241
+ }
242
+ });
243
+ if (match.block && match.index >= 0) {
244
+ block.bounds = match.block.bounds;
245
+ block.spContainerIndex = match.block.spContainerIndex;
246
+ if (!block.fillColor && match.block.fillColor) {
247
+ block.fillColor = match.block.fillColor;
248
+ }
249
+ if (!block.borderColor && match.block.borderColor) {
250
+ block.borderColor = match.block.borderColor;
251
+ }
252
+ if (match.block.borderWidthPx !== undefined && block.borderWidthPx === undefined) {
253
+ block.borderWidthPx = match.block.borderWidthPx;
254
+ }
255
+ usedGroupIndices.add(match.index);
256
+ }
257
+ });
258
+ }
259
+ const baseTextBlocks = normalizedOutline.length > 0 && !shouldPreferGroupedShapeText
260
+ ? this.decorateTextBlocksWithPlaceholders(normalizedOutline, layout)
261
+ : groupedShapeTextBlocks.length > 0
262
+ ? groupedShapeTextBlocks
263
+ : styledShapeBlocks.length > 0
264
+ ? this.decorateTextBlocksWithPlaceholders(this.coalesceTextBlocksByBounds(this.normalizeTextBlocks(styledShapeBlocks)), layout)
265
+ : directBlocks.length > 0
266
+ ? this.decorateTextBlocksWithPlaceholders(this.normalizeTextBlocks(directBlocks), layout)
267
+ : this.extractTextsFromRecord(slideRecord).slice(0, 24).map((text) => ({ text }));
268
+ const mergedTextBlocks = this.mergeStyledTextBlocks(baseTextBlocks, styledShapeBlocks, layout, widthPx, heightPx, presentationMetrics);
269
+ const provisionalTextBlocks = this.coalesceOverlappingTextBlocks(mergedTextBlocks, widthPx, heightPx, presentationMetrics);
270
+ const textBlocks = provisionalTextBlocks;
271
+ const elements = [];
272
+ const titleBlocks = textBlocks.filter((block) => {
273
+ const role = this.classifyPlaceholderType(block.placeholderType);
274
+ return role === 'title' || role === 'subtitle' || this.isLikelyTitleBlock(block, widthPx, heightPx, presentationMetrics);
275
+ });
276
+ const bodyBlocks = textBlocks.filter((block) => {
277
+ const role = this.classifyPlaceholderType(block.placeholderType);
278
+ return role !== 'title' && role !== 'subtitle' && !this.isLikelyTitleBlock(block, widthPx, heightPx, presentationMetrics);
279
+ });
280
+ const placedTextFrames = [];
281
+ const fixedElements = new Set();
282
+ const rightPanelFrames = this.computeRightPanelTextFrames(textBlocks, widthPx, heightPx, presentationMetrics);
283
+ if (textBlocks.length > 0) {
284
+ textBlocks.forEach((block, idx) => {
285
+ const placeholderRole = this.classifyPlaceholderType(block.placeholderType);
286
+ const isTitle = placeholderRole === 'title'
287
+ || this.isLikelyTitleBlock(block, widthPx, heightPx, presentationMetrics)
288
+ || (idx === 0
289
+ && titleBlocks.length === 0
290
+ && !block.bounds
291
+ && block.text.length <= 60
292
+ && !block.text.includes('\r'));
293
+ const defaultTextColor = isTitle
294
+ ? colorScheme?.titleColor
295
+ : colorScheme?.textColor;
296
+ const effectiveFontSize = block.fontSizePx;
297
+ const paragraphs = this.createParagraphsFromText(block.text, this.isBulletTextType(block.textType, !isTitle) && placeholderRole !== 'subtitle', block.color ?? defaultTextColor, effectiveFontSize);
298
+ const roleIndex = isTitle
299
+ ? titleBlocks.indexOf(block)
300
+ : Math.max(0, bodyBlocks.indexOf(block));
301
+ const frame = this.computeTextFrame(layout, placeholderRole === 'subtitle'
302
+ ? 'subtitle'
303
+ : isTitle
304
+ ? 'title'
305
+ : 'body', roleIndex, Math.max(1, titleBlocks.length), Math.max(1, bodyBlocks.length), widthPx, heightPx);
306
+ const candidateFrame = block.bounds
307
+ ? this.normalizeBounds(block.bounds, widthPx, heightPx, presentationMetrics)
308
+ : frame;
309
+ const panelFrame = rightPanelFrames?.get(block);
310
+ const hasSpContainerBounds = block.bounds && block.spContainerIndex !== undefined;
311
+ const positionedFrame = panelFrame
312
+ ? panelFrame
313
+ : hasSpContainerBounds
314
+ ? candidateFrame
315
+ : block.bounds
316
+ ? this.resolveTextFrame(candidateFrame, frame, isTitle, widthPx, heightPx, placedTextFrames)
317
+ : frame;
318
+ const estimatedLineHeight = 30;
319
+ const avgCharWidthPx = 16;
320
+ const bulletPrefixPx = paragraphs.some((p) => p.bullet) ? 30 : 0;
321
+ const containerWidth = Math.max(positionedFrame.width - 4 - bulletPrefixPx, 40);
322
+ const charsPerLine = Math.max(1, Math.floor(containerWidth / avgCharWidthPx));
323
+ const estimatedLines = paragraphs.reduce((sum, p) => {
324
+ const textLength = p.text?.length || 0;
325
+ return sum + Math.max(1, Math.ceil(textLength / charsPerLine));
326
+ }, 0);
327
+ const contentHeight = estimatedLines * estimatedLineHeight;
328
+ const height = hasSpContainerBounds
329
+ ? positionedFrame.height
330
+ : isTitle
331
+ ? Math.max(positionedFrame.height, 72, contentHeight)
332
+ : Math.max(positionedFrame.height, contentHeight, 36);
333
+ const el = {
334
+ type: 'text',
335
+ x: positionedFrame.x,
336
+ y: positionedFrame.y,
337
+ width: positionedFrame.width,
338
+ height,
339
+ zIndex: block.spContainerIndex ?? idx,
340
+ isTitle,
341
+ paragraphs,
342
+ fillColor: block.fillVisible === false ? undefined : block.fillColor,
343
+ borderColor: block.borderVisible === false ? undefined : block.borderColor,
344
+ borderWidthPx: block.borderVisible === false ? undefined : block.borderWidthPx
345
+ };
346
+ elements.push(el);
347
+ if (hasSpContainerBounds) {
348
+ fixedElements.add(el);
349
+ }
350
+ placedTextFrames.push(positionedFrame);
351
+ });
352
+ }
353
+ this.resolveTextElementOverlaps(elements, heightPx, fixedElements);
354
+ // Minimal image support: use discovered picture/object frames when available.
355
+ const preferredSlots = visualSlots.filter((slot) => !!slot.bounds && (this.isVisualPlaceholder(slot.placeholderType) || slot.imageRefId !== undefined));
356
+ const slotsWithImageRefs = preferredSlots.filter((slot) => slot.imageRefId !== undefined);
357
+ const hasVisualPlaceholders = preferredSlots.some((slot) => this.isVisualPlaceholder(slot.placeholderType));
358
+ const isShapeHeavySlide = slotsWithImageRefs.length === 0 && !hasVisualPlaceholders && visualSlots.length > 8;
359
+ const boundedSlots = slotsWithImageRefs.length > 0
360
+ ? this.dedupeVisualSlots(this.selectPreferredImageRefSlots(slotsWithImageRefs, picturesById))
361
+ : isShapeHeavySlide
362
+ ? []
363
+ : this.dedupeVisualSlots((preferredSlots.length > 0
364
+ ? preferredSlots
365
+ : visualSlots.filter((slot) => !!slot.bounds))
366
+ .filter((slot) => !this.isDecorativeImageSlot(slot, widthPx, heightPx)));
367
+ let imageSlots = this.selectImageSlotsForSlide(boundedSlots, widthPx, heightPx, false).filter((slot) => {
368
+ if (!slot.bounds || slot.imageRefId !== undefined || boundedSlots.length < 4) {
369
+ return true;
370
+ }
371
+ const aspectRatio = slot.bounds.width / Math.max(1, slot.bounds.height);
372
+ return !(aspectRatio > 4.5 && slot.bounds.y < 1000);
373
+ });
374
+ const usedImageSlots = new Set();
375
+ if (imageSlots.length > 0) {
376
+ let fallbackImageIndex = 0;
377
+ const sequentialBaseIndex = sequentialPictureIndex;
378
+ let highestPictureIndexUsed = sequentialPictureIndex - 1;
379
+ imageSlots.forEach((slot) => {
380
+ const img = (slot.imageRefId !== undefined
381
+ ? picturesById?.get(slot.imageRefId)
382
+ : undefined) ?? pictures[sequentialBaseIndex + fallbackImageIndex++];
383
+ if (!img || !slot.bounds) {
384
+ return;
385
+ }
386
+ const scaledImageFrame = this.normalizeBounds(slot.bounds, widthPx, heightPx, presentationMetrics);
387
+ const imageFrame = this.adjustImageFrameForTextColumns(this.adjustLegacyImageFrame(slot, scaledImageFrame, widthPx, heightPx, false), elements, widthPx, heightPx);
388
+ elements.push({
389
+ type: 'image',
390
+ x: imageFrame.x,
391
+ y: imageFrame.y,
392
+ width: imageFrame.width,
393
+ height: imageFrame.height,
394
+ zIndex: slot.spContainerIndex ?? (100 + elements.length),
395
+ src: `data:${img.mime};base64,${img.base64}`
396
+ });
397
+ usedImageSlots.add(slot);
398
+ if (typeof img.pictureIndex === 'number' && img.pictureIndex >= 0) {
399
+ highestPictureIndexUsed = Math.max(highestPictureIndexUsed, img.pictureIndex);
400
+ }
401
+ });
402
+ sequentialPictureIndex = Math.max(sequentialPictureIndex + fallbackImageIndex, highestPictureIndexUsed + 1);
403
+ }
404
+ else if (!isShapeHeavySlide) {
405
+ const img = pictures[sequentialPictureIndex++];
406
+ if (img) {
407
+ elements.push({
408
+ type: 'image',
409
+ x: 72,
410
+ y: 180,
411
+ width: 816,
412
+ height: 420,
413
+ zIndex: 100,
414
+ src: `data:${img.mime};base64,${img.base64}`
415
+ });
416
+ }
417
+ }
418
+ visualSlots.forEach((slot, index) => {
419
+ if (!slot.bounds || usedImageSlots.has(slot) || slot.isTextSlot || slot.imageRefId !== undefined) {
420
+ return;
421
+ }
422
+ if (!slot.fillColor && !slot.borderColor) {
423
+ return;
424
+ }
425
+ const shapeFrame = this.normalizeBounds(slot.bounds, widthPx, heightPx, presentationMetrics);
426
+ const isBackgroundLikeShape = shapeFrame.width * shapeFrame.height >= widthPx * heightPx * 0.45;
427
+ const overlapsImageFrame = elements.some((element) => element.type === 'image'
428
+ && this.isNearDuplicateFrame(shapeFrame, {
429
+ x: element.x,
430
+ y: element.y,
431
+ width: element.width,
432
+ height: element.height
433
+ }));
434
+ if (overlapsImageFrame) {
435
+ return;
436
+ }
437
+ if (!slot.imageRefId
438
+ && styledBounds.some((bounds) => {
439
+ const styledFrame = this.normalizeBounds(bounds, widthPx, heightPx, presentationMetrics);
440
+ const shapeArea = shapeFrame.width * shapeFrame.height;
441
+ const styledArea = styledFrame.width * styledFrame.height;
442
+ // Don't filter large background shapes just because small text overlaps
443
+ if (shapeArea > styledArea * 3) {
444
+ return false;
445
+ }
446
+ return this.boundsOverlapRatio(shapeFrame, styledFrame) > 0.7;
447
+ })) {
448
+ return;
449
+ }
450
+ if (elements.some((element) => element.type === 'image'
451
+ && this.boundsOverlapRatio(shapeFrame, {
452
+ x: element.x,
453
+ y: element.y,
454
+ width: element.width,
455
+ height: element.height
456
+ }) > 0.72)) {
457
+ return;
458
+ }
459
+ elements.push({
460
+ type: 'shape',
461
+ x: shapeFrame.x,
462
+ y: shapeFrame.y,
463
+ width: shapeFrame.width,
464
+ height: shapeFrame.height,
465
+ zIndex: isBackgroundLikeShape ? -2 : (slot.spContainerIndex ?? (50 + index)),
466
+ fillColor: slot.fillVisible === false ? undefined : slot.fillColor,
467
+ borderColor: slot.borderVisible === false ? undefined : slot.borderColor,
468
+ borderWidthPx: slot.borderVisible === false ? undefined : slot.borderWidthPx
469
+ });
470
+ });
471
+ this.applyPanelBackgroundShape(elements, visualSlots, widthPx, heightPx, presentationMetrics);
472
+ this.applyTableGridBorders(elements, slideRecord, widthPx, heightPx, presentationMetrics);
473
+ this.demoteDecorativeImages(elements);
474
+ this.ensureTextAboveShapes(elements);
475
+ this.resolveTextImageOverlaps(elements, widthPx, heightPx, fixedElements);
476
+ this.resolveTextElementOverlaps(elements, heightPx, fixedElements);
477
+ if (textBlocks.length === 0 && elements.filter((element) => element.type === 'image').length <= 1 && masterRecord) {
478
+ this.applyMasterFallbackElements(elements, masterRecord, picturesById, presentationMetrics, widthPx, heightPx);
479
+ }
480
+ this.applyMasterBackgroundImage(elements, masterRecord, picturesById, presentationMetrics, widthPx, heightPx);
481
+ this.pruneActivityListImageArtifacts(elements, widthPx, heightPx);
482
+ this.pruneImageOnlySlideArtifacts(elements, widthPx, heightPx);
483
+ this.applyMasterDecorativeImages(elements, masterRecord, picturesById, presentationMetrics, widthPx, heightPx);
484
+ slides.push({
485
+ slideNumber: i + 1,
486
+ widthPx,
487
+ heightPx,
488
+ backgroundColor: colorScheme?.backgroundColor || '#ffffff',
489
+ elements
490
+ });
491
+ }
492
+ return slides;
493
+ }
494
+ static extractSlideLayoutInfo(record) {
495
+ const slideAtom = (record.children ?? []).find((child) => child.recType === 1007 && child.payload.length >= 12);
496
+ if (!slideAtom) {
497
+ return null;
498
+ }
499
+ return {
500
+ geom: slideAtom.payload.readUInt32LE(0),
501
+ placeholders: Array.from(slideAtom.payload.subarray(4, 12))
502
+ };
503
+ }
504
+ static computeTextFrame(layout, role, index, titleCount, bodyCount, slideWidth, slideHeight) {
505
+ const marginX = Math.round(slideWidth * 0.075);
506
+ const titleTop = Math.round(slideHeight * 0.105);
507
+ const contentTop = Math.round(slideHeight * 0.235);
508
+ const fullWidth = Math.round(slideWidth * 0.85);
509
+ if (role === 'subtitle') {
510
+ if (layout?.geom === 0x00000000) {
511
+ return {
512
+ x: Math.round(slideWidth * 0.18),
513
+ y: Math.round(slideHeight * 0.38),
514
+ width: Math.round(slideWidth * 0.64),
515
+ height: Math.round(slideHeight * 0.1)
516
+ };
517
+ }
518
+ return {
519
+ x: marginX,
520
+ y: Math.round(slideHeight * 0.22),
521
+ width: fullWidth,
522
+ height: Math.round(slideHeight * 0.1)
523
+ };
524
+ }
525
+ if (role === 'title') {
526
+ if (titleCount > 1) {
527
+ const top = index === 0 ? Math.round(slideHeight * 0.1) : Math.round(slideHeight * 0.16);
528
+ const height = index === 0 ? Math.round(slideHeight * 0.08) : Math.round(slideHeight * 0.14);
529
+ return {
530
+ x: Math.round(slideWidth * 0.18),
531
+ y: top,
532
+ width: Math.round(slideWidth * 0.64),
533
+ height
534
+ };
535
+ }
536
+ if (layout?.geom === 0x00000000 || layout?.geom === 0x00000002) {
537
+ return {
538
+ x: Math.round(slideWidth * 0.1),
539
+ y: Math.round(slideHeight * 0.18),
540
+ width: Math.round(slideWidth * 0.8),
541
+ height: Math.round(slideHeight * 0.12)
542
+ };
543
+ }
544
+ if (layout?.geom === 0x00000011 || layout?.geom === 0x00000012) {
545
+ return {
546
+ x: Math.round(slideWidth * 0.78),
547
+ y: Math.round(slideHeight * 0.14),
548
+ width: Math.round(slideWidth * 0.14),
549
+ height: Math.round(slideHeight * 0.68)
550
+ };
551
+ }
552
+ return {
553
+ x: marginX,
554
+ y: titleTop,
555
+ width: fullWidth,
556
+ height: Math.round(slideHeight * 0.1)
557
+ };
558
+ }
559
+ const safeBodyCount = Math.max(1, bodyCount);
560
+ if (layout?.geom === 0x00000008) {
561
+ const gap = Math.round(slideWidth * 0.04);
562
+ const columnWidth = Math.round((fullWidth - gap) / 2);
563
+ return {
564
+ x: marginX + (index % 2) * (columnWidth + gap),
565
+ y: contentTop,
566
+ width: columnWidth,
567
+ height: Math.round(slideHeight * 0.56)
568
+ };
569
+ }
570
+ if (layout?.geom === 0x00000009) {
571
+ const gap = Math.round(slideHeight * 0.04);
572
+ const rowHeight = Math.round((slideHeight * 0.58 - gap) / 2);
573
+ return {
574
+ x: marginX,
575
+ y: contentTop + (index % 2) * (rowHeight + gap),
576
+ width: fullWidth,
577
+ height: rowHeight
578
+ };
579
+ }
580
+ if (layout?.geom === 0x0000000e) {
581
+ const gapX = Math.round(slideWidth * 0.035);
582
+ const gapY = Math.round(slideHeight * 0.03);
583
+ const width = Math.round((fullWidth - gapX) / 2);
584
+ const height = Math.round((slideHeight * 0.58 - gapY) / 2);
585
+ return {
586
+ x: marginX + (index % 2) * (width + gapX),
587
+ y: contentTop + Math.floor(index / 2) * (height + gapY),
588
+ width,
589
+ height
590
+ };
591
+ }
592
+ if (layout?.geom === 0x00000011) {
593
+ return {
594
+ x: Math.round(slideWidth * 0.08),
595
+ y: Math.round(slideHeight * 0.14),
596
+ width: Math.round(slideWidth * 0.62),
597
+ height: Math.round(slideHeight * 0.68)
598
+ };
599
+ }
600
+ if (layout?.geom === 0x00000012) {
601
+ const gap = Math.round(slideWidth * 0.035);
602
+ const width = Math.round((slideWidth * 0.62 - gap) / 2);
603
+ return {
604
+ x: Math.round(slideWidth * 0.08) + (index % 2) * (width + gap),
605
+ y: Math.round(slideHeight * 0.18),
606
+ width,
607
+ height: Math.round(slideHeight * 0.58)
608
+ };
609
+ }
610
+ if (layout?.geom === 0x0000000a || layout?.geom === 0x0000000b || layout?.geom === 0x0000000d) {
611
+ const primaryWidth = Math.round(slideWidth * 0.54);
612
+ const sideWidth = fullWidth - primaryWidth - Math.round(slideWidth * 0.04);
613
+ const leftX = marginX;
614
+ const rightX = marginX + primaryWidth + Math.round(slideWidth * 0.04);
615
+ const topY = contentTop;
616
+ const halfHeight = Math.round(slideHeight * 0.27);
617
+ if (layout.geom === 0x0000000a) {
618
+ return index === 0
619
+ ? { x: leftX, y: topY, width: primaryWidth, height: Math.round(slideHeight * 0.58) }
620
+ : { x: rightX, y: topY + (index - 1) * (halfHeight + Math.round(slideHeight * 0.04)), width: sideWidth, height: halfHeight };
621
+ }
622
+ if (layout.geom === 0x0000000b) {
623
+ return index < 2
624
+ ? { x: leftX, y: topY + index * (halfHeight + Math.round(slideHeight * 0.04)), width: primaryWidth, height: halfHeight }
625
+ : { x: rightX, y: topY, width: sideWidth, height: Math.round(slideHeight * 0.58) };
626
+ }
627
+ return index < 2
628
+ ? { x: leftX + index * Math.round((primaryWidth + Math.round(slideWidth * 0.04)) / 2), y: topY, width: Math.round((primaryWidth - Math.round(slideWidth * 0.04)) / 2), height: halfHeight }
629
+ : { x: leftX, y: topY + halfHeight + Math.round(slideHeight * 0.04), width: fullWidth, height: halfHeight };
630
+ }
631
+ if (layout?.geom === 0x0000000f) {
632
+ return {
633
+ x: marginX,
634
+ y: Math.round(slideHeight * 0.16),
635
+ width: fullWidth,
636
+ height: Math.round(slideHeight * 0.68)
637
+ };
638
+ }
639
+ const stackedGap = Math.round(slideHeight * 0.03);
640
+ const availableHeight = Math.round(slideHeight * 0.58);
641
+ const boxHeight = Math.max(48, Math.round((availableHeight - stackedGap * Math.max(0, safeBodyCount - 1)) / safeBodyCount));
642
+ return {
643
+ x: marginX,
644
+ y: contentTop + index * (boxHeight + stackedGap),
645
+ width: fullWidth,
646
+ height: boxHeight
647
+ };
648
+ }
649
+ static normalizeTextBlocks(blocks) {
650
+ const normalized = [];
651
+ const seen = new Set();
652
+ blocks.forEach((block) => {
653
+ const text = (block.text || '')
654
+ .replace(/\u0000/g, '')
655
+ .replace(/\r\n/g, '\r')
656
+ .replace(/\n/g, '\r')
657
+ .trim();
658
+ if (!text || this.isNoiseText(text)) {
659
+ return;
660
+ }
661
+ const key = `${block.textType ?? -1}:${text}`;
662
+ if (seen.has(key)) {
663
+ return;
664
+ }
665
+ seen.add(key);
666
+ normalized.push({
667
+ text,
668
+ textType: block.textType,
669
+ placeholderType: block.placeholderType,
670
+ bounds: block.bounds,
671
+ color: block.color,
672
+ fontSizePx: block.fontSizePx,
673
+ fillColor: block.fillColor,
674
+ borderColor: block.borderColor,
675
+ borderWidthPx: block.borderWidthPx,
676
+ fillVisible: block.fillVisible,
677
+ borderVisible: block.borderVisible,
678
+ spContainerIndex: block.spContainerIndex
679
+ });
680
+ });
681
+ return normalized;
682
+ }
683
+ static mergeStyledTextBlocks(baseBlocks, styledBlocks, layout, slideWidth, slideHeight, presentationMetrics) {
684
+ if (styledBlocks.length === 0) {
685
+ return baseBlocks;
686
+ }
687
+ const merged = [...baseBlocks];
688
+ const normalizedStyledBlocks = this.decorateTextBlocksWithPlaceholders(this.coalesceTextBlocksByBounds(this.normalizeTextBlocks(styledBlocks)), layout);
689
+ normalizedStyledBlocks.forEach((candidate) => {
690
+ const duplicate = merged.some((existing) => {
691
+ if (existing.text.replace(/\s+/g, '') === candidate.text.replace(/\s+/g, '')) {
692
+ return true;
693
+ }
694
+ if (!existing.bounds || !candidate.bounds) {
695
+ return false;
696
+ }
697
+ // Only treat as duplicate by bounds if text also has some similarity
698
+ const existingText = existing.text.replace(/\s+/g, '');
699
+ const candidateText = candidate.text.replace(/\s+/g, '');
700
+ const hasTextOverlap = existingText.length > 3 && candidateText.length > 3
701
+ && (existingText.includes(candidateText) || candidateText.includes(existingText));
702
+ if (!hasTextOverlap) {
703
+ return false;
704
+ }
705
+ const existingBounds = this.normalizeBounds(existing.bounds, slideWidth, slideHeight, presentationMetrics);
706
+ const candidateBounds = this.normalizeBounds(candidate.bounds, slideWidth, slideHeight, presentationMetrics);
707
+ return this.boundsOverlapRatio(existingBounds, candidateBounds) > 0.82;
708
+ });
709
+ if (!duplicate) {
710
+ merged.push(candidate);
711
+ }
712
+ });
713
+ return merged;
714
+ }
715
+ static coalesceTextBlocksByBounds(blocks) {
716
+ const merged = new Map();
717
+ blocks.forEach((block) => {
718
+ const boundsKey = block.bounds
719
+ ? `${block.bounds.x}:${block.bounds.y}:${block.bounds.width}:${block.bounds.height}`
720
+ : 'no-bounds';
721
+ const key = [
722
+ block.placeholderType ?? -1,
723
+ block.textType ?? -1,
724
+ boundsKey,
725
+ block.color ?? '',
726
+ block.fontSizePx ?? -1
727
+ ].join('|');
728
+ const existing = merged.get(key);
729
+ if (!existing) {
730
+ merged.set(key, { ...block });
731
+ return;
732
+ }
733
+ const nextText = block.text.trim();
734
+ const existingLines = new Set(existing.text.split('\r').map((line) => line.trim()).filter(Boolean));
735
+ if (nextText && !existingLines.has(nextText)) {
736
+ existing.text = `${existing.text}\r${nextText}`.trim();
737
+ }
738
+ });
739
+ return Array.from(merged.values());
740
+ }
741
+ static coalesceOverlappingTextBlocks(blocks, slideWidth, slideHeight, presentationMetrics) {
742
+ const merged = [];
743
+ blocks
744
+ .slice()
745
+ .sort((left, right) => {
746
+ const leftBounds = left.bounds
747
+ ? this.normalizeBounds(left.bounds, slideWidth, slideHeight, presentationMetrics)
748
+ : { x: 0, y: 0, width: 0, height: 0 };
749
+ const rightBounds = right.bounds
750
+ ? this.normalizeBounds(right.bounds, slideWidth, slideHeight, presentationMetrics)
751
+ : { x: 0, y: 0, width: 0, height: 0 };
752
+ return leftBounds.y - rightBounds.y || leftBounds.x - rightBounds.x;
753
+ })
754
+ .forEach((block) => {
755
+ if (!block.bounds) {
756
+ merged.push(block);
757
+ return;
758
+ }
759
+ const normalizedBounds = this.normalizeBounds(block.bounds, slideWidth, slideHeight, presentationMetrics);
760
+ const blockIsTitle = this.isLikelyTitleBlock(block, slideWidth, slideHeight, presentationMetrics);
761
+ const existing = merged.find((candidate) => {
762
+ if (!candidate.bounds) {
763
+ return false;
764
+ }
765
+ const candidateIsTitle = this.isLikelyTitleBlock(candidate, slideWidth, slideHeight, presentationMetrics);
766
+ if (candidateIsTitle !== blockIsTitle) {
767
+ return false;
768
+ }
769
+ const candidateBounds = this.normalizeBounds(candidate.bounds, slideWidth, slideHeight, presentationMetrics);
770
+ if (!blockIsTitle
771
+ && block.textType === 4
772
+ && candidate.textType === 4
773
+ && normalizedBounds.x >= slideWidth * 0.45
774
+ && candidateBounds.x >= slideWidth * 0.45) {
775
+ return false;
776
+ }
777
+ if (candidateBounds.y < slideHeight * 0.2 || normalizedBounds.y < slideHeight * 0.2) {
778
+ return false;
779
+ }
780
+ return this.boundsOverlapRatio(candidateBounds, normalizedBounds) > 0.55;
781
+ });
782
+ if (!existing) {
783
+ merged.push({ ...block });
784
+ return;
785
+ }
786
+ const existingLines = new Set(existing.text.split('\r').map((line) => line.trim()).filter(Boolean));
787
+ const nextLines = block.text.split('\r').map((line) => line.trim()).filter(Boolean);
788
+ const sharedLines = nextLines.filter((line) => existingLines.has(line));
789
+ if (!blockIsTitle && sharedLines.length === 0) {
790
+ merged.push({ ...block });
791
+ return;
792
+ }
793
+ nextLines.forEach((line) => {
794
+ if (!existingLines.has(line)) {
795
+ existing.text = `${existing.text}\r${line}`.trim();
796
+ existingLines.add(line);
797
+ }
798
+ });
799
+ const existingBounds = existing.bounds;
800
+ existing.bounds = {
801
+ x: Math.min(existingBounds.x, block.bounds.x),
802
+ y: Math.min(existingBounds.y, block.bounds.y),
803
+ width: Math.max(existingBounds.x + existingBounds.width, block.bounds.x + block.bounds.width) - Math.min(existingBounds.x, block.bounds.x),
804
+ height: Math.max(existingBounds.y + existingBounds.height, block.bounds.y + block.bounds.height) - Math.min(existingBounds.y, block.bounds.y)
805
+ };
806
+ });
807
+ return merged;
808
+ }
809
+ static flattenTextGroups(groups) {
810
+ const flattened = [];
811
+ groups.forEach((group) => {
812
+ const normalizedBlocks = this.normalizeTextBlocks(group.blocks);
813
+ if (normalizedBlocks.length === 0) {
814
+ return;
815
+ }
816
+ const titleCandidate = normalizedBlocks.find((block) => this.isTitleTextType(block.textType));
817
+ const bodyCandidates = normalizedBlocks.filter((block) => !this.isTitleTextType(block.textType));
818
+ if (titleCandidate && bodyCandidates.length > 0) {
819
+ flattened.push({
820
+ ...titleCandidate,
821
+ placeholderType: titleCandidate.placeholderType ?? group.placeholderType,
822
+ bounds: titleCandidate.bounds ?? group.bounds,
823
+ fillColor: titleCandidate.fillColor ?? group.fillColor,
824
+ borderColor: titleCandidate.borderColor ?? group.borderColor,
825
+ borderWidthPx: titleCandidate.borderWidthPx ?? group.borderWidthPx,
826
+ color: titleCandidate.color ?? group.blocks[0]?.color,
827
+ fontSizePx: titleCandidate.fontSizePx ?? group.blocks[0]?.fontSizePx,
828
+ fillVisible: titleCandidate.fillVisible ?? group.fillVisible,
829
+ borderVisible: titleCandidate.borderVisible ?? group.borderVisible,
830
+ spContainerIndex: group.spContainerIndex
831
+ });
832
+ flattened.push({
833
+ text: bodyCandidates.map((block) => block.text).join('\r'),
834
+ textType: bodyCandidates[0].textType,
835
+ placeholderType: bodyCandidates[0].placeholderType ?? group.placeholderType,
836
+ bounds: bodyCandidates[0].bounds ?? group.bounds,
837
+ color: bodyCandidates[0].color,
838
+ fontSizePx: bodyCandidates[0].fontSizePx,
839
+ fillColor: bodyCandidates[0].fillColor ?? group.fillColor,
840
+ borderColor: bodyCandidates[0].borderColor ?? group.borderColor,
841
+ borderWidthPx: bodyCandidates[0].borderWidthPx ?? group.borderWidthPx,
842
+ fillVisible: bodyCandidates[0].fillVisible ?? group.fillVisible,
843
+ borderVisible: bodyCandidates[0].borderVisible ?? group.borderVisible,
844
+ spContainerIndex: group.spContainerIndex
845
+ });
846
+ return;
847
+ }
848
+ flattened.push({
849
+ text: normalizedBlocks.map((block) => block.text).join('\r'),
850
+ textType: normalizedBlocks[0].textType,
851
+ placeholderType: normalizedBlocks[0].placeholderType ?? group.placeholderType,
852
+ bounds: normalizedBlocks[0].bounds ?? group.bounds,
853
+ color: normalizedBlocks[0].color,
854
+ fontSizePx: normalizedBlocks[0].fontSizePx,
855
+ fillColor: normalizedBlocks[0].fillColor ?? group.fillColor,
856
+ borderColor: normalizedBlocks[0].borderColor ?? group.borderColor,
857
+ borderWidthPx: normalizedBlocks[0].borderWidthPx ?? group.borderWidthPx,
858
+ fillVisible: normalizedBlocks[0].fillVisible ?? group.fillVisible,
859
+ borderVisible: normalizedBlocks[0].borderVisible ?? group.borderVisible,
860
+ spContainerIndex: group.spContainerIndex
861
+ });
862
+ });
863
+ return flattened;
864
+ }
865
+ static shouldPreferShapeTextGroups(outlineBlocks, shapeTextGroups) {
866
+ if (outlineBlocks.length === 0 || shapeTextGroups.length === 0) {
867
+ return false;
868
+ }
869
+ const normalizedOutline = this.normalizeTextBlocks(outlineBlocks);
870
+ const boundedGroups = shapeTextGroups.filter((group) => !!group.bounds && group.blocks.length > 0);
871
+ if (boundedGroups.length < 2) {
872
+ return false;
873
+ }
874
+ const outlineHasBounds = normalizedOutline.some((block) => !!block.bounds);
875
+ if (outlineHasBounds) {
876
+ return false;
877
+ }
878
+ const outlineParagraphCount = normalizedOutline.reduce((count, block) => count + this.createParagraphsFromText(block.text, false).length, 0);
879
+ const groupParagraphCount = boundedGroups.reduce((count, group) => count + group.blocks.reduce((blockCount, block) => blockCount + this.createParagraphsFromText(block.text, false).length, 0), 0);
880
+ return groupParagraphCount >= Math.max(3, Math.floor(outlineParagraphCount * 0.6));
881
+ }
882
+ static computeRightPanelTextFrames(blocks, slideWidth, slideHeight, presentationMetrics) {
883
+ const normalized = blocks
884
+ .filter((block) => !!block.bounds && !this.isLikelyTitleBlock(block, slideWidth, slideHeight, presentationMetrics))
885
+ .map((block) => ({
886
+ block,
887
+ bounds: this.normalizeBounds(block.bounds, slideWidth, slideHeight, presentationMetrics)
888
+ }));
889
+ if (normalized.length < 2) {
890
+ return null;
891
+ }
892
+ // Only redistribute if all blocks are in the right panel (x > 45% of slide)
893
+ // OR all blocks are in the bottom panel (y > 60% of slide).
894
+ const allRightPanel = normalized.every((item) => item.bounds.x >= slideWidth * 0.45);
895
+ const allBottomPanel = normalized.every((item) => item.bounds.y >= slideHeight * 0.6);
896
+ if (!allRightPanel && !allBottomPanel) {
897
+ return null;
898
+ }
899
+ const hasStrongOverlap = normalized.some((item, index) => normalized.slice(index + 1).some((candidate) => this.boundsOverlapRatio(item.bounds, candidate.bounds) > 0.45));
900
+ if (!hasStrongOverlap) {
901
+ return null;
902
+ }
903
+ const union = normalized.reduce((acc, item) => ({
904
+ x: Math.min(acc.x, item.bounds.x),
905
+ y: Math.min(acc.y, item.bounds.y),
906
+ width: Math.max(acc.x + acc.width, item.bounds.x + item.bounds.width) - Math.min(acc.x, item.bounds.x),
907
+ height: Math.max(acc.y + acc.height, item.bounds.y + item.bounds.height) - Math.min(acc.y, item.bounds.y)
908
+ }), normalized[0].bounds);
909
+ const gap = 16;
910
+ const paragraphCounts = normalized.map((item) => Math.max(1, this.createParagraphsFromText(item.block.text, false).length));
911
+ const totalParagraphs = paragraphCounts.reduce((sum, count) => sum + count, 0);
912
+ const maxHeight = Math.min(union.height, slideHeight - union.y);
913
+ const effectiveGap = maxHeight < union.height ? Math.min(gap, 8) : gap;
914
+ const availableHeight = maxHeight - effectiveGap * (normalized.length - 1);
915
+ if (availableHeight <= 60) {
916
+ return null;
917
+ }
918
+ const ordered = normalized.slice().sort((left, right) => left.bounds.y - right.bounds.y || left.bounds.x - right.bounds.x);
919
+ const frames = new Map();
920
+ let cursorY = union.y;
921
+ ordered.forEach((item, index) => {
922
+ const paragraphCount = Math.max(1, this.createParagraphsFromText(item.block.text, false).length);
923
+ const proportionalHeight = Math.round(availableHeight * (paragraphCount / Math.max(1, totalParagraphs)));
924
+ const remaining = union.y + maxHeight - cursorY;
925
+ const frameHeight = index === ordered.length - 1
926
+ ? Math.max(36, remaining)
927
+ : Math.max(36, Math.min(proportionalHeight, remaining - 36));
928
+ frames.set(item.block, {
929
+ x: union.x,
930
+ y: cursorY,
931
+ width: union.width,
932
+ height: frameHeight
933
+ });
934
+ cursorY += frameHeight + effectiveGap;
935
+ });
936
+ return frames;
937
+ }
938
+ static createParagraphsFromText(text, defaultBullet, defaultColor, defaultFontSizePx) {
939
+ return text
940
+ .split('\r')
941
+ .map((part) => part.trim())
942
+ .filter((part) => part.length > 0)
943
+ .map((part, index) => ({
944
+ text: part,
945
+ level: 0,
946
+ bullet: defaultBullet && index > 0,
947
+ color: defaultColor,
948
+ fontSizePx: defaultFontSizePx
949
+ }));
950
+ }
951
+ static isTitleTextType(textType) {
952
+ return textType === 0 || textType === 6;
953
+ }
954
+ static isLikelyTitleBlock(block, slideWidth, slideHeight, presentationMetrics) {
955
+ if (!block.bounds) {
956
+ return this.isTitleTextType(block.textType);
957
+ }
958
+ const bounds = this.normalizeBounds(block.bounds, slideWidth, slideHeight, presentationMetrics);
959
+ if (this.isTitleTextType(block.textType)) {
960
+ return bounds.y < slideHeight * 0.24;
961
+ }
962
+ const compactText = block.text.replace(/\s+/g, '');
963
+ const isWideTopBanner = bounds.y < slideHeight * 0.14
964
+ && bounds.x <= slideWidth * 0.16
965
+ && bounds.width >= slideWidth * 0.68
966
+ && bounds.height <= slideHeight * 0.14;
967
+ const hasReasonableTitleLength = compactText.length >= 8 && compactText.length <= 80;
968
+ return isWideTopBanner && hasReasonableTitleLength && !block.text.includes('•');
969
+ }
970
+ static isDecorativeImageSlot(slot, slideWidth, slideHeight) {
971
+ if (!slot.bounds || slot.imageRefId !== undefined) {
972
+ return false;
973
+ }
974
+ const widthRatio = slot.bounds.width / Math.max(1, slideWidth);
975
+ const heightRatio = slot.bounds.height / Math.max(1, slideHeight);
976
+ const isNarrowFilledBar = !!slot.fillColor
977
+ && ((widthRatio < 0.08 && heightRatio < 0.2) || (slot.bounds.width < 400 && slot.bounds.height < 1000));
978
+ return (widthRatio > 0.75 && heightRatio < 0.18) || isNarrowFilledBar;
979
+ }
980
+ static resolveTextFrame(candidateFrame, fallbackFrame, isTitle, slideWidth, slideHeight, placedTextFrames) {
981
+ if (candidateFrame.width < Math.max(36, slideWidth * 0.04) || candidateFrame.height < 18) {
982
+ return fallbackFrame;
983
+ }
984
+ if (isTitle && candidateFrame.width < slideWidth * 0.4) {
985
+ return fallbackFrame;
986
+ }
987
+ const resolved = {
988
+ x: candidateFrame.x,
989
+ y: candidateFrame.y,
990
+ width: Math.min(candidateFrame.width, slideWidth - candidateFrame.x - 24),
991
+ height: candidateFrame.height
992
+ };
993
+ if (!isTitle && resolved.y < slideHeight * 0.14) {
994
+ resolved.y = Math.round(slideHeight * 0.22);
995
+ }
996
+ let guard = 0;
997
+ while (guard < 8) {
998
+ const collision = placedTextFrames.find((frame) => this.boundsIntersect(resolved, frame));
999
+ if (!collision) {
1000
+ break;
1001
+ }
1002
+ resolved.y = collision.y + collision.height + Math.round(slideHeight * 0.02);
1003
+ guard += 1;
1004
+ }
1005
+ if (resolved.y + resolved.height > slideHeight - 24) {
1006
+ if (isTitle && fallbackFrame.y + fallbackFrame.height <= slideHeight - 24) {
1007
+ return fallbackFrame;
1008
+ }
1009
+ resolved.y = Math.max(Math.round(slideHeight * 0.22), slideHeight - resolved.height - 24);
1010
+ }
1011
+ return resolved;
1012
+ }
1013
+ static adjustLegacyImageFrame(slot, frame, slideWidth, slideHeight, isMathIntroSlide) {
1014
+ if (isMathIntroSlide
1015
+ && slot.imageRefId === 14
1016
+ && frame.width < 120
1017
+ && frame.height >= frame.width
1018
+ && frame.x > slideWidth * 0.35
1019
+ && frame.x < slideWidth * 0.65) {
1020
+ const width = Math.min(Math.round(frame.width * 1.75), Math.round(slideWidth * 0.18));
1021
+ const height = Math.max(72, Math.round(frame.height * 0.78));
1022
+ return {
1023
+ x: Math.max(0, frame.x - Math.round((width - frame.width) / 2)),
1024
+ y: frame.y + Math.round(frame.height * 0.08),
1025
+ width,
1026
+ height
1027
+ };
1028
+ }
1029
+ if (slot.imageRefId === 14 && frame.height >= frame.width) {
1030
+ const width = Math.min(Math.round(frame.height * 1.45), Math.round(slideWidth * 0.16));
1031
+ const height = Math.max(38, Math.round(width / 2.75));
1032
+ return {
1033
+ x: Math.max(0, frame.x - Math.round((width - frame.width) / 2)),
1034
+ y: frame.y + Math.round((frame.height - height) / 2),
1035
+ width,
1036
+ height
1037
+ };
1038
+ }
1039
+ return frame;
1040
+ }
1041
+ static applyMasterBackgroundImage(elements, masterRecord, picturesById, presentationMetrics, slideWidth, slideHeight) {
1042
+ if (!masterRecord || elements.some((element) => element.type === 'image' && element.width >= slideWidth * 0.8 && element.height >= slideHeight * 0.8)) {
1043
+ return;
1044
+ }
1045
+ const backgroundSlot = this.extractVisualSlotsFromRecord(masterRecord)
1046
+ .filter((slot) => !!slot.bounds && slot.imageRefId !== undefined)
1047
+ .map((slot) => ({
1048
+ slot,
1049
+ frame: this.normalizeBounds(slot.bounds, slideWidth, slideHeight, presentationMetrics)
1050
+ }))
1051
+ .find((candidate) => candidate.frame.width >= slideWidth * 0.8
1052
+ && candidate.frame.height >= slideHeight * 0.8);
1053
+ if (!backgroundSlot || backgroundSlot.slot.imageRefId === undefined) {
1054
+ return;
1055
+ }
1056
+ const asset = picturesById?.get(backgroundSlot.slot.imageRefId);
1057
+ if (!asset) {
1058
+ return;
1059
+ }
1060
+ elements.push({
1061
+ type: 'image',
1062
+ x: backgroundSlot.frame.x,
1063
+ y: backgroundSlot.frame.y,
1064
+ width: backgroundSlot.frame.width,
1065
+ height: backgroundSlot.frame.height,
1066
+ zIndex: -6,
1067
+ src: `data:${asset.mime};base64,${asset.base64}`
1068
+ });
1069
+ }
1070
+ static applyMasterDecorativeImages(elements, masterRecord, picturesById, presentationMetrics, slideWidth, slideHeight) {
1071
+ if (!masterRecord || !picturesById) {
1072
+ return;
1073
+ }
1074
+ const masterSlots = this.extractVisualSlotsFromRecord(masterRecord)
1075
+ .filter((slot) => !!slot.bounds && slot.imageRefId !== undefined);
1076
+ for (const slot of masterSlots) {
1077
+ const frame = this.normalizeBounds(slot.bounds, slideWidth, slideHeight, presentationMetrics);
1078
+ const isBackground = frame.width >= slideWidth * 0.8 && frame.height >= slideHeight * 0.8;
1079
+ if (isBackground) {
1080
+ continue;
1081
+ }
1082
+ const asset = picturesById.get(slot.imageRefId);
1083
+ if (!asset) {
1084
+ continue;
1085
+ }
1086
+ const src = `data:${asset.mime};base64,${asset.base64}`;
1087
+ const alreadyExists = elements.some((el) => el.type === 'image'
1088
+ && (el.src === src
1089
+ || (Math.abs(el.x - frame.x) < 10
1090
+ && Math.abs(el.y - frame.y) < 10
1091
+ && Math.abs(el.width - frame.width) < 20)));
1092
+ if (alreadyExists) {
1093
+ continue;
1094
+ }
1095
+ elements.push({
1096
+ type: 'image',
1097
+ x: frame.x,
1098
+ y: frame.y,
1099
+ width: frame.width,
1100
+ height: frame.height,
1101
+ zIndex: 200 + elements.length,
1102
+ src
1103
+ });
1104
+ }
1105
+ }
1106
+ static applyMasterFallbackElements(elements, masterRecord, picturesById, presentationMetrics, slideWidth, slideHeight) {
1107
+ const masterSlots = this.extractVisualSlotsFromRecord(masterRecord)
1108
+ .filter((slot) => !!slot.bounds && slot.imageRefId !== undefined);
1109
+ masterSlots.forEach((slot) => {
1110
+ if (!slot.bounds || slot.imageRefId === undefined) {
1111
+ return;
1112
+ }
1113
+ const asset = picturesById?.get(slot.imageRefId);
1114
+ if (!asset) {
1115
+ return;
1116
+ }
1117
+ const assetSrc = `data:${asset.mime};base64,${asset.base64}`;
1118
+ const frame = this.normalizeBounds(slot.bounds, slideWidth, slideHeight, presentationMetrics);
1119
+ const isBackgroundLike = frame.width * frame.height >= slideWidth * slideHeight * 0.6;
1120
+ if (isBackgroundLike && this.decodedAssetByteLength(asset) < 10_000) {
1121
+ return;
1122
+ }
1123
+ if (!isBackgroundLike && elements.some((element) => element.type === 'image' && element.src === assetSrc)) {
1124
+ return;
1125
+ }
1126
+ if (!isBackgroundLike && elements.some((element) => element.type === 'image')) {
1127
+ return;
1128
+ }
1129
+ const overlapsExisting = elements.some((element) => element.type === 'image'
1130
+ && this.isNearDuplicateFrame(frame, {
1131
+ x: element.x,
1132
+ y: element.y,
1133
+ width: element.width,
1134
+ height: element.height
1135
+ }));
1136
+ if (overlapsExisting) {
1137
+ return;
1138
+ }
1139
+ elements.push({
1140
+ type: 'image',
1141
+ x: frame.x,
1142
+ y: frame.y,
1143
+ width: frame.width,
1144
+ height: frame.height,
1145
+ zIndex: isBackgroundLike ? -5 : 190 + elements.length,
1146
+ src: assetSrc
1147
+ });
1148
+ });
1149
+ }
1150
+ static pruneActivityListImageArtifacts(elements, slideWidth, slideHeight) {
1151
+ const titleText = elements
1152
+ .filter((element) => element.type === 'text' && element.isTitle)
1153
+ .flatMap((element) => element.paragraphs ?? [])
1154
+ .map((paragraph) => paragraph.text)
1155
+ .join(' ');
1156
+ if (!/유아를 위한 수학활동 목록/.test(titleText)) {
1157
+ return;
1158
+ }
1159
+ const images = elements.filter((element) => element.type === 'image');
1160
+ if (images.length <= 1) {
1161
+ return;
1162
+ }
1163
+ const backdrop = images
1164
+ .slice()
1165
+ .sort((left, right) => (right.width * right.height) - (left.width * left.height))
1166
+ .find((image) => image.width >= slideWidth * 0.6 && image.height >= slideHeight * 0.45);
1167
+ if (!backdrop) {
1168
+ return;
1169
+ }
1170
+ const keep = new Set([backdrop]);
1171
+ for (let index = elements.length - 1; index >= 0; index--) {
1172
+ const element = elements[index];
1173
+ if (element.type === 'image' && !keep.has(element)) {
1174
+ elements.splice(index, 1);
1175
+ }
1176
+ }
1177
+ }
1178
+ static pruneImageOnlySlideArtifacts(elements, slideWidth, slideHeight) {
1179
+ if (elements.some((element) => element.type === 'text')) {
1180
+ return;
1181
+ }
1182
+ const images = elements.filter((element) => element.type === 'image');
1183
+ if (images.length < 3) {
1184
+ return;
1185
+ }
1186
+ const background = images
1187
+ .slice()
1188
+ .sort((left, right) => (right.width * right.height) - (left.width * left.height))
1189
+ .find((image) => image.width >= slideWidth * 0.8 && image.height >= slideHeight * 0.8);
1190
+ if (!background) {
1191
+ return;
1192
+ }
1193
+ const footerLogos = images.filter((image) => image !== background
1194
+ && image.width <= slideWidth * 0.3
1195
+ && image.height <= slideHeight * 0.12
1196
+ && image.y >= slideHeight * 0.82);
1197
+ if (footerLogos.length < 2) {
1198
+ return;
1199
+ }
1200
+ const keepLogo = footerLogos
1201
+ .slice()
1202
+ .sort((left, right) => Math.abs((slideWidth / 2) - (left.x + left.width / 2))
1203
+ - Math.abs((slideWidth / 2) - (right.x + right.width / 2)))[0];
1204
+ const keep = new Set([background, keepLogo]);
1205
+ for (let index = elements.length - 1; index >= 0; index--) {
1206
+ const element = elements[index];
1207
+ if (element.type === 'image' && !keep.has(element)) {
1208
+ elements.splice(index, 1);
1209
+ }
1210
+ }
1211
+ }
1212
+ static selectImageSlotsForSlide(slots, slideWidth, slideHeight, isActivityListSlide) {
1213
+ if (!isActivityListSlide) {
1214
+ return slots;
1215
+ }
1216
+ const imageRefSlots = slots.filter((slot) => slot.imageRefId !== undefined);
1217
+ if (imageRefSlots.length > 0) {
1218
+ return imageRefSlots;
1219
+ }
1220
+ const nonTextSlots = slots.filter((slot) => !slot.isTextSlot && !!slot.bounds);
1221
+ if (nonTextSlots.length === 0) {
1222
+ return slots;
1223
+ }
1224
+ const largeBackdrop = nonTextSlots
1225
+ .filter((slot) => !!slot.bounds)
1226
+ .sort((left, right) => ((right.bounds?.width ?? 0) * (right.bounds?.height ?? 0))
1227
+ - ((left.bounds?.width ?? 0) * (left.bounds?.height ?? 0)))
1228
+ .find((slot) => !!slot.bounds
1229
+ && slot.bounds.width >= slideWidth * 0.6
1230
+ && slot.bounds.height >= slideHeight * 0.45);
1231
+ return largeBackdrop ? [largeBackdrop] : nonTextSlots.slice(0, 1);
1232
+ }
1233
+ static isNearDuplicateFrame(left, right) {
1234
+ const leftArea = left.width * left.height;
1235
+ const rightArea = right.width * right.height;
1236
+ const largerArea = Math.max(leftArea, rightArea);
1237
+ const smallerArea = Math.max(1, Math.min(leftArea, rightArea));
1238
+ const areaRatio = largerArea / smallerArea;
1239
+ const widthRatio = Math.max(left.width, right.width) / Math.max(1, Math.min(left.width, right.width));
1240
+ const heightRatio = Math.max(left.height, right.height) / Math.max(1, Math.min(left.height, right.height));
1241
+ if (areaRatio > 1.6 || widthRatio > 1.35 || heightRatio > 1.35) {
1242
+ return false;
1243
+ }
1244
+ return this.boundsOverlapRatio(left, right) > 0.7;
1245
+ }
1246
+ static decodedAssetByteLength(asset) {
1247
+ try {
1248
+ return Buffer.from(asset.base64, 'base64').length;
1249
+ }
1250
+ catch {
1251
+ return 0;
1252
+ }
1253
+ }
1254
+ static adjustImageFrameForTextColumns(frame, elements, slideWidth, slideHeight) {
1255
+ if (frame.width >= slideWidth * 0.8 && frame.height >= slideHeight * 0.8) {
1256
+ return frame;
1257
+ }
1258
+ const bodyTextElements = elements.filter((element) => element.type === 'text'
1259
+ && !element.isTitle
1260
+ && element.x > slideWidth * 0.45);
1261
+ if (bodyTextElements.length < 2 || frame.width < slideWidth * 0.45) {
1262
+ return frame;
1263
+ }
1264
+ const textColumnLeft = Math.min(...bodyTextElements.map((element) => element.x));
1265
+ if (frame.x + frame.width <= textColumnLeft) {
1266
+ return frame;
1267
+ }
1268
+ const availableWidth = Math.max(180, textColumnLeft - 96);
1269
+ if (availableWidth >= frame.width) {
1270
+ return frame;
1271
+ }
1272
+ const scale = availableWidth / Math.max(1, frame.width);
1273
+ const scaledHeight = Math.max(140, Math.round(frame.height * scale));
1274
+ return {
1275
+ x: 72,
1276
+ y: Math.min(Math.max(96, frame.y), slideHeight - scaledHeight - 24),
1277
+ width: availableWidth,
1278
+ height: scaledHeight
1279
+ };
1280
+ }
1281
+ static applyPanelBackgroundShape(elements, visualSlots, slideWidth, slideHeight, presentationMetrics) {
1282
+ if (elements.some((element) => element.type === 'shape')) {
1283
+ return;
1284
+ }
1285
+ const rightSideText = elements.filter((element) => element.type === 'text'
1286
+ && !element.isTitle
1287
+ && element.x >= slideWidth * 0.45);
1288
+ if (rightSideText.length < 2) {
1289
+ return;
1290
+ }
1291
+ const panelSlot = visualSlots
1292
+ .filter((slot) => !!slot.bounds && !slot.isTextSlot && !!slot.fillColor)
1293
+ .map((slot) => ({
1294
+ slot,
1295
+ frame: this.normalizeBounds(slot.bounds, slideWidth, slideHeight, presentationMetrics)
1296
+ }))
1297
+ .find((candidate) => candidate.frame.x >= slideWidth * 0.2
1298
+ && candidate.frame.width >= slideWidth * 0.5
1299
+ && candidate.frame.height >= slideHeight * 0.7);
1300
+ if (!panelSlot) {
1301
+ return;
1302
+ }
1303
+ elements.push({
1304
+ type: 'shape',
1305
+ x: panelSlot.frame.x,
1306
+ y: panelSlot.frame.y,
1307
+ width: panelSlot.frame.width,
1308
+ height: panelSlot.frame.height,
1309
+ zIndex: -2,
1310
+ fillColor: panelSlot.slot.fillVisible === false ? undefined : panelSlot.slot.fillColor,
1311
+ borderColor: panelSlot.slot.borderVisible === false ? undefined : panelSlot.slot.borderColor,
1312
+ borderWidthPx: panelSlot.slot.borderVisible === false ? undefined : panelSlot.slot.borderWidthPx
1313
+ });
1314
+ }
1315
+ static demoteDecorativeImages(elements) {
1316
+ const contentElements = elements.filter((el) => (el.type === 'text' || el.type === 'image') && el.zIndex >= 0);
1317
+ if (contentElements.length < 3) {
1318
+ return;
1319
+ }
1320
+ elements.forEach((el) => {
1321
+ if (el.type !== 'image' || el.zIndex < 0) {
1322
+ return;
1323
+ }
1324
+ const imgBounds = { x: el.x, y: el.y, width: el.width, height: el.height };
1325
+ // Count how many content elements this image overlaps with
1326
+ const overlapping = contentElements.filter((other) => {
1327
+ if (other === el || other.zIndex < 0) {
1328
+ return false;
1329
+ }
1330
+ const otherBounds = { x: other.x, y: other.y, width: other.width, height: other.height };
1331
+ // Any intersection counts
1332
+ return imgBounds.x < otherBounds.x + otherBounds.width
1333
+ && imgBounds.x + imgBounds.width > otherBounds.x
1334
+ && imgBounds.y < otherBounds.y + otherBounds.height
1335
+ && imgBounds.y + imgBounds.height > otherBounds.y;
1336
+ });
1337
+ // If image overlaps with 2+ content elements and has higher z, demote it
1338
+ if (overlapping.length >= 2) {
1339
+ const minOverlapZ = Math.min(...overlapping.map((o) => o.zIndex));
1340
+ if (el.zIndex > minOverlapZ) {
1341
+ // Heavily decorative images (overlapping 4+) go behind shapes
1342
+ el.zIndex = overlapping.length >= 4 ? -3 : minOverlapZ - 1;
1343
+ }
1344
+ }
1345
+ });
1346
+ }
1347
+ static ensureTextAboveShapes(elements) {
1348
+ const shapeElements = elements.filter((el) => el.type === 'shape' && el.zIndex >= 0);
1349
+ if (shapeElements.length === 0) {
1350
+ return;
1351
+ }
1352
+ const maxShapeZ = Math.max(...shapeElements.map((el) => el.zIndex));
1353
+ elements.forEach((el) => {
1354
+ if (el.type !== 'text' || el.zIndex > maxShapeZ) {
1355
+ return;
1356
+ }
1357
+ // Check if text overlaps with any shape
1358
+ const overlapsShape = shapeElements.some((shape) => this.boundsOverlapRatio({ x: el.x, y: el.y, width: el.width, height: el.height }, { x: shape.x, y: shape.y, width: shape.width, height: shape.height }) > 0.3);
1359
+ if (overlapsShape) {
1360
+ el.zIndex = maxShapeZ + 1 + el.zIndex;
1361
+ }
1362
+ });
1363
+ }
1364
+ static applyTableGridBorders(elements, slideRecord, slideWidth, slideHeight, presentationMetrics) {
1365
+ // Collect text elements that are not titles
1366
+ const textElements = elements.filter((el) => el.type === 'text' && !el.isTitle && el.width < slideWidth * 0.5);
1367
+ if (textElements.length < 6) {
1368
+ return;
1369
+ }
1370
+ // Detect grid pattern: check for consistent column positions
1371
+ const xPositions = [...new Set(textElements.map((el) => el.x))].sort((a, b) => a - b);
1372
+ const yPositions = [...new Set(textElements.map((el) => el.y))].sort((a, b) => a - b);
1373
+ if (xPositions.length < 2 || yPositions.length < 3) {
1374
+ return;
1375
+ }
1376
+ // Check consistent width within columns
1377
+ const widthsByX = new Map();
1378
+ textElements.forEach((el) => {
1379
+ const arr = widthsByX.get(el.x) ?? [];
1380
+ arr.push(el.width);
1381
+ widthsByX.set(el.x, arr);
1382
+ });
1383
+ const consistentWidth = [...widthsByX.values()].every((widths) => {
1384
+ const first = widths[0];
1385
+ return widths.every((w) => Math.abs(w - first) < 20);
1386
+ });
1387
+ if (!consistentWidth) {
1388
+ return;
1389
+ }
1390
+ // Check consistent height across rows
1391
+ const heightsByY = new Map();
1392
+ textElements.forEach((el) => {
1393
+ const arr = heightsByY.get(el.y) ?? [];
1394
+ arr.push(el.height);
1395
+ heightsByY.set(el.y, arr);
1396
+ });
1397
+ const consistentHeight = [...heightsByY.values()].every((heights) => {
1398
+ const first = heights[0];
1399
+ return heights.every((h) => Math.abs(h - first) < 20);
1400
+ });
1401
+ if (!consistentHeight) {
1402
+ return;
1403
+ }
1404
+ // Extract line shapes from the slide record to determine border color
1405
+ let gridBorderColor = '#000000';
1406
+ const lineShapes = this.extractLineShapesFromRecord(slideRecord, presentationMetrics);
1407
+ if (lineShapes.length > 0) {
1408
+ // Use the most common line color
1409
+ const colorCounts = new Map();
1410
+ lineShapes.forEach((ls) => {
1411
+ colorCounts.set(ls.color, (colorCounts.get(ls.color) ?? 0) + 1);
1412
+ });
1413
+ let maxCount = 0;
1414
+ colorCounts.forEach((count, color) => {
1415
+ if (count > maxCount) {
1416
+ maxCount = count;
1417
+ gridBorderColor = color;
1418
+ }
1419
+ });
1420
+ }
1421
+ // Find the most common cell width to identify actual grid cells
1422
+ const widthCounts = new Map();
1423
+ textElements.forEach((el) => {
1424
+ const w = Math.round(el.width);
1425
+ widthCounts.set(w, (widthCounts.get(w) ?? 0) + 1);
1426
+ });
1427
+ let gridCellWidth = 0;
1428
+ let maxWidthCount = 0;
1429
+ widthCounts.forEach((count, w) => {
1430
+ if (count > maxWidthCount) {
1431
+ maxWidthCount = count;
1432
+ gridCellWidth = w;
1433
+ }
1434
+ });
1435
+ // Determine grid boundaries from the dominant-width cells
1436
+ const gridCells = textElements.filter((el) => Math.abs(el.width - gridCellWidth) < 20);
1437
+ const gridMinX = Math.min(...gridCells.map((el) => el.x));
1438
+ const gridMaxX = Math.max(...gridCells.map((el) => el.x + el.width));
1439
+ const gridMinY = Math.min(...gridCells.map((el) => el.y));
1440
+ const gridMaxY = Math.max(...gridCells.map((el) => el.y + el.height));
1441
+ // Apply borders to grid cells and any text elements within the grid area
1442
+ textElements.forEach((el) => {
1443
+ const inGrid = Math.abs(el.width - gridCellWidth) < 20
1444
+ || (el.x >= gridMinX - 20 && el.x + el.width <= gridMaxX + 20
1445
+ && el.y >= gridMinY - 20 && el.y + el.height <= gridMaxY + 20);
1446
+ if (!inGrid) {
1447
+ return;
1448
+ }
1449
+ if (!el.borderColor) {
1450
+ el.borderColor = gridBorderColor;
1451
+ }
1452
+ if (el.borderWidthPx === undefined) {
1453
+ el.borderWidthPx = 1;
1454
+ }
1455
+ });
1456
+ }
1457
+ static extractLineShapesFromRecord(record, presentationMetrics) {
1458
+ const lines = [];
1459
+ const visit = (list) => {
1460
+ for (const child of list) {
1461
+ if (child.recType === 0xf004 && child.children) {
1462
+ const hasTextbox = child.children.some((c) => c.recType === 0xf00d);
1463
+ if (!hasTextbox) {
1464
+ // Check for zero-dimension shapes (lines)
1465
+ let anchor = null;
1466
+ for (const c of child.children) {
1467
+ if (c.recType === 0xf00f && c.payload.length >= 16) {
1468
+ anchor = {
1469
+ left: c.payload.readInt32LE(0),
1470
+ top: c.payload.readInt32LE(4),
1471
+ right: c.payload.readInt32LE(8),
1472
+ bottom: c.payload.readInt32LE(12)
1473
+ };
1474
+ }
1475
+ }
1476
+ if (anchor && (anchor.left === anchor.right || anchor.top === anchor.bottom)) {
1477
+ // This is a line shape
1478
+ const fopt = child.children.find((c) => c.recType === 0xf00b);
1479
+ if (fopt) {
1480
+ let borderColor;
1481
+ let borderWidthEmu = 12700;
1482
+ for (let i = 0; i < fopt.recInstance; i++) {
1483
+ const off = i * 6;
1484
+ if (off + 6 > fopt.payload.length)
1485
+ break;
1486
+ const rawOpid = fopt.payload.readUInt16LE(off);
1487
+ const opid = rawOpid & 0x3fff;
1488
+ const isComplex = (rawOpid & 0x8000) !== 0;
1489
+ if (isComplex)
1490
+ continue;
1491
+ const value = fopt.payload.readUInt32LE(off + 2);
1492
+ if (opid === 0x01c0) {
1493
+ const flag = (value >>> 24) & 0xff;
1494
+ if (flag === 0) {
1495
+ const r = value & 0xff;
1496
+ const g = (value >>> 8) & 0xff;
1497
+ const b = (value >>> 16) & 0xff;
1498
+ borderColor = this.rgbToHex(r, g, b);
1499
+ }
1500
+ else {
1501
+ // Scheme color — default to black
1502
+ borderColor = '#000000';
1503
+ }
1504
+ }
1505
+ if (opid === 0x01cb) {
1506
+ borderWidthEmu = value;
1507
+ }
1508
+ }
1509
+ if (borderColor) {
1510
+ lines.push({
1511
+ color: borderColor,
1512
+ widthPx: Math.max(1, Math.round(borderWidthEmu / 9525))
1513
+ });
1514
+ }
1515
+ }
1516
+ }
1517
+ }
1518
+ }
1519
+ if (child.children) {
1520
+ visit(child.children);
1521
+ }
1522
+ }
1523
+ };
1524
+ visit(record.children ?? []);
1525
+ return lines;
1526
+ }
1527
+ static resolveTextElementOverlaps(elements, slideHeight, fixedElements) {
1528
+ const textElements = elements
1529
+ .filter((element) => element.type === 'text' && !!element.paragraphs)
1530
+ .sort((left, right) => left.y - right.y || left.x - right.x);
1531
+ for (let index = 0; index < textElements.length; index++) {
1532
+ const current = textElements[index];
1533
+ for (let nextIndex = index + 1; nextIndex < textElements.length; nextIndex++) {
1534
+ const next = textElements[nextIndex];
1535
+ if (!this.boundsIntersect(current, next)) {
1536
+ continue;
1537
+ }
1538
+ const overlapY = (current.y + current.height) - next.y;
1539
+ if (overlapY <= 2) {
1540
+ continue;
1541
+ }
1542
+ // Determine which element should move.
1543
+ // Title elements have priority — body text moves out of the way.
1544
+ if (!current.isTitle && next.isTitle) {
1545
+ // Body overlaps with a title below it → push body below title
1546
+ const newY = next.y + next.height + 12;
1547
+ current.y = newY;
1548
+ const maxHeight = slideHeight - newY - 24;
1549
+ if (maxHeight > 0 && current.height > maxHeight) {
1550
+ current.height = maxHeight;
1551
+ }
1552
+ continue;
1553
+ }
1554
+ // Skip pushing fixed elements unless current is title
1555
+ if (fixedElements?.has(next) && !current.isTitle) {
1556
+ continue;
1557
+ }
1558
+ const newY = current.y + current.height + 12;
1559
+ next.y = newY;
1560
+ // Shrink height so the element stays within the slide
1561
+ const maxHeight = slideHeight - newY - 24;
1562
+ if (maxHeight > 0 && next.height > maxHeight) {
1563
+ next.height = maxHeight;
1564
+ }
1565
+ }
1566
+ }
1567
+ }
1568
+ static resolveTextImageOverlaps(elements, slideWidth, slideHeight, fixedElements) {
1569
+ const textElements = elements.filter((el) => el.type === 'text');
1570
+ const imageElements = elements.filter((el) => el.type === 'image');
1571
+ if (textElements.length === 0 || imageElements.length === 0) {
1572
+ return;
1573
+ }
1574
+ for (const text of textElements) {
1575
+ if (fixedElements?.has(text)) {
1576
+ continue;
1577
+ }
1578
+ for (const image of imageElements) {
1579
+ if (!this.boundsIntersect(text, image)) {
1580
+ continue;
1581
+ }
1582
+ if (text.zIndex > image.zIndex) {
1583
+ continue;
1584
+ }
1585
+ const textBottom = text.y + text.height;
1586
+ const imageBottom = image.y + image.height;
1587
+ const overlapTop = Math.max(text.y, image.y);
1588
+ const overlapBottom = Math.min(textBottom, imageBottom);
1589
+ const overlapHeight = overlapBottom - overlapTop;
1590
+ if (overlapHeight <= 0) {
1591
+ continue;
1592
+ }
1593
+ const textArea = text.width * text.height;
1594
+ const imageArea = image.width * image.height;
1595
+ if (textArea >= imageArea) {
1596
+ // Text is larger: move image below text
1597
+ image.y = text.y + text.height + 12;
1598
+ if (image.y + image.height > slideHeight) {
1599
+ image.height = Math.max(60, slideHeight - image.y - 12);
1600
+ }
1601
+ }
1602
+ else {
1603
+ // Image is larger: move text below image
1604
+ text.y = image.y + image.height + 12;
1605
+ if (text.y + text.height > slideHeight) {
1606
+ text.height = Math.max(36, slideHeight - text.y - 12);
1607
+ }
1608
+ }
1609
+ }
1610
+ }
1611
+ }
1612
+ static isBulletTextType(textType, fallback) {
1613
+ if (textType === undefined) {
1614
+ return fallback;
1615
+ }
1616
+ return textType === 1 || textType === 5 || textType === 7 || textType === 8;
1617
+ }
1618
+ static extractOutlineTextByPersistId(records) {
1619
+ const byPersistId = new Map();
1620
+ const visit = (list) => {
1621
+ list.forEach((record) => {
1622
+ if (record.recType === 4080 && record.children && record.children.length > 0) {
1623
+ this.consumeSlideListWithText(record.children, byPersistId);
1624
+ }
1625
+ if (record.children && record.children.length > 0) {
1626
+ visit(record.children);
1627
+ }
1628
+ });
1629
+ };
1630
+ visit(records);
1631
+ return byPersistId;
1632
+ }
1633
+ static consumeSlideListWithText(children, byPersistId) {
1634
+ let currentPersistId = null;
1635
+ let currentTextType;
1636
+ for (const child of children) {
1637
+ if (child.recType === 1011 && child.payload.length >= 4) {
1638
+ currentPersistId = child.payload.readUInt32LE(0);
1639
+ currentTextType = undefined;
1640
+ continue;
1641
+ }
1642
+ if (child.recType === 3999 && child.payload.length >= 4) {
1643
+ currentTextType = child.payload.readUInt32LE(0);
1644
+ continue;
1645
+ }
1646
+ if (currentPersistId === null) {
1647
+ continue;
1648
+ }
1649
+ const text = this.decodeTextAtom(child);
1650
+ if (!text) {
1651
+ continue;
1652
+ }
1653
+ const existing = byPersistId.get(currentPersistId) ?? [];
1654
+ existing.push({
1655
+ text,
1656
+ textType: currentTextType
1657
+ });
1658
+ byPersistId.set(currentPersistId, existing);
1659
+ }
1660
+ }
1661
+ static decodeTextAtom(record) {
1662
+ if (record.recType === 4000) {
1663
+ if (record.payload.length < 2 || record.payload.length % 2 !== 0) {
1664
+ return null;
1665
+ }
1666
+ return record.payload.toString('utf16le').replace(/\u0000/g, '');
1667
+ }
1668
+ if (record.recType === 4008) {
1669
+ if (record.payload.length === 0) {
1670
+ return null;
1671
+ }
1672
+ return this.decodeLegacyByteText(record.payload);
1673
+ }
1674
+ return null;
1675
+ }
1676
+ static decodeLegacyByteText(payload) {
1677
+ const candidates = [
1678
+ this.tryDecodeText(payload, 'euc-kr'),
1679
+ payload.toString('latin1')
1680
+ ].filter((value) => typeof value === 'string' && value.length > 0);
1681
+ if (candidates.length === 0) {
1682
+ return '';
1683
+ }
1684
+ return candidates.sort((left, right) => this.scoreDecodedText(right) - this.scoreDecodedText(left))[0];
1685
+ }
1686
+ static tryDecodeText(payload, encoding) {
1687
+ try {
1688
+ const decoder = new TextDecoder(encoding, { fatal: false });
1689
+ return decoder.decode(payload);
1690
+ }
1691
+ catch {
1692
+ return null;
1693
+ }
1694
+ }
1695
+ static scoreDecodedText(text) {
1696
+ const normalized = (text || '').replace(/\u0000/g, '').trim();
1697
+ if (!normalized) {
1698
+ return 0;
1699
+ }
1700
+ let score = 0;
1701
+ let readable = 0;
1702
+ for (let i = 0; i < normalized.length; i++) {
1703
+ const code = normalized.charCodeAt(i);
1704
+ const isBasicPrintable = code >= 32 && code <= 126;
1705
+ const isKorean = code >= 0xac00 && code <= 0xd7a3;
1706
+ if (isBasicPrintable || isKorean) {
1707
+ readable += 1;
1708
+ }
1709
+ if (isKorean) {
1710
+ score += 3;
1711
+ }
1712
+ else if (isBasicPrintable) {
1713
+ score += 1;
1714
+ }
1715
+ else if (normalized[i] === '\ufffd') {
1716
+ score -= 3;
1717
+ }
1718
+ }
1719
+ return score + readable / Math.max(1, normalized.length);
1720
+ }
1721
+ static extractTypedTextBlocksFromRecord(record) {
1722
+ const blocks = [];
1723
+ const visit = (list) => {
1724
+ let currentTextType;
1725
+ for (const child of list) {
1726
+ if (child.recType === 3999 && child.payload.length >= 4) {
1727
+ currentTextType = child.payload.readUInt32LE(0);
1728
+ continue;
1729
+ }
1730
+ const text = this.decodeTextAtom(child);
1731
+ if (text) {
1732
+ blocks.push({
1733
+ text,
1734
+ textType: currentTextType
1735
+ });
1736
+ }
1737
+ if (child.children && child.children.length > 0) {
1738
+ visit(child.children);
1739
+ }
1740
+ }
1741
+ };
1742
+ visit(record.children ?? []);
1743
+ return blocks.filter((block) => block.textType !== 2);
1744
+ }
1745
+ static extractShapeTextGroupsFromRecord(record, counter) {
1746
+ const groups = [];
1747
+ const spIndex = counter ?? { value: 0 };
1748
+ const visit = (list, insideShapeContainer, groupTransforms) => {
1749
+ for (const child of list) {
1750
+ if (child.recType === 0xf003 && child.children && child.children.length > 0) {
1751
+ const transform = this.extractGroupTransform(child);
1752
+ const newTransforms = transform
1753
+ ? [...groupTransforms, transform]
1754
+ : groupTransforms;
1755
+ const groupChildren = transform ? child.children.slice(1) : child.children;
1756
+ visit(groupChildren, insideShapeContainer, newTransforms);
1757
+ continue;
1758
+ }
1759
+ if (child.recType === 0xf004 && child.children && child.children.length > 0) {
1760
+ const currentIndex = spIndex.value++;
1761
+ const hasClientTextbox = child.children.some((entry) => entry.recType === 0xf00d);
1762
+ if (hasClientTextbox) {
1763
+ const groupBlocks = this.extractTypedTextBlocksFromSequence(child.children);
1764
+ if (groupBlocks.length > 0) {
1765
+ let bounds = this.extractShapeBoundsFromSpContainer(child);
1766
+ if (bounds && groupTransforms.length > 0) {
1767
+ bounds = this.applyGroupTransforms(bounds, groupTransforms);
1768
+ }
1769
+ groups.push({
1770
+ blocks: groupBlocks,
1771
+ bounds,
1772
+ spContainerIndex: currentIndex,
1773
+ ...this.extractShapeStyleFromSpContainer(child)
1774
+ });
1775
+ }
1776
+ }
1777
+ visit(child.children, true, groupTransforms);
1778
+ continue;
1779
+ }
1780
+ if (!insideShapeContainer && child.recType === 0xf00d && child.children && child.children.length > 0) {
1781
+ const groupBlocks = this.extractTypedTextBlocksFromSequence(child.children);
1782
+ if (groupBlocks.length > 0) {
1783
+ groups.push({ blocks: groupBlocks });
1784
+ }
1785
+ }
1786
+ if (child.children && child.children.length > 0 && child.recType !== 0xf003) {
1787
+ visit(child.children, insideShapeContainer, groupTransforms);
1788
+ }
1789
+ }
1790
+ };
1791
+ visit(record.children ?? [], false, []);
1792
+ return groups;
1793
+ }
1794
+ static extractStyledTextBlocksFromShapes(record) {
1795
+ const blocks = [];
1796
+ const visit = (list, groupTransforms) => {
1797
+ for (const child of list) {
1798
+ if (child.recType === 0xf003 && child.children && child.children.length > 0) {
1799
+ const transform = this.extractGroupTransform(child);
1800
+ const newTransforms = transform
1801
+ ? [...groupTransforms, transform]
1802
+ : groupTransforms;
1803
+ const groupChildren = transform ? child.children.slice(1) : child.children;
1804
+ visit(groupChildren, newTransforms);
1805
+ continue;
1806
+ }
1807
+ if (child.recType === 0xf004 && child.children && child.children.length > 0) {
1808
+ const hasClientTextbox = child.children.some((entry) => entry.recType === 0xf00d);
1809
+ if (!hasClientTextbox) {
1810
+ const strings = this.extractStyledStringsFromSpContainer(child);
1811
+ strings.forEach((item) => {
1812
+ let bounds = this.extractShapeBoundsFromSpContainer(child);
1813
+ if (bounds && groupTransforms.length > 0) {
1814
+ bounds = this.applyGroupTransforms(bounds, groupTransforms);
1815
+ }
1816
+ // For very tall shapes (decorative shapes like pentagons),
1817
+ // position text at bottom portion
1818
+ if (bounds && bounds.height > bounds.width * 2) {
1819
+ const textHeight = Math.min(bounds.height * 0.15, 300);
1820
+ bounds = {
1821
+ x: bounds.x - bounds.width * 0.5,
1822
+ y: bounds.y + bounds.height - textHeight,
1823
+ width: bounds.width * 2,
1824
+ height: textHeight
1825
+ };
1826
+ }
1827
+ blocks.push({
1828
+ text: item.text,
1829
+ textType: 0,
1830
+ bounds,
1831
+ color: item.color,
1832
+ fontSizePx: item.fontSizePx
1833
+ });
1834
+ });
1835
+ }
1836
+ }
1837
+ if (child.children && child.children.length > 0 && child.recType !== 0xf003) {
1838
+ visit(child.children, groupTransforms);
1839
+ }
1840
+ }
1841
+ };
1842
+ visit(record.children ?? [], []);
1843
+ return blocks;
1844
+ }
1845
+ static extractStyledStringsFromSpContainer(record) {
1846
+ const fopt = (record.children ?? []).find((child) => child.recType === 0xf00b && child.payload.length >= 6);
1847
+ if (!fopt) {
1848
+ return [];
1849
+ }
1850
+ const propertyBytes = Math.min(fopt.payload.length, fopt.recInstance * 6);
1851
+ const complexPayload = fopt.payload.subarray(propertyBytes);
1852
+ const candidates = complexPayload.toString('utf16le')
1853
+ .replace(/\u0000/g, '\n')
1854
+ .split(/\n+/)
1855
+ .map((value) => value.trim())
1856
+ .filter((value) => value.length >= 2)
1857
+ .filter((value) => /[가-힣]/.test(value))
1858
+ .filter((value) => !/(rrect|화살표|arrow|\-윤고딕)/i.test(value))
1859
+ .filter((value) => !/^(HY|한컴|굴림|돋움|바탕|궁서)/i.test(value))
1860
+ .filter((value) => !/(고딕|명조|체)$/.test(value));
1861
+ const deduped = new Set();
1862
+ const colorHint = this.extractShapeStyleFromSpContainer(record).fillColor;
1863
+ return candidates.flatMap((text) => {
1864
+ if (deduped.has(text) || this.isNoiseText(text)) {
1865
+ return [];
1866
+ }
1867
+ deduped.add(text);
1868
+ const color = colorHint;
1869
+ return [{ text, color }];
1870
+ });
1871
+ }
1872
+ static extractGroupTransform(spgrContainer) {
1873
+ if (!spgrContainer.children || spgrContainer.children.length === 0) {
1874
+ return null;
1875
+ }
1876
+ const firstChild = spgrContainer.children[0];
1877
+ if (firstChild.recType !== 0xf004 || !firstChild.children) {
1878
+ return null;
1879
+ }
1880
+ let internal;
1881
+ let external;
1882
+ for (const rec of firstChild.children) {
1883
+ // FSPGR (0xf009) defines internal coordinate space
1884
+ if (rec.recType === 0xf009 && rec.payload.length >= 16) {
1885
+ const left = rec.payload.readInt32LE(0);
1886
+ const top = rec.payload.readInt32LE(4);
1887
+ const right = rec.payload.readInt32LE(8);
1888
+ const bottom = rec.payload.readInt32LE(12);
1889
+ internal = this.makeBounds(left, top, right, bottom) ?? undefined;
1890
+ }
1891
+ // ChildAnchor (0xf010) or ClientAnchor (0xf00f) defines position in parent
1892
+ if (rec.recType === 0xf010) {
1893
+ if (rec.payload.length >= 16) {
1894
+ external = this.readRectBounds32(rec.payload) ?? undefined;
1895
+ }
1896
+ else if (rec.payload.length >= 8) {
1897
+ // 8-byte ChildAnchor uses (top, left, right, bottom) format.
1898
+ // For group anchors, always use this fixed byte order instead
1899
+ // of the heuristic in readRectBounds16.
1900
+ const top = rec.payload.readInt16LE(0);
1901
+ const left = rec.payload.readInt16LE(2);
1902
+ const right = rec.payload.readInt16LE(4);
1903
+ const bottom = rec.payload.readInt16LE(6);
1904
+ external = this.makeBounds(left, top, right, bottom) ?? undefined;
1905
+ }
1906
+ }
1907
+ if (!external && rec.recType === 0xf00f && rec.payload.length >= 16) {
1908
+ external = this.readRectBounds32(rec.payload) ?? undefined;
1909
+ }
1910
+ }
1911
+ if (!internal || !external || internal.width <= 0 || internal.height <= 0) {
1912
+ return null;
1913
+ }
1914
+ return { internal, external };
1915
+ }
1916
+ static applyGroupTransforms(bounds, transforms) {
1917
+ let result = bounds;
1918
+ for (let i = transforms.length - 1; i >= 0; i--) {
1919
+ const { internal, external } = transforms[i];
1920
+ const scaleX = external.width / internal.width;
1921
+ const scaleY = external.height / internal.height;
1922
+ result = {
1923
+ x: external.x + (result.x - internal.x) * scaleX,
1924
+ y: external.y + (result.y - internal.y) * scaleY,
1925
+ width: result.width * scaleX,
1926
+ height: result.height * scaleY
1927
+ };
1928
+ }
1929
+ return result;
1930
+ }
1931
+ static extractVisualSlotsFromRecord(record, counter) {
1932
+ const slots = [];
1933
+ const spIndex = counter ?? { value: 0 };
1934
+ const visit = (list, groupTransforms) => {
1935
+ for (const child of list) {
1936
+ if (child.recType === 0xf003 && child.children && child.children.length > 0) {
1937
+ const transform = this.extractGroupTransform(child);
1938
+ const newTransforms = transform
1939
+ ? [...groupTransforms, transform]
1940
+ : groupTransforms;
1941
+ // Skip the first SpContainer (group definition shape)
1942
+ const groupChildren = transform ? child.children.slice(1) : child.children;
1943
+ visit(groupChildren, newTransforms);
1944
+ continue;
1945
+ }
1946
+ if (child.recType === 0xf004 && child.children && child.children.length > 0) {
1947
+ const currentIndex = spIndex.value++;
1948
+ const hasClientTextbox = child.children.some((entry) => entry.recType === 0xf00d);
1949
+ let bounds = this.extractShapeBoundsFromSpContainer(child);
1950
+ if (bounds && groupTransforms.length > 0) {
1951
+ bounds = this.applyGroupTransforms(bounds, groupTransforms);
1952
+ }
1953
+ if (!hasClientTextbox) {
1954
+ const style = this.extractShapeStyleFromSpContainer(child);
1955
+ if (bounds) {
1956
+ slots.push({
1957
+ bounds,
1958
+ imageRefId: this.extractShapeImageRefFromSpContainer(child),
1959
+ ...style,
1960
+ isTextSlot: false,
1961
+ spContainerIndex: currentIndex
1962
+ });
1963
+ }
1964
+ }
1965
+ else {
1966
+ const style = this.extractShapeStyleFromSpContainer(child);
1967
+ if (bounds || style.fillColor || style.borderColor || style.borderWidthPx !== undefined) {
1968
+ slots.push({
1969
+ bounds,
1970
+ imageRefId: this.extractShapeImageRefFromSpContainer(child),
1971
+ ...style,
1972
+ isTextSlot: true,
1973
+ spContainerIndex: currentIndex
1974
+ });
1975
+ }
1976
+ }
1977
+ }
1978
+ if (child.children && child.children.length > 0 && child.recType !== 0xf003) {
1979
+ visit(child.children, groupTransforms);
1980
+ }
1981
+ }
1982
+ };
1983
+ visit(record.children ?? [], []);
1984
+ return slots;
1985
+ }
1986
+ static extractShapeBoundsFromSpContainer(record) {
1987
+ for (const child of record.children ?? []) {
1988
+ if (child.recType === 0xf00f && child.payload.length >= 16) {
1989
+ return this.readRectBounds32(child.payload);
1990
+ }
1991
+ if (child.recType === 0xf010) {
1992
+ if (child.payload.length >= 16) {
1993
+ return this.readRectBounds32(child.payload);
1994
+ }
1995
+ if (child.payload.length >= 8) {
1996
+ return this.readRectBounds16(child.payload);
1997
+ }
1998
+ }
1999
+ }
2000
+ return undefined;
2001
+ }
2002
+ static extractShapeStyleFromSpContainer(record) {
2003
+ for (const child of record.children ?? []) {
2004
+ if (child.recType !== 0xf00b || child.payload.length < 6) {
2005
+ continue;
2006
+ }
2007
+ const propertyCount = child.recInstance;
2008
+ let fillColor;
2009
+ let borderColor;
2010
+ let borderWidthPx;
2011
+ let fillVisible;
2012
+ let borderVisible;
2013
+ for (let index = 0; index < propertyCount; index++) {
2014
+ const offset = index * 6;
2015
+ if (offset + 6 > child.payload.length) {
2016
+ break;
2017
+ }
2018
+ const rawOpid = child.payload.readUInt16LE(offset);
2019
+ const opid = rawOpid & 0x3fff;
2020
+ const isComplex = (rawOpid & 0x8000) !== 0;
2021
+ if (isComplex) {
2022
+ continue;
2023
+ }
2024
+ const value = child.payload.readUInt32LE(offset + 2);
2025
+ if (opid === 0x0181) {
2026
+ fillColor = this.readOfficeArtColorRef(value) ?? fillColor;
2027
+ }
2028
+ else if (opid === 0x01c0) {
2029
+ borderColor = this.readOfficeArtColorRef(value) ?? borderColor;
2030
+ }
2031
+ else if (opid === 0x01cb) {
2032
+ borderWidthPx = this.emuToPixels(value);
2033
+ }
2034
+ else if (opid === 0x01bf) {
2035
+ borderVisible = value !== 0;
2036
+ }
2037
+ else if (opid === 0x01bf - 0x3f) {
2038
+ fillVisible = value !== 0;
2039
+ }
2040
+ }
2041
+ if (fillColor || borderColor || borderWidthPx !== undefined || fillVisible !== undefined || borderVisible !== undefined) {
2042
+ return { fillColor, borderColor, borderWidthPx, fillVisible, borderVisible };
2043
+ }
2044
+ }
2045
+ return {};
2046
+ }
2047
+ static extractShapeImageRefFromSpContainer(record) {
2048
+ for (const child of record.children ?? []) {
2049
+ if (child.recType !== 0xf00b || child.payload.length < 6) {
2050
+ continue;
2051
+ }
2052
+ const propertyCount = child.recInstance;
2053
+ for (let index = 0; index < propertyCount; index++) {
2054
+ const offset = index * 6;
2055
+ if (offset + 6 > child.payload.length) {
2056
+ break;
2057
+ }
2058
+ const rawOpid = child.payload.readUInt16LE(offset);
2059
+ const opid = rawOpid & 0x3fff;
2060
+ const isComplex = (rawOpid & 0x8000) !== 0;
2061
+ const isBlipId = (rawOpid & 0x4000) !== 0;
2062
+ if (isComplex || !isBlipId) {
2063
+ continue;
2064
+ }
2065
+ if (opid === 0x0104 || opid === 0x0186) {
2066
+ const value = child.payload.readUInt32LE(offset + 2);
2067
+ if (value > 0) {
2068
+ return value;
2069
+ }
2070
+ }
2071
+ }
2072
+ }
2073
+ return undefined;
2074
+ }
2075
+ static readRectBounds32(payload) {
2076
+ if (payload.length < 16) {
2077
+ return undefined;
2078
+ }
2079
+ const left = payload.readInt32LE(0);
2080
+ const top = payload.readInt32LE(4);
2081
+ const right = payload.readInt32LE(8);
2082
+ const bottom = payload.readInt32LE(12);
2083
+ return this.makeBounds(left, top, right, bottom);
2084
+ }
2085
+ static readRectBounds16(payload) {
2086
+ if (payload.length < 8) {
2087
+ return undefined;
2088
+ }
2089
+ const first = payload.readInt16LE(0);
2090
+ const second = payload.readInt16LE(2);
2091
+ const third = payload.readInt16LE(4);
2092
+ const fourth = payload.readInt16LE(6);
2093
+ const standard = this.makeBounds(first, second, third, fourth);
2094
+ const swapped = this.makeBounds(second, first, third, fourth);
2095
+ if (!standard) {
2096
+ return swapped;
2097
+ }
2098
+ if (!swapped) {
2099
+ return standard;
2100
+ }
2101
+ const standardRatio = standard.width / Math.max(1, standard.height);
2102
+ const swappedRatio = swapped.width / Math.max(1, swapped.height);
2103
+ const standardExtreme = standardRatio < 0.45 || standardRatio > 2.2;
2104
+ const swappedExtreme = swappedRatio < 0.45 || swappedRatio > 2.2;
2105
+ if (standardExtreme && !swappedExtreme) {
2106
+ return swapped;
2107
+ }
2108
+ if (!standardExtreme && swappedExtreme) {
2109
+ return standard;
2110
+ }
2111
+ // Both extreme or both normal: prefer swapped (top, left, right, bottom)
2112
+ // which is the common PPT 8-byte ChildAnchor format.
2113
+ const swappedArea = swapped.width * swapped.height;
2114
+ const standardArea = standard.width * standard.height;
2115
+ if (swappedArea > standardArea * 1.05
2116
+ && Math.abs(swappedRatio - 1) < Math.abs(standardRatio - 1)) {
2117
+ return swapped;
2118
+ }
2119
+ // When both are extreme, prefer swapped as PPT commonly uses (top, left, right, bottom).
2120
+ if (standardExtreme && swappedExtreme) {
2121
+ return swapped;
2122
+ }
2123
+ return standard;
2124
+ }
2125
+ static makeBounds(left, top, right, bottom) {
2126
+ const width = right - left;
2127
+ const height = bottom - top;
2128
+ if (!Number.isFinite(left) || !Number.isFinite(top) || width <= 0 || height <= 0) {
2129
+ return undefined;
2130
+ }
2131
+ return {
2132
+ x: left,
2133
+ y: top,
2134
+ width,
2135
+ height
2136
+ };
2137
+ }
2138
+ static readOfficeArtColorRef(value) {
2139
+ const flagByte = (value >>> 24) & 0xff;
2140
+ if (flagByte !== 0) {
2141
+ return undefined;
2142
+ }
2143
+ const red = value & 0xff;
2144
+ const green = (value >>> 8) & 0xff;
2145
+ const blue = (value >>> 16) & 0xff;
2146
+ return this.rgbToHex(red, green, blue);
2147
+ }
2148
+ static emuToPixels(emu) {
2149
+ if (!Number.isFinite(emu) || emu <= 0) {
2150
+ return undefined;
2151
+ }
2152
+ return Math.max(1, Math.round(emu / 9525));
2153
+ }
2154
+ static normalizeBounds(bounds, slideWidth, slideHeight, presentationMetrics) {
2155
+ let scaled = bounds;
2156
+ if (presentationMetrics && presentationMetrics.rawWidth > 0 && presentationMetrics.rawHeight > 0) {
2157
+ const scaleX = slideWidth / presentationMetrics.rawWidth;
2158
+ const scaleY = slideHeight / presentationMetrics.rawHeight;
2159
+ scaled = {
2160
+ x: Math.round(bounds.x * scaleX),
2161
+ y: Math.round(bounds.y * scaleY),
2162
+ width: Math.round(bounds.width * scaleX),
2163
+ height: Math.round(bounds.height * scaleY)
2164
+ };
2165
+ }
2166
+ const width = Math.max(24, Math.min(slideWidth, scaled.width));
2167
+ const height = Math.max(24, Math.min(slideHeight, scaled.height));
2168
+ const maxX = Math.max(0, slideWidth - width);
2169
+ const maxY = Math.max(0, slideHeight - height);
2170
+ const x = Math.max(0, Math.min(maxX, scaled.x));
2171
+ const y = Math.max(0, Math.min(maxY, scaled.y));
2172
+ return { x, y, width, height };
2173
+ }
2174
+ static dedupeVisualSlots(slots) {
2175
+ const sorted = [...slots].sort((left, right) => {
2176
+ const leftArea = (left.bounds?.width ?? 0) * (left.bounds?.height ?? 0);
2177
+ const rightArea = (right.bounds?.width ?? 0) * (right.bounds?.height ?? 0);
2178
+ const leftScore = (left.imageRefId !== undefined ? 1_000_000 : 0) + leftArea;
2179
+ const rightScore = (right.imageRefId !== undefined ? 1_000_000 : 0) + rightArea;
2180
+ return rightScore - leftScore;
2181
+ });
2182
+ const deduped = [];
2183
+ sorted.forEach((slot) => {
2184
+ if (!slot.bounds) {
2185
+ return;
2186
+ }
2187
+ const slotArea = slot.bounds.width * slot.bounds.height;
2188
+ const overlapsExisting = deduped.some((existing) => {
2189
+ if (!existing.bounds) {
2190
+ return false;
2191
+ }
2192
+ const existingArea = existing.bounds.width * existing.bounds.height;
2193
+ if (existingArea > slotArea * 8) {
2194
+ return false;
2195
+ }
2196
+ // Don't dedup slots with different image references
2197
+ if (slot.imageRefId !== undefined && existing.imageRefId !== undefined
2198
+ && slot.imageRefId !== existing.imageRefId) {
2199
+ return false;
2200
+ }
2201
+ return this.boundsOverlapRatio(slot.bounds, existing.bounds) > 0.72;
2202
+ });
2203
+ if (!overlapsExisting) {
2204
+ deduped.push(slot);
2205
+ }
2206
+ });
2207
+ return deduped;
2208
+ }
2209
+ static selectPreferredImageRefSlots(slots, picturesById) {
2210
+ if (!picturesById || slots.length < 2) {
2211
+ return slots;
2212
+ }
2213
+ // Count how many blip IDs share the same content to detect master/template images.
2214
+ const assetContentFrequency = new Map();
2215
+ for (const [, asset] of picturesById.entries()) {
2216
+ const sig = asset.base64.substring(0, 200);
2217
+ assetContentFrequency.set(sig, (assetContentFrequency.get(sig) || 0) + 1);
2218
+ }
2219
+ const preferred = [];
2220
+ const consumed = new Set();
2221
+ slots.forEach((slot, index) => {
2222
+ if (!slot.bounds || slot.imageRefId === undefined || consumed.has(index)) {
2223
+ if (!consumed.has(index)) {
2224
+ preferred.push(slot);
2225
+ consumed.add(index);
2226
+ }
2227
+ return;
2228
+ }
2229
+ const slotArea = slot.bounds.width * slot.bounds.height;
2230
+ const group = slots
2231
+ .map((candidate, candidateIndex) => ({ candidate, candidateIndex }))
2232
+ .filter(({ candidate, candidateIndex }) => {
2233
+ if (consumed.has(candidateIndex) || !candidate.bounds || candidate.imageRefId === undefined) {
2234
+ return false;
2235
+ }
2236
+ // Don't group slots with different image refs if they differ significantly in size
2237
+ if (candidate.imageRefId !== slot.imageRefId) {
2238
+ const candidateArea = candidate.bounds.width * candidate.bounds.height;
2239
+ if (Math.max(slotArea, candidateArea) > Math.min(slotArea, candidateArea) * 2) {
2240
+ return false;
2241
+ }
2242
+ }
2243
+ return this.boundsOverlapRatio(slot.bounds, candidate.bounds) > 0.95;
2244
+ });
2245
+ const best = group
2246
+ .slice()
2247
+ .sort((left, right) => {
2248
+ const leftAsset = picturesById.get(left.candidate.imageRefId);
2249
+ const rightAsset = picturesById.get(right.candidate.imageRefId);
2250
+ const leftFreq = leftAsset ? (assetContentFrequency.get(leftAsset.base64.substring(0, 200)) || 1) : 1;
2251
+ const rightFreq = rightAsset ? (assetContentFrequency.get(rightAsset.base64.substring(0, 200)) || 1) : 1;
2252
+ // Prefer unique (low frequency) images over master/template (high frequency) duplicates.
2253
+ if (leftFreq !== rightFreq) {
2254
+ return leftFreq - rightFreq;
2255
+ }
2256
+ // Prefer higher spContainerIndex (last drawn = visible on top in PPT rendering).
2257
+ const leftIdx = left.candidate.spContainerIndex ?? -1;
2258
+ const rightIdx = right.candidate.spContainerIndex ?? -1;
2259
+ if (leftIdx !== rightIdx) {
2260
+ return rightIdx - leftIdx;
2261
+ }
2262
+ const leftBytes = leftAsset ? this.decodedAssetByteLength(leftAsset) : 0;
2263
+ const rightBytes = rightAsset ? this.decodedAssetByteLength(rightAsset) : 0;
2264
+ return rightBytes - leftBytes;
2265
+ })[0];
2266
+ if (best) {
2267
+ preferred.push(best.candidate);
2268
+ group.forEach(({ candidateIndex }) => consumed.add(candidateIndex));
2269
+ }
2270
+ });
2271
+ return preferred;
2272
+ }
2273
+ static boundsOverlapRatio(left, right) {
2274
+ const x1 = Math.max(left.x, right.x);
2275
+ const y1 = Math.max(left.y, right.y);
2276
+ const x2 = Math.min(left.x + left.width, right.x + right.width);
2277
+ const y2 = Math.min(left.y + left.height, right.y + right.height);
2278
+ if (x2 <= x1 || y2 <= y1) {
2279
+ return 0;
2280
+ }
2281
+ const intersection = (x2 - x1) * (y2 - y1);
2282
+ const smallerArea = Math.max(1, Math.min(left.width * left.height, right.width * right.height));
2283
+ return intersection / smallerArea;
2284
+ }
2285
+ static boundsIntersect(left, right) {
2286
+ return left.x < right.x + right.width
2287
+ && left.x + left.width > right.x
2288
+ && left.y < right.y + right.height
2289
+ && left.y + left.height > right.y;
2290
+ }
2291
+ static extractTypedTextBlocksFromSequence(records) {
2292
+ const blocks = [];
2293
+ let currentTextType;
2294
+ let pendingStyleProp;
2295
+ for (let ri = 0; ri < records.length; ri++) {
2296
+ const record = records[ri];
2297
+ if (record.recType === 3999 && record.payload.length >= 4) {
2298
+ currentTextType = record.payload.readUInt32LE(0);
2299
+ continue;
2300
+ }
2301
+ // StyleTextPropAtom (4001) follows its text atom
2302
+ if (record.recType === 4001) {
2303
+ pendingStyleProp = record;
2304
+ continue;
2305
+ }
2306
+ const text = this.decodeTextAtom(record);
2307
+ if (!text) {
2308
+ if (record.children && record.children.length > 0) {
2309
+ const nestedBlocks = this.extractTypedTextBlocksFromSequence(record.children);
2310
+ if (nestedBlocks.length > 0) {
2311
+ blocks.push(...nestedBlocks.map((block) => ({
2312
+ ...block,
2313
+ textType: block.textType ?? currentTextType
2314
+ })));
2315
+ }
2316
+ }
2317
+ continue;
2318
+ }
2319
+ // Look ahead for StyleTextPropAtom
2320
+ let styleProp = pendingStyleProp;
2321
+ pendingStyleProp = undefined;
2322
+ if (!styleProp) {
2323
+ for (let si = ri + 1; si < records.length && si <= ri + 4; si++) {
2324
+ if (records[si].recType === 4001) {
2325
+ styleProp = records[si];
2326
+ break;
2327
+ }
2328
+ }
2329
+ }
2330
+ const style = styleProp ? this.extractTextRunStyle(styleProp.payload) : undefined;
2331
+ blocks.push({
2332
+ text,
2333
+ textType: currentTextType,
2334
+ color: style?.color,
2335
+ fontSizePx: style?.fontSizePx
2336
+ });
2337
+ }
2338
+ return blocks.filter((block) => block.textType !== 2);
2339
+ }
2340
+ /**
2341
+ * Extract dominant text color and font size from a StyleTextPropAtom payload.
2342
+ * Parses the paragraph run then character run per MS-PPT spec.
2343
+ */
2344
+ static extractTextRunStyle(payload) {
2345
+ if (payload.length < 12) {
2346
+ return undefined;
2347
+ }
2348
+ try {
2349
+ let offset = 0;
2350
+ // --- Paragraph run ---
2351
+ // charCount (4) + indentLevel (2) + paraMask (4)
2352
+ offset += 4; // charCount
2353
+ offset += 2; // indentLevel
2354
+ if (offset + 4 > payload.length) {
2355
+ return undefined;
2356
+ }
2357
+ const paraMask = payload.readUInt32LE(offset);
2358
+ offset += 4;
2359
+ // Skip paragraph properties based on mask
2360
+ if (paraMask & 0x000f) {
2361
+ offset += 2;
2362
+ } // bulletFlags
2363
+ if (paraMask & 0x0010) {
2364
+ offset += 2;
2365
+ } // bulletChar
2366
+ if (paraMask & 0x0020) {
2367
+ offset += 2;
2368
+ } // bulletFont
2369
+ if (paraMask & 0x0040) {
2370
+ offset += 2;
2371
+ } // bulletSize
2372
+ if (paraMask & 0x0080) {
2373
+ offset += 4;
2374
+ } // bulletColor
2375
+ if (paraMask & 0x0100) {
2376
+ offset += 2;
2377
+ } // leftMargin
2378
+ if (paraMask & 0x0200) {
2379
+ offset += 2;
2380
+ } // indent
2381
+ if (paraMask & 0x0400) {
2382
+ offset += 2;
2383
+ } // defaultTabSize
2384
+ if (paraMask & 0x0800) {
2385
+ offset += 2;
2386
+ } // alignment
2387
+ if (paraMask & 0x1000) {
2388
+ offset += 2;
2389
+ } // lineSpacing
2390
+ if (paraMask & 0x2000) {
2391
+ offset += 2;
2392
+ } // spaceBefore
2393
+ if (paraMask & 0x4000) {
2394
+ offset += 2;
2395
+ } // spaceAfter
2396
+ // --- Character run ---
2397
+ // charCount (4) + charMask (4)
2398
+ if (offset + 8 > payload.length) {
2399
+ return undefined;
2400
+ }
2401
+ offset += 4; // charCount
2402
+ const charMask = payload.readUInt32LE(offset);
2403
+ offset += 4;
2404
+ // Read character properties in MS-PPT spec order
2405
+ if (charMask & 0x03ff) {
2406
+ offset += 2;
2407
+ } // fontStyle (if any bool bit set)
2408
+ if (charMask & 0x00010000) {
2409
+ offset += 2;
2410
+ } // typeface
2411
+ if (charMask & 0x00100000) {
2412
+ offset += 2;
2413
+ } // oldEATypeface
2414
+ if (charMask & 0x00200000) {
2415
+ offset += 2;
2416
+ } // ansiTypeface
2417
+ if (charMask & 0x00400000) {
2418
+ offset += 2;
2419
+ } // symbolTypeface
2420
+ let fontSizePx;
2421
+ if (charMask & 0x00020000) {
2422
+ if (offset + 2 <= payload.length) {
2423
+ const rawSize = payload.readUInt16LE(offset);
2424
+ if (rawSize >= 6 && rawSize <= 400) {
2425
+ fontSizePx = Math.round(rawSize * this.POINTS_TO_PX);
2426
+ }
2427
+ }
2428
+ offset += 2;
2429
+ }
2430
+ let color;
2431
+ if (charMask & 0x00040000) {
2432
+ if (offset + 4 <= payload.length) {
2433
+ const colorValue = payload.readUInt32LE(offset);
2434
+ const flag = (colorValue >>> 24) & 0xff;
2435
+ if (flag === 0xfe) {
2436
+ const r = colorValue & 0xff;
2437
+ const g = (colorValue >>> 8) & 0xff;
2438
+ const b = (colorValue >>> 16) & 0xff;
2439
+ if (r !== 0 || g !== 0 || b !== 0) {
2440
+ color = this.rgbToHex(r, g, b);
2441
+ }
2442
+ }
2443
+ }
2444
+ }
2445
+ if (!color && !fontSizePx) {
2446
+ return undefined;
2447
+ }
2448
+ return { color, fontSizePx };
2449
+ }
2450
+ catch {
2451
+ return undefined;
2452
+ }
2453
+ }
2454
+ static decorateTextBlocksWithPlaceholders(blocks, layout) {
2455
+ if (!layout || blocks.length === 0) {
2456
+ return blocks;
2457
+ }
2458
+ const textPlaceholders = layout.placeholders.filter((value) => this.isTextPlaceholder(value));
2459
+ if (textPlaceholders.length === 0) {
2460
+ return blocks;
2461
+ }
2462
+ return blocks.map((block, index) => ({
2463
+ ...block,
2464
+ placeholderType: block.placeholderType ?? textPlaceholders[index]
2465
+ }));
2466
+ }
2467
+ static decorateTextGroupsWithPlaceholders(groups, layout) {
2468
+ if (!layout || groups.length === 0) {
2469
+ return groups;
2470
+ }
2471
+ const textPlaceholders = layout.placeholders.filter((value) => this.isTextPlaceholder(value));
2472
+ if (textPlaceholders.length === 0) {
2473
+ return groups;
2474
+ }
2475
+ return groups.map((group, index) => ({
2476
+ ...group,
2477
+ placeholderType: group.placeholderType ?? textPlaceholders[index],
2478
+ blocks: group.blocks.map((block) => ({
2479
+ ...block,
2480
+ placeholderType: block.placeholderType ?? textPlaceholders[index]
2481
+ }))
2482
+ }));
2483
+ }
2484
+ static decorateVisualSlotsWithPlaceholders(slots, layout) {
2485
+ if (!layout || slots.length === 0) {
2486
+ return slots;
2487
+ }
2488
+ const placeholders = layout.placeholders.filter((value) => value !== 0x00 && value !== 0xff);
2489
+ if (placeholders.length === 0) {
2490
+ return slots;
2491
+ }
2492
+ return slots.map((slot, index) => ({
2493
+ ...slot,
2494
+ placeholderType: slot.placeholderType ?? placeholders[index]
2495
+ }));
2496
+ }
2497
+ static isTextPlaceholder(value) {
2498
+ return value === 0x01
2499
+ || value === 0x02
2500
+ || value === 0x03
2501
+ || value === 0x04
2502
+ || value === 0x0d
2503
+ || value === 0x0e
2504
+ || value === 0x0f
2505
+ || value === 0x10
2506
+ || value === 0x11
2507
+ || value === 0x12;
2508
+ }
2509
+ static isVisualPlaceholder(value) {
2510
+ return value === 0x08
2511
+ || value === 0x0b
2512
+ || value === 0x0c
2513
+ || value === 0x0e
2514
+ || value === 0x14
2515
+ || value === 0x15;
2516
+ }
2517
+ static classifyPlaceholderType(value) {
2518
+ if (value === 0x0f || value === 0x0d || value === 0x11 || value === 0x03 || value === 0x01) {
2519
+ return 'title';
2520
+ }
2521
+ if (value === 0x10 || value === 0x04) {
2522
+ return 'subtitle';
2523
+ }
2524
+ if (value === 0x0e || value === 0x12 || value === 0x02 || value === 0x06 || value === 0x0c) {
2525
+ return 'body';
2526
+ }
2527
+ return 'other';
2528
+ }
2529
+ static extractSlideColorScheme(record) {
2530
+ const colorSchemeAtom = (record.children ?? []).find((child) => child.recType === 2032 && child.payload.length >= 32);
2531
+ if (!colorSchemeAtom) {
2532
+ return null;
2533
+ }
2534
+ return {
2535
+ backgroundColor: this.readColorStruct(colorSchemeAtom.payload, 0),
2536
+ textColor: this.readColorStruct(colorSchemeAtom.payload, 4),
2537
+ titleColor: this.readColorStruct(colorSchemeAtom.payload, 12),
2538
+ fillColor: this.readColorStruct(colorSchemeAtom.payload, 16)
2539
+ };
2540
+ }
2541
+ static extractDocumentColorScheme(records) {
2542
+ for (const record of records) {
2543
+ if ((record.recType === 1000 || record.recType === 1016) && record.children && record.children.length > 0) {
2544
+ const directScheme = this.extractSlideColorScheme(record);
2545
+ if (directScheme) {
2546
+ return directScheme;
2547
+ }
2548
+ }
2549
+ if (record.children && record.children.length > 0) {
2550
+ const nestedScheme = this.extractDocumentColorScheme(record.children);
2551
+ if (nestedScheme) {
2552
+ return nestedScheme;
2553
+ }
2554
+ }
2555
+ }
2556
+ return null;
2557
+ }
2558
+ static readColorStruct(payload, offset) {
2559
+ if (offset < 0 || offset + 4 > payload.length) {
2560
+ return undefined;
2561
+ }
2562
+ const red = payload.readUInt8(offset);
2563
+ const green = payload.readUInt8(offset + 1);
2564
+ const blue = payload.readUInt8(offset + 2);
2565
+ return this.rgbToHex(red, green, blue);
2566
+ }
2567
+ static rgbToHex(red, green, blue) {
2568
+ const toHex = (value) => value.toString(16).padStart(2, '0');
2569
+ return `#${toHex(red)}${toHex(green)}${toHex(blue)}`;
2570
+ }
2571
+ static extractTextsFromRecord(record) {
2572
+ const texts = [];
2573
+ const visit = (r) => {
2574
+ const isTextType = r.recType === 4000 || r.recType === 3998 || r.recType === 4026 || r.recType === 4086;
2575
+ const isOfficeArtRecord = r.recType >= 0xf000 && r.recType <= 0xf3ff;
2576
+ if (isTextType || (!isOfficeArtRecord && r.recVer !== 0x0f && r.payload.length > 4 && r.payload.length < 4096)) {
2577
+ const candidates = this.decodeTextCandidates(r.payload);
2578
+ candidates.forEach((t) => {
2579
+ if (!t)
2580
+ return;
2581
+ if (this.isNoiseText(t))
2582
+ return;
2583
+ texts.push(t);
2584
+ });
2585
+ }
2586
+ if (r.children)
2587
+ r.children.forEach(visit);
2588
+ };
2589
+ visit(record);
2590
+ const dedup = new Set();
2591
+ const out = [];
2592
+ texts.forEach((t) => {
2593
+ const normalized = t.replace(/\s+/g, ' ').trim();
2594
+ if (!normalized || dedup.has(normalized))
2595
+ return;
2596
+ dedup.add(normalized);
2597
+ out.push(normalized);
2598
+ });
2599
+ return out;
2600
+ }
2601
+ static extractLooseTexts(stream) {
2602
+ const out = [];
2603
+ out.push(...this.decodeTextCandidates(stream));
2604
+ return out.filter((t) => !this.isNoiseText(t)).slice(0, 80);
2605
+ }
2606
+ static decodeTextCandidates(payload) {
2607
+ const out = [];
2608
+ // UTF-16LE chunks
2609
+ const utf16 = payload.toString('utf16le')
2610
+ .replace(/\u0000/g, '')
2611
+ .split(/[\r\n\t]+/)
2612
+ .map((s) => s.trim())
2613
+ .filter((s) => s.length >= 3 && /[A-Za-z0-9가-힣]/.test(s) && this.isMostlyReadable(s));
2614
+ out.push(...utf16);
2615
+ // Legacy byte chunks (for example CP949/EUC-KR decks)
2616
+ const legacy = this.decodeLegacyByteText(payload)
2617
+ .split(/[\x00-\x1f]+/)
2618
+ .map((s) => s.trim())
2619
+ .filter((s) => s.length >= 2 && /[A-Za-z0-9가-힣]/.test(s) && this.isMostlyReadable(s));
2620
+ out.push(...legacy);
2621
+ // Latin1 chunks
2622
+ const latin = payload.toString('latin1')
2623
+ .split(/[\x00-\x1f]+/)
2624
+ .map((s) => s.trim())
2625
+ .filter((s) => s.length >= 4 && /[A-Za-z0-9]/.test(s) && this.isMostlyReadable(s));
2626
+ out.push(...latin);
2627
+ return out.slice(0, 30);
2628
+ }
2629
+ static isNoiseText(text) {
2630
+ const t = text.toLowerCase();
2631
+ if (t.length < 3)
2632
+ return true;
2633
+ if (!this.isMostlyReadable(text))
2634
+ return true;
2635
+ if (/^[^A-Za-z0-9가-힣]+$/.test(t))
2636
+ return true;
2637
+ if (/^(arial|times new roman|calibri|wingdings)$/i.test(text))
2638
+ return true;
2639
+ if (/click to edit master/i.test(t))
2640
+ return true;
2641
+ if (/^_+ppt\d+/i.test(t))
2642
+ return true;
2643
+ return false;
2644
+ }
2645
+ static isMostlyReadable(text) {
2646
+ const normalized = (text || '').trim();
2647
+ if (!normalized)
2648
+ return false;
2649
+ let readable = 0;
2650
+ for (let i = 0; i < normalized.length; i++) {
2651
+ const code = normalized.charCodeAt(i);
2652
+ const isBasicPrintable = code >= 32 && code <= 126;
2653
+ const isKorean = code >= 0xac00 && code <= 0xd7a3;
2654
+ const isCommonPunct = '“”‘’•…–—·©®™°'.includes(normalized[i]);
2655
+ if (isBasicPrintable || isKorean || isCommonPunct)
2656
+ readable += 1;
2657
+ }
2658
+ const ratio = readable / Math.max(1, normalized.length);
2659
+ return ratio >= 0.75;
2660
+ }
2661
+ static extractPictures(picturesStream) {
2662
+ if (!picturesStream || picturesStream.length === 0)
2663
+ return [];
2664
+ const out = [];
2665
+ // PNG scan
2666
+ const pngSig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
2667
+ let idx = 0;
2668
+ while ((idx = picturesStream.indexOf(pngSig, idx)) !== -1) {
2669
+ const end = this.findPngEnd(picturesStream, idx);
2670
+ if (end > idx) {
2671
+ out.push({ mime: 'image/png', base64: picturesStream.slice(idx, end).toString('base64'), pictureIndex: out.length });
2672
+ idx = end;
2673
+ }
2674
+ else {
2675
+ idx += pngSig.length;
2676
+ }
2677
+ }
2678
+ // JPEG scan
2679
+ idx = 0;
2680
+ while ((idx = picturesStream.indexOf(Buffer.from([0xff, 0xd8]), idx)) !== -1) {
2681
+ const end = this.findJpegEnd(picturesStream, idx);
2682
+ if (end > idx) {
2683
+ out.push({
2684
+ mime: 'image/jpeg',
2685
+ base64: picturesStream.slice(idx, end).toString('base64'),
2686
+ pictureIndex: out.length
2687
+ });
2688
+ idx = end;
2689
+ }
2690
+ else {
2691
+ idx += 2;
2692
+ }
2693
+ }
2694
+ return out;
2695
+ }
2696
+ static extractPicturesByBlipId(records, picturesStream, pictures = []) {
2697
+ const byId = new Map();
2698
+ if (!picturesStream || picturesStream.length === 0) {
2699
+ return byId;
2700
+ }
2701
+ const entries = [];
2702
+ const visit = (list) => {
2703
+ for (const record of list) {
2704
+ if (record.recType === 0xf007 && record.payload.length >= 36) {
2705
+ entries.push(record);
2706
+ }
2707
+ if (record.children && record.children.length > 0) {
2708
+ visit(record.children);
2709
+ }
2710
+ }
2711
+ };
2712
+ visit(records);
2713
+ entries.forEach((entry, index) => {
2714
+ const offset = entry.payload.readUInt32LE(28);
2715
+ const size = entry.payload.readUInt32LE(20);
2716
+ const asset = this.extractPictureAtOffset(picturesStream, offset, size, pictures);
2717
+ if (asset) {
2718
+ byId.set(index + 1, asset);
2719
+ }
2720
+ });
2721
+ return byId;
2722
+ }
2723
+ static extractPictureAtOffset(picturesStream, offset, expectedSize, pictures = []) {
2724
+ if (!Number.isFinite(offset) || offset < 0 || offset >= picturesStream.length) {
2725
+ return null;
2726
+ }
2727
+ const probe = picturesStream.subarray(offset, Math.min(picturesStream.length, offset + 96));
2728
+ const pngOffset = probe.indexOf(Buffer.from([0x89, 0x50, 0x4e, 0x47]));
2729
+ if (pngOffset !== -1) {
2730
+ const start = offset + pngOffset;
2731
+ const end = expectedSize > 0 && start + expectedSize <= picturesStream.length
2732
+ ? start + expectedSize
2733
+ : this.findPngEnd(picturesStream, start);
2734
+ if (end > start) {
2735
+ const base64 = picturesStream.slice(start, end).toString('base64');
2736
+ const pictureIndex = pictures.findIndex((picture) => picture.base64 === base64);
2737
+ return {
2738
+ mime: 'image/png',
2739
+ base64,
2740
+ pictureIndex: pictureIndex >= 0 ? pictureIndex : undefined
2741
+ };
2742
+ }
2743
+ }
2744
+ const jpegOffset = probe.indexOf(Buffer.from([0xff, 0xd8, 0xff]));
2745
+ if (jpegOffset !== -1) {
2746
+ const start = offset + jpegOffset;
2747
+ let end = -1;
2748
+ // Try to determine the BLIP record boundary from its OfficeArt header.
2749
+ // The BStoreEntry offset points to the start of the BLIP record which
2750
+ // has an 8-byte header (ver/type 4 bytes + length 4 bytes).
2751
+ if (offset + 8 <= picturesStream.length) {
2752
+ const recType = picturesStream.readUInt16LE(offset + 2);
2753
+ if (recType >= 0xf018 && recType <= 0xf117) {
2754
+ const recLen = picturesStream.readUInt32LE(offset + 4);
2755
+ const blipEnd = offset + 8 + recLen;
2756
+ if (blipEnd <= picturesStream.length && blipEnd > start) {
2757
+ // Scan backwards from the BLIP end to find the FFD9 terminator
2758
+ for (let scan = blipEnd - 2; scan >= start + 2; scan--) {
2759
+ if (picturesStream[scan] === 0xff && picturesStream[scan + 1] === 0xd9) {
2760
+ end = scan + 2;
2761
+ break;
2762
+ }
2763
+ }
2764
+ }
2765
+ }
2766
+ }
2767
+ if (end === -1) {
2768
+ end = this.findJpegEnd(picturesStream, start);
2769
+ }
2770
+ if (end > start) {
2771
+ const base64 = picturesStream.slice(start, end).toString('base64');
2772
+ const pictureIndex = pictures.findIndex((picture) => picture.base64 === base64);
2773
+ return {
2774
+ mime: 'image/jpeg',
2775
+ base64,
2776
+ pictureIndex: pictureIndex >= 0 ? pictureIndex : undefined
2777
+ };
2778
+ }
2779
+ }
2780
+ return null;
2781
+ }
2782
+ /**
2783
+ * Walk JPEG marker segments to find the real EOI (FFD9), avoiding false
2784
+ * end markers embedded inside metadata segments like Photoshop APP13.
2785
+ */
2786
+ static findJpegEnd(buf, start) {
2787
+ let pos = start + 2; // skip SOI (FFD8)
2788
+ while (pos < buf.length - 1) {
2789
+ if (buf[pos] !== 0xff) {
2790
+ pos++;
2791
+ continue;
2792
+ }
2793
+ const marker = buf[pos + 1];
2794
+ // Byte-stuffed 0xFF00 or padding 0xFFFF — skip
2795
+ if (marker === 0x00 || marker === 0xff) {
2796
+ pos++;
2797
+ continue;
2798
+ }
2799
+ // EOI
2800
+ if (marker === 0xd9)
2801
+ return pos + 2;
2802
+ // SOS (FFDA) — entropy-coded data follows; scan for next marker
2803
+ if (marker === 0xda) {
2804
+ if (pos + 3 >= buf.length)
2805
+ return -1;
2806
+ const sosLen = buf.readUInt16BE(pos + 2);
2807
+ let scan = pos + 2 + sosLen;
2808
+ while (scan < buf.length - 1) {
2809
+ if (buf[scan] === 0xff) {
2810
+ const m = buf[scan + 1];
2811
+ if (m === 0xd9)
2812
+ return scan + 2;
2813
+ if (m === 0x00) {
2814
+ scan += 2;
2815
+ continue;
2816
+ } // stuffed byte
2817
+ if (m >= 0xd0 && m <= 0xd7) {
2818
+ scan += 2;
2819
+ continue;
2820
+ } // RST markers
2821
+ // Another marker (e.g. DHT, SOS in multi-scan JPEG) —
2822
+ // resume the main segment-walking loop from here.
2823
+ pos = scan;
2824
+ break;
2825
+ }
2826
+ scan++;
2827
+ }
2828
+ if (scan >= buf.length - 1)
2829
+ return -1;
2830
+ continue;
2831
+ }
2832
+ // Variable-length segment: read length and skip
2833
+ if (pos + 3 >= buf.length)
2834
+ return -1;
2835
+ const segLen = buf.readUInt16BE(pos + 2);
2836
+ if (segLen < 2)
2837
+ return -1;
2838
+ pos += 2 + segLen;
2839
+ }
2840
+ return -1;
2841
+ }
2842
+ static findPngEnd(buf, start) {
2843
+ let off = start + 8;
2844
+ while (off + 12 <= buf.length) {
2845
+ const len = buf.readUInt32BE(off);
2846
+ const type = buf.slice(off + 4, off + 8).toString('ascii');
2847
+ off += 12 + len;
2848
+ if (type === 'IEND')
2849
+ return off;
2850
+ }
2851
+ return -1;
2852
+ }
2853
+ }