@gmickel/gno 1.46.0 → 2.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 (235) hide show
  1. package/README.md +17 -5
  2. package/THIRD_PARTY_NOTICES.md +46 -0
  3. package/assets/skill/SKILL.md +7 -6
  4. package/assets/spa-production.json.gz +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v2.1.0.zip +0 -0
  6. package/browser-extension/artifacts/gno-browser-clipper-v2.1.0.zip.sha256 +1 -0
  7. package/browser-extension/dist/chunk-4tc9v0ja.js +74 -0
  8. package/browser-extension/dist/manifest.json +1 -1
  9. package/browser-extension/dist/preview.html +1 -1
  10. package/browser-extension/dist/service-worker.js +32 -33
  11. package/bunfig.toml +2 -0
  12. package/package.json +40 -26
  13. package/spec/cli.md +29 -4
  14. package/spec/db/schema.sql +146 -1
  15. package/spec/mcp.md +26 -0
  16. package/src/app/context-runtime-types.ts +3 -0
  17. package/src/app/context-runtime.ts +2 -0
  18. package/src/cli/commands/ask.ts +6 -1
  19. package/src/cli/commands/daemon.ts +21 -8
  20. package/src/cli/commands/embed.ts +77 -41
  21. package/src/cli/detach.ts +3 -2
  22. package/src/config/types.ts +3 -3
  23. package/src/converters/adapters/markitdownTs/adapter.ts +1 -2
  24. package/src/converters/adapters/officeparser/adapter.ts +1 -2
  25. package/src/converters/versions.ts +6 -8
  26. package/src/core/context-evidence.ts +8 -4
  27. package/src/core/job-manager.ts +95 -13
  28. package/src/core/network-boundary-inventory.ts +10 -0
  29. package/src/core/shutdown-budget.ts +45 -0
  30. package/src/embed/backlog.ts +107 -4
  31. package/src/embed/batch.ts +42 -2
  32. package/src/embed/fingerprint.ts +16 -0
  33. package/src/embed/retry.ts +113 -5
  34. package/src/embed/variant-backlog.ts +105 -0
  35. package/src/embed/variant-plan.ts +62 -0
  36. package/src/embed/variant-retry.ts +113 -0
  37. package/src/ingestion/graph-reconciliation.ts +327 -0
  38. package/src/ingestion/sync.ts +9 -272
  39. package/src/llm/http-inference.ts +6 -0
  40. package/src/llm/httpEmbedding.ts +37 -6
  41. package/src/llm/httpGeneration.ts +18 -3
  42. package/src/llm/httpRerank.ts +23 -5
  43. package/src/llm/inference-cancellation.ts +168 -0
  44. package/src/llm/inference-scope.ts +202 -0
  45. package/src/llm/lazy-ports.ts +115 -0
  46. package/src/llm/native-worker/client.ts +541 -0
  47. package/src/llm/native-worker/dispatcher.ts +228 -0
  48. package/src/llm/native-worker/embedding-identity.ts +33 -0
  49. package/src/llm/native-worker/entry.ts +173 -0
  50. package/src/llm/native-worker/errors.ts +32 -0
  51. package/src/llm/native-worker/evaluation.ts +16 -0
  52. package/src/llm/native-worker/owned-exit.ts +108 -0
  53. package/src/llm/native-worker/owner.ts +141 -0
  54. package/src/llm/native-worker/ports.ts +317 -0
  55. package/src/llm/native-worker/protocol.ts +442 -0
  56. package/src/llm/native-worker/runtime-config.ts +92 -0
  57. package/src/llm/nodeLlamaCpp/adapter.ts +77 -20
  58. package/src/llm/nodeLlamaCpp/embedding.ts +130 -46
  59. package/src/llm/nodeLlamaCpp/generation.ts +34 -5
  60. package/src/llm/nodeLlamaCpp/lifecycle-options.ts +99 -0
  61. package/src/llm/nodeLlamaCpp/lifecycle.ts +209 -204
  62. package/src/llm/nodeLlamaCpp/rerank-capacity.ts +111 -0
  63. package/src/llm/nodeLlamaCpp/rerank.ts +118 -27
  64. package/src/llm/nodeLlamaCpp/simulator-handle.ts +73 -0
  65. package/src/llm/nodeLlamaCpp/simulator-install.ts +124 -0
  66. package/src/llm/nodeLlamaCpp/simulator-session.ts +240 -0
  67. package/src/llm/nodeLlamaCpp/simulator-types.ts +80 -0
  68. package/src/llm/types.ts +35 -5
  69. package/src/mcp/context.ts +27 -0
  70. package/src/mcp/http-transport.ts +12 -10
  71. package/src/mcp/server.ts +3 -0
  72. package/src/mcp/tool-profile.ts +30 -8
  73. package/src/mcp/tools/context.ts +8 -11
  74. package/src/mcp/tools/embed.ts +1 -1
  75. package/src/mcp/tools/index-cmd.ts +1 -1
  76. package/src/mcp/tools/index.ts +10 -8
  77. package/src/mcp/tools/query.ts +14 -30
  78. package/src/mcp/tools/vsearch.ts +1 -1
  79. package/src/pipeline/answer.ts +23 -3
  80. package/src/pipeline/claim-verifier.ts +6 -0
  81. package/src/pipeline/expansion.ts +43 -40
  82. package/src/pipeline/explain.ts +6 -2
  83. package/src/pipeline/filters.ts +63 -0
  84. package/src/pipeline/fusion.ts +29 -9
  85. package/src/pipeline/graph-retrieval.ts +29 -9
  86. package/src/pipeline/hybrid.ts +198 -55
  87. package/src/pipeline/hydration.ts +161 -0
  88. package/src/pipeline/owner-fusion.ts +87 -0
  89. package/src/pipeline/rerank.ts +35 -11
  90. package/src/pipeline/search.ts +13 -2
  91. package/src/pipeline/types.ts +5 -3
  92. package/src/pipeline/vsearch.ts +87 -7
  93. package/src/sdk/client.ts +47 -3
  94. package/src/sdk/embed.ts +63 -39
  95. package/src/serve/background-runtime.ts +1 -1
  96. package/src/serve/context.ts +41 -56
  97. package/src/serve/embed-scheduler.ts +58 -35
  98. package/src/serve/public/components/IndexingProgress.tsx +46 -60
  99. package/src/serve/public/components/PublishExportDialog.tsx +266 -0
  100. package/src/serve/public/globals.built.css +1 -1
  101. package/src/serve/public/globals.css +35 -0
  102. package/src/serve/public/lib/publish-export.ts +81 -1
  103. package/src/serve/public/lib/shiki-language-ids.ts +14 -0
  104. package/src/serve/public/pages/Collections.tsx +12 -46
  105. package/src/serve/public/pages/DocView.tsx +14 -52
  106. package/src/serve/resident-admission.ts +36 -36
  107. package/src/serve/resident-background-work.ts +20 -2
  108. package/src/serve/resident-request.ts +11 -5
  109. package/src/serve/resident-runtime.ts +97 -61
  110. package/src/serve/resident-shutdown.ts +153 -0
  111. package/src/serve/routes/api.ts +3 -1
  112. package/src/serve/server.ts +47 -26
  113. package/src/store/migrations/028-vector-variants.ts +54 -0
  114. package/src/store/migrations/029-graph-reference-state.ts +77 -0
  115. package/src/store/migrations/index.ts +4 -0
  116. package/src/store/sqlite/adapter.ts +251 -183
  117. package/src/store/sqlite/eligibility.ts +174 -0
  118. package/src/store/sqlite/graph-edge-application.ts +66 -0
  119. package/src/store/sqlite/graph-reference-state.ts +194 -0
  120. package/src/store/sqlite/legacy-vector-ownership.ts +79 -0
  121. package/src/store/types.ts +80 -12
  122. package/src/store/vector/eligibility.ts +36 -0
  123. package/src/store/vector/freshness.ts +33 -6
  124. package/src/store/vector/lazy.ts +81 -0
  125. package/src/store/vector/sqlite-vec.ts +106 -54
  126. package/src/store/vector/stats.ts +14 -3
  127. package/src/store/vector/types.ts +35 -2
  128. package/src/store/vector/variant-search.ts +192 -0
  129. package/src/store/vector/variants.ts +451 -0
  130. package/vendor/converters/markitdown-ts/LICENSE +21 -0
  131. package/vendor/converters/markitdown-ts/dist/index.cjs +1180 -0
  132. package/vendor/converters/markitdown-ts/dist/index.d.cts +46 -0
  133. package/vendor/converters/markitdown-ts/dist/index.d.mts +46 -0
  134. package/vendor/converters/markitdown-ts/dist/index.d.ts +46 -0
  135. package/vendor/converters/markitdown-ts/dist/index.mjs +1152 -0
  136. package/vendor/converters/markitdown-ts/package.json +77 -0
  137. package/vendor/converters/officeparser/LICENSE +21 -0
  138. package/vendor/converters/officeparser/dist/OfficeConverter.d.ts +47 -0
  139. package/vendor/converters/officeparser/dist/OfficeConverter.js +76 -0
  140. package/vendor/converters/officeparser/dist/OfficeGenerator.d.ts +23 -0
  141. package/vendor/converters/officeparser/dist/OfficeGenerator.js +73 -0
  142. package/vendor/converters/officeparser/dist/OfficeParser.d.ts +106 -0
  143. package/vendor/converters/officeparser/dist/OfficeParser.js +332 -0
  144. package/vendor/converters/officeparser/dist/cli.d.ts +28 -0
  145. package/vendor/converters/officeparser/dist/cli.js +381 -0
  146. package/vendor/converters/officeparser/dist/defaults.d.ts +41 -0
  147. package/vendor/converters/officeparser/dist/defaults.js +218 -0
  148. package/vendor/converters/officeparser/dist/generators/BaseGenerator.d.ts +107 -0
  149. package/vendor/converters/officeparser/dist/generators/BaseGenerator.js +248 -0
  150. package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.d.ts +82 -0
  151. package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.js +797 -0
  152. package/vendor/converters/officeparser/dist/generators/CsvGenerator.d.ts +38 -0
  153. package/vendor/converters/officeparser/dist/generators/CsvGenerator.js +245 -0
  154. package/vendor/converters/officeparser/dist/generators/EpubGenerator.d.ts +43 -0
  155. package/vendor/converters/officeparser/dist/generators/EpubGenerator.js +315 -0
  156. package/vendor/converters/officeparser/dist/generators/HtmlGenerator.d.ts +59 -0
  157. package/vendor/converters/officeparser/dist/generators/HtmlGenerator.js +1942 -0
  158. package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.d.ts +96 -0
  159. package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.js +1175 -0
  160. package/vendor/converters/officeparser/dist/generators/PdfGenerator.d.ts +22 -0
  161. package/vendor/converters/officeparser/dist/generators/PdfGenerator.js +194 -0
  162. package/vendor/converters/officeparser/dist/generators/RtfGenerator.d.ts +29 -0
  163. package/vendor/converters/officeparser/dist/generators/RtfGenerator.js +316 -0
  164. package/vendor/converters/officeparser/dist/generators/TextGenerator.d.ts +13 -0
  165. package/vendor/converters/officeparser/dist/generators/TextGenerator.js +201 -0
  166. package/vendor/converters/officeparser/dist/index.d.ts +60 -0
  167. package/vendor/converters/officeparser/dist/index.js +72 -0
  168. package/vendor/converters/officeparser/dist/index.mjs +18 -0
  169. package/vendor/converters/officeparser/dist/officeparser.browser.d.ts +2621 -0
  170. package/vendor/converters/officeparser/dist/officeparser.browser.iife.js +1336 -0
  171. package/vendor/converters/officeparser/dist/officeparser.browser.mjs +1335 -0
  172. package/vendor/converters/officeparser/dist/officeparser.browser.slim.d.ts +2621 -0
  173. package/vendor/converters/officeparser/dist/officeparser.browser.slim.iife.js +1336 -0
  174. package/vendor/converters/officeparser/dist/officeparser.browser.slim.mjs +1335 -0
  175. package/vendor/converters/officeparser/dist/parsers/CsvParser.d.ts +9 -0
  176. package/vendor/converters/officeparser/dist/parsers/CsvParser.js +115 -0
  177. package/vendor/converters/officeparser/dist/parsers/EpubParser.d.ts +8 -0
  178. package/vendor/converters/officeparser/dist/parsers/EpubParser.js +217 -0
  179. package/vendor/converters/officeparser/dist/parsers/ExcelParser.d.ts +32 -0
  180. package/vendor/converters/officeparser/dist/parsers/ExcelParser.js +736 -0
  181. package/vendor/converters/officeparser/dist/parsers/HtmlParser.d.ts +2 -0
  182. package/vendor/converters/officeparser/dist/parsers/HtmlParser.js +1287 -0
  183. package/vendor/converters/officeparser/dist/parsers/MarkdownParser.d.ts +2 -0
  184. package/vendor/converters/officeparser/dist/parsers/MarkdownParser.js +1272 -0
  185. package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.d.ts +31 -0
  186. package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.js +1819 -0
  187. package/vendor/converters/officeparser/dist/parsers/PdfParser.d.ts +67 -0
  188. package/vendor/converters/officeparser/dist/parsers/PdfParser.js +848 -0
  189. package/vendor/converters/officeparser/dist/parsers/PowerPointParser.d.ts +32 -0
  190. package/vendor/converters/officeparser/dist/parsers/PowerPointParser.js +950 -0
  191. package/vendor/converters/officeparser/dist/parsers/RtfParser.d.ts +187 -0
  192. package/vendor/converters/officeparser/dist/parsers/RtfParser.js +1801 -0
  193. package/vendor/converters/officeparser/dist/parsers/WordParser.d.ts +79 -0
  194. package/vendor/converters/officeparser/dist/parsers/WordParser.js +1177 -0
  195. package/vendor/converters/officeparser/dist/sbom.cdx.json +1763 -0
  196. package/vendor/converters/officeparser/dist/types.d.ts +2507 -0
  197. package/vendor/converters/officeparser/dist/types.js +107 -0
  198. package/vendor/converters/officeparser/dist/utils/astUtils.d.ts +16 -0
  199. package/vendor/converters/officeparser/dist/utils/astUtils.js +33 -0
  200. package/vendor/converters/officeparser/dist/utils/chartUtils.d.ts +6 -0
  201. package/vendor/converters/officeparser/dist/utils/chartUtils.js +257 -0
  202. package/vendor/converters/officeparser/dist/utils/configUtils.d.ts +44 -0
  203. package/vendor/converters/officeparser/dist/utils/configUtils.js +315 -0
  204. package/vendor/converters/officeparser/dist/utils/dateUtils.d.ts +17 -0
  205. package/vendor/converters/officeparser/dist/utils/dateUtils.js +69 -0
  206. package/vendor/converters/officeparser/dist/utils/envUtils.d.ts +29 -0
  207. package/vendor/converters/officeparser/dist/utils/envUtils.js +152 -0
  208. package/vendor/converters/officeparser/dist/utils/errorUtils.d.ts +72 -0
  209. package/vendor/converters/officeparser/dist/utils/errorUtils.js +245 -0
  210. package/vendor/converters/officeparser/dist/utils/imageUtils.d.ts +66 -0
  211. package/vendor/converters/officeparser/dist/utils/imageUtils.js +133 -0
  212. package/vendor/converters/officeparser/dist/utils/mathUtils.d.ts +42 -0
  213. package/vendor/converters/officeparser/dist/utils/mathUtils.js +385 -0
  214. package/vendor/converters/officeparser/dist/utils/moduleLoader.d.ts +18 -0
  215. package/vendor/converters/officeparser/dist/utils/moduleLoader.js +106 -0
  216. package/vendor/converters/officeparser/dist/utils/ocrUtils.d.ts +42 -0
  217. package/vendor/converters/officeparser/dist/utils/ocrUtils.js +428 -0
  218. package/vendor/converters/officeparser/dist/utils/sanitize.d.ts +148 -0
  219. package/vendor/converters/officeparser/dist/utils/sanitize.js +344 -0
  220. package/vendor/converters/officeparser/dist/utils/sheetUtils.d.ts +7 -0
  221. package/vendor/converters/officeparser/dist/utils/sheetUtils.js +35 -0
  222. package/vendor/converters/officeparser/dist/utils/styleMapper.d.ts +36 -0
  223. package/vendor/converters/officeparser/dist/utils/styleMapper.js +224 -0
  224. package/vendor/converters/officeparser/dist/utils/xmlUtils.d.ts +163 -0
  225. package/vendor/converters/officeparser/dist/utils/xmlUtils.js +461 -0
  226. package/vendor/converters/officeparser/dist/utils/zipUtils.d.ts +134 -0
  227. package/vendor/converters/officeparser/dist/utils/zipUtils.js +337 -0
  228. package/vendor/converters/officeparser/package.json +147 -0
  229. package/vendor/converters/upstream-manifest.json +124 -0
  230. package/vendor/dependency-fixes/README.md +77 -0
  231. package/vendor/dependency-fixes/vendor-converters.py +83 -0
  232. package/browser-extension/artifacts/gno-browser-clipper-v1.46.0.zip +0 -0
  233. package/browser-extension/artifacts/gno-browser-clipper-v1.46.0.zip.sha256 +0 -1
  234. package/browser-extension/dist/chunk-627emwpj.js +0 -75
  235. /package/browser-extension/dist/{chunk-ydfx5d7p.css → chunk-z74y8n8c.css} +0 -0
@@ -0,0 +1,848 @@
1
+ "use strict";
2
+ /**
3
+ * PDF Parser
4
+ *
5
+ * Extracts text, metadata, images, links, and attachments from PDF files using PDF.js (pdfjs-dist).
6
+ *
7
+ * **Features:**
8
+ * - Text extraction with formatting (bold, italic, font, size)
9
+ * - Comprehensive metadata extraction (title, author, subject, creator, producer, creation/modification dates)
10
+ * - Hyperlink extraction from PDF annotations
11
+ * - Heading detection via font size heuristics
12
+ * - Image extraction as attachments with optional OCR (using Tesseract.js)
13
+ * - Embedded file attachment extraction
14
+ * - Layout preservation (respects order of text and images)
15
+ *
16
+ * **PDF Format Limitations (compared to DOCX/ODT):**
17
+ *
18
+ * PDF was designed as a "page description language" for visual fidelity, not semantic structure.
19
+ * The following features **cannot be reliably extracted** from PDFs:
20
+ *
21
+ * - **Tables**: PDF has no table structure. Tables are just text positioned to look tabular.
22
+ * Extracting tables would require complex spatial analysis with many false positives.
23
+ * See: https://stackoverflow.com/questions/36978446/why-is-it-difficult-to-extract-data-from-pdfs
24
+ *
25
+ * - **Lists**: PDF has no list structure. Bullets/numbers are just text characters.
26
+ * No hierarchy or list type information is stored. Would require heuristic detection
27
+ * that would have many edge cases and errors.
28
+ *
29
+ * - **Styles**: PDF has no style definitions like "Heading1" or "Normal". Only visual
30
+ * properties (font, size) exist. We use font size heuristics to detect headings.
31
+ *
32
+ * - **Notes (Footnotes/Endnotes)**: PDF has no concept of footnotes/endnotes as structured
33
+ * elements. They're just smaller text at the bottom of pages.
34
+ *
35
+ * - **Text Color**: While PDF stores color, pdfjs-dist doesn't expose text color in the
36
+ * textContent API. Would require parsing the operator stream which is complex.
37
+ *
38
+ * - **Background Color**: Same limitation as text color.
39
+ *
40
+ * - **Underline/Strikethrough**: These are drawn as separate line elements in PDF,
41
+ * not properties of text. Association would require spatial analysis.
42
+ *
43
+ * **Parsing Approach:**
44
+ * 1. Load PDF document using pdfjs-dist.
45
+ * 2. Extract global metadata from document info dictionary.
46
+ * 3. Extract embedded file attachments.
47
+ * 4. Iterate through each page:
48
+ * a. Collect text items with position and formatting.
49
+ * b. Collect link annotations with associated text.
50
+ * c. Collect images from the operator list.
51
+ * d. Sort all items by vertical position (top-to-bottom reading order).
52
+ * e. Group text into paragraphs/headings based on line breaks and font sizes.
53
+ * f. Process images as attachments with optional OCR.
54
+ * 5. Apply heading detection based on font size heuristics.
55
+ *
56
+ * @module PdfParser
57
+ * @see https://mozilla.github.io/pdf.js/ PDF.js documentation
58
+ * @see https://www.adobe.com/devnet/pdf/pdf_reference.html PDF Reference
59
+ */
60
+ Object.defineProperty(exports, "__esModule", { value: true });
61
+ exports.parsePdf = void 0;
62
+ const defaults_js_1 = require("../defaults.js");
63
+ const types_js_1 = require("../types.js");
64
+ const astUtils_js_1 = require("../utils/astUtils.js");
65
+ const dateUtils_js_1 = require("../utils/dateUtils.js");
66
+ const envUtils_js_1 = require("../utils/envUtils.js");
67
+ const errorUtils_js_1 = require("../utils/errorUtils.js");
68
+ const imageUtils_js_1 = require("../utils/imageUtils.js");
69
+ const moduleLoader_js_1 = require("../utils/moduleLoader.js");
70
+ const ocrUtils_js_1 = require("../utils/ocrUtils.js");
71
+ /** Type guard for TextItem in PDF.js 5.x */
72
+ function isTextItem(item) {
73
+ return item && typeof item.str === 'string' && Array.isArray(item.transform) && item.transform.length >= 6;
74
+ }
75
+ /**
76
+ * Calculates statistics about font sizes in the document.
77
+ * Used for heading detection heuristics.
78
+ */
79
+ function calculateFontStats(pageItems) {
80
+ const sizes = [];
81
+ for (const page of pageItems) {
82
+ for (const item of page) {
83
+ if (item.type === 'text' && item.height > 0) {
84
+ sizes.push(item.height);
85
+ }
86
+ }
87
+ }
88
+ if (sizes.length === 0)
89
+ return { median: 12, max: 12 };
90
+ sizes.sort((a, b) => a - b);
91
+ const median = sizes[Math.floor(sizes.length / 2)];
92
+ const max = sizes[sizes.length - 1];
93
+ return { median, max };
94
+ }
95
+ /**
96
+ * Determines if a text item should be considered a heading based on its font size.
97
+ *
98
+ * Heuristic: Text that is at least 20% larger than the median body text size
99
+ * is considered a heading. The level (1-6) is determined by relative size.
100
+ *
101
+ * @param fontSize - The font size of the text
102
+ * @param fontStats - Statistics about fonts in the document
103
+ * @returns Heading level (1-6) or 0 if not a heading
104
+ */
105
+ function detectHeadingLevel(fontSize, fontStats) {
106
+ // If font is less than 20% larger than median, it's not a heading
107
+ if (fontSize <= fontStats.median * 1.2)
108
+ return 0;
109
+ // Calculate heading level based on how much larger than median
110
+ const ratio = fontSize / fontStats.median;
111
+ if (ratio >= 2.0)
112
+ return 1; // 2x or more = H1
113
+ if (ratio >= 1.7)
114
+ return 2; // 1.7x-2x = H2
115
+ if (ratio >= 1.5)
116
+ return 3; // 1.5x-1.7x = H3
117
+ if (ratio >= 1.35)
118
+ return 4; // 1.35x-1.5x = H4
119
+ if (ratio >= 1.2)
120
+ return 5; // 1.2x-1.35x = H5
121
+ return 0; // Below threshold
122
+ }
123
+ function findLinkForText(item, annotations) {
124
+ const itemMinX = item.x;
125
+ const itemMaxX = item.x + item.width;
126
+ const itemMinY = item.y;
127
+ const itemMaxY = item.y + item.height;
128
+ for (const annot of annotations) {
129
+ const [x1, y1, x2, y2] = annot.rect;
130
+ const annotMinX = Math.min(x1, x2);
131
+ const annotMaxX = Math.max(x1, x2);
132
+ const annotMinY = Math.min(y1, y2);
133
+ const annotMaxY = Math.max(y1, y2);
134
+ // Check for any intersection between boxes
135
+ const intersects = (itemMinX < annotMaxX && itemMaxX > annotMinX) &&
136
+ (itemMinY < annotMaxY && itemMaxY > annotMinY);
137
+ if (intersects) {
138
+ return annot;
139
+ }
140
+ }
141
+ return undefined;
142
+ }
143
+ /**
144
+ * Encodes raw RGBA data into a 24-bit BMP buffer with a white background.
145
+ * Transparency (alpha channel) is flattened against white.
146
+ *
147
+ * @param width - Image width
148
+ * @param height - Image height
149
+ * @param data - RGBA pixel data
150
+ * @returns BMP Buffer
151
+ */
152
+ function encodeBmp(width, height, data) {
153
+ // BMP row size must be a multiple of 4 bytes
154
+ const rowSize = Math.floor((24 * width + 31) / 32) * 4;
155
+ const padding = rowSize - (width * 3);
156
+ const headerSize = 54; // 14 (File Header) + 40 (DIB Header)
157
+ const imageSize = rowSize * height;
158
+ const fileSize = headerSize + imageSize;
159
+ const buffer = Buffer.alloc(fileSize);
160
+ // --- File Header (14 bytes) ---
161
+ buffer.write('BM', 0); // Signature
162
+ buffer.writeUInt32LE(fileSize, 2); // File Size
163
+ buffer.writeUInt32LE(0, 6); // Reserved
164
+ buffer.writeUInt32LE(headerSize, 10); // Offset to pixel data
165
+ // --- DIB Header (BITMAPINFOHEADER - 40 bytes) ---
166
+ buffer.writeUInt32LE(40, 14); // Header Size
167
+ buffer.writeInt32LE(width, 18); // Width
168
+ buffer.writeInt32LE(-height, 22); // Height (negative for top-down)
169
+ buffer.writeUInt16LE(1, 26); // Planes
170
+ buffer.writeUInt16LE(24, 28); // Bit Count (24-bit RGB)
171
+ buffer.writeUInt32LE(0, 30); // Compression (BI_RGB)
172
+ buffer.writeUInt32LE(imageSize, 34); // Image Size
173
+ buffer.writeInt32LE(2835, 38); // X PixelsPerMeter (72 DPI)
174
+ buffer.writeInt32LE(2835, 42); // Y PixelsPerMeter (72 DPI)
175
+ buffer.writeUInt32LE(0, 46); // Colors Used
176
+ buffer.writeUInt32LE(0, 50); // Colors Important
177
+ // --- Pixel Data ---
178
+ let offset = headerSize;
179
+ for (let y = 0; y < height; y++) {
180
+ for (let x = 0; x < width; x++) {
181
+ const i = (y * width + x) * 4;
182
+ // RGBA input
183
+ const r = data[i + 0];
184
+ const g = data[i + 1];
185
+ const b = data[i + 2];
186
+ const a = data[i + 3];
187
+ // Flatten alpha against white background
188
+ // out = alpha * pixel + (1 - alpha) * white
189
+ // white = 255
190
+ const alpha = a / 255;
191
+ const outR = Math.round(r * alpha + 255 * (1 - alpha));
192
+ const outG = Math.round(g * alpha + 255 * (1 - alpha));
193
+ const outB = Math.round(b * alpha + 255 * (1 - alpha));
194
+ // Write as BGR (BMP standard)
195
+ buffer[offset + 0] = outB;
196
+ buffer[offset + 1] = outG;
197
+ buffer[offset + 2] = outR;
198
+ offset += 3;
199
+ }
200
+ // Write padding
201
+ for (let p = 0; p < padding; p++) {
202
+ buffer[offset] = 0;
203
+ offset++;
204
+ }
205
+ }
206
+ return buffer;
207
+ }
208
+ /**
209
+ * Converts raw PDF image data to a buffer for attachment extraction.
210
+ *
211
+ * **Important Limitation:**
212
+ * PDF images are stored as raw pixel data (RGB, RGBA, or grayscale), not as encoded
213
+ * image files like PNG or JPEG. This function converts the raw data to a normalized
214
+ * RGBA buffer, but this is NOT a valid image file format.
215
+ *
216
+ * For display, the raw RGBA data would need to be encoded to PNG/JPEG, which requires
217
+ * an additional library like `sharp` or `pngjs`. Currently, this is stored as raw bytes.
218
+ *
219
+ * OCR is NOT supported for PDF images because Tesseract.js requires encoded image files
220
+ * (PNG, JPEG, etc.), not raw pixel data. To enable OCR, a PNG encoder would need to be added.
221
+ *
222
+ * @param data - Raw pixel data from PDF.js
223
+ * @param width - Image width in pixels
224
+ * @param height - Image height in pixels
225
+ * @param kind - PDF.js image kind (1=Grayscale, 2=RGB, 3=RGBA)
226
+ * @returns Buffer containing RGBA pixel data (NOT an encoded image file)
227
+ */
228
+ function convertToRgbaBuffer(data, width, height, kind) {
229
+ // PDF.js image kind values:
230
+ // 1 = GRAYSCALE
231
+ // 2 = RGB
232
+ // 3 = RGBA
233
+ let rgbaData;
234
+ if (kind === 1) {
235
+ // Grayscale - expand to RGBA
236
+ rgbaData = new Uint8ClampedArray(width * height * 4);
237
+ for (let i = 0; i < width * height; i++) {
238
+ const gray = data[i];
239
+ rgbaData[i * 4] = gray;
240
+ rgbaData[i * 4 + 1] = gray;
241
+ rgbaData[i * 4 + 2] = gray;
242
+ rgbaData[i * 4 + 3] = 255;
243
+ }
244
+ }
245
+ else if (kind === 2 || data.length === width * height * 3) {
246
+ // RGB - add alpha channel
247
+ rgbaData = new Uint8ClampedArray(width * height * 4);
248
+ for (let i = 0; i < width * height; i++) {
249
+ rgbaData[i * 4] = data[i * 3];
250
+ rgbaData[i * 4 + 1] = data[i * 3 + 1];
251
+ rgbaData[i * 4 + 2] = data[i * 3 + 2];
252
+ rgbaData[i * 4 + 3] = 255;
253
+ }
254
+ }
255
+ else {
256
+ // Assume RGBA
257
+ rgbaData = data instanceof Uint8ClampedArray ? data : new Uint8ClampedArray(data);
258
+ }
259
+ return Buffer.from(rgbaData.buffer, rgbaData.byteOffset, rgbaData.byteLength);
260
+ }
261
+ /**
262
+ * Parses a PDF file and extracts content.
263
+ *
264
+ * @param buffer - The PDF file buffer
265
+ * @param config - Parser configuration
266
+ * @returns Promise resolving to the parsed AST
267
+ */
268
+ const parsePdf = async (buffer, config) => {
269
+ (0, errorUtils_js_1.checkAbortSignal)(config.abortSignal);
270
+ const pdfjs = await (0, moduleLoader_js_1.loadPdfJs)();
271
+ // Configure worker
272
+ const workerSrc = config.pdfWorkerSrc;
273
+ if (envUtils_js_1.isBrowser) {
274
+ pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;
275
+ }
276
+ else {
277
+ // Node.js: Try to auto-resolve local worker path to avoid remote download errors
278
+ (0, envUtils_js_1.assertNode)('pdf-worker-auto-resolution');
279
+ let resolved = false;
280
+ // If the user provided a custom path (not the default CDN one), use it.
281
+ // Otherwise, check if the worker is already loaded globally, or try to find it locally.
282
+ if (workerSrc !== defaults_js_1.DEFAULT_OFFICE_PARSER_CONFIG.pdfWorkerSrc && workerSrc !== '') {
283
+ pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;
284
+ resolved = true;
285
+ }
286
+ else if (globalThis.pdfjsWorker) {
287
+ resolved = true;
288
+ }
289
+ else {
290
+ try {
291
+ // We use require.resolve to find the exact path of the installed package.
292
+ // @ts-ignore - 'require' is available in Node.js/CommonJS environment
293
+ const localWorkerPath = require.resolve('pdfjs-dist/legacy/build/pdf.worker.mjs');
294
+ // Use file:// URL for the worker source in Node.js to ensure compatibility with ESM-native PDF.js 5.x
295
+ // We use dynamic import for 'url' to avoid breaking browser bundles
296
+ const { pathToFileURL } = await import('url');
297
+ pdfjs.GlobalWorkerOptions.workerSrc = pathToFileURL(localWorkerPath).href;
298
+ resolved = true;
299
+ }
300
+ catch (e) {
301
+ (0, errorUtils_js_1.logWarning)(types_js_1.OfficeWarningType.PDF_WORKER_FALLBACK, config, undefined, e);
302
+ }
303
+ }
304
+ if (!resolved) {
305
+ pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;
306
+ }
307
+ }
308
+ const uint8Array = new Uint8Array(buffer);
309
+ const loadingTask = pdfjs.getDocument({
310
+ data: uint8Array,
311
+ verbosity: 0, // ERRORS only, suppresses warnings
312
+ // Harden against untrusted PDFs: don't let pdf.js JIT font/CMap fast-paths
313
+ // compile via `new Function`.
314
+ isEvalSupported: false
315
+ });
316
+ // Handle loading errors, specifically missing worker in browser
317
+ let pdfDocument;
318
+ try {
319
+ pdfDocument = await loadingTask.promise;
320
+ }
321
+ catch (e) {
322
+ const message = e instanceof Error ? e.message : String(e);
323
+ if (message.includes('workerSrc') || message.includes('No "GlobalWorkerOptions.workerSrc" specified')) {
324
+ throw (0, errorUtils_js_1.getOfficeError)(types_js_1.OfficeErrorType.PDF_WORKER_MISSING, config);
325
+ }
326
+ throw e;
327
+ }
328
+ const content = [];
329
+ const attachments = [];
330
+ const numPages = pdfDocument.numPages;
331
+ // Collect all page items for font statistics before processing
332
+ const allPageItems = [];
333
+ // --- Metadata Extraction ---
334
+ // Extract all available metadata from the PDF info dictionary.
335
+ // Note: Some metadata fields depend on how the PDF was created.
336
+ // - Producer: Software that created the PDF
337
+ // - Creator: Application that made the original document
338
+ // - Keywords, Description are rarely present
339
+ const meta = await pdfDocument.getMetadata().catch(() => ({ info: {} }));
340
+ const info = meta.info;
341
+ const metadata = {
342
+ pages: numPages,
343
+ title: info?.Title,
344
+ author: info?.Author,
345
+ subject: info?.Subject,
346
+ description: info?.Keywords, // Map Keywords to description as closest match
347
+ created: (0, dateUtils_js_1.parseOfficeDate)(info?.CreationDate),
348
+ modified: (0, dateUtils_js_1.parseOfficeDate)(info?.ModDate),
349
+ // Note: lastModifiedBy is not available in PDF format - there's no concept of "last modifier"
350
+ // The Author field only tracks original author.
351
+ };
352
+ // Extract non-standard entries from the PDF Info dictionary as custom properties.
353
+ // The standard keys are defined by the PDF spec; anything else is user/tool-defined.
354
+ const standardPdfInfoKeys = new Set([
355
+ 'Title', 'Author', 'Subject', 'Keywords', 'Creator', 'Producer',
356
+ 'CreationDate', 'ModDate', 'Trapped', 'IsAcroFormPresent', 'IsXFAPresent',
357
+ 'IsCollectionPresent', 'IsSignaturesPresent', 'PDFFormatVersion'
358
+ ]);
359
+ if (info) {
360
+ metadata.nativeProperties = {};
361
+ for (const [key, val] of Object.entries(info)) {
362
+ if (key === 'Custom' && typeof val === 'object' && !Array.isArray(val) && !(val instanceof Date) && val !== null) {
363
+ for (const [customKey, customVal] of Object.entries(val)) {
364
+ metadata.nativeProperties[customKey] = customVal;
365
+ }
366
+ }
367
+ else {
368
+ metadata.nativeProperties[key] = val;
369
+ }
370
+ }
371
+ const customProperties = {};
372
+ for (const key of Object.keys(info)) {
373
+ if (standardPdfInfoKeys.has(key))
374
+ continue;
375
+ const val = info[key];
376
+ if (val === null || val === undefined)
377
+ continue;
378
+ // pdf.js groups document-level custom metadata under a 'Custom' object.
379
+ // Flatten its entries directly into customProperties.
380
+ if (key === 'Custom' && typeof val === 'object' && !Array.isArray(val) && !(val instanceof Date)) {
381
+ for (const [customKey, customVal] of Object.entries(val)) {
382
+ if (customVal === null || customVal === undefined)
383
+ continue;
384
+ if (typeof customVal === 'string' || typeof customVal === 'number' || typeof customVal === 'boolean' || customVal instanceof Date) {
385
+ customProperties[customKey] = customVal;
386
+ }
387
+ }
388
+ continue;
389
+ }
390
+ if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean' || val instanceof Date) {
391
+ customProperties[key] = val;
392
+ }
393
+ }
394
+ if (Object.keys(customProperties).length > 0) {
395
+ metadata.customProperties = customProperties;
396
+ }
397
+ }
398
+ if (meta.metadata) {
399
+ if (!metadata.nativeProperties)
400
+ metadata.nativeProperties = {};
401
+ const xmp = meta.metadata;
402
+ if (typeof xmp.getAll === 'function') {
403
+ metadata.nativeProperties['XMP'] = xmp.getAll();
404
+ }
405
+ else {
406
+ metadata.nativeProperties['XMP'] = xmp;
407
+ }
408
+ }
409
+ // --- Embedded File Attachment Extraction ---
410
+ /**
411
+ * PDF can contain embedded files (not images in content, but attached files).
412
+ * These are separate from images in the page content stream.
413
+ */
414
+ try {
415
+ const embeddedFiles = await pdfDocument.getAttachments();
416
+ if (embeddedFiles && config.extractAttachments) {
417
+ for (const name in embeddedFiles) {
418
+ const file = embeddedFiles[name];
419
+ const fileBuffer = Buffer.from(file.content);
420
+ const attachment = (0, imageUtils_js_1.createAttachment)(file.filename, fileBuffer);
421
+ attachments.push(attachment);
422
+ }
423
+ }
424
+ }
425
+ catch (e) {
426
+ (0, errorUtils_js_1.logWarning)(types_js_1.OfficeWarningType.ATTACHMENT_EXTRACTION_FAILED, config, undefined, e);
427
+ }
428
+ // --- First Pass: Collect all items for font statistics ---
429
+ for (let i = 1; i <= numPages; i++) {
430
+ (0, errorUtils_js_1.checkAbortSignal)(config.abortSignal);
431
+ let page;
432
+ let textContent;
433
+ const pageItems = [];
434
+ try {
435
+ page = await pdfDocument.getPage(i);
436
+ // Extract text content
437
+ textContent = await page.getTextContent();
438
+ }
439
+ catch (e) {
440
+ (0, errorUtils_js_1.logWarning)(types_js_1.OfficeWarningType.PAGE_LOAD_FAILED, config, i, e);
441
+ // Push empty items to maintain index alignment for second pass
442
+ allPageItems.push(pageItems);
443
+ continue;
444
+ }
445
+ const commonObjs = page.commonObjs;
446
+ const fontCache = new Map();
447
+ for (const item of textContent.items) {
448
+ // PDF.js 5.x: textContent.items can contain TextMarkedContent which lack
449
+ // 'str' and 'transform'. Skip these to avoid crashes and page skipping.
450
+ if (!isTextItem(item)) {
451
+ continue;
452
+ }
453
+ // At this point we know the item is a TextItem
454
+ const textItem = item;
455
+ const transform = textItem.transform;
456
+ const x = transform[4];
457
+ const y = transform[5];
458
+ const width = textItem.width || 0;
459
+ const height = textItem.height || Math.abs(transform[3]) || 12;
460
+ // Extract formatting from font
461
+ const formatting = {};
462
+ let fontName;
463
+ if (textItem.fontName && commonObjs) {
464
+ try {
465
+ if (commonObjs.has(textItem.fontName)) {
466
+ let fontData = fontCache.get(textItem.fontName);
467
+ if (!fontData) {
468
+ // Use callback-based get to ensure safe resolution
469
+ fontData = await new Promise((resolve) => {
470
+ // @ts-ignore - commonObjs.get is callback-based in legacy builds
471
+ commonObjs.get(textItem.fontName, (data) => resolve(data));
472
+ });
473
+ fontCache.set(textItem.fontName, fontData);
474
+ }
475
+ if (fontData?.name && typeof fontData.name === 'string') {
476
+ // Remove PDF subset prefix (6 uppercase letters + '+')
477
+ fontName = fontData.name.replace(/^[A-Z]{6}\+/, '');
478
+ formatting.font = fontName;
479
+ // Detect bold/italic from font name
480
+ const lowerName = fontData.name.toLowerCase();
481
+ if (lowerName.includes('bold'))
482
+ formatting.bold = true;
483
+ if (lowerName.includes('italic') || lowerName.includes('oblique'))
484
+ formatting.italic = true;
485
+ }
486
+ }
487
+ }
488
+ catch {
489
+ // Font lookup failed, continue without font info
490
+ }
491
+ }
492
+ if (height > 0) {
493
+ formatting.size = Math.round(height).toString();
494
+ }
495
+ pageItems.push({
496
+ type: 'text',
497
+ x,
498
+ y,
499
+ width,
500
+ height,
501
+ text: textItem.str,
502
+ fontName,
503
+ formatting
504
+ });
505
+ }
506
+ // Extract images if enabled
507
+ if (config.extractAttachments || config.ocr) {
508
+ try {
509
+ const ops = await page.getOperatorList();
510
+ const fnArray = ops.fnArray;
511
+ const argsArray = ops.argsArray;
512
+ for (let j = 0; j < fnArray.length; j++) {
513
+ const fn = fnArray[j];
514
+ if (fn === pdfjs.OPS.dependency) {
515
+ const deps = argsArray[j];
516
+ for (const dep of deps) {
517
+ // In pdfjs-dist v3+, get() throws if not resolved unless a callback is provided.
518
+ // We must use the callback pattern to wait for resolution.
519
+ try {
520
+ if (page.objs.has(dep))
521
+ continue;
522
+ await new Promise((resolve) => {
523
+ const timeout = setTimeout(() => {
524
+ resolve();
525
+ }, 500);
526
+ page.objs.get(dep, (data) => {
527
+ clearTimeout(timeout);
528
+ resolve();
529
+ });
530
+ });
531
+ }
532
+ catch (e) {
533
+ (0, errorUtils_js_1.logWarning)(types_js_1.OfficeWarningType.DEPENDENCY_LOAD_FAILED, config, dep, e);
534
+ }
535
+ }
536
+ }
537
+ if (fn === pdfjs.OPS.paintImageXObject || fn === pdfjs.OPS.paintXObject) {
538
+ const imgName = argsArray[j][0];
539
+ try {
540
+ let hasObj = page.objs.has(imgName);
541
+ let targetObjs = page.objs;
542
+ if (!hasObj && page.commonObjs.has(imgName)) {
543
+ hasObj = true;
544
+ targetObjs = page.commonObjs;
545
+ }
546
+ if (hasObj) {
547
+ // Use callback-based get to ensure safe resolution
548
+ const imgObj = await new Promise((resolve) => {
549
+ // @ts-ignore - targetObjs.get is callback-based
550
+ targetObjs.get(imgName, (data) => resolve(data));
551
+ });
552
+ // Browser-specific: Handle ImageBitmap if data is missing
553
+ if (envUtils_js_1.isBrowser && !imgObj.data && imgObj.bitmap) {
554
+ try {
555
+ const canvas = document.createElement('canvas');
556
+ canvas.width = imgObj.width;
557
+ canvas.height = imgObj.height;
558
+ const ctx = canvas.getContext('2d');
559
+ if (ctx) {
560
+ ctx.drawImage(imgObj.bitmap, 0, 0);
561
+ imgObj.data = ctx.getImageData(0, 0, imgObj.width, imgObj.height).data;
562
+ imgObj.kind = 3; // RGBA
563
+ }
564
+ }
565
+ catch (e) {
566
+ (0, errorUtils_js_1.logWarning)(types_js_1.OfficeWarningType.IMAGE_PROCESSING_FAILED, config, undefined, e);
567
+ }
568
+ }
569
+ if (imgObj?.data && imgObj.width > 0 && imgObj.height > 0) {
570
+ // Find position from transform matrix
571
+ let imgX = 0, imgY = 0;
572
+ for (let k = j - 1; k >= 0; k--) {
573
+ if (fnArray[k] === pdfjs.OPS.transform) {
574
+ imgX = argsArray[k][4];
575
+ imgY = argsArray[k][5];
576
+ break;
577
+ }
578
+ }
579
+ pageItems.push({
580
+ type: 'image',
581
+ x: imgX,
582
+ y: imgY,
583
+ name: imgName,
584
+ data: imgObj.data,
585
+ width: imgObj.width,
586
+ height: imgObj.height,
587
+ kind: imgObj.kind
588
+ });
589
+ }
590
+ }
591
+ }
592
+ catch {
593
+ // Image access failed, continue
594
+ }
595
+ }
596
+ }
597
+ }
598
+ catch (e) {
599
+ (0, errorUtils_js_1.logWarning)(types_js_1.OfficeWarningType.IMAGE_EXTRACTION_FAILED, config, `from page ${i}`, e);
600
+ }
601
+ }
602
+ allPageItems.push(pageItems);
603
+ }
604
+ // Calculate font statistics for heading detection
605
+ const fontStats = calculateFontStats(allPageItems);
606
+ // --- Second Pass: Process pages with font statistics ---
607
+ for (let i = 0; i < allPageItems.length; i++) {
608
+ (0, errorUtils_js_1.checkAbortSignal)(config.abortSignal);
609
+ const pageNum = i + 1;
610
+ let page;
611
+ try {
612
+ page = await pdfDocument.getPage(pageNum);
613
+ }
614
+ catch (e) {
615
+ (0, errorUtils_js_1.logWarning)(types_js_1.OfficeWarningType.PAGE_LOAD_FAILED, config, pageNum, e);
616
+ continue;
617
+ }
618
+ const pageItems = allPageItems[i];
619
+ const pageContent = [];
620
+ // Extract link annotations for this page
621
+ const annotations = [];
622
+ const matchedAnnotations = new Set();
623
+ try {
624
+ const annots = await page.getAnnotations();
625
+ for (const annot of annots) {
626
+ if (annot.subtype === 'Link' && annot.rect) {
627
+ // PDF.js 5.x compatibility: url might be in 'url', 'unsafeUrl', or 'data.url'
628
+ const url = annot.url || annot.unsafeUrl || annot.data?.url;
629
+ annotations.push({
630
+ rect: annot.rect,
631
+ url: url,
632
+ dest: annot.dest
633
+ });
634
+ }
635
+ }
636
+ }
637
+ catch (e) {
638
+ (0, errorUtils_js_1.logWarning)(types_js_1.OfficeWarningType.ANNOTATION_EXTRACTION_FAILED, config, pageNum, e);
639
+ }
640
+ // Sort items: Y descending (top to bottom), then X ascending (left to right)
641
+ pageItems.sort((a, b) => {
642
+ // Relax tolerance slightly (5 -> 7) for PDF.js 5.x coordinate precision
643
+ if (Math.abs(b.y - a.y) > 7)
644
+ return b.y - a.y;
645
+ return a.x - b.x;
646
+ });
647
+ // Process sorted items into content nodes
648
+ let currentNode = null;
649
+ let currentNodeFontSize = 0;
650
+ let lastY = -1;
651
+ let imageCounter = 0;
652
+ for (const item of pageItems) {
653
+ if (item.type === 'text') {
654
+ const text = item.text;
655
+ if (!text)
656
+ continue;
657
+ // Check for new line
658
+ const isNewLine = lastY !== -1 && Math.abs(item.y - lastY) > 5;
659
+ if (isNewLine && currentNode) {
660
+ // Finalize and push current node
661
+ if ((currentNode.text || '').trim().length > 0) {
662
+ pageContent.push(currentNode);
663
+ }
664
+ currentNode = null;
665
+ }
666
+ // Skip pure whitespace at start of lines
667
+ if (!currentNode && text.trim().length === 0) {
668
+ lastY = item.y;
669
+ continue;
670
+ }
671
+ // Determine if this should be a heading
672
+ const headingLevel = detectHeadingLevel(item.height, fontStats);
673
+ if (!currentNode) {
674
+ // Start new node
675
+ if (headingLevel > 0) {
676
+ currentNode = {
677
+ type: 'heading',
678
+ text: '',
679
+ children: [],
680
+ metadata: { level: headingLevel }
681
+ };
682
+ }
683
+ else {
684
+ currentNode = {
685
+ type: 'paragraph',
686
+ text: '',
687
+ children: []
688
+ };
689
+ }
690
+ currentNodeFontSize = item.height;
691
+ }
692
+ // Handle whitespace
693
+ if (text.trim().length === 0) {
694
+ if (currentNode.children && currentNode.children.length > 0) {
695
+ const lastChild = currentNode.children[currentNode.children.length - 1];
696
+ if (lastChild.type === 'text' && lastChild.text) {
697
+ lastChild.text += text;
698
+ currentNode.text += text;
699
+ }
700
+ }
701
+ lastY = item.y;
702
+ continue;
703
+ }
704
+ // Add space between words if needed
705
+ if (currentNode.text && currentNode.text.length > 0 && !currentNode.text.endsWith(' ')) {
706
+ currentNode.text += ' ';
707
+ if (currentNode.children && currentNode.children.length > 0) {
708
+ const lastChild = currentNode.children[currentNode.children.length - 1];
709
+ if (lastChild.type === 'text' && lastChild.text) {
710
+ lastChild.text += ' ';
711
+ }
712
+ }
713
+ }
714
+ currentNode.text += text;
715
+ // Check for link
716
+ const link = findLinkForText(item, annotations);
717
+ if (link)
718
+ matchedAnnotations.add(link);
719
+ let textMetadata;
720
+ if (link) {
721
+ if (link.url) {
722
+ textMetadata = {
723
+ link: link.url,
724
+ linkType: link.url.startsWith('#') ? 'internal' : 'external'
725
+ };
726
+ }
727
+ else if (link.dest) {
728
+ // Internal destination
729
+ textMetadata = {
730
+ link: typeof link.dest === 'string' ? `#${link.dest}` : '#internal',
731
+ linkType: 'internal'
732
+ };
733
+ }
734
+ }
735
+ // Try to merge with last child if same formatting and no link change
736
+ let merged = false;
737
+ if (currentNode.children && currentNode.children.length > 0 && !textMetadata) {
738
+ const lastChild = currentNode.children[currentNode.children.length - 1];
739
+ if (lastChild.type === 'text' &&
740
+ isSameFormatting(lastChild.formatting, item.formatting) &&
741
+ !lastChild.metadata) {
742
+ lastChild.text = (lastChild.text || '') + text;
743
+ merged = true;
744
+ }
745
+ }
746
+ if (!merged) {
747
+ const textNode = {
748
+ type: 'text',
749
+ text: text,
750
+ formatting: Object.keys(item.formatting).length > 0 ? item.formatting : undefined
751
+ };
752
+ if (textMetadata) {
753
+ textNode.metadata = textMetadata;
754
+ }
755
+ currentNode.children?.push(textNode);
756
+ }
757
+ lastY = item.y;
758
+ }
759
+ else if (item.type === 'image') {
760
+ // Flush current node
761
+ if (currentNode) {
762
+ if ((currentNode.text || '').trim().length > 0) {
763
+ pageContent.push(currentNode);
764
+ }
765
+ currentNode = null;
766
+ }
767
+ imageCounter++;
768
+ // Note: Using .bmp extension since we encode to BMP for broad compatibility
769
+ const attachmentName = `pdf_image_p${pageNum}_${imageCounter}.bmp`;
770
+ /**
771
+ * Image extraction for PDF files.
772
+ *
773
+ * PDF stores images as raw pixel data. We convert to BMP for compatibility.
774
+ */
775
+ if (config.extractAttachments) {
776
+ try {
777
+ const imageBuffer = convertToRgbaBuffer(item.data, item.width, item.height, item.kind);
778
+ // Encode as BMP
779
+ const bmpBuffer = encodeBmp(item.width, item.height, new Uint8Array(imageBuffer));
780
+ const attachment = (0, imageUtils_js_1.createAttachment)(attachmentName, bmpBuffer);
781
+ attachment.mimeType = 'image/bmp';
782
+ // Perform OCR if enabled
783
+ if (config.ocr) {
784
+ try {
785
+ // Skip OCR for very small images/artifacts (e.g. < 10px) to avoid Tesseract warnings
786
+ if (item.width >= 10 && item.height >= 10) {
787
+ attachment.ocrText = (await (0, ocrUtils_js_1.performOcr)(bmpBuffer, { ...config.ocrConfig })).trim();
788
+ }
789
+ }
790
+ catch (e) {
791
+ (0, errorUtils_js_1.logWarning)(types_js_1.OfficeWarningType.OCR_FAILED, config, attachmentName, e);
792
+ }
793
+ }
794
+ attachments.push(attachment);
795
+ // Create image content node
796
+ const imageMetadata = {
797
+ attachmentName,
798
+ };
799
+ pageContent.push({
800
+ type: 'image',
801
+ text: attachment.ocrText || '',
802
+ metadata: { ...imageMetadata }
803
+ });
804
+ }
805
+ catch (e) {
806
+ (0, errorUtils_js_1.logWarning)(types_js_1.OfficeWarningType.IMAGE_EXTRACTION_FAILED, config, attachmentName, e);
807
+ }
808
+ }
809
+ }
810
+ }
811
+ // Flush last node
812
+ if (currentNode && (currentNode.text || '').trim().length > 0) {
813
+ pageContent.push(currentNode);
814
+ }
815
+ // Add page node to content
816
+ content.push({
817
+ type: 'page',
818
+ children: pageContent,
819
+ text: pageContent.map(node => node.text).join(config.newlineDelimiter),
820
+ metadata: { pageNumber: pageNum }
821
+ });
822
+ }
823
+ const toTextSync = () => content.map(c => c.text).join(config.newlineDelimiter);
824
+ return (0, astUtils_js_1.createAST)('pdf', metadata, content, attachments, config, undefined, toTextSync);
825
+ };
826
+ exports.parsePdf = parsePdf;
827
+ /**
828
+ * Helper to compare two text formatting objects.
829
+ * Returns true if both have the same properties with the same values.
830
+ */
831
+ function isSameFormatting(a, b) {
832
+ if (!a && !b)
833
+ return true;
834
+ if (!a || !b)
835
+ return false;
836
+ const keysA = Object.keys(a).sort();
837
+ const keysB = Object.keys(b).sort();
838
+ if (keysA.length !== keysB.length)
839
+ return false;
840
+ for (let i = 0; i < keysA.length; i++) {
841
+ const key = keysA[i];
842
+ if (keysA[i] !== keysB[i])
843
+ return false;
844
+ if (a[key] !== b[key])
845
+ return false;
846
+ }
847
+ return true;
848
+ }