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,1400 @@
1
+ // PDF viewer — DOM renderer over pdf.js (DESIGN.md §3-②, docs/viewers/pdf.md).
2
+ //
3
+ // Base entry rule (ADR 14): no external imports here. pdf.js and pdf-lib are
4
+ // injected through `PdfViewerDeps`; `viewers/pdf/self-loading` supplies the
5
+ // dynamic-import loaders for platforms whose bundler can resolve the optional
6
+ // peers. Without `loadPdfLib` the editing features render disabled with a
7
+ // reason (degraded mode, ADR 24) — reading always works.
8
+ //
9
+ // CSP rules: no eval, no inline handlers, no innerHTML. No window.prompt /
10
+ // alert / confirm — those are blocked inside the VS Code webview, so text
11
+ // annotations use an inline input overlay instead.
12
+ import { MountAbortedError, VIEWER_ROOT_CLASS } from '../types.js';
13
+ import { createPdfController, mergeHighlightRects, PDF_ZOOM_LEVELS } from './controller.js';
14
+ import { buildSavedPdf, mergePdfBytes, parseLayer, savedPdfName, SIDECAR_LAYER_NAME, SIDECAR_ORIGINAL_NAME } from './editing.js';
15
+ import { pdfViewerCss } from './styles.js';
16
+ export { createPdfController, PDF_ZOOM_LEVELS } from './controller.js';
17
+ export { pdfViewerCss } from './styles.js';
18
+ export { buildEditedPdf, mergePdfBytes, savedPdfName } from './editing.js';
19
+ /** Viewer metadata — single source for the registry codegen (DESIGN.md §7). */
20
+ export const PDF_VIEWER_META = {
21
+ id: 'pdf',
22
+ displayNameKey: 'pdf.title',
23
+ extensions: ['pdf'],
24
+ priority: 20,
25
+ requiredServices: [],
26
+ optionalServices: ['save', 'writeback', 'filePick'],
27
+ inputOwnership: 'borrows'
28
+ };
29
+ const THUMB_FALLBACK_RENDER_COUNT = 10;
30
+ function isPasswordError(error) {
31
+ return (typeof error === 'object' &&
32
+ error !== null &&
33
+ error.name === 'PasswordException');
34
+ }
35
+ export async function mountPdfViewer(input, container, ctx, deps, options = {}) {
36
+ if (options.signal?.aborted)
37
+ throw new MountAbortedError();
38
+ const t = ctx.i18n.t.bind(ctx.i18n);
39
+ // ------------------------------------------------------------- root/frame
40
+ const isolation = options.styleIsolation ?? 'shadow';
41
+ let root;
42
+ if (isolation === 'shadow' && typeof container.attachShadow === 'function') {
43
+ root = container.shadowRoot ?? container.attachShadow({ mode: 'open' });
44
+ root.replaceChildren();
45
+ const style = document.createElement('style');
46
+ style.textContent = pdfViewerCss;
47
+ root.appendChild(style);
48
+ }
49
+ else {
50
+ container.classList.add(VIEWER_ROOT_CLASS, 'omni-viewer--pdf');
51
+ root = container;
52
+ }
53
+ const el = (tag, className, text) => {
54
+ const node = document.createElement(tag);
55
+ if (className)
56
+ node.className = className;
57
+ if (text !== undefined)
58
+ node.textContent = text;
59
+ return node;
60
+ };
61
+ const frame = el('div', 'omni-pdf');
62
+ const header = el('div', 'omni-pdf__header');
63
+ const title = el('div', 'omni-pdf__title', input.fileName);
64
+ const toolbar = el('div', 'omni-pdf__toolbar');
65
+ const pageInfo = el('span', 'omni-pdf__page-info');
66
+ const pageInput = el('input', 'omni-pdf__page-input');
67
+ pageInput.type = 'number';
68
+ pageInput.min = '1';
69
+ pageInput.step = '1';
70
+ pageInput.value = '1';
71
+ pageInput.setAttribute('aria-label', t('pdf.goToPage'));
72
+ const pageTotal = el('span', 'omni-pdf__page-total', '/ 0');
73
+ pageInfo.append(pageInput, pageTotal);
74
+ header.append(title, toolbar);
75
+ const zoomBar = el('div', 'omni-pdf__zoom');
76
+ const body = el('div', 'omni-pdf__body');
77
+ const thumbs = el('div', 'omni-pdf__thumbs');
78
+ const pages = el('div', 'omni-pdf__pages');
79
+ body.append(thumbs, pages);
80
+ const status = el('div', 'omni-pdf__status', t('pdf.loading'));
81
+ status.setAttribute('aria-live', 'polite');
82
+ frame.append(header, zoomBar, body, status);
83
+ root.appendChild(frame);
84
+ // ------------------------------------------------------------- lifecycle
85
+ let disposed = false;
86
+ let doc;
87
+ let loadingTask;
88
+ let pageObserver;
89
+ let thumbObserver;
90
+ let records = [];
91
+ let renderEpoch = 0;
92
+ let currentPage = 1;
93
+ let tool = 'view';
94
+ let highlightColor = '#ffeb3b';
95
+ let underlineColor = '#000000';
96
+ let strikeoutColor = '#000000';
97
+ let markupKind = 'highlight';
98
+ let pageLayout = 'single';
99
+ let matchThemeColors = false;
100
+ let sourceBytes = input.data;
101
+ /** A merge changed the document without going through the edit stack. */
102
+ let mergedSinceSave = false;
103
+ let pdfLib;
104
+ let unsubscribe;
105
+ let controller = createPdfController(0);
106
+ /** Layout inputs of the last rebuild — annotation-only changes skip it. */
107
+ let layoutKey = '';
108
+ const abortListener = () => {
109
+ loadingTask?.destroy();
110
+ };
111
+ options.signal?.addEventListener('abort', abortListener, { once: true });
112
+ function throwIfAborted() {
113
+ if (options.signal?.aborted) {
114
+ teardown();
115
+ throw new MountAbortedError();
116
+ }
117
+ }
118
+ function teardown() {
119
+ disposed = true;
120
+ document.removeEventListener('keydown', keyHandler);
121
+ document.removeEventListener('selectionchange', syncMarkupAvailability);
122
+ options.signal?.removeEventListener('abort', abortListener);
123
+ unsubscribe?.();
124
+ pageObserver?.disconnect();
125
+ thumbObserver?.disconnect();
126
+ for (const record of records) {
127
+ record.task?.cancel();
128
+ record.textLayer?.cancel();
129
+ }
130
+ records = [];
131
+ void doc?.destroy();
132
+ doc = undefined;
133
+ root.replaceChildren();
134
+ if (root === container) {
135
+ container.classList.remove(VIEWER_ROOT_CLASS, 'omni-viewer--pdf');
136
+ }
137
+ }
138
+ const keyHandler = (keyEvent) => {
139
+ const target = keyEvent.target;
140
+ if ((keyEvent.key === 'Delete' || keyEvent.key === 'Backspace')
141
+ && !(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) {
142
+ const selectedId = controller.state.selectedAnnotationId;
143
+ const selected = controller.state.annotations.find((annotation) => annotation.id === selectedId);
144
+ if (selected) {
145
+ keyEvent.preventDefault();
146
+ controller.dispatch({ type: 'remove-annotation', id: selected.id });
147
+ return;
148
+ }
149
+ }
150
+ if ((!keyEvent.metaKey && !keyEvent.ctrlKey) || keyEvent.altKey || keyEvent.key.toLowerCase() !== 'z')
151
+ return;
152
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)
153
+ return;
154
+ keyEvent.preventDefault();
155
+ controller.dispatch({ type: keyEvent.shiftKey ? 'redo' : 'undo' });
156
+ };
157
+ document.addEventListener('keydown', keyHandler);
158
+ // -------------------------------------------------------------- pdf.js
159
+ const pdfjs = await deps.loadPdfjs();
160
+ throwIfAborted();
161
+ pdfjs.GlobalWorkerOptions.workerSrc = await ctx.assets.resolveAssetUrl('assets/pdfjs/pdf.worker.min.mjs');
162
+ throwIfAborted();
163
+ async function loadDocument(bytes, password) {
164
+ const request = {
165
+ // pdf.js transfers the buffer to its worker; keep ours intact.
166
+ data: bytes.slice()
167
+ };
168
+ if (password !== undefined)
169
+ request.password = password;
170
+ loadingTask = pdfjs.getDocument(request);
171
+ try {
172
+ return await loadingTask.promise;
173
+ }
174
+ finally {
175
+ loadingTask = undefined;
176
+ }
177
+ }
178
+ /**
179
+ * Read the hybrid sidecar (pristine source + layer JSON) from a document's
180
+ * attachments. Returns null unless BOTH parts are present and the layer
181
+ * parses — a lone layer over already-flattened pages would double up.
182
+ */
183
+ async function readSidecar(d) {
184
+ try {
185
+ const attachments = await d.getAttachments?.();
186
+ if (!attachments)
187
+ return null;
188
+ let pristine;
189
+ let layerText;
190
+ for (const entry of Object.values(attachments)) {
191
+ if (entry.filename === SIDECAR_ORIGINAL_NAME)
192
+ pristine = entry.content;
193
+ else if (entry.filename === SIDECAR_LAYER_NAME) {
194
+ layerText = new TextDecoder().decode(entry.content);
195
+ }
196
+ }
197
+ if (!pristine || layerText === undefined)
198
+ return null;
199
+ const layer = parseLayer(layerText);
200
+ return layer ? { pristine, layer } : null;
201
+ }
202
+ catch {
203
+ return null;
204
+ }
205
+ }
206
+ // ------------------------------------------------------- password prompt
207
+ function askPassword(retry) {
208
+ return new Promise((resolve) => {
209
+ const overlay = el('div', 'omni-pdf__modal is-open');
210
+ const panel = el('div', 'omni-pdf__panel');
211
+ panel.appendChild(el('div', undefined, t('pdf.passwordRequired')));
212
+ if (retry) {
213
+ panel.appendChild(el('div', 'omni-pdf__error', t('pdf.passwordIncorrect')));
214
+ }
215
+ const inputEl = el('input');
216
+ inputEl.type = 'password';
217
+ inputEl.setAttribute('aria-label', t('pdf.password'));
218
+ const actions = el('div', 'omni-pdf__panel-actions');
219
+ const cancel = el('button', undefined, t('pdf.cancel'));
220
+ cancel.type = 'button';
221
+ const submit = el('button', undefined, t('pdf.passwordSubmit'));
222
+ submit.type = 'button';
223
+ actions.append(cancel, submit);
224
+ panel.append(inputEl, actions);
225
+ overlay.appendChild(panel);
226
+ frame.appendChild(overlay);
227
+ const finish = (value) => {
228
+ overlay.remove();
229
+ resolve(value);
230
+ };
231
+ submit.addEventListener('click', () => finish(inputEl.value));
232
+ cancel.addEventListener('click', () => finish(null));
233
+ inputEl.addEventListener('keydown', (e) => {
234
+ if (e.key === 'Enter')
235
+ finish(inputEl.value);
236
+ if (e.key === 'Escape')
237
+ finish(null);
238
+ });
239
+ inputEl.focus();
240
+ });
241
+ }
242
+ // --------------------------------------------------------------- toolbar
243
+ const editingAvailable = typeof deps.loadPdfLib === 'function';
244
+ function toolButton(labelKey, onClick, parent = toolbar) {
245
+ const b = el('button', 'omni-pdf__tool', t(labelKey));
246
+ b.type = 'button';
247
+ b.addEventListener('click', onClick);
248
+ parent.appendChild(b);
249
+ return b;
250
+ }
251
+ /** Degraded mode (ADR 24): disabled with a reason, never hidden. */
252
+ function degrade(button, reasonKey) {
253
+ button.disabled = true;
254
+ button.title = t(reasonKey);
255
+ }
256
+ toolbar.appendChild(pageInfo);
257
+ const textBtn = toolButton('pdf.annotationText', () => {
258
+ tool = tool === 'text' ? 'view' : 'text';
259
+ syncToolButtons();
260
+ status.textContent = tool === 'text' ? t('pdf.textPlace') : '';
261
+ });
262
+ const signatureBtn = toolButton('pdf.signature', () => {
263
+ tool = tool === 'signature' ? 'view' : 'signature';
264
+ syncToolButtons();
265
+ status.textContent = tool === 'signature' ? t('pdf.signaturePlace') : '';
266
+ });
267
+ const markupWrap = el('div', 'omni-pdf__markup-wrap');
268
+ toolbar.appendChild(markupWrap);
269
+ const highlightBtn = toolButton('✎', () => {
270
+ if (captureMarkup(markupKind))
271
+ status.textContent = '';
272
+ }, markupWrap);
273
+ // Avoid moving focus to the toolbar before its click handler reads the
274
+ // browser selection (some hosts otherwise clear it on mousedown).
275
+ highlightBtn.addEventListener('mousedown', (event) => event.preventDefault());
276
+ const markupMenuBtn = el('button', 'omni-pdf__markup-menu-btn', '⌄');
277
+ markupMenuBtn.type = 'button';
278
+ markupMenuBtn.setAttribute('aria-label', t('pdf.markupOptions'));
279
+ markupMenuBtn.setAttribute('aria-expanded', 'false');
280
+ const markupMenu = el('div', 'omni-pdf__markup-menu');
281
+ markupMenu.setAttribute('role', 'menu');
282
+ const colorInput = (labelKey, initial, onChange) => {
283
+ const input = el('input', 'omni-pdf__highlight-color');
284
+ input.type = 'color';
285
+ input.value = initial;
286
+ input.setAttribute('aria-label', t(labelKey));
287
+ input.title = t(labelKey);
288
+ input.addEventListener('input', () => onChange(input.value));
289
+ return input;
290
+ };
291
+ const highlightColorInput = colorInput('pdf.highlightColor', highlightColor, (value) => {
292
+ highlightColor = value;
293
+ });
294
+ const underlineColorInput = colorInput('pdf.underlineColor', underlineColor, (value) => {
295
+ underlineColor = value;
296
+ });
297
+ const strikeoutColorInput = colorInput('pdf.strikeoutColor', strikeoutColor, (value) => {
298
+ strikeoutColor = value;
299
+ });
300
+ const markupChoice = (kind, labelKey, input) => {
301
+ const row = el('div', 'omni-pdf__markup-choice');
302
+ const button = el('button', undefined, t(labelKey));
303
+ button.type = 'button';
304
+ button.setAttribute('role', 'menuitemradio');
305
+ button.addEventListener('mousedown', (event) => event.preventDefault());
306
+ button.addEventListener('click', () => {
307
+ markupKind = kind;
308
+ syncMarkupKind();
309
+ markupMenu.classList.remove('is-open');
310
+ markupMenuBtn.setAttribute('aria-expanded', 'false');
311
+ });
312
+ row.append(button, input);
313
+ markupMenu.appendChild(row);
314
+ return button;
315
+ };
316
+ const highlightChoiceBtn = markupChoice('highlight', 'pdf.highlight', highlightColorInput);
317
+ const underlineBtn = markupChoice('underline', 'pdf.underline', underlineColorInput);
318
+ const strikeoutBtn = markupChoice('strikeout', 'pdf.strikeout', strikeoutColorInput);
319
+ function syncMarkupKind() {
320
+ const current = markupKind === 'highlight' ? '✎' : markupKind === 'underline' ? 'U̲' : 'S̶';
321
+ const labelKey = markupKind === 'highlight'
322
+ ? 'pdf.highlight'
323
+ : markupKind === 'underline' ? 'pdf.underline' : 'pdf.strikeout';
324
+ highlightBtn.textContent = current;
325
+ highlightBtn.setAttribute('aria-label', t(labelKey));
326
+ highlightBtn.title = t(labelKey);
327
+ for (const [kind, button] of [
328
+ ['highlight', highlightChoiceBtn],
329
+ ['underline', underlineBtn],
330
+ ['strikeout', strikeoutBtn]
331
+ ])
332
+ button.setAttribute('aria-checked', String(kind === markupKind));
333
+ }
334
+ syncMarkupKind();
335
+ markupWrap.append(highlightBtn, markupMenuBtn, markupMenu);
336
+ markupMenuBtn.addEventListener('click', () => {
337
+ const isOpen = markupMenu.classList.toggle('is-open');
338
+ markupMenuBtn.setAttribute('aria-expanded', String(isOpen));
339
+ });
340
+ const resetBtn = toolButton('pdf.resetPages', () => controller.dispatch({ type: 'reset-pages' }));
341
+ const saveBtn = toolButton('pdf.save', () => void save());
342
+ const saveAsBtn = toolButton('pdf.saveAs', () => void saveAs());
343
+ const mergeBtn = toolButton('pdf.merge', () => void merge());
344
+ function syncToolButtons() {
345
+ textBtn.setAttribute('aria-pressed', String(tool === 'text'));
346
+ signatureBtn.setAttribute('aria-pressed', String(tool === 'signature'));
347
+ // Placement tools need raw canvas clicks; view mode keeps text selectable.
348
+ pages.classList.toggle('omni-pdf__pages--placing', tool === 'text' || tool === 'signature');
349
+ }
350
+ syncToolButtons();
351
+ if (!editingAvailable) {
352
+ for (const b of [textBtn, signatureBtn, highlightBtn, markupMenuBtn, highlightChoiceBtn, underlineBtn, strikeoutBtn, resetBtn, saveBtn, saveAsBtn, mergeBtn]) {
353
+ degrade(b, 'pdf.editingUnavailable');
354
+ }
355
+ highlightColorInput.disabled = true;
356
+ underlineColorInput.disabled = true;
357
+ strikeoutColorInput.disabled = true;
358
+ }
359
+ else {
360
+ if (!ctx.writeback)
361
+ degrade(saveBtn, 'common.noWriteback');
362
+ if (!ctx.save)
363
+ degrade(saveAsBtn, 'common.noFileSave');
364
+ if (!ctx.filePick)
365
+ degrade(mergeBtn, 'pdf.noFilePick');
366
+ }
367
+ function syncMarkupAvailability() {
368
+ if (!editingAvailable)
369
+ return;
370
+ const enabled = activeSelection() !== null;
371
+ highlightBtn.disabled = !enabled;
372
+ markupMenuBtn.disabled = !enabled;
373
+ highlightChoiceBtn.disabled = !enabled;
374
+ underlineBtn.disabled = !enabled;
375
+ strikeoutBtn.disabled = !enabled;
376
+ if (!enabled) {
377
+ markupMenu.classList.remove('is-open');
378
+ markupMenuBtn.setAttribute('aria-expanded', 'false');
379
+ }
380
+ }
381
+ document.addEventListener('selectionchange', syncMarkupAvailability);
382
+ pages.addEventListener('mouseup', () => requestAnimationFrame(syncMarkupAvailability));
383
+ syncMarkupAvailability();
384
+ const zoomOutBtn = el('button', 'omni-pdf__zoom-btn', '−');
385
+ zoomOutBtn.type = 'button';
386
+ zoomOutBtn.setAttribute('aria-label', t('pdf.zoomOut'));
387
+ zoomOutBtn.addEventListener('click', () => controller.dispatch({ type: 'zoom-out' }));
388
+ const zoomLevel = el('span', 'omni-pdf__zoom-level');
389
+ zoomLevel.setAttribute('aria-live', 'polite');
390
+ const zoomInBtn = el('button', 'omni-pdf__zoom-btn', '+');
391
+ zoomInBtn.type = 'button';
392
+ zoomInBtn.setAttribute('aria-label', t('pdf.zoomIn'));
393
+ zoomInBtn.addEventListener('click', () => controller.dispatch({ type: 'zoom-in' }));
394
+ const viewMenuWrap = el('div', 'omni-pdf__view-menu-wrap');
395
+ const viewMenuBtn = el('button', 'omni-pdf__view-menu-btn', '⌄');
396
+ viewMenuBtn.type = 'button';
397
+ viewMenuBtn.setAttribute('aria-label', t('pdf.viewOptions'));
398
+ viewMenuBtn.setAttribute('aria-expanded', 'false');
399
+ const viewMenu = el('div', 'omni-pdf__view-menu');
400
+ viewMenu.setAttribute('role', 'menu');
401
+ const fitWidthBtn = el('button', undefined, t('pdf.fitWidth'));
402
+ const fitHeightBtn = el('button', undefined, t('pdf.fitHeight'));
403
+ const singlePageBtn = el('button', undefined, t('pdf.singlePage'));
404
+ const spreadOddBtn = el('button', undefined, t('pdf.twoPagesOdd'));
405
+ const spreadEvenBtn = el('button', undefined, t('pdf.twoPagesEven'));
406
+ const themeBtn = el('button', undefined, t('pdf.matchTheme'));
407
+ const separator = document.createElement('hr');
408
+ const separator2 = document.createElement('hr');
409
+ for (const button of [fitWidthBtn, fitHeightBtn, singlePageBtn, spreadOddBtn, spreadEvenBtn, themeBtn]) {
410
+ button.type = 'button';
411
+ button.setAttribute('role', 'menuitemcheckbox');
412
+ button.setAttribute('aria-checked', 'false');
413
+ }
414
+ viewMenu.append(fitWidthBtn, fitHeightBtn, separator, singlePageBtn, spreadOddBtn, spreadEvenBtn, separator2, themeBtn);
415
+ viewMenuWrap.append(viewMenuBtn, viewMenu);
416
+ zoomBar.append(zoomOutBtn, zoomLevel, zoomInBtn, viewMenuWrap);
417
+ const closeViewMenu = () => {
418
+ viewMenu.classList.remove('is-open');
419
+ viewMenuBtn.setAttribute('aria-expanded', 'false');
420
+ };
421
+ viewMenuBtn.addEventListener('click', () => {
422
+ const isOpen = viewMenu.classList.toggle('is-open');
423
+ viewMenuBtn.setAttribute('aria-expanded', String(isOpen));
424
+ });
425
+ viewMenu.addEventListener('keydown', (event) => {
426
+ if (event.key === 'Escape') {
427
+ closeViewMenu();
428
+ viewMenuBtn.focus();
429
+ }
430
+ });
431
+ function fitPage(axis) {
432
+ const current = records.find((record) => record.pageNumber === currentPage) ?? records[0];
433
+ if (!current)
434
+ return;
435
+ const viewport = current.page.getViewport({ scale: 1 });
436
+ const available = axis === 'width'
437
+ ? pages.clientWidth - 40
438
+ : pages.clientHeight - 40;
439
+ const dimension = axis === 'width' ? viewport.width : viewport.height;
440
+ if (available > 0 && dimension > 0) {
441
+ controller.dispatch({ type: 'set-zoom', zoom: available * 100 / dimension });
442
+ }
443
+ closeViewMenu();
444
+ }
445
+ function setPageLayout(next) {
446
+ pageLayout = next;
447
+ singlePageBtn.setAttribute('aria-checked', String(next === 'single'));
448
+ spreadOddBtn.setAttribute('aria-checked', String(next === 'spread-odd'));
449
+ spreadEvenBtn.setAttribute('aria-checked', String(next === 'spread-even'));
450
+ pages.classList.toggle('omni-pdf__pages--spread', next !== 'single');
451
+ void rebuildLayout();
452
+ closeViewMenu();
453
+ }
454
+ fitWidthBtn.addEventListener('click', () => fitPage('width'));
455
+ fitHeightBtn.addEventListener('click', () => fitPage('height'));
456
+ singlePageBtn.addEventListener('click', () => setPageLayout('single'));
457
+ spreadOddBtn.addEventListener('click', () => setPageLayout('spread-odd'));
458
+ spreadEvenBtn.addEventListener('click', () => setPageLayout('spread-even'));
459
+ themeBtn.addEventListener('click', () => {
460
+ matchThemeColors = !matchThemeColors;
461
+ pages.classList.toggle('omni-pdf__pages--theme', matchThemeColors);
462
+ themeBtn.setAttribute('aria-checked', String(matchThemeColors));
463
+ closeViewMenu();
464
+ });
465
+ setPageLayout('single');
466
+ // ------------------------------------------------------ signature modal
467
+ const signatureModal = el('div', 'omni-pdf__modal');
468
+ const signaturePanel = el('div', 'omni-pdf__panel');
469
+ const signatureLabel = el('label', 'omni-pdf__field', t('pdf.signatureColor'));
470
+ const signatureColorInput = el('input');
471
+ signatureColorInput.type = 'color';
472
+ signatureColorInput.value = '#000000';
473
+ signatureColorInput.setAttribute('aria-label', t('pdf.signatureColor'));
474
+ signatureLabel.appendChild(signatureColorInput);
475
+ const signatureCanvas = el('canvas');
476
+ signatureCanvas.width = 480;
477
+ signatureCanvas.height = 180;
478
+ signatureCanvas.className = 'omni-pdf__signature-pad';
479
+ const signatureActions = el('div', 'omni-pdf__panel-actions');
480
+ const clearSignatureBtn = el('button', undefined, t('pdf.signatureClear'));
481
+ clearSignatureBtn.type = 'button';
482
+ const cancelSignatureBtn = el('button', undefined, t('pdf.cancel'));
483
+ cancelSignatureBtn.type = 'button';
484
+ const useSignatureBtn = el('button', undefined, t('pdf.signatureUse'));
485
+ useSignatureBtn.type = 'button';
486
+ signatureActions.append(clearSignatureBtn, cancelSignatureBtn, useSignatureBtn);
487
+ signaturePanel.append(signatureLabel, signatureCanvas, signatureActions);
488
+ signatureModal.appendChild(signaturePanel);
489
+ frame.appendChild(signatureModal);
490
+ const signatureContext = signatureCanvas.getContext('2d');
491
+ let signing = false;
492
+ let pendingSignaturePosition;
493
+ const clearPad = () => {
494
+ signatureContext?.clearRect(0, 0, signatureCanvas.width, signatureCanvas.height);
495
+ };
496
+ const padPoint = (event) => {
497
+ const box = signatureCanvas.getBoundingClientRect();
498
+ return {
499
+ x: ((event.clientX - box.left) * signatureCanvas.width) / (box.width || 1),
500
+ y: ((event.clientY - box.top) * signatureCanvas.height) / (box.height || 1)
501
+ };
502
+ };
503
+ signatureCanvas.addEventListener('pointerdown', (event) => {
504
+ if (!signatureContext)
505
+ return;
506
+ signing = true;
507
+ signatureCanvas.setPointerCapture?.(event.pointerId);
508
+ const p = padPoint(event);
509
+ signatureContext.beginPath();
510
+ signatureContext.moveTo(p.x, p.y);
511
+ });
512
+ signatureCanvas.addEventListener('pointermove', (event) => {
513
+ if (!signing || !signatureContext)
514
+ return;
515
+ const p = padPoint(event);
516
+ signatureContext.lineTo(p.x, p.y);
517
+ signatureContext.strokeStyle = signatureColorInput.value;
518
+ signatureContext.lineWidth = 2.5;
519
+ signatureContext.lineCap = 'round';
520
+ signatureContext.stroke();
521
+ });
522
+ signatureCanvas.addEventListener('pointerup', () => {
523
+ signing = false;
524
+ });
525
+ clearSignatureBtn.addEventListener('click', clearPad);
526
+ cancelSignatureBtn.addEventListener('click', () => {
527
+ signatureModal.classList.remove('is-open');
528
+ tool = 'view';
529
+ syncToolButtons();
530
+ });
531
+ useSignatureBtn.addEventListener('click', () => {
532
+ const image = trimmedSignature();
533
+ const position = pendingSignaturePosition;
534
+ if (!image || !position)
535
+ return;
536
+ pendingSignaturePosition = undefined;
537
+ signatureModal.classList.remove('is-open');
538
+ tool = 'view';
539
+ syncToolButtons();
540
+ controller.dispatch({
541
+ type: 'add-annotation',
542
+ annotation: {
543
+ kind: 'signature',
544
+ page: position.page,
545
+ x: position.x,
546
+ y: position.y,
547
+ width: 120,
548
+ height: 60,
549
+ dataUrl: image
550
+ }
551
+ });
552
+ });
553
+ function trimmedSignature() {
554
+ const pixels = signatureContext?.getImageData(0, 0, signatureCanvas.width, signatureCanvas.height);
555
+ if (!pixels)
556
+ return undefined;
557
+ let minX = signatureCanvas.width;
558
+ let minY = signatureCanvas.height;
559
+ let maxX = -1;
560
+ let maxY = -1;
561
+ for (let y = 0; y < signatureCanvas.height; y++) {
562
+ for (let x = 0; x < signatureCanvas.width; x++) {
563
+ if ((pixels.data[(y * signatureCanvas.width + x) * 4 + 3] ?? 0) > 0) {
564
+ minX = Math.min(minX, x);
565
+ minY = Math.min(minY, y);
566
+ maxX = Math.max(maxX, x);
567
+ maxY = Math.max(maxY, y);
568
+ }
569
+ }
570
+ }
571
+ if (maxX < minX)
572
+ return undefined;
573
+ const pad = 8;
574
+ const crop = document.createElement('canvas');
575
+ crop.width = maxX - minX + 1 + pad * 2;
576
+ crop.height = maxY - minY + 1 + pad * 2;
577
+ crop.getContext('2d')?.drawImage(signatureCanvas, minX, minY, maxX - minX + 1, maxY - minY + 1, pad, pad, maxX - minX + 1, maxY - minY + 1);
578
+ return crop.toDataURL('image/png');
579
+ }
580
+ /** Rasterize user-added text for portable PDF flattening. The sidecar
581
+ * keeps the original string, size and color, so Omni can still edit it. */
582
+ function rasterizeText(text, size, color) {
583
+ const padding = 2;
584
+ const scale = Math.max(2, globalThis.devicePixelRatio || 1);
585
+ const measure = document.createElement('canvas').getContext('2d');
586
+ if (!measure) {
587
+ return { rasterDataUrl: '', rasterWidth: 0, rasterHeight: 0 };
588
+ }
589
+ measure.font = `${size}px sans-serif`;
590
+ const width = Math.ceil(measure.measureText(text).width + padding * 2);
591
+ const height = Math.ceil(size * 1.3 + padding * 2);
592
+ const canvas = document.createElement('canvas');
593
+ canvas.width = Math.ceil(width * scale);
594
+ canvas.height = Math.ceil(height * scale);
595
+ const context = canvas.getContext('2d');
596
+ if (!context)
597
+ return { rasterDataUrl: '', rasterWidth: 0, rasterHeight: 0 };
598
+ context.scale(scale, scale);
599
+ context.font = `${size}px sans-serif`;
600
+ context.textBaseline = 'top';
601
+ context.fillStyle = color;
602
+ context.fillText(text, padding, padding);
603
+ return { rasterDataUrl: canvas.toDataURL('image/png'), rasterWidth: width, rasterHeight: height };
604
+ }
605
+ // -------------------------------------------------------- text overlay
606
+ /** Inline text-annotation controls (window.prompt is blocked in webviews). */
607
+ function openTextInput(wrapper, pageNumber, x, y) {
608
+ const scale = controller.state.zoom / 100;
609
+ const editor = el('div', 'omni-pdf__text-editor');
610
+ const inputEl = el('input', 'omni-pdf__text-input');
611
+ inputEl.type = 'text';
612
+ inputEl.placeholder = t('pdf.annotationText');
613
+ inputEl.setAttribute('aria-label', t('pdf.annotationText'));
614
+ const sizeInput = el('input');
615
+ sizeInput.type = 'number';
616
+ sizeInput.min = '8';
617
+ sizeInput.max = '96';
618
+ sizeInput.step = '1';
619
+ sizeInput.value = '16';
620
+ sizeInput.className = 'omni-pdf__text-size-input';
621
+ sizeInput.setAttribute('aria-label', t('pdf.annotationSize'));
622
+ const colorInput = el('input');
623
+ colorInput.type = 'color';
624
+ colorInput.value = '#000000';
625
+ colorInput.className = 'omni-pdf__color-input';
626
+ colorInput.setAttribute('aria-label', t('pdf.annotationColor'));
627
+ editor.style.left = `${x * scale}px`;
628
+ editor.style.top = `${y * scale}px`;
629
+ editor.append(inputEl, sizeInput, colorInput);
630
+ let done = false;
631
+ const finish = (commit) => {
632
+ if (done)
633
+ return;
634
+ done = true;
635
+ const text = inputEl.value.trim();
636
+ editor.remove();
637
+ tool = 'view';
638
+ syncToolButtons();
639
+ if (commit && text) {
640
+ const size = Math.min(96, Math.max(8, Number(sizeInput.value) || 16));
641
+ const raster = rasterizeText(text, size, colorInput.value);
642
+ controller.dispatch({
643
+ type: 'add-annotation',
644
+ annotation: { kind: 'text', page: pageNumber, x, y, text, size, color: colorInput.value, ...raster }
645
+ });
646
+ }
647
+ };
648
+ inputEl.addEventListener('keydown', (e) => {
649
+ e.stopPropagation();
650
+ if (e.key === 'Enter')
651
+ finish(true);
652
+ if (e.key === 'Escape')
653
+ finish(false);
654
+ });
655
+ inputEl.addEventListener('blur', (event) => {
656
+ const next = event.relatedTarget;
657
+ if (next instanceof Node && editor.contains(next))
658
+ return;
659
+ finish(true);
660
+ });
661
+ wrapper.appendChild(editor);
662
+ inputEl.focus();
663
+ }
664
+ // ------------------------------------------------------- highlight tool
665
+ /** In shadow isolation the text lives in the shadow root, so prefer its
666
+ * selection API (Chromium) and fall back to the document selection.
667
+ * Chromium may return an empty Selection object for one of these APIs,
668
+ * so do not let that mask the other API's real range. */
669
+ function activeSelection() {
670
+ const shadow = root;
671
+ const shadowSelection = shadow.getSelection?.();
672
+ const documentSelection = document.getSelection();
673
+ return [shadowSelection, documentSelection].find((selection) => !!selection && !selection.isCollapsed && selection.rangeCount > 0) ?? null;
674
+ }
675
+ /** Turn the current selection into a portable text markup annotation. */
676
+ function captureMarkup(kind) {
677
+ const color = kind === 'highlight'
678
+ ? highlightColor
679
+ : kind === 'underline' ? underlineColor : strikeoutColor;
680
+ const selection = activeSelection();
681
+ if (!selection || selection.isCollapsed || selection.rangeCount === 0) {
682
+ ctx.logger.log('info', '[pdf highlight] no active text selection');
683
+ return false;
684
+ }
685
+ ctx.logger.log('info', `[pdf ${kind}] selection ranges=${selection.rangeCount} textLength=${selection.toString().length}`);
686
+ const scale = controller.state.zoom / 100;
687
+ const byPage = new Map();
688
+ for (let i = 0; i < selection.rangeCount; i++) {
689
+ for (const rect of Array.from(selection.getRangeAt(i).getClientRects())) {
690
+ if (rect.width < 1 || rect.height < 1)
691
+ continue;
692
+ const cx = rect.left + rect.width / 2;
693
+ const cy = rect.top + rect.height / 2;
694
+ const record = records.find((r) => {
695
+ const b = r.wrapper.getBoundingClientRect();
696
+ return cx >= b.left && cx <= b.right && cy >= b.top && cy <= b.bottom;
697
+ });
698
+ if (!record)
699
+ continue;
700
+ const b = record.wrapper.getBoundingClientRect();
701
+ const list = byPage.get(record.pageNumber) ?? [];
702
+ list.push({
703
+ x: (rect.left - b.left) / scale,
704
+ y: (rect.top - b.top) / scale,
705
+ width: rect.width / scale,
706
+ height: rect.height / scale
707
+ });
708
+ byPage.set(record.pageNumber, list);
709
+ }
710
+ }
711
+ if (byPage.size === 0) {
712
+ ctx.logger.log('info', '[pdf highlight] selection has no PDF page rectangles');
713
+ return false;
714
+ }
715
+ for (const [page, rects] of byPage) {
716
+ controller.dispatch({
717
+ type: 'add-annotation',
718
+ annotation: { kind, page, rects: mergeHighlightRects(rects), color }
719
+ });
720
+ }
721
+ selection.removeAllRanges();
722
+ ctx.logger.log('info', `[pdf ${kind}] added pages=${byPage.size}`);
723
+ return true;
724
+ }
725
+ function captureHighlight() {
726
+ return captureMarkup('highlight');
727
+ }
728
+ // ------------------------------------------------------- annotations UI
729
+ function renderOverlays(record) {
730
+ for (const node of record.wrapper.querySelectorAll('.omni-pdf__annotation')) {
731
+ node.remove();
732
+ }
733
+ const scale = controller.state.zoom / 100;
734
+ for (const annotation of controller.state.annotations) {
735
+ if (annotation.page !== record.pageNumber)
736
+ continue;
737
+ if (annotation.kind === 'highlight' || annotation.kind === 'underline' || annotation.kind === 'strikeout') {
738
+ const rects = mergeHighlightRects(annotation.rects);
739
+ const deleteRect = rects.reduce((best, rect) => rect.y < best.y || (rect.y === best.y && rect.x + rect.width > best.x + best.width)
740
+ ? rect
741
+ : best);
742
+ const setGroupHover = (hovered) => {
743
+ for (const node of record.wrapper.querySelectorAll('.omni-pdf__markup')) {
744
+ if (node.dataset.annotationId === annotation.id) {
745
+ node.classList.toggle('is-hovered', hovered);
746
+ }
747
+ }
748
+ };
749
+ for (const rect of rects) {
750
+ const box = el('div', `omni-pdf__annotation omni-pdf__markup omni-pdf__${annotation.kind}`);
751
+ box.dataset.annotationId = annotation.id;
752
+ box.tabIndex = 0;
753
+ box.setAttribute('role', 'button');
754
+ box.setAttribute('aria-label', t(annotation.kind === 'highlight'
755
+ ? 'pdf.highlight'
756
+ : annotation.kind === 'underline' ? 'pdf.underline' : 'pdf.strikeout'));
757
+ if (controller.state.selectedAnnotationId === annotation.id) {
758
+ box.classList.add('is-selected');
759
+ }
760
+ box.style.left = `${rect.x * scale}px`;
761
+ box.style.top = `${rect.y * scale}px`;
762
+ box.style.width = `${rect.width * scale}px`;
763
+ box.style.height = `${rect.height * scale}px`;
764
+ box.style.setProperty('--omni-hl-color', annotation.color);
765
+ box.title = t(annotation.kind === 'highlight'
766
+ ? 'pdf.highlight'
767
+ : annotation.kind === 'underline' ? 'pdf.underline' : 'pdf.strikeout');
768
+ box.addEventListener('click', (event) => {
769
+ event.preventDefault();
770
+ event.stopPropagation();
771
+ controller.dispatch({ type: 'select-annotation', id: annotation.id });
772
+ });
773
+ box.addEventListener('pointerenter', () => setGroupHover(true));
774
+ box.addEventListener('pointerleave', (event) => {
775
+ const next = event.relatedTarget;
776
+ const nextHighlight = next instanceof Element
777
+ ? next.closest('.omni-pdf__markup')
778
+ : null;
779
+ if (nextHighlight?.dataset.annotationId !== annotation.id) {
780
+ setGroupHover(false);
781
+ }
782
+ });
783
+ if (rect === deleteRect) {
784
+ const deleteButton = el('button', 'omni-pdf__annotation-delete', '×');
785
+ deleteButton.type = 'button';
786
+ deleteButton.setAttribute('aria-label', t('pdf.deleteAnnotation'));
787
+ deleteButton.addEventListener('pointerdown', (event) => {
788
+ event.preventDefault();
789
+ event.stopPropagation();
790
+ });
791
+ deleteButton.addEventListener('click', (event) => {
792
+ event.preventDefault();
793
+ event.stopPropagation();
794
+ controller.dispatch({ type: 'remove-annotation', id: annotation.id });
795
+ });
796
+ box.appendChild(deleteButton);
797
+ }
798
+ record.wrapper.appendChild(box);
799
+ }
800
+ continue;
801
+ }
802
+ const overlay = el('div', 'omni-pdf__annotation');
803
+ overlay.tabIndex = 0;
804
+ overlay.setAttribute('role', 'button');
805
+ overlay.setAttribute('aria-label', annotation.kind === 'text' ? annotation.text : t('pdf.signature'));
806
+ if (controller.state.selectedAnnotationId === annotation.id) {
807
+ overlay.classList.add('is-selected');
808
+ }
809
+ overlay.style.left = `${annotation.x * scale}px`;
810
+ overlay.style.top = `${annotation.y * scale}px`;
811
+ if (annotation.kind === 'text') {
812
+ overlay.textContent = annotation.text;
813
+ overlay.style.fontSize = `${annotation.size * scale}px`;
814
+ overlay.style.color = annotation.color;
815
+ }
816
+ else {
817
+ const image = el('img');
818
+ image.src = annotation.dataUrl;
819
+ image.alt = t('pdf.signature');
820
+ image.style.width = `${annotation.width * scale}px`;
821
+ image.style.height = `${annotation.height * scale}px`;
822
+ overlay.appendChild(image);
823
+ }
824
+ let dragStart;
825
+ overlay.addEventListener('pointerdown', (event) => {
826
+ if (event.target.closest('.omni-pdf__annotation-delete'))
827
+ return;
828
+ event.preventDefault();
829
+ event.stopPropagation();
830
+ dragStart = {
831
+ x: annotation.x,
832
+ y: annotation.y,
833
+ clientX: event.clientX,
834
+ clientY: event.clientY
835
+ };
836
+ overlay.classList.add('is-selected');
837
+ overlay.setPointerCapture?.(event.pointerId);
838
+ });
839
+ overlay.addEventListener('pointermove', (event) => {
840
+ if (!dragStart)
841
+ return;
842
+ const x = dragStart.x + (event.clientX - dragStart.clientX) / scale;
843
+ const y = dragStart.y + (event.clientY - dragStart.clientY) / scale;
844
+ overlay.style.left = `${x * scale}px`;
845
+ overlay.style.top = `${y * scale}px`;
846
+ });
847
+ overlay.addEventListener('pointerup', (event) => {
848
+ if (!dragStart)
849
+ return;
850
+ const x = dragStart.x + (event.clientX - dragStart.clientX) / scale;
851
+ const y = dragStart.y + (event.clientY - dragStart.clientY) / scale;
852
+ dragStart = undefined;
853
+ overlay.focus({ preventScroll: true });
854
+ controller.dispatch({ type: 'move-annotation', id: annotation.id, x, y });
855
+ controller.dispatch({ type: 'select-annotation', id: annotation.id });
856
+ });
857
+ const deleteButton = el('button', 'omni-pdf__annotation-delete', '×');
858
+ deleteButton.type = 'button';
859
+ deleteButton.setAttribute('aria-label', t('pdf.deleteAnnotation'));
860
+ deleteButton.addEventListener('pointerdown', (event) => {
861
+ event.preventDefault();
862
+ event.stopPropagation();
863
+ });
864
+ deleteButton.addEventListener('click', (event) => {
865
+ event.preventDefault();
866
+ event.stopPropagation();
867
+ controller.dispatch({ type: 'remove-annotation', id: annotation.id });
868
+ });
869
+ overlay.appendChild(deleteButton);
870
+ record.wrapper.appendChild(overlay);
871
+ }
872
+ }
873
+ // -------------------------------------------------------------- render
874
+ async function renderPage(record, epoch) {
875
+ if (disposed || epoch !== renderEpoch || record.rendered || record.rendering)
876
+ return;
877
+ record.rendering = true;
878
+ const viewport = record.page.getViewport({ scale: controller.state.zoom / 100 });
879
+ const canvas = document.createElement('canvas');
880
+ canvas.width = Math.floor(viewport.width);
881
+ canvas.height = Math.floor(viewport.height);
882
+ canvas.setAttribute('role', 'img');
883
+ canvas.setAttribute('aria-label', t('common.page', { page: record.pageNumber, pages: controller.state.pageOrder.length }));
884
+ const canvasContext = canvas.getContext('2d');
885
+ if (!canvasContext) {
886
+ record.rendering = false;
887
+ return;
888
+ }
889
+ record.wrapper.replaceChildren(canvas);
890
+ const task = record.page.render({ canvasContext, viewport });
891
+ record.task = task;
892
+ try {
893
+ await task.promise;
894
+ if (!disposed && epoch === renderEpoch) {
895
+ record.rendered = true;
896
+ await renderTextLayer(record, viewport, epoch);
897
+ renderOverlays(record);
898
+ }
899
+ }
900
+ catch {
901
+ // Cancellation during zoom / reload is expected.
902
+ }
903
+ finally {
904
+ record.rendering = false;
905
+ record.task = undefined;
906
+ }
907
+ }
908
+ /** Overlay pdf.js's selectable text layer on the rendered canvas so users
909
+ * can select/copy text and (later) highlight it. */
910
+ async function renderTextLayer(record, viewport, epoch) {
911
+ try {
912
+ const textContentSource = await record.page.getTextContent();
913
+ if (disposed || epoch !== renderEpoch)
914
+ return;
915
+ // Keep pdf.js's conventional class as well as our scoped class:
916
+ // integrations may provide the upstream text-layer rules, while
917
+ // the scoped rules below cover shadow-root rendering.
918
+ const container = el('div', 'omni-pdf__text-layer textLayer');
919
+ container.style.setProperty('--scale-factor', String(controller.state.zoom / 100));
920
+ container.style.width = `${Math.floor(viewport.width)}px`;
921
+ container.style.height = `${Math.floor(viewport.height)}px`;
922
+ const textLayer = new pdfjs.TextLayer({ textContentSource, container, viewport });
923
+ record.textLayer?.cancel();
924
+ record.textLayer = textLayer;
925
+ await textLayer.render();
926
+ if (disposed || epoch !== renderEpoch) {
927
+ textLayer.cancel();
928
+ return;
929
+ }
930
+ // Insert under annotation overlays (which renderOverlays appends next).
931
+ record.wrapper.appendChild(container);
932
+ ctx.logger.log('info', `[pdf text-layer] page=${record.pageNumber} rendered children=${container.childElementCount}`);
933
+ }
934
+ catch (error) {
935
+ // Cancellation during zoom/reload is normal, but log other cases
936
+ // so a host integration can diagnose missing selectable text.
937
+ if (!disposed && epoch === renderEpoch) {
938
+ ctx.logger.log('info', `[pdf text-layer] page=${record.pageNumber} failed: ${String(error)}`);
939
+ }
940
+ }
941
+ }
942
+ function setCurrentPage(pageNumber) {
943
+ currentPage = pageNumber;
944
+ pageInput.value = String(currentPage);
945
+ pageTotal.textContent = `/ ${records.length}`;
946
+ records.forEach((record, index) => {
947
+ thumbs.children[index]?.toggleAttribute('aria-current', record.pageNumber === currentPage);
948
+ });
949
+ }
950
+ function goToPageInput() {
951
+ const pageNumber = Number(pageInput.value);
952
+ const record = records.find((item) => item.pageNumber === pageNumber);
953
+ if (!record) {
954
+ pageInput.value = String(currentPage);
955
+ return;
956
+ }
957
+ scrollToPage(record);
958
+ setCurrentPage(pageNumber);
959
+ }
960
+ pageInput.addEventListener('keydown', (event) => {
961
+ if (event.key !== 'Enter')
962
+ return;
963
+ event.preventDefault();
964
+ goToPageInput();
965
+ pageInput.blur();
966
+ });
967
+ pageInput.addEventListener('change', goToPageInput);
968
+ function scrollToPage(record) {
969
+ const before = pages.scrollTop;
970
+ const viewport = pages.getBoundingClientRect();
971
+ const page = record.wrapper.getBoundingClientRect();
972
+ ctx.logger.log('info', `[pdf navigation] request page=${record.pageNumber} scrollTop=${before} ` +
973
+ `viewport=${viewport.top},${viewport.height} page=${page.top},${page.height}`);
974
+ // The page panel has its own fixed-height scrollport, so a single
975
+ // coordinate-based movement is enough. Combining this with
976
+ // scrollIntoView applied the same offset twice.
977
+ pages.scrollTop = Math.max(0, pages.scrollTop + page.top - viewport.top);
978
+ requestAnimationFrame(() => {
979
+ ctx.logger.log('info', `[pdf navigation] result page=${record.pageNumber} scrollTop=${pages.scrollTop} ` +
980
+ `delta=${pages.scrollTop - before}`);
981
+ });
982
+ }
983
+ function syncCurrentPageFromViewport() {
984
+ const viewport = pages.getBoundingClientRect();
985
+ const visible = records.filter((record) => {
986
+ const box = record.wrapper.getBoundingClientRect();
987
+ return box.bottom > viewport.top && box.top < viewport.bottom;
988
+ });
989
+ if (visible.length === 0)
990
+ return;
991
+ const current = visible.find((record) => {
992
+ const box = record.wrapper.getBoundingClientRect();
993
+ return box.top <= viewport.top && box.bottom > viewport.top;
994
+ }) ?? visible.sort((a, b) => {
995
+ const aDistance = Math.abs(a.wrapper.getBoundingClientRect().top - viewport.top);
996
+ const bDistance = Math.abs(b.wrapper.getBoundingClientRect().top - viewport.top);
997
+ return aDistance - bDistance;
998
+ })[0];
999
+ if (current)
1000
+ setCurrentPage(current.pageNumber);
1001
+ }
1002
+ pages.addEventListener('scroll', syncCurrentPageFromViewport, { passive: true });
1003
+ async function rebuildLayout() {
1004
+ if (!doc || disposed)
1005
+ return;
1006
+ // Preserve the source page the reader is actually viewing. `currentPage`
1007
+ // alone is not sufficient here: the lazy-render observer deliberately
1008
+ // observes pages outside the viewport as well.
1009
+ const previousViewport = pages.getBoundingClientRect();
1010
+ const visibleBeforeRebuild = records
1011
+ .filter((record) => {
1012
+ const box = record.wrapper.getBoundingClientRect();
1013
+ return box.bottom > previousViewport.top && box.top < previousViewport.bottom;
1014
+ })
1015
+ .sort((a, b) => {
1016
+ const aDistance = Math.abs(a.wrapper.getBoundingClientRect().top - previousViewport.top);
1017
+ const bDistance = Math.abs(b.wrapper.getBoundingClientRect().top - previousViewport.top);
1018
+ return aDistance - bDistance;
1019
+ })[0];
1020
+ const pageToRestore = visibleBeforeRebuild?.pageNumber ?? currentPage;
1021
+ // Retain where the reader was within the page, rather than only its
1022
+ // page number. This keeps zoom from jumping to a page boundary.
1023
+ const pageOffsetRatio = visibleBeforeRebuild
1024
+ ? Math.max(0, Math.min(1, (previousViewport.top - visibleBeforeRebuild.wrapper.getBoundingClientRect().top) / Math.max(1, visibleBeforeRebuild.wrapper.getBoundingClientRect().height)))
1025
+ : 0;
1026
+ const epoch = ++renderEpoch;
1027
+ pageObserver?.disconnect();
1028
+ thumbObserver?.disconnect();
1029
+ for (const record of records) {
1030
+ record.task?.cancel();
1031
+ record.textLayer?.cancel();
1032
+ }
1033
+ records = [];
1034
+ pages.replaceChildren();
1035
+ thumbs.replaceChildren();
1036
+ const scale = controller.state.zoom / 100;
1037
+ const pageTotal = controller.state.pageOrder.length;
1038
+ const thumbRenderers = [];
1039
+ for (const pageNumber of controller.state.pageOrder) {
1040
+ const page = await doc.getPage(pageNumber);
1041
+ if (disposed || epoch !== renderEpoch)
1042
+ return;
1043
+ const viewport = page.getViewport({ scale });
1044
+ const wrapper = el('div', 'omni-pdf__page');
1045
+ wrapper.dataset.pageNumber = String(pageNumber);
1046
+ wrapper.style.width = `${viewport.width}px`;
1047
+ wrapper.style.height = `${viewport.height}px`;
1048
+ wrapper.appendChild(el('div', 'omni-pdf__placeholder', t('common.page', { page: pageNumber, pages: pageTotal })));
1049
+ wrapper.addEventListener('click', (event) => {
1050
+ const target = event.target;
1051
+ if (target instanceof Element && target.closest('.omni-pdf__annotation'))
1052
+ return;
1053
+ if (tool !== 'text' && tool !== 'signature') {
1054
+ if (controller.state.selectedAnnotationId) {
1055
+ controller.dispatch({ type: 'select-annotation', id: null });
1056
+ }
1057
+ return;
1058
+ }
1059
+ if (target !== wrapper && !(target instanceof HTMLCanvasElement))
1060
+ return;
1061
+ const bounds = wrapper.getBoundingClientRect();
1062
+ const currentScale = controller.state.zoom / 100;
1063
+ const x = (event.clientX - bounds.left) / currentScale;
1064
+ const y = (event.clientY - bounds.top) / currentScale;
1065
+ if (tool === 'text') {
1066
+ openTextInput(wrapper, pageNumber, x, y);
1067
+ }
1068
+ else {
1069
+ pendingSignaturePosition = { page: pageNumber, x, y };
1070
+ clearPad();
1071
+ signatureModal.classList.add('is-open');
1072
+ }
1073
+ });
1074
+ if (pageLayout === 'spread-even' && records.length === 0) {
1075
+ const blank = el('div', 'omni-pdf__spread-blank');
1076
+ blank.style.width = `${viewport.width}px`;
1077
+ blank.style.height = `${viewport.height}px`;
1078
+ pages.appendChild(blank);
1079
+ }
1080
+ pages.appendChild(wrapper);
1081
+ const record = {
1082
+ pageNumber,
1083
+ page,
1084
+ wrapper,
1085
+ rendered: false,
1086
+ rendering: false,
1087
+ task: undefined,
1088
+ textLayer: undefined
1089
+ };
1090
+ records.push(record);
1091
+ // --- thumbnail (lazy-rendered — docs §5: never all at once) ---
1092
+ const thumb = el('button', 'omni-pdf__thumb');
1093
+ thumb.type = 'button';
1094
+ thumb.setAttribute('aria-label', t('common.page', { page: pageNumber, pages: pageTotal }));
1095
+ const thumbCanvas = el('canvas');
1096
+ const thumbViewport = page.getViewport({
1097
+ scale: Math.min(0.18, 90 / (viewport.width / scale || 1))
1098
+ });
1099
+ thumbCanvas.width = Math.floor(thumbViewport.width);
1100
+ thumbCanvas.height = Math.floor(thumbViewport.height);
1101
+ let thumbRendered = false;
1102
+ const renderThumb = () => {
1103
+ if (thumbRendered || disposed || epoch !== renderEpoch)
1104
+ return;
1105
+ thumbRendered = true;
1106
+ const thumbContext = thumbCanvas.getContext('2d');
1107
+ if (!thumbContext)
1108
+ return;
1109
+ void page
1110
+ .render({ canvasContext: thumbContext, viewport: thumbViewport })
1111
+ .promise.catch(() => undefined);
1112
+ };
1113
+ thumbRenderers.push({ element: thumb, render: renderThumb });
1114
+ const label = el('span', 'omni-pdf__thumb-label', String(pageNumber));
1115
+ thumb.append(thumbCanvas, label);
1116
+ thumb.addEventListener('click', () => {
1117
+ // Keep thumbnail navigation inside the PDF scroll container.
1118
+ scrollToPage(record);
1119
+ setCurrentPage(pageNumber);
1120
+ });
1121
+ thumb.draggable = true;
1122
+ thumb.addEventListener('dragstart', (event) => {
1123
+ event.dataTransfer?.setData('text/plain', String(pageNumber));
1124
+ thumb.classList.add('is-dragging');
1125
+ });
1126
+ thumb.addEventListener('dragend', () => {
1127
+ for (const node of [...thumbs.children]) {
1128
+ node.classList.remove('is-dragging', 'drop-before', 'drop-after');
1129
+ }
1130
+ });
1131
+ thumb.addEventListener('dragover', (event) => {
1132
+ event.preventDefault();
1133
+ for (const node of [...thumbs.children]) {
1134
+ node.classList.remove('drop-before', 'drop-after');
1135
+ }
1136
+ const rect = thumb.getBoundingClientRect();
1137
+ thumb.classList.add(event.clientY < rect.top + rect.height / 2 ? 'drop-before' : 'drop-after');
1138
+ });
1139
+ thumb.addEventListener('dragleave', () => thumb.classList.remove('drop-before', 'drop-after'));
1140
+ thumb.addEventListener('drop', (event) => {
1141
+ event.preventDefault();
1142
+ const order = controller.state.pageOrder;
1143
+ const from = order.indexOf(Number(event.dataTransfer?.getData('text/plain')));
1144
+ let to = order.indexOf(pageNumber);
1145
+ if (thumb.classList.contains('drop-after'))
1146
+ to += 1;
1147
+ if (from >= 0 && to >= 0) {
1148
+ if (from < to)
1149
+ to--;
1150
+ controller.dispatch({
1151
+ type: 'reorder-pages',
1152
+ from,
1153
+ to: Math.min(to, order.length - 1)
1154
+ });
1155
+ }
1156
+ });
1157
+ if (editingAvailable) {
1158
+ const remove = el('button', 'omni-pdf__thumb-remove', '×');
1159
+ remove.type = 'button';
1160
+ remove.setAttribute('aria-label', t('pdf.deletePage'));
1161
+ remove.addEventListener('click', (event) => {
1162
+ event.stopPropagation();
1163
+ controller.dispatch({ type: 'delete-page', page: pageNumber });
1164
+ });
1165
+ thumb.appendChild(remove);
1166
+ }
1167
+ thumbs.appendChild(thumb);
1168
+ }
1169
+ if (typeof IntersectionObserver === 'function') {
1170
+ pageObserver = new IntersectionObserver((entries) => {
1171
+ for (const entry of entries) {
1172
+ if (!entry.isIntersecting)
1173
+ continue;
1174
+ const record = records.find((item) => item.wrapper === entry.target);
1175
+ if (!record)
1176
+ continue;
1177
+ void renderPage(record, epoch);
1178
+ }
1179
+ }, { root: pages, rootMargin: '150% 0px 150% 0px', threshold: 0 });
1180
+ for (const record of records)
1181
+ pageObserver.observe(record.wrapper);
1182
+ thumbObserver = new IntersectionObserver((entries) => {
1183
+ for (const entry of entries) {
1184
+ if (!entry.isIntersecting)
1185
+ continue;
1186
+ thumbRenderers.find((item) => item.element === entry.target)?.render();
1187
+ }
1188
+ }, { root: thumbs, rootMargin: '100% 0px 100% 0px', threshold: 0 });
1189
+ for (const item of thumbRenderers)
1190
+ thumbObserver.observe(item.element);
1191
+ }
1192
+ else {
1193
+ // No observer support: render the current page and nearby thumbs.
1194
+ for (const item of thumbRenderers.slice(0, THUMB_FALLBACK_RENDER_COUNT)) {
1195
+ item.render();
1196
+ }
1197
+ }
1198
+ const current = records.find((record) => record.pageNumber === pageToRestore) ?? records[0];
1199
+ if (current) {
1200
+ setCurrentPage(current.pageNumber);
1201
+ void renderPage(current, epoch);
1202
+ // Do not use scrollIntoView here: it may scroll the host document
1203
+ // as well as the PDF list. Restrict restoration to the PDF list.
1204
+ if (visibleBeforeRebuild) {
1205
+ pages.scrollTop = Math.max(0, current.wrapper.offsetTop + current.wrapper.offsetHeight * pageOffsetRatio);
1206
+ }
1207
+ }
1208
+ syncCurrentPageFromViewport();
1209
+ status.textContent = `${records.length} ${t('pdf.pages')}`;
1210
+ }
1211
+ function refreshChrome() {
1212
+ const state = controller.state;
1213
+ zoomLevel.textContent = `${state.zoom}%`;
1214
+ zoomOutBtn.disabled = state.zoom === PDF_ZOOM_LEVELS[0];
1215
+ zoomInBtn.disabled = state.zoom === PDF_ZOOM_LEVELS[PDF_ZOOM_LEVELS.length - 1];
1216
+ if (editingAvailable && ctx.writeback) {
1217
+ saveBtn.disabled = !(state.dirty || mergedSinceSave);
1218
+ }
1219
+ }
1220
+ /** Full rebuild only when zoom / page structure changed; annotation-only
1221
+ * changes just refresh the overlays (no canvas re-render, no scroll loss). */
1222
+ function onStateChange() {
1223
+ const state = controller.state;
1224
+ const nextKey = JSON.stringify({ zoom: state.zoom, pageOrder: state.pageOrder });
1225
+ refreshChrome();
1226
+ if (nextKey !== layoutKey) {
1227
+ layoutKey = nextKey;
1228
+ void rebuildLayout();
1229
+ return;
1230
+ }
1231
+ for (const record of records) {
1232
+ if (record.rendered)
1233
+ renderOverlays(record);
1234
+ }
1235
+ }
1236
+ function adoptController(next) {
1237
+ unsubscribe?.();
1238
+ controller = next;
1239
+ layoutKey = JSON.stringify({
1240
+ zoom: next.state.zoom,
1241
+ pageOrder: next.state.pageOrder
1242
+ });
1243
+ unsubscribe = next.subscribe(onStateChange);
1244
+ }
1245
+ // ------------------------------------------------------- editing actions
1246
+ async function ensurePdfLib() {
1247
+ if (!deps.loadPdfLib)
1248
+ throw new Error('pdf-lib unavailable');
1249
+ pdfLib ??= await deps.loadPdfLib();
1250
+ return pdfLib;
1251
+ }
1252
+ function stateForSave() {
1253
+ const state = controller.state;
1254
+ return {
1255
+ ...state,
1256
+ annotations: state.annotations.map((annotation) => {
1257
+ if (annotation.kind !== 'text'
1258
+ || (annotation.rasterDataUrl && annotation.rasterWidth && annotation.rasterHeight))
1259
+ return annotation;
1260
+ return { ...annotation, ...rasterizeText(annotation.text, annotation.size, annotation.color) };
1261
+ })
1262
+ };
1263
+ }
1264
+ async function save() {
1265
+ if (!ctx.writeback || saveBtn.disabled)
1266
+ return;
1267
+ try {
1268
+ const lib = await ensurePdfLib();
1269
+ // Flatten from the pristine base (not the on-disk flattened copy),
1270
+ // embedding the sidecar so overlays stay removable on reopen. The
1271
+ // in-memory session keeps `sourceBytes` pristine + the live layer;
1272
+ // we only rebaseline the dirty marker.
1273
+ const data = await buildSavedPdf(lib, sourceBytes, stateForSave());
1274
+ await ctx.writeback.write(data);
1275
+ mergedSinceSave = false;
1276
+ controller.dispatch({ type: 'mark-saved' });
1277
+ refreshChrome();
1278
+ status.textContent = t('common.savedToOriginal');
1279
+ }
1280
+ catch (error) {
1281
+ ctx.logger.log('error', `pdf save failed: ${String(error)}`);
1282
+ status.textContent = t('common.saveFailed');
1283
+ }
1284
+ }
1285
+ async function saveAs() {
1286
+ if (!ctx.save || saveAsBtn.disabled)
1287
+ return;
1288
+ try {
1289
+ const lib = await ensurePdfLib();
1290
+ // Keep the same hybrid sidecar as Save so a Save As document can
1291
+ // be reopened in Omni Viewer with all overlays still editable.
1292
+ // Other PDF readers continue to see the flattened result.
1293
+ const data = await buildSavedPdf(lib, sourceBytes, stateForSave());
1294
+ await ctx.save.saveFile(savedPdfName(input.fileName), data, 'application/pdf');
1295
+ status.textContent = t('common.saved', { name: savedPdfName(input.fileName) });
1296
+ }
1297
+ catch (error) {
1298
+ ctx.logger.log('error', `pdf save-as failed: ${String(error)}`);
1299
+ status.textContent = t('common.saveFailed');
1300
+ }
1301
+ }
1302
+ async function merge() {
1303
+ if (!ctx.filePick || mergeBtn.disabled)
1304
+ return;
1305
+ const picked = await ctx.filePick.pickFile({ accept: ['application/pdf', '.pdf'] });
1306
+ if (!picked || disposed)
1307
+ return;
1308
+ try {
1309
+ status.textContent = t('pdf.merging');
1310
+ const lib = await ensurePdfLib();
1311
+ // `sourceBytes` is the pristine base while the controller holds
1312
+ // the live reorder/delete/annotation layer. Keep that layer and
1313
+ // append the new source pages to its current visible order.
1314
+ const previousState = controller.state;
1315
+ const previousSourcePageCount = doc?.numPages ?? sourceBytes.length;
1316
+ const merged = await mergePdfBytes(lib, sourceBytes, picked.data);
1317
+ await doc?.destroy();
1318
+ doc = await loadDocument(merged);
1319
+ if (disposed) {
1320
+ void doc.destroy();
1321
+ return;
1322
+ }
1323
+ sourceBytes = merged;
1324
+ currentPage = 1;
1325
+ // The merged pages exist only in memory until the user saves.
1326
+ mergedSinceSave = true;
1327
+ const appendedPages = Array.from({ length: Math.max(0, doc.numPages - previousSourcePageCount) }, (_, index) => previousSourcePageCount + index + 1);
1328
+ adoptController(createPdfController(doc.numPages, {
1329
+ pageOrder: [...previousState.pageOrder, ...appendedPages],
1330
+ annotations: previousState.annotations
1331
+ }));
1332
+ refreshChrome();
1333
+ await rebuildLayout();
1334
+ status.textContent = t('pdf.mergeComplete');
1335
+ }
1336
+ catch (error) {
1337
+ ctx.logger.log('error', `pdf merge failed: ${String(error)}`);
1338
+ status.textContent = t('pdf.mergeFailed');
1339
+ }
1340
+ }
1341
+ // ------------------------------------------------------------- open doc
1342
+ let password;
1343
+ for (;;) {
1344
+ try {
1345
+ doc = await loadDocument(sourceBytes, password);
1346
+ break;
1347
+ }
1348
+ catch (error) {
1349
+ throwIfAborted();
1350
+ if (isPasswordError(error)) {
1351
+ const entered = await askPassword(password !== undefined);
1352
+ throwIfAborted();
1353
+ if (entered !== null) {
1354
+ password = entered;
1355
+ continue;
1356
+ }
1357
+ status.textContent = t('pdf.passwordRequired');
1358
+ }
1359
+ else {
1360
+ ctx.logger.log('error', `pdf load failed: ${String(error)}`);
1361
+ status.textContent = t('pdf.loadFailed');
1362
+ }
1363
+ status.classList.add('omni-pdf__status--error');
1364
+ // Stable failed handle: one empty controller, normal dispose.
1365
+ const failedController = createPdfController(0);
1366
+ return {
1367
+ get controller() {
1368
+ return failedController;
1369
+ },
1370
+ isDirty: () => false,
1371
+ dispose: teardown
1372
+ };
1373
+ }
1374
+ }
1375
+ throwIfAborted();
1376
+ // Hybrid rehydration: if this file was saved by us, its visible pages are
1377
+ // flattened but it carries the pristine source + layer JSON. Render/edit
1378
+ // from the pristine base and restore the overlays as a removable layer.
1379
+ const sidecar = await readSidecar(doc);
1380
+ if (sidecar) {
1381
+ await doc.destroy();
1382
+ sourceBytes = sidecar.pristine;
1383
+ doc = await loadDocument(sourceBytes, password);
1384
+ throwIfAborted();
1385
+ adoptController(createPdfController(doc.numPages, sidecar.layer));
1386
+ }
1387
+ else {
1388
+ adoptController(createPdfController(doc.numPages));
1389
+ }
1390
+ refreshChrome();
1391
+ await rebuildLayout();
1392
+ throwIfAborted();
1393
+ return {
1394
+ get controller() {
1395
+ return controller;
1396
+ },
1397
+ isDirty: () => controller.state.dirty || mergedSinceSave,
1398
+ dispose: teardown
1399
+ };
1400
+ }