@@ -82,32 +82,59 @@ type PageOut = (Vec<Node>, Vec<(String, String)>);
8282/// parallel page-worker owns its own `Worker` so inference runs concurrently
8383/// without sharing an ONNX session (`ort`'s `Session::run` is `&mut self`).
8484struct Worker {
85- layout : layout:: LayoutModel ,
85+ /// `None` when `no_ocr` skips layout entirely — no model load, no inference.
86+ layout : Option < layout:: LayoutModel > ,
8687 ocr : Option < ocr:: OcrModel > ,
8788 /// TableFormer structure model; `None` when its ONNX graphs aren't present
88- /// (the assembler then falls back to geometric table reconstruction).
89+ /// (the assembler then falls back to geometric table reconstruction) or
90+ /// when `no_table_former`/`no_ocr` skip it.
8991 tables : Option < tableformer:: TableFormer > ,
92+ /// Skip layout, OCR, and TableFormer; reconstruct text purely from the PDF's
93+ /// embedded text layer. See [`Pipeline::no_ocr`].
94+ no_ocr : bool ,
9095}
9196
9297impl Worker {
93- fn load ( intra : usize , no_table_former : bool ) -> Result < Self , PdfError > {
98+ fn load ( intra : usize , no_table_former : bool , no_ocr : bool ) -> Result < Self , PdfError > {
9499 Ok ( Self {
95- layout : layout:: LayoutModel :: load_with ( intra) . map_err ( PdfError :: Layout ) ?,
100+ layout : if no_ocr {
101+ None
102+ } else {
103+ Some ( layout:: LayoutModel :: load_with ( intra) . map_err ( PdfError :: Layout ) ?)
104+ } ,
96105 ocr : None ,
97- tables : if no_table_former {
106+ tables : if no_table_former || no_ocr {
98107 None
99108 } else {
100109 tableformer:: TableFormer :: load_with ( intra)
101110 } ,
111+ no_ocr,
102112 } )
103113 }
104114
105115 /// Run layout (+ OCR for cell-less pages) + TableFormer and assemble page `n`
106116 /// into its nodes and links. Pure given the page (mutates only the worker's
107117 /// lazily-loaded OCR model), so it is safe to run concurrently across pages.
108118 fn process ( & mut self , n : usize , page : & mut PdfPage ) -> Result < PageOut , PdfError > {
119+ if self . no_ocr {
120+ // Fastest path: no layout/OCR/TableFormer inference at all. The PDF's
121+ // embedded text cells (if any) become flat, line-grouped paragraphs in
122+ // reading order via the same orphan-region machinery that normally
123+ // rescues text the detector missed — here it rescues *all* of it.
124+ // Pages with no embedded text layer (scanned/image-only) yield nothing;
125+ // convert those without `no_ocr`.
126+ let mut regions = Vec :: new ( ) ;
127+ assemble:: add_orphan_regions ( & mut regions, & page. cells ) ;
128+ let table_rows = vec ! [ None ; regions. len( ) ] ;
129+ return Ok ( timing:: timed ( "assemble_page" , || {
130+ assemble:: assemble_page ( page, regions, & table_rows)
131+ } ) ) ;
132+ }
109133 let regions = timing:: timed ( "layout.predict" , || {
110- self . layout . predict ( & page. image , page. width , page. height )
134+ self . layout
135+ . as_mut ( )
136+ . expect ( "layout model loaded unless no_ocr" )
137+ . predict ( & page. image , page. width , page. height )
111138 } )
112139 . map_err ( |e| PdfError :: Layout ( format ! ( "page {}: {e}" , n + 1 ) ) ) ?;
113140 // Resolve overlapping detections once, before OCR.
@@ -215,6 +242,8 @@ pub struct Pipeline {
215242 /// Skip loading/running TableFormer; table regions fall back to geometric
216243 /// reconstruction. See [`Pipeline::no_table_former`].
217244 no_table_former : bool ,
245+ /// Skip layout, OCR, and TableFormer entirely. See [`Pipeline::no_ocr`].
246+ no_ocr : bool ,
218247}
219248
220249impl Pipeline {
@@ -228,6 +257,7 @@ impl Pipeline {
228257 target_workers : pdf_worker_count ( ) ,
229258 parallel_min : pdf_parallel_min ( ) ,
230259 no_table_former : false ,
260+ no_ocr : false ,
231261 } )
232262 }
233263
@@ -242,10 +272,27 @@ impl Pipeline {
242272 self
243273 }
244274
275+ /// Skip layout detection, OCR, and TableFormer entirely — no model load, no
276+ /// inference of any kind. The PDF's embedded text cells are grouped by line
277+ /// and emitted as plain paragraphs in reading order: no headings, lists,
278+ /// tables, code blocks, or pictures, since that structure comes from the
279+ /// layout model. The fastest possible PDF path, but pages with no embedded
280+ /// text layer (scanned/image-only PDFs) yield no text at all — convert those
281+ /// without this flag. Implies `no_table_former`. No effect if a worker is
282+ /// already loaded; set this before the first conversion.
283+ pub fn no_ocr ( mut self , disable : bool ) -> Self {
284+ self . no_ocr = disable;
285+ self
286+ }
287+
245288 /// The full-intra serial worker, loaded on first use.
246289 fn primary ( & mut self ) -> Result < & mut Worker , PdfError > {
247290 if self . primary . is_none ( ) {
248- self . primary = Some ( Worker :: load ( intra_threads ( ) , self . no_table_former ) ?) ;
291+ self . primary = Some ( Worker :: load (
292+ intra_threads ( ) ,
293+ self . no_table_former ,
294+ self . no_ocr ,
295+ ) ?) ;
249296 }
250297 Ok ( self . primary . as_mut ( ) . unwrap ( ) )
251298 }
@@ -280,8 +327,9 @@ impl Pipeline {
280327 name : & str ,
281328 ) -> Result < DoclingDocument , PdfError > {
282329 let mut doc = DoclingDocument :: new ( name) ;
330+ let render_image = !self . no_ocr ;
283331 let worker = self . primary ( ) ?;
284- pdfium_backend:: for_each_page ( bytes, password, |n, _total, mut page| {
332+ pdfium_backend:: for_each_page ( bytes, password, render_image , |n, _total, mut page| {
285333 let ( nodes, links) = worker. process ( n, & mut page) ?;
286334 doc. nodes . extend ( nodes) ;
287335 doc. links . extend ( links) ;
@@ -304,6 +352,7 @@ impl Pipeline {
304352 ) -> Result < DoclingDocument , PdfError > {
305353 self . ensure_pool ( ) ?;
306354 let n_workers = self . pool . len ( ) ;
355+ let render_image = !self . no_ocr ;
307356 let ( work_tx, work_rx) = sync_channel :: < ( usize , PdfPage ) > ( n_workers * 2 ) ;
308357 let work_rx: Arc < Mutex < Receiver < ( usize , PdfPage ) > > > = Arc :: new ( Mutex :: new ( work_rx) ) ;
309358 let results: Arc < Mutex < Vec < ( usize , PageOut ) > > > = Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ;
@@ -335,11 +384,12 @@ impl Pipeline {
335384 // Render on this thread and feed the workers; backpressure blocks here
336385 // when the channel is full. Dropping `work_tx` afterwards signals the
337386 // workers (recv → Err) to finish.
338- let render = pdfium_backend:: for_each_page ( bytes, password, |i, _total, page| {
339- work_tx
340- . send ( ( i, page) )
341- . map_err ( |_| PdfError :: Pdfium ( "page-worker channel closed" . into ( ) ) )
342- } ) ;
387+ let render =
388+ pdfium_backend:: for_each_page ( bytes, password, render_image, |i, _total, page| {
389+ work_tx
390+ . send ( ( i, page) )
391+ . map_err ( |_| PdfError :: Pdfium ( "page-worker channel closed" . into ( ) ) )
392+ } ) ;
343393 drop ( work_tx) ;
344394 if let Err ( e) = render {
345395 let mut slot = first_err. lock ( ) . unwrap ( ) ;
@@ -412,8 +462,9 @@ impl Pipeline {
412462 F : FnMut ( Vec < Node > , Vec < ( String , String ) > ) -> Result < ( ) , PdfError > ,
413463 {
414464 let mut asm = assemble:: StreamAssembler :: new ( ) ;
465+ let render_image = !self . no_ocr ;
415466 let worker = self . primary ( ) ?;
416- pdfium_backend:: for_each_page ( bytes, password, |n, _total, mut page| {
467+ pdfium_backend:: for_each_page ( bytes, password, render_image , |n, _total, mut page| {
417468 let ( nodes, links) = worker. process ( n, & mut page) ?;
418469 emit ( asm. push ( nodes) , links)
419470 } ) ?;
@@ -437,6 +488,7 @@ impl Pipeline {
437488 {
438489 self . ensure_pool ( ) ?;
439490 let n_workers = self . pool . len ( ) ;
491+ let render_image = !self . no_ocr ;
440492 let ( work_tx, work_rx) = sync_channel :: < ( usize , PdfPage ) > ( n_workers * 2 ) ;
441493 let work_rx: Arc < Mutex < Receiver < ( usize , PdfPage ) > > > = Arc :: new ( Mutex :: new ( work_rx) ) ;
442494 // Workers and the renderer report here; the calling thread drains it in
@@ -467,12 +519,16 @@ impl Pipeline {
467519 {
468520 let res_tx = res_tx. clone ( ) ;
469521 s. spawn ( move || {
470- let render =
471- pdfium_backend:: for_each_page ( bytes, password, |i, _total, page| {
522+ let render = pdfium_backend:: for_each_page (
523+ bytes,
524+ password,
525+ render_image,
526+ |i, _total, page| {
472527 work_tx
473528 . send ( ( i, page) )
474529 . map_err ( |_| PdfError :: Pdfium ( "page-worker channel closed" . into ( ) ) )
475- } ) ;
530+ } ,
531+ ) ;
476532 drop ( work_tx) ; // signal workers to finish
477533 if let Err ( e) = render {
478534 let _ = res_tx. send ( Err ( e) ) ;
@@ -527,9 +583,10 @@ impl Pipeline {
527583 }
528584 let intra = pdf_intra ( ) ;
529585 let no_table_former = self . no_table_former ;
586+ let no_ocr = self . no_ocr ;
530587 let loaded: Vec < Result < Worker , PdfError > > = std:: thread:: scope ( |s| {
531588 let handles: Vec < _ > = ( 0 ..need)
532- . map ( |_| s. spawn ( move || Worker :: load ( intra, no_table_former) ) )
589+ . map ( |_| s. spawn ( move || Worker :: load ( intra, no_table_former, no_ocr ) ) )
533590 . collect ( ) ;
534591 handles. into_iter ( ) . map ( |h| h. join ( ) . unwrap ( ) ) . collect ( )
535592 } ) ;
@@ -587,53 +644,62 @@ pub fn convert(
587644 password : Option < & str > ,
588645 name : & str ,
589646) -> Result < DoclingDocument , PdfError > {
590- convert_with_options ( bytes, password, name, false )
647+ convert_with_options ( bytes, password, name, false , false )
591648}
592649
593650/// Like [`convert`], but optionally skips loading/running TableFormer (see
594- /// [`Pipeline::no_table_former`]).
651+ /// [`Pipeline::no_table_former`]) and/or layout+OCR+TableFormer entirely (see
652+ /// [`Pipeline::no_ocr`]).
595653pub fn convert_with_options (
596654 bytes : & [ u8 ] ,
597655 password : Option < & str > ,
598656 name : & str ,
599657 no_table_former : bool ,
658+ no_ocr : bool ,
600659) -> Result < DoclingDocument , PdfError > {
601660 Pipeline :: new ( ) ?
602661 . no_table_former ( no_table_former)
662+ . no_ocr ( no_ocr)
603663 . convert ( bytes, password, name)
604664}
605665
606666/// Convenience one-shot image conversion (loads the pipeline per call).
607667pub fn convert_image ( bytes : & [ u8 ] , name : & str ) -> Result < DoclingDocument , PdfError > {
608- convert_image_with_options ( bytes, name, false )
668+ convert_image_with_options ( bytes, name, false , false )
609669}
610670
611671/// Like [`convert_image`], but optionally skips loading/running TableFormer (see
612- /// [`Pipeline::no_table_former`]).
672+ /// [`Pipeline::no_table_former`]) and/or layout+OCR+TableFormer entirely (see
673+ /// [`Pipeline::no_ocr`]).
613674pub fn convert_image_with_options (
614675 bytes : & [ u8 ] ,
615676 name : & str ,
616677 no_table_former : bool ,
678+ no_ocr : bool ,
617679) -> Result < DoclingDocument , PdfError > {
618680 Pipeline :: new ( ) ?
619681 . no_table_former ( no_table_former)
682+ . no_ocr ( no_ocr)
620683 . convert_image ( bytes, name)
621684}
622685
623686/// Convert pre-segmented pages (image + already-known text cells, e.g. METS/hOCR
624687/// scans) through the shared layout + assembly pipeline.
625688pub fn convert_pages ( pages : Vec < PdfPage > , name : & str ) -> Result < DoclingDocument , PdfError > {
626- convert_pages_with_options ( pages, name, false )
689+ convert_pages_with_options ( pages, name, false , false )
627690}
628691
629692/// Like [`convert_pages`], but optionally skips loading/running TableFormer (see
630- /// [`Pipeline::no_table_former`]).
693+ /// [`Pipeline::no_table_former`]) and/or layout+OCR+TableFormer entirely (see
694+ /// [`Pipeline::no_ocr`]).
631695pub fn convert_pages_with_options (
632696 pages : Vec < PdfPage > ,
633697 name : & str ,
634698 no_table_former : bool ,
699+ no_ocr : bool ,
635700) -> Result < DoclingDocument , PdfError > {
636701 Pipeline :: new ( ) ?
637702 . no_table_former ( no_table_former)
703+ . no_ocr ( no_ocr)
638704 . process_pages ( pages, name)
639705}
0 commit comments