@@ -138,7 +138,7 @@ impl PdfDocument {
138138 let mut pages = Vec :: new ( ) ;
139139 for ( i, page) in doc. pages ( ) . iter ( ) . enumerate ( ) {
140140 let rc = rust. as_mut ( ) . and_then ( |v| v. get_mut ( i) . map ( std:: mem:: take) ) ;
141- pages. push ( extract_page ( & page, & ffi, i as i32 , rc) ?) ;
141+ pages. push ( extract_page ( & page, & ffi, i as i32 , rc, true ) ?) ;
142142 }
143143 Ok ( PdfDocument { pages } )
144144 }
@@ -173,8 +173,20 @@ pub fn page_count(bytes: &[u8], password: Option<&str>) -> Result<usize, PdfiumE
173173/// large PDF would otherwise hold gigabytes of bitmaps at once. `f` receives the
174174/// zero-based page index and the total page count.
175175///
176+ /// `render_image` controls whether the page bitmap is rasterized at all: layout,
177+ /// OCR, TableFormer, and picture cropping all need it, but a caller that skips
178+ /// every one of those (the `no_ocr` fast path) doesn't, and rasterizing +
179+ /// downsampling a page is by far the most expensive step per page — skipping it
180+ /// is most of `no_ocr`'s speedup. `PdfPage::image` is a 1×1 placeholder when
181+ /// `false`; do not read it.
182+ ///
176183/// `E` is the caller's error type; pdfium errors convert into it via `From`.
177- pub fn for_each_page < E , F > ( bytes : & [ u8 ] , password : Option < & str > , mut f : F ) -> Result < ( ) , E >
184+ pub fn for_each_page < E , F > (
185+ bytes : & [ u8 ] ,
186+ password : Option < & str > ,
187+ render_image : bool ,
188+ mut f : F ,
189+ ) -> Result < ( ) , E >
178190where
179191 E : From < PdfiumError > ,
180192 F : FnMut ( usize , usize , PdfPage ) -> Result < ( ) , E > ,
@@ -187,7 +199,7 @@ where
187199 let total = pages. len ( ) as usize ;
188200 for ( i, page) in pages. iter ( ) . enumerate ( ) {
189201 let rc = rust. as_mut ( ) . and_then ( |v| v. get_mut ( i) . map ( std:: mem:: take) ) ;
190- let extracted = extract_page ( & page, & ffi, i as i32 , rc) ?;
202+ let extracted = extract_page ( & page, & ffi, i as i32 , rc, render_image ) ?;
191203 f ( i, total, extracted) ?;
192204 }
193205 Ok ( ( ) )
@@ -198,52 +210,69 @@ fn extract_page(
198210 ffi : & FfiText < ' _ > ,
199211 index : i32 ,
200212 rust_cells : Option < crate :: textparse:: PageParserCells > ,
213+ render_image : bool ,
201214) -> Result < PdfPage , PdfiumError > {
202215 let width = page. width ( ) . value ;
203216 let height = page. height ( ) . value ;
204217
205- let ( mut cells, mut code_cells, mut word_cells) =
206- crate :: timing:: timed ( "ffi.page_cells" , || ffi. page_cells ( index, height) ) ;
207- if cells. is_empty ( ) {
208- cells = segment_cells ( & page. text ( ) ?, height) ;
209- }
210218 // Default: use the pure-Rust text parser instead of pdfium's text layer
211219 // (override with `DOCLING_PDFIUM_TEXT`). Prose line cells always come from the
212220 // parser; word and code cells do too unless `DOCLING_PDFIUM_WORDS` keeps them
213221 // on pdfium (the parser's word grouping reproduces docling-parse's, which
214222 // TableFormer matches against — roadmap item 6). A page the parser couldn't
215223 // read (no text layer) keeps pdfium's cells.
216- if let Some ( rc) = rust_cells {
217- if !rc. prose . is_empty ( ) {
218- cells = rc. prose ;
219- }
220- if use_parser_words ( ) && !rc. words . is_empty ( ) {
221- word_cells = rc. words ;
222- }
223- if use_parser_code ( ) && !rc. code . is_empty ( ) {
224- code_cells = rc. code ;
225- }
224+ let rc = rust_cells. unwrap_or_default ( ) ;
225+ let need_pdfium_prose = rc. prose . is_empty ( ) ;
226+ let need_pdfium_words = !use_parser_words ( ) || rc. words . is_empty ( ) ;
227+ let need_pdfium_code = !use_parser_code ( ) || rc. code . is_empty ( ) ;
228+
229+ // The parser covers prose/words/code from one shared glyph pass, so on the
230+ // common (parser-succeeded) page all three are already satisfied and this
231+ // pdfium FFI call — otherwise fully discarded below — is skipped outright.
232+ let ( mut cells, mut code_cells, mut word_cells) =
233+ if need_pdfium_prose || need_pdfium_words || need_pdfium_code {
234+ let ( mut cells, code_cells, word_cells) =
235+ crate :: timing:: timed ( "ffi.page_cells" , || ffi. page_cells ( index, height) ) ;
236+ if cells. is_empty ( ) {
237+ cells = segment_cells ( & page. text ( ) ?, height) ;
238+ }
239+ ( cells, code_cells, word_cells)
240+ } else {
241+ ( Vec :: new ( ) , Vec :: new ( ) , Vec :: new ( ) )
242+ } ;
243+ if !rc. prose . is_empty ( ) {
244+ cells = rc. prose ;
245+ }
246+ if use_parser_words ( ) && !rc. words . is_empty ( ) {
247+ word_cells = rc. words ;
248+ }
249+ if use_parser_code ( ) && !rc. code . is_empty ( ) {
250+ code_cells = rc. code ;
226251 }
227252
228- // docling renders at 1.5× the target scale and downsamples "to make it
229- // sharper" (pypdfium2 → PIL BICUBIC). Replicate exactly: the TableFormer
230- // model is pixel-sensitive, so the page bitmap must match byte-for-byte.
231- // `CatmullRom` is the same a=-0.5 cubic kernel as PIL's BICUBIC.
232- const SUPERSAMPLE : f32 = 1.5 ;
233- let tw = ( width * RENDER_SCALE * SUPERSAMPLE ) . round ( ) . max ( 1.0 ) as i32 ;
234- let th = ( height * RENDER_SCALE * SUPERSAMPLE ) . round ( ) . max ( 1.0 ) as i32 ;
235- let cfg = PdfRenderConfig :: new ( )
236- . set_target_width ( tw)
237- . set_target_height ( th) ;
238- let big = crate :: timing:: timed ( "pdfium.render" , || {
239- page. render_with_config ( & cfg)
240- . map ( |b| b. as_image ( ) . into_rgb8 ( ) )
241- } ) ?;
242- let dw = ( width * RENDER_SCALE ) . round ( ) . max ( 1.0 ) as u32 ;
243- let dh = ( height * RENDER_SCALE ) . round ( ) . max ( 1.0 ) as u32 ;
244- let image = crate :: timing:: timed ( "image.resize" , || {
245- image:: imageops:: resize ( & big, dw, dh, image:: imageops:: FilterType :: CatmullRom )
246- } ) ;
253+ let image = if render_image {
254+ // docling renders at 1.5× the target scale and downsamples "to make it
255+ // sharper" (pypdfium2 → PIL BICUBIC). Replicate exactly: the TableFormer
256+ // model is pixel-sensitive, so the page bitmap must match byte-for-byte.
257+ // `CatmullRom` is the same a=-0.5 cubic kernel as PIL's BICUBIC.
258+ const SUPERSAMPLE : f32 = 1.5 ;
259+ let tw = ( width * RENDER_SCALE * SUPERSAMPLE ) . round ( ) . max ( 1.0 ) as i32 ;
260+ let th = ( height * RENDER_SCALE * SUPERSAMPLE ) . round ( ) . max ( 1.0 ) as i32 ;
261+ let cfg = PdfRenderConfig :: new ( )
262+ . set_target_width ( tw)
263+ . set_target_height ( th) ;
264+ let big = crate :: timing:: timed ( "pdfium.render" , || {
265+ page. render_with_config ( & cfg)
266+ . map ( |b| b. as_image ( ) . into_rgb8 ( ) )
267+ } ) ?;
268+ let dw = ( width * RENDER_SCALE ) . round ( ) . max ( 1.0 ) as u32 ;
269+ let dh = ( height * RENDER_SCALE ) . round ( ) . max ( 1.0 ) as u32 ;
270+ crate :: timing:: timed ( "image.resize" , || {
271+ image:: imageops:: resize ( & big, dw, dh, image:: imageops:: FilterType :: CatmullRom )
272+ } )
273+ } else {
274+ RgbImage :: new ( 1 , 1 )
275+ } ;
247276
248277 Ok ( PdfPage {
249278 width,
0 commit comments