Skip to content

Commit 7c950a0

Browse files
authored
Merge pull request #164 from artiz/claude/force-ocr
pdf: force_full_page_ocr on every surface (#157)
2 parents a1eb14f + 5977a1b commit 7c950a0

23 files changed

Lines changed: 1163 additions & 118 deletions

File tree

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,15 @@ all, only the PDF's embedded text cells grouped into flat paragraphs by
386386
reading order (no headings/lists/tables/pictures). It's the fastest PDF path
387387
by a wide margin, but a scanned/image-only PDF (no embedded text layer) comes
388388
back empty rather than erroring, so a caller can detect that and re-convert
389-
without the flag.
389+
without the flag. `--force-full-page-ocr` is the opposite escape hatch
390+
(docling's `force_full_page_ocr`): OCR every page from its rendered image
391+
even when it carries a text layer — for layers that exist but lie (broken
392+
encodings, subset fonts with garbage mappings, a scanned form with a few
393+
typed-in field values). Ignored under `--no-ocr`, mirroring docling. The same
394+
switch is available on every surface: `force_full_page_ocr(bool)` on the
395+
library builder, a `force_full_page_ocr` option in docling-serve, the
396+
`force_full_page_ocr=` kwarg in Python, `forceFullPageOcr` in Node, and the
397+
"Force OCR" toggle in the wasm demo.
390398

391399
### VLM pipeline (remote endpoint)
392400

crates/docling-cli/src/main.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//! The docling.rs counterpart of `docling.cli.main`; `docling-rs serve`
44
//! (with `--features serve`) starts the HTTP conversion API.
55
//!
6-
//! Usage: docling-rs [--strict] [--to md|json] [--pages A-B] [--images MODE] [--fetch-images] [--no-stream] [--no-table-former] [--no-ocr] [--ocr-lang en|ch] [--pipeline standard|vlm] [--vlm-endpoint URL] [--vlm-model NAME] [--asr-model PRESET] [--video-frames N] [--use-web-browser] [--enrich-picture-classes] [--enrich-code] [--enrich-formula] <input-file>
6+
//! Usage: docling-rs [--strict] [--to md|json] [--pages A-B] [--images MODE] [--fetch-images] [--no-stream] [--no-table-former] [--no-ocr] [--force-full-page-ocr] [--ocr-lang en|ch] [--pipeline standard|vlm] [--vlm-endpoint URL] [--vlm-model NAME] [--asr-model PRESET] [--video-frames N] [--use-web-browser] [--enrich-picture-classes] [--enrich-code] [--enrich-formula] <input-file>
77
//! --to md|json output format (default: md). `json` emits docling-core's
88
//! native DoclingDocument JSON (export_to_dict).
99
//! --pages A-B convert only PDF pages A through B (1-based, inclusive;
@@ -37,6 +37,9 @@
3737
//! whisper_base_en, whisper_small_en, whisper_distil_small_en
3838
//! (models under models/asr/<preset>/; fetch them with
3939
//! download_dependencies.sh --asr-model=<preset>)
40+
//! --force-full-page-ocr OCR every PDF page even when it has a text layer
41+
//! (docling's force_full_page_ocr) — for layers that lie:
42+
//! broken encodings, forms with a few typed-in fields
4043
//! --no-ocr skip layout detection, OCR, and TableFormer entirely for
4144
//! PDF/image input — no model load or inference at all.
4245
//! Emits the embedded text layer as flat paragraphs in
@@ -86,6 +89,7 @@ fn main() -> ExitCode {
8689
let mut no_stream = false;
8790
let mut no_table_former = false;
8891
let mut no_ocr = false;
92+
let mut force_full_page_ocr = false;
8993
let mut use_web_browser = false;
9094
let mut asr_model: Option<String> = None;
9195
let mut video_frames: Option<usize> = None;
@@ -107,6 +111,7 @@ fn main() -> ExitCode {
107111
"--no-stream" => no_stream = true,
108112
"--no-table-former" => no_table_former = true,
109113
"--no-ocr" => no_ocr = true,
114+
"--force-full-page-ocr" => force_full_page_ocr = true,
110115
"--use-web-browser" => use_web_browser = true,
111116
// Opt-in enrichment models (docling CLI flag names): picture
112117
// classification, code rewrite + language, formula LaTeX.
@@ -202,7 +207,7 @@ fn main() -> ExitCode {
202207
};
203208

204209
let Some(path) = path else {
205-
eprintln!("usage: docling-rs [--strict] [--to md|json] [--images MODE] [--fetch-images] [--no-stream] [--no-table-former] [--no-ocr] [--ocr-lang en|ch] [--use-web-browser] <input-file>");
210+
eprintln!("usage: docling-rs [--strict] [--to md|json] [--images MODE] [--fetch-images] [--no-stream] [--no-table-former] [--no-ocr] [--force-full-page-ocr] [--ocr-lang en|ch] [--use-web-browser] <input-file>");
206211
return ExitCode::from(2);
207212
};
208213

@@ -261,6 +266,7 @@ fn main() -> ExitCode {
261266
.fetch_images(fetch_images)
262267
.no_table_former(no_table_former)
263268
.no_ocr(no_ocr)
269+
.force_full_page_ocr(force_full_page_ocr)
264270
.use_web_browser(use_web_browser)
265271
.do_picture_classification(enrich_picture_classes)
266272
.do_code_enrichment(enrich_code)

crates/docling-node/src/lib.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ pub struct ConverterOptions {
4444
/// proper Latin word spacing) or `"ch"` (the multilingual
4545
/// docling-conformance model). Formats that never OCR ignore it.
4646
pub ocr_lang: Option<String>,
47+
/// OCR every PDF page even when it carries an embedded text layer
48+
/// (docling's `force_full_page_ocr`) — for text layers that exist but lie.
49+
/// Default `false`.
50+
pub force_full_page_ocr: Option<bool>,
4751
/// Emit cleaner, more conformant Markdown (code-fence languages preserved,
4852
/// no inline-run spacing artifacts) instead of docling's byte-for-byte
4953
/// legacy output. Markdown only. Default `false`.
@@ -86,6 +90,9 @@ pub struct ConvertOptions {
8690
pub pages: Option<String>,
8791
/// OCR recognition language for scanned pages: `"en"` (default) | `"ch"`.
8892
pub ocr_lang: Option<String>,
93+
/// OCR every PDF page even when it carries a text layer (docling's
94+
/// `force_full_page_ocr`). Default `false`.
95+
pub force_full_page_ocr: Option<bool>,
8996
pub allowed_formats: Option<Vec<String>>,
9097
pub to: Option<String>,
9198
pub image_mode: Option<String>,
@@ -142,6 +149,7 @@ struct ConvertConfig {
142149
video_frames: Option<usize>,
143150
page_range: Option<(usize, usize)>,
144151
ocr_lang: Option<String>,
152+
force_full_page_ocr: bool,
145153
allowed_formats: Option<Vec<InputFormat>>,
146154
to: OutputKind,
147155
image_mode: ImageMode,
@@ -201,6 +209,7 @@ fn build_config(o: ConvertOptions) -> Result<ConvertConfig> {
201209
video_frames: o.video_frames.map(|n| n as usize),
202210
page_range: parse_pages(o.pages.as_deref())?,
203211
ocr_lang: parse_ocr_lang(o.ocr_lang)?,
212+
force_full_page_ocr: o.force_full_page_ocr.unwrap_or(false),
204213
allowed_formats: allowed,
205214
to: parse_output_kind(o.to.as_deref())?,
206215
image_mode: parse_image_mode(o.image_mode.as_deref())?,
@@ -231,6 +240,7 @@ fn build_converter(cfg: &ConvertConfig) -> RsConverter {
231240
let base = base
232241
.strict(cfg.strict)
233242
.fetch_images(cfg.fetch_images)
243+
.force_full_page_ocr(cfg.force_full_page_ocr)
234244
.asr_model(cfg.asr_model.clone());
235245
let base = match cfg.video_frames {
236246
Some(max) => base.video_frames(max),
@@ -416,6 +426,7 @@ pub struct DocumentConverter {
416426
video_frames: Option<usize>,
417427
page_range: Option<(usize, usize)>,
418428
ocr_lang: Option<String>,
429+
force_full_page_ocr: bool,
419430
allowed_formats: Option<Vec<InputFormat>>,
420431
}
421432

@@ -439,6 +450,7 @@ impl DocumentConverter {
439450
video_frames: o.video_frames.map(|n| n as usize),
440451
page_range: parse_pages(o.pages.as_deref())?,
441452
ocr_lang: parse_ocr_lang(o.ocr_lang.clone())?,
453+
force_full_page_ocr: o.force_full_page_ocr.unwrap_or(false),
442454
allowed_formats: allowed,
443455
})
444456
}
@@ -452,6 +464,7 @@ impl DocumentConverter {
452464
video_frames: self.video_frames,
453465
page_range: self.page_range,
454466
ocr_lang: self.ocr_lang.clone(),
467+
force_full_page_ocr: self.force_full_page_ocr,
455468
allowed_formats: self.allowed_formats.clone(),
456469
to: parse_output_kind(out.to.as_deref())?,
457470
image_mode: parse_image_mode(out.image_mode.as_deref())?,
@@ -865,6 +878,7 @@ fn output_config(out: Option<OutputOptions>, strict: bool) -> Result<ConvertConf
865878
asr_model: None,
866879
video_frames: None,
867880
page_range: None,
881+
force_full_page_ocr: false,
868882
ocr_lang: None,
869883
allowed_formats: None,
870884
to: parse_output_kind(out.to.as_deref())?,

0 commit comments

Comments
 (0)