edgeparse 0.2.2__tar.gz → 0.2.4__tar.gz

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.
Files changed (226) hide show
  1. {edgeparse-0.2.2 → edgeparse-0.2.4}/Cargo.lock +5 -5
  2. {edgeparse-0.2.2 → edgeparse-0.2.4}/Cargo.toml +1 -1
  3. {edgeparse-0.2.2 → edgeparse-0.2.4}/PKG-INFO +1 -1
  4. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/api/config.rs +29 -0
  5. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/lib.rs +76 -9
  6. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/output/markdown.rs +269 -0
  7. edgeparse-0.2.4/crates/edgeparse-core/src/pdf/raster_table_ocr.rs +5333 -0
  8. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/orchestrator.rs +171 -87
  9. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/cluster_table_detector.rs +75 -1
  10. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/table_detector.rs +286 -3
  11. edgeparse-0.2.2/crates/edgeparse-core/src/pdf/raster_table_ocr.rs +0 -1607
  12. {edgeparse-0.2.2 → edgeparse-0.2.4}/README.md +0 -0
  13. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/Cargo.toml +0 -0
  14. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/README.md +0 -0
  15. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/api/batch.rs +0 -0
  16. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/api/config_loader.rs +0 -0
  17. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/api/filter.rs +0 -0
  18. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/api/mod.rs +0 -0
  19. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/hybrid.rs +0 -0
  20. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/models/bbox.rs +0 -0
  21. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/models/chunks.rs +0 -0
  22. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/models/content.rs +0 -0
  23. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/models/document.rs +0 -0
  24. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/models/enums.rs +0 -0
  25. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/models/list.rs +0 -0
  26. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/models/mod.rs +0 -0
  27. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/models/semantic.rs +0 -0
  28. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/models/table.rs +0 -0
  29. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/models/text.rs +0 -0
  30. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/output/csv.rs +0 -0
  31. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/output/docx.rs +0 -0
  32. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/output/html.rs +0 -0
  33. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/output/json.rs +0 -0
  34. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/output/legacy_json.rs +0 -0
  35. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/output/mod.rs +0 -0
  36. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/output/text.rs +0 -0
  37. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/output/toc_builder.rs +0 -0
  38. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pdf/annotation_enrichment.rs +0 -0
  39. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pdf/annotation_extractor.rs +0 -0
  40. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pdf/bookmark_extractor.rs +0 -0
  41. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pdf/chunk_parser.rs +0 -0
  42. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pdf/data/glyphlist.txt +0 -0
  43. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pdf/encryption.rs +0 -0
  44. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pdf/font.rs +0 -0
  45. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pdf/form_extractor.rs +0 -0
  46. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pdf/graphics_state.rs +0 -0
  47. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pdf/hyperlink_extractor.rs +0 -0
  48. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pdf/image_extractor.rs +0 -0
  49. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pdf/line_extractor.rs +0 -0
  50. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pdf/loader.rs +0 -0
  51. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pdf/metadata_writer.rs +0 -0
  52. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pdf/mod.rs +0 -0
  53. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pdf/page_info.rs +0 -0
  54. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pdf/text_extractor.rs +0 -0
  55. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/error_recovery.rs +0 -0
  56. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/logging.rs +0 -0
  57. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/mod.rs +0 -0
  58. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/parallel.rs +0 -0
  59. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/boxed_heading_promoter.rs +0 -0
  60. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/caption_linker.rs +0 -0
  61. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/column_detector.rs +0 -0
  62. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/content_filter.rs +0 -0
  63. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/content_sanitizer.rs +0 -0
  64. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/cross_page_linker.rs +0 -0
  65. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/figure_detector.rs +0 -0
  66. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/footnote_detector.rs +0 -0
  67. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/footnote_linker.rs +0 -0
  68. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/header_footer.rs +0 -0
  69. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/heading_detector.rs +0 -0
  70. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/id_assignment.rs +0 -0
  71. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/list_detector.rs +0 -0
  72. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/list_pass2.rs +0 -0
  73. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/mod.rs +0 -0
  74. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/nesting_level.rs +0 -0
  75. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/output_builder.rs +0 -0
  76. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/paragraph_detector.rs +0 -0
  77. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/reading_order.rs +0 -0
  78. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/table_content_assigner.rs +0 -0
  79. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/text_block_grouper.rs +0 -0
  80. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/text_line_grouper.rs +0 -0
  81. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/toc_detector.rs +0 -0
  82. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/pipeline/stages/watermark_detector.rs +0 -0
  83. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/tagged/mod.rs +0 -0
  84. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/tagged/processor.rs +0 -0
  85. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/tagged/struct_tree.rs +0 -0
  86. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/utils/diff.rs +0 -0
  87. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/utils/font_metrics_cache.rs +0 -0
  88. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/utils/image_dedup.rs +0 -0
  89. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/utils/language_detector.rs +0 -0
  90. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/utils/layout_analysis.rs +0 -0
  91. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/utils/mod.rs +0 -0
  92. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/utils/page_range.rs +0 -0
  93. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/utils/sanitizer.rs +0 -0
  94. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/utils/statistics.rs +0 -0
  95. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/utils/text_normalizer.rs +0 -0
  96. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/utils/xref_index.rs +0 -0
  97. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/src/utils/xycut.rs +0 -0
  98. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/tests/integration_tests.rs +0 -0
  99. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-core/tests/perf_benchmarks.rs +0 -0
  100. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-python/Cargo.toml +0 -0
  101. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/edgeparse-python/src/lib.rs +0 -0
  102. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/.cargo_vcs_info.json +0 -0
  103. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/.chglog/CHANGELOG.tpl.md +0 -0
  104. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/.chglog/config.yml +0 -0
  105. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/.editorconfig +0 -0
  106. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/.gitattributes +0 -0
  107. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/.github/workflows/ci.yml +0 -0
  108. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/.gitignore +0 -0
  109. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/.vscode/launch.json +0 -0
  110. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/.vscode/settings.json +0 -0
  111. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/CHANGELOG.md +0 -0
  112. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/Cargo.toml +0 -0
  113. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/LICENSE +0 -0
  114. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/README.md +0 -0
  115. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/assets/AnnotationDemo.pdf +0 -0
  116. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/assets/Incremental.pdf +0 -0
  117. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/assets/encrypted.pdf +0 -0
  118. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/assets/example.pdf +0 -0
  119. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/assets/pdf_icon.jpg +0 -0
  120. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/assets/supported_color_type/L16.png +0 -0
  121. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/assets/supported_color_type/L8.jpg +0 -0
  122. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/assets/supported_color_type/La16.png +0 -0
  123. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/assets/supported_color_type/La8.png +0 -0
  124. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/assets/supported_color_type/Rgb16.png +0 -0
  125. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/assets/supported_color_type/Rgb8.jpg +0 -0
  126. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/assets/supported_color_type/Rgba16.png +0 -0
  127. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/assets/supported_color_type/Rgba8.png +0 -0
  128. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/assets/unicode.pdf +0 -0
  129. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/benches/datetime.rs +0 -0
  130. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/benches/parse.rs +0 -0
  131. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/add_barcode.rs +0 -0
  132. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/analyze_object_streams.rs +0 -0
  133. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/analyze_page_contents.rs +0 -0
  134. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/analyze_pdf.rs +0 -0
  135. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/analyze_references.rs +0 -0
  136. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/check_raw_objstream.rs +0 -0
  137. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/compress_existing_pdf.rs +0 -0
  138. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/create.rs +0 -0
  139. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/create_text_heavy_pdf.rs +0 -0
  140. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/debug_compression_detailed.rs +0 -0
  141. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/debug_compression_full.rs +0 -0
  142. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/debug_object_stream.rs +0 -0
  143. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/debug_save_with_objstreams.rs +0 -0
  144. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/decrypt.rs +0 -0
  145. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/encrypt.rs +0 -0
  146. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/extract_text.rs +0 -0
  147. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/extract_toc.rs +0 -0
  148. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/final_compression_test.rs +0 -0
  149. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/inspect_object_stream.rs +0 -0
  150. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/merge.rs +0 -0
  151. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/object_streams.rs +0 -0
  152. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/print_annotations.rs +0 -0
  153. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/replace_partial_text.rs +0 -0
  154. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/rotate.rs +0 -0
  155. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/test_better_compression.rs +0 -0
  156. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/test_decryption.rs +0 -0
  157. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/test_direct_objstream.rs +0 -0
  158. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/test_filter_roundtrip.rs +0 -0
  159. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/test_metadata.rs +0 -0
  160. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/test_object_stream_creation.rs +0 -0
  161. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/test_object_stream_roundtrip.rs +0 -0
  162. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/test_real_pdf_compression_with_fix.rs +0 -0
  163. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/test_structural_compression.rs +0 -0
  164. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/verify_decryption.rs +0 -0
  165. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/verify_page_compression.rs +0 -0
  166. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/verify_pdf.rs +0 -0
  167. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/examples/verify_trailer_compression_fix.rs +0 -0
  168. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/rustfmt.toml +0 -0
  169. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/bookmarks.rs +0 -0
  170. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/cmap_section.rs +0 -0
  171. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/common_data_structures/mod.rs +0 -0
  172. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/content.rs +0 -0
  173. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/creator.rs +0 -0
  174. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/datetime.rs +0 -0
  175. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/destinations.rs +0 -0
  176. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/document.rs +0 -0
  177. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/encodings/cmap.rs +0 -0
  178. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/encodings/glyphnames.rs +0 -0
  179. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/encodings/mappings.rs +0 -0
  180. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/encodings/mod.rs +0 -0
  181. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/encryption/algorithms.rs +0 -0
  182. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/encryption/crypt_filters.rs +0 -0
  183. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/encryption/pkcs5.rs +0 -0
  184. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/encryption/rc4.rs +0 -0
  185. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/encryption.rs +0 -0
  186. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/error.rs +0 -0
  187. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/filters/mod.rs +0 -0
  188. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/filters/png.rs +0 -0
  189. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/font.rs +0 -0
  190. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/incremental_document.rs +0 -0
  191. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/lib.rs +0 -0
  192. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/object.rs +0 -0
  193. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/object_stream.rs +0 -0
  194. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/outlines.rs +0 -0
  195. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/parser/cmap_parser.rs +0 -0
  196. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/parser/mod.rs +0 -0
  197. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/parser_aux.rs +0 -0
  198. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/processor.rs +0 -0
  199. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/reader.rs +0 -0
  200. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/save_options.rs +0 -0
  201. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/toc.rs +0 -0
  202. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/writer.rs +0 -0
  203. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/xobject.rs +0 -0
  204. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/src/xref.rs +0 -0
  205. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/tests/annotation.rs +0 -0
  206. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/tests/catalog_compression_integration_test.rs +0 -0
  207. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/tests/decryption.rs +0 -0
  208. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/tests/extract_test.rs +0 -0
  209. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/tests/font_test.rs +0 -0
  210. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/tests/incremental_document.rs +0 -0
  211. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/tests/metadata_test.rs +0 -0
  212. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/tests/modify.rs +0 -0
  213. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/tests/object_stream_comprehensive_test.rs +0 -0
  214. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/tests/object_stream_edge_cases_test.rs +0 -0
  215. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/tests/object_stream_methods_test.rs +0 -0
  216. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/tests/object_stream_performance_test.rs +0 -0
  217. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/tests/resources/fonts/Montserrat-Regular.ttf +0 -0
  218. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/tests/simple_object_stream_test.rs +0 -0
  219. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/tests/trailer_reference_compression_test.rs +0 -0
  220. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/tests/unicode.rs +0 -0
  221. {edgeparse-0.2.2 → edgeparse-0.2.4}/crates/pdf-cos/tests/utils.rs +0 -0
  222. {edgeparse-0.2.2 → edgeparse-0.2.4}/edgeparse/__init__.py +0 -0
  223. {edgeparse-0.2.2 → edgeparse-0.2.4}/edgeparse/_types.py +0 -0
  224. {edgeparse-0.2.2 → edgeparse-0.2.4}/edgeparse/cli.py +0 -0
  225. {edgeparse-0.2.2 → edgeparse-0.2.4}/edgeparse/py.typed +0 -0
  226. {edgeparse-0.2.2 → edgeparse-0.2.4}/pyproject.toml +0 -0
@@ -636,7 +636,7 @@ dependencies = [
636
636
 
637
637
  [[package]]
638
638
  name = "edgeparse-cli"
639
- version = "0.2.2"
639
+ version = "0.2.4"
640
640
  dependencies = [
641
641
  "anyhow",
642
642
  "clap",
@@ -650,7 +650,7 @@ dependencies = [
650
650
 
651
651
  [[package]]
652
652
  name = "edgeparse-core"
653
- version = "0.2.2"
653
+ version = "0.2.4"
654
654
  dependencies = [
655
655
  "anyhow",
656
656
  "base64",
@@ -674,7 +674,7 @@ dependencies = [
674
674
 
675
675
  [[package]]
676
676
  name = "edgeparse-node"
677
- version = "0.2.2"
677
+ version = "0.2.4"
678
678
  dependencies = [
679
679
  "edgeparse-core",
680
680
  "napi",
@@ -686,7 +686,7 @@ dependencies = [
686
686
 
687
687
  [[package]]
688
688
  name = "edgeparse-python"
689
- version = "0.2.2"
689
+ version = "0.2.4"
690
690
  dependencies = [
691
691
  "edgeparse-core",
692
692
  "pyo3",
@@ -695,7 +695,7 @@ dependencies = [
695
695
 
696
696
  [[package]]
697
697
  name = "edgeparse-wasm"
698
- version = "0.2.2"
698
+ version = "0.2.4"
699
699
  dependencies = [
700
700
  "console_error_panic_hook",
701
701
  "console_log",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/edgeparse-core", "crates/pdf-cos", "crates/edgeparse-python"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.2.2"
6
+ version = "0.2.4"
7
7
  edition = "2021"
8
8
  rust-version = "1.85"
9
9
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edgeparse
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: Apache Software License
@@ -164,6 +164,21 @@ impl Default for ProcessingConfig {
164
164
  }
165
165
  }
166
166
 
167
+ impl ProcessingConfig {
168
+ /// Returns true when a hybrid backend is configured.
169
+ pub fn hybrid_enabled(&self) -> bool {
170
+ !matches!(self.hybrid, HybridBackend::Off)
171
+ }
172
+
173
+ /// Returns true when local OCR recovery should run.
174
+ ///
175
+ /// OCR is only active in hybrid mode and can still be disabled explicitly
176
+ /// via `raster_table_ocr`.
177
+ pub fn raster_table_ocr_enabled(&self) -> bool {
178
+ self.raster_table_ocr && self.hybrid_enabled()
179
+ }
180
+ }
181
+
167
182
  #[cfg(test)]
168
183
  mod tests {
169
184
  use super::*;
@@ -182,4 +197,18 @@ mod tests {
182
197
  assert_eq!(config.hybrid, HybridBackend::Off);
183
198
  assert_eq!(config.hybrid_timeout, 30000);
184
199
  }
200
+
201
+ #[test]
202
+ fn test_raster_table_ocr_requires_hybrid_mode() {
203
+ let mut config = ProcessingConfig::default();
204
+ assert!(!config.hybrid_enabled());
205
+ assert!(!config.raster_table_ocr_enabled());
206
+
207
+ config.hybrid = HybridBackend::DoclingFast;
208
+ assert!(config.hybrid_enabled());
209
+ assert!(config.raster_table_ocr_enabled());
210
+
211
+ config.raster_table_ocr = false;
212
+ assert!(!config.raster_table_ocr_enabled());
213
+ }
185
214
  }
@@ -25,10 +25,12 @@ use crate::pdf::chunk_parser::extract_page_chunks;
25
25
  use crate::pdf::page_info;
26
26
  #[cfg(not(target_arch = "wasm32"))]
27
27
  use crate::pdf::raster_table_ocr::{
28
- recover_page_raster_table_cell_text, recover_raster_table_borders,
28
+ recover_dominant_image_text_chunks, recover_page_raster_table_cell_text,
29
+ recover_raster_table_borders,
29
30
  };
30
31
  use crate::pipeline::orchestrator::{run_pipeline, PipelineState};
31
32
  use crate::tagged::struct_tree::build_mcid_map;
33
+ use std::time::Instant;
32
34
 
33
35
  /// Main entry point: convert a PDF file to structured data.
34
36
  ///
@@ -46,23 +48,50 @@ pub fn convert(
46
48
  input_path: &std::path::Path,
47
49
  config: &ProcessingConfig,
48
50
  ) -> Result<PdfDocument, EdgePdfError> {
51
+ let timing_enabled = timing_enabled();
52
+ let total_start = Instant::now();
53
+
54
+ let phase_start = Instant::now();
49
55
  let raw_doc = pdf::loader::load_pdf(input_path, config.password.as_deref())?;
56
+ log_phase_duration(timing_enabled, "load_pdf", phase_start);
50
57
 
51
58
  // Extract per-page geometry (MediaBox, CropBox, rotation) for use throughout the pipeline.
59
+ let phase_start = Instant::now();
52
60
  let page_info_list = page_info::extract_page_info(&raw_doc.document);
61
+ log_phase_duration(timing_enabled, "extract_page_info", phase_start);
53
62
 
54
63
  // Extract text chunks from each page
55
64
  let pages_map = raw_doc.document.get_pages();
65
+ // Index by 1-based page number for fast lookup during optional OCR recovery.
66
+ // Keep this out of the default fast path when OCR is disabled.
67
+ let page_info_by_number: Vec<Option<&page_info::PageInfo>> =
68
+ if config.raster_table_ocr_enabled() {
69
+ let mut index = vec![None; pages_map.len().saturating_add(1)];
70
+ for info in &page_info_list {
71
+ if let Some(slot) = index.get_mut(info.page_number as usize) {
72
+ *slot = Some(info);
73
+ }
74
+ }
75
+ index
76
+ } else {
77
+ Vec::new()
78
+ };
56
79
  let mut page_contents = Vec::with_capacity(pages_map.len());
57
80
 
81
+ let phase_start = Instant::now();
58
82
  for (&page_num, &page_id) in &pages_map {
59
83
  let page_chunks = extract_page_chunks(&raw_doc.document, page_num, page_id)?;
84
+ let mut recovered_text_chunks = Vec::new();
60
85
  let mut recovered_tables = Vec::new();
61
- if config.raster_table_ocr {
62
- if let Some(page_info) = page_info_list
63
- .iter()
64
- .find(|info| info.page_number == page_num)
65
- {
86
+ if config.raster_table_ocr_enabled() {
87
+ if let Some(Some(page_info)) = page_info_by_number.get(page_num as usize) {
88
+ recovered_text_chunks = recover_dominant_image_text_chunks(
89
+ input_path,
90
+ &page_info.crop_box,
91
+ page_num,
92
+ &page_chunks.text_chunks,
93
+ &page_chunks.image_chunks,
94
+ );
66
95
  recovered_tables = recover_raster_table_borders(
67
96
  input_path,
68
97
  &page_info.crop_box,
@@ -77,6 +106,11 @@ pub fn convert(
77
106
  .into_iter()
78
107
  .map(ContentElement::TextChunk)
79
108
  .collect();
109
+ elements.extend(
110
+ recovered_text_chunks
111
+ .into_iter()
112
+ .map(ContentElement::TextChunk),
113
+ );
80
114
 
81
115
  elements.extend(
82
116
  page_chunks
@@ -104,12 +138,15 @@ pub fn convert(
104
138
 
105
139
  page_contents.push(elements);
106
140
  }
141
+ log_phase_duration(timing_enabled, "extract_page_chunks", phase_start);
107
142
 
108
143
  // Run the processing pipeline
144
+ let phase_start = Instant::now();
109
145
  let mcid_map = build_mcid_map(&raw_doc.document);
110
146
  let mut pipeline_state = PipelineState::with_mcid_map(page_contents, config.clone(), mcid_map)
111
- .with_page_info(page_info_list.clone());
147
+ .with_page_info(page_info_list);
112
148
  run_pipeline(&mut pipeline_state)?;
149
+ log_phase_duration(timing_enabled, "run_pipeline", phase_start);
113
150
 
114
151
  // Build the output document
115
152
  let file_name = input_path
@@ -126,9 +163,10 @@ pub fn convert(
126
163
  doc.creation_date = raw_doc.metadata.creation_date;
127
164
  doc.modification_date = raw_doc.metadata.modification_date;
128
165
 
129
- if config.raster_table_ocr {
166
+ let phase_start = Instant::now();
167
+ if config.raster_table_ocr_enabled() {
130
168
  for (page_idx, page) in pipeline_state.pages.iter_mut().enumerate() {
131
- if let Some(page_info) = page_info_list.get(page_idx) {
169
+ if let Some(page_info) = pipeline_state.page_info.get(page_idx) {
132
170
  recover_page_raster_table_cell_text(
133
171
  input_path,
134
172
  &page_info.crop_box,
@@ -138,11 +176,19 @@ pub fn convert(
138
176
  }
139
177
  }
140
178
  }
179
+ log_phase_duration(
180
+ timing_enabled,
181
+ "recover_page_raster_table_cell_text",
182
+ phase_start,
183
+ );
141
184
 
142
185
  // Flatten pipeline output into document kids
186
+ let phase_start = Instant::now();
143
187
  for page in pipeline_state.pages {
144
188
  doc.kids.extend(page);
145
189
  }
190
+ log_phase_duration(timing_enabled, "flatten_document", phase_start);
191
+ log_phase_duration(timing_enabled, "convert_total", total_start);
146
192
 
147
193
  Ok(doc)
148
194
  }
@@ -271,6 +317,27 @@ impl From<lopdf::Error> for EdgePdfError {
271
317
  }
272
318
  }
273
319
 
320
+ fn timing_enabled() -> bool {
321
+ std::env::var("EDGEPARSE_TIMING")
322
+ .map(|value| {
323
+ matches!(
324
+ value.to_ascii_lowercase().as_str(),
325
+ "1" | "true" | "yes" | "on"
326
+ )
327
+ })
328
+ .unwrap_or(false)
329
+ }
330
+
331
+ fn log_phase_duration(enabled: bool, phase: &str, start: Instant) {
332
+ if enabled {
333
+ log::info!(
334
+ "Timing {}: {:.2} ms",
335
+ phase,
336
+ start.elapsed().as_secs_f64() * 1000.0
337
+ );
338
+ }
339
+ }
340
+
274
341
  #[cfg(test)]
275
342
  mod tests {
276
343
  use super::*;
@@ -208,6 +208,15 @@ fn render_markdown_core(doc: &PdfDocument) -> String {
208
208
  continue;
209
209
  }
210
210
 
211
+ // Demote carried-over table header rows that were promoted to
212
+ // headings by the pipeline but only duplicate the table above.
213
+ if looks_like_table_header_duplicate_heading(doc, i, trimmed) {
214
+ output.push_str(&escape_md_line_start(trimmed));
215
+ output.push_str("\n\n");
216
+ i += 1;
217
+ continue;
218
+ }
219
+
211
220
  // Demote headings that sit in the bottom margin of the page
212
221
  // (running footers misclassified as headings by the pipeline).
213
222
  if looks_like_bottom_margin_heading(doc, i) {
@@ -1436,6 +1445,9 @@ fn render_layout_single_caption_chart_document_cached(
1436
1445
  if doc.number_of_pages != 1 {
1437
1446
  return None;
1438
1447
  }
1448
+ if document_has_populated_table(doc) {
1449
+ return None;
1450
+ }
1439
1451
 
1440
1452
  let caption_indices = doc
1441
1453
  .kids
@@ -1546,6 +1558,22 @@ fn render_layout_single_caption_chart_document_cached(
1546
1558
  Some(output.trim_end().to_string() + "\n")
1547
1559
  }
1548
1560
 
1561
+ fn document_has_populated_table(doc: &PdfDocument) -> bool {
1562
+ doc.kids.iter().any(|element| {
1563
+ table_border_from_element(element).is_some_and(|table| {
1564
+ table.num_rows >= 2
1565
+ && table.num_columns >= 2
1566
+ && table.rows.iter().any(|row| {
1567
+ row.cells
1568
+ .iter()
1569
+ .filter(|cell| !cell_text_content(cell).trim().is_empty())
1570
+ .count()
1571
+ >= 2
1572
+ })
1573
+ })
1574
+ })
1575
+ }
1576
+
1549
1577
  fn looks_like_chart_noise_element(_element: &ContentElement, text: &str) -> bool {
1550
1578
  if text.is_empty() {
1551
1579
  return false;
@@ -7814,6 +7842,9 @@ fn should_render_paragraph_as_heading(
7814
7842
  if looks_like_top_margin_running_header(doc, idx, text) {
7815
7843
  return false;
7816
7844
  }
7845
+ if looks_like_hyphenated_table_title_continuation(doc, idx, text, next) {
7846
+ return true;
7847
+ }
7817
7848
  if should_render_element_as_heading(&doc.kids[idx], text, next) {
7818
7849
  return true;
7819
7850
  }
@@ -8379,6 +8410,135 @@ fn should_render_element_as_heading(
8379
8410
  && !is_attribution
8380
8411
  }
8381
8412
 
8413
+ fn looks_like_hyphenated_table_title_continuation(
8414
+ doc: &PdfDocument,
8415
+ idx: usize,
8416
+ text: &str,
8417
+ next: Option<&ContentElement>,
8418
+ ) -> bool {
8419
+ if !matches!(
8420
+ next,
8421
+ Some(ContentElement::Table(_)) | Some(ContentElement::TableBorder(_))
8422
+ ) {
8423
+ return false;
8424
+ }
8425
+
8426
+ let trimmed = text.trim();
8427
+ if trimmed.is_empty()
8428
+ || starts_with_caption_prefix(trimmed)
8429
+ || looks_like_numbered_section(trimmed)
8430
+ || looks_like_keyword_numbered_section(trimmed)
8431
+ || !trimmed.ends_with(':')
8432
+ {
8433
+ return false;
8434
+ }
8435
+
8436
+ let word_count = trimmed.split_whitespace().count();
8437
+ if !(3..=5).contains(&word_count) || trimmed.len() > 60 {
8438
+ return false;
8439
+ }
8440
+
8441
+ let Some(first_alpha) = trimmed.chars().find(|ch| ch.is_alphabetic()) else {
8442
+ return false;
8443
+ };
8444
+ if first_alpha.is_lowercase() {
8445
+ return false;
8446
+ }
8447
+
8448
+ let Some(prev_idx) = idx.checked_sub(1) else {
8449
+ return false;
8450
+ };
8451
+ let prev_text = extract_element_text(&doc.kids[prev_idx]);
8452
+ let prev_trimmed = prev_text.trim();
8453
+ !prev_trimmed.is_empty() && prev_trimmed.ends_with('-')
8454
+ }
8455
+
8456
+ fn looks_like_table_header_duplicate_heading(doc: &PdfDocument, idx: usize, text: &str) -> bool {
8457
+ let trimmed = text.trim();
8458
+ if trimmed.is_empty()
8459
+ || starts_with_caption_prefix(trimmed)
8460
+ || looks_like_numbered_section(trimmed)
8461
+ || looks_like_keyword_numbered_section(trimmed)
8462
+ {
8463
+ return false;
8464
+ }
8465
+
8466
+ let word_count = trimmed.split_whitespace().count();
8467
+ if !(3..=10).contains(&word_count) || trimmed.len() > 96 {
8468
+ return false;
8469
+ }
8470
+
8471
+ let Some(prev_idx) = idx.checked_sub(1) else {
8472
+ return false;
8473
+ };
8474
+ let Some(previous_table) = table_border_from_element(&doc.kids[prev_idx]) else {
8475
+ return false;
8476
+ };
8477
+ if previous_table.num_columns < 3 || previous_table.rows.len() < 3 {
8478
+ return false;
8479
+ }
8480
+
8481
+ let mut rendered_rows = collect_table_border_rows(previous_table);
8482
+ if rendered_rows.is_empty() {
8483
+ return false;
8484
+ }
8485
+ merge_continuation_rows(&mut rendered_rows);
8486
+ trim_leading_table_carryover_rows(&mut rendered_rows);
8487
+
8488
+ let Some(header_row) = rendered_rows.first() else {
8489
+ return false;
8490
+ };
8491
+ let header_text = header_row
8492
+ .iter()
8493
+ .map(|cell| cell.trim())
8494
+ .filter(|cell| !cell.is_empty())
8495
+ .collect::<Vec<_>>()
8496
+ .join(" ");
8497
+ if !equivalent_heading_text(trimmed, &header_text) {
8498
+ return false;
8499
+ }
8500
+
8501
+ let page_number = doc.kids[idx].page_number();
8502
+ let mut short_fragments = 0usize;
8503
+ let mut numeric_fragments = 0usize;
8504
+
8505
+ for candidate in doc.kids.iter().skip(idx + 1) {
8506
+ if candidate.page_number() != page_number {
8507
+ break;
8508
+ }
8509
+ if matches!(
8510
+ candidate,
8511
+ ContentElement::Table(_) | ContentElement::TableBorder(_)
8512
+ ) {
8513
+ break;
8514
+ }
8515
+
8516
+ let fragment = extract_element_text(candidate);
8517
+ let fragment_trimmed = fragment.trim();
8518
+ if fragment_trimmed.is_empty()
8519
+ || looks_like_margin_page_number(doc, candidate, fragment_trimmed)
8520
+ {
8521
+ continue;
8522
+ }
8523
+
8524
+ let fragment_words = fragment_trimmed.split_whitespace().count();
8525
+ if fragment_words > 6 {
8526
+ return false;
8527
+ }
8528
+
8529
+ short_fragments += 1;
8530
+ if fragment_trimmed.chars().any(|ch| ch.is_ascii_digit()) {
8531
+ numeric_fragments += 1;
8532
+ }
8533
+
8534
+ if short_fragments >= 3 {
8535
+ break;
8536
+ }
8537
+ }
8538
+
8539
+ short_fragments >= 2 && numeric_fragments >= 1
8540
+ }
8541
+
8382
8542
  fn looks_like_top_margin_running_header(doc: &PdfDocument, idx: usize, text: &str) -> bool {
8383
8543
  let trimmed = text.trim();
8384
8544
  if trimmed.is_empty() || trimmed.split_whitespace().count() > 6 {
@@ -13033,6 +13193,32 @@ mod tests {
13033
13193
  assert!(md.contains("| kaolinite | 10 |"));
13034
13194
  }
13035
13195
 
13196
+ #[test]
13197
+ fn test_single_caption_chart_renderer_skips_documents_with_populated_tables() {
13198
+ let mut doc = PdfDocument::new("table-with-caption.pdf".to_string());
13199
+ doc.number_of_pages = 1;
13200
+ for idx in 0..10 {
13201
+ let bottom = 720.0 - idx as f64 * 18.0;
13202
+ doc.kids.push(make_paragraph(
13203
+ "Explanatory body text that should remain outside the chart-only renderer.",
13204
+ bottom,
13205
+ bottom + 10.0,
13206
+ ));
13207
+ }
13208
+ doc.kids.push(make_paragraph(
13209
+ "Figure 7.2: Kinematic Viscosity of Water at Atmospheric Pressure.",
13210
+ 150.0,
13211
+ 162.0,
13212
+ ));
13213
+ doc.kids.push(make_two_column_table(&[
13214
+ ("Temperature", "Viscosity"),
13215
+ ("20", "1.004"),
13216
+ ("25", "0.893"),
13217
+ ]));
13218
+
13219
+ assert!(render_layout_single_caption_chart_document(&doc).is_none());
13220
+ }
13221
+
13036
13222
  #[test]
13037
13223
  fn test_blank_right_column_table_is_not_misrendered_as_toc() {
13038
13224
  let mut doc = PdfDocument::new("flocculation-table.pdf".to_string());
@@ -13389,6 +13575,89 @@ mod tests {
13389
13575
  ));
13390
13576
  }
13391
13577
 
13578
+ #[test]
13579
+ fn test_hyphenated_table_title_continuation_renders_as_heading() {
13580
+ let mut doc = PdfDocument::new("hyphenated-table-title.pdf".to_string());
13581
+ doc.number_of_pages = 1;
13582
+ doc.kids.push(make_paragraph_at(
13583
+ 72.0,
13584
+ 724.0,
13585
+ 520.0,
13586
+ 738.0,
13587
+ "With this in mind, here we have the 7 key competence areas selected to form a part of Eco-",
13588
+ ));
13589
+ doc.kids.push(make_paragraph_at(
13590
+ 72.0,
13591
+ 704.0,
13592
+ 260.0,
13593
+ 718.0,
13594
+ "Circle's Competence Framework:",
13595
+ ));
13596
+ doc.kids.push(make_n_column_table(
13597
+ &[
13598
+ vec!["Eco-Circle Competence Framework"],
13599
+ vec!["#1: The 3 Rs: Recycle-Reuse-Reduce"],
13600
+ vec!["#2: Lifecycle of Circular Economy"],
13601
+ ],
13602
+ &[(140.0, 460.0)],
13603
+ ));
13604
+
13605
+ let md = to_markdown(&doc).unwrap();
13606
+ assert!(md.contains("# Circle's Competence Framework:"), "{md}");
13607
+ }
13608
+
13609
+ #[test]
13610
+ fn test_duplicate_table_header_heading_is_demoted() {
13611
+ let mut doc = PdfDocument::new("duplicate-table-header-heading.pdf".to_string());
13612
+ doc.number_of_pages = 1;
13613
+ doc.kids
13614
+ .push(make_heading("MOHAVE COMMUNITY COLLEGE BIO181"));
13615
+ doc.kids.push(make_n_column_table(
13616
+ &[
13617
+ vec![
13618
+ "",
13619
+ "Saccharometer",
13620
+ "DI Water",
13621
+ "Glucose Solution",
13622
+ "Yeast Suspension",
13623
+ ],
13624
+ vec!["1", "", "8 ml", "6 ml", "0 ml"],
13625
+ vec!["2", "", "12 ml", "0 ml", "2 ml"],
13626
+ vec!["3", "", "6 ml", "6 ml", "2 ml"],
13627
+ ],
13628
+ &[
13629
+ (72.0, 110.0),
13630
+ (110.0, 210.0),
13631
+ (210.0, 300.0),
13632
+ (300.0, 430.0),
13633
+ (430.0, 540.0),
13634
+ ],
13635
+ ));
13636
+ doc.kids.push(make_heading_at(
13637
+ 72.0,
13638
+ 92.0,
13639
+ 390.0,
13640
+ 108.0,
13641
+ "Saccharometer DI Water Glucose Solution Yeast Suspension",
13642
+ ));
13643
+ doc.kids
13644
+ .push(make_paragraph_at(72.0, 72.0, 120.0, 88.0, "below"));
13645
+ doc.kids
13646
+ .push(make_paragraph_at(72.0, 56.0, 240.0, 72.0, "1 16 ml 12 ml"));
13647
+ doc.kids
13648
+ .push(make_paragraph_at(296.0, 56.0, 340.0, 72.0, "0 ml"));
13649
+
13650
+ let md = to_markdown(&doc).unwrap();
13651
+ assert!(
13652
+ md.contains("Saccharometer DI Water Glucose Solution Yeast Suspension"),
13653
+ "{md}"
13654
+ );
13655
+ assert!(
13656
+ !md.contains("# Saccharometer DI Water Glucose Solution Yeast Suspension"),
13657
+ "{md}"
13658
+ );
13659
+ }
13660
+
13392
13661
  #[test]
13393
13662
  fn test_geometric_panel_headers_are_promoted_into_table() {
13394
13663
  let mut doc = PdfDocument::new("ai-pack-panel.pdf".to_string());