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,590 @@
1
+ """从字体、绘图线及链接注解提取原生行内证据。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import statistics
7
+ from typing import Any, Sequence
8
+
9
+ from .....schema import BBox
10
+ from .....document.pdf.document import PDFLinkAnnotation
11
+ from .common import (
12
+ _bbox_intersection_area,
13
+ _canonical_styles,
14
+ _coerce_bbox,
15
+ _normalize_match_fragment,
16
+ _ordered_line_chars,
17
+ _style_line_reading_order_key,
18
+ )
19
+ from .types import (
20
+ _PDF_BOLD_FONT_NAME_RE,
21
+ _PDF_FONT_SUBSET_PREFIX_RE,
22
+ _PDF_LIST_MARKER_CHARS,
23
+ _PDF_TEXT_DECORATION_ORDER,
24
+ PDF_BOLD_MIN_COMPARABLE_CHAR_COUNT,
25
+ PDF_BOLD_MIN_WEIGHT,
26
+ PDF_FONT_FORCE_BOLD_FLAG,
27
+ PDF_LINK_CHAR_OVERLAP_THRESHOLD,
28
+ STRIKETHROUGH_CENTER_TOLERANCE_HEIGHT_RATIO,
29
+ TEXT_DECORATION_ENDPOINT_TOLERANCE_HEIGHT_RATIO,
30
+ TEXT_DECORATION_MAX_WIDTH_HEIGHT_RATIO,
31
+ TEXT_DECORATION_MIN_LENGTH_HEIGHT_RATIO,
32
+ TEXT_DECORATION_MIN_TEXT_COVERAGE_RATIO,
33
+ UNDERLINE_BOTTOM_TOLERANCE_HEIGHT_RATIO,
34
+ UNDERLINE_FRACTION_MAX_GAP_HEIGHT_RATIO,
35
+ UNDERLINE_FRACTION_MIN_LOWER_LINE_COVERAGE,
36
+ PDFTextDecoration,
37
+ PDFTextLinkLine,
38
+ PDFTextLinkRange,
39
+ PDFTextStyle,
40
+ PDFTextStyleLine,
41
+ PDFTextStyleRange,
42
+ _DrawingMatch,
43
+ _LineCandidate,
44
+ _VisibleChar,
45
+ )
46
+
47
+
48
+ def _pdf_font_metadata(char: dict[str, Any]) -> tuple[str, int, float | None]:
49
+ """读取单个字符的规范字体名、FontDescriptor flags 和有效字重。"""
50
+
51
+ font = char.get("font")
52
+ if not isinstance(font, dict):
53
+ return "", 0, None
54
+ font_name = _PDF_FONT_SUBSET_PREFIX_RE.sub(
55
+ "",
56
+ str(font.get("name") or ""),
57
+ )
58
+ try:
59
+ font_flags = int(font.get("flags") or 0)
60
+ except (TypeError, ValueError):
61
+ font_flags = 0
62
+ try:
63
+ font_weight = float(font.get("weight"))
64
+ except (TypeError, ValueError):
65
+ font_weight = math.nan
66
+ if not math.isfinite(font_weight) or font_weight <= 0:
67
+ font_weight = None
68
+ return font_name, font_flags, font_weight
69
+
70
+
71
+ def _char_font_styles(char: dict[str, Any]) -> frozenset[PDFTextStyle]:
72
+ """只依据直接字体证据返回 PDF 字符粗体样式。"""
73
+
74
+ font_name, font_flags, font_weight = _pdf_font_metadata(char)
75
+ styles: set[PDFTextStyle] = set()
76
+ if (
77
+ font_flags & PDF_FONT_FORCE_BOLD_FLAG
78
+ or (font_weight is not None and font_weight >= PDF_BOLD_MIN_WEIGHT)
79
+ or bool(_PDF_BOLD_FONT_NAME_RE.search(font_name))
80
+ ):
81
+ styles.add("bold")
82
+ return frozenset(styles)
83
+
84
+
85
+ def _has_list_marker_separator(
86
+ chars: Sequence[dict[str, Any]],
87
+ marker_source_index: int,
88
+ next_source_index: int,
89
+ median_height: float,
90
+ ) -> bool:
91
+ """判断行首项目符号与后续正文之间是否存在空白或明显视觉间隔。"""
92
+
93
+ if any(str(chars[index].get("char") or "").isspace() for index in range(marker_source_index + 1, next_source_index)):
94
+ return True
95
+ marker_bbox = _coerce_bbox(chars[marker_source_index].get("bbox"))
96
+ next_bbox = _coerce_bbox(chars[next_source_index].get("bbox"))
97
+ return bool(marker_bbox is not None and next_bbox is not None and next_bbox[0] - marker_bbox[2] >= 0.5 * median_height)
98
+
99
+
100
+ def _filter_pdf_bold_runs(
101
+ chars: Sequence[dict[str, Any]],
102
+ font_styles: Sequence[frozenset[PDFTextStyle]],
103
+ median_height: float,
104
+ ) -> list[frozenset[PDFTextStyle]]:
105
+ """过滤过短粗体 run 和与正文分离的行首项目符号粗体。"""
106
+
107
+ output = list(font_styles)
108
+ comparable_chars = [
109
+ (source_index, fragment)
110
+ for source_index, char in enumerate(chars)
111
+ if (fragment := _normalize_match_fragment(char.get("char")))
112
+ ]
113
+ run_start = 0
114
+ while run_start < len(comparable_chars):
115
+ source_index, _fragment = comparable_chars[run_start]
116
+ if "bold" not in output[source_index]:
117
+ run_start += 1
118
+ continue
119
+ run_end = run_start + 1
120
+ while run_end < len(comparable_chars):
121
+ next_source_index, _next_fragment = comparable_chars[run_end]
122
+ if "bold" not in output[next_source_index]:
123
+ break
124
+ run_end += 1
125
+
126
+ run = comparable_chars[run_start:run_end]
127
+ run_text = "".join(fragment for _index, fragment in run)
128
+ is_short = len(run_text) < PDF_BOLD_MIN_COMPARABLE_CHAR_COUNT
129
+ is_isolated_leading_marker = (
130
+ run_start == 0
131
+ and run_end < len(comparable_chars)
132
+ and bool(run_text)
133
+ and all(char in _PDF_LIST_MARKER_CHARS for char in run_text)
134
+ and _has_list_marker_separator(
135
+ chars,
136
+ run[-1][0],
137
+ comparable_chars[run_end][0],
138
+ median_height,
139
+ )
140
+ )
141
+ if is_short or is_isolated_leading_marker:
142
+ for run_source_index, _run_fragment in run:
143
+ output[run_source_index] = frozenset(style for style in output[run_source_index] if style != "bold")
144
+ run_start = run_end
145
+ return output
146
+
147
+
148
+ def _build_line_candidate(line: Any) -> _LineCandidate | None:
149
+ """从视觉水平 line 构造字符几何候选,旋转文字和退化行返回空。"""
150
+
151
+ if int(getattr(line, "angle", 0) or 0) % 360 != 0:
152
+ return None
153
+ line_bbox = _coerce_bbox(getattr(line, "bbox", None))
154
+ if line_bbox is None:
155
+ return None
156
+ chars = _ordered_line_chars(line)
157
+ visible_chars: list[_VisibleChar] = []
158
+ for char_index, char in enumerate(chars):
159
+ text = str(char.get("char") or "")
160
+ bbox = _coerce_bbox(char.get("bbox"))
161
+ if bbox is None or not text.isprintable() or text.isspace():
162
+ continue
163
+ visible_chars.append(_VisibleChar(source_index=char_index, bbox=bbox))
164
+ if not visible_chars:
165
+ return None
166
+
167
+ heights = [char.bbox[3] - char.bbox[1] for char in visible_chars]
168
+ median_height = statistics.median(heights)
169
+ if median_height <= 0:
170
+ return None
171
+ body_chars = [char for char, height in zip(visible_chars, heights) if height >= 0.8 * median_height]
172
+ if not body_chars:
173
+ return None
174
+ font_styles = [_char_font_styles(char) for char in chars]
175
+ return _LineCandidate(
176
+ bbox=line_bbox,
177
+ chars=chars,
178
+ visible_chars=visible_chars,
179
+ median_height=median_height,
180
+ center_y=statistics.median((char.bbox[1] + char.bbox[3]) / 2 for char in body_chars),
181
+ bottom_y=statistics.median(char.bbox[3] for char in body_chars),
182
+ source_index=int(getattr(line, "source_index", 0) or 0),
183
+ font_styles=_filter_pdf_bold_runs(
184
+ chars,
185
+ font_styles,
186
+ median_height,
187
+ ),
188
+ decoration_ranges={
189
+ "underline": [],
190
+ "strikethrough": [],
191
+ },
192
+ )
193
+
194
+
195
+ def _drawing_match_for_line(
196
+ line: _LineCandidate,
197
+ drawing: Any,
198
+ style: PDFTextDecoration,
199
+ ) -> _DrawingMatch | None:
200
+ """按目标纵向锚点和公共几何规则校验单条文本装饰线。"""
201
+
202
+ if getattr(drawing, "orientation", None) != "horizontal":
203
+ return None
204
+ drawing_bbox = _coerce_bbox(getattr(drawing, "bbox", None))
205
+ if drawing_bbox is None:
206
+ return None
207
+ drawing_length = drawing_bbox[2] - drawing_bbox[0]
208
+ if drawing_length < TEXT_DECORATION_MIN_LENGTH_HEIGHT_RATIO * line.median_height:
209
+ return None
210
+ drawing_center_y = (drawing_bbox[1] + drawing_bbox[3]) / 2
211
+ target_y = line.bottom_y if style == "underline" else line.center_y
212
+ target_tolerance = (
213
+ UNDERLINE_BOTTOM_TOLERANCE_HEIGHT_RATIO if style == "underline" else STRIKETHROUGH_CENTER_TOLERANCE_HEIGHT_RATIO
214
+ )
215
+ target_distance_ratio = abs(drawing_center_y - target_y) / line.median_height
216
+ if target_distance_ratio > target_tolerance:
217
+ return None
218
+ try:
219
+ drawing_width = max(0.0, float(getattr(drawing, "width", 0.0) or 0.0))
220
+ except (TypeError, ValueError):
221
+ return None
222
+ if drawing_width > TEXT_DECORATION_MAX_WIDTH_HEIGHT_RATIO * line.median_height:
223
+ return None
224
+
225
+ hit_chars = [char for char in line.visible_chars if drawing_bbox[0] <= (char.bbox[0] + char.bbox[2]) / 2 <= drawing_bbox[2]]
226
+ if not hit_chars:
227
+ return None
228
+ hit_left = min(char.bbox[0] for char in hit_chars)
229
+ hit_right = max(char.bbox[2] for char in hit_chars)
230
+ if (hit_right - hit_left) / drawing_length < TEXT_DECORATION_MIN_TEXT_COVERAGE_RATIO:
231
+ return None
232
+ endpoint_distance = min(
233
+ abs(drawing_bbox[0] - hit_left),
234
+ abs(drawing_bbox[2] - hit_right),
235
+ )
236
+ if endpoint_distance > TEXT_DECORATION_ENDPOINT_TOLERANCE_HEIGHT_RATIO * line.median_height:
237
+ return None
238
+
239
+ overlap = max(
240
+ 0.0,
241
+ min(line.bbox[2], drawing_bbox[2]) - max(line.bbox[0], drawing_bbox[0]),
242
+ )
243
+ horizontal_overlap_ratio = overlap / max(
244
+ 0.01,
245
+ min(line.bbox[2] - line.bbox[0], drawing_length),
246
+ )
247
+ return _DrawingMatch(
248
+ style=style,
249
+ start_index=min(char.source_index for char in hit_chars),
250
+ end_index=max(char.source_index for char in hit_chars) + 1,
251
+ target_distance_ratio=target_distance_ratio,
252
+ horizontal_overlap_ratio=horizontal_overlap_ratio,
253
+ )
254
+
255
+
256
+ def _merge_source_ranges(ranges: list[tuple[int, int]]) -> list[tuple[int, int]]:
257
+ """合并重叠或相邻的来源字符区间。"""
258
+
259
+ merged: list[tuple[int, int]] = []
260
+ for start, end in sorted(ranges):
261
+ if start >= end:
262
+ continue
263
+ if merged and start <= merged[-1][1]:
264
+ merged[-1] = (merged[-1][0], max(merged[-1][1], end))
265
+ else:
266
+ merged.append((start, end))
267
+ return merged
268
+
269
+
270
+ def _line_style_payload(line: _LineCandidate) -> PDFTextStyleLine | None:
271
+ """把来源字符的字体与装饰线证据转换为紧凑文本样式区间。"""
272
+
273
+ decoration_styles: list[set[PDFTextStyle]] = [set() for _char in line.chars]
274
+ for style in _PDF_TEXT_DECORATION_ORDER:
275
+ for start, end in _merge_source_ranges(line.decoration_ranges[style]):
276
+ for char_index in range(max(0, start), min(end, len(line.chars))):
277
+ decoration_styles[char_index].add(style)
278
+ compact_parts: list[str] = []
279
+ compact_styles: list[tuple[PDFTextStyle, ...]] = []
280
+ for char_index, char in enumerate(line.chars):
281
+ fragment = _normalize_match_fragment(char.get("char"))
282
+ if not fragment:
283
+ continue
284
+ compact_parts.append(fragment)
285
+ styles = set(line.font_styles[char_index])
286
+ styles.update(decoration_styles[char_index])
287
+ canonical_styles = _canonical_styles(styles)
288
+ compact_styles.extend([canonical_styles] * len(fragment))
289
+
290
+ text = "".join(compact_parts)
291
+ if not text:
292
+ return None
293
+ compact_ranges: list[PDFTextStyleRange] = []
294
+ active_start = 0
295
+ active_styles: tuple[PDFTextStyle, ...] = ()
296
+ for offset, styles in enumerate([*compact_styles, ()]):
297
+ if styles == active_styles:
298
+ continue
299
+ if active_styles:
300
+ compact_ranges.append(PDFTextStyleRange(active_start, offset, active_styles))
301
+ active_start = offset
302
+ active_styles = styles
303
+ return PDFTextStyleLine(
304
+ bbox=line.bbox,
305
+ text=text,
306
+ style_ranges=tuple(compact_ranges),
307
+ source_index=line.source_index,
308
+ )
309
+
310
+
311
+ def _build_line_geometry_grids(
312
+ candidates: Sequence[_LineCandidate],
313
+ ) -> tuple[
314
+ float,
315
+ dict[int, list[tuple[int, PDFTextDecoration]]],
316
+ dict[int, list[int]],
317
+ ]:
318
+ """按装饰线锚点和行顶坐标建立网格,限制每条 drawing 的局部比较范围。"""
319
+
320
+ grid_size = max(
321
+ 1.0,
322
+ statistics.median(line.median_height for line in candidates),
323
+ )
324
+ anchor_grid: dict[int, list[tuple[int, PDFTextDecoration]]] = {}
325
+ top_grid: dict[int, list[int]] = {}
326
+ for line_index, line in enumerate(candidates):
327
+ top_grid.setdefault(math.floor(line.bbox[1] / grid_size), []).append(line_index)
328
+ for style, target_y, tolerance_ratio in (
329
+ (
330
+ "underline",
331
+ line.bottom_y,
332
+ UNDERLINE_BOTTOM_TOLERANCE_HEIGHT_RATIO,
333
+ ),
334
+ (
335
+ "strikethrough",
336
+ line.center_y,
337
+ STRIKETHROUGH_CENTER_TOLERANCE_HEIGHT_RATIO,
338
+ ),
339
+ ):
340
+ tolerance = tolerance_ratio * line.median_height
341
+ start_cell = math.floor((target_y - tolerance) / grid_size)
342
+ end_cell = math.floor((target_y + tolerance) / grid_size)
343
+ for cell in range(start_cell, end_cell + 1):
344
+ anchor_grid.setdefault(cell, []).append((line_index, style))
345
+ return grid_size, anchor_grid, top_grid
346
+
347
+
348
+ def _is_fraction_bar_candidate(
349
+ candidates: Sequence[_LineCandidate],
350
+ top_grid: dict[int, list[int]],
351
+ grid_size: float,
352
+ line_index: int,
353
+ drawing_bbox: BBox,
354
+ ) -> bool:
355
+ """用紧邻且被横线覆盖的下方文本 run 排除公式分数线。"""
356
+
357
+ line = candidates[line_index]
358
+ drawing_center_y = (drawing_bbox[1] + drawing_bbox[3]) / 2
359
+ max_lower_top = drawing_center_y + UNDERLINE_FRACTION_MAX_GAP_HEIGHT_RATIO * line.median_height
360
+ lower_indices: set[int] = set()
361
+ for cell in range(
362
+ math.floor(drawing_center_y / grid_size),
363
+ math.floor(max_lower_top / grid_size) + 1,
364
+ ):
365
+ lower_indices.update(top_grid.get(cell, ()))
366
+ for lower_index in lower_indices:
367
+ if lower_index == line_index:
368
+ continue
369
+ lower_line = candidates[lower_index]
370
+ if not drawing_center_y <= lower_line.bbox[1] <= max_lower_top:
371
+ continue
372
+ lower_width = lower_line.bbox[2] - lower_line.bbox[0]
373
+ horizontal_overlap = max(
374
+ 0.0,
375
+ min(drawing_bbox[2], lower_line.bbox[2]) - max(drawing_bbox[0], lower_line.bbox[0]),
376
+ )
377
+ if horizontal_overlap / max(0.01, lower_width) >= UNDERLINE_FRACTION_MIN_LOWER_LINE_COVERAGE:
378
+ return True
379
+ return False
380
+
381
+
382
+ def detect_pdf_text_style_lines(
383
+ lines: Sequence[Any],
384
+ drawing_lines: Sequence[Any],
385
+ ) -> list[PDFTextStyleLine]:
386
+ """从视觉文本 run 与页面 drawing 中生成全部水平行样式证据。"""
387
+
388
+ candidates = [candidate for line in lines if (candidate := _build_line_candidate(line)) is not None]
389
+ if not candidates:
390
+ return []
391
+ horizontal_drawings = [drawing for drawing in drawing_lines if getattr(drawing, "orientation", None) == "horizontal"]
392
+ if horizontal_drawings:
393
+ grid_size, anchor_grid, top_grid = _build_line_geometry_grids(candidates)
394
+ for drawing in horizontal_drawings:
395
+ drawing_bbox = _coerce_bbox(getattr(drawing, "bbox", None))
396
+ if drawing_bbox is None:
397
+ continue
398
+ drawing_center_y = (drawing_bbox[1] + drawing_bbox[3]) / 2
399
+ candidate_anchors = anchor_grid.get(
400
+ math.floor(drawing_center_y / grid_size),
401
+ [],
402
+ )
403
+ matches: list[tuple[int, _DrawingMatch]] = []
404
+ for line_index, style in candidate_anchors:
405
+ match = _drawing_match_for_line(
406
+ candidates[line_index],
407
+ drawing,
408
+ style,
409
+ )
410
+ if match is None:
411
+ continue
412
+ if style == "underline" and _is_fraction_bar_candidate(
413
+ candidates,
414
+ top_grid,
415
+ grid_size,
416
+ line_index,
417
+ drawing_bbox,
418
+ ):
419
+ continue
420
+ matches.append((line_index, match))
421
+ if not matches:
422
+ continue
423
+ line_index, best_match = min(
424
+ matches,
425
+ key=lambda item: (
426
+ item[1].target_distance_ratio,
427
+ -item[1].horizontal_overlap_ratio,
428
+ candidates[item[0]].source_index,
429
+ _PDF_TEXT_DECORATION_ORDER.index(item[1].style),
430
+ ),
431
+ )
432
+ candidates[line_index].decoration_ranges[best_match.style].append((best_match.start_index, best_match.end_index))
433
+
434
+ payloads = [payload for line in candidates if (payload := _line_style_payload(line)) is not None]
435
+ if not any(line.style_ranges for line in payloads):
436
+ return []
437
+ return sorted(
438
+ payloads,
439
+ key=_style_line_reading_order_key,
440
+ )
441
+
442
+
443
+ def _link_region_hits_char(region: BBox, char_bbox: BBox) -> bool:
444
+ """按字符中心或字符面积覆盖率判断 Link 区域是否命中字符。"""
445
+
446
+ center_x = (char_bbox[0] + char_bbox[2]) / 2
447
+ center_y = (char_bbox[1] + char_bbox[3]) / 2
448
+ if region[0] <= center_x <= region[2] and region[1] <= center_y <= region[3]:
449
+ return True
450
+ char_area = max(
451
+ 0.01,
452
+ (char_bbox[2] - char_bbox[0]) * (char_bbox[3] - char_bbox[1]),
453
+ )
454
+ return _bbox_intersection_area(region, char_bbox) / char_area >= PDF_LINK_CHAR_OVERLAP_THRESHOLD
455
+
456
+
457
+ def _link_targets_for_char(
458
+ char_bbox: BBox,
459
+ annotations: Sequence[PDFLinkAnnotation],
460
+ ) -> set[str]:
461
+ """返回命中字符的全部不同链接目标,供冲突检测使用。"""
462
+
463
+ return {
464
+ annotation.target
465
+ for annotation in annotations
466
+ if any(_link_region_hits_char(region, char_bbox) for region in annotation.bboxes)
467
+ }
468
+
469
+
470
+ def _compact_link_ranges(
471
+ compact_targets: Sequence[str | None],
472
+ ) -> tuple[PDFTextLinkRange, ...]:
473
+ """把逐字符链接目标压缩为同目标连续区间。"""
474
+
475
+ ranges: list[PDFTextLinkRange] = []
476
+ active_start = 0
477
+ active_target: str | None = None
478
+ for offset, target in enumerate([*compact_targets, None]):
479
+ if target == active_target:
480
+ continue
481
+ if active_target is not None:
482
+ ranges.append(
483
+ PDFTextLinkRange(
484
+ start=active_start,
485
+ end=offset,
486
+ target=active_target,
487
+ )
488
+ )
489
+ active_start = offset
490
+ active_target = target
491
+ return tuple(ranges)
492
+
493
+
494
+ def _build_link_line_payload(
495
+ line: Any,
496
+ annotations: Sequence[PDFLinkAnnotation],
497
+ fallback_source_index: int,
498
+ ) -> PDFTextLinkLine | None:
499
+ """把一个视觉文本 run 与 Link 区域相交结果转换为紧凑链接证据。"""
500
+
501
+ try:
502
+ angle = int(getattr(line, "angle", 0) or 0) % 360
503
+ except (TypeError, ValueError):
504
+ return None
505
+ if angle not in {0, 90, 180, 270}:
506
+ return None
507
+ line_bbox = _coerce_bbox(getattr(line, "bbox", None))
508
+ if line_bbox is None:
509
+ return None
510
+ nearby_annotations = [
511
+ annotation
512
+ for annotation in annotations
513
+ if any(_bbox_intersection_area(line_bbox, region) > 0 for region in annotation.bboxes)
514
+ ]
515
+ if not nearby_annotations:
516
+ return None
517
+
518
+ compact_parts: list[str] = []
519
+ compact_targets: list[str | None] = []
520
+ for char in _ordered_line_chars(line):
521
+ fragment = _normalize_match_fragment(char.get("char"))
522
+ if not fragment:
523
+ continue
524
+ char_bbox = _coerce_bbox(char.get("bbox"))
525
+ targets = _link_targets_for_char(char_bbox, nearby_annotations) if char_bbox is not None else set()
526
+ # 同一字符落入不同目标时不猜测 PDF 点击层级,保留为普通文本。
527
+ target = next(iter(targets)) if len(targets) == 1 else None
528
+ compact_parts.append(fragment)
529
+ compact_targets.extend([target] * len(fragment))
530
+
531
+ text = "".join(compact_parts)
532
+ link_ranges = _compact_link_ranges(compact_targets)
533
+ if not text or not link_ranges:
534
+ return None
535
+ try:
536
+ source_index = int(getattr(line, "source_index", fallback_source_index))
537
+ except (TypeError, ValueError):
538
+ source_index = fallback_source_index
539
+ return PDFTextLinkLine(
540
+ bbox=line_bbox,
541
+ text=text,
542
+ link_ranges=link_ranges,
543
+ source_index=source_index,
544
+ )
545
+
546
+
547
+ def detect_pdf_text_link_lines(
548
+ lines: Sequence[Any],
549
+ annotations: Sequence[PDFLinkAnnotation],
550
+ ) -> list[PDFTextLinkLine]:
551
+ """从视觉文本 run 与 PDF Link 注解生成字符级超链接证据。"""
552
+
553
+ if not annotations:
554
+ return []
555
+ payloads = [
556
+ payload
557
+ for line_index, line in enumerate(lines)
558
+ if (
559
+ payload := _build_link_line_payload(
560
+ line,
561
+ annotations,
562
+ line_index,
563
+ )
564
+ )
565
+ is not None
566
+ ]
567
+ return sorted(
568
+ payloads,
569
+ key=lambda line: (line.source_index, line.bbox[1], line.bbox[0]),
570
+ )
571
+
572
+
573
+ __all__ = [
574
+ "_pdf_font_metadata",
575
+ "_char_font_styles",
576
+ "_has_list_marker_separator",
577
+ "_filter_pdf_bold_runs",
578
+ "_build_line_candidate",
579
+ "_drawing_match_for_line",
580
+ "_merge_source_ranges",
581
+ "_line_style_payload",
582
+ "_build_line_geometry_grids",
583
+ "_is_fraction_bar_candidate",
584
+ "detect_pdf_text_style_lines",
585
+ "_link_region_hits_char",
586
+ "_link_targets_for_char",
587
+ "_compact_link_ranges",
588
+ "_build_link_line_payload",
589
+ "detect_pdf_text_link_lines",
590
+ ]