docling-serve-client 0.4.0 → 0.5.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 +270 -29
- package/dist/capabilities.d.ts +10 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/core/input.d.ts +1 -1
- package/dist/core/pdf.d.ts +2 -2
- package/dist/core/validate.d.ts +2 -2
- package/dist/core/validate.d.ts.map +1 -1
- package/dist/images.d.ts +6 -3
- package/dist/images.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +146 -52
- package/dist/index.js.map +11 -10
- package/dist/options.d.ts +15 -5
- package/dist/options.d.ts.map +1 -1
- package/dist/types.d.ts +123 -9
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/capabilities.ts +109 -0
- package/src/client.ts +132 -65
- package/src/core/input.ts +3 -3
- package/src/core/pdf.ts +2 -2
- package/src/core/validate.ts +2 -2
- package/src/images.ts +4 -4
- package/src/index.ts +8 -0
- package/src/options.ts +23 -7
- package/src/response.ts +1 -1
- package/src/types.ts +136 -10
package/src/client.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Copyright 2026 Martin Winkler
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
+
import { readCapabilities } from './capabilities.js';
|
|
4
5
|
import { errorMessage, fetchJson } from './core/http.js';
|
|
5
6
|
import { detectInputFormat, type InputFormat } from './core/input.js';
|
|
6
7
|
import { findTruncatedXref } from './core/pdf.js';
|
|
@@ -23,12 +24,15 @@ import type {
|
|
|
23
24
|
ConvertInput,
|
|
24
25
|
DoclingClient,
|
|
25
26
|
DoclingConfig,
|
|
27
|
+
DoclingSettings,
|
|
26
28
|
DocumentJob,
|
|
27
|
-
|
|
29
|
+
JobRef,
|
|
28
30
|
JobState,
|
|
29
31
|
MarkdownJob,
|
|
30
32
|
Result,
|
|
33
|
+
ServerCapabilities,
|
|
31
34
|
ServerVersions,
|
|
35
|
+
WithSettings,
|
|
32
36
|
} from './types.js';
|
|
33
37
|
|
|
34
38
|
// -------------------------------------------------------------------
|
|
@@ -55,15 +59,30 @@ type FormatFields = {
|
|
|
55
59
|
pageImages: boolean;
|
|
56
60
|
};
|
|
57
61
|
|
|
62
|
+
/** Parsed JSON answer of a conversion POST, with the settings its form was built from. */
|
|
63
|
+
type Posted = {
|
|
64
|
+
body: unknown;
|
|
65
|
+
settings: DoclingSettings;
|
|
66
|
+
};
|
|
67
|
+
|
|
58
68
|
// -------------------------------------------------------------------
|
|
59
69
|
// --- 2. Constants
|
|
60
70
|
// -------------------------------------------------------------------
|
|
61
71
|
|
|
62
|
-
/**
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
/** Table structure model; pinned so the server's fallback preset cannot change table output. */
|
|
73
|
+
const TABLE_STRUCTURE_PRESET = 'tableformer_v1_accurate';
|
|
74
|
+
|
|
75
|
+
/** Model of `pipeline: 'vlm'`. */
|
|
76
|
+
const VLM_PIPELINE_PRESET = 'granite_docling';
|
|
77
|
+
|
|
78
|
+
/** Model of `chartExtraction`. */
|
|
79
|
+
const CHART_EXTRACTION_PRESET = 'granite_vision_v4';
|
|
80
|
+
|
|
81
|
+
/** Model of `formulaEnrichment` and `codeEnrichment`. */
|
|
82
|
+
const CODE_FORMULA_PRESET = 'codeformulav2';
|
|
83
|
+
|
|
84
|
+
/** Matches each upper-case letter of a camelCase settings key, for its snake_case form field name. */
|
|
85
|
+
const MATCH_UPPER_CASE = /[A-Z]/g;
|
|
67
86
|
|
|
68
87
|
/** Error message for missing, empty or unrecognized input bytes. */
|
|
69
88
|
const INVALID_INPUT = 'Expected a non-empty PDF, PNG, JPEG, TIFF or WebP as Uint8Array';
|
|
@@ -106,18 +125,29 @@ export function createDoclingClient(config: DoclingConfig): DoclingClient {
|
|
|
106
125
|
submitMarkdown,
|
|
107
126
|
submitDocument,
|
|
108
127
|
check: check as DoclingClient['check'],
|
|
128
|
+
capabilities,
|
|
109
129
|
};
|
|
110
130
|
|
|
111
131
|
/** Converts PDF or image bytes to md in one request. */
|
|
112
|
-
async function convertMarkdown(
|
|
132
|
+
async function convertMarkdown(
|
|
133
|
+
input: ConvertInput,
|
|
134
|
+
options?: unknown,
|
|
135
|
+
): Promise<Result<WithSettings<ConvertedMarkdown>>> {
|
|
113
136
|
const call = resolveMarkdownCall(defaults, options);
|
|
114
137
|
const fields: FormatFields = { format: 'md', embedImages: call.output.images === 'extract', pageImages: false };
|
|
115
138
|
const res = await post('/v1/convert/file', input, call.options, fields, call.signal);
|
|
116
|
-
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
return res;
|
|
141
|
+
}
|
|
142
|
+
const converted = await withVersions<ConvertedMarkdown>(readMarkdown(res.data.body, call.output));
|
|
143
|
+
return converted.ok ? ok({ ...converted.data, settings: res.data.settings }) : converted;
|
|
117
144
|
}
|
|
118
145
|
|
|
119
146
|
/** Converts PDF or image bytes to a DoclingDocument in one request. */
|
|
120
|
-
async function convertDocument(
|
|
147
|
+
async function convertDocument(
|
|
148
|
+
input: ConvertInput,
|
|
149
|
+
options?: unknown,
|
|
150
|
+
): Promise<Result<WithSettings<ConvertedDocument>>> {
|
|
121
151
|
const call = resolveDocumentCall(defaults, options);
|
|
122
152
|
const fields: FormatFields = {
|
|
123
153
|
format: 'json',
|
|
@@ -125,15 +155,21 @@ export function createDoclingClient(config: DoclingConfig): DoclingClient {
|
|
|
125
155
|
pageImages: call.pageImages,
|
|
126
156
|
};
|
|
127
157
|
const res = await post('/v1/convert/file', input, call.options, fields, call.signal);
|
|
128
|
-
|
|
158
|
+
if (!res.ok) {
|
|
159
|
+
return res;
|
|
160
|
+
}
|
|
161
|
+
const converted = await withVersions<ConvertedDocument>(readDocument(res.data.body));
|
|
162
|
+
return converted.ok ? ok({ ...converted.data, settings: res.data.settings }) : converted;
|
|
129
163
|
}
|
|
130
164
|
|
|
131
|
-
/** Starts an async md conversion; the job carries the image handling for `check
|
|
165
|
+
/** Starts an async md conversion; the job carries the image handling for `check` and the sent settings. */
|
|
132
166
|
async function submitMarkdown(input: ConvertInput, options?: unknown): Promise<Result<MarkdownJob>> {
|
|
133
167
|
const call = resolveMarkdownCall(defaults, options);
|
|
134
168
|
const fields: FormatFields = { format: 'md', embedImages: call.output.images === 'extract', pageImages: false };
|
|
135
|
-
const
|
|
136
|
-
return
|
|
169
|
+
const task = await submit(input, call.options, fields, call.signal);
|
|
170
|
+
return task.ok
|
|
171
|
+
? ok({ kind: 'markdown', id: task.data.id, output: call.output, settings: task.data.settings })
|
|
172
|
+
: task;
|
|
137
173
|
}
|
|
138
174
|
|
|
139
175
|
/** Starts an async document conversion; page images need no job field, `check` moves any it finds. */
|
|
@@ -144,8 +180,8 @@ export function createDoclingClient(config: DoclingConfig): DoclingClient {
|
|
|
144
180
|
embedImages: call.images === 'embedded',
|
|
145
181
|
pageImages: call.pageImages,
|
|
146
182
|
};
|
|
147
|
-
const
|
|
148
|
-
return
|
|
183
|
+
const task = await submit(input, call.options, fields, call.signal);
|
|
184
|
+
return task.ok ? ok({ kind: 'document', id: task.data.id, settings: task.data.settings }) : task;
|
|
149
185
|
}
|
|
150
186
|
|
|
151
187
|
/**
|
|
@@ -156,7 +192,7 @@ export function createDoclingClient(config: DoclingConfig): DoclingClient {
|
|
|
156
192
|
* `Result already fetched or expired`
|
|
157
193
|
*/
|
|
158
194
|
async function check(
|
|
159
|
-
job:
|
|
195
|
+
job: JobRef,
|
|
160
196
|
options?: unknown,
|
|
161
197
|
): Promise<Result<JobState<ConvertedMarkdown | ConvertedDocument>>> {
|
|
162
198
|
const { signal } = resolveCheckOptions(options);
|
|
@@ -209,18 +245,33 @@ export function createDoclingClient(config: DoclingConfig): DoclingClient {
|
|
|
209
245
|
}
|
|
210
246
|
}
|
|
211
247
|
|
|
212
|
-
/**
|
|
248
|
+
/**
|
|
249
|
+
* **Queries `GET /v1/capabilities` on demand**.
|
|
250
|
+
*
|
|
251
|
+
* - older servers without the endpoint answer `http` (404)
|
|
252
|
+
*/
|
|
253
|
+
async function capabilities(options?: unknown): Promise<Result<ServerCapabilities>> {
|
|
254
|
+
const { signal } = resolveCheckOptions(options);
|
|
255
|
+
const res = await get('/v1/capabilities', signal);
|
|
256
|
+
if (!res.ok) {
|
|
257
|
+
return failWithVersions(res.error);
|
|
258
|
+
}
|
|
259
|
+
const read = readCapabilities(res.data);
|
|
260
|
+
return read.ok ? read : failWithVersions(read.error);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Posts the input to the async endpoint; resolves to the task id and the sent settings. */
|
|
213
264
|
async function submit(
|
|
214
265
|
input: ConvertInput,
|
|
215
266
|
options: ResolvedOptions,
|
|
216
267
|
fields: FormatFields,
|
|
217
268
|
signal: AbortSignal | undefined,
|
|
218
|
-
): Promise<Result<string>> {
|
|
269
|
+
): Promise<Result<{ id: string; settings: DoclingSettings }>> {
|
|
219
270
|
const res = await post('/v1/convert/file/async', input, options, fields, signal);
|
|
220
271
|
if (!res.ok) {
|
|
221
272
|
return res;
|
|
222
273
|
}
|
|
223
|
-
const id: unknown = (res.data as DoclingTaskStatus | null)?.task_id;
|
|
274
|
+
const id: unknown = (res.data.body as DoclingTaskStatus | null)?.task_id;
|
|
224
275
|
if (readText(id) === undefined) {
|
|
225
276
|
return failWithVersions(ERR.INVALID_RESPONSE('Response has no task_id'));
|
|
226
277
|
}
|
|
@@ -228,7 +279,7 @@ export function createDoclingClient(config: DoclingConfig): DoclingClient {
|
|
|
228
279
|
const shown = JSON.stringify(id).slice(0, MAX_SHOWN_ID_LENGTH);
|
|
229
280
|
return failWithVersions(ERR.INVALID_RESPONSE(`Response has an unusable task_id: ${shown}`));
|
|
230
281
|
}
|
|
231
|
-
return ok(id);
|
|
282
|
+
return ok({ id, settings: res.data.settings });
|
|
232
283
|
}
|
|
233
284
|
|
|
234
285
|
/** Detects and checks the input format, then sends a multipart POST with timeout; errors already carry versions. */
|
|
@@ -238,14 +289,15 @@ export function createDoclingClient(config: DoclingConfig): DoclingClient {
|
|
|
238
289
|
options: ResolvedOptions,
|
|
239
290
|
fields: FormatFields,
|
|
240
291
|
signal: AbortSignal | undefined,
|
|
241
|
-
): Promise<Result<
|
|
292
|
+
): Promise<Result<Posted>> {
|
|
242
293
|
const format = readInputFormat(input);
|
|
243
294
|
if (!format.ok) {
|
|
244
295
|
return format;
|
|
245
296
|
}
|
|
297
|
+
const settings = buildSettings(format.data, options, fields);
|
|
246
298
|
let body: FormData;
|
|
247
299
|
try {
|
|
248
|
-
body = buildFormData(input, format.data,
|
|
300
|
+
body = buildFormData(input, format.data, settings);
|
|
249
301
|
} catch (error) {
|
|
250
302
|
return fail(ERR.INVALID_INPUT(`Input could not be prepared for upload: ${errorMessage(error)}`));
|
|
251
303
|
}
|
|
@@ -256,7 +308,7 @@ export function createDoclingClient(config: DoclingConfig): DoclingClient {
|
|
|
256
308
|
body,
|
|
257
309
|
...(limited === undefined ? {} : { signal: limited }),
|
|
258
310
|
});
|
|
259
|
-
return res.ok ? res : failWithVersions(res.error);
|
|
311
|
+
return res.ok ? ok({ body: res.data, settings }) : failWithVersions(res.error);
|
|
260
312
|
}
|
|
261
313
|
|
|
262
314
|
/** GET request; `limit: false` skips `timeoutMs` (used by `/version`). */
|
|
@@ -354,60 +406,75 @@ function readInputFormat(input: unknown): Result<InputFormat> {
|
|
|
354
406
|
}
|
|
355
407
|
|
|
356
408
|
/**
|
|
357
|
-
* **
|
|
409
|
+
* **Every form field of a conversion request, as typed settings**.
|
|
358
410
|
*
|
|
359
|
-
* -
|
|
360
|
-
* - md: `md_compact_tables=true`, no page break placeholder
|
|
411
|
+
* - md: `md_compact_tables=true`, empty page break placeholder; json sends neither
|
|
361
412
|
* - images: `image_export_mode=embedded` when picture data or page images are requested, else `placeholder`;
|
|
362
413
|
* `include_images` and `include_page_images` pick which data docling sends
|
|
363
414
|
* - `images_scale` fixed at `PAGE_IMAGE_SCALE`; it sets picture and page image resolution only, OCR has its own scale
|
|
364
415
|
* - image input: OCR always on and never full-page, whatever `ocr` and `forceOcr` say
|
|
416
|
+
* - model presets pinned; a preset of a disabled stage is not sent
|
|
417
|
+
* - fields docling-serve would default silently are sent with that default
|
|
365
418
|
*/
|
|
366
|
-
function
|
|
367
|
-
|
|
368
|
-
{ fromFormat, mimeType, fileName }: InputFormat,
|
|
419
|
+
function buildSettings(
|
|
420
|
+
{ fromFormat }: InputFormat,
|
|
369
421
|
options: ResolvedOptions,
|
|
370
422
|
{ format, embedImages, pageImages }: FormatFields,
|
|
371
|
-
):
|
|
423
|
+
): DoclingSettings {
|
|
424
|
+
// Image has no text layer: without OCR it yields no text, and full-page OCR merges words.
|
|
425
|
+
const isImage = fromFormat === 'image';
|
|
426
|
+
const isMarkdown = format === 'md';
|
|
427
|
+
return {
|
|
428
|
+
fromFormats: fromFormat,
|
|
429
|
+
toFormats: format,
|
|
430
|
+
imageExportMode: embedImages || pageImages ? 'embedded' : 'placeholder',
|
|
431
|
+
includeImages: embedImages,
|
|
432
|
+
includePageImages: pageImages,
|
|
433
|
+
imagesScale: PAGE_IMAGE_SCALE,
|
|
434
|
+
mdCompactTables: isMarkdown ? true : null,
|
|
435
|
+
mdPageBreakPlaceholder: isMarkdown ? '' : null,
|
|
436
|
+
doOcr: isImage || options.ocr,
|
|
437
|
+
forceOcr: !isImage && options.forceOcr,
|
|
438
|
+
ocrPreset: options.ocrEngine,
|
|
439
|
+
ocrLang: [...(options.ocrLang ?? [])],
|
|
440
|
+
pdfBackend: options.pdfBackend,
|
|
441
|
+
pipeline: options.pipeline,
|
|
442
|
+
vlmPipelinePreset: options.pipeline === 'vlm' ? VLM_PIPELINE_PRESET : null,
|
|
443
|
+
doTableStructure: true,
|
|
444
|
+
tableMode: 'accurate',
|
|
445
|
+
tableStructurePreset: TABLE_STRUCTURE_PRESET,
|
|
446
|
+
tableCellMatching: true,
|
|
447
|
+
doPdfHeadingHierarchy: options.headingHierarchy,
|
|
448
|
+
doFormulaEnrichment: options.formulaEnrichment,
|
|
449
|
+
doCodeEnrichment: options.codeEnrichment,
|
|
450
|
+
codeFormulaPreset: options.formulaEnrichment || options.codeEnrichment ? CODE_FORMULA_PRESET : null,
|
|
451
|
+
doChartExtraction: options.chartExtraction,
|
|
452
|
+
chartExtractionPreset: options.chartExtraction ? CHART_EXTRACTION_PRESET : null,
|
|
453
|
+
doPictureClassification: false,
|
|
454
|
+
doPictureDescription: false,
|
|
455
|
+
pageRange: options.pageRange === undefined ? null : [options.pageRange[0], options.pageRange[1]],
|
|
456
|
+
documentTimeout: options.documentTimeout ?? null,
|
|
457
|
+
abortOnError: false,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* **Builds the multipart body from the settings, so sent fields and returned settings cannot differ**.
|
|
463
|
+
*
|
|
464
|
+
* - field name: settings key in snake_case (`ocrPreset` → `ocr_preset`)
|
|
465
|
+
* - `null` not sent; lists as repeated fields (empty list: not sent); booleans and numbers as strings
|
|
466
|
+
* - input: file name and MIME type matching `from_formats`
|
|
467
|
+
*/
|
|
468
|
+
function buildFormData(input: ConvertInput, { mimeType, fileName }: InputFormat, settings: DoclingSettings): FormData {
|
|
372
469
|
const form = new FormData();
|
|
373
470
|
// Blob does not accept SharedArrayBuffer-backed input: copy only that into a fresh ArrayBuffer.
|
|
374
471
|
const bytes = input.buffer instanceof ArrayBuffer ? (input as Uint8Array<ArrayBuffer>) : new Uint8Array(input);
|
|
375
472
|
form.append('files', new Blob([bytes], { type: mimeType }), fileName);
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (format === 'md') {
|
|
382
|
-
form.append('md_compact_tables', 'true');
|
|
383
|
-
}
|
|
384
|
-
form.append('include_images', String(embedImages));
|
|
385
|
-
form.append('include_page_images', String(pageImages));
|
|
386
|
-
form.append('image_export_mode', embedImages || pageImages ? 'embedded' : 'placeholder');
|
|
387
|
-
form.append('images_scale', String(PAGE_IMAGE_SCALE));
|
|
388
|
-
form.append('do_pdf_heading_hierarchy', String(options.headingHierarchy));
|
|
389
|
-
form.append('do_formula_enrichment', String(options.formulaEnrichment));
|
|
390
|
-
form.append('do_code_enrichment', String(options.codeEnrichment));
|
|
391
|
-
if (options.formulaEnrichment || options.codeEnrichment) {
|
|
392
|
-
form.append('code_formula_preset', 'codeformulav2');
|
|
393
|
-
}
|
|
394
|
-
form.append('do_chart_extraction', String(options.chartExtraction));
|
|
395
|
-
form.append('pipeline', options.pipeline);
|
|
396
|
-
form.append('pdf_backend', options.pdfBackend);
|
|
397
|
-
// an image has no text layer: without OCR it yields no text, and full-page OCR merges its words
|
|
398
|
-
const isImage = fromFormat === 'image';
|
|
399
|
-
form.append('do_ocr', String(isImage || options.ocr));
|
|
400
|
-
form.append('force_ocr', String(!isImage && options.forceOcr));
|
|
401
|
-
// lists are sent as repeated fields
|
|
402
|
-
for (const lang of options.ocrLang ?? []) {
|
|
403
|
-
form.append('ocr_lang', lang);
|
|
404
|
-
}
|
|
405
|
-
if (options.pageRange !== undefined) {
|
|
406
|
-
form.append('page_range', String(options.pageRange[0]));
|
|
407
|
-
form.append('page_range', String(options.pageRange[1]));
|
|
408
|
-
}
|
|
409
|
-
if (options.documentTimeout !== undefined) {
|
|
410
|
-
form.append('document_timeout', String(options.documentTimeout));
|
|
473
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
474
|
+
const name = key.replace(MATCH_UPPER_CASE, (letter) => `_${letter.toLowerCase()}`);
|
|
475
|
+
for (const item of value === null ? [] : Array.isArray(value) ? value : [value]) {
|
|
476
|
+
form.append(name, String(item));
|
|
477
|
+
}
|
|
411
478
|
}
|
|
412
479
|
return form;
|
|
413
480
|
}
|
package/src/core/input.ts
CHANGED
|
@@ -33,10 +33,10 @@ type ImageSignature = {
|
|
|
33
33
|
const IMAGE_SIGNATURES: readonly ImageSignature[] = [
|
|
34
34
|
{ mimeType: 'image/png', extension: 'png', parts: [[0, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]]] },
|
|
35
35
|
{ mimeType: 'image/jpeg', extension: 'jpg', parts: [[0, [0xff, 0xd8, 0xff]]] },
|
|
36
|
-
//
|
|
36
|
+
// Little-endian `II*\0` and big-endian `MM\0*`.
|
|
37
37
|
{ mimeType: 'image/tiff', extension: 'tiff', parts: [[0, [0x49, 0x49, 0x2a, 0x00]]] },
|
|
38
38
|
{ mimeType: 'image/tiff', extension: 'tiff', parts: [[0, [0x4d, 0x4d, 0x00, 0x2a]]] },
|
|
39
|
-
// `RIFF` <size> `WEBP
|
|
39
|
+
// `RIFF` <size> `WEBP`.
|
|
40
40
|
{
|
|
41
41
|
mimeType: 'image/webp',
|
|
42
42
|
extension: 'webp',
|
|
@@ -59,7 +59,7 @@ const PDF_FORMAT: InputFormat = { fromFormat: 'pdf', mimeType: 'application/pdf'
|
|
|
59
59
|
*
|
|
60
60
|
* - image signatures must start at byte 0; PDF: `%PDF-` within the first 1024 bytes
|
|
61
61
|
* - images are checked first, so an image whose data happens to contain `%PDF-` is still an image
|
|
62
|
-
*
|
|
62
|
+
* - returns `undefined` for empty, non-`Uint8Array` or unrecognized input
|
|
63
63
|
*/
|
|
64
64
|
export function detectInputFormat(bytes: unknown): InputFormat | undefined {
|
|
65
65
|
if (!(bytes instanceof Uint8Array) || bytes.length === 0) {
|
package/src/core/pdf.ts
CHANGED
|
@@ -40,11 +40,11 @@ export function isPdf(bytes: unknown): bytes is Uint8Array {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* **Returns cross-reference offset if PDF is truncated**: last
|
|
43
|
+
* **Returns cross-reference offset if PDF is truncated**: last `startxref` points at or past EOF.
|
|
44
44
|
*
|
|
45
45
|
* - typical for aborted downloads; docling then fails with a vague load error
|
|
46
46
|
* - only the last 2 KB are read, as latin1
|
|
47
|
-
* - returns
|
|
47
|
+
* - returns `undefined` when offset lies inside file or no `startxref` found
|
|
48
48
|
*/
|
|
49
49
|
export function findTruncatedXref(bytes: Uint8Array): number | undefined {
|
|
50
50
|
const tail = new TextDecoder('latin1').decode(bytes.subarray(Math.max(0, bytes.length - TAIL_LENGTH)));
|
package/src/core/validate.ts
CHANGED
|
@@ -74,12 +74,12 @@ export function isPlainObject(value: unknown): value is Record<string, unknown>
|
|
|
74
74
|
return prototype === Object.prototype || prototype === null;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
/**
|
|
77
|
+
/** Returns true for boolean primitives (`true` or `false`). */
|
|
78
78
|
export function isBoolean(value: unknown): value is boolean {
|
|
79
79
|
return value === true || value === false;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/**
|
|
82
|
+
/** Returns true when string belongs to allowed set. */
|
|
83
83
|
export function isOneOf<T extends string>(value: unknown, allowed: readonly T[]): value is T {
|
|
84
84
|
return typeof value === 'string' && (allowed as readonly string[]).includes(value);
|
|
85
85
|
}
|
package/src/images.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import { isPlainObject } from './core/validate.js';
|
|
5
|
-
import type { DoclingDocument, PageItem
|
|
5
|
+
import type { BoundingBox, DoclingDocument, PageItem } from './document.js';
|
|
6
6
|
import type {
|
|
7
7
|
ExtractedImage,
|
|
8
8
|
ImageOptions,
|
|
@@ -154,7 +154,7 @@ export function extractImages(markdown: string, options: Required<ImageOptions>)
|
|
|
154
154
|
}
|
|
155
155
|
const size = readPngSize(bytes);
|
|
156
156
|
if (options.omitSmall && size !== undefined && isSmallImage(size)) {
|
|
157
|
-
//
|
|
157
|
+
// Marker, so removing it below also normalizes surrounding blank lines.
|
|
158
158
|
output += OMITTED_IMAGE;
|
|
159
159
|
continue;
|
|
160
160
|
}
|
|
@@ -218,11 +218,11 @@ export function extractPageImages(document: DoclingDocument): ExtractedPageImage
|
|
|
218
218
|
* - `prov`: any `prov[]` entry; for a table cell pass `{ page_no: table.prov[0].page_no, bbox: cell.bbox }`
|
|
219
219
|
* - flips `BOTTOMLEFT` boxes using the page height from `document.pages`, then scales points by `PAGE_IMAGE_SCALE`
|
|
220
220
|
* - works without `pageImages: true`; the boxes fit the page images of any conversion by this client
|
|
221
|
-
* - returns `undefined` when page_no or
|
|
221
|
+
* - returns `undefined` when `page_no` or coordinate is not a finite number, or page is missing from `document.pages`
|
|
222
222
|
*/
|
|
223
223
|
export function toPageImageBox(
|
|
224
224
|
document: DoclingDocument,
|
|
225
|
-
prov:
|
|
225
|
+
prov: { page_no: number; bbox?: BoundingBox | null },
|
|
226
226
|
): PageImageBox | undefined {
|
|
227
227
|
const pageNo = prov?.page_no;
|
|
228
228
|
if (!isFiniteNumber(pageNo)) {
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
export type * from './document.js';
|
|
9
9
|
export type {
|
|
10
10
|
CallOptions,
|
|
11
|
+
CapabilityLimits,
|
|
12
|
+
CapabilityPreset,
|
|
13
|
+
CapabilityStage,
|
|
11
14
|
ClientOptions,
|
|
12
15
|
ConversionFailure,
|
|
13
16
|
ConvertError,
|
|
@@ -17,6 +20,7 @@ export type {
|
|
|
17
20
|
DoclingClient,
|
|
18
21
|
DoclingConfig,
|
|
19
22
|
DoclingOptions,
|
|
23
|
+
DoclingSettings,
|
|
20
24
|
DocumentImageMode,
|
|
21
25
|
DocumentJob,
|
|
22
26
|
DocumentOptions,
|
|
@@ -27,12 +31,14 @@ export type {
|
|
|
27
31
|
ImageSize,
|
|
28
32
|
ImageWarning,
|
|
29
33
|
Job,
|
|
34
|
+
JobRef,
|
|
30
35
|
JobState,
|
|
31
36
|
MarkdownImageMode,
|
|
32
37
|
MarkdownJob,
|
|
33
38
|
MarkdownJobOutput,
|
|
34
39
|
MarkdownOptions,
|
|
35
40
|
MarkdownWarning,
|
|
41
|
+
OcrEngine,
|
|
36
42
|
PageImage,
|
|
37
43
|
PageImageBox,
|
|
38
44
|
PageImageWarning,
|
|
@@ -40,11 +46,13 @@ export type {
|
|
|
40
46
|
PdfInput,
|
|
41
47
|
Pipeline,
|
|
42
48
|
Result,
|
|
49
|
+
ServerCapabilities,
|
|
43
50
|
ServerDetails,
|
|
44
51
|
ServerError,
|
|
45
52
|
ServerVersions,
|
|
46
53
|
ServerWarning,
|
|
47
54
|
TaskFailure,
|
|
55
|
+
WithSettings,
|
|
48
56
|
} from './types.js';
|
|
49
57
|
|
|
50
58
|
// -------------------------------------------------------------------
|
package/src/options.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type {
|
|
|
19
19
|
DocumentImageMode,
|
|
20
20
|
DocumentOptions,
|
|
21
21
|
ImageOptions,
|
|
22
|
-
|
|
22
|
+
JobRef,
|
|
23
23
|
MarkdownImageMode,
|
|
24
24
|
MarkdownJobOutput,
|
|
25
25
|
MarkdownOptions,
|
|
@@ -89,6 +89,7 @@ export const DEFAULT_OPTIONS: Readonly<ResolvedOptions> = Object.freeze({
|
|
|
89
89
|
pdfBackend: 'docling_parse',
|
|
90
90
|
ocr: true,
|
|
91
91
|
forceOcr: false,
|
|
92
|
+
ocrEngine: 'auto',
|
|
92
93
|
});
|
|
93
94
|
|
|
94
95
|
/** Rules for docling options, accepted by the factory and every conversion call. */
|
|
@@ -101,6 +102,8 @@ const CALL_OPTION_RULES: Record<keyof DoclingOptions, OptionRule> = {
|
|
|
101
102
|
pdfBackend: (value) => isOneOf(value, PDF_BACKENDS),
|
|
102
103
|
ocr: isBoolean,
|
|
103
104
|
forceOcr: isBoolean,
|
|
105
|
+
// Open list: presets depend on server installation, unknown IDs are left to server.
|
|
106
|
+
ocrEngine: (value) => typeof value === 'string' && value !== '',
|
|
104
107
|
};
|
|
105
108
|
|
|
106
109
|
/** Rules for factory options: per-call options plus the factory-only ones. */
|
|
@@ -133,7 +136,7 @@ const IMAGE_OPTION_RULES: Record<keyof ImageOptions, OptionRule> = {
|
|
|
133
136
|
omitSmall: isBoolean,
|
|
134
137
|
};
|
|
135
138
|
|
|
136
|
-
/** Rules for `check` options. */
|
|
139
|
+
/** Rules for `check` and `capabilities` options. */
|
|
137
140
|
const CHECK_RULES: Record<keyof CallOptions, OptionRule> = {
|
|
138
141
|
signal: isAbortSignal,
|
|
139
142
|
};
|
|
@@ -196,7 +199,10 @@ export function resolveConfig(config: unknown): ResolvedConfig {
|
|
|
196
199
|
};
|
|
197
200
|
}
|
|
198
201
|
|
|
199
|
-
/**
|
|
202
|
+
/**
|
|
203
|
+
* Validates md call options over factory defaults.
|
|
204
|
+
* @throws `TypeError` on invalid options.
|
|
205
|
+
*/
|
|
200
206
|
export function resolveMarkdownCall(defaults: Readonly<ResolvedOptions>, options: unknown): MarkdownCall {
|
|
201
207
|
const {
|
|
202
208
|
signal,
|
|
@@ -208,7 +214,10 @@ export function resolveMarkdownCall(defaults: Readonly<ResolvedOptions>, options
|
|
|
208
214
|
return { options: { ...defaults, ...docling }, output, ...(signal === undefined ? {} : { signal }) };
|
|
209
215
|
}
|
|
210
216
|
|
|
211
|
-
/**
|
|
217
|
+
/**
|
|
218
|
+
* Validates document call options over factory defaults.
|
|
219
|
+
* @throws `TypeError` on invalid options.
|
|
220
|
+
*/
|
|
212
221
|
export function resolveDocumentCall(defaults: Readonly<ResolvedOptions>, options: unknown): DocumentCall {
|
|
213
222
|
const {
|
|
214
223
|
signal,
|
|
@@ -219,7 +228,10 @@ export function resolveDocumentCall(defaults: Readonly<ResolvedOptions>, options
|
|
|
219
228
|
return { options: { ...defaults, ...docling }, images, pageImages, ...(signal === undefined ? {} : { signal }) };
|
|
220
229
|
}
|
|
221
230
|
|
|
222
|
-
/**
|
|
231
|
+
/**
|
|
232
|
+
* Validates signal-only call options (`check`, `capabilities`).
|
|
233
|
+
* @throws `TypeError` on invalid options.
|
|
234
|
+
*/
|
|
223
235
|
export function resolveCheckOptions(options: unknown): CallOptions {
|
|
224
236
|
return validateKeys<CallOptions>(callObject(options), CHECK_RULES);
|
|
225
237
|
}
|
|
@@ -228,8 +240,9 @@ export function resolveCheckOptions(options: unknown): CallOptions {
|
|
|
228
240
|
* **Checks a job object against the shape `submitMarkdown()` and `submitDocument()` return**.
|
|
229
241
|
*
|
|
230
242
|
* - never throws: a malformed job is `invalid_input`
|
|
243
|
+
* - `settings` and other extra keys are ignored and not returned
|
|
231
244
|
*/
|
|
232
|
-
export function validateJob(job: unknown): Result<
|
|
245
|
+
export function validateJob(job: unknown): Result<JobRef> {
|
|
233
246
|
if (!isPlainObject(job) || !isJobId(job.id)) {
|
|
234
247
|
return fail(ERR.INVALID_INPUT(INVALID_JOB));
|
|
235
248
|
}
|
|
@@ -259,7 +272,10 @@ export function isJobId(value: unknown): value is string {
|
|
|
259
272
|
// --- 4. Internal Functions
|
|
260
273
|
// -------------------------------------------------------------------
|
|
261
274
|
|
|
262
|
-
/**
|
|
275
|
+
/**
|
|
276
|
+
* Returns call options as a plain object (`undefined` becomes `{}`).
|
|
277
|
+
* @throws `TypeError` when options is not a plain object.
|
|
278
|
+
*/
|
|
263
279
|
function callObject(options: unknown): Record<string, unknown> {
|
|
264
280
|
if (options === undefined) {
|
|
265
281
|
return {};
|
package/src/response.ts
CHANGED
|
@@ -212,7 +212,7 @@ function readServerError(error: DoclingErrorItem): ServerWarning[] {
|
|
|
212
212
|
];
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
/**
|
|
215
|
+
/** Docling `page_no`: integer >= 1; 0, negatives and fractions are rejected. */
|
|
216
216
|
function isPageNumber(value: unknown): value is number {
|
|
217
217
|
return typeof value === 'number' && Number.isInteger(value) && value >= 1;
|
|
218
218
|
}
|