edgeparse 0.2.1__tar.gz → 0.2.3__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.1 → edgeparse-0.2.3}/Cargo.lock +5 -5
  2. {edgeparse-0.2.1 → edgeparse-0.2.3}/Cargo.toml +1 -1
  3. {edgeparse-0.2.1 → edgeparse-0.2.3}/PKG-INFO +1 -1
  4. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/api/config.rs +33 -0
  5. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/lib.rs +92 -21
  6. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/output/markdown.rs +224 -0
  7. edgeparse-0.2.3/crates/edgeparse-core/src/pdf/raster_table_ocr.rs +5047 -0
  8. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/orchestrator.rs +171 -87
  9. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/cluster_table_detector.rs +75 -1
  10. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/table_detector.rs +129 -3
  11. edgeparse-0.2.1/crates/edgeparse-core/src/pdf/raster_table_ocr.rs +0 -1607
  12. {edgeparse-0.2.1 → edgeparse-0.2.3}/README.md +0 -0
  13. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/Cargo.toml +0 -0
  14. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/README.md +0 -0
  15. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/api/batch.rs +0 -0
  16. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/api/config_loader.rs +0 -0
  17. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/api/filter.rs +0 -0
  18. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/api/mod.rs +0 -0
  19. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/hybrid.rs +0 -0
  20. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/bbox.rs +0 -0
  21. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/chunks.rs +0 -0
  22. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/content.rs +0 -0
  23. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/document.rs +0 -0
  24. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/enums.rs +0 -0
  25. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/list.rs +0 -0
  26. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/mod.rs +0 -0
  27. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/semantic.rs +0 -0
  28. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/table.rs +0 -0
  29. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/text.rs +0 -0
  30. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/output/csv.rs +0 -0
  31. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/output/docx.rs +0 -0
  32. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/output/html.rs +0 -0
  33. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/output/json.rs +0 -0
  34. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/output/legacy_json.rs +0 -0
  35. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/output/mod.rs +0 -0
  36. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/output/text.rs +0 -0
  37. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/output/toc_builder.rs +0 -0
  38. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/annotation_enrichment.rs +0 -0
  39. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/annotation_extractor.rs +0 -0
  40. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/bookmark_extractor.rs +0 -0
  41. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/chunk_parser.rs +0 -0
  42. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/data/glyphlist.txt +0 -0
  43. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/encryption.rs +0 -0
  44. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/font.rs +0 -0
  45. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/form_extractor.rs +0 -0
  46. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/graphics_state.rs +0 -0
  47. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/hyperlink_extractor.rs +0 -0
  48. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/image_extractor.rs +0 -0
  49. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/line_extractor.rs +0 -0
  50. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/loader.rs +0 -0
  51. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/metadata_writer.rs +0 -0
  52. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/mod.rs +0 -0
  53. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/page_info.rs +0 -0
  54. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/text_extractor.rs +0 -0
  55. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/error_recovery.rs +0 -0
  56. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/logging.rs +0 -0
  57. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/mod.rs +0 -0
  58. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/parallel.rs +0 -0
  59. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/boxed_heading_promoter.rs +0 -0
  60. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/caption_linker.rs +0 -0
  61. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/column_detector.rs +0 -0
  62. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/content_filter.rs +0 -0
  63. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/content_sanitizer.rs +0 -0
  64. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/cross_page_linker.rs +0 -0
  65. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/figure_detector.rs +0 -0
  66. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/footnote_detector.rs +0 -0
  67. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/footnote_linker.rs +0 -0
  68. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/header_footer.rs +0 -0
  69. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/heading_detector.rs +0 -0
  70. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/id_assignment.rs +0 -0
  71. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/list_detector.rs +0 -0
  72. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/list_pass2.rs +0 -0
  73. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/mod.rs +0 -0
  74. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/nesting_level.rs +0 -0
  75. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/output_builder.rs +0 -0
  76. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/paragraph_detector.rs +0 -0
  77. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/reading_order.rs +0 -0
  78. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/table_content_assigner.rs +0 -0
  79. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/text_block_grouper.rs +0 -0
  80. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/text_line_grouper.rs +0 -0
  81. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/toc_detector.rs +0 -0
  82. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/watermark_detector.rs +0 -0
  83. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/tagged/mod.rs +0 -0
  84. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/tagged/processor.rs +0 -0
  85. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/tagged/struct_tree.rs +0 -0
  86. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/diff.rs +0 -0
  87. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/font_metrics_cache.rs +0 -0
  88. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/image_dedup.rs +0 -0
  89. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/language_detector.rs +0 -0
  90. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/layout_analysis.rs +0 -0
  91. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/mod.rs +0 -0
  92. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/page_range.rs +0 -0
  93. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/sanitizer.rs +0 -0
  94. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/statistics.rs +0 -0
  95. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/text_normalizer.rs +0 -0
  96. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/xref_index.rs +0 -0
  97. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/xycut.rs +0 -0
  98. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/tests/integration_tests.rs +0 -0
  99. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-core/tests/perf_benchmarks.rs +0 -0
  100. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-python/Cargo.toml +0 -0
  101. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/edgeparse-python/src/lib.rs +0 -0
  102. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/.cargo_vcs_info.json +0 -0
  103. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/.chglog/CHANGELOG.tpl.md +0 -0
  104. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/.chglog/config.yml +0 -0
  105. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/.editorconfig +0 -0
  106. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/.gitattributes +0 -0
  107. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/.github/workflows/ci.yml +0 -0
  108. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/.gitignore +0 -0
  109. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/.vscode/launch.json +0 -0
  110. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/.vscode/settings.json +0 -0
  111. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/CHANGELOG.md +0 -0
  112. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/Cargo.toml +0 -0
  113. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/LICENSE +0 -0
  114. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/README.md +0 -0
  115. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/assets/AnnotationDemo.pdf +0 -0
  116. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/assets/Incremental.pdf +0 -0
  117. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/assets/encrypted.pdf +0 -0
  118. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/assets/example.pdf +0 -0
  119. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/assets/pdf_icon.jpg +0 -0
  120. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/assets/supported_color_type/L16.png +0 -0
  121. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/assets/supported_color_type/L8.jpg +0 -0
  122. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/assets/supported_color_type/La16.png +0 -0
  123. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/assets/supported_color_type/La8.png +0 -0
  124. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/assets/supported_color_type/Rgb16.png +0 -0
  125. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/assets/supported_color_type/Rgb8.jpg +0 -0
  126. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/assets/supported_color_type/Rgba16.png +0 -0
  127. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/assets/supported_color_type/Rgba8.png +0 -0
  128. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/assets/unicode.pdf +0 -0
  129. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/benches/datetime.rs +0 -0
  130. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/benches/parse.rs +0 -0
  131. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/add_barcode.rs +0 -0
  132. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/analyze_object_streams.rs +0 -0
  133. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/analyze_page_contents.rs +0 -0
  134. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/analyze_pdf.rs +0 -0
  135. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/analyze_references.rs +0 -0
  136. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/check_raw_objstream.rs +0 -0
  137. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/compress_existing_pdf.rs +0 -0
  138. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/create.rs +0 -0
  139. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/create_text_heavy_pdf.rs +0 -0
  140. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/debug_compression_detailed.rs +0 -0
  141. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/debug_compression_full.rs +0 -0
  142. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/debug_object_stream.rs +0 -0
  143. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/debug_save_with_objstreams.rs +0 -0
  144. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/decrypt.rs +0 -0
  145. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/encrypt.rs +0 -0
  146. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/extract_text.rs +0 -0
  147. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/extract_toc.rs +0 -0
  148. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/final_compression_test.rs +0 -0
  149. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/inspect_object_stream.rs +0 -0
  150. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/merge.rs +0 -0
  151. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/object_streams.rs +0 -0
  152. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/print_annotations.rs +0 -0
  153. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/replace_partial_text.rs +0 -0
  154. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/rotate.rs +0 -0
  155. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/test_better_compression.rs +0 -0
  156. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/test_decryption.rs +0 -0
  157. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/test_direct_objstream.rs +0 -0
  158. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/test_filter_roundtrip.rs +0 -0
  159. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/test_metadata.rs +0 -0
  160. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/test_object_stream_creation.rs +0 -0
  161. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/test_object_stream_roundtrip.rs +0 -0
  162. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/test_real_pdf_compression_with_fix.rs +0 -0
  163. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/test_structural_compression.rs +0 -0
  164. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/verify_decryption.rs +0 -0
  165. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/verify_page_compression.rs +0 -0
  166. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/verify_pdf.rs +0 -0
  167. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/examples/verify_trailer_compression_fix.rs +0 -0
  168. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/rustfmt.toml +0 -0
  169. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/bookmarks.rs +0 -0
  170. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/cmap_section.rs +0 -0
  171. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/common_data_structures/mod.rs +0 -0
  172. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/content.rs +0 -0
  173. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/creator.rs +0 -0
  174. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/datetime.rs +0 -0
  175. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/destinations.rs +0 -0
  176. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/document.rs +0 -0
  177. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/encodings/cmap.rs +0 -0
  178. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/encodings/glyphnames.rs +0 -0
  179. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/encodings/mappings.rs +0 -0
  180. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/encodings/mod.rs +0 -0
  181. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/encryption/algorithms.rs +0 -0
  182. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/encryption/crypt_filters.rs +0 -0
  183. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/encryption/pkcs5.rs +0 -0
  184. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/encryption/rc4.rs +0 -0
  185. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/encryption.rs +0 -0
  186. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/error.rs +0 -0
  187. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/filters/mod.rs +0 -0
  188. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/filters/png.rs +0 -0
  189. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/font.rs +0 -0
  190. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/incremental_document.rs +0 -0
  191. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/lib.rs +0 -0
  192. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/object.rs +0 -0
  193. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/object_stream.rs +0 -0
  194. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/outlines.rs +0 -0
  195. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/parser/cmap_parser.rs +0 -0
  196. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/parser/mod.rs +0 -0
  197. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/parser_aux.rs +0 -0
  198. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/processor.rs +0 -0
  199. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/reader.rs +0 -0
  200. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/save_options.rs +0 -0
  201. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/toc.rs +0 -0
  202. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/writer.rs +0 -0
  203. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/xobject.rs +0 -0
  204. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/src/xref.rs +0 -0
  205. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/tests/annotation.rs +0 -0
  206. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/tests/catalog_compression_integration_test.rs +0 -0
  207. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/tests/decryption.rs +0 -0
  208. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/tests/extract_test.rs +0 -0
  209. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/tests/font_test.rs +0 -0
  210. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/tests/incremental_document.rs +0 -0
  211. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/tests/metadata_test.rs +0 -0
  212. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/tests/modify.rs +0 -0
  213. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/tests/object_stream_comprehensive_test.rs +0 -0
  214. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/tests/object_stream_edge_cases_test.rs +0 -0
  215. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/tests/object_stream_methods_test.rs +0 -0
  216. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/tests/object_stream_performance_test.rs +0 -0
  217. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/tests/resources/fonts/Montserrat-Regular.ttf +0 -0
  218. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/tests/simple_object_stream_test.rs +0 -0
  219. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/tests/trailer_reference_compression_test.rs +0 -0
  220. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/tests/unicode.rs +0 -0
  221. {edgeparse-0.2.1 → edgeparse-0.2.3}/crates/pdf-cos/tests/utils.rs +0 -0
  222. {edgeparse-0.2.1 → edgeparse-0.2.3}/edgeparse/__init__.py +0 -0
  223. {edgeparse-0.2.1 → edgeparse-0.2.3}/edgeparse/_types.py +0 -0
  224. {edgeparse-0.2.1 → edgeparse-0.2.3}/edgeparse/cli.py +0 -0
  225. {edgeparse-0.2.1 → edgeparse-0.2.3}/edgeparse/py.typed +0 -0
  226. {edgeparse-0.2.1 → edgeparse-0.2.3}/pyproject.toml +0 -0
@@ -636,7 +636,7 @@ dependencies = [
636
636
 
637
637
  [[package]]
638
638
  name = "edgeparse-cli"
639
- version = "0.2.1"
639
+ version = "0.2.3"
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.1"
653
+ version = "0.2.3"
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.1"
677
+ version = "0.2.3"
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.1"
689
+ version = "0.2.3"
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.1"
698
+ version = "0.2.3"
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.1"
6
+ version = "0.2.3"
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.1
3
+ Version: 0.2.3
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: Apache Software License
@@ -114,6 +114,8 @@ pub struct ProcessingConfig {
114
114
  pub image_format: ImageFormat,
115
115
  /// Directory for extracted images
116
116
  pub image_dir: Option<String>,
117
+ /// Enable raster table OCR recovery on image-based tables
118
+ pub raster_table_ocr: bool,
117
119
  /// Pages to extract (e.g., "1,3,5-7")
118
120
  pub pages: Option<String>,
119
121
  /// Include headers/footers in output
@@ -150,6 +152,7 @@ impl Default for ProcessingConfig {
150
152
  image_output: ImageOutput::External,
151
153
  image_format: ImageFormat::Png,
152
154
  image_dir: None,
155
+ raster_table_ocr: true,
153
156
  pages: None,
154
157
  include_header_footer: false,
155
158
  hybrid: HybridBackend::Off,
@@ -161,6 +164,21 @@ impl Default for ProcessingConfig {
161
164
  }
162
165
  }
163
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
+
164
182
  #[cfg(test)]
165
183
  mod tests {
166
184
  use super::*;
@@ -175,7 +193,22 @@ mod tests {
175
193
  assert_eq!(config.table_method, TableMethod::Default);
176
194
  assert_eq!(config.image_output, ImageOutput::External);
177
195
  assert_eq!(config.image_format, ImageFormat::Png);
196
+ assert!(config.raster_table_ocr);
178
197
  assert_eq!(config.hybrid, HybridBackend::Off);
179
198
  assert_eq!(config.hybrid_timeout, 30000);
180
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
+ }
181
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,35 +48,69 @@ 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 let Some(page_info) = page_info_list
62
- .iter()
63
- .find(|info| info.page_number == page_num)
64
- {
65
- recovered_tables = recover_raster_table_borders(
66
- input_path,
67
- &page_info.crop_box,
68
- page_num,
69
- &page_chunks.text_chunks,
70
- &page_chunks.image_chunks,
71
- );
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
+ );
95
+ recovered_tables = recover_raster_table_borders(
96
+ input_path,
97
+ &page_info.crop_box,
98
+ page_num,
99
+ &page_chunks.text_chunks,
100
+ &page_chunks.image_chunks,
101
+ );
102
+ }
72
103
  }
73
104
  let mut elements: Vec<ContentElement> = page_chunks
74
105
  .text_chunks
75
106
  .into_iter()
76
107
  .map(ContentElement::TextChunk)
77
108
  .collect();
109
+ elements.extend(
110
+ recovered_text_chunks
111
+ .into_iter()
112
+ .map(ContentElement::TextChunk),
113
+ );
78
114
 
79
115
  elements.extend(
80
116
  page_chunks
@@ -102,12 +138,15 @@ pub fn convert(
102
138
 
103
139
  page_contents.push(elements);
104
140
  }
141
+ log_phase_duration(timing_enabled, "extract_page_chunks", phase_start);
105
142
 
106
143
  // Run the processing pipeline
144
+ let phase_start = Instant::now();
107
145
  let mcid_map = build_mcid_map(&raw_doc.document);
108
146
  let mut pipeline_state = PipelineState::with_mcid_map(page_contents, config.clone(), mcid_map)
109
- .with_page_info(page_info_list.clone());
147
+ .with_page_info(page_info_list);
110
148
  run_pipeline(&mut pipeline_state)?;
149
+ log_phase_duration(timing_enabled, "run_pipeline", phase_start);
111
150
 
112
151
  // Build the output document
113
152
  let file_name = input_path
@@ -124,21 +163,32 @@ pub fn convert(
124
163
  doc.creation_date = raw_doc.metadata.creation_date;
125
164
  doc.modification_date = raw_doc.metadata.modification_date;
126
165
 
127
- for (page_idx, page) in pipeline_state.pages.iter_mut().enumerate() {
128
- if let Some(page_info) = page_info_list.get(page_idx) {
129
- recover_page_raster_table_cell_text(
130
- input_path,
131
- &page_info.crop_box,
132
- page_info.page_number,
133
- page,
134
- );
166
+ let phase_start = Instant::now();
167
+ if config.raster_table_ocr_enabled() {
168
+ for (page_idx, page) in pipeline_state.pages.iter_mut().enumerate() {
169
+ if let Some(page_info) = pipeline_state.page_info.get(page_idx) {
170
+ recover_page_raster_table_cell_text(
171
+ input_path,
172
+ &page_info.crop_box,
173
+ page_info.page_number,
174
+ page,
175
+ );
176
+ }
135
177
  }
136
178
  }
179
+ log_phase_duration(
180
+ timing_enabled,
181
+ "recover_page_raster_table_cell_text",
182
+ phase_start,
183
+ );
137
184
 
138
185
  // Flatten pipeline output into document kids
186
+ let phase_start = Instant::now();
139
187
  for page in pipeline_state.pages {
140
188
  doc.kids.extend(page);
141
189
  }
190
+ log_phase_duration(timing_enabled, "flatten_document", phase_start);
191
+ log_phase_duration(timing_enabled, "convert_total", total_start);
142
192
 
143
193
  Ok(doc)
144
194
  }
@@ -267,6 +317,27 @@ impl From<lopdf::Error> for EdgePdfError {
267
317
  }
268
318
  }
269
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
+
270
341
  #[cfg(test)]
271
342
  mod tests {
272
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) {
@@ -7814,6 +7823,9 @@ fn should_render_paragraph_as_heading(
7814
7823
  if looks_like_top_margin_running_header(doc, idx, text) {
7815
7824
  return false;
7816
7825
  }
7826
+ if looks_like_hyphenated_table_title_continuation(doc, idx, text, next) {
7827
+ return true;
7828
+ }
7817
7829
  if should_render_element_as_heading(&doc.kids[idx], text, next) {
7818
7830
  return true;
7819
7831
  }
@@ -8379,6 +8391,135 @@ fn should_render_element_as_heading(
8379
8391
  && !is_attribution
8380
8392
  }
8381
8393
 
8394
+ fn looks_like_hyphenated_table_title_continuation(
8395
+ doc: &PdfDocument,
8396
+ idx: usize,
8397
+ text: &str,
8398
+ next: Option<&ContentElement>,
8399
+ ) -> bool {
8400
+ if !matches!(
8401
+ next,
8402
+ Some(ContentElement::Table(_)) | Some(ContentElement::TableBorder(_))
8403
+ ) {
8404
+ return false;
8405
+ }
8406
+
8407
+ let trimmed = text.trim();
8408
+ if trimmed.is_empty()
8409
+ || starts_with_caption_prefix(trimmed)
8410
+ || looks_like_numbered_section(trimmed)
8411
+ || looks_like_keyword_numbered_section(trimmed)
8412
+ || !trimmed.ends_with(':')
8413
+ {
8414
+ return false;
8415
+ }
8416
+
8417
+ let word_count = trimmed.split_whitespace().count();
8418
+ if !(3..=5).contains(&word_count) || trimmed.len() > 60 {
8419
+ return false;
8420
+ }
8421
+
8422
+ let Some(first_alpha) = trimmed.chars().find(|ch| ch.is_alphabetic()) else {
8423
+ return false;
8424
+ };
8425
+ if first_alpha.is_lowercase() {
8426
+ return false;
8427
+ }
8428
+
8429
+ let Some(prev_idx) = idx.checked_sub(1) else {
8430
+ return false;
8431
+ };
8432
+ let prev_text = extract_element_text(&doc.kids[prev_idx]);
8433
+ let prev_trimmed = prev_text.trim();
8434
+ !prev_trimmed.is_empty() && prev_trimmed.ends_with('-')
8435
+ }
8436
+
8437
+ fn looks_like_table_header_duplicate_heading(doc: &PdfDocument, idx: usize, text: &str) -> bool {
8438
+ let trimmed = text.trim();
8439
+ if trimmed.is_empty()
8440
+ || starts_with_caption_prefix(trimmed)
8441
+ || looks_like_numbered_section(trimmed)
8442
+ || looks_like_keyword_numbered_section(trimmed)
8443
+ {
8444
+ return false;
8445
+ }
8446
+
8447
+ let word_count = trimmed.split_whitespace().count();
8448
+ if !(3..=10).contains(&word_count) || trimmed.len() > 96 {
8449
+ return false;
8450
+ }
8451
+
8452
+ let Some(prev_idx) = idx.checked_sub(1) else {
8453
+ return false;
8454
+ };
8455
+ let Some(previous_table) = table_border_from_element(&doc.kids[prev_idx]) else {
8456
+ return false;
8457
+ };
8458
+ if previous_table.num_columns < 3 || previous_table.rows.len() < 3 {
8459
+ return false;
8460
+ }
8461
+
8462
+ let mut rendered_rows = collect_table_border_rows(previous_table);
8463
+ if rendered_rows.is_empty() {
8464
+ return false;
8465
+ }
8466
+ merge_continuation_rows(&mut rendered_rows);
8467
+ trim_leading_table_carryover_rows(&mut rendered_rows);
8468
+
8469
+ let Some(header_row) = rendered_rows.first() else {
8470
+ return false;
8471
+ };
8472
+ let header_text = header_row
8473
+ .iter()
8474
+ .map(|cell| cell.trim())
8475
+ .filter(|cell| !cell.is_empty())
8476
+ .collect::<Vec<_>>()
8477
+ .join(" ");
8478
+ if !equivalent_heading_text(trimmed, &header_text) {
8479
+ return false;
8480
+ }
8481
+
8482
+ let page_number = doc.kids[idx].page_number();
8483
+ let mut short_fragments = 0usize;
8484
+ let mut numeric_fragments = 0usize;
8485
+
8486
+ for candidate in doc.kids.iter().skip(idx + 1) {
8487
+ if candidate.page_number() != page_number {
8488
+ break;
8489
+ }
8490
+ if matches!(
8491
+ candidate,
8492
+ ContentElement::Table(_) | ContentElement::TableBorder(_)
8493
+ ) {
8494
+ break;
8495
+ }
8496
+
8497
+ let fragment = extract_element_text(candidate);
8498
+ let fragment_trimmed = fragment.trim();
8499
+ if fragment_trimmed.is_empty()
8500
+ || looks_like_margin_page_number(doc, candidate, fragment_trimmed)
8501
+ {
8502
+ continue;
8503
+ }
8504
+
8505
+ let fragment_words = fragment_trimmed.split_whitespace().count();
8506
+ if fragment_words > 6 {
8507
+ return false;
8508
+ }
8509
+
8510
+ short_fragments += 1;
8511
+ if fragment_trimmed.chars().any(|ch| ch.is_ascii_digit()) {
8512
+ numeric_fragments += 1;
8513
+ }
8514
+
8515
+ if short_fragments >= 3 {
8516
+ break;
8517
+ }
8518
+ }
8519
+
8520
+ short_fragments >= 2 && numeric_fragments >= 1
8521
+ }
8522
+
8382
8523
  fn looks_like_top_margin_running_header(doc: &PdfDocument, idx: usize, text: &str) -> bool {
8383
8524
  let trimmed = text.trim();
8384
8525
  if trimmed.is_empty() || trimmed.split_whitespace().count() > 6 {
@@ -13389,6 +13530,89 @@ mod tests {
13389
13530
  ));
13390
13531
  }
13391
13532
 
13533
+ #[test]
13534
+ fn test_hyphenated_table_title_continuation_renders_as_heading() {
13535
+ let mut doc = PdfDocument::new("hyphenated-table-title.pdf".to_string());
13536
+ doc.number_of_pages = 1;
13537
+ doc.kids.push(make_paragraph_at(
13538
+ 72.0,
13539
+ 724.0,
13540
+ 520.0,
13541
+ 738.0,
13542
+ "With this in mind, here we have the 7 key competence areas selected to form a part of Eco-",
13543
+ ));
13544
+ doc.kids.push(make_paragraph_at(
13545
+ 72.0,
13546
+ 704.0,
13547
+ 260.0,
13548
+ 718.0,
13549
+ "Circle's Competence Framework:",
13550
+ ));
13551
+ doc.kids.push(make_n_column_table(
13552
+ &[
13553
+ vec!["Eco-Circle Competence Framework"],
13554
+ vec!["#1: The 3 Rs: Recycle-Reuse-Reduce"],
13555
+ vec!["#2: Lifecycle of Circular Economy"],
13556
+ ],
13557
+ &[(140.0, 460.0)],
13558
+ ));
13559
+
13560
+ let md = to_markdown(&doc).unwrap();
13561
+ assert!(md.contains("# Circle's Competence Framework:"), "{md}");
13562
+ }
13563
+
13564
+ #[test]
13565
+ fn test_duplicate_table_header_heading_is_demoted() {
13566
+ let mut doc = PdfDocument::new("duplicate-table-header-heading.pdf".to_string());
13567
+ doc.number_of_pages = 1;
13568
+ doc.kids
13569
+ .push(make_heading("MOHAVE COMMUNITY COLLEGE BIO181"));
13570
+ doc.kids.push(make_n_column_table(
13571
+ &[
13572
+ vec![
13573
+ "",
13574
+ "Saccharometer",
13575
+ "DI Water",
13576
+ "Glucose Solution",
13577
+ "Yeast Suspension",
13578
+ ],
13579
+ vec!["1", "", "8 ml", "6 ml", "0 ml"],
13580
+ vec!["2", "", "12 ml", "0 ml", "2 ml"],
13581
+ vec!["3", "", "6 ml", "6 ml", "2 ml"],
13582
+ ],
13583
+ &[
13584
+ (72.0, 110.0),
13585
+ (110.0, 210.0),
13586
+ (210.0, 300.0),
13587
+ (300.0, 430.0),
13588
+ (430.0, 540.0),
13589
+ ],
13590
+ ));
13591
+ doc.kids.push(make_heading_at(
13592
+ 72.0,
13593
+ 92.0,
13594
+ 390.0,
13595
+ 108.0,
13596
+ "Saccharometer DI Water Glucose Solution Yeast Suspension",
13597
+ ));
13598
+ doc.kids
13599
+ .push(make_paragraph_at(72.0, 72.0, 120.0, 88.0, "below"));
13600
+ doc.kids
13601
+ .push(make_paragraph_at(72.0, 56.0, 240.0, 72.0, "1 16 ml 12 ml"));
13602
+ doc.kids
13603
+ .push(make_paragraph_at(296.0, 56.0, 340.0, 72.0, "0 ml"));
13604
+
13605
+ let md = to_markdown(&doc).unwrap();
13606
+ assert!(
13607
+ md.contains("Saccharometer DI Water Glucose Solution Yeast Suspension"),
13608
+ "{md}"
13609
+ );
13610
+ assert!(
13611
+ !md.contains("# Saccharometer DI Water Glucose Solution Yeast Suspension"),
13612
+ "{md}"
13613
+ );
13614
+ }
13615
+
13392
13616
  #[test]
13393
13617
  fn test_geometric_panel_headers_are_promoted_into_table() {
13394
13618
  let mut doc = PdfDocument::new("ai-pack-panel.pdf".to_string());