docvortex 0.2.1__py3-none-any.whl

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 (364) hide show
  1. docvortex/__init__.py +21 -0
  2. docvortex/analyzers/__init__.py +3 -0
  3. docvortex/analyzers/native/__init__.py +37 -0
  4. docvortex/analyzers/native/_shared/__init__.py +3 -0
  5. docvortex/analyzers/native/_shared/hyperlink.py +16 -0
  6. docvortex/analyzers/native/_shared/image.py +8 -0
  7. docvortex/analyzers/native/_shared/markup/__init__.py +47 -0
  8. docvortex/analyzers/native/_shared/markup/anchors.py +30 -0
  9. docvortex/analyzers/native/_shared/markup/formula.py +37 -0
  10. docvortex/analyzers/native/_shared/markup/projector.py +57 -0
  11. docvortex/analyzers/native/_shared/markup/styles.py +19 -0
  12. docvortex/analyzers/native/_shared/mathml.py +17 -0
  13. docvortex/analyzers/native/_shared/names.py +7 -0
  14. docvortex/analyzers/native/_shared/xycut.py +414 -0
  15. docvortex/analyzers/native/contracts.py +44 -0
  16. docvortex/analyzers/native/csv.py +351 -0
  17. docvortex/analyzers/native/epub/__init__.py +18 -0
  18. docvortex/analyzers/native/epub/constants.py +45 -0
  19. docvortex/analyzers/native/epub/converter.py +80 -0
  20. docvortex/analyzers/native/epub/errors.py +20 -0
  21. docvortex/analyzers/native/epub/metadata.py +31 -0
  22. docvortex/analyzers/native/epub/package.py +649 -0
  23. docvortex/analyzers/native/epub/xhtml.py +306 -0
  24. docvortex/analyzers/native/html/__init__.py +6 -0
  25. docvortex/analyzers/native/html/anchors.py +271 -0
  26. docvortex/analyzers/native/html/constants.py +25 -0
  27. docvortex/analyzers/native/html/contracts.py +7 -0
  28. docvortex/analyzers/native/html/converter.py +117 -0
  29. docvortex/analyzers/native/html/document.py +389 -0
  30. docvortex/analyzers/native/html/errors.py +12 -0
  31. docvortex/analyzers/native/html/resources.py +365 -0
  32. docvortex/analyzers/native/html/selector.py +421 -0
  33. docvortex/analyzers/native/models.py +209 -0
  34. docvortex/analyzers/native/ofd/__init__.py +14 -0
  35. docvortex/analyzers/native/ofd/constants.py +59 -0
  36. docvortex/analyzers/native/ofd/converter.py +31 -0
  37. docvortex/analyzers/native/ofd/errors.py +16 -0
  38. docvortex/analyzers/native/ofd/geometry.py +215 -0
  39. docvortex/analyzers/native/ofd/images.py +124 -0
  40. docvortex/analyzers/native/ofd/metadata.py +53 -0
  41. docvortex/analyzers/native/ofd/models.py +170 -0
  42. docvortex/analyzers/native/ofd/package.py +346 -0
  43. docvortex/analyzers/native/ofd/path.py +201 -0
  44. docvortex/analyzers/native/ofd/reading_order.py +293 -0
  45. docvortex/analyzers/native/ofd/resources.py +122 -0
  46. docvortex/analyzers/native/ofd/scene.py +347 -0
  47. docvortex/analyzers/native/ofd/table.py +240 -0
  48. docvortex/analyzers/native/ofd/text.py +521 -0
  49. docvortex/analyzers/native/office/__init__.py +3 -0
  50. docvortex/analyzers/native/office/doc/__init__.py +3 -0
  51. docvortex/analyzers/native/office/doc/bookmarks.py +81 -0
  52. docvortex/analyzers/native/office/doc/doc_converter.py +573 -0
  53. docvortex/analyzers/native/office/doc/fib.py +261 -0
  54. docvortex/analyzers/native/office/doc/fields.py +101 -0
  55. docvortex/analyzers/native/office/doc/formatting.py +171 -0
  56. docvortex/analyzers/native/office/doc/images.py +162 -0
  57. docvortex/analyzers/native/office/doc/lists.py +356 -0
  58. docvortex/analyzers/native/office/doc/models.py +164 -0
  59. docvortex/analyzers/native/office/doc/parser.py +851 -0
  60. docvortex/analyzers/native/office/doc/pieces.py +256 -0
  61. docvortex/analyzers/native/office/doc/records.py +40 -0
  62. docvortex/analyzers/native/office/doc/sprm.py +269 -0
  63. docvortex/analyzers/native/office/doc/styles.py +215 -0
  64. docvortex/analyzers/native/office/docx/__init__.py +3 -0
  65. docvortex/analyzers/native/office/docx/context.py +71 -0
  66. docvortex/analyzers/native/office/docx/docx_converter.py +614 -0
  67. docvortex/analyzers/native/office/docx/equationxml.py +119 -0
  68. docvortex/analyzers/native/office/docx/fields.py +816 -0
  69. docvortex/analyzers/native/office/docx/formatting_types.py +23 -0
  70. docvortex/analyzers/native/office/docx/main.py +50 -0
  71. docvortex/analyzers/native/office/docx/numbering.py +491 -0
  72. docvortex/analyzers/native/office/docx/office_xml.py +57 -0
  73. docvortex/analyzers/native/office/docx/package_normalizer.py +248 -0
  74. docvortex/analyzers/native/office/docx/resources.py +526 -0
  75. docvortex/analyzers/native/office/docx/styles.py +693 -0
  76. docvortex/analyzers/native/office/docx/tables.py +515 -0
  77. docvortex/analyzers/native/office/equation/__init__.py +3 -0
  78. docvortex/analyzers/native/office/equation/image.py +470 -0
  79. docvortex/analyzers/native/office/equation/latex_dict.py +324 -0
  80. docvortex/analyzers/native/office/equation/mtef.py +885 -0
  81. docvortex/analyzers/native/office/equation/mtef_v5.py +941 -0
  82. docvortex/analyzers/native/office/equation/omml.py +561 -0
  83. docvortex/analyzers/native/office/equation/ooxml.py +62 -0
  84. docvortex/analyzers/native/office/errors.py +33 -0
  85. docvortex/analyzers/native/office/image.py +307 -0
  86. docvortex/analyzers/native/office/legacy/__init__.py +3 -0
  87. docvortex/analyzers/native/office/legacy/binary.py +43 -0
  88. docvortex/analyzers/native/office/legacy/officeart.py +362 -0
  89. docvortex/analyzers/native/office/legacy/ole.py +110 -0
  90. docvortex/analyzers/native/office/limits.py +12 -0
  91. docvortex/analyzers/native/office/odf/__init__.py +3 -0
  92. docvortex/analyzers/native/office/odf/chart.py +76 -0
  93. docvortex/analyzers/native/office/odf/constants.py +70 -0
  94. docvortex/analyzers/native/office/odf/converters.py +403 -0
  95. docvortex/analyzers/native/office/odf/errors.py +18 -0
  96. docvortex/analyzers/native/office/odf/metadata.py +91 -0
  97. docvortex/analyzers/native/office/odf/models.py +176 -0
  98. docvortex/analyzers/native/office/odf/package.py +270 -0
  99. docvortex/analyzers/native/office/odf/styles.py +329 -0
  100. docvortex/analyzers/native/office/odf/table.py +469 -0
  101. docvortex/analyzers/native/office/odf/text.py +1002 -0
  102. docvortex/analyzers/native/office/ooxml_chart.py +1016 -0
  103. docvortex/analyzers/native/office/opc.py +38 -0
  104. docvortex/analyzers/native/office/ppt/__init__.py +3 -0
  105. docvortex/analyzers/native/office/ppt/models.py +118 -0
  106. docvortex/analyzers/native/office/ppt/parser.py +1895 -0
  107. docvortex/analyzers/native/office/ppt/ppt_converter.py +292 -0
  108. docvortex/analyzers/native/office/ppt/records.py +131 -0
  109. docvortex/analyzers/native/office/ppt/style_text.py +247 -0
  110. docvortex/analyzers/native/office/pptx/__init__.py +3 -0
  111. docvortex/analyzers/native/office/pptx/context.py +113 -0
  112. docvortex/analyzers/native/office/pptx/lists.py +558 -0
  113. docvortex/analyzers/native/office/pptx/main.py +20 -0
  114. docvortex/analyzers/native/office/pptx/package_normalizer.py +321 -0
  115. docvortex/analyzers/native/office/pptx/pptx_converter.py +323 -0
  116. docvortex/analyzers/native/office/pptx/resources.py +329 -0
  117. docvortex/analyzers/native/office/pptx/shapes.py +393 -0
  118. docvortex/analyzers/native/office/pptx/text_styles.py +625 -0
  119. docvortex/analyzers/native/office/pptx/titles.py +178 -0
  120. docvortex/analyzers/native/office/rich_text.py +420 -0
  121. docvortex/analyzers/native/office/rtf/__init__.py +3 -0
  122. docvortex/analyzers/native/office/rtf/converter.py +708 -0
  123. docvortex/analyzers/native/office/rtf/lexer.py +213 -0
  124. docvortex/analyzers/native/office/rtf/math.py +339 -0
  125. docvortex/analyzers/native/office/rtf/models.py +185 -0
  126. docvortex/analyzers/native/office/rtf/parser.py +1553 -0
  127. docvortex/analyzers/native/office/spreadsheet/__init__.py +3 -0
  128. docvortex/analyzers/native/office/spreadsheet/html.py +81 -0
  129. docvortex/analyzers/native/office/spreadsheet/models.py +79 -0
  130. docvortex/analyzers/native/office/spreadsheet/projector.py +928 -0
  131. docvortex/analyzers/native/office/streams.py +18 -0
  132. docvortex/analyzers/native/office/xls/__init__.py +3 -0
  133. docvortex/analyzers/native/office/xls/chart.py +132 -0
  134. docvortex/analyzers/native/office/xls/embedded_chart.py +299 -0
  135. docvortex/analyzers/native/office/xls/models.py +109 -0
  136. docvortex/analyzers/native/office/xls/number_format.py +521 -0
  137. docvortex/analyzers/native/office/xls/parser.py +1145 -0
  138. docvortex/analyzers/native/office/xls/records.py +201 -0
  139. docvortex/analyzers/native/office/xls/strings.py +205 -0
  140. docvortex/analyzers/native/office/xls/xls_converter.py +354 -0
  141. docvortex/analyzers/native/office/xlsx/__init__.py +3 -0
  142. docvortex/analyzers/native/office/xlsx/main.py +20 -0
  143. docvortex/analyzers/native/office/xlsx/ooxml_ole.py +522 -0
  144. docvortex/analyzers/native/office/xlsx/package_normalizer.py +310 -0
  145. docvortex/analyzers/native/office/xlsx/xlsx_converter.py +716 -0
  146. docvortex/analyzers/native/pdf/__init__.py +3 -0
  147. docvortex/analyzers/native/pdf/auxiliary_text.py +1671 -0
  148. docvortex/analyzers/native/pdf/char_geometry.py +1662 -0
  149. docvortex/analyzers/native/pdf/code_blocks.py +535 -0
  150. docvortex/analyzers/native/pdf/formulas.py +1985 -0
  151. docvortex/analyzers/native/pdf/geometry.py +281 -0
  152. docvortex/analyzers/native/pdf/graphics.py +1501 -0
  153. docvortex/analyzers/native/pdf/index_blocks.py +268 -0
  154. docvortex/analyzers/native/pdf/inline/__init__.py +3 -0
  155. docvortex/analyzers/native/pdf/inline/common.py +112 -0
  156. docvortex/analyzers/native/pdf/inline/detection.py +590 -0
  157. docvortex/analyzers/native/pdf/inline/matching.py +1185 -0
  158. docvortex/analyzers/native/pdf/inline/materialize.py +457 -0
  159. docvortex/analyzers/native/pdf/inline/scripts.py +975 -0
  160. docvortex/analyzers/native/pdf/inline/types.py +385 -0
  161. docvortex/analyzers/native/pdf/line_layout.py +1106 -0
  162. docvortex/analyzers/native/pdf/line_merging.py +1223 -0
  163. docvortex/analyzers/native/pdf/models.py +246 -0
  164. docvortex/analyzers/native/pdf/native_text.py +1004 -0
  165. docvortex/analyzers/native/pdf/pipeline.py +1390 -0
  166. docvortex/analyzers/native/pdf/script_geometry.py +636 -0
  167. docvortex/analyzers/native/pdf/shared.py +30 -0
  168. docvortex/analyzers/native/pdf/spatial_text.py +383 -0
  169. docvortex/analyzers/native/pdf/table_annotations.py +446 -0
  170. docvortex/analyzers/native/pdf/table_constants.py +48 -0
  171. docvortex/analyzers/native/pdf/table_detection.py +227 -0
  172. docvortex/analyzers/native/pdf/table_filled_grid.py +218 -0
  173. docvortex/analyzers/native/pdf/table_geometry.py +40 -0
  174. docvortex/analyzers/native/pdf/table_materialization.py +444 -0
  175. docvortex/analyzers/native/pdf/table_recovery/__init__.py +15 -0
  176. docvortex/analyzers/native/pdf/table_recovery/candidate.py +356 -0
  177. docvortex/analyzers/native/pdf/table_recovery/contracts.py +154 -0
  178. docvortex/analyzers/native/pdf/table_recovery/engine.py +609 -0
  179. docvortex/analyzers/native/pdf/table_recovery/geometry.py +164 -0
  180. docvortex/analyzers/native/pdf/table_recovery/sparse_common.py +85 -0
  181. docvortex/analyzers/native/pdf/table_recovery/sparse_hybrid.py +804 -0
  182. docvortex/analyzers/native/pdf/table_recovery/sparse_multiline.py +1122 -0
  183. docvortex/analyzers/native/pdf/table_recovery/text.py +414 -0
  184. docvortex/analyzers/native/pdf/table_recovery/text_grid.py +618 -0
  185. docvortex/analyzers/native/pdf/table_recovery/vector.py +1931 -0
  186. docvortex/analyzers/native/pdf/table_rows.py +34 -0
  187. docvortex/analyzers/native/pdf/table_rules.py +1129 -0
  188. docvortex/analyzers/native/pdf/table_text_styles.py +283 -0
  189. docvortex/analyzers/native/pdf/tables.py +147 -0
  190. docvortex/analyzers/native/pdf/text_assembly/__init__.py +3 -0
  191. docvortex/analyzers/native/pdf/text_assembly/annotations.py +581 -0
  192. docvortex/analyzers/native/pdf/text_assembly/assembly.py +292 -0
  193. docvortex/analyzers/native/pdf/text_assembly/common.py +477 -0
  194. docvortex/analyzers/native/pdf/text_assembly/footnotes.py +394 -0
  195. docvortex/analyzers/native/pdf/text_assembly/merging.py +1274 -0
  196. docvortex/analyzers/native/pdf/text_assembly/rows.py +692 -0
  197. docvortex/analyzers/native/pdf/text_blocks.py +82 -0
  198. docvortex/analyzers/native/pdf/text_styles.py +55 -0
  199. docvortex/analyzers/native/pdf/title_analysis/__init__.py +3 -0
  200. docvortex/analyzers/native/pdf/title_analysis/body_profile.py +215 -0
  201. docvortex/analyzers/native/pdf/title_analysis/common.py +117 -0
  202. docvortex/analyzers/native/pdf/title_analysis/document_profile.py +164 -0
  203. docvortex/analyzers/native/pdf/title_analysis/lane_titles.py +758 -0
  204. docvortex/analyzers/native/pdf/title_analysis/page_titles.py +1024 -0
  205. docvortex/analyzers/native/pdf/title_analysis/prototype.py +194 -0
  206. docvortex/analyzers/native/pdf/title_analysis/structural.py +1081 -0
  207. docvortex/analyzers/native/pdf/titles.py +75 -0
  208. docvortex/analyzers/native/pdf/typography.py +19 -0
  209. docvortex/analyzers/native/pdf/visual_annotations.py +1262 -0
  210. docvortex/api.py +180 -0
  211. docvortex/assets/__init__.py +5 -0
  212. docvortex/assets/store.py +51 -0
  213. docvortex/cli.py +54 -0
  214. docvortex/codecs/__init__.py +3 -0
  215. docvortex/codecs/html/__init__.py +22 -0
  216. docvortex/codecs/html/contracts.py +236 -0
  217. docvortex/codecs/html/materializer.py +331 -0
  218. docvortex/codecs/html/parser.py +763 -0
  219. docvortex/codecs/html/resources.py +34 -0
  220. docvortex/codecs/json.py +17 -0
  221. docvortex/content/__init__.py +5 -0
  222. docvortex/content/inline.py +248 -0
  223. docvortex/content/markup/__init__.py +44 -0
  224. docvortex/content/markup/anchors.py +188 -0
  225. docvortex/content/markup/formula.py +280 -0
  226. docvortex/content/markup/projector.py +1237 -0
  227. docvortex/content/markup/styles.py +327 -0
  228. docvortex/content/mathml.py +167 -0
  229. docvortex/content/normalization.py +188 -0
  230. docvortex/content/spans.py +183 -0
  231. docvortex/content/table/__init__.py +18 -0
  232. docvortex/content/table/blocks.py +152 -0
  233. docvortex/content/table/content.py +425 -0
  234. docvortex/content/table/document.py +104 -0
  235. docvortex/content/table/html.py +399 -0
  236. docvortex/content/table/models.py +76 -0
  237. docvortex/content/table/rules.py +42 -0
  238. docvortex/content/table/structure.py +221 -0
  239. docvortex/content/tree.py +5 -0
  240. docvortex/document/__init__.py +3 -0
  241. docvortex/document/contracts.py +22 -0
  242. docvortex/document/detection.py +389 -0
  243. docvortex/document/filetypes.py +175 -0
  244. docvortex/document/page_range.py +167 -0
  245. docvortex/document/pdf/__init__.py +19 -0
  246. docvortex/document/pdf/classify.py +1138 -0
  247. docvortex/document/pdf/constants.py +132 -0
  248. docvortex/document/pdf/diagnostics.py +350 -0
  249. docvortex/document/pdf/document.py +582 -0
  250. docvortex/document/pdf/font_runtime.py +335 -0
  251. docvortex/document/pdf/geometry.py +31 -0
  252. docvortex/document/pdf/images.py +654 -0
  253. docvortex/document/pdf/native_annotations.py +367 -0
  254. docvortex/document/pdf/native_contracts.py +169 -0
  255. docvortex/document/pdf/native_coordinates.py +216 -0
  256. docvortex/document/pdf/native_lifecycle.py +16 -0
  257. docvortex/document/pdf/native_objects.py +902 -0
  258. docvortex/document/pdf/native_text_geometry.py +315 -0
  259. docvortex/document/pdf/pdfium.py +325 -0
  260. docvortex/document/pdf/raster.py +46 -0
  261. docvortex/document/pdf/text/__init__.py +62 -0
  262. docvortex/document/pdf/text/contracts.py +211 -0
  263. docvortex/document/pdf/text/extract.py +165 -0
  264. docvortex/document/pdf/text/geometry.py +16 -0
  265. docvortex/document/pdf/text/groups.py +162 -0
  266. docvortex/document/pdf/visual_geometry.py +201 -0
  267. docvortex/document/pdf/visuals.py +343 -0
  268. docvortex/document/source.py +92 -0
  269. docvortex/errors.py +21 -0
  270. docvortex/export/__init__.py +3 -0
  271. docvortex/export/bundle.py +98 -0
  272. docvortex/export/files.py +66 -0
  273. docvortex/export/middle.py +208 -0
  274. docvortex/foundation/__init__.py +3 -0
  275. docvortex/foundation/geometry.py +125 -0
  276. docvortex/foundation/hyperlink.py +65 -0
  277. docvortex/foundation/image.py +48 -0
  278. docvortex/foundation/image_encoding.py +30 -0
  279. docvortex/foundation/image_payload.py +280 -0
  280. docvortex/foundation/language.py +92 -0
  281. docvortex/foundation/platform.py +38 -0
  282. docvortex/foundation/text.py +153 -0
  283. docvortex/foundation/type_identity.py +20 -0
  284. docvortex/foundation/xml_names.py +20 -0
  285. docvortex/options.py +30 -0
  286. docvortex/postprocess/__init__.py +3 -0
  287. docvortex/postprocess/content.py +53 -0
  288. docvortex/postprocess/document.py +19 -0
  289. docvortex/postprocess/lists.py +236 -0
  290. docvortex/postprocess/page_blocks.py +214 -0
  291. docvortex/postprocess/pages.py +95 -0
  292. docvortex/postprocess/paragraphs.py +580 -0
  293. docvortex/postprocess/visual.py +715 -0
  294. docvortex/render/__init__.py +48 -0
  295. docvortex/render/_internal/__init__.py +3 -0
  296. docvortex/render/_internal/common/__init__.py +3 -0
  297. docvortex/render/_internal/common/context.py +43 -0
  298. docvortex/render/_internal/common/html_table.py +178 -0
  299. docvortex/render/_internal/common/index.py +33 -0
  300. docvortex/render/_internal/common/list_items.py +158 -0
  301. docvortex/render/_internal/common/planner.py +140 -0
  302. docvortex/render/_internal/docx/__init__.py +3 -0
  303. docvortex/render/_internal/docx/assets.py +202 -0
  304. docvortex/render/_internal/docx/inline.py +434 -0
  305. docvortex/render/_internal/docx/math.py +220 -0
  306. docvortex/render/_internal/docx/renderer.py +905 -0
  307. docvortex/render/_internal/docx/styles.py +195 -0
  308. docvortex/render/_internal/docx/table.py +442 -0
  309. docvortex/render/_internal/epub/__init__.py +5 -0
  310. docvortex/render/_internal/epub/assets.py +173 -0
  311. docvortex/render/_internal/epub/package.py +249 -0
  312. docvortex/render/_internal/epub/renderer.py +1156 -0
  313. docvortex/render/_internal/html/__init__.py +3 -0
  314. docvortex/render/_internal/html/inline.py +346 -0
  315. docvortex/render/_internal/html/renderer.py +1041 -0
  316. docvortex/render/_internal/html/sanitizer.py +478 -0
  317. docvortex/render/_internal/html/table.py +121 -0
  318. docvortex/render/_internal/latex/__init__.py +1 -0
  319. docvortex/render/_internal/latex/assets.py +85 -0
  320. docvortex/render/_internal/latex/inline.py +145 -0
  321. docvortex/render/_internal/latex/renderer.py +506 -0
  322. docvortex/render/_internal/latex/table.py +347 -0
  323. docvortex/render/_internal/markdown/__init__.py +3 -0
  324. docvortex/render/_internal/markdown/assets.py +78 -0
  325. docvortex/render/_internal/markdown/blocks.py +635 -0
  326. docvortex/render/_internal/markdown/escaping.py +51 -0
  327. docvortex/render/_internal/markdown/inline.py +260 -0
  328. docvortex/render/_internal/markdown/renderer.py +93 -0
  329. docvortex/render/_internal/markdown/table.py +281 -0
  330. docvortex/render/_internal/pdf/__init__.py +3 -0
  331. docvortex/render/_internal/pdf/assets.py +197 -0
  332. docvortex/render/_internal/pdf/formula.py +417 -0
  333. docvortex/render/_internal/pdf/inline.py +343 -0
  334. docvortex/render/_internal/pdf/renderer.py +734 -0
  335. docvortex/render/_internal/pdf/styles.py +206 -0
  336. docvortex/render/_internal/pdf/table.py +272 -0
  337. docvortex/render/_internal/structured_content/__init__.py +3 -0
  338. docvortex/render/_internal/structured_content/renderer.py +193 -0
  339. docvortex/render/api.py +199 -0
  340. docvortex/render/contracts.py +205 -0
  341. docvortex/render/docx.py +38 -0
  342. docvortex/render/epub.py +35 -0
  343. docvortex/render/fragments.py +70 -0
  344. docvortex/render/html.py +29 -0
  345. docvortex/render/latex.py +24 -0
  346. docvortex/render/markdown.py +50 -0
  347. docvortex/render/pdf.py +25 -0
  348. docvortex/render/structured_content.py +23 -0
  349. docvortex/resources/epub/docvortex.css +91 -0
  350. docvortex/resources/fasttext-langdetect/lid.176.ftz +0 -0
  351. docvortex/resources/fonts/DroidSansFallbackFull.ttf +0 -0
  352. docvortex/resources/fonts/NOTICE +190 -0
  353. docvortex/resources/fonts/manifest.json +9 -0
  354. docvortex/resources/html/docvortex.css +601 -0
  355. docvortex/resources/html/docvortex.min.css +1 -0
  356. docvortex/result.py +91 -0
  357. docvortex/schema.py +1141 -0
  358. docvortex/version.py +3 -0
  359. docvortex-0.2.1.dist-info/METADATA +193 -0
  360. docvortex-0.2.1.dist-info/RECORD +364 -0
  361. docvortex-0.2.1.dist-info/WHEEL +5 -0
  362. docvortex-0.2.1.dist-info/entry_points.txt +2 -0
  363. docvortex-0.2.1.dist-info/licenses/LICENSE.md +21 -0
  364. docvortex-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,758 @@
1
+ """在既有正文统计和结构证据下分类栏内标题。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import statistics
6
+ from typing import Literal
7
+
8
+ from .....schema import BBox
9
+ from ..geometry import _bbox_axis_overlap_ratio, _bbox_center_x, _bbox_center_y, _bbox_union_many
10
+ from ..line_layout import (
11
+ _effective_text_row_gap,
12
+ _font_signatures_share_family,
13
+ _font_weights_conflict,
14
+ _line_effective_height,
15
+ _should_connect_semantic_rows,
16
+ _title_fonts_compatible,
17
+ )
18
+ from ..models import _DocumentBodyProfile, _DocumentTitleProfile, _LaneBodyProfile, _LineItem, _TextLane
19
+ from .body_profile import _line_uses_document_regular_font
20
+ from .common import _line_inside_visual_container, _line_near_visual_container
21
+ from .prototype import _line_conflicts_document_title_profile, _matching_document_title_prototype
22
+
23
+
24
+ def _classify_paragraph_titles_in_lane(
25
+ lane: _TextLane,
26
+ profile: _LaneBodyProfile,
27
+ local_page_width: float,
28
+ local_page_height: float,
29
+ container_bboxes: list[BBox],
30
+ *,
31
+ document_title_bottom: float | None,
32
+ preserve_front_matter_boundaries: bool,
33
+ physical_gaps: dict[int, tuple[float | None, float | None]],
34
+ grid_title_suppressions: set[int],
35
+ document_body_profile: _DocumentBodyProfile | None,
36
+ document_title_profile: _DocumentTitleProfile | None,
37
+ page_index: int,
38
+ ) -> None:
39
+ """以字号、样式、留白、对齐、栏宽和容器邻接判定段落标题。"""
40
+
41
+ lane_width = max(0.1, lane.right - lane.left)
42
+ rows = lane.lines
43
+ selected_indices: set[int] = set()
44
+ front_matter_boundary = _infer_front_matter_boundary(
45
+ lane,
46
+ profile,
47
+ local_page_height,
48
+ document_title_bottom=document_title_bottom,
49
+ )
50
+ for index, (line, bbox) in enumerate(rows):
51
+ if line.semantic_type is not None:
52
+ continue
53
+ if line.source_index in grid_title_suppressions:
54
+ continue
55
+ if not 0.07 * local_page_height <= _bbox_center_y(bbox) <= 0.93 * local_page_height:
56
+ continue
57
+ inside_front_matter = front_matter_boundary is not None and _bbox_center_y(bbox) <= front_matter_boundary
58
+ if inside_front_matter and not preserve_front_matter_boundaries:
59
+ continue
60
+ if (
61
+ document_title_bottom is not None
62
+ and document_title_bottom <= 0.6 * local_page_height
63
+ and _bbox_center_y(bbox) >= 0.84 * local_page_height
64
+ ):
65
+ # 首页底部短行通常是版本、日期等封面元数据,不属于正文标题层级。
66
+ continue
67
+ line_height = _line_effective_height(line, bbox)
68
+ title_prototype = _matching_document_title_prototype(
69
+ line,
70
+ bbox,
71
+ lane,
72
+ document_body_profile,
73
+ document_title_profile,
74
+ )
75
+ title_profile_size_conflict = title_prototype is None and _line_conflicts_document_title_profile(
76
+ line,
77
+ bbox,
78
+ lane,
79
+ document_body_profile,
80
+ document_title_profile,
81
+ )
82
+ inside_visual_container = _line_inside_visual_container(
83
+ bbox,
84
+ container_bboxes,
85
+ )
86
+ near_visual_container = _line_near_visual_container(
87
+ bbox,
88
+ container_bboxes,
89
+ profile.body_height,
90
+ )
91
+ if inside_visual_container or (near_visual_container and title_prototype is None):
92
+ continue
93
+
94
+ recurrent_regular_font = _line_uses_document_regular_font(
95
+ line,
96
+ document_body_profile,
97
+ )
98
+ document_regular_body_candidate = (
99
+ recurrent_regular_font
100
+ and document_body_profile is not None
101
+ and line_height >= 0.9 * document_body_profile.body_height
102
+ )
103
+ reference_body_height = profile.body_height
104
+ if profile.body_row_count < 3 and document_body_profile is not None:
105
+ reference_body_height = document_body_profile.body_height
106
+ if document_regular_body_candidate and document_body_profile is not None:
107
+ reference_body_height = max(
108
+ reference_body_height,
109
+ document_body_profile.body_height,
110
+ )
111
+ height_ratio = line_height / reference_body_height
112
+ width_ratio = (bbox[2] - bbox[0]) / lane_width
113
+ centered = abs(_bbox_center_x(bbox) - (lane.left + lane.right) / 2.0) <= 0.12 * lane_width
114
+ left_aligned = abs(bbox[0] - lane.left) <= 0.65 * profile.body_height
115
+ if line.median_glyph_width is not None and bbox[2] - bbox[0] <= 1.25 * line.median_glyph_width and height_ratio < 1.18:
116
+ continue
117
+ style_differs = (
118
+ not recurrent_regular_font
119
+ and profile.body_font is not None
120
+ and line.font_signature is not None
121
+ and line.font_coverage >= 0.75
122
+ and not _font_signatures_share_family(
123
+ line.font_signature,
124
+ profile.body_font,
125
+ )
126
+ )
127
+ low_coverage_style_differs = (
128
+ not recurrent_regular_font
129
+ and profile.body_font is not None
130
+ and line.font_signature is not None
131
+ and 0.5 <= line.font_coverage < 0.75
132
+ and not _font_signatures_share_family(
133
+ line.font_signature,
134
+ profile.body_font,
135
+ )
136
+ )
137
+ style_support = profile.style_support.get(line.font_signature, 1.0) if line.font_signature is not None else 1.0
138
+ weight_emphasized = (
139
+ profile.body_weight is not None
140
+ and line.dominant_font_weight is not None
141
+ and line.dominant_font_weight >= max(profile.body_weight + 100.0, 1.15 * profile.body_weight)
142
+ )
143
+ gap_above = _normalized_title_gap(
144
+ rows,
145
+ index,
146
+ direction=-1,
147
+ body_height=profile.body_height,
148
+ physical_gaps=physical_gaps,
149
+ )
150
+ gap_below = _normalized_title_gap(
151
+ rows,
152
+ index,
153
+ direction=1,
154
+ body_height=profile.body_height,
155
+ physical_gaps=physical_gaps,
156
+ )
157
+ regular_gap_ratio = profile.regular_gap / max(0.1, profile.body_height)
158
+ gap_above_excess = max(0.0, gap_above - regular_gap_ratio)
159
+ gap_below_excess = max(0.0, gap_below - regular_gap_ratio)
160
+ has_spacing_signal = gap_above >= 0.35
161
+
162
+ if _visual_row_has_body_style_sibling(rows, index):
163
+ continue
164
+ if _is_near_full_mixed_inline_row(
165
+ rows,
166
+ index,
167
+ lane_width,
168
+ profile,
169
+ ):
170
+ continue
171
+ if (
172
+ index > 0
173
+ and rows[index - 1][0].semantic_type == "paragraph_title"
174
+ and width_ratio >= 0.85
175
+ and gap_above < 1.0
176
+ and not _title_fonts_compatible(rows[index - 1][0], line)
177
+ ):
178
+ continue
179
+ if _continues_local_body_row(
180
+ rows,
181
+ index,
182
+ lane_width,
183
+ profile,
184
+ ):
185
+ continue
186
+ has_following_body_row = _has_following_body_row(
187
+ rows,
188
+ index,
189
+ lane_width,
190
+ lane.left,
191
+ profile,
192
+ )
193
+ compact_local_transition = (
194
+ low_coverage_style_differs
195
+ and left_aligned
196
+ and width_ratio <= 0.65
197
+ and gap_above >= 0.1
198
+ and gap_below >= 0.2
199
+ and has_following_body_row
200
+ )
201
+ compact_text_section = (
202
+ height_ratio < 0.9
203
+ and centered
204
+ and width_ratio <= 0.7
205
+ and gap_above >= 0.35
206
+ and gap_below >= 0.2
207
+ and (page_index != 0 or style_differs or weight_emphasized or title_prototype is not None)
208
+ and _has_following_compact_text_section(
209
+ rows,
210
+ index,
211
+ lane_width,
212
+ )
213
+ )
214
+ weak_regular_pitch_body_candidate = (
215
+ document_body_profile is not None
216
+ and 0.9 <= line_height / document_body_profile.body_height <= 1.1
217
+ and title_prototype is None
218
+ and (not centered or width_ratio >= 0.75)
219
+ and not weight_emphasized
220
+ and gap_above_excess < 0.1
221
+ and gap_below_excess < 0.1
222
+ )
223
+ if weak_regular_pitch_body_candidate:
224
+ continue
225
+ if (
226
+ title_profile_size_conflict
227
+ and document_body_profile is not None
228
+ and line_height <= 1.18 * document_body_profile.body_height
229
+ and not weight_emphasized
230
+ ):
231
+ continue
232
+ prototype_promotion = (
233
+ title_prototype is not None
234
+ and (width_ratio <= 0.9 or (bbox[2] - bbox[0]) / max(0.1, local_page_width) <= 0.8)
235
+ and (centered or left_aligned)
236
+ and (
237
+ (document_body_profile is not None and line_height >= 1.18 * document_body_profile.body_height)
238
+ or near_visual_container
239
+ or (has_following_body_row and gap_above_excess >= 0.1)
240
+ or gap_above_excess >= 0.35
241
+ )
242
+ )
243
+ prototype_inline_heading = (
244
+ document_body_profile is not None
245
+ and line_height <= 1.15 * document_body_profile.body_height
246
+ and _is_full_width_inline_heading(
247
+ rows,
248
+ index,
249
+ lane_width,
250
+ profile,
251
+ )
252
+ )
253
+ if prototype_promotion and not prototype_inline_heading:
254
+ line.semantic_type = "paragraph_title"
255
+ selected_indices.add(index)
256
+ continue
257
+ if (
258
+ document_body_profile is not None
259
+ and 0.9 <= line_height / document_body_profile.body_height <= 1.1
260
+ and title_prototype is None
261
+ and centered
262
+ and not has_following_body_row
263
+ and not compact_text_section
264
+ ):
265
+ continue
266
+ if height_ratio < 0.9 and not inside_front_matter and not has_following_body_row and not compact_text_section:
267
+ continue
268
+ if width_ratio >= 0.9 and height_ratio < 1.15 and not (style_differs and gap_above >= 0.65):
269
+ continue
270
+ score = 0.0
271
+ if style_differs or compact_local_transition:
272
+ score += 2.0
273
+ if compact_local_transition or style_support <= 0.2:
274
+ score += 0.75
275
+ if weight_emphasized:
276
+ score += 1.0
277
+ if height_ratio >= 1.18:
278
+ score += 2.0
279
+ elif height_ratio <= 0.9 and centered:
280
+ score += 0.75
281
+ if width_ratio <= 0.78:
282
+ score += 0.75
283
+ if centered and width_ratio <= 0.85:
284
+ score += 1.25
285
+ if gap_above >= 0.65:
286
+ score += 1.25
287
+ elif gap_above >= 0.35:
288
+ score += 0.5
289
+ if gap_below >= 0.35:
290
+ score += 0.75
291
+ if left_aligned:
292
+ score += 0.25
293
+
294
+ precisely_centered = abs(_bbox_center_x(bbox) - (lane.left + lane.right) / 2.0) <= 0.05 * lane_width
295
+ centered_structural_fallback = (
296
+ 0.75 <= height_ratio <= 1.15
297
+ and precisely_centered
298
+ and width_ratio <= 0.7
299
+ and not inside_front_matter
300
+ and not (page_index == 0 and _bbox_center_y(bbox) <= 0.2 * local_page_height)
301
+ and 0.35 <= gap_above <= 1.5
302
+ and 0.2 <= gap_below <= 1.5
303
+ and has_following_body_row
304
+ and not _is_continuous_field_row(
305
+ rows,
306
+ index,
307
+ lane_width,
308
+ profile,
309
+ )
310
+ )
311
+ strong_layout_signal = (
312
+ height_ratio >= 1.18
313
+ or style_differs
314
+ or compact_local_transition
315
+ or weight_emphasized
316
+ or centered_structural_fallback
317
+ or compact_text_section
318
+ )
319
+ if score >= 4.0 and (has_spacing_signal or compact_local_transition) and strong_layout_signal:
320
+ if _is_full_width_inline_heading(
321
+ rows,
322
+ index,
323
+ lane_width,
324
+ profile,
325
+ ):
326
+ continue
327
+ line.semantic_type = "paragraph_title"
328
+ selected_indices.add(index)
329
+
330
+ _unify_visual_row_title_types(
331
+ lane,
332
+ selected_indices,
333
+ )
334
+ _expand_paragraph_title_neighbors(
335
+ lane,
336
+ selected_indices,
337
+ profile,
338
+ )
339
+ if front_matter_boundary is not None and preserve_front_matter_boundaries:
340
+ _protect_front_matter_title_types(
341
+ lane,
342
+ profile,
343
+ front_matter_boundary,
344
+ )
345
+
346
+
347
+ def _protect_front_matter_title_types(
348
+ lane: _TextLane,
349
+ profile: _LaneBodyProfile,
350
+ front_matter_boundary: float,
351
+ ) -> None:
352
+ """把作者区误命中的标题降为正文,并仅保留原本需要跨行聚合的内部标记。"""
353
+
354
+ front_title_indices = [
355
+ index
356
+ for index, (line, bbox) in enumerate(lane.lines)
357
+ if line.semantic_type == "paragraph_title" and _bbox_center_y(bbox) <= front_matter_boundary
358
+ ]
359
+ connected_indices: set[int] = set()
360
+ for position, first_index in enumerate(front_title_indices):
361
+ for second_index in front_title_indices[position + 1 :]:
362
+ if _should_connect_semantic_rows(
363
+ lane.lines[first_index],
364
+ lane.lines[second_index],
365
+ lane,
366
+ profile.regular_gap,
367
+ [],
368
+ [],
369
+ ):
370
+ connected_indices.update((first_index, second_index))
371
+ for index in front_title_indices:
372
+ lane.lines[index][0].semantic_type = "text" if index in connected_indices else None
373
+
374
+
375
+ def _visual_row_has_body_style_sibling(
376
+ rows: list[tuple[_LineItem, BBox]],
377
+ index: int,
378
+ ) -> bool:
379
+ """检查同一完整视觉行是否同时包含标题样式与正文样式 run。"""
380
+
381
+ line, _bbox = rows[index]
382
+ if line.visual_row_id is None:
383
+ return False
384
+ siblings = [
385
+ sibling for sibling, _sibling_bbox in rows if sibling is not line and sibling.visual_row_id == line.visual_row_id
386
+ ]
387
+ return any(not _title_fonts_compatible(line, sibling) for sibling in siblings)
388
+
389
+
390
+ def _is_near_full_mixed_inline_row(
391
+ rows: list[tuple[_LineItem, BBox]],
392
+ index: int,
393
+ lane_width: float,
394
+ profile: _LaneBodyProfile,
395
+ ) -> bool:
396
+ """识别近满栏混合字体行内强调,避免把粗体条目头单独标成标题。"""
397
+
398
+ line, bbox = rows[index]
399
+ line_height = _line_effective_height(line, bbox)
400
+ if (
401
+ line.font_coverage >= 0.95
402
+ or (bbox[2] - bbox[0]) < 0.85 * lane_width
403
+ or not 0.85 <= line_height / profile.body_height <= 1.15
404
+ or index + 1 >= len(rows)
405
+ ):
406
+ return False
407
+ next_line, next_bbox = rows[index + 1]
408
+ next_height = _line_effective_height(next_line, next_bbox)
409
+ gap_below = _effective_text_row_gap(rows[index], rows[index + 1])
410
+ return (
411
+ next_line.semantic_type is None
412
+ and 0.8 <= next_height / profile.body_height <= 1.2
413
+ and gap_below <= profile.regular_gap + 0.25 * profile.body_height
414
+ )
415
+
416
+
417
+ def _continues_local_body_row(
418
+ rows: list[tuple[_LineItem, BBox]],
419
+ index: int,
420
+ lane_width: float,
421
+ profile: _LaneBodyProfile,
422
+ ) -> bool:
423
+ """识别紧随同字体满行的正文尾行,阻止全局小字号基线造成标题误判。"""
424
+
425
+ if index <= 0:
426
+ return False
427
+ previous_line, previous_bbox = rows[index - 1]
428
+ line, bbox = rows[index]
429
+ if previous_line.semantic_type is not None:
430
+ return False
431
+ previous_visual_row = (
432
+ [item for item in rows[:index] if item[0].visual_row_id == previous_line.visual_row_id]
433
+ if previous_line.visual_row_id is not None
434
+ else [rows[index - 1]]
435
+ )
436
+ previous_style_members = [
437
+ member
438
+ for member, _member_bbox in previous_visual_row
439
+ if member.font_signature is not None and member.font_coverage >= 0.5
440
+ ]
441
+ if (
442
+ line.font_signature is not None
443
+ and previous_style_members
444
+ and not any(
445
+ _font_signatures_share_family(member.font_signature, line.font_signature)
446
+ and not _font_weights_conflict(member, line)
447
+ for member in previous_style_members
448
+ )
449
+ ):
450
+ return False
451
+ previous_bbox = _bbox_union_many([member_bbox for _member, member_bbox in previous_visual_row])
452
+ previous_height = statistics.median(
453
+ _line_effective_height(member, member_bbox) for member, member_bbox in previous_visual_row
454
+ )
455
+ line_height = _line_effective_height(line, bbox)
456
+ pair_height = max(previous_height, line_height)
457
+ gap = _effective_text_row_gap(
458
+ (previous_line, previous_bbox),
459
+ rows[index],
460
+ )
461
+ return (
462
+ 0.8 <= line_height / previous_height <= 1.25
463
+ and previous_bbox[2] - previous_bbox[0] >= 0.75 * lane_width
464
+ and abs(bbox[0] - previous_bbox[0]) <= 2.0 * pair_height
465
+ and -0.25 * pair_height <= gap <= profile.regular_gap + pair_height
466
+ )
467
+
468
+
469
+ def _is_continuous_field_row(
470
+ rows: list[tuple[_LineItem, BBox]],
471
+ index: int,
472
+ lane_width: float,
473
+ profile: _LaneBodyProfile,
474
+ ) -> bool:
475
+ """识别同缩进、同尺度且紧邻的连续字段行,阻止其借居中误差成为标题。"""
476
+
477
+ _line, bbox = rows[index]
478
+ line_height = _line_effective_height(*rows[index])
479
+ if not 0.85 <= line_height / profile.body_height <= 1.15:
480
+ return False
481
+ for neighbor_index in (index - 1, index + 1):
482
+ if not 0 <= neighbor_index < len(rows):
483
+ continue
484
+ neighbor_line, neighbor_bbox = rows[neighbor_index]
485
+ neighbor_height = _line_effective_height(neighbor_line, neighbor_bbox)
486
+ if (
487
+ neighbor_line.semantic_type is None
488
+ and 0.85 <= neighbor_height / profile.body_height <= 1.15
489
+ and neighbor_bbox[2] - neighbor_bbox[0] >= 0.35 * lane_width
490
+ and abs(neighbor_bbox[0] - bbox[0]) <= 0.75 * profile.body_height
491
+ and max(
492
+ _effective_text_row_gap(rows[neighbor_index], rows[index]),
493
+ _effective_text_row_gap(rows[index], rows[neighbor_index]),
494
+ )
495
+ <= 1.5 * profile.body_height
496
+ ):
497
+ return True
498
+ return False
499
+
500
+
501
+ def _is_full_width_inline_heading(
502
+ rows: list[tuple[_LineItem, BBox]],
503
+ index: int,
504
+ lane_width: float,
505
+ profile: _LaneBodyProfile,
506
+ ) -> bool:
507
+ """识别后接常规正文续行的满栏正常字号行内标题。"""
508
+
509
+ line, bbox = rows[index]
510
+ line_height = _line_effective_height(line, bbox)
511
+ if (
512
+ not 0.85 <= line_height / profile.body_height <= 1.15
513
+ or (bbox[2] - bbox[0]) < 0.9 * lane_width
514
+ or index + 1 >= len(rows)
515
+ ):
516
+ return False
517
+
518
+ next_line, next_bbox = rows[index + 1]
519
+ next_height = _line_effective_height(next_line, next_bbox)
520
+ next_uses_body_font = (
521
+ profile.body_font is None
522
+ or next_line.font_signature is None
523
+ or next_line.font_coverage < 0.75
524
+ or _font_signatures_share_family(
525
+ next_line.font_signature,
526
+ profile.body_font,
527
+ )
528
+ )
529
+ return (
530
+ next_line.semantic_type is None
531
+ and next_uses_body_font
532
+ and 0.75 <= next_height / profile.body_height <= 1.15
533
+ and (next_bbox[2] - next_bbox[0]) >= 0.75 * lane_width
534
+ and _effective_text_row_gap(rows[index], rows[index + 1]) <= profile.regular_gap + 0.25 * profile.body_height
535
+ )
536
+
537
+
538
+ def _has_following_body_row(
539
+ rows: list[tuple[_LineItem, BBox]],
540
+ index: int,
541
+ lane_width: float,
542
+ lane_left: float,
543
+ profile: _LaneBodyProfile,
544
+ ) -> bool:
545
+ """只检查紧邻且空间相关的下一行,禁止用远处正文支撑小字号标题。"""
546
+
547
+ if index + 1 >= len(rows):
548
+ return False
549
+ current = rows[index]
550
+ line, bbox = rows[index + 1]
551
+ gap = _effective_text_row_gap(current, (line, bbox))
552
+ horizontally_related = (
553
+ _bbox_axis_overlap_ratio(current[1], bbox, axis="x") >= 0.35
554
+ or abs(current[1][0] - bbox[0]) <= 0.75 * profile.body_height
555
+ )
556
+ current_centered = abs(_bbox_center_x(current[1]) - (lane_left + lane_width / 2.0)) <= 0.12 * lane_width
557
+ compact_left_aligned = (
558
+ current[1][2] - current[1][0] <= 0.35 * lane_width and abs(current[1][0] - lane_left) <= 0.65 * profile.body_height
559
+ )
560
+ maximum_gap = (1.5 if current_centered else 1.25 if compact_left_aligned else 0.75) * profile.body_height
561
+ height_ratio = _line_effective_height(line, bbox) / profile.body_height
562
+ width_ratio = (bbox[2] - bbox[0]) / lane_width
563
+ return (
564
+ line.semantic_type is None
565
+ and horizontally_related
566
+ and -0.25 * profile.body_height <= gap <= maximum_gap
567
+ and 0.9 <= height_ratio <= 1.2
568
+ and width_ratio >= 0.45
569
+ )
570
+
571
+
572
+ def _has_following_compact_text_section(
573
+ rows: list[tuple[_LineItem, BBox]],
574
+ index: int,
575
+ lane_width: float,
576
+ ) -> bool:
577
+ """检查小字号候选后是否紧接三行同尺度的宽文本区段。"""
578
+
579
+ candidate = rows[index]
580
+ candidate_height = _line_effective_height(*candidate)
581
+ following_rows = rows[index + 1 : index + 4]
582
+ if len(following_rows) < 3:
583
+ return False
584
+
585
+ previous = candidate
586
+ for current in following_rows:
587
+ line, bbox = current
588
+ height_ratio = _line_effective_height(line, bbox) / candidate_height
589
+ width_ratio = (bbox[2] - bbox[0]) / lane_width
590
+ gap = _effective_text_row_gap(previous, current)
591
+ if (
592
+ line.semantic_type is not None
593
+ or not 0.75 <= height_ratio <= 1.25
594
+ or width_ratio < 0.45
595
+ or not -0.25 * candidate_height <= gap <= 0.75 * candidate_height
596
+ ):
597
+ return False
598
+ previous = current
599
+ return True
600
+
601
+
602
+ def _unify_visual_row_title_types(
603
+ lane: _TextLane,
604
+ selected_indices: set[int],
605
+ ) -> None:
606
+ """按完整 visual_row_id 统一标题类型:同样式整行晋升,混合样式整行降级。"""
607
+
608
+ row_indices: dict[int, list[int]] = {}
609
+ for index, (line, _bbox) in enumerate(lane.lines):
610
+ if line.visual_row_id is not None:
611
+ row_indices.setdefault(line.visual_row_id, []).append(index)
612
+ for indices in row_indices.values():
613
+ title_indices = [index for index in indices if lane.lines[index][0].semantic_type == "paragraph_title"]
614
+ if not title_indices:
615
+ continue
616
+ anchor_line, anchor_bbox = lane.lines[title_indices[0]]
617
+ anchor_height = _line_effective_height(anchor_line, anchor_bbox)
618
+ compatible = all(
619
+ 0.75 <= _line_effective_height(lane.lines[index][0], lane.lines[index][1]) / anchor_height <= 1.25
620
+ and _title_fonts_compatible(anchor_line, lane.lines[index][0])
621
+ for index in indices
622
+ )
623
+ if compatible:
624
+ for index in indices:
625
+ lane.lines[index][0].semantic_type = "paragraph_title"
626
+ selected_indices.add(index)
627
+ continue
628
+ for index in title_indices:
629
+ lane.lines[index][0].semantic_type = None
630
+ selected_indices.discard(index)
631
+
632
+
633
+ def _infer_front_matter_boundary(
634
+ lane: _TextLane,
635
+ profile: _LaneBodyProfile,
636
+ local_page_height: float,
637
+ *,
638
+ document_title_bottom: float | None,
639
+ ) -> float | None:
640
+ """用短行后衔接满栏正文的几何转折点界定首页作者等前置信息。"""
641
+
642
+ if document_title_bottom is None:
643
+ return None
644
+ default_boundary = max(
645
+ document_title_bottom + 2.5 * profile.body_height,
646
+ 0.39 * local_page_height,
647
+ )
648
+ lane_width = max(0.1, lane.right - lane.left)
649
+ for index, (line, bbox) in enumerate(lane.lines[:-1]):
650
+ if bbox[1] <= document_title_bottom:
651
+ continue
652
+ next_line, next_bbox = lane.lines[index + 1]
653
+ width_ratio = (bbox[2] - bbox[0]) / lane_width
654
+ next_width_ratio = (next_bbox[2] - next_bbox[0]) / lane_width
655
+ left_aligned = abs(bbox[0] - lane.left) <= 0.65 * profile.body_height
656
+ centered = abs(_bbox_center_x(bbox) - (lane.left + lane.right) / 2.0) <= 0.12 * lane_width
657
+ next_height_ratio = _line_effective_height(next_line, next_bbox) / profile.body_height
658
+ gap_below = max(0.0, _effective_text_row_gap((line, bbox), (next_line, next_bbox)))
659
+ if (
660
+ width_ratio <= 0.35
661
+ and next_width_ratio >= 0.75
662
+ and (left_aligned or centered)
663
+ and 0.75 <= next_height_ratio <= 1.35
664
+ and gap_below <= 1.5 * profile.body_height
665
+ ):
666
+ return min(default_boundary, bbox[1] - 0.1)
667
+ return default_boundary
668
+
669
+
670
+ def _normalized_title_gap(
671
+ rows: list[tuple[_LineItem, BBox]],
672
+ index: int,
673
+ *,
674
+ direction: Literal[-1, 1],
675
+ body_height: float,
676
+ physical_gaps: dict[int, tuple[float | None, float | None]],
677
+ ) -> float:
678
+ """返回栏内邻行和页面最近物理邻行中较小的归一化净空。"""
679
+
680
+ neighbor_index = index + direction
681
+ if 0 <= neighbor_index < len(rows):
682
+ if direction < 0:
683
+ lane_gap = _effective_text_row_gap(rows[neighbor_index], rows[index])
684
+ else:
685
+ lane_gap = _effective_text_row_gap(rows[index], rows[neighbor_index])
686
+ else:
687
+ lane_gap = 0.5 * body_height
688
+ physical_pair = physical_gaps.get(rows[index][0].source_index, (None, None))
689
+ physical_gap = physical_pair[0 if direction < 0 else 1]
690
+ gap = lane_gap if physical_gap is None else min(lane_gap, physical_gap)
691
+ return max(0.0, gap) / max(0.1, body_height)
692
+
693
+
694
+ def _expand_paragraph_title_neighbors(
695
+ lane: _TextLane,
696
+ selected_indices: set[int],
697
+ profile: _LaneBodyProfile,
698
+ ) -> None:
699
+ """用相邻行的字体、尺寸、对齐和紧凑净空补齐折行段落标题。"""
700
+
701
+ if not selected_indices:
702
+ return
703
+ rows = lane.lines
704
+ lane_width = max(0.1, lane.right - lane.left)
705
+ pending = list(selected_indices)
706
+ while pending:
707
+ selected_index = pending.pop()
708
+ selected_line, selected_bbox = rows[selected_index]
709
+ selected_height = _line_effective_height(selected_line, selected_bbox)
710
+ for candidate_index in (selected_index - 1, selected_index + 1):
711
+ if not 0 <= candidate_index < len(rows) or candidate_index in selected_indices:
712
+ continue
713
+ candidate_line, candidate_bbox = rows[candidate_index]
714
+ if candidate_line.semantic_type is not None:
715
+ continue
716
+ candidate_height = _line_effective_height(candidate_line, candidate_bbox)
717
+ if not 0.75 <= candidate_height / selected_height <= 1.25:
718
+ continue
719
+ if candidate_bbox[2] - candidate_bbox[0] > 0.8 * lane_width:
720
+ continue
721
+ if (
722
+ selected_line.font_signature is not None
723
+ and candidate_line.font_signature is not None
724
+ and selected_line.font_signature != candidate_line.font_signature
725
+ ):
726
+ continue
727
+ if not _title_fonts_compatible(selected_line, candidate_line):
728
+ continue
729
+ vertical_gap = max(
730
+ candidate_bbox[1] - selected_bbox[3],
731
+ selected_bbox[1] - candidate_bbox[3],
732
+ 0.0,
733
+ )
734
+ centers_align = abs(_bbox_center_x(candidate_bbox) - _bbox_center_x(selected_bbox)) <= 0.15 * lane_width
735
+ left_edges_align = abs(candidate_bbox[0] - selected_bbox[0]) <= 0.75 * profile.body_height
736
+ wrapped_indent = abs(candidate_bbox[0] - selected_bbox[0]) <= 2.0 * profile.body_height
737
+ if vertical_gap > 0.65 * profile.body_height or not (centers_align or left_edges_align or wrapped_indent):
738
+ continue
739
+ candidate_line.semantic_type = "paragraph_title"
740
+ selected_indices.add(candidate_index)
741
+ pending.append(candidate_index)
742
+
743
+
744
+ __all__ = [
745
+ "_classify_paragraph_titles_in_lane",
746
+ "_protect_front_matter_title_types",
747
+ "_visual_row_has_body_style_sibling",
748
+ "_is_near_full_mixed_inline_row",
749
+ "_continues_local_body_row",
750
+ "_is_continuous_field_row",
751
+ "_is_full_width_inline_heading",
752
+ "_has_following_body_row",
753
+ "_has_following_compact_text_section",
754
+ "_unify_visual_row_title_types",
755
+ "_infer_front_matter_boundary",
756
+ "_normalized_title_gap",
757
+ "_expand_paragraph_title_neighbors",
758
+ ]