@@ -526,21 +526,72 @@ pub fn group_into_lines(items: Vec<TextItem>) -> Vec<TextLine> {
526526 all_lines
527527}
528528
529+ /// Determine if Y-sorting should be used instead of stream order.
530+ /// Returns true if the stream order appears chaotic (items jump around in Y position).
531+ fn should_use_y_sorting ( items : & [ TextItem ] ) -> bool {
532+ if items. len ( ) < 5 {
533+ return false ; // Not enough items to judge
534+ }
535+
536+ // Sample Y positions from stream order
537+ let y_positions: Vec < f32 > = items. iter ( ) . map ( |i| i. y ) . collect ( ) ;
538+
539+ // Count "order violations" - cases where Y increases (going up) when it should decrease
540+ // In proper reading order, Y should generally decrease (top to bottom)
541+ let mut large_jumps_up = 0 ;
542+ let mut large_jumps_down = 0 ;
543+ let jump_threshold = 50.0 ; // Significant Y jump
544+
545+ for window in y_positions. windows ( 2 ) {
546+ let delta = window[ 1 ] - window[ 0 ] ;
547+ if delta > jump_threshold {
548+ large_jumps_up += 1 ; // Y increased significantly (jumped up on page)
549+ } else if delta < -jump_threshold {
550+ large_jumps_down += 1 ; // Y decreased significantly (normal reading direction)
551+ }
552+ }
553+
554+ // If there are many upward jumps relative to downward jumps, order is chaotic
555+ // A well-ordered document should have mostly downward progression
556+ let total_jumps = large_jumps_up + large_jumps_down;
557+ if total_jumps < 3 {
558+ return false ; // Not enough jumps to judge
559+ }
560+
561+ // If more than 40% of large jumps are upward, use Y-sorting
562+ let chaos_ratio = large_jumps_up as f32 / total_jumps as f32 ;
563+ chaos_ratio > 0.4
564+ }
565+
529566/// Group items from a single column into lines
530- /// Preserves PDF stream order (which is typically reading order) and only groups
531- /// consecutive items on the same line by their X position.
567+ /// Uses heuristics to decide between PDF stream order and Y-position sorting.
532568fn group_single_column ( items : Vec < TextItem > ) -> Vec < TextLine > {
533569 if items. is_empty ( ) {
534570 return Vec :: new ( ) ;
535571 }
536572
537- // DO NOT sort by Y - preserve PDF stream order which is usually reading order
538- // Only merge consecutive items that are on the same line (same Y within tolerance)
573+ // Decide whether to use stream order or Y-sorting
574+ let use_y_sorting = should_use_y_sorting ( & items) ;
575+
576+ let items = if use_y_sorting {
577+ // Sort by Y descending (top to bottom in PDF coords)
578+ let mut sorted = items;
579+ sorted. sort_by ( |a, b| {
580+ b. y . partial_cmp ( & a. y )
581+ . unwrap_or ( std:: cmp:: Ordering :: Equal )
582+ . then ( a. x . partial_cmp ( & b. x ) . unwrap_or ( std:: cmp:: Ordering :: Equal ) )
583+ } ) ;
584+ sorted
585+ } else {
586+ items
587+ } ;
588+
589+ // Group items into lines
539590 let mut lines: Vec < TextLine > = Vec :: new ( ) ;
540591 let y_tolerance = 3.0 ;
541592
542593 for item in items {
543- // Only check the most recent line for merging (to preserve stream order)
594+ // Only check the most recent line for merging
544595 let should_merge = lines. last ( ) . map_or ( false , |last_line| {
545596 last_line. page == item. page && ( last_line. y - item. y ) . abs ( ) < y_tolerance
546597 } ) ;
0 commit comments