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,975 @@
1
+ """按公式区域和字符几何识别上下标证据。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import statistics
6
+ import unicodedata
7
+ from typing import Any, Literal, Sequence
8
+
9
+ from .....schema import BBox
10
+ from ..geometry import _rotate_bbox_to_upright
11
+ from ..script_geometry import ScriptRole, classify_char_script_roles
12
+ from .common import _coerce_bbox, _normalize_match_fragment, _ordered_line_chars
13
+ from .types import (
14
+ _PDF_SCRIPT_AUTHOR_MARKS,
15
+ _PDF_SCRIPT_CITATION_BRACKETS,
16
+ _PDF_SCRIPT_COMPACT_JOINERS,
17
+ _PDF_SCRIPT_MATH_BASE_CHARS,
18
+ _PDF_SCRIPT_SIGN_CHARS,
19
+ _PDF_SCRIPT_SPACED_OPERATORS,
20
+ _PDF_SCRIPT_TOKEN_CONNECTORS,
21
+ _PDF_SCRIPT_TRAILING_MARKS,
22
+ PDFTextScriptLine,
23
+ PDFTextScriptRange,
24
+ )
25
+
26
+
27
+ def _rotate_origin_to_upright(
28
+ origin: tuple[float, float],
29
+ page_size: tuple[float, float],
30
+ angle: int,
31
+ ) -> tuple[float, float]:
32
+ """把页面字符 origin 旋到当前 Flash 行的局部正向坐标。"""
33
+ x, y = origin
34
+ page_width, page_height = page_size
35
+ if angle == 270:
36
+ return page_height - y, x
37
+ if angle == 90:
38
+ return y, page_width - x
39
+ if angle == 180:
40
+ return page_width - x, page_height - y
41
+ return origin
42
+
43
+
44
+ def _bbox_center_inside_region(bbox: BBox, region: BBox) -> bool:
45
+ """判断字符 tight bbox 中心是否落入公式区域。"""
46
+ center_x = (bbox[0] + bbox[2]) / 2
47
+ center_y = (bbox[1] + bbox[3]) / 2
48
+ return region[0] <= center_x <= region[2] and region[1] <= center_y <= region[3]
49
+
50
+
51
+ def _script_region_memberships(
52
+ chars: list[dict[str, Any]],
53
+ tight_bboxes: dict[int, BBox],
54
+ regions: list[BBox],
55
+ ) -> list[int | None]:
56
+ """按页面 tight 中心把字符分配到公式区域,区域外返回 None。"""
57
+ memberships: list[int | None] = []
58
+ for char in chars:
59
+ char_idx = char.get("char_idx")
60
+ tight_bbox = tight_bboxes.get(char_idx) if isinstance(char_idx, int) else None
61
+ region_index = None
62
+ if tight_bbox is not None:
63
+ region_index = next(
64
+ (index for index, region in enumerate(regions) if _bbox_center_inside_region(tight_bbox, region)),
65
+ None,
66
+ )
67
+ memberships.append(region_index)
68
+ return memberships
69
+
70
+
71
+ def _script_char_text(char: dict[str, Any]) -> str:
72
+ """返回单字符脚本判定使用的稳定文本。"""
73
+ return str(char.get("char", ""))
74
+
75
+
76
+ def _is_cjk_text(text: str) -> bool:
77
+ """判断单字符是否属于 CJK、日文假名或韩文书写系统。"""
78
+ if len(text) != 1:
79
+ return False
80
+ codepoint = ord(text)
81
+ return (
82
+ 0x3400 <= codepoint <= 0x4DBF
83
+ or 0x4E00 <= codepoint <= 0x9FFF
84
+ or 0xF900 <= codepoint <= 0xFAFF
85
+ or 0x3040 <= codepoint <= 0x30FF
86
+ or 0xAC00 <= codepoint <= 0xD7AF
87
+ )
88
+
89
+
90
+ def _is_math_identifier_char(text: str) -> bool:
91
+ """识别可与拉丁 base/index 共同组成数学 token 的字母数字字符。"""
92
+ if len(text) != 1 or _is_cjk_text(text):
93
+ return False
94
+ if text.isascii():
95
+ return text.isalnum()
96
+ if "0" <= text <= "9":
97
+ return True
98
+ category = unicodedata.category(text)
99
+ unicode_name = unicodedata.name(text, "")
100
+ return (
101
+ text in _PDF_SCRIPT_MATH_BASE_CHARS
102
+ or "GREEK" in unicode_name
103
+ or "MATHEMATICAL" in unicode_name
104
+ or category in {"Lu", "Ll", "Lm"}
105
+ )
106
+
107
+
108
+ def _is_math_script_token_char(text: str) -> bool:
109
+ """判断字符是否属于可按 source order 重新锚定的数学 token。"""
110
+ return _is_math_identifier_char(text) or text in _PDF_SCRIPT_TOKEN_CONNECTORS
111
+
112
+
113
+ def _iter_math_script_tokens(chars: list[dict[str, Any]]) -> list[list[int]]:
114
+ """按连续数学 identifier 和连接符切分局部 token,并在 CJK 边界断开。"""
115
+ tokens: list[list[int]] = []
116
+ current: list[int] = []
117
+ for index, char in enumerate(chars):
118
+ if _is_math_script_token_char(_script_char_text(char)):
119
+ current.append(index)
120
+ continue
121
+ if current:
122
+ tokens.append(current)
123
+ current = []
124
+ if current:
125
+ tokens.append(current)
126
+ return tokens
127
+
128
+
129
+ def _citation_script_indices(chars: list[dict[str, Any]], roles: list[ScriptRole]) -> set[int]:
130
+ """识别方括号引用区间,避免保守 token 规则删除数字引用。"""
131
+ protected: set[int] = set()
132
+ for start, char in enumerate(chars):
133
+ closing = _PDF_SCRIPT_CITATION_BRACKETS.get(_script_char_text(char))
134
+ if closing is None:
135
+ continue
136
+ for end in range(start + 1, min(len(chars), start + 16)):
137
+ if _script_char_text(chars[end]) != closing:
138
+ continue
139
+ if any(roles[index] != "body" and _script_char_text(chars[index]).isalnum() for index in range(start + 1, end)):
140
+ protected.update(range(start, end + 1))
141
+ break
142
+ return protected
143
+
144
+
145
+ def _token_origin(
146
+ char: dict[str, Any],
147
+ origins: dict[int, tuple[float, float]],
148
+ ) -> float | None:
149
+ """读取 token 字符的局部正向 origin y。"""
150
+ char_idx = char.get("char_idx")
151
+ origin = origins.get(char_idx) if isinstance(char_idx, int) else None
152
+ return float(origin[1]) if origin is not None else None
153
+
154
+
155
+ def _token_tight_height(
156
+ char: dict[str, Any],
157
+ tight_bboxes: dict[int, BBox],
158
+ ) -> float:
159
+ """读取 token 字符的局部正向 tight 高度。"""
160
+ char_idx = char.get("char_idx")
161
+ bbox = tight_bboxes.get(char_idx) if isinstance(char_idx, int) else None
162
+ return max(0.0, bbox[3] - bbox[1]) if bbox is not None else 0.0
163
+
164
+
165
+ def _has_adjacent_math_base(
166
+ chars: list[dict[str, Any]],
167
+ index: int,
168
+ roles: list[ScriptRole],
169
+ tight_bboxes: dict[int, BBox],
170
+ origins: dict[int, tuple[float, float]],
171
+ ) -> bool:
172
+ """判断孤立索引左侧是否存在紧邻且位移明确的非 CJK 数学 base。"""
173
+ if index <= 0 or roles[index - 1] != "body":
174
+ return False
175
+ base_text = _script_char_text(chars[index - 1])
176
+ if not _is_math_identifier_char(base_text):
177
+ return False
178
+ base_origin = _token_origin(chars[index - 1], origins)
179
+ script_origin = _token_origin(chars[index], origins)
180
+ base_height = _token_tight_height(chars[index - 1], tight_bboxes)
181
+ base_idx = chars[index - 1].get("char_idx")
182
+ script_idx = chars[index].get("char_idx")
183
+ base_bbox = tight_bboxes.get(base_idx) if isinstance(base_idx, int) else None
184
+ script_bbox = tight_bboxes.get(script_idx) if isinstance(script_idx, int) else None
185
+ if base_origin is None or script_origin is None or base_bbox is None or script_bbox is None:
186
+ return False
187
+ return (
188
+ _bbox_axis_overlap(base_bbox, script_bbox, axis="y") > 0
189
+ or _horizontal_gap_between_bboxes(base_bbox, script_bbox) <= max(2.0, 0.5 * base_height)
190
+ ) and abs(script_origin - base_origin) >= max(0.35, 0.08 * base_height)
191
+
192
+
193
+ def _horizontal_gap_between_bboxes(first: BBox, second: BBox) -> float:
194
+ """返回两个 tight bbox 的水平间隙。"""
195
+ return max(0.0, first[0] - second[2], second[0] - first[2])
196
+
197
+
198
+ def _token_split_position(
199
+ chars: list[dict[str, Any]],
200
+ token: list[int],
201
+ roles: list[ScriptRole],
202
+ tight_bboxes: dict[int, BBox],
203
+ origins: dict[int, tuple[float, float]],
204
+ ) -> int | None:
205
+ """用最左 origin 簇和显式连接符确定 base 与索引的分界。"""
206
+ alnum_positions = [index for index in token if _is_math_identifier_char(_script_char_text(chars[index]))]
207
+ if len(alnum_positions) < 2:
208
+ return None
209
+ first = alnum_positions[0]
210
+ first_origin = _token_origin(chars[first], origins)
211
+ first_height = _token_tight_height(chars[first], tight_bboxes)
212
+ origin_tolerance = max(0.35, 0.06 * first_height)
213
+ leading_connectors = [
214
+ index for index in token if index < first and _script_char_text(chars[index]) in _PDF_SCRIPT_TOKEN_CONNECTORS
215
+ ]
216
+ if leading_connectors:
217
+ return alnum_positions[1]
218
+ scripted_positions = [position for position in alnum_positions[1:] if roles[position] != "body"]
219
+ if roles[first] == "body" and scripted_positions:
220
+ first_scripted = scripted_positions[0]
221
+ prefix = [index for index in token if index < first_scripted]
222
+ suffix = [index for index in token if index >= first_scripted]
223
+ if (
224
+ len(prefix) >= 2
225
+ and all(roles[index] == "body" and _script_char_text(chars[index]).isalpha() for index in prefix)
226
+ and all(roles[index] == "sup" and _script_char_text(chars[index]).isdigit() for index in suffix)
227
+ ):
228
+ # 姓名、词语后的数字上标已具备明确边界,不用正文内部的下伸字形重新切分。
229
+ return first_scripted
230
+ for position in alnum_positions[1:]:
231
+ if any(_script_char_text(chars[index]) in _PDF_SCRIPT_TOKEN_CONNECTORS for index in range(first + 1, position)):
232
+ return position
233
+ origin = _token_origin(chars[position], origins)
234
+ if first_origin is not None and origin is not None and abs(origin - first_origin) > origin_tolerance:
235
+ return position
236
+ if roles[first] == "body" and scripted_positions:
237
+ return scripted_positions[0]
238
+ return None
239
+
240
+
241
+ def _script_geometry_is_aligned(
242
+ chars: list[dict[str, Any]],
243
+ first: int,
244
+ second: int,
245
+ tight_bboxes: dict[int, BBox],
246
+ origins: dict[int, tuple[float, float]],
247
+ ) -> bool:
248
+ """判断两个字符是否处在同一 displaced baseline 上。"""
249
+ first_origin = _token_origin(chars[first], origins)
250
+ second_origin = _token_origin(chars[second], origins)
251
+ first_height = _token_tight_height(chars[first], tight_bboxes)
252
+ second_height = _token_tight_height(chars[second], tight_bboxes)
253
+ first_idx = chars[first].get("char_idx")
254
+ second_idx = chars[second].get("char_idx")
255
+ first_bbox = tight_bboxes.get(first_idx) if isinstance(first_idx, int) else None
256
+ second_bbox = tight_bboxes.get(second_idx) if isinstance(second_idx, int) else None
257
+ if first_origin is None or second_origin is None or first_bbox is None or second_bbox is None:
258
+ return False
259
+ scale = max(first_height, second_height, 1.0)
260
+ first_center = (first_bbox[1] + first_bbox[3]) / 2
261
+ second_center = (second_bbox[1] + second_bbox[3]) / 2
262
+ return abs(first_origin - second_origin) <= max(0.35, 0.06 * scale) and abs(first_center - second_center) <= max(
263
+ 0.75,
264
+ 0.3 * scale,
265
+ )
266
+
267
+
268
+ def _nearest_nonspace_index(
269
+ chars: list[dict[str, Any]],
270
+ start: int,
271
+ step: Literal[-1, 1],
272
+ ) -> int | None:
273
+ """从指定位置向前或向后查找最近的非空白字符。"""
274
+ index = start + step
275
+ while 0 <= index < len(chars):
276
+ if not _script_char_text(chars[index]).isspace():
277
+ return index
278
+ index += step
279
+ return None
280
+
281
+
282
+ def _close_spaced_script_operators(
283
+ chars: list[dict[str, Any]],
284
+ roles: list[ScriptRole],
285
+ tight_bboxes: dict[int, BBox],
286
+ origins: dict[int, tuple[float, float]],
287
+ ) -> None:
288
+ """跨少量 PDF 空格闭合同基线的 `1 - x` 一类角标 run。"""
289
+ for seed, role in enumerate(list(roles)):
290
+ if role == "body" or not _is_math_identifier_char(_script_char_text(chars[seed])):
291
+ continue
292
+ operator_index = _nearest_nonspace_index(chars, seed, 1)
293
+ if operator_index is None or operator_index - seed > 3:
294
+ continue
295
+ if _script_char_text(chars[operator_index]) not in _PDF_SCRIPT_SPACED_OPERATORS:
296
+ continue
297
+ target = _nearest_nonspace_index(chars, operator_index, 1)
298
+ if target is None or target - operator_index > 3:
299
+ continue
300
+ if not _is_math_identifier_char(_script_char_text(chars[target])):
301
+ continue
302
+ if not _script_geometry_is_aligned(chars, seed, operator_index, tight_bboxes, origins):
303
+ continue
304
+ if not _script_geometry_is_aligned(chars, seed, target, tight_bboxes, origins):
305
+ continue
306
+ roles[operator_index] = role
307
+ roles[target] = role
308
+
309
+
310
+ def _close_compact_aligned_script_suffixes(
311
+ chars: list[dict[str, Any]],
312
+ raw_roles: list[ScriptRole],
313
+ refined_roles: list[ScriptRole],
314
+ tight_bboxes: dict[int, BBox],
315
+ origins: dict[int, tuple[float, float]],
316
+ ) -> None:
317
+ """把已有可信角标 run 后同基线的紧凑连字符后缀整体闭合。"""
318
+ for joiner_index in range(1, len(chars) - 1):
319
+ if _script_char_text(chars[joiner_index]) not in _PDF_SCRIPT_COMPACT_JOINERS:
320
+ continue
321
+ left_seed = joiner_index - 1
322
+ role = refined_roles[left_seed]
323
+ if role == "body" or not _is_math_identifier_char(_script_char_text(chars[left_seed])):
324
+ continue
325
+
326
+ left_start = left_seed
327
+ while (
328
+ left_start > 0
329
+ and refined_roles[left_start - 1] == role
330
+ and _is_math_identifier_char(_script_char_text(chars[left_start - 1]))
331
+ ):
332
+ left_start -= 1
333
+ if left_seed - left_start + 1 < 2:
334
+ continue
335
+ anchor_index = left_start - 1
336
+ if (
337
+ anchor_index < 0
338
+ or refined_roles[anchor_index] != "body"
339
+ or not _is_math_identifier_char(_script_char_text(chars[anchor_index]))
340
+ ):
341
+ continue
342
+
343
+ suffix_start = joiner_index + 1
344
+ suffix_end = suffix_start
345
+ while suffix_end < len(chars) and _is_math_identifier_char(_script_char_text(chars[suffix_end])):
346
+ suffix_end += 1
347
+ if suffix_end - suffix_start < 2:
348
+ continue
349
+ restored_indices = range(joiner_index, suffix_end)
350
+ if any(raw_roles[index] != role for index in restored_indices):
351
+ continue
352
+ if not _script_geometry_is_aligned(chars, left_seed, joiner_index, tight_bboxes, origins):
353
+ continue
354
+ if any(
355
+ not _script_geometry_is_aligned(chars, left_seed, index, tight_bboxes, origins)
356
+ for index in range(suffix_start, suffix_end)
357
+ ):
358
+ continue
359
+ refined_roles[joiner_index:suffix_end] = [role] * (suffix_end - joiner_index)
360
+
361
+
362
+ def _protected_subscript_indices(
363
+ chars: list[dict[str, Any]],
364
+ roles: list[ScriptRole],
365
+ tokens: list[list[int]],
366
+ tight_bboxes: dict[int, BBox],
367
+ origins: dict[int, tuple[float, float]],
368
+ ) -> set[int]:
369
+ """找出拥有内部 base 或与其同基线连通的下标字符。"""
370
+ protected: set[int] = set()
371
+ for token in tokens:
372
+ for position, index in enumerate(token):
373
+ if roles[index] != "sub" or not _is_math_identifier_char(_script_char_text(chars[index])):
374
+ continue
375
+ if any(
376
+ earlier < index and roles[earlier] == "body" and _is_math_identifier_char(_script_char_text(chars[earlier]))
377
+ for earlier in token[:position]
378
+ ) or _has_adjacent_math_base(chars, index, roles, tight_bboxes, origins):
379
+ protected.add(index)
380
+ changed = True
381
+ while changed:
382
+ changed = False
383
+ for index, role in enumerate(roles):
384
+ if role != "sub" or index in protected or not _is_math_identifier_char(_script_char_text(chars[index])):
385
+ continue
386
+ for seed in tuple(protected):
387
+ start, end = sorted((seed, index))
388
+ if end - start > 5 or not _script_geometry_is_aligned(chars, seed, index, tight_bboxes, origins):
389
+ continue
390
+ if all(
391
+ _script_char_text(chars[bridge]).isspace()
392
+ or _script_char_text(chars[bridge]) in _PDF_SCRIPT_TOKEN_CONNECTORS
393
+ or _script_char_text(chars[bridge]) in _PDF_SCRIPT_SIGN_CHARS
394
+ or _script_char_text(chars[bridge]) == "."
395
+ for bridge in range(start + 1, end)
396
+ ):
397
+ protected.add(index)
398
+ changed = True
399
+ break
400
+ return protected
401
+
402
+
403
+ def _refine_math_script_tokens(
404
+ chars: list[dict[str, Any]],
405
+ roles: list[ScriptRole],
406
+ tight_bboxes: dict[int, BBox],
407
+ origins: dict[int, tuple[float, float]],
408
+ *,
409
+ formula_region: bool,
410
+ ) -> list[ScriptRole]:
411
+ """以最左稳定簇保护 base,并对弱单字符和复杂未分段 token 保守拒识。"""
412
+ refined = list(roles)
413
+ citation_indices = _citation_script_indices(chars, refined)
414
+ complex_unsegmented_token = False
415
+ tokens = _iter_math_script_tokens(chars)
416
+ token_alnum_positions = {
417
+ tuple(token): [index for index in token if _is_math_identifier_char(_script_char_text(chars[index]))]
418
+ for token in tokens
419
+ }
420
+ token_splits = {
421
+ tuple(token): _token_split_position(
422
+ chars,
423
+ token,
424
+ refined,
425
+ tight_bboxes,
426
+ origins,
427
+ )
428
+ for token in tokens
429
+ }
430
+ token_families: dict[str, list[tuple[int, ...]]] = {}
431
+ for token in tokens:
432
+ key = tuple(token)
433
+ alnum_positions = token_alnum_positions[key]
434
+ if len(alnum_positions) >= 2:
435
+ token_families.setdefault(_script_char_text(chars[alnum_positions[0]]), []).append(key)
436
+ trusted_family_bases = {
437
+ base
438
+ for base, members in token_families.items()
439
+ if len(members) >= 3
440
+ or any(token_splits[member] is not None for member in members)
441
+ or any(any(_script_char_text(chars[index]) in _PDF_SCRIPT_TOKEN_CONNECTORS for index in member) for member in members)
442
+ }
443
+ for token in tokens:
444
+ if any(index in citation_indices for index in token):
445
+ continue
446
+ token_key = tuple(token)
447
+ alnum_positions = token_alnum_positions[token_key]
448
+ if not alnum_positions or not any(refined[index] != "body" for index in token):
449
+ continue
450
+ token_roles = {refined[index] for index in token if refined[index] != "body"}
451
+ if token_roles == {"sup", "sub"}:
452
+ complex_unsegmented_token = True
453
+ if len(alnum_positions) == 1:
454
+ continue
455
+ first_position = alnum_positions[0]
456
+ suffix_positions = alnum_positions[1:]
457
+ if (
458
+ refined[first_position] == "sup"
459
+ and all(refined[index] == "body" for index in suffix_positions)
460
+ and len(suffix_positions) >= 2
461
+ and all(_script_char_text(chars[index]).isalpha() for index in suffix_positions)
462
+ ):
463
+ continue
464
+ split_position = token_splits[token_key]
465
+ if split_position is None and _script_char_text(chars[alnum_positions[0]]) in trusted_family_bases:
466
+ split_position = alnum_positions[1]
467
+ if split_position is None:
468
+ if all(refined[index] != "body" for index in alnum_positions):
469
+ for index in token:
470
+ refined[index] = "body"
471
+ continue
472
+ base_positions = [index for index in alnum_positions if index < split_position]
473
+ base_origins = [origin for index in base_positions if (origin := _token_origin(chars[index], origins)) is not None]
474
+ base_heights = [_token_tight_height(chars[index], tight_bboxes) for index in base_positions]
475
+ base_origin = statistics.median(base_origins) if base_origins else None
476
+ base_height = statistics.median([height for height in base_heights if height > 0]) if any(base_heights) else 0.0
477
+ for index in token:
478
+ if index < split_position or _script_char_text(chars[index]) in _PDF_SCRIPT_TOKEN_CONNECTORS:
479
+ refined[index] = "body"
480
+ continue
481
+ text = _script_char_text(chars[index])
482
+ if not _is_math_identifier_char(text) or refined[index] != "body":
483
+ continue
484
+ origin = _token_origin(chars[index], origins)
485
+ if base_origin is None or origin is None:
486
+ continue
487
+ shift = origin - base_origin
488
+ if abs(shift) >= max(0.35, 0.08 * base_height):
489
+ refined[index] = "sub" if shift > 0 else "sup"
490
+ _close_spaced_script_operators(
491
+ chars,
492
+ refined,
493
+ tight_bboxes,
494
+ origins,
495
+ )
496
+ if complex_unsegmented_token and not formula_region:
497
+ for index in range(len(refined)):
498
+ if index not in citation_indices:
499
+ refined[index] = "body"
500
+ scripted_alnum = [
501
+ index for index, role in enumerate(refined) if role != "body" and _script_char_text(chars[index]).isalnum()
502
+ ]
503
+ if not formula_region and len(scripted_alnum) >= 2 and any(_script_char_text(char) in {"∑", "∫"} for char in chars):
504
+ for index in range(len(refined)):
505
+ if index not in citation_indices:
506
+ refined[index] = "body"
507
+ has_compact_multiply = any(
508
+ _script_char_text(char) == "×"
509
+ and 0 < index < len(chars) - 1
510
+ and not _script_char_text(chars[index - 1]).isspace()
511
+ and not _script_char_text(chars[index + 1]).isspace()
512
+ for index, char in enumerate(chars)
513
+ )
514
+ if not formula_region and len(scripted_alnum) >= 2 and has_compact_multiply:
515
+ for index in range(len(refined)):
516
+ if index not in citation_indices:
517
+ refined[index] = "body"
518
+ if not formula_region:
519
+ for operator_index, char in enumerate(chars):
520
+ operator = _script_char_text(char)
521
+ nearby = [candidate for candidate in scripted_alnum if abs(candidate - operator_index) <= 5]
522
+ if (
523
+ operator in {"/", "⁄"}
524
+ and any(candidate < operator_index for candidate in nearby)
525
+ and any(candidate > operator_index for candidate in nearby)
526
+ ):
527
+ for candidate in nearby:
528
+ if candidate not in citation_indices:
529
+ refined[candidate] = "body"
530
+ protected_subscripts = _protected_subscript_indices(
531
+ chars,
532
+ refined,
533
+ tokens,
534
+ tight_bboxes,
535
+ origins,
536
+ )
537
+ for index, role in enumerate(list(refined)):
538
+ if (
539
+ role == "sub"
540
+ and index not in citation_indices
541
+ and index not in protected_subscripts
542
+ and _is_math_identifier_char(_script_char_text(chars[index]))
543
+ ):
544
+ refined[index] = "body"
545
+ for index, role in enumerate(list(refined)):
546
+ if role == "body" or index in citation_indices:
547
+ continue
548
+ text = _script_char_text(chars[index])
549
+ if text.isalnum() or text in _PDF_SCRIPT_AUTHOR_MARKS:
550
+ continue
551
+ if text in {",", ","} and all(
552
+ 0 <= neighbor < len(refined)
553
+ and refined[neighbor] == role
554
+ and (_script_char_text(chars[neighbor]).isalnum() or _script_char_text(chars[neighbor]) in _PDF_SCRIPT_AUTHOR_MARKS)
555
+ for neighbor in (index - 1, index + 1)
556
+ ):
557
+ continue
558
+ if text == "." and all(
559
+ 0 <= neighbor < len(refined) and refined[neighbor] == role and _script_char_text(chars[neighbor]).isdigit()
560
+ for neighbor in (index - 1, index + 1)
561
+ ):
562
+ continue
563
+ if text in _PDF_SCRIPT_SIGN_CHARS:
564
+ sign_neighbors = [
565
+ neighbor
566
+ for step in (-1, 1)
567
+ if (neighbor := _nearest_nonspace_index(chars, index, step)) is not None
568
+ and abs(neighbor - index) <= 3
569
+ and refined[neighbor] == role
570
+ ]
571
+ if sign_neighbors and any(_script_char_text(chars[neighbor]).isdigit() for neighbor in sign_neighbors):
572
+ continue
573
+ if text in _PDF_SCRIPT_TRAILING_MARKS:
574
+ previous = _nearest_nonspace_index(chars, index, -1)
575
+ body_prefix = previous - 1 if previous is not None else -1
576
+ if (
577
+ role == "sup"
578
+ and previous is not None
579
+ and index - previous == 1
580
+ and body_prefix >= 0
581
+ and refined[body_prefix] == "body"
582
+ and _script_char_text(chars[body_prefix]).isalpha()
583
+ and refined[previous] == role
584
+ and roles[index] == role
585
+ and _script_char_text(chars[previous]).isalpha()
586
+ and _script_geometry_is_aligned(chars, previous, index, tight_bboxes, origins)
587
+ ):
588
+ continue
589
+ refined[index] = "body"
590
+ if not formula_region:
591
+ _close_compact_aligned_script_suffixes(
592
+ chars,
593
+ roles,
594
+ refined,
595
+ tight_bboxes,
596
+ origins,
597
+ )
598
+ return refined
599
+
600
+
601
+ def _bbox_axis_overlap(first: BBox, second: BBox, *, axis: Literal["x", "y"]) -> float:
602
+ """返回两个 bbox 在指定轴上的绝对重叠长度。"""
603
+ start, end = (0, 2) if axis == "x" else (1, 3)
604
+ return max(0.0, min(first[end], second[end]) - max(first[start], second[start]))
605
+
606
+
607
+ def _fraction_member_indices(
608
+ page_size: tuple[float, float],
609
+ all_chars: list[dict[str, Any]],
610
+ tight_bboxes: dict[int, BBox],
611
+ drawing_lines: Sequence[Any],
612
+ angle: int,
613
+ ) -> set[int]:
614
+ """按页面方向一次识别分数线两侧的上下叠字,供复杂分式整块拒识。"""
615
+ if not all_chars or not drawing_lines:
616
+ return set()
617
+ local_chars: list[tuple[int, str, BBox]] = []
618
+ local_heights = []
619
+ for char in all_chars:
620
+ char_idx = char.get("char_idx")
621
+ text = _script_char_text(char)
622
+ bbox = tight_bboxes.get(char_idx) if isinstance(char_idx, int) else None
623
+ if not isinstance(char_idx, int) or bbox is None or not text.isprintable() or text.isspace():
624
+ continue
625
+ local_bbox = _rotate_bbox_to_upright(bbox, page_size, angle)
626
+ local_chars.append((char_idx, text, local_bbox))
627
+ local_heights.append(local_bbox[3] - local_bbox[1])
628
+ scale = statistics.median([height for height in local_heights if height > 0]) if local_heights else 8.0
629
+ members: set[int] = set()
630
+ for drawing in drawing_lines:
631
+ raw_bbox = _coerce_bbox(getattr(drawing, "bbox", drawing))
632
+ if raw_bbox is None:
633
+ continue
634
+ local_rule = _rotate_bbox_to_upright(raw_bbox, page_size, angle)
635
+ width = local_rule[2] - local_rule[0]
636
+ height = local_rule[3] - local_rule[1]
637
+ if width < max(2.0, 0.45 * scale) or width > 12.0 * scale or height > max(1.25, 0.25 * scale):
638
+ continue
639
+ rule_y = (local_rule[1] + local_rule[3]) / 2
640
+ aligned = [
641
+ (char_idx, bbox)
642
+ for char_idx, text, bbox in local_chars
643
+ if text.isalnum()
644
+ and abs((bbox[1] + bbox[3]) / 2 - rule_y) <= 2.25 * scale
645
+ and (
646
+ _bbox_axis_overlap(bbox, local_rule, axis="x") > 0
647
+ or local_rule[0] - 0.25 * scale <= (bbox[0] + bbox[2]) / 2 <= local_rule[2] + 0.25 * scale
648
+ )
649
+ ]
650
+ above = [
651
+ (char_idx, bbox)
652
+ for char_idx, bbox in aligned
653
+ if bbox[3] <= rule_y + 0.2 * scale and rule_y - bbox[3] <= 1.75 * scale
654
+ ]
655
+ below = [
656
+ (char_idx, bbox)
657
+ for char_idx, bbox in aligned
658
+ if bbox[1] >= rule_y - 0.2 * scale and bbox[1] - rule_y <= 1.75 * scale
659
+ ]
660
+ # 超过局部公式尺度的长横线更像脚注/段落分隔线,不用于分式成员抑制。
661
+ if width > 8.0 * scale:
662
+ continue
663
+ if above and below:
664
+ members.update(char_idx for char_idx, _bbox in above)
665
+ members.update(char_idx for char_idx, _bbox in below)
666
+ return members
667
+
668
+
669
+ def _strong_structural_script_roles(
670
+ chars: list[dict[str, Any]],
671
+ tight_bboxes: dict[int, BBox],
672
+ origins: dict[int, tuple[float, float]],
673
+ ) -> dict[int, ScriptRole]:
674
+ """提取可在恢复公式区域中保留的引用和邻接 base 强脚本证据。"""
675
+
676
+ roles = classify_char_script_roles(
677
+ chars,
678
+ tight_bboxes=tight_bboxes,
679
+ origins=origins,
680
+ )
681
+ strong_roles: dict[int, ScriptRole] = {}
682
+ for index in _citation_script_indices(chars, roles):
683
+ if roles[index] != "body":
684
+ strong_roles[index] = roles[index]
685
+ for index, role in enumerate(roles):
686
+ if role != "sup" or not _is_math_identifier_char(_script_char_text(chars[index])):
687
+ continue
688
+ base_height = _token_tight_height(chars[index - 1], tight_bboxes) if index > 0 else 0.0
689
+ script_height = _token_tight_height(chars[index], tight_bboxes)
690
+ if base_height <= 0 or script_height > 0.8 * base_height:
691
+ continue
692
+ next_index = _nearest_nonspace_index(chars, index, 1)
693
+ if next_index is not None and _is_math_script_token_char(_script_char_text(chars[next_index])):
694
+ continue
695
+ if _has_adjacent_math_base(
696
+ chars,
697
+ index,
698
+ roles,
699
+ tight_bboxes,
700
+ origins,
701
+ ):
702
+ strong_roles[index] = role
703
+ return strong_roles
704
+
705
+
706
+ def _classify_script_runs(
707
+ chars: list[dict[str, Any]],
708
+ local_tight_bboxes: dict[int, BBox],
709
+ local_origins: dict[int, tuple[float, float]],
710
+ memberships: list[int | None],
711
+ ) -> tuple[list[str], list[int], list[bool]]:
712
+ """按公式区域边界分段分类,并要求公式段内部存在稳定 body。"""
713
+ roles: list[ScriptRole] = ["body"] * len(chars)
714
+ body_counts = [0] * len(chars)
715
+ formula_flags = [False] * len(chars)
716
+ start = 0
717
+ while start < len(chars):
718
+ membership = memberships[start]
719
+ end = start + 1
720
+ while end < len(chars) and memberships[end] == membership:
721
+ end += 1
722
+ run_chars = chars[start:end]
723
+ run_indices = {int(char["char_idx"]) for char in run_chars if isinstance(char.get("char_idx"), int)}
724
+ run_roles = classify_char_script_roles(
725
+ run_chars,
726
+ tight_bboxes={index: local_tight_bboxes[index] for index in run_indices if index in local_tight_bboxes},
727
+ origins={index: local_origins[index] for index in run_indices if index in local_origins},
728
+ )
729
+ run_roles = _refine_math_script_tokens(
730
+ run_chars,
731
+ run_roles,
732
+ local_tight_bboxes,
733
+ local_origins,
734
+ formula_region=membership is not None,
735
+ )
736
+ visible = [
737
+ index
738
+ for index, char in enumerate(run_chars)
739
+ if str(char.get("char", "")).isprintable() and not str(char.get("char", "")).isspace()
740
+ ]
741
+ body_count = sum(run_roles[index] == "body" and str(run_chars[index].get("char", "")).isalnum() for index in visible)
742
+ marked_count = sum(run_roles[index] != "body" for index in visible)
743
+ body_tight_heights = [
744
+ local_tight_bboxes[int(run_chars[index]["char_idx"])][3] - local_tight_bboxes[int(run_chars[index]["char_idx"])][1]
745
+ for index in visible
746
+ if run_roles[index] == "body"
747
+ and isinstance(run_chars[index].get("char_idx"), int)
748
+ and int(run_chars[index]["char_idx"]) in local_tight_bboxes
749
+ ]
750
+ script_tight_heights = [
751
+ local_tight_bboxes[int(run_chars[index]["char_idx"])][3] - local_tight_bboxes[int(run_chars[index]["char_idx"])][1]
752
+ for index in visible
753
+ if run_roles[index] != "body"
754
+ and isinstance(run_chars[index].get("char_idx"), int)
755
+ and int(run_chars[index]["char_idx"]) in local_tight_bboxes
756
+ ]
757
+ stable_formula_body = (
758
+ body_count > 0
759
+ and bool(body_tight_heights)
760
+ and (not script_tight_heights or max(body_tight_heights) >= 1.1 * max(script_tight_heights))
761
+ )
762
+ if membership is not None and (not stable_formula_body or marked_count >= len(visible)):
763
+ run_roles = ["body"] * len(run_chars)
764
+ for offset, role in enumerate(run_roles, start=start):
765
+ roles[offset] = role
766
+ body_counts[offset] = body_count
767
+ formula_flags[offset] = membership is not None
768
+ start = end
769
+ return roles, body_counts, formula_flags
770
+
771
+
772
+ def _script_line_char_roles(
773
+ line: Any,
774
+ page_size: tuple[float, float],
775
+ tight_bboxes: dict[int, BBox],
776
+ origins: dict[int, tuple[float, float]],
777
+ fraction_members: set[int],
778
+ ) -> tuple[list[dict[str, Any]], list[ScriptRole], list[int], list[bool]]:
779
+ """按正文同款公式分段返回原字符及其上下标角色。"""
780
+
781
+ chars = _ordered_line_chars(line)
782
+ if not chars:
783
+ return [], [], [], []
784
+ angle = int(getattr(line, "angle", 0) or 0) % 360
785
+ local_chars: list[dict[str, Any]] = []
786
+ local_tight_bboxes: dict[int, BBox] = {}
787
+ local_origins: dict[int, tuple[float, float]] = {}
788
+ for char in chars:
789
+ local_char = dict(char)
790
+ bbox = _coerce_bbox(char.get("bbox"))
791
+ if bbox is not None:
792
+ local_char["bbox"] = _rotate_bbox_to_upright(bbox, page_size, angle)
793
+ local_chars.append(local_char)
794
+ char_idx = char.get("char_idx")
795
+ if not isinstance(char_idx, int):
796
+ continue
797
+ tight_bbox = tight_bboxes.get(char_idx)
798
+ if tight_bbox is not None:
799
+ local_tight_bboxes[char_idx] = _rotate_bbox_to_upright(
800
+ tight_bbox,
801
+ page_size,
802
+ angle,
803
+ )
804
+ origin = origins.get(char_idx)
805
+ if origin is not None:
806
+ local_origins[char_idx] = _rotate_origin_to_upright(
807
+ origin,
808
+ page_size,
809
+ angle,
810
+ )
811
+ regions = [bbox for value in getattr(line, "inline_math_regions", []) if (bbox := _coerce_bbox(value)) is not None]
812
+ memberships = _script_region_memberships(chars, tight_bboxes, regions)
813
+ roles, body_counts, formula_flags = _classify_script_runs(
814
+ local_chars,
815
+ local_tight_bboxes,
816
+ local_origins,
817
+ memberships,
818
+ )
819
+ if bool(getattr(line, "compact_formula_cluster", False)) or (
820
+ bool(getattr(line, "restored_inline_cluster", False)) and bool(regions)
821
+ ):
822
+ strong_structural_roles = _strong_structural_script_roles(
823
+ local_chars,
824
+ local_tight_bboxes,
825
+ local_origins,
826
+ )
827
+ roles = [strong_structural_roles.get(index, "body") for index in range(len(roles))]
828
+ for index, char in enumerate(chars):
829
+ char_idx = char.get("char_idx")
830
+ if isinstance(char_idx, int) and char_idx in fraction_members:
831
+ roles[index] = "body"
832
+ return chars, roles, body_counts, formula_flags
833
+
834
+
835
+ def _script_line_payload(
836
+ line: Any,
837
+ page_size: tuple[float, float],
838
+ tight_bboxes: dict[int, BBox],
839
+ origins: dict[int, tuple[float, float]],
840
+ fraction_members: set[int],
841
+ ) -> PDFTextScriptLine | None:
842
+ """把 Flash 行转换为公式分段后的紧凑上下标 sidecar。"""
843
+
844
+ chars, roles, body_counts, formula_flags = _script_line_char_roles(
845
+ line,
846
+ page_size,
847
+ tight_bboxes,
848
+ origins,
849
+ fraction_members,
850
+ )
851
+ if not chars:
852
+ return None
853
+ angle = int(getattr(line, "angle", 0) or 0) % 360
854
+ compact_parts: list[str] = []
855
+ compact_roles: list[str] = []
856
+ compact_bboxes: list[BBox | None] = []
857
+ compact_body_counts: list[int] = []
858
+ compact_formula_flags: list[bool] = []
859
+ for index, char in enumerate(chars):
860
+ fragment = _normalize_match_fragment(char.get("char"))
861
+ if not fragment:
862
+ continue
863
+ compact_parts.append(fragment)
864
+ char_idx = char.get("char_idx")
865
+ page_tight_bbox = tight_bboxes.get(char_idx) if isinstance(char_idx, int) else None
866
+ compact_roles.extend([roles[index]] * len(fragment))
867
+ compact_bboxes.extend([page_tight_bbox] * len(fragment))
868
+ compact_body_counts.extend([body_counts[index]] * len(fragment))
869
+ compact_formula_flags.extend([formula_flags[index]] * len(fragment))
870
+ text = "".join(compact_parts)
871
+ if not text:
872
+ return None
873
+ ranges: list[PDFTextScriptRange] = []
874
+ start = 0
875
+ while start < len(compact_roles):
876
+ role = compact_roles[start]
877
+ end = start + 1
878
+ while end < len(compact_roles) and compact_roles[end] == role:
879
+ end += 1
880
+ if role in {"sup", "sub"}:
881
+ range_bboxes = [bbox for bbox in compact_bboxes[start:end] if bbox is not None]
882
+ if range_bboxes:
883
+ page_bbox = (
884
+ min(bbox[0] for bbox in range_bboxes),
885
+ min(bbox[1] for bbox in range_bboxes),
886
+ max(bbox[2] for bbox in range_bboxes),
887
+ max(bbox[3] for bbox in range_bboxes),
888
+ )
889
+ ranges.append(
890
+ PDFTextScriptRange(
891
+ start=start,
892
+ end=end,
893
+ style="superscript" if role == "sup" else "subscript",
894
+ bbox=page_bbox,
895
+ stable_body_count=max(compact_body_counts[start:end], default=0),
896
+ formula_region=any(compact_formula_flags[start:end]),
897
+ )
898
+ )
899
+ start = end
900
+ return PDFTextScriptLine(
901
+ bbox=getattr(line, "bbox"),
902
+ text=text,
903
+ script_ranges=tuple(ranges),
904
+ source_index=int(getattr(line, "source_index", 0) or 0),
905
+ angle=angle,
906
+ )
907
+
908
+
909
+ def detect_pdf_text_script_lines(
910
+ lines: list[Any],
911
+ page_size: tuple[float, float],
912
+ tight_bboxes: dict[int, BBox],
913
+ origins: dict[int, tuple[float, float]],
914
+ *,
915
+ all_chars: list[dict[str, Any]] | None = None,
916
+ drawing_lines: Sequence[Any] | None = None,
917
+ ) -> list[PDFTextScriptLine]:
918
+ """检测 Flash 剩余自然文本行中的上下标候选。"""
919
+ resolved_chars = all_chars or []
920
+ resolved_drawings = drawing_lines or []
921
+ fraction_members_by_angle = {
922
+ angle: _fraction_member_indices(
923
+ page_size,
924
+ resolved_chars,
925
+ tight_bboxes,
926
+ resolved_drawings,
927
+ angle,
928
+ )
929
+ for angle in {int(getattr(line, "angle", 0) or 0) % 360 for line in lines}
930
+ }
931
+ return [
932
+ payload
933
+ for line in lines
934
+ if (
935
+ payload := _script_line_payload(
936
+ line,
937
+ page_size,
938
+ tight_bboxes,
939
+ origins,
940
+ fraction_members_by_angle[int(getattr(line, "angle", 0) or 0) % 360],
941
+ )
942
+ )
943
+ is not None
944
+ ]
945
+
946
+
947
+ __all__ = [
948
+ "_rotate_origin_to_upright",
949
+ "_bbox_center_inside_region",
950
+ "_script_region_memberships",
951
+ "_script_char_text",
952
+ "_is_cjk_text",
953
+ "_is_math_identifier_char",
954
+ "_is_math_script_token_char",
955
+ "_iter_math_script_tokens",
956
+ "_citation_script_indices",
957
+ "_token_origin",
958
+ "_token_tight_height",
959
+ "_has_adjacent_math_base",
960
+ "_horizontal_gap_between_bboxes",
961
+ "_token_split_position",
962
+ "_script_geometry_is_aligned",
963
+ "_nearest_nonspace_index",
964
+ "_close_spaced_script_operators",
965
+ "_close_compact_aligned_script_suffixes",
966
+ "_protected_subscript_indices",
967
+ "_refine_math_script_tokens",
968
+ "_bbox_axis_overlap",
969
+ "_fraction_member_indices",
970
+ "_strong_structural_script_roles",
971
+ "_classify_script_runs",
972
+ "_script_line_char_roles",
973
+ "_script_line_payload",
974
+ "detect_pdf_text_script_lines",
975
+ ]