@gmickel/gno 1.46.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/THIRD_PARTY_NOTICES.md +46 -0
- package/assets/skill/SKILL.md +7 -6
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.0.0.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.0.0.zip.sha256 +1 -0
- package/browser-extension/dist/chunk-4tc9v0ja.js +74 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/browser-extension/dist/preview.html +1 -1
- package/browser-extension/dist/service-worker.js +32 -33
- package/bunfig.toml +2 -0
- package/package.json +40 -26
- package/spec/cli.md +21 -4
- package/spec/db/schema.sql +146 -1
- package/spec/mcp.md +26 -0
- package/src/app/context-runtime-types.ts +3 -0
- package/src/app/context-runtime.ts +2 -0
- package/src/cli/commands/ask.ts +6 -1
- package/src/cli/commands/daemon.ts +21 -8
- package/src/cli/commands/embed.ts +77 -41
- package/src/cli/detach.ts +3 -2
- package/src/config/types.ts +3 -3
- package/src/converters/adapters/markitdownTs/adapter.ts +1 -2
- package/src/converters/adapters/officeparser/adapter.ts +1 -2
- package/src/converters/versions.ts +6 -8
- package/src/core/context-evidence.ts +8 -4
- package/src/core/job-manager.ts +95 -13
- package/src/core/network-boundary-inventory.ts +10 -0
- package/src/core/shutdown-budget.ts +45 -0
- package/src/embed/backlog.ts +107 -4
- package/src/embed/batch.ts +42 -2
- package/src/embed/fingerprint.ts +16 -0
- package/src/embed/retry.ts +113 -5
- package/src/embed/variant-backlog.ts +105 -0
- package/src/embed/variant-plan.ts +62 -0
- package/src/embed/variant-retry.ts +113 -0
- package/src/ingestion/graph-reconciliation.ts +327 -0
- package/src/ingestion/sync.ts +9 -272
- package/src/llm/http-inference.ts +6 -0
- package/src/llm/httpEmbedding.ts +37 -6
- package/src/llm/httpGeneration.ts +18 -3
- package/src/llm/httpRerank.ts +23 -5
- package/src/llm/inference-cancellation.ts +168 -0
- package/src/llm/inference-scope.ts +202 -0
- package/src/llm/lazy-ports.ts +115 -0
- package/src/llm/native-worker/client.ts +541 -0
- package/src/llm/native-worker/dispatcher.ts +228 -0
- package/src/llm/native-worker/embedding-identity.ts +33 -0
- package/src/llm/native-worker/entry.ts +173 -0
- package/src/llm/native-worker/errors.ts +32 -0
- package/src/llm/native-worker/evaluation.ts +16 -0
- package/src/llm/native-worker/owned-exit.ts +108 -0
- package/src/llm/native-worker/owner.ts +141 -0
- package/src/llm/native-worker/ports.ts +317 -0
- package/src/llm/native-worker/protocol.ts +442 -0
- package/src/llm/native-worker/runtime-config.ts +92 -0
- package/src/llm/nodeLlamaCpp/adapter.ts +77 -20
- package/src/llm/nodeLlamaCpp/embedding.ts +130 -46
- package/src/llm/nodeLlamaCpp/generation.ts +34 -5
- package/src/llm/nodeLlamaCpp/lifecycle-options.ts +99 -0
- package/src/llm/nodeLlamaCpp/lifecycle.ts +209 -204
- package/src/llm/nodeLlamaCpp/rerank-capacity.ts +111 -0
- package/src/llm/nodeLlamaCpp/rerank.ts +118 -27
- package/src/llm/nodeLlamaCpp/simulator-handle.ts +73 -0
- package/src/llm/nodeLlamaCpp/simulator-install.ts +124 -0
- package/src/llm/nodeLlamaCpp/simulator-session.ts +240 -0
- package/src/llm/nodeLlamaCpp/simulator-types.ts +80 -0
- package/src/llm/types.ts +35 -5
- package/src/mcp/context.ts +27 -0
- package/src/mcp/http-transport.ts +12 -10
- package/src/mcp/server.ts +3 -0
- package/src/mcp/tool-profile.ts +30 -8
- package/src/mcp/tools/context.ts +8 -11
- package/src/mcp/tools/embed.ts +1 -1
- package/src/mcp/tools/index-cmd.ts +1 -1
- package/src/mcp/tools/index.ts +10 -8
- package/src/mcp/tools/query.ts +14 -30
- package/src/mcp/tools/vsearch.ts +1 -1
- package/src/pipeline/answer.ts +23 -3
- package/src/pipeline/claim-verifier.ts +6 -0
- package/src/pipeline/expansion.ts +43 -40
- package/src/pipeline/explain.ts +6 -2
- package/src/pipeline/filters.ts +63 -0
- package/src/pipeline/fusion.ts +29 -9
- package/src/pipeline/graph-retrieval.ts +29 -9
- package/src/pipeline/hybrid.ts +198 -55
- package/src/pipeline/hydration.ts +161 -0
- package/src/pipeline/owner-fusion.ts +87 -0
- package/src/pipeline/rerank.ts +35 -11
- package/src/pipeline/search.ts +13 -2
- package/src/pipeline/types.ts +5 -3
- package/src/pipeline/vsearch.ts +87 -7
- package/src/sdk/client.ts +47 -3
- package/src/sdk/embed.ts +63 -39
- package/src/serve/background-runtime.ts +1 -1
- package/src/serve/context.ts +41 -56
- package/src/serve/embed-scheduler.ts +58 -35
- package/src/serve/public/components/IndexingProgress.tsx +46 -60
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/shiki-language-ids.ts +14 -0
- package/src/serve/resident-admission.ts +36 -36
- package/src/serve/resident-background-work.ts +20 -2
- package/src/serve/resident-request.ts +11 -5
- package/src/serve/resident-runtime.ts +97 -61
- package/src/serve/resident-shutdown.ts +153 -0
- package/src/serve/routes/api.ts +3 -1
- package/src/serve/server.ts +47 -26
- package/src/store/migrations/028-vector-variants.ts +54 -0
- package/src/store/migrations/029-graph-reference-state.ts +77 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +251 -183
- package/src/store/sqlite/eligibility.ts +174 -0
- package/src/store/sqlite/graph-edge-application.ts +66 -0
- package/src/store/sqlite/graph-reference-state.ts +194 -0
- package/src/store/sqlite/legacy-vector-ownership.ts +79 -0
- package/src/store/types.ts +80 -12
- package/src/store/vector/eligibility.ts +36 -0
- package/src/store/vector/freshness.ts +33 -6
- package/src/store/vector/lazy.ts +81 -0
- package/src/store/vector/sqlite-vec.ts +106 -54
- package/src/store/vector/stats.ts +14 -3
- package/src/store/vector/types.ts +35 -2
- package/src/store/vector/variant-search.ts +192 -0
- package/src/store/vector/variants.ts +451 -0
- package/vendor/converters/markitdown-ts/LICENSE +21 -0
- package/vendor/converters/markitdown-ts/dist/index.cjs +1180 -0
- package/vendor/converters/markitdown-ts/dist/index.d.cts +46 -0
- package/vendor/converters/markitdown-ts/dist/index.d.mts +46 -0
- package/vendor/converters/markitdown-ts/dist/index.d.ts +46 -0
- package/vendor/converters/markitdown-ts/dist/index.mjs +1152 -0
- package/vendor/converters/markitdown-ts/package.json +77 -0
- package/vendor/converters/officeparser/LICENSE +21 -0
- package/vendor/converters/officeparser/dist/OfficeConverter.d.ts +47 -0
- package/vendor/converters/officeparser/dist/OfficeConverter.js +76 -0
- package/vendor/converters/officeparser/dist/OfficeGenerator.d.ts +23 -0
- package/vendor/converters/officeparser/dist/OfficeGenerator.js +73 -0
- package/vendor/converters/officeparser/dist/OfficeParser.d.ts +106 -0
- package/vendor/converters/officeparser/dist/OfficeParser.js +332 -0
- package/vendor/converters/officeparser/dist/cli.d.ts +28 -0
- package/vendor/converters/officeparser/dist/cli.js +381 -0
- package/vendor/converters/officeparser/dist/defaults.d.ts +41 -0
- package/vendor/converters/officeparser/dist/defaults.js +218 -0
- package/vendor/converters/officeparser/dist/generators/BaseGenerator.d.ts +107 -0
- package/vendor/converters/officeparser/dist/generators/BaseGenerator.js +248 -0
- package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.d.ts +82 -0
- package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.js +797 -0
- package/vendor/converters/officeparser/dist/generators/CsvGenerator.d.ts +38 -0
- package/vendor/converters/officeparser/dist/generators/CsvGenerator.js +245 -0
- package/vendor/converters/officeparser/dist/generators/EpubGenerator.d.ts +43 -0
- package/vendor/converters/officeparser/dist/generators/EpubGenerator.js +315 -0
- package/vendor/converters/officeparser/dist/generators/HtmlGenerator.d.ts +59 -0
- package/vendor/converters/officeparser/dist/generators/HtmlGenerator.js +1942 -0
- package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.d.ts +96 -0
- package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.js +1175 -0
- package/vendor/converters/officeparser/dist/generators/PdfGenerator.d.ts +22 -0
- package/vendor/converters/officeparser/dist/generators/PdfGenerator.js +194 -0
- package/vendor/converters/officeparser/dist/generators/RtfGenerator.d.ts +29 -0
- package/vendor/converters/officeparser/dist/generators/RtfGenerator.js +316 -0
- package/vendor/converters/officeparser/dist/generators/TextGenerator.d.ts +13 -0
- package/vendor/converters/officeparser/dist/generators/TextGenerator.js +201 -0
- package/vendor/converters/officeparser/dist/index.d.ts +60 -0
- package/vendor/converters/officeparser/dist/index.js +72 -0
- package/vendor/converters/officeparser/dist/index.mjs +18 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.d.ts +2621 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.iife.js +1336 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.mjs +1335 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.slim.d.ts +2621 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.slim.iife.js +1336 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.slim.mjs +1335 -0
- package/vendor/converters/officeparser/dist/parsers/CsvParser.d.ts +9 -0
- package/vendor/converters/officeparser/dist/parsers/CsvParser.js +115 -0
- package/vendor/converters/officeparser/dist/parsers/EpubParser.d.ts +8 -0
- package/vendor/converters/officeparser/dist/parsers/EpubParser.js +217 -0
- package/vendor/converters/officeparser/dist/parsers/ExcelParser.d.ts +32 -0
- package/vendor/converters/officeparser/dist/parsers/ExcelParser.js +736 -0
- package/vendor/converters/officeparser/dist/parsers/HtmlParser.d.ts +2 -0
- package/vendor/converters/officeparser/dist/parsers/HtmlParser.js +1287 -0
- package/vendor/converters/officeparser/dist/parsers/MarkdownParser.d.ts +2 -0
- package/vendor/converters/officeparser/dist/parsers/MarkdownParser.js +1272 -0
- package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.d.ts +31 -0
- package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.js +1819 -0
- package/vendor/converters/officeparser/dist/parsers/PdfParser.d.ts +67 -0
- package/vendor/converters/officeparser/dist/parsers/PdfParser.js +848 -0
- package/vendor/converters/officeparser/dist/parsers/PowerPointParser.d.ts +32 -0
- package/vendor/converters/officeparser/dist/parsers/PowerPointParser.js +950 -0
- package/vendor/converters/officeparser/dist/parsers/RtfParser.d.ts +187 -0
- package/vendor/converters/officeparser/dist/parsers/RtfParser.js +1801 -0
- package/vendor/converters/officeparser/dist/parsers/WordParser.d.ts +79 -0
- package/vendor/converters/officeparser/dist/parsers/WordParser.js +1177 -0
- package/vendor/converters/officeparser/dist/sbom.cdx.json +1763 -0
- package/vendor/converters/officeparser/dist/types.d.ts +2507 -0
- package/vendor/converters/officeparser/dist/types.js +107 -0
- package/vendor/converters/officeparser/dist/utils/astUtils.d.ts +16 -0
- package/vendor/converters/officeparser/dist/utils/astUtils.js +33 -0
- package/vendor/converters/officeparser/dist/utils/chartUtils.d.ts +6 -0
- package/vendor/converters/officeparser/dist/utils/chartUtils.js +257 -0
- package/vendor/converters/officeparser/dist/utils/configUtils.d.ts +44 -0
- package/vendor/converters/officeparser/dist/utils/configUtils.js +315 -0
- package/vendor/converters/officeparser/dist/utils/dateUtils.d.ts +17 -0
- package/vendor/converters/officeparser/dist/utils/dateUtils.js +69 -0
- package/vendor/converters/officeparser/dist/utils/envUtils.d.ts +29 -0
- package/vendor/converters/officeparser/dist/utils/envUtils.js +152 -0
- package/vendor/converters/officeparser/dist/utils/errorUtils.d.ts +72 -0
- package/vendor/converters/officeparser/dist/utils/errorUtils.js +245 -0
- package/vendor/converters/officeparser/dist/utils/imageUtils.d.ts +66 -0
- package/vendor/converters/officeparser/dist/utils/imageUtils.js +133 -0
- package/vendor/converters/officeparser/dist/utils/mathUtils.d.ts +42 -0
- package/vendor/converters/officeparser/dist/utils/mathUtils.js +385 -0
- package/vendor/converters/officeparser/dist/utils/moduleLoader.d.ts +18 -0
- package/vendor/converters/officeparser/dist/utils/moduleLoader.js +106 -0
- package/vendor/converters/officeparser/dist/utils/ocrUtils.d.ts +42 -0
- package/vendor/converters/officeparser/dist/utils/ocrUtils.js +428 -0
- package/vendor/converters/officeparser/dist/utils/sanitize.d.ts +148 -0
- package/vendor/converters/officeparser/dist/utils/sanitize.js +344 -0
- package/vendor/converters/officeparser/dist/utils/sheetUtils.d.ts +7 -0
- package/vendor/converters/officeparser/dist/utils/sheetUtils.js +35 -0
- package/vendor/converters/officeparser/dist/utils/styleMapper.d.ts +36 -0
- package/vendor/converters/officeparser/dist/utils/styleMapper.js +224 -0
- package/vendor/converters/officeparser/dist/utils/xmlUtils.d.ts +163 -0
- package/vendor/converters/officeparser/dist/utils/xmlUtils.js +461 -0
- package/vendor/converters/officeparser/dist/utils/zipUtils.d.ts +134 -0
- package/vendor/converters/officeparser/dist/utils/zipUtils.js +337 -0
- package/vendor/converters/officeparser/package.json +147 -0
- package/vendor/converters/upstream-manifest.json +124 -0
- package/vendor/dependency-fixes/README.md +77 -0
- package/vendor/dependency-fixes/vendor-converters.py +83 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.46.0.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.46.0.zip.sha256 +0 -1
- package/browser-extension/dist/chunk-627emwpj.js +0 -75
- /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
|
+
}
|