@retinue/agentkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +310 -0
- package/dist/adapters/bullmq/consumer.d.ts +33 -0
- package/dist/adapters/bullmq/consumer.js +41 -0
- package/dist/adapters/bullmq/dispatcher.d.ts +74 -0
- package/dist/adapters/bullmq/dispatcher.js +160 -0
- package/dist/adapters/bullmq/export.d.ts +31 -0
- package/dist/adapters/bullmq/export.js +53 -0
- package/dist/adapters/bullmq/extraction.d.ts +42 -0
- package/dist/adapters/bullmq/extraction.js +63 -0
- package/dist/adapters/bullmq/index.d.ts +13 -0
- package/dist/adapters/bullmq/index.js +13 -0
- package/dist/adapters/bullmq/lock.d.ts +77 -0
- package/dist/adapters/bullmq/lock.js +126 -0
- package/dist/adapters/bullmq/queue.d.ts +50 -0
- package/dist/adapters/bullmq/queue.js +81 -0
- package/dist/adapters/memory/artifact-exports.d.ts +11 -0
- package/dist/adapters/memory/artifact-exports.js +102 -0
- package/dist/adapters/memory/artifacts.d.ts +15 -0
- package/dist/adapters/memory/artifacts.js +134 -0
- package/dist/adapters/memory/blobs.d.ts +7 -0
- package/dist/adapters/memory/blobs.js +27 -0
- package/dist/adapters/memory/evaluation.d.ts +18 -0
- package/dist/adapters/memory/evaluation.js +148 -0
- package/dist/adapters/memory/files.d.ts +27 -0
- package/dist/adapters/memory/files.js +0 -0
- package/dist/adapters/memory/flows.d.ts +16 -0
- package/dist/adapters/memory/flows.js +117 -0
- package/dist/adapters/memory/hitl.d.ts +9 -0
- package/dist/adapters/memory/hitl.js +130 -0
- package/dist/adapters/memory/idempotency.d.ts +13 -0
- package/dist/adapters/memory/idempotency.js +32 -0
- package/dist/adapters/memory/index.d.ts +39 -0
- package/dist/adapters/memory/index.js +107 -0
- package/dist/adapters/memory/knowledge.d.ts +43 -0
- package/dist/adapters/memory/knowledge.js +248 -0
- package/dist/adapters/memory/mcp.d.ts +9 -0
- package/dist/adapters/memory/mcp.js +37 -0
- package/dist/adapters/memory/message-store.d.ts +17 -0
- package/dist/adapters/memory/message-store.js +70 -0
- package/dist/adapters/memory/principal-memory.d.ts +7 -0
- package/dist/adapters/memory/principal-memory.js +83 -0
- package/dist/adapters/memory/runtime.d.ts +29 -0
- package/dist/adapters/memory/runtime.js +0 -0
- package/dist/adapters/memory/sessions.d.ts +29 -0
- package/dist/adapters/memory/sessions.js +0 -0
- package/dist/adapters/memory/skills.d.ts +10 -0
- package/dist/adapters/memory/skills.js +41 -0
- package/dist/adapters/memory/thread-summary.d.ts +7 -0
- package/dist/adapters/memory/thread-summary.js +29 -0
- package/dist/adapters/memory/usage-limits.d.ts +13 -0
- package/dist/adapters/memory/usage-limits.js +72 -0
- package/dist/adapters/memory/usage.d.ts +16 -0
- package/dist/adapters/memory/usage.js +279 -0
- package/dist/adapters/otel/index.d.ts +111 -0
- package/dist/adapters/otel/index.js +133 -0
- package/dist/adapters/postgres/artifact-exports.d.ts +12 -0
- package/dist/adapters/postgres/artifact-exports.js +117 -0
- package/dist/adapters/postgres/artifacts.d.ts +16 -0
- package/dist/adapters/postgres/artifacts.js +172 -0
- package/dist/adapters/postgres/checkpoint-store.d.ts +16 -0
- package/dist/adapters/postgres/checkpoint-store.js +34 -0
- package/dist/adapters/postgres/config.d.ts +15 -0
- package/dist/adapters/postgres/config.js +187 -0
- package/dist/adapters/postgres/conversation-store.d.ts +4 -0
- package/dist/adapters/postgres/conversation-store.js +82 -0
- package/dist/adapters/postgres/evaluation.d.ts +17 -0
- package/dist/adapters/postgres/evaluation.js +193 -0
- package/dist/adapters/postgres/file-content.d.ts +30 -0
- package/dist/adapters/postgres/file-content.js +111 -0
- package/dist/adapters/postgres/files.d.ts +19 -0
- package/dist/adapters/postgres/files.js +209 -0
- package/dist/adapters/postgres/flows.d.ts +20 -0
- package/dist/adapters/postgres/flows.js +206 -0
- package/dist/adapters/postgres/hitl.d.ts +5 -0
- package/dist/adapters/postgres/hitl.js +247 -0
- package/dist/adapters/postgres/index.d.ts +35 -0
- package/dist/adapters/postgres/index.js +35 -0
- package/dist/adapters/postgres/knowledge.d.ts +48 -0
- package/dist/adapters/postgres/knowledge.js +255 -0
- package/dist/adapters/postgres/memory.d.ts +14 -0
- package/dist/adapters/postgres/memory.js +194 -0
- package/dist/adapters/postgres/message-store.d.ts +11 -0
- package/dist/adapters/postgres/message-store.js +145 -0
- package/dist/adapters/postgres/migrations.d.ts +69 -0
- package/dist/adapters/postgres/migrations.js +1594 -0
- package/dist/adapters/postgres/pg-executor.d.ts +19 -0
- package/dist/adapters/postgres/pg-executor.js +32 -0
- package/dist/adapters/postgres/retention.d.ts +26 -0
- package/dist/adapters/postgres/retention.js +59 -0
- package/dist/adapters/postgres/rollups.d.ts +17 -0
- package/dist/adapters/postgres/rollups.js +267 -0
- package/dist/adapters/postgres/run-coordinator.d.ts +5 -0
- package/dist/adapters/postgres/run-coordinator.js +98 -0
- package/dist/adapters/postgres/run-event-log.d.ts +26 -0
- package/dist/adapters/postgres/run-event-log.js +30 -0
- package/dist/adapters/postgres/run-store.d.ts +4 -0
- package/dist/adapters/postgres/run-store.js +199 -0
- package/dist/adapters/postgres/schema.d.ts +39 -0
- package/dist/adapters/postgres/schema.js +70 -0
- package/dist/adapters/postgres/session-state.d.ts +7 -0
- package/dist/adapters/postgres/session-state.js +99 -0
- package/dist/adapters/postgres/sql.d.ts +8 -0
- package/dist/adapters/postgres/sql.js +2 -0
- package/dist/adapters/postgres/transaction.d.ts +37 -0
- package/dist/adapters/postgres/transaction.js +93 -0
- package/dist/adapters/postgres/unit-of-work.d.ts +18 -0
- package/dist/adapters/postgres/unit-of-work.js +8 -0
- package/dist/adapters/postgres/usage-limits.d.ts +15 -0
- package/dist/adapters/postgres/usage-limits.js +136 -0
- package/dist/adapters/postgres/usage.d.ts +15 -0
- package/dist/adapters/postgres/usage.js +226 -0
- package/dist/adapters/redis/index.d.ts +9 -0
- package/dist/adapters/redis/index.js +9 -0
- package/dist/adapters/redis/realtime.d.ts +74 -0
- package/dist/adapters/redis/realtime.js +112 -0
- package/dist/adapters/supabase/index.d.ts +88 -0
- package/dist/adapters/supabase/index.js +84 -0
- package/dist/adapters/supabase/realtime.d.ts +30 -0
- package/dist/adapters/supabase/realtime.js +53 -0
- package/dist/adapters/supabase/rls.d.ts +99 -0
- package/dist/adapters/supabase/rls.js +216 -0
- package/dist/adapters/supabase/storage.d.ts +50 -0
- package/dist/adapters/supabase/storage.js +207 -0
- package/dist/agents/agent.d.ts +66 -0
- package/dist/agents/agent.js +209 -0
- package/dist/agents/define.d.ts +21 -0
- package/dist/agents/define.js +63 -0
- package/dist/agents/engine.d.ts +98 -0
- package/dist/agents/engine.js +462 -0
- package/dist/agents/index.d.ts +50 -0
- package/dist/agents/index.js +17 -0
- package/dist/artifacts/index.d.ts +114 -0
- package/dist/artifacts/index.js +219 -0
- package/dist/authorization/index.d.ts +76 -0
- package/dist/authorization/index.js +76 -0
- package/dist/capabilities/index.d.ts +120 -0
- package/dist/capabilities/index.js +167 -0
- package/dist/capabilities/runtime.d.ts +89 -0
- package/dist/capabilities/runtime.js +84 -0
- package/dist/citations/index.d.ts +161 -0
- package/dist/citations/index.js +182 -0
- package/dist/context/assembler.d.ts +82 -0
- package/dist/context/assembler.js +129 -0
- package/dist/context/compaction.d.ts +45 -0
- package/dist/context/compaction.js +55 -0
- package/dist/context/index.d.ts +75 -0
- package/dist/context/index.js +17 -0
- package/dist/core/content-parts.d.ts +194 -0
- package/dist/core/content-parts.js +23 -0
- package/dist/core/context.d.ts +51 -0
- package/dist/core/context.js +9 -0
- package/dist/core/env.d.ts +25 -0
- package/dist/core/env.js +41 -0
- package/dist/core/errors.d.ts +30 -0
- package/dist/core/errors.js +65 -0
- package/dist/core/events.d.ts +139 -0
- package/dist/core/events.js +99 -0
- package/dist/core/ids.d.ts +52 -0
- package/dist/core/ids.js +13 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.js +9 -0
- package/dist/core/tokens.d.ts +22 -0
- package/dist/core/tokens.js +22 -0
- package/dist/core/validation.d.ts +34 -0
- package/dist/core/validation.js +176 -0
- package/dist/documents/extraction.d.ts +121 -0
- package/dist/documents/extraction.js +293 -0
- package/dist/documents/index.d.ts +199 -0
- package/dist/documents/index.js +65 -0
- package/dist/documents/parsers/pdf.d.ts +47 -0
- package/dist/documents/parsers/pdf.js +508 -0
- package/dist/documents/parsers/text.d.ts +59 -0
- package/dist/documents/parsers/text.js +325 -0
- package/dist/documents/read-tool.d.ts +52 -0
- package/dist/documents/read-tool.js +109 -0
- package/dist/documents/render.d.ts +29 -0
- package/dist/documents/render.js +59 -0
- package/dist/documents/vision.d.ts +159 -0
- package/dist/documents/vision.js +214 -0
- package/dist/entries/adapters-bullmq.d.ts +8 -0
- package/dist/entries/adapters-bullmq.js +8 -0
- package/dist/entries/adapters-otel.d.ts +13 -0
- package/dist/entries/adapters-otel.js +13 -0
- package/dist/entries/adapters-postgres.d.ts +10 -0
- package/dist/entries/adapters-postgres.js +10 -0
- package/dist/entries/adapters-redis.d.ts +3 -0
- package/dist/entries/adapters-redis.js +3 -0
- package/dist/entries/context.d.ts +20 -0
- package/dist/entries/context.js +20 -0
- package/dist/entries/flows.d.ts +15 -0
- package/dist/entries/flows.js +15 -0
- package/dist/entries/hitl.d.ts +10 -0
- package/dist/entries/hitl.js +10 -0
- package/dist/entries/knowledge.d.ts +18 -0
- package/dist/entries/knowledge.js +19 -0
- package/dist/entries/mcp.d.ts +10 -0
- package/dist/entries/mcp.js +10 -0
- package/dist/entries/observability.d.ts +14 -0
- package/dist/entries/observability.js +16 -0
- package/dist/entries/persistence.d.ts +11 -0
- package/dist/entries/persistence.js +11 -0
- package/dist/entries/providers.d.ts +14 -0
- package/dist/entries/providers.js +14 -0
- package/dist/entries/runtime.d.ts +13 -0
- package/dist/entries/runtime.js +13 -0
- package/dist/entries/server.d.ts +24 -0
- package/dist/entries/server.js +24 -0
- package/dist/entries/tools.d.ts +21 -0
- package/dist/entries/tools.js +21 -0
- package/dist/entries/usage.d.ts +10 -0
- package/dist/entries/usage.js +10 -0
- package/dist/evaluation/gate.d.ts +168 -0
- package/dist/evaluation/gate.js +180 -0
- package/dist/evaluation/graders.d.ts +125 -0
- package/dist/evaluation/graders.js +203 -0
- package/dist/evaluation/index.d.ts +120 -0
- package/dist/evaluation/index.js +183 -0
- package/dist/evaluation/judge.d.ts +75 -0
- package/dist/evaluation/judge.js +111 -0
- package/dist/export/index.d.ts +162 -0
- package/dist/export/index.js +363 -0
- package/dist/export/markdown.d.ts +19 -0
- package/dist/export/markdown.js +29 -0
- package/dist/export/pdf.d.ts +73 -0
- package/dist/export/pdf.js +407 -0
- package/dist/files/context.d.ts +97 -0
- package/dist/files/context.js +185 -0
- package/dist/files/index.d.ts +210 -0
- package/dist/files/index.js +338 -0
- package/dist/files/read-tool.d.ts +81 -0
- package/dist/files/read-tool.js +163 -0
- package/dist/files/turn-parts.d.ts +96 -0
- package/dist/files/turn-parts.js +171 -0
- package/dist/flows/index.d.ts +270 -0
- package/dist/flows/index.js +62 -0
- package/dist/flows/interpreter.d.ts +146 -0
- package/dist/flows/interpreter.js +426 -0
- package/dist/flows/runner.d.ts +145 -0
- package/dist/flows/runner.js +270 -0
- package/dist/graphql/index.d.ts +8 -0
- package/dist/graphql/index.js +8 -0
- package/dist/graphql/resolvers.d.ts +237 -0
- package/dist/graphql/resolvers.js +253 -0
- package/dist/graphql/schema.d.ts +11 -0
- package/dist/graphql/schema.js +258 -0
- package/dist/graphql/sse.d.ts +77 -0
- package/dist/graphql/sse.js +100 -0
- package/dist/hitl/approved-execution.d.ts +127 -0
- package/dist/hitl/approved-execution.js +177 -0
- package/dist/hitl/index.d.ts +79 -0
- package/dist/hitl/index.js +12 -0
- package/dist/hitl/service.d.ts +221 -0
- package/dist/hitl/service.js +268 -0
- package/dist/idempotency/index.d.ts +70 -0
- package/dist/idempotency/index.js +59 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +60 -0
- package/dist/knowledge/chunking.d.ts +57 -0
- package/dist/knowledge/chunking.js +158 -0
- package/dist/knowledge/index.d.ts +119 -0
- package/dist/knowledge/index.js +166 -0
- package/dist/knowledge/retrieval.d.ts +146 -0
- package/dist/knowledge/retrieval.js +170 -0
- package/dist/loadtest/harness.d.ts +168 -0
- package/dist/loadtest/harness.js +507 -0
- package/dist/loadtest/index.d.ts +13 -0
- package/dist/loadtest/index.js +13 -0
- package/dist/loadtest/injection.d.ts +89 -0
- package/dist/loadtest/injection.js +147 -0
- package/dist/loadtest/metrics.d.ts +197 -0
- package/dist/loadtest/metrics.js +160 -0
- package/dist/loadtest/runbooks.d.ts +28 -0
- package/dist/loadtest/runbooks.js +159 -0
- package/dist/loadtest/scenario.d.ts +104 -0
- package/dist/loadtest/scenario.js +208 -0
- package/dist/mcp/egress.d.ts +53 -0
- package/dist/mcp/egress.js +115 -0
- package/dist/mcp/index.d.ts +93 -0
- package/dist/mcp/index.js +33 -0
- package/dist/mcp/provider.d.ts +62 -0
- package/dist/mcp/provider.js +0 -0
- package/dist/models/index.d.ts +98 -0
- package/dist/models/index.js +74 -0
- package/dist/models/pricing.d.ts +24 -0
- package/dist/models/pricing.js +37 -0
- package/dist/models/provider-factory.d.ts +31 -0
- package/dist/models/provider-factory.js +67 -0
- package/dist/models/streaming.d.ts +145 -0
- package/dist/models/streaming.js +272 -0
- package/dist/models/vision.d.ts +38 -0
- package/dist/models/vision.js +62 -0
- package/dist/persistence/index.d.ts +1654 -0
- package/dist/persistence/index.js +226 -0
- package/dist/principal-memory/index.d.ts +106 -0
- package/dist/principal-memory/index.js +89 -0
- package/dist/retention/index.d.ts +89 -0
- package/dist/retention/index.js +70 -0
- package/dist/runtime/checkpoint.d.ts +37 -0
- package/dist/runtime/checkpoint.js +22 -0
- package/dist/runtime/index.d.ts +118 -0
- package/dist/runtime/index.js +69 -0
- package/dist/runtime/retry.d.ts +95 -0
- package/dist/runtime/retry.js +126 -0
- package/dist/runtime/serialization.d.ts +85 -0
- package/dist/runtime/serialization.js +95 -0
- package/dist/runtime/streaming.d.ts +54 -0
- package/dist/runtime/streaming.js +115 -0
- package/dist/runtime/worker.d.ts +130 -0
- package/dist/runtime/worker.js +405 -0
- package/dist/security/checklist.d.ts +53 -0
- package/dist/security/checklist.js +204 -0
- package/dist/security/findings.d.ts +56 -0
- package/dist/security/findings.js +168 -0
- package/dist/security/index.d.ts +14 -0
- package/dist/security/index.js +14 -0
- package/dist/security/prompt-safety.d.ts +100 -0
- package/dist/security/prompt-safety.js +133 -0
- package/dist/server/boot.d.ts +32 -0
- package/dist/server/boot.js +36 -0
- package/dist/server/cli-worker.d.ts +37 -0
- package/dist/server/cli-worker.js +151 -0
- package/dist/server/cli.d.ts +27 -0
- package/dist/server/cli.js +74 -0
- package/dist/server/config.d.ts +42 -0
- package/dist/server/config.js +127 -0
- package/dist/server/health.d.ts +59 -0
- package/dist/server/health.js +90 -0
- package/dist/server/host.d.ts +39 -0
- package/dist/server/host.js +124 -0
- package/dist/server/index.d.ts +15 -0
- package/dist/server/index.js +15 -0
- package/dist/server/main.d.ts +16 -0
- package/dist/server/main.js +31 -0
- package/dist/server/sse-route.d.ts +21 -0
- package/dist/server/sse-route.js +282 -0
- package/dist/skills/index.d.ts +67 -0
- package/dist/skills/index.js +31 -0
- package/dist/skills/resolver.d.ts +54 -0
- package/dist/skills/resolver.js +121 -0
- package/dist/teams/index.d.ts +93 -0
- package/dist/teams/index.js +207 -0
- package/dist/telemetry/index.d.ts +157 -0
- package/dist/telemetry/index.js +71 -0
- package/dist/telemetry/instrument.d.ts +108 -0
- package/dist/telemetry/instrument.js +232 -0
- package/dist/telemetry/log-events.d.ts +17 -0
- package/dist/telemetry/log-events.js +58 -0
- package/dist/telemetry/metrics.d.ts +123 -0
- package/dist/telemetry/metrics.js +135 -0
- package/dist/telemetry/noop.d.ts +39 -0
- package/dist/telemetry/noop.js +143 -0
- package/dist/telemetry/redaction.d.ts +64 -0
- package/dist/telemetry/redaction.js +153 -0
- package/dist/telemetry/spans.d.ts +56 -0
- package/dist/telemetry/spans.js +78 -0
- package/dist/telemetry/trace-context.d.ts +55 -0
- package/dist/telemetry/trace-context.js +60 -0
- package/dist/toolkit/compute.d.ts +53 -0
- package/dist/toolkit/compute.js +152 -0
- package/dist/toolkit/data.d.ts +98 -0
- package/dist/toolkit/data.js +235 -0
- package/dist/toolkit/http.d.ts +113 -0
- package/dist/toolkit/http.js +205 -0
- package/dist/toolkit/index.d.ts +21 -0
- package/dist/toolkit/index.js +17 -0
- package/dist/toolkit/web.d.ts +107 -0
- package/dist/toolkit/web.js +147 -0
- package/dist/tools/define.d.ts +25 -0
- package/dist/tools/define.js +45 -0
- package/dist/tools/delegating.d.ts +132 -0
- package/dist/tools/delegating.js +211 -0
- package/dist/tools/index.d.ts +129 -0
- package/dist/tools/index.js +33 -0
- package/dist/tools/library/compute.d.ts +11 -0
- package/dist/tools/library/compute.js +46 -0
- package/dist/tools/library/data.d.ts +16 -0
- package/dist/tools/library/data.js +92 -0
- package/dist/tools/library/http.d.ts +28 -0
- package/dist/tools/library/http.js +71 -0
- package/dist/tools/library/index.d.ts +97 -0
- package/dist/tools/library/index.js +134 -0
- package/dist/tools/library/knowledge.d.ts +39 -0
- package/dist/tools/library/knowledge.js +58 -0
- package/dist/tools/library/web.d.ts +19 -0
- package/dist/tools/library/web.js +65 -0
- package/dist/tools/meta-tools.d.ts +19 -0
- package/dist/tools/meta-tools.js +36 -0
- package/dist/tools/registry.d.ts +146 -0
- package/dist/tools/registry.js +291 -0
- package/dist/usage/index.d.ts +105 -0
- package/dist/usage/index.js +20 -0
- package/dist/usage/quota.d.ts +258 -0
- package/dist/usage/quota.js +510 -0
- package/dist/usage/recorder.d.ts +29 -0
- package/dist/usage/recorder.js +96 -0
- package/dist/usage/rollups.d.ts +121 -0
- package/dist/usage/rollups.js +157 -0
- package/dist/worker/export.d.ts +57 -0
- package/dist/worker/export.js +81 -0
- package/dist/worker/extraction.d.ts +57 -0
- package/dist/worker/extraction.js +84 -0
- package/dist/worker/main.d.ts +103 -0
- package/dist/worker/main.js +159 -0
- package/package.json +187 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF text extraction (#131).
|
|
3
|
+
*
|
|
4
|
+
* Over the raw PDF syntax, with `node:zlib` for the compressed streams and no dependency. That is a real
|
|
5
|
+
* decision with real limits, so they are stated here rather than discovered later.
|
|
6
|
+
*
|
|
7
|
+
* **What it handles.** PDFs produced by the tools people actually use — Word, LaTeX, Chrome's print-to-PDF,
|
|
8
|
+
* Google Docs, most report generators. It walks the content streams, follows the text operators, and uses the
|
|
9
|
+
* positioning operators to reconstruct lines and paragraphs. Font size changes become heading levels, because
|
|
10
|
+
* a PDF has no headings: it has text that happens to be bigger, and inferring from that is the only signal
|
|
11
|
+
* available.
|
|
12
|
+
*
|
|
13
|
+
* **What it does not handle**, each of which is reported as a typed failure rather than as empty text:
|
|
14
|
+
*
|
|
15
|
+
* - **Encrypted documents.** Detected by `/Encrypt` in the trailer and refused. Extracting from one would
|
|
16
|
+
* mean implementing the standard security handler, which is a decryption tool wearing a parser's clothes.
|
|
17
|
+
* - **Scans with no text layer.** A photograph of a page contains no text operators at all. Reported as
|
|
18
|
+
* `no-text-layer`, which is the answer that sends someone to OCR instead of to a bug report.
|
|
19
|
+
* - **Custom-encoded embedded fonts.** A Type0/CID font with an embedded `ToUnicode` map needs that map
|
|
20
|
+
* applied; without it the bytes are glyph indices, not characters. Detected as mojibake and warned about
|
|
21
|
+
* rather than silently returned as text — a garbled answer is worse than a refusal.
|
|
22
|
+
*
|
|
23
|
+
* Tables are the honest weak spot and the comment on `groupIntoBlocks` says how far it gets: a PDF does not
|
|
24
|
+
* contain tables, it contains text at coordinates, and column inference from coordinates is a heuristic. It
|
|
25
|
+
* recovers regular grids and says so when it is unsure.
|
|
26
|
+
*/
|
|
27
|
+
import { inflateSync } from "node:zlib";
|
|
28
|
+
import { createBlockBuilder } from "./text.js";
|
|
29
|
+
/**
|
|
30
|
+
* Emitted when the inflated total is cut off.
|
|
31
|
+
*
|
|
32
|
+
* A named constant because a test asserting on it is asserting on the bound itself. The alternative — checking
|
|
33
|
+
* that *something* was truncated — passes when the block or text ceiling fires instead, which is a different
|
|
34
|
+
* bound and does nothing to stop the decompression work.
|
|
35
|
+
*/
|
|
36
|
+
export const DECOMPRESSION_LIMIT_WARNING = "Stopped decompressing: the document's streams expand past the allowed size.";
|
|
37
|
+
const ascii = (bytes) =>
|
|
38
|
+
// latin1, not utf-8: PDF syntax is bytes, and decoding the structure as UTF-8 would corrupt the binary
|
|
39
|
+
// stream data sitting inside it. Text content is decoded separately, per string.
|
|
40
|
+
Buffer.from(bytes).toString("latin1");
|
|
41
|
+
/**
|
|
42
|
+
* `\` escapes inside a PDF literal string.
|
|
43
|
+
*
|
|
44
|
+
* Including the line continuation, which is the one people forget: a backslash at end of line means "no
|
|
45
|
+
* character here", and treating it as a literal backslash inserts one into every wrapped string.
|
|
46
|
+
*/
|
|
47
|
+
export const unescapePdfString = (raw) => {
|
|
48
|
+
let out = "";
|
|
49
|
+
let i = 0;
|
|
50
|
+
while (i < raw.length) {
|
|
51
|
+
const ch = raw[i];
|
|
52
|
+
if (ch !== "\\") {
|
|
53
|
+
out += ch;
|
|
54
|
+
i += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const next = raw[i + 1];
|
|
58
|
+
if (next === undefined)
|
|
59
|
+
break;
|
|
60
|
+
if (next === "n")
|
|
61
|
+
out += "\n";
|
|
62
|
+
else if (next === "r")
|
|
63
|
+
out += "\r";
|
|
64
|
+
else if (next === "t")
|
|
65
|
+
out += "\t";
|
|
66
|
+
else if (next === "b")
|
|
67
|
+
out += "\b";
|
|
68
|
+
else if (next === "f")
|
|
69
|
+
out += "\f";
|
|
70
|
+
else if (next === "\n") {
|
|
71
|
+
// Line continuation: contributes nothing.
|
|
72
|
+
}
|
|
73
|
+
else if (next === "\r") {
|
|
74
|
+
if (raw[i + 2] === "\n")
|
|
75
|
+
i += 1;
|
|
76
|
+
}
|
|
77
|
+
else if (next >= "0" && next <= "7") {
|
|
78
|
+
// Up to three octal digits, and no more — `\0501` is a character followed by a `1`.
|
|
79
|
+
let octal = "";
|
|
80
|
+
let j = i + 1;
|
|
81
|
+
while (j < raw.length && octal.length < 3 && raw[j] !== undefined && raw[j] >= "0" && raw[j] <= "7") {
|
|
82
|
+
octal += raw[j];
|
|
83
|
+
j += 1;
|
|
84
|
+
}
|
|
85
|
+
out += String.fromCharCode(Number.parseInt(octal, 8));
|
|
86
|
+
i = j;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
else
|
|
90
|
+
out += next;
|
|
91
|
+
i += 2;
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
};
|
|
95
|
+
/** A hex string, `<48656C6C6F>`. An odd final digit is padded with zero, as the spec requires. */
|
|
96
|
+
export const decodeHexString = (raw) => {
|
|
97
|
+
const hex = raw.replace(/[^0-9A-Fa-f]/g, "");
|
|
98
|
+
const padded = hex.length % 2 === 1 ? `${hex}0` : hex;
|
|
99
|
+
let out = "";
|
|
100
|
+
for (let i = 0; i < padded.length; i += 2)
|
|
101
|
+
out += String.fromCharCode(Number.parseInt(padded.slice(i, i + 2), 16));
|
|
102
|
+
return out;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* The content streams, decompressed.
|
|
106
|
+
*
|
|
107
|
+
* Streams are located by scanning for `stream`/`endstream` rather than by walking the xref table. The xref is
|
|
108
|
+
* the correct route and also the one that breaks first: an incrementally-updated or linearised PDF has
|
|
109
|
+
* several, a damaged one has a wrong offset, and a scan finds the content either way. The cost is that
|
|
110
|
+
* non-content streams (fonts, images) are also found — they simply yield no text operators.
|
|
111
|
+
*/
|
|
112
|
+
const contentStreams = (raw, limits) => {
|
|
113
|
+
const streams = [];
|
|
114
|
+
const warnings = [];
|
|
115
|
+
let hitDecompressionLimit = false;
|
|
116
|
+
let index = 0;
|
|
117
|
+
let inflatedBytes = 0;
|
|
118
|
+
while (index < raw.length) {
|
|
119
|
+
const start = raw.indexOf("stream", index);
|
|
120
|
+
if (start === -1)
|
|
121
|
+
break;
|
|
122
|
+
// `endstream`/`endobj` also contain "stream"; require the keyword to stand alone.
|
|
123
|
+
const before = raw[start - 1];
|
|
124
|
+
if (before !== undefined && /[A-Za-z]/.test(before)) {
|
|
125
|
+
index = start + 6;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
let dataStart = start + "stream".length;
|
|
129
|
+
if (raw[dataStart] === "\r")
|
|
130
|
+
dataStart += 1;
|
|
131
|
+
if (raw[dataStart] === "\n")
|
|
132
|
+
dataStart += 1;
|
|
133
|
+
const end = raw.indexOf("endstream", dataStart);
|
|
134
|
+
if (end === -1)
|
|
135
|
+
break;
|
|
136
|
+
// The dictionary immediately before the keyword says how this stream is encoded.
|
|
137
|
+
const dictStart = raw.lastIndexOf("<<", start);
|
|
138
|
+
const dict = dictStart === -1 ? "" : raw.slice(dictStart, start);
|
|
139
|
+
const body = raw.slice(dataStart, end);
|
|
140
|
+
index = end + "endstream".length;
|
|
141
|
+
if (/\/Subtype\s*\/(Image|Form1)/.test(dict) && !/\/FlateDecode/.test(dict))
|
|
142
|
+
continue;
|
|
143
|
+
if (/\/FlateDecode/.test(dict)) {
|
|
144
|
+
try {
|
|
145
|
+
const out = inflateSync(Buffer.from(body, "latin1"));
|
|
146
|
+
// The decompression bomb bound. A single-page PDF can carry a stream that inflates to gigabytes, so
|
|
147
|
+
// the ceiling is on the *inflated* total and not on the file — checking the file size alone is the
|
|
148
|
+
// check that does not stop this.
|
|
149
|
+
inflatedBytes += out.byteLength;
|
|
150
|
+
if (inflatedBytes > limits.maxTextBytes * 4) {
|
|
151
|
+
warnings.push(DECOMPRESSION_LIMIT_WARNING);
|
|
152
|
+
hitDecompressionLimit = true;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
streams.push(out.toString("latin1"));
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// A stream that will not inflate is usually a font or an image subtype this scan picked up, not a
|
|
159
|
+
// broken document, so it is skipped rather than failing the whole file.
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (!/\/Filter/.test(dict))
|
|
164
|
+
streams.push(body);
|
|
165
|
+
}
|
|
166
|
+
return { streams, warnings, hitDecompressionLimit };
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Walk one content stream's text operators.
|
|
170
|
+
*
|
|
171
|
+
* A small state machine over the operators that move or draw text: `Tf` (font and size), `Td`/`TD`/`Tm`/`T*`
|
|
172
|
+
* (position), `TL` (leading), `Tj`/`'`/`"` (show), `TJ` (show array). Everything else is ignored, which is
|
|
173
|
+
* most of a PDF — paths, colours and clipping have no bearing on the text.
|
|
174
|
+
*/
|
|
175
|
+
const readFragments = (stream, page, into) => {
|
|
176
|
+
let x = 0;
|
|
177
|
+
let y = 0;
|
|
178
|
+
let size = 12;
|
|
179
|
+
let leading = 0;
|
|
180
|
+
let index = 0;
|
|
181
|
+
const push = (text) => {
|
|
182
|
+
if (text !== "")
|
|
183
|
+
into.push({ text, x, y, size, page });
|
|
184
|
+
};
|
|
185
|
+
const numbersBefore = (at, count) => {
|
|
186
|
+
// Operands precede their operator in PostScript-like syntax, so the arguments are behind us.
|
|
187
|
+
const chunk = stream.slice(Math.max(0, at - 120), at);
|
|
188
|
+
const found = chunk.match(/-?\d*\.?\d+/g) ?? [];
|
|
189
|
+
return found.slice(-count).map(Number);
|
|
190
|
+
};
|
|
191
|
+
while (index < stream.length) {
|
|
192
|
+
const char = stream[index];
|
|
193
|
+
if (char === "(") {
|
|
194
|
+
// Literal string. Nesting is legal and unescaped parens must balance, so depth is tracked.
|
|
195
|
+
let depth = 1;
|
|
196
|
+
let raw = "";
|
|
197
|
+
let j = index + 1;
|
|
198
|
+
while (j < stream.length && depth > 0) {
|
|
199
|
+
const c = stream[j];
|
|
200
|
+
if (c === "\\") {
|
|
201
|
+
raw += c;
|
|
202
|
+
raw += stream[j + 1] ?? "";
|
|
203
|
+
j += 2;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (c === "(")
|
|
207
|
+
depth += 1;
|
|
208
|
+
if (c === ")") {
|
|
209
|
+
depth -= 1;
|
|
210
|
+
if (depth === 0)
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
raw += c;
|
|
214
|
+
j += 1;
|
|
215
|
+
}
|
|
216
|
+
const text = unescapePdfString(raw);
|
|
217
|
+
// Which operator follows decides whether this string is drawn and whether the line advances first.
|
|
218
|
+
const after = stream.slice(j + 1, j + 12);
|
|
219
|
+
if (/^\s*'/.test(after)) {
|
|
220
|
+
y -= leading;
|
|
221
|
+
x = 0;
|
|
222
|
+
push(text);
|
|
223
|
+
}
|
|
224
|
+
else if (/^\s*"/.test(after)) {
|
|
225
|
+
y -= leading;
|
|
226
|
+
x = 0;
|
|
227
|
+
push(text);
|
|
228
|
+
}
|
|
229
|
+
else if (/^\s*(Tj|TJ)/.test(after) || /^\s*[\d\s.-]*(Tj|TJ)/.test(after)) {
|
|
230
|
+
push(text);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
push(text);
|
|
234
|
+
}
|
|
235
|
+
index = j + 1;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (char === "<" && stream[index + 1] !== "<") {
|
|
239
|
+
const close = stream.indexOf(">", index);
|
|
240
|
+
if (close === -1)
|
|
241
|
+
break;
|
|
242
|
+
push(decodeHexString(stream.slice(index + 1, close)));
|
|
243
|
+
index = close + 1;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const op = /^(BT|ET|T\*|TD|Td|Tm|TL|Tf)/.exec(stream.slice(index));
|
|
247
|
+
if (op) {
|
|
248
|
+
const name = op[1];
|
|
249
|
+
if (name === "BT") {
|
|
250
|
+
x = 0;
|
|
251
|
+
y = 0;
|
|
252
|
+
}
|
|
253
|
+
else if (name === "T*") {
|
|
254
|
+
y -= leading;
|
|
255
|
+
x = 0;
|
|
256
|
+
}
|
|
257
|
+
else if (name === "Td" || name === "TD") {
|
|
258
|
+
const [dx, dy] = numbersBefore(index, 2);
|
|
259
|
+
x += dx ?? 0;
|
|
260
|
+
y += dy ?? 0;
|
|
261
|
+
// `TD` also sets the leading to -dy, which is what makes subsequent `T*` advance correctly.
|
|
262
|
+
if (name === "TD")
|
|
263
|
+
leading = -(dy ?? 0);
|
|
264
|
+
}
|
|
265
|
+
else if (name === "Tm") {
|
|
266
|
+
const nums = numbersBefore(index, 6);
|
|
267
|
+
x = nums[4] ?? x;
|
|
268
|
+
y = nums[5] ?? y;
|
|
269
|
+
// The matrix scale multiplies the font size; a document setting `Tf 1` and scaling by 12 in `Tm` is
|
|
270
|
+
// common, and reading the size from `Tf` alone would make every heading look body-sized.
|
|
271
|
+
const scaleY = nums[3] ?? 1;
|
|
272
|
+
if (scaleY !== 0 && Math.abs(scaleY) !== 1)
|
|
273
|
+
size = Math.abs(size * scaleY);
|
|
274
|
+
}
|
|
275
|
+
else if (name === "TL") {
|
|
276
|
+
leading = numbersBefore(index, 1)[0] ?? leading;
|
|
277
|
+
}
|
|
278
|
+
else if (name === "Tf") {
|
|
279
|
+
size = numbersBefore(index, 1)[0] ?? size;
|
|
280
|
+
}
|
|
281
|
+
index += name?.length ?? 1;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
index += 1;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
/** Mojibake test: text that is mostly unmapped glyph indices rather than characters. */
|
|
288
|
+
const looksLikeGlyphIndices = (text) => {
|
|
289
|
+
if (text.length < 20)
|
|
290
|
+
return false;
|
|
291
|
+
let odd = 0;
|
|
292
|
+
for (const ch of text) {
|
|
293
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
294
|
+
if (code < 9 || (code > 13 && code < 32) || (code >= 0xe000 && code <= 0xf8ff))
|
|
295
|
+
odd += 1;
|
|
296
|
+
}
|
|
297
|
+
return odd / text.length > 0.3;
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* Fragments to blocks.
|
|
301
|
+
*
|
|
302
|
+
* Fragments on the same baseline are one line; a larger-than-usual gap between baselines starts a new
|
|
303
|
+
* paragraph. A line whose font is clearly larger than the document's body size becomes a heading, its level
|
|
304
|
+
* from how much larger.
|
|
305
|
+
*
|
|
306
|
+
* **Tables.** A PDF contains no tables — it contains text at coordinates — so this recovers a grid when the
|
|
307
|
+
* evidence is strong: three or more consecutive lines whose fragments start at the same set of x positions.
|
|
308
|
+
* That catches the regular tables report generators produce and misses merged cells and ruled-only layouts.
|
|
309
|
+
* When lines share x positions but inconsistently, the text is kept as paragraphs and a warning says the
|
|
310
|
+
* layout may not have survived — a wrong table is worse than no table, because a wrong one looks authoritative.
|
|
311
|
+
*/
|
|
312
|
+
const groupIntoBlocks = (fragments, builder) => {
|
|
313
|
+
if (fragments.length === 0)
|
|
314
|
+
return;
|
|
315
|
+
const lines = [];
|
|
316
|
+
for (const fragment of [...fragments].sort((a, b) => a.page !== b.page ? a.page - b.page : b.y !== a.y ? b.y - a.y : a.x - b.x)) {
|
|
317
|
+
const last = lines[lines.length - 1];
|
|
318
|
+
if (last && last.page === fragment.page && Math.abs(last.y - fragment.y) < 2) {
|
|
319
|
+
last.parts.push(fragment);
|
|
320
|
+
last.size = Math.max(last.size, fragment.size);
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
lines.push({ page: fragment.page, y: fragment.y, size: fragment.size, parts: [fragment] });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const joinLine = (line) => {
|
|
327
|
+
let text = "";
|
|
328
|
+
let previousEnd = null;
|
|
329
|
+
for (const part of line.parts) {
|
|
330
|
+
// A gap wider than a couple of characters is a space the PDF expressed by moving rather than by
|
|
331
|
+
// drawing one. Without this, `Total` and `1,234` in adjacent columns become `Total1,234`.
|
|
332
|
+
if (previousEnd !== null && part.x - previousEnd > line.size * 0.25)
|
|
333
|
+
text += " ";
|
|
334
|
+
text += part.text;
|
|
335
|
+
previousEnd = part.x + part.text.length * line.size * 0.5;
|
|
336
|
+
}
|
|
337
|
+
return text.replace(/\s+/g, " ").trim();
|
|
338
|
+
};
|
|
339
|
+
// The body size is the most common size, which is what makes "larger than body" mean anything.
|
|
340
|
+
//
|
|
341
|
+
// Weighted by *characters*, not by line count. Headings are short, so a document with a title, two section
|
|
342
|
+
// headings and three lines of prose has more heading lines than body lines and a line-count vote elects the
|
|
343
|
+
// heading size as the body -- after which nothing is a heading and the document's structure is gone. Found
|
|
344
|
+
// rendering a short report in #134 and reading it back with this parser.
|
|
345
|
+
const sizeCounts = new Map();
|
|
346
|
+
for (const line of lines) {
|
|
347
|
+
const rounded = Math.round(line.size);
|
|
348
|
+
const weight = Math.max(1, joinLine(line).length);
|
|
349
|
+
sizeCounts.set(rounded, (sizeCounts.get(rounded) ?? 0) + weight);
|
|
350
|
+
}
|
|
351
|
+
// Most common size wins, and on a tie the *smaller* one does. That tie-break is not a detail: a short
|
|
352
|
+
// document with one heading and one paragraph has a 1-1 tie, and picking the larger as "body" makes the
|
|
353
|
+
// heading look body-sized and flattens the only structure the document had. A body is never a document's
|
|
354
|
+
// largest text.
|
|
355
|
+
const bodySize = [...sizeCounts.entries()].sort((a, b) => (b[1] !== a[1] ? b[1] - a[1] : a[0] - b[0]))[0]?.[0] ?? 12;
|
|
356
|
+
const headingLevel = (size) => {
|
|
357
|
+
const ratio = size / bodySize;
|
|
358
|
+
if (ratio < 1.15)
|
|
359
|
+
return null;
|
|
360
|
+
if (ratio >= 1.8)
|
|
361
|
+
return 1;
|
|
362
|
+
if (ratio >= 1.5)
|
|
363
|
+
return 2;
|
|
364
|
+
if (ratio >= 1.3)
|
|
365
|
+
return 3;
|
|
366
|
+
return 4;
|
|
367
|
+
};
|
|
368
|
+
/** x positions a line's fragments start at, rounded so near-identical columns match. */
|
|
369
|
+
const xKey = (line) => line.parts.map((p) => Math.round(p.x / 5) * 5).join(",");
|
|
370
|
+
let index = 0;
|
|
371
|
+
let paragraph = [];
|
|
372
|
+
let paragraphPage = lines[0]?.page ?? 1;
|
|
373
|
+
const flushParagraph = () => {
|
|
374
|
+
if (paragraph.length === 0)
|
|
375
|
+
return true;
|
|
376
|
+
const text = paragraph.join(" ").replace(/\s+/g, " ").trim();
|
|
377
|
+
paragraph = [];
|
|
378
|
+
return text === "" ? true : builder.push({ kind: "paragraph", text, page: paragraphPage });
|
|
379
|
+
};
|
|
380
|
+
while (index < lines.length) {
|
|
381
|
+
const line = lines[index];
|
|
382
|
+
if (!line)
|
|
383
|
+
break;
|
|
384
|
+
// A table candidate: consecutive lines with more than one column at matching x positions.
|
|
385
|
+
if (line.parts.length > 1) {
|
|
386
|
+
const key = xKey(line);
|
|
387
|
+
let run = 1;
|
|
388
|
+
while (index + run < lines.length) {
|
|
389
|
+
const next = lines[index + run];
|
|
390
|
+
if (!next || next.page !== line.page || xKey(next) !== key)
|
|
391
|
+
break;
|
|
392
|
+
run += 1;
|
|
393
|
+
}
|
|
394
|
+
if (run >= 3) {
|
|
395
|
+
if (!flushParagraph())
|
|
396
|
+
return;
|
|
397
|
+
const rows = lines.slice(index, index + run).map((l) => l.parts.map((p) => p.text.trim()));
|
|
398
|
+
// The header claim needs evidence: a first row of non-empty cells in a different size or all
|
|
399
|
+
// non-numeric. Claiming a header that is really data mislabels every column.
|
|
400
|
+
const first = rows[0] ?? [];
|
|
401
|
+
const hasHeader = first.length > 0 &&
|
|
402
|
+
first.every((cell) => cell !== "") &&
|
|
403
|
+
first.some((cell) => !/^-?[\d.,%$€£\s]+$/.test(cell));
|
|
404
|
+
if (!builder.push({ kind: "table", rows, hasHeader, page: line.page }))
|
|
405
|
+
return;
|
|
406
|
+
index += run;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const text = joinLine(line);
|
|
411
|
+
if (text === "") {
|
|
412
|
+
index += 1;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const level = headingLevel(line.size);
|
|
416
|
+
if (level !== null) {
|
|
417
|
+
if (!flushParagraph())
|
|
418
|
+
return;
|
|
419
|
+
if (!builder.push({ kind: "heading", level, text, page: line.page }))
|
|
420
|
+
return;
|
|
421
|
+
index += 1;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const previous = lines[index - 1];
|
|
425
|
+
const gap = previous && previous.page === line.page ? previous.y - line.y : Number.POSITIVE_INFINITY;
|
|
426
|
+
// A gap much larger than the line height is a paragraph break; a normal one is a wrap, and joining
|
|
427
|
+
// wrapped lines is what turns a PDF back into sentences.
|
|
428
|
+
if (gap > line.size * 1.8 && !flushParagraph())
|
|
429
|
+
return;
|
|
430
|
+
if (paragraph.length === 0)
|
|
431
|
+
paragraphPage = line.page;
|
|
432
|
+
paragraph.push(text);
|
|
433
|
+
index += 1;
|
|
434
|
+
}
|
|
435
|
+
flushParagraph();
|
|
436
|
+
};
|
|
437
|
+
/** Pages, from the page-tree objects. `/Count` is authoritative when present; the object count is the fallback. */
|
|
438
|
+
const countPages = (raw) => {
|
|
439
|
+
const count = /\/Type\s*\/Pages[^>]*?\/Count\s+(\d+)/.exec(raw);
|
|
440
|
+
if (count?.[1] !== undefined)
|
|
441
|
+
return Number(count[1]);
|
|
442
|
+
return (raw.match(/\/Type\s*\/Page[^s]/g) ?? []).length || 1;
|
|
443
|
+
};
|
|
444
|
+
export const parsePdf = (bytes, limits) => {
|
|
445
|
+
const raw = ascii(bytes);
|
|
446
|
+
if (!raw.startsWith("%PDF-"))
|
|
447
|
+
return { reason: "malformed", message: "That file does not begin with a PDF header." };
|
|
448
|
+
// Before anything is decoded: extracting from an encrypted document would mean implementing the security
|
|
449
|
+
// handler, which is a decryption tool rather than a parser.
|
|
450
|
+
if (/\/Encrypt\s/.test(raw))
|
|
451
|
+
return {
|
|
452
|
+
reason: "encrypted",
|
|
453
|
+
message: "That PDF is encrypted. Remove its password protection and attach it again.",
|
|
454
|
+
};
|
|
455
|
+
const pageCount = countPages(raw);
|
|
456
|
+
if (pageCount > limits.maxPages)
|
|
457
|
+
return {
|
|
458
|
+
reason: "too-many-pages",
|
|
459
|
+
// The limit is named, not implied. "Too long" sends someone to guess where to split it.
|
|
460
|
+
message: `That PDF has ${pageCount} pages and the limit is ${limits.maxPages}. Split it and attach the part you need.`,
|
|
461
|
+
};
|
|
462
|
+
const { streams, warnings, hitDecompressionLimit } = contentStreams(raw, limits);
|
|
463
|
+
const builder = createBlockBuilder(limits);
|
|
464
|
+
for (const warning of warnings)
|
|
465
|
+
builder.warn(warning);
|
|
466
|
+
const fragments = [];
|
|
467
|
+
// Streams are found in file order, which is page order in every PDF a normal tool writes. The page number
|
|
468
|
+
// is therefore approximate for an unusual layout, and it is metadata on a block rather than something the
|
|
469
|
+
// extraction depends on.
|
|
470
|
+
streams.forEach((stream, i) => {
|
|
471
|
+
if (fragments.length > limits.maxBlocks * 4)
|
|
472
|
+
return;
|
|
473
|
+
readFragments(stream, Math.min(i + 1, pageCount), fragments);
|
|
474
|
+
});
|
|
475
|
+
if (fragments.length === 0) {
|
|
476
|
+
// Which absence this is matters, and the two answers send a user to different places. A document whose
|
|
477
|
+
// streams were too big to decompress is `too-large` -- reporting it as a scan would send someone to run
|
|
478
|
+
// OCR on a file whose problem is its size. Found by sabotage: the bomb case was reported as a scan.
|
|
479
|
+
if (hitDecompressionLimit)
|
|
480
|
+
return {
|
|
481
|
+
reason: "too-large",
|
|
482
|
+
message: "That PDF's compressed content expands past the extraction limit and could not be read.",
|
|
483
|
+
};
|
|
484
|
+
return {
|
|
485
|
+
reason: "no-text-layer",
|
|
486
|
+
// The distinction that matters: this is a scan, not a broken file.
|
|
487
|
+
message: "That PDF has no extractable text — it is most likely a scan. It needs optical character recognition.",
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
const joined = fragments.map((f) => f.text).join("");
|
|
491
|
+
if (looksLikeGlyphIndices(joined))
|
|
492
|
+
builder.warn("The text may be garbled: this PDF uses embedded fonts whose character map could not be applied.");
|
|
493
|
+
groupIntoBlocks(fragments, builder);
|
|
494
|
+
if (builder.done().blocks.length === 0)
|
|
495
|
+
return {
|
|
496
|
+
reason: "no-text-layer",
|
|
497
|
+
message: "That PDF's text could not be reconstructed into readable content.",
|
|
498
|
+
};
|
|
499
|
+
return { ...builder.done(), pageCount };
|
|
500
|
+
};
|
|
501
|
+
export const createPdfDocumentParser = () => ({
|
|
502
|
+
id: "pdf",
|
|
503
|
+
mediaTypes: ["application/pdf"],
|
|
504
|
+
async parse({ bytes, limits }) {
|
|
505
|
+
return parsePdf(bytes, limits);
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
//# sourceMappingURL=pdf.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text, Markdown, CSV and JSON extraction (#131).
|
|
3
|
+
*
|
|
4
|
+
* The easy formats, and worth doing properly rather than passing straight through. A CSV handed to a model as
|
|
5
|
+
* one long string is the flattening this whole module exists to avoid — the row and column that give a number
|
|
6
|
+
* its meaning are exactly what gets lost. So a CSV becomes a `table` block with its cells intact, Markdown
|
|
7
|
+
* headings keep their level, and pipe tables survive as tables.
|
|
8
|
+
*
|
|
9
|
+
* Everything here is bounded the same way the PDF parser is, because "it's only text" is how a 200 MB CSV
|
|
10
|
+
* occupies a worker.
|
|
11
|
+
*/
|
|
12
|
+
import type { DocumentBlock, DocumentParser, ExtractedDocument, ExtractionFailure, ExtractionLimits } from "../index.js";
|
|
13
|
+
/**
|
|
14
|
+
* Accumulates blocks against the limits, so every parser stops the same way.
|
|
15
|
+
*
|
|
16
|
+
* Shared rather than repeated: the two parsers in this module and the PDF one all need the same three
|
|
17
|
+
* ceilings, and three copies of "am I over budget" is three chances for one of them to be wrong.
|
|
18
|
+
*/
|
|
19
|
+
export declare const createBlockBuilder: (limits: ExtractionLimits) => {
|
|
20
|
+
/** Returns false once full, so a caller can stop reading rather than keep parsing into a bin. */
|
|
21
|
+
push(block: DocumentBlock): boolean;
|
|
22
|
+
warn(message: string): void;
|
|
23
|
+
readonly truncated: boolean;
|
|
24
|
+
done(pageCount?: number): ExtractedDocument;
|
|
25
|
+
};
|
|
26
|
+
export type BlockBuilder = ReturnType<typeof createBlockBuilder>;
|
|
27
|
+
/**
|
|
28
|
+
* Markdown, and plain text as its degenerate case.
|
|
29
|
+
*
|
|
30
|
+
* Plain text has no headings or tables, so running it through the same parser costs nothing and means one
|
|
31
|
+
* code path instead of two that differ in how they split paragraphs.
|
|
32
|
+
*/
|
|
33
|
+
export declare const parseMarkdown: (bytes: Uint8Array, limits: ExtractionLimits) => ExtractedDocument;
|
|
34
|
+
/**
|
|
35
|
+
* A CSV row splitter that understands quotes.
|
|
36
|
+
*
|
|
37
|
+
* Not `line.split(",")`. A quoted field can contain a comma, a newline and an escaped quote, and a splitter
|
|
38
|
+
* that ignores that silently shifts every subsequent column — the worst possible failure for a table, because
|
|
39
|
+
* the result is plausible.
|
|
40
|
+
*/
|
|
41
|
+
export declare const parseDelimited: (bytes: Uint8Array, limits: ExtractionLimits, delimiter?: string) => ExtractedDocument;
|
|
42
|
+
/**
|
|
43
|
+
* JSON as a document.
|
|
44
|
+
*
|
|
45
|
+
* An array of flat objects is a table and is by far the most common shape a JSON attachment has, so it is
|
|
46
|
+
* extracted as one. Anything else is rendered as indented text: still readable, and honest about the fact
|
|
47
|
+
* that there was no table to find.
|
|
48
|
+
*/
|
|
49
|
+
export declare const parseJsonDocument: (bytes: Uint8Array, limits: ExtractionLimits) => ExtractedDocument | ExtractionFailure;
|
|
50
|
+
/**
|
|
51
|
+
* The parser for every text-shaped type.
|
|
52
|
+
*
|
|
53
|
+
* One parser dispatching on the type it was handed, rather than five parsers sharing a builder. The formats
|
|
54
|
+
* differ only in how a line is read; the bounds, the decoding and the block shapes are identical, and five
|
|
55
|
+
* copies of those is five chances for one to be wrong.
|
|
56
|
+
*/
|
|
57
|
+
export declare const TEXT_MEDIA_TYPES: readonly ["text/plain", "text/markdown", "text/x-markdown", "text/csv", "text/tab-separated-values", "application/json"];
|
|
58
|
+
export declare const createTextDocumentParser: () => DocumentParser;
|
|
59
|
+
//# sourceMappingURL=text.d.ts.map
|