edgeparse 0.2.2__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.2 → edgeparse-0.2.3}/Cargo.lock +5 -5
  2. {edgeparse-0.2.2 → edgeparse-0.2.3}/Cargo.toml +1 -1
  3. {edgeparse-0.2.2 → edgeparse-0.2.3}/PKG-INFO +1 -1
  4. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/api/config.rs +29 -0
  5. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/lib.rs +76 -9
  6. {edgeparse-0.2.2 → 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.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/orchestrator.rs +171 -87
  9. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/cluster_table_detector.rs +75 -1
  10. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/table_detector.rs +129 -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.3}/README.md +0 -0
  13. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/Cargo.toml +0 -0
  14. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/README.md +0 -0
  15. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/api/batch.rs +0 -0
  16. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/api/config_loader.rs +0 -0
  17. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/api/filter.rs +0 -0
  18. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/api/mod.rs +0 -0
  19. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/hybrid.rs +0 -0
  20. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/bbox.rs +0 -0
  21. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/chunks.rs +0 -0
  22. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/content.rs +0 -0
  23. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/document.rs +0 -0
  24. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/enums.rs +0 -0
  25. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/list.rs +0 -0
  26. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/mod.rs +0 -0
  27. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/semantic.rs +0 -0
  28. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/table.rs +0 -0
  29. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/models/text.rs +0 -0
  30. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/output/csv.rs +0 -0
  31. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/output/docx.rs +0 -0
  32. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/output/html.rs +0 -0
  33. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/output/json.rs +0 -0
  34. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/output/legacy_json.rs +0 -0
  35. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/output/mod.rs +0 -0
  36. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/output/text.rs +0 -0
  37. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/output/toc_builder.rs +0 -0
  38. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/annotation_enrichment.rs +0 -0
  39. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/annotation_extractor.rs +0 -0
  40. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/bookmark_extractor.rs +0 -0
  41. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/chunk_parser.rs +0 -0
  42. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/data/glyphlist.txt +0 -0
  43. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/encryption.rs +0 -0
  44. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/font.rs +0 -0
  45. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/form_extractor.rs +0 -0
  46. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/graphics_state.rs +0 -0
  47. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/hyperlink_extractor.rs +0 -0
  48. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/image_extractor.rs +0 -0
  49. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/line_extractor.rs +0 -0
  50. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/loader.rs +0 -0
  51. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/metadata_writer.rs +0 -0
  52. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/mod.rs +0 -0
  53. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/page_info.rs +0 -0
  54. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pdf/text_extractor.rs +0 -0
  55. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/error_recovery.rs +0 -0
  56. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/logging.rs +0 -0
  57. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/mod.rs +0 -0
  58. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/parallel.rs +0 -0
  59. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/boxed_heading_promoter.rs +0 -0
  60. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/caption_linker.rs +0 -0
  61. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/column_detector.rs +0 -0
  62. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/content_filter.rs +0 -0
  63. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/content_sanitizer.rs +0 -0
  64. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/cross_page_linker.rs +0 -0
  65. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/figure_detector.rs +0 -0
  66. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/footnote_detector.rs +0 -0
  67. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/footnote_linker.rs +0 -0
  68. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/header_footer.rs +0 -0
  69. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/heading_detector.rs +0 -0
  70. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/id_assignment.rs +0 -0
  71. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/list_detector.rs +0 -0
  72. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/list_pass2.rs +0 -0
  73. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/mod.rs +0 -0
  74. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/nesting_level.rs +0 -0
  75. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/output_builder.rs +0 -0
  76. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/paragraph_detector.rs +0 -0
  77. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/reading_order.rs +0 -0
  78. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/table_content_assigner.rs +0 -0
  79. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/text_block_grouper.rs +0 -0
  80. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/text_line_grouper.rs +0 -0
  81. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/toc_detector.rs +0 -0
  82. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/pipeline/stages/watermark_detector.rs +0 -0
  83. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/tagged/mod.rs +0 -0
  84. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/tagged/processor.rs +0 -0
  85. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/tagged/struct_tree.rs +0 -0
  86. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/diff.rs +0 -0
  87. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/font_metrics_cache.rs +0 -0
  88. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/image_dedup.rs +0 -0
  89. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/language_detector.rs +0 -0
  90. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/layout_analysis.rs +0 -0
  91. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/mod.rs +0 -0
  92. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/page_range.rs +0 -0
  93. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/sanitizer.rs +0 -0
  94. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/statistics.rs +0 -0
  95. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/text_normalizer.rs +0 -0
  96. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/xref_index.rs +0 -0
  97. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/src/utils/xycut.rs +0 -0
  98. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/tests/integration_tests.rs +0 -0
  99. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-core/tests/perf_benchmarks.rs +0 -0
  100. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-python/Cargo.toml +0 -0
  101. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/edgeparse-python/src/lib.rs +0 -0
  102. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/.cargo_vcs_info.json +0 -0
  103. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/.chglog/CHANGELOG.tpl.md +0 -0
  104. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/.chglog/config.yml +0 -0
  105. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/.editorconfig +0 -0
  106. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/.gitattributes +0 -0
  107. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/.github/workflows/ci.yml +0 -0
  108. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/.gitignore +0 -0
  109. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/.vscode/launch.json +0 -0
  110. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/.vscode/settings.json +0 -0
  111. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/CHANGELOG.md +0 -0
  112. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/Cargo.toml +0 -0
  113. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/LICENSE +0 -0
  114. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/README.md +0 -0
  115. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/assets/AnnotationDemo.pdf +0 -0
  116. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/assets/Incremental.pdf +0 -0
  117. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/assets/encrypted.pdf +0 -0
  118. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/assets/example.pdf +0 -0
  119. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/assets/pdf_icon.jpg +0 -0
  120. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/assets/supported_color_type/L16.png +0 -0
  121. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/assets/supported_color_type/L8.jpg +0 -0
  122. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/assets/supported_color_type/La16.png +0 -0
  123. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/assets/supported_color_type/La8.png +0 -0
  124. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/assets/supported_color_type/Rgb16.png +0 -0
  125. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/assets/supported_color_type/Rgb8.jpg +0 -0
  126. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/assets/supported_color_type/Rgba16.png +0 -0
  127. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/assets/supported_color_type/Rgba8.png +0 -0
  128. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/assets/unicode.pdf +0 -0
  129. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/benches/datetime.rs +0 -0
  130. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/benches/parse.rs +0 -0
  131. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/add_barcode.rs +0 -0
  132. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/analyze_object_streams.rs +0 -0
  133. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/analyze_page_contents.rs +0 -0
  134. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/analyze_pdf.rs +0 -0
  135. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/analyze_references.rs +0 -0
  136. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/check_raw_objstream.rs +0 -0
  137. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/compress_existing_pdf.rs +0 -0
  138. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/create.rs +0 -0
  139. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/create_text_heavy_pdf.rs +0 -0
  140. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/debug_compression_detailed.rs +0 -0
  141. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/debug_compression_full.rs +0 -0
  142. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/debug_object_stream.rs +0 -0
  143. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/debug_save_with_objstreams.rs +0 -0
  144. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/decrypt.rs +0 -0
  145. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/encrypt.rs +0 -0
  146. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/extract_text.rs +0 -0
  147. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/extract_toc.rs +0 -0
  148. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/final_compression_test.rs +0 -0
  149. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/inspect_object_stream.rs +0 -0
  150. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/merge.rs +0 -0
  151. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/object_streams.rs +0 -0
  152. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/print_annotations.rs +0 -0
  153. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/replace_partial_text.rs +0 -0
  154. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/rotate.rs +0 -0
  155. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/test_better_compression.rs +0 -0
  156. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/test_decryption.rs +0 -0
  157. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/test_direct_objstream.rs +0 -0
  158. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/test_filter_roundtrip.rs +0 -0
  159. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/test_metadata.rs +0 -0
  160. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/test_object_stream_creation.rs +0 -0
  161. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/test_object_stream_roundtrip.rs +0 -0
  162. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/test_real_pdf_compression_with_fix.rs +0 -0
  163. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/test_structural_compression.rs +0 -0
  164. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/verify_decryption.rs +0 -0
  165. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/verify_page_compression.rs +0 -0
  166. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/verify_pdf.rs +0 -0
  167. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/examples/verify_trailer_compression_fix.rs +0 -0
  168. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/rustfmt.toml +0 -0
  169. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/bookmarks.rs +0 -0
  170. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/cmap_section.rs +0 -0
  171. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/common_data_structures/mod.rs +0 -0
  172. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/content.rs +0 -0
  173. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/creator.rs +0 -0
  174. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/datetime.rs +0 -0
  175. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/destinations.rs +0 -0
  176. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/document.rs +0 -0
  177. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/encodings/cmap.rs +0 -0
  178. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/encodings/glyphnames.rs +0 -0
  179. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/encodings/mappings.rs +0 -0
  180. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/encodings/mod.rs +0 -0
  181. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/encryption/algorithms.rs +0 -0
  182. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/encryption/crypt_filters.rs +0 -0
  183. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/encryption/pkcs5.rs +0 -0
  184. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/encryption/rc4.rs +0 -0
  185. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/encryption.rs +0 -0
  186. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/error.rs +0 -0
  187. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/filters/mod.rs +0 -0
  188. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/filters/png.rs +0 -0
  189. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/font.rs +0 -0
  190. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/incremental_document.rs +0 -0
  191. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/lib.rs +0 -0
  192. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/object.rs +0 -0
  193. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/object_stream.rs +0 -0
  194. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/outlines.rs +0 -0
  195. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/parser/cmap_parser.rs +0 -0
  196. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/parser/mod.rs +0 -0
  197. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/parser_aux.rs +0 -0
  198. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/processor.rs +0 -0
  199. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/reader.rs +0 -0
  200. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/save_options.rs +0 -0
  201. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/toc.rs +0 -0
  202. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/writer.rs +0 -0
  203. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/xobject.rs +0 -0
  204. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/src/xref.rs +0 -0
  205. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/tests/annotation.rs +0 -0
  206. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/tests/catalog_compression_integration_test.rs +0 -0
  207. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/tests/decryption.rs +0 -0
  208. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/tests/extract_test.rs +0 -0
  209. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/tests/font_test.rs +0 -0
  210. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/tests/incremental_document.rs +0 -0
  211. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/tests/metadata_test.rs +0 -0
  212. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/tests/modify.rs +0 -0
  213. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/tests/object_stream_comprehensive_test.rs +0 -0
  214. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/tests/object_stream_edge_cases_test.rs +0 -0
  215. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/tests/object_stream_methods_test.rs +0 -0
  216. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/tests/object_stream_performance_test.rs +0 -0
  217. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/tests/resources/fonts/Montserrat-Regular.ttf +0 -0
  218. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/tests/simple_object_stream_test.rs +0 -0
  219. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/tests/trailer_reference_compression_test.rs +0 -0
  220. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/tests/unicode.rs +0 -0
  221. {edgeparse-0.2.2 → edgeparse-0.2.3}/crates/pdf-cos/tests/utils.rs +0 -0
  222. {edgeparse-0.2.2 → edgeparse-0.2.3}/edgeparse/__init__.py +0 -0
  223. {edgeparse-0.2.2 → edgeparse-0.2.3}/edgeparse/_types.py +0 -0
  224. {edgeparse-0.2.2 → edgeparse-0.2.3}/edgeparse/cli.py +0 -0
  225. {edgeparse-0.2.2 → edgeparse-0.2.3}/edgeparse/py.typed +0 -0
  226. {edgeparse-0.2.2 → 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.2"
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.2"
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.2"
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.2"
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.2"
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.2"
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.2
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
@@ -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) {
@@ -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());