@semiont/content 0.5.24 → 0.5.25
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 +55 -11
- package/dist/index.d.ts +328 -31
- package/dist/index.js +780 -61
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -87,25 +87,69 @@ When the project has `[git] sync = true` in `.semiont/config`, the store keeps t
|
|
|
87
87
|
|
|
88
88
|
Every method accepts `{ noGit: true }` to skip staging for a single call. Without git sync, the store falls back to plain filesystem operations.
|
|
89
89
|
|
|
90
|
-
## PDF
|
|
90
|
+
## PDF Extraction
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
`EXTRACTORS['pdf-text-layer']` turns a PDF into text plus the geometry that
|
|
93
|
+
indexes it, routing by what the document actually holds:
|
|
94
|
+
|
|
95
|
+
| Class | Document | Read by |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| A | native text layer | pdf.js, directly |
|
|
98
|
+
| B | scanned — pixels only | OCR (Tesseract) |
|
|
99
|
+
| C | hybrid — some pages scanned | both; pages still unread are reported |
|
|
100
|
+
| D | tables | grid pages rewritten as markdown rows |
|
|
101
|
+
| E | forms | AcroForm values folded in, anchored to their widgets |
|
|
102
|
+
| F / G | encrypted, corrupt | declined by name, from the parser error |
|
|
93
103
|
|
|
94
104
|
```typescript
|
|
95
|
-
import {
|
|
105
|
+
import { EXTRACTORS } from '@semiont/content';
|
|
106
|
+
import { textExtractionOf, locate } from '@semiont/core';
|
|
96
107
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
console.log(layer.text); // Full extracted text
|
|
100
|
-
console.log(layer.pages.length); // Page dimensions in PDF points
|
|
108
|
+
const extracted = await EXTRACTORS[textExtractionOf('application/pdf')]!
|
|
109
|
+
.extract(pdfBytes, 'application/pdf');
|
|
101
110
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
//
|
|
111
|
+
if (!('declined' in extracted)) {
|
|
112
|
+
extracted.text; // reading-order text
|
|
113
|
+
extracted.items; // positioned runs indexing it
|
|
114
|
+
extracted.method; // 'pdf-text-layer' | 'ocr' | 'table' | 'form'
|
|
115
|
+
extracted.unreadPages; // class C: pages no reader could recover
|
|
105
116
|
}
|
|
106
117
|
```
|
|
107
118
|
|
|
108
|
-
|
|
119
|
+
A decline is named (`'no-text-layer' | 'encrypted' | 'corrupt' | 'too-large'`)
|
|
120
|
+
rather than a bare null, so a caller can settle with the reason.
|
|
121
|
+
`'no-text-layer'` means recognition ran and came up empty — not that it was
|
|
122
|
+
never attempted.
|
|
123
|
+
|
|
124
|
+
`extractPdfTextLayer()` is the lower-level reader underneath class A, returning
|
|
125
|
+
`null` for a document with no text operators anywhere.
|
|
126
|
+
|
|
127
|
+
Coordinates are PDF points, origin bottom-left; the Y-flip to canvas pixels
|
|
128
|
+
happens in the browser. The vocabulary these produce — `AnchoredText`,
|
|
129
|
+
`PdfTextItem` — and the `locate` / `textUnder` pair that reads it are exported
|
|
130
|
+
from [`@semiont/core`](../core/README.md), so the browser can reason over
|
|
131
|
+
geometry without importing this package's extraction stack.
|
|
132
|
+
|
|
133
|
+
## Anchored-text store
|
|
134
|
+
|
|
135
|
+
OCR costs ~2.9 s per scanned page and six consumers read the same document, so
|
|
136
|
+
what the engine produced is kept rather than re-derived:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
import { createAnchoredTextStore } from '@semiont/content';
|
|
140
|
+
|
|
141
|
+
const store = createAnchoredTextStore(dir, logger);
|
|
142
|
+
await store.write(checksum, { text, items });
|
|
143
|
+
const map = await store.read(checksum); // null on any miss
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Derived values only, keyed by content checksum and stamped with the versions of
|
|
147
|
+
this package, the engine and its traineddata. A stamp mismatch, a corrupt file
|
|
148
|
+
and an absent one are all the same answer: a miss. The store may make things
|
|
149
|
+
faster, never make them fail.
|
|
150
|
+
|
|
151
|
+
See **[ANCHORING.md](../../docs/system/ANCHORING.md)** for the pipeline this
|
|
152
|
+
sits in.
|
|
109
153
|
|
|
110
154
|
## Utilities
|
|
111
155
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SemiontProject } from '@semiont/core/node';
|
|
2
|
-
import { Logger, SupportedMediaType,
|
|
2
|
+
import { Logger, SupportedMediaType, ExtractionOutcome, PdfTextItem, TextExtraction, IContentTransport, AnchoredText } from '@semiont/core';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* WorkingTreeStore - Manages files in the project working tree
|
|
@@ -165,6 +165,299 @@ declare function calculateChecksum(content: string | Buffer): string;
|
|
|
165
165
|
*/
|
|
166
166
|
declare function verifyChecksum(content: string | Buffer, checksum: string): boolean;
|
|
167
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Anchored-text cache — the persistent half of ANCHORED-TEXT-CACHE.md Lane 2.
|
|
170
|
+
*
|
|
171
|
+
* OCR costs ~2.9 s per scanned page, and six passes read the same document (five
|
|
172
|
+
* detection motivations plus the smelter's embed), each its own job in its own
|
|
173
|
+
* process. This stores what the engine produced so only the first pass pays.
|
|
174
|
+
*
|
|
175
|
+
* **Derived values only.** Everything here is reproducible from the source
|
|
176
|
+
* bytes, which is what makes a stamp miss safe. An authored coordinate map is
|
|
177
|
+
* embedded in the PDF Semiont generated, not stored alongside one — see
|
|
178
|
+
* `PDF-GENERATION.md`, which owns that decision and states the negative:
|
|
179
|
+
* never this store.
|
|
180
|
+
*
|
|
181
|
+
* The seam is `extract()` (PERSIST-ANCHORS D1/P2b): the record is the FINISHED
|
|
182
|
+
* extraction outcome — classification, geometry, provenance, or a named
|
|
183
|
+
* decline — so a hit skips the native parse and the engine both, and every
|
|
184
|
+
* geometry-yielding extraction stores an entry, native documents included.
|
|
185
|
+
* That is what makes the anchored-text endpoint answer for every resource
|
|
186
|
+
* whose extraction yields geometry, and what lets the reconcile planner treat
|
|
187
|
+
* "no entry under the current checksum" as work (P0's third drift class).
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
/** The two halves of the wire record, split for storage. */
|
|
191
|
+
type SuccessOutcome = Exclude<ExtractionOutcome, {
|
|
192
|
+
declined: string;
|
|
193
|
+
}>;
|
|
194
|
+
type DeclineOutcome = Extract<ExtractionOutcome, {
|
|
195
|
+
declined: string;
|
|
196
|
+
}>;
|
|
197
|
+
/**
|
|
198
|
+
* One line of recognized text: the geometry every word on it shares, plus the
|
|
199
|
+
* per-word parts that differ.
|
|
200
|
+
*
|
|
201
|
+
* Grouping is by *contiguous runs* of equal `(y, h)`, never by scanning for all
|
|
202
|
+
* items at a given y. That makes the codec lossless and order-preserving for
|
|
203
|
+
* any input — compression is the only thing that depends on words actually
|
|
204
|
+
* arriving in reading order, and correctness never is.
|
|
205
|
+
*
|
|
206
|
+
* Sharing `y`/`h` is measured-safe rather than assumed: within-line word-height
|
|
207
|
+
* spread is 0.0pt in both native and OCR'd output, because the engine already
|
|
208
|
+
* normalizes word boxes to the line. Per-word `x` and `width` are stored
|
|
209
|
+
* explicitly and NOT derived from neighbouring split positions — deriving width
|
|
210
|
+
* from the gap to the next word would widen every box to touch its neighbour,
|
|
211
|
+
* which would silently change the coverage arithmetic `textUnder` is calibrated
|
|
212
|
+
* on (RUN_COVERAGE_THRESHOLD, tuned against ink-tight boxes).
|
|
213
|
+
*/
|
|
214
|
+
interface CachedLine {
|
|
215
|
+
/** 1-indexed page. */
|
|
216
|
+
p: number;
|
|
217
|
+
/** PDF points, bottom-left origin — shared by every word on the line. */
|
|
218
|
+
y: number;
|
|
219
|
+
h: number;
|
|
220
|
+
/** `[x, width, start, end]` per word; offsets index `CachedAnchoredText.text`. */
|
|
221
|
+
words: [number, number, number, number][];
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The stored record: one extraction OUTCOME for the whole resource
|
|
225
|
+
* (PERSIST-ANCHORS decision D1) — the anchored text with its provenance
|
|
226
|
+
* (`method`, `pdfClass`, `ocrConfidence`, `unreadPages`), or a named decline.
|
|
227
|
+
*
|
|
228
|
+
* Whole-resource on every side, deliberately. The producer's own shape is a
|
|
229
|
+
* per-page map, but that is an artifact of how `ocrPages` iterates, and letting
|
|
230
|
+
* it reach storage would have forced every consumer — the transport, the
|
|
231
|
+
* browser, a headless client — to reassemble pages it never asked to see.
|
|
232
|
+
*
|
|
233
|
+
* The `ocrConfidence` SUMMARY is stored (v2) — this repairs the regression
|
|
234
|
+
* OCR-CONFIDENCE-LOST.md records, where a hit answered with no confidence at
|
|
235
|
+
* all. Per-word confidences remain unstored: the summary is the record's
|
|
236
|
+
* quality provenance; the word list is operator log detail.
|
|
237
|
+
*
|
|
238
|
+
* v1 records (bare `{ text, lines }`, no provenance) read as misses under the
|
|
239
|
+
* v2 prefix; the reconcile planner's third drift class re-derives them.
|
|
240
|
+
*/
|
|
241
|
+
type CachedAnchoredText = ({
|
|
242
|
+
v: 2;
|
|
243
|
+
/** Engine + traineddata + our assembly code. A mismatch is a clean miss. */
|
|
244
|
+
stamp: string;
|
|
245
|
+
text: string;
|
|
246
|
+
lines: CachedLine[];
|
|
247
|
+
} & Omit<SuccessOutcome, 'text' | 'items'>) | ({
|
|
248
|
+
v: 2;
|
|
249
|
+
stamp: string;
|
|
250
|
+
} & DeclineOutcome);
|
|
251
|
+
interface AnchoredTextStore {
|
|
252
|
+
/**
|
|
253
|
+
* The stored map for this key, or null for any miss. Never throws.
|
|
254
|
+
*
|
|
255
|
+
* The key is the **content checksum of the bytes the map derives from**
|
|
256
|
+
* (PERSIST-ANCHORS decision A): a representation is its bytes, so the
|
|
257
|
+
* checksum is its identity, and geometry derived from one revision of the
|
|
258
|
+
* bytes is unreachable by a reader holding a different revision — by
|
|
259
|
+
* construction, not by invalidation. Callers holding some other handle
|
|
260
|
+
* (a resource id) reach the artifact through an index, not by a second
|
|
261
|
+
* key scheme here.
|
|
262
|
+
*/
|
|
263
|
+
read(key: string): Promise<ExtractionOutcome | null>;
|
|
264
|
+
/** Record an extraction outcome under the content checksum of its source
|
|
265
|
+
* bytes. A store that cannot write is still a store. */
|
|
266
|
+
write(key: string, outcome: ExtractionOutcome): Promise<void>;
|
|
267
|
+
/**
|
|
268
|
+
* Every key `read()` would currently HIT — entries under a stale stamp or
|
|
269
|
+
* unreadable files are excluded, exactly as `read()` would exclude them.
|
|
270
|
+
* That equivalence is load-bearing: the reconcile planner treats a listed
|
|
271
|
+
* key as "artifact present" and plans re-derivation for the rest
|
|
272
|
+
* (PERSIST-ANCHORS P0, the third drift class), so a key listed here but
|
|
273
|
+
* missed by `read()` would be a permanent loss the diff can never see —
|
|
274
|
+
* the exact shape of the post-engine-upgrade hole this filter closes.
|
|
275
|
+
* One bulk call per reconcile, never a probe per resource. Never throws.
|
|
276
|
+
*/
|
|
277
|
+
list(): Promise<string[]>;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* A file-backed store under `dir` — one file per content key, sharded as
|
|
281
|
+
* `{ab}/{cd}/{key}.json` via the same `getShardPath` the event log uses
|
|
282
|
+
* (PERSIST-ANCHORS decision E). Same convention, separate tree: `.semiont/`
|
|
283
|
+
* is the KB's committed system of record; everything here is derived,
|
|
284
|
+
* reclaimable, and never a source of truth.
|
|
285
|
+
*
|
|
286
|
+
* `dir` is the caller's, out of `Project.anchoredTextDir`: this package has no idea
|
|
287
|
+
* which project it is serving. Every failure path is a miss rather than an
|
|
288
|
+
* error, matching the rule extraction already follows for unreadable pages —
|
|
289
|
+
* the cache may make things faster, never make them fail.
|
|
290
|
+
*/
|
|
291
|
+
declare function createAnchoredTextStore(dir: string, logger?: Logger): AnchoredTextStore;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* ContentExtractor — strategy-keyed text extraction for embedding.
|
|
295
|
+
*
|
|
296
|
+
* The registry is keyed by `TextExtraction` from `@semiont/core` — the
|
|
297
|
+
* media-type registry's dispatch vocabulary — never by a second media-type
|
|
298
|
+
* list (SMELTER-MEDIA-TYPES.md, Design §1): there is exactly one media-type
|
|
299
|
+
* table in the system, and this registry consumes it. The Smelter resolves
|
|
300
|
+
* `textExtractionOf(contentType)` and looks the extractor up by strategy; a
|
|
301
|
+
* `null` slot means decline (settle skipped, reason 'no-extractor').
|
|
302
|
+
*
|
|
303
|
+
* Extraction is ephemeral: `extract` runs at read time, its output feeds the
|
|
304
|
+
* chunker, and is discarded — no stored derived representation. Annotations
|
|
305
|
+
* anchor to native geometry (`items`), never to extracted-text offsets, so
|
|
306
|
+
* re-extraction can never break an anchor.
|
|
307
|
+
*/
|
|
308
|
+
|
|
309
|
+
interface ExtractedText {
|
|
310
|
+
/** Reading-order plain text, ready for the chunker. */
|
|
311
|
+
text: string;
|
|
312
|
+
/**
|
|
313
|
+
* Positioned text runs indexing `text`, for callers that anchor; absent for
|
|
314
|
+
* pure text, where character offsets are the anchor. Named `items` to match
|
|
315
|
+
* `AnchoredText`/`PdfTextLayer` — one concept, one name, and no collision
|
|
316
|
+
* with the OCR engine's own "blocks" (which are page regions, not runs).
|
|
317
|
+
*/
|
|
318
|
+
items?: PdfTextItem[];
|
|
319
|
+
method: 'text-passthrough' | 'pdf-text-layer' | 'table' | 'form' | 'ocr';
|
|
320
|
+
pdfClass?: 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G';
|
|
321
|
+
/**
|
|
322
|
+
* How well the engine read the pixels, when any of this text came from OCR.
|
|
323
|
+
*
|
|
324
|
+
* Extraction quality, deliberately NOT anchor confidence: the two answer
|
|
325
|
+
* different questions. `AnchorConfidence` asks whether the renderer
|
|
326
|
+
* relocated a stored span in the current text, and for a PDF the answer is
|
|
327
|
+
* always "exactly" — the viewrect is absolute. This asks whether the glyphs
|
|
328
|
+
* under that box were read correctly, which no client can recompute.
|
|
329
|
+
* Reported for operators rather than stored on annotations, following the
|
|
330
|
+
* existing rule that anchor-audit detail belongs in logs.
|
|
331
|
+
*/
|
|
332
|
+
ocrConfidence?: {
|
|
333
|
+
/** Mean per-word confidence, 0–100. */
|
|
334
|
+
mean: number;
|
|
335
|
+
/** Words the engine was unsure of — the number worth acting on. */
|
|
336
|
+
lowConfidenceWords: number;
|
|
337
|
+
totalWords: number;
|
|
338
|
+
};
|
|
339
|
+
/**
|
|
340
|
+
* 1-indexed pages this extraction could not read — present only when a
|
|
341
|
+
* document is partially covered (class C). Naming the gap is the point:
|
|
342
|
+
* without it a hybrid document embeds its native pages and says nothing
|
|
343
|
+
* about the rest, so coverage silently overstates what search can see.
|
|
344
|
+
* This is the work list OCR consumes.
|
|
345
|
+
*/
|
|
346
|
+
unreadPages?: number[];
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* A named decline — an extractor that ran and decided it cannot yield text
|
|
350
|
+
* says why, so the settled signal can carry the class reason (a bare null
|
|
351
|
+
* could not name its class; SMELTER-MEDIA-TYPES Phase 0 log, note a).
|
|
352
|
+
*/
|
|
353
|
+
interface ExtractionDecline {
|
|
354
|
+
declined: 'no-text-layer' | 'encrypted' | 'corrupt' | 'too-large';
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Where a strategy may reuse an earlier recognition, and under what key.
|
|
358
|
+
*
|
|
359
|
+
* The caller supplies the key, and derives it from the bytes it actually
|
|
360
|
+
* holds — `calculateChecksum` over the same Buffer it passes to `extract()` —
|
|
361
|
+
* never from a descriptor's claim. A catalog-derived key can race a byte
|
|
362
|
+
* change (bytes fetched at one moment, descriptor read at another) and file
|
|
363
|
+
* or read geometry under an identity that does not describe the bytes being
|
|
364
|
+
* extracted. The write path made recompute-over-claim the rule
|
|
365
|
+
* (PERSIST-ANCHORS P1b); readers mirror it (P1c). One SHA-256 over bytes
|
|
366
|
+
* already in memory is noise against the engine pass a hit avoids.
|
|
367
|
+
*
|
|
368
|
+
* Optional throughout: a caller that passes nothing extracts uncached and is
|
|
369
|
+
* unaffected. The seam is `extract()` itself (PERSIST-ANCHORS D1/P2b): a hit
|
|
370
|
+
* returns the FINISHED outcome — classification, geometry, provenance, or a
|
|
371
|
+
* named decline — so neither the native parse nor the engine runs. Every
|
|
372
|
+
* geometry-yielding extraction produces an entry, native documents included;
|
|
373
|
+
* the 'decode' strategy ignores the cache (no geometry, nothing expensive).
|
|
374
|
+
*/
|
|
375
|
+
interface ExtractionCache {
|
|
376
|
+
key: string;
|
|
377
|
+
store: AnchoredTextStore;
|
|
378
|
+
}
|
|
379
|
+
interface ContentExtractor {
|
|
380
|
+
/**
|
|
381
|
+
* Whether this strategy's extractions carry positioned runs (`items`) — the
|
|
382
|
+
* geometry an anchored-text artifact is made of. Declared, not probed:
|
|
383
|
+
* the reconcile planner must know "should an artifact exist?" without
|
|
384
|
+
* running the extractor (PERSIST-ANCHORS P0, the third drift class), and
|
|
385
|
+
* the declaration keeps the planner's gate and the live fetch's behavior
|
|
386
|
+
* twins by construction. Text strategies anchor by character offset and
|
|
387
|
+
* declare false.
|
|
388
|
+
*/
|
|
389
|
+
yieldsGeometry: boolean;
|
|
390
|
+
/**
|
|
391
|
+
* Extract embeddable/annotatable text, or decline with the class reason
|
|
392
|
+
* (scanned-without-OCR, encrypted, corrupt). The caller skips embedding
|
|
393
|
+
* and settles skipped with that reason.
|
|
394
|
+
*/
|
|
395
|
+
extract(content: Buffer, mediaType: string, cache?: ExtractionCache): Promise<ExtractedText | ExtractionDecline>;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Strategy → extractor. A `null` slot is a decline: the strategy names a
|
|
399
|
+
* capability nothing currently provides ('none' permanently).
|
|
400
|
+
*/
|
|
401
|
+
declare const EXTRACTORS: Record<TextExtraction, ContentExtractor | null>;
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* PDF extractor — the 'pdf-text-layer' strategy (SMELTER-MEDIA-TYPES).
|
|
405
|
+
*
|
|
406
|
+
* Wraps the shared `extractPdfTextLayer` reader (detection's other consumer)
|
|
407
|
+
* and turns a PDF into text plus the geometry that indexes it, by class:
|
|
408
|
+
*
|
|
409
|
+
* A native text layer → read directly
|
|
410
|
+
* B scanned → read the page pixels by OCR
|
|
411
|
+
* C hybrid → both, with any page still unread reported
|
|
412
|
+
* D tables → grid pages rewritten as markdown rows
|
|
413
|
+
* E forms → AcroForm values folded in, anchored to widgets
|
|
414
|
+
* F/G encrypted, corrupt → declined by name, from the parser error
|
|
415
|
+
*
|
|
416
|
+
* Everything runs inline. OCR was originally planned off the hot path, but
|
|
417
|
+
* the Smelter's lanes are per-resource and concurrent, so a slow page delays
|
|
418
|
+
* only its own resource — see SMELTER-MEDIA-TYPES Design §4 (revised).
|
|
419
|
+
*/
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Largest PDF this will attempt, in bytes.
|
|
423
|
+
*
|
|
424
|
+
* A PDF is a compressed container, so input size bounds nothing on its own —
|
|
425
|
+
* but it is the one number available before the parser touches the file, and
|
|
426
|
+
* refusing here means a hostile or pathological document never gets to expand
|
|
427
|
+
* inside pdf.js. Chosen to sit above real corpora (a few hundred pages of
|
|
428
|
+
* scanned FOIA material runs tens of megabytes) while still being a ceiling.
|
|
429
|
+
*
|
|
430
|
+
* A starting point, not a measured optimum — revisit against a real corpus
|
|
431
|
+
* (SMELTER-MEDIA-TYPES, live-testing follow-up). The per-image budget in
|
|
432
|
+
* `pdf-page-images` guards the decoded side, which is where the unbounded
|
|
433
|
+
* growth actually lives.
|
|
434
|
+
*/
|
|
435
|
+
declare const MAX_PDF_BYTES: number;
|
|
436
|
+
/** Whether a document is small enough to attempt. Exported because the
|
|
437
|
+
* threshold is a judgement, and judgements deserve tests that do not have to
|
|
438
|
+
* materialize two hundred megabytes to ask the question. */
|
|
439
|
+
declare function withinByteBudget(bytes: number): boolean;
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* `AnchoredTextStore` over `IContentTransport` — how an out-of-process
|
|
443
|
+
* extraction seam reaches the one real store (PERSIST-ANCHORS P2c).
|
|
444
|
+
*
|
|
445
|
+
* Every cache consumer runs outside the backend — the smelter worker and the
|
|
446
|
+
* detection workers — while the KnowledgeSystem owns the storage. This
|
|
447
|
+
* adapter maps the store contract onto the transport's three
|
|
448
|
+
* checksum-addressed calls, so `ExtractionCache { key, store }` works
|
|
449
|
+
* identically in-process (LocalContentTransport → the store directly) and
|
|
450
|
+
* over the wire (HttpContentTransport → the /anchored-text routes).
|
|
451
|
+
*
|
|
452
|
+
* It honors the store contract's failure rule — the cache may make things
|
|
453
|
+
* faster, never make them fail: a read or list failure is a miss, a write
|
|
454
|
+
* failure is swallowed (debug-logged). Callers that need a write to be LOUD
|
|
455
|
+
* — the re-anchor path, whose artifact IS the job — use the transport's
|
|
456
|
+
* `putAnchoredText` directly, not this adapter.
|
|
457
|
+
*/
|
|
458
|
+
|
|
459
|
+
declare function anchoredTextStoreOverTransport(content: IContentTransport, logger?: Logger): AnchoredTextStore;
|
|
460
|
+
|
|
168
461
|
/**
|
|
169
462
|
* PDF Text Layer Types
|
|
170
463
|
*
|
|
@@ -173,37 +466,58 @@ declare function verifyChecksum(content: string | Buffer, checksum: string): boo
|
|
|
173
466
|
* (Y increases upward). The Y-flip to canvas pixels happens downstream in the
|
|
174
467
|
* browser; the server has no canvas.
|
|
175
468
|
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
469
|
+
* The anchoring vocabulary these build on — `AnchoredText`, `PdfTextItem`,
|
|
470
|
+
* `PdfCoordinate`, and the `locate`/`textUnder` pair — lives in `@semiont/core`
|
|
471
|
+
* alongside the viewrect FragmentSelector codec, so the browser can reason over
|
|
472
|
+
* geometry without importing this package's extraction stack.
|
|
178
473
|
*/
|
|
474
|
+
|
|
179
475
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
476
|
+
* One filled AcroForm field: the value a form carries outside its drawn text
|
|
477
|
+
* layer. Geometry is the widget's rectangle, in the same PDF-point,
|
|
478
|
+
* bottom-left-origin space as `PdfTextItem` — so a consumer that folds field
|
|
479
|
+
* values into text can anchor them like any other run.
|
|
182
480
|
*/
|
|
183
|
-
interface
|
|
184
|
-
|
|
185
|
-
|
|
481
|
+
interface PdfFormField {
|
|
482
|
+
name: string;
|
|
483
|
+
value: string;
|
|
186
484
|
page: number;
|
|
187
485
|
x: number;
|
|
188
486
|
y: number;
|
|
189
487
|
width: number;
|
|
190
488
|
height: number;
|
|
191
489
|
}
|
|
192
|
-
/** Page dimensions in PDF points */
|
|
490
|
+
/** Page dimensions in PDF points, plus the page's span of `PdfTextLayer.text` */
|
|
193
491
|
interface PdfPageInfo {
|
|
194
492
|
pageNumber: number;
|
|
195
493
|
widthPt: number;
|
|
196
494
|
heightPt: number;
|
|
495
|
+
textStart: number;
|
|
496
|
+
textEnd: number;
|
|
497
|
+
/**
|
|
498
|
+
* Whether this page carries text-showing operators. False means the page
|
|
499
|
+
* is scanned: its characters exist only as pixels, so reading it needs
|
|
500
|
+
* OCR rather than `getTextContent`. Per-PAGE, deliberately: a document
|
|
501
|
+
* mixing native and scanned pages (class C) must route each page
|
|
502
|
+
* separately, and a single document-level flag cannot express that.
|
|
503
|
+
*/
|
|
504
|
+
hasTextLayer: boolean;
|
|
197
505
|
}
|
|
198
506
|
/**
|
|
199
507
|
* The full extracted text layer for a PDF.
|
|
200
508
|
* `text` is the reading-order concatenation across all pages.
|
|
201
509
|
* Each `item` is one text run carrying its character range into `text` plus PDF-point geometry.
|
|
202
510
|
*/
|
|
203
|
-
interface PdfTextLayer {
|
|
511
|
+
interface PdfTextLayer extends AnchoredText {
|
|
204
512
|
pages: PdfPageInfo[];
|
|
205
|
-
|
|
206
|
-
|
|
513
|
+
/**
|
|
514
|
+
* Filled AcroForm field values, empty for a document without a form.
|
|
515
|
+
* Deliberately NOT folded into `text`: the reader reports what the
|
|
516
|
+
* document holds, and each consumer projects what it needs — detection
|
|
517
|
+
* reads `text`/`items` and is unaffected by a form's presence, while the
|
|
518
|
+
* embedding extractor folds these in (SMELTER-MEDIA-TYPES class E).
|
|
519
|
+
*/
|
|
520
|
+
fields: PdfFormField[];
|
|
207
521
|
}
|
|
208
522
|
|
|
209
523
|
/**
|
|
@@ -218,22 +532,5 @@ interface PdfTextLayer {
|
|
|
218
532
|
|
|
219
533
|
declare function extractPdfTextLayer(bytes: Uint8Array | Buffer): Promise<PdfTextLayer | null>;
|
|
220
534
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
* (single-line or multi-line).
|
|
224
|
-
*
|
|
225
|
-
* Finds all overlapping items [start, end), groups them by page and line, and
|
|
226
|
-
* records one bounding rectangle per line as a PdfCoordinate.
|
|
227
|
-
*
|
|
228
|
-
* Returns both the per-line `rects` and the `overlap` items they were computed
|
|
229
|
-
* from — so a caller that also needs the covered text (e.g. buildPdfAnnotation's
|
|
230
|
-
* geometry↔text containment invariant) reuses this single `layer.items` scan
|
|
231
|
-
* instead of re-filtering. Both arrays are empty if no item overlaps the span.
|
|
232
|
-
*/
|
|
233
|
-
declare function locate(layer: PdfTextLayer, start: number, end: number): {
|
|
234
|
-
rects: PdfCoordinate[];
|
|
235
|
-
overlap: PdfTextItem[];
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
export { ChecksumMismatchError, WorkingTreeStore, calculateChecksum, deriveStorageUri, extractPdfTextLayer, locate, verifyChecksum };
|
|
239
|
-
export type { PdfPageInfo, PdfTextItem, PdfTextLayer, StoredResource };
|
|
535
|
+
export { ChecksumMismatchError, EXTRACTORS, MAX_PDF_BYTES, WorkingTreeStore, anchoredTextStoreOverTransport, calculateChecksum, createAnchoredTextStore, deriveStorageUri, extractPdfTextLayer, verifyChecksum, withinByteBudget };
|
|
536
|
+
export type { AnchoredTextStore, CachedAnchoredText, CachedLine, ContentExtractor, ExtractedText, ExtractionCache, ExtractionDecline, PdfFormField, PdfPageInfo, PdfTextLayer, StoredResource };
|