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,351 @@
1
+ """将分隔符文本 CSV 转换为 DocVortex 单页表格 model-list。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import codecs
6
+ import csv as csv_module
7
+ import html
8
+ import re
9
+ from collections import Counter
10
+ from io import StringIO
11
+ from typing import Any, BinaryIO, Final, Literal, TypeAlias
12
+
13
+ from ftfy.badness import badness
14
+
15
+ from ...schema import BlockType
16
+
17
+ MAX_CSV_BYTES: Final = 200 * 1024 * 1024
18
+ MAX_CSV_ROWS: Final = 1_048_576
19
+ MAX_CSV_COLUMNS: Final = 16_384
20
+ # CSV 会把每个槽位实体化为 HTML/DOM 节点,预算需显著低于稀疏电子表格投影上限。
21
+ MAX_CSV_GRID_SLOTS: Final = 250_000
22
+ MAX_CSV_RENDERED_BYTES: Final = 256 * 1024 * 1024
23
+
24
+ _DELIMITER_CANDIDATES: Final = (",", ";", "\t", "|")
25
+ _DELIMITER_SAMPLE_RECORDS: Final = 20
26
+ _HEADER_SAMPLE_ROWS: Final = 50
27
+ _HEADER_KIND_DOMINANCE_NUM: Final = 9
28
+ _HEADER_KIND_DOMINANCE_DEN: Final = 10
29
+ _MAX_HEADER_LABEL_CHARS: Final = 64
30
+ _SEP_DIRECTIVE_RE = re.compile(r"\Asep=(?P<delimiter>[,;\t|])(?:\r\n|\n|\r|$)", re.IGNORECASE)
31
+ _DISALLOWED_CONTROL_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f\ud800-\udfff]")
32
+ _DATE_RE = re.compile(r"^\d{1,4}[-/.]\d{1,2}[-/.]\d{1,4}(?:[ T]\d{1,2}:\d{2}(?::\d{2})?(?:\.\d+)?Z?)?$")
33
+ _TIME_RE = re.compile(r"^\d{1,2}:\d{2}(?::\d{2})?(?:\.\d+)?Z?$")
34
+
35
+ CsvValueKind: TypeAlias = Literal["number", "boolean", "date", "text"]
36
+
37
+
38
+ def _text_quality_score(text: str) -> int:
39
+ """计算候选解码文本的异常字符分数,分数越低越可信。"""
40
+ control_penalty = len(_DISALLOWED_CONTROL_RE.findall(text)) * 10
41
+ return badness(text) + control_penalty
42
+
43
+
44
+ def _decode_csv_bytes(file_bytes: bytes) -> str:
45
+ """按 BOM、UTF-8、GB18030、Windows-1252 的固定顺序严格解码 CSV。"""
46
+ if file_bytes.startswith((codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE)):
47
+ raise ValueError("Unsupported CSV encoding: UTF-32")
48
+ if file_bytes.startswith(codecs.BOM_UTF8):
49
+ return file_bytes.decode("utf-8-sig", errors="strict")
50
+ if file_bytes.startswith((codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE)):
51
+ return file_bytes.decode("utf-16", errors="strict")
52
+ try:
53
+ return file_bytes.decode("utf-8", errors="strict")
54
+ except UnicodeDecodeError:
55
+ pass
56
+
57
+ candidates: dict[str, str] = {}
58
+ for encoding in ("gb18030", "cp1252"):
59
+ try:
60
+ candidates[encoding] = file_bytes.decode(encoding, errors="strict")
61
+ except UnicodeDecodeError:
62
+ continue
63
+ if not candidates:
64
+ raise ValueError("Unsupported CSV encoding; expected UTF-8, UTF-16, GB18030, or Windows-1252")
65
+ return min(
66
+ candidates.items(),
67
+ key=lambda item: (
68
+ _text_quality_score(item[1]),
69
+ 0 if item[0] == "cp1252" else 1,
70
+ ),
71
+ )[1]
72
+
73
+
74
+ def _extract_sep_directive(text: str) -> tuple[str, str | None]:
75
+ """提取 Excel 风格的 sep 指令,并从后续 CSV 数据中移除该物理行。"""
76
+ match = _SEP_DIRECTIVE_RE.match(text)
77
+ if match is None:
78
+ return text, None
79
+ return text[match.end() :], match.group("delimiter")
80
+
81
+
82
+ def _sample_record_widths(text: str, delimiter: str) -> list[int]:
83
+ """用候选分隔符读取有限个完整逻辑记录并返回每条记录的字段数。"""
84
+ reader = csv_module.reader(
85
+ StringIO(text, newline=""),
86
+ delimiter=delimiter,
87
+ quotechar='"',
88
+ doublequote=True,
89
+ skipinitialspace=False,
90
+ strict=True,
91
+ )
92
+ widths: list[int] = []
93
+ try:
94
+ for record in reader:
95
+ widths.append(max(1, len(record)))
96
+ if len(widths) >= _DELIMITER_SAMPLE_RECORDS:
97
+ break
98
+ except csv_module.Error:
99
+ return []
100
+ return widths
101
+
102
+
103
+ def _sniff_delimiter(text: str) -> str:
104
+ """按逻辑记录列宽的一致性选择分隔符,并在完全平局时优先逗号。"""
105
+ best_delimiter = ","
106
+ best_score = (0, 0, 0)
107
+ for preference, delimiter in enumerate(_DELIMITER_CANDIDATES):
108
+ widths = _sample_record_widths(text, delimiter)
109
+ if not widths:
110
+ continue
111
+ width_counts = Counter(widths)
112
+ modal_width, frequency = max(width_counts.items(), key=lambda item: (item[1], item[0]))
113
+ if modal_width < 2:
114
+ continue
115
+ score = (frequency, modal_width, -preference)
116
+ if score > best_score:
117
+ best_delimiter = delimiter
118
+ best_score = score
119
+ return best_delimiter
120
+
121
+
122
+ def _read_csv_rows(text: str, delimiter: str) -> list[list[str]]:
123
+ """严格读取全部 CSV 记录,同时执行行数、列数与网格规模限制。"""
124
+ reader = csv_module.reader(
125
+ StringIO(text, newline=""),
126
+ delimiter=delimiter,
127
+ quotechar='"',
128
+ doublequote=True,
129
+ skipinitialspace=False,
130
+ strict=True,
131
+ )
132
+ rows: list[list[str]] = []
133
+ max_columns = 0
134
+ try:
135
+ for record in reader:
136
+ row = list(record) or [""]
137
+ next_row_count = len(rows) + 1
138
+ if next_row_count > MAX_CSV_ROWS:
139
+ raise ValueError(f"CSV exceeds max_rows={MAX_CSV_ROWS}")
140
+ next_max_columns = max(max_columns, len(row))
141
+ if next_max_columns > MAX_CSV_COLUMNS:
142
+ raise ValueError(f"CSV exceeds max_columns={MAX_CSV_COLUMNS}")
143
+ if next_row_count * next_max_columns > MAX_CSV_GRID_SLOTS:
144
+ raise ValueError(f"CSV exceeds max_grid_slots={MAX_CSV_GRID_SLOTS}")
145
+ rows.append(row)
146
+ max_columns = next_max_columns
147
+ except csv_module.Error as exc:
148
+ raise ValueError(f"Malformed CSV near physical line {reader.line_num}: {exc}") from exc
149
+ return rows
150
+
151
+
152
+ def _classify_value(value: str) -> CsvValueKind | None:
153
+ """把非空字段粗分为数字、布尔、日期或文本,供表头投票使用。"""
154
+ normalized = value.strip()
155
+ if not normalized:
156
+ return None
157
+ numeric = normalized.removesuffix("%")
158
+ compact_numeric = "".join(char for char in numeric if char not in {",", " ", "_", "\u00a0"})
159
+ if any(char.isascii() and char.isdigit() for char in compact_numeric):
160
+ try:
161
+ float(compact_numeric)
162
+ except ValueError:
163
+ pass
164
+ else:
165
+ return "number"
166
+ if normalized.casefold() in {"true", "false", "yes", "no"}:
167
+ return "boolean"
168
+ if _DATE_RE.fullmatch(normalized) or _TIME_RE.fullmatch(normalized):
169
+ return "date"
170
+ return "text"
171
+
172
+
173
+ def _dominant_kind(values: list[str]) -> CsvValueKind | None:
174
+ """返回至少覆盖九成非空主体值的字段类型,没有优势类型时返回空。"""
175
+ kinds = [kind for value in values if (kind := _classify_value(value)) is not None]
176
+ if not kinds:
177
+ return None
178
+ counts = Counter(kinds)
179
+ for kind in ("number", "boolean", "date", "text"):
180
+ if counts[kind] * _HEADER_KIND_DOMINANCE_DEN >= len(kinds) * _HEADER_KIND_DOMINANCE_NUM:
181
+ return kind
182
+ return None
183
+
184
+
185
+ def _fold_header_value(value: str) -> str:
186
+ """生成忽略首尾空白和大小写的表头比较值。"""
187
+ return value.strip().casefold()
188
+
189
+
190
+ def _modal_row_width(rows: list[list[str]]) -> int:
191
+ """返回行宽众数,频次相同时选择更宽的记录。"""
192
+ if not rows:
193
+ return 0
194
+ counts = Counter(len(row) for row in rows)
195
+ return max(counts.items(), key=lambda item: (item[1], item[0]))[0]
196
+
197
+
198
+ def _infer_header_row(rows: list[list[str]]) -> bool:
199
+ """根据首行标签形态和主体列类型保守判断 CSV 是否具有一行表头。"""
200
+ if len(rows) < 2:
201
+ return False
202
+ body = rows[1 : _HEADER_SAMPLE_ROWS + 1]
203
+ if len(rows[0]) != _modal_row_width(body):
204
+ return False
205
+
206
+ header = rows[0]
207
+ seen_labels: set[str] = set()
208
+ for column, value in enumerate(header):
209
+ folded = _fold_header_value(value)
210
+ if not folded:
211
+ if column == 0:
212
+ continue
213
+ return False
214
+ if "\n" in value or "\r" in value or len(value) > _MAX_HEADER_LABEL_CHARS:
215
+ return False
216
+ if folded in seen_labels:
217
+ return False
218
+ seen_labels.add(folded)
219
+
220
+ header_votes = 0
221
+ data_votes = 0
222
+ for column, label in enumerate(header):
223
+ values = [row[column].strip() for row in body if column < len(row) and row[column].strip()]
224
+ if not values:
225
+ continue
226
+ label_kind = _classify_value(label)
227
+ dominant_kind = _dominant_kind(values)
228
+ if dominant_kind is not None and dominant_kind != "text":
229
+ if label_kind == "text" or (column == 0 and not label.strip()):
230
+ header_votes += 1
231
+ else:
232
+ data_votes += 1
233
+ continue
234
+ folded_label = _fold_header_value(label)
235
+ if folded_label and any(_fold_header_value(value) == folded_label for value in values):
236
+ data_votes += 1
237
+
238
+ if header_votes or data_votes:
239
+ return header_votes > data_votes
240
+ return True
241
+
242
+
243
+ def _normalize_row_widths(rows: list[list[str]]) -> list[list[str]]:
244
+ """在不改变已有字段内容的前提下,把短记录补齐到最大列宽。"""
245
+ if not rows:
246
+ return []
247
+ max_columns = max(len(row) for row in rows)
248
+ return [row + [""] * (max_columns - len(row)) for row in rows]
249
+
250
+
251
+ def _render_field_html(value: str) -> str:
252
+ """转义一个 CSV 字段,规范换行并替换 HTML 不允许的控制字符。"""
253
+ normalized = value.replace("\r\n", "\n").replace("\r", "\n")
254
+ normalized = _DISALLOWED_CONTROL_RE.sub("\ufffd", normalized)
255
+ return html.escape(normalized, quote=True).replace("\n", "<br>")
256
+
257
+
258
+ def _rendered_field_utf8_bytes(value: str, remaining_budget: int) -> int:
259
+ """在不创建转义字符串的前提下计算字段渲染后的 UTF-8 字节数。"""
260
+ rendered_bytes = 0
261
+ index = 0
262
+ while index < len(value):
263
+ char = value[index]
264
+ codepoint = ord(char)
265
+ if char == "\r":
266
+ if index + 1 < len(value) and value[index + 1] == "\n":
267
+ index += 1
268
+ addition = len("<br>")
269
+ elif char == "\n":
270
+ addition = len("<br>")
271
+ elif codepoint <= 0x08 or codepoint in {0x0B, 0x0C, 0x7F} or 0x0E <= codepoint <= 0x1F:
272
+ addition = 3
273
+ elif 0xD800 <= codepoint <= 0xDFFF:
274
+ addition = 3
275
+ elif char == "&":
276
+ addition = len("&amp;")
277
+ elif char in {"<", ">"}:
278
+ addition = len("&lt;")
279
+ elif char in {'"', "'"}:
280
+ addition = len("&quot;")
281
+ elif codepoint <= 0x7F:
282
+ addition = 1
283
+ elif codepoint <= 0x7FF:
284
+ addition = 2
285
+ elif codepoint <= 0xFFFF:
286
+ addition = 3
287
+ else:
288
+ addition = 4
289
+ rendered_bytes += addition
290
+ if rendered_bytes > remaining_budget:
291
+ raise ValueError(f"CSV exceeds max_rendered_bytes={MAX_CSV_RENDERED_BYTES}")
292
+ index += 1
293
+ return rendered_bytes
294
+
295
+
296
+ def _charge_rendered_bytes(used_bytes: int, additional_bytes: int) -> int:
297
+ """累计 CSV HTML 输出预算,并在写入 StringIO 前拒绝超限内容。"""
298
+ if additional_bytes < 0 or used_bytes > MAX_CSV_RENDERED_BYTES - additional_bytes:
299
+ raise ValueError(f"CSV exceeds max_rendered_bytes={MAX_CSV_RENDERED_BYTES}")
300
+ return used_bytes + additional_bytes
301
+
302
+
303
+ def _rows_to_html(rows: list[list[str]], *, has_header: bool) -> str:
304
+ """增量构造安全表格 HTML,避免为每个单元格保留独立字符串对象。"""
305
+ output = StringIO()
306
+ rendered_bytes = 0
307
+ rendered_bytes = _charge_rendered_bytes(rendered_bytes, len("<table>"))
308
+ output.write("<table>")
309
+ for row_index, row in enumerate(rows):
310
+ tag = "th" if has_header and row_index == 0 else "td"
311
+ row_prefix = "\n <tr>"
312
+ rendered_bytes = _charge_rendered_bytes(rendered_bytes, len(row_prefix))
313
+ output.write(row_prefix)
314
+ for value in row:
315
+ cell_prefix = f"\n <{tag}>"
316
+ cell_suffix = f"</{tag}>"
317
+ rendered_bytes = _charge_rendered_bytes(rendered_bytes, len(cell_prefix) + len(cell_suffix))
318
+ remaining_budget = MAX_CSV_RENDERED_BYTES - rendered_bytes
319
+ field_bytes = _rendered_field_utf8_bytes(value, remaining_budget)
320
+ rendered_bytes = _charge_rendered_bytes(rendered_bytes, field_bytes)
321
+ output.write(cell_prefix)
322
+ output.write(_render_field_html(value))
323
+ output.write(cell_suffix)
324
+ row_suffix = "\n </tr>"
325
+ rendered_bytes = _charge_rendered_bytes(rendered_bytes, len(row_suffix))
326
+ output.write(row_suffix)
327
+ table_suffix = "\n</table>"
328
+ _charge_rendered_bytes(rendered_bytes, len(table_suffix))
329
+ output.write(table_suffix)
330
+ return output.getvalue()
331
+
332
+
333
+ def convert_csv(file_binary: BinaryIO) -> list[list[dict[str, Any]]]:
334
+ """读取 CSV 二进制流并返回单逻辑页的表格 model-list。"""
335
+ file_bytes = file_binary.read(MAX_CSV_BYTES + 1)
336
+ if len(file_bytes) > MAX_CSV_BYTES:
337
+ raise ValueError(f"CSV exceeds max_bytes={MAX_CSV_BYTES}")
338
+
339
+ text = _decode_csv_bytes(file_bytes)
340
+ text, declared_delimiter = _extract_sep_directive(text)
341
+ delimiter = declared_delimiter or _sniff_delimiter(text)
342
+ rows = _read_csv_rows(text, delimiter)
343
+ if not rows:
344
+ return [[]]
345
+ has_header = _infer_header_row(rows)
346
+ normalized_rows = _normalize_row_widths(rows)
347
+ table_html = _rows_to_html(normalized_rows, has_header=has_header)
348
+ return [[{"type": BlockType.TABLE, "content": table_html}]]
349
+
350
+
351
+ __all__ = ["convert_csv"]
@@ -0,0 +1,18 @@
1
+ """Flash EPUB 原生解析实现。"""
2
+
3
+ from .converter import EpubConverter
4
+ from .errors import EpubEncryptedError, EpubError, EpubParseError, EpubResourceLimitError
5
+ from .metadata import extract_epub_metadata
6
+ from .package import EpubPackage, detect_epub, detect_epub_path
7
+
8
+ __all__ = [
9
+ "EpubConverter",
10
+ "EpubEncryptedError",
11
+ "EpubError",
12
+ "EpubPackage",
13
+ "EpubParseError",
14
+ "EpubResourceLimitError",
15
+ "detect_epub",
16
+ "detect_epub_path",
17
+ "extract_epub_metadata",
18
+ ]
@@ -0,0 +1,45 @@
1
+ """EPUB 媒体类型、命名空间与固定资源上限。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final
6
+
7
+
8
+ EPUB_MIME: Final = "application/epub+zip"
9
+ EPUB_PACKAGE_MIME: Final = "application/oebps-package+xml"
10
+ XHTML_MEDIA_TYPES: Final = frozenset({"application/xhtml+xml", "text/html"})
11
+ SVG_MEDIA_TYPE: Final = "image/svg+xml"
12
+
13
+ MAX_ENTRY_BYTES: Final = 128 * 1024 * 1024
14
+ MAX_TOTAL_BYTES: Final = 512 * 1024 * 1024
15
+ MAX_ENTRY_COUNT: Final = 100_000
16
+ MAX_XML_DEPTH: Final = 256
17
+ MAX_XML_NODES: Final = 2_000_000
18
+ MAX_ASSET_TOTAL_BYTES: Final = 128 * 1024 * 1024
19
+
20
+ IMAGE_MEDIA_BY_EXTENSION: Final[dict[str, str]] = {
21
+ "bmp": "image/bmp",
22
+ "gif": "image/gif",
23
+ "jpeg": "image/jpeg",
24
+ "jpg": "image/jpeg",
25
+ "png": "image/png",
26
+ "tif": "image/tiff",
27
+ "tiff": "image/tiff",
28
+ "webp": "image/webp",
29
+ "svg": SVG_MEDIA_TYPE,
30
+ }
31
+
32
+
33
+ __all__ = [
34
+ "EPUB_MIME",
35
+ "EPUB_PACKAGE_MIME",
36
+ "IMAGE_MEDIA_BY_EXTENSION",
37
+ "MAX_ASSET_TOTAL_BYTES",
38
+ "MAX_ENTRY_BYTES",
39
+ "MAX_ENTRY_COUNT",
40
+ "MAX_TOTAL_BYTES",
41
+ "MAX_XML_DEPTH",
42
+ "MAX_XML_NODES",
43
+ "SVG_MEDIA_TYPE",
44
+ "XHTML_MEDIA_TYPES",
45
+ ]
@@ -0,0 +1,80 @@
1
+ """EPUB OCF/OPF/spine 到 DocVortex raw model-list 的原生 converter。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, BinaryIO
6
+
7
+ from loguru import logger
8
+ from lxml import etree # type: ignore[reportMissingImports]
9
+
10
+ from .constants import SVG_MEDIA_TYPE, XHTML_MEDIA_TYPES
11
+ from .errors import EpubEncryptedError, EpubParseError, EpubResourceLimitError
12
+ from .package import EpubPackage
13
+ from .xhtml import EpubChapterConverter, build_anchor_registry, convert_svg_spine
14
+
15
+
16
+ class EpubConverter:
17
+ """把 EPUB spine 转换为稳定的逐逻辑页 raw blocks。"""
18
+
19
+ def __init__(self) -> None:
20
+ """初始化空页面结果。"""
21
+ self.pages: list[list[dict[str, Any]]] = []
22
+
23
+ def convert(self, file_binary: BinaryIO) -> None:
24
+ """读取调用方 EPUB 流,转换整本内容并保持输入流所有权。"""
25
+ package = EpubPackage(file_binary.read())
26
+ try:
27
+ parsed: list[tuple[int, str, str, etree._Element | None]] = []
28
+ readable_count = 0
29
+ for index, spine_item in enumerate(package.spine):
30
+ if spine_item.path is None or spine_item.media_type is None:
31
+ parsed.append((index, "", "", None))
32
+ logger.warning("Skipping unsupported EPUB spine item index={} idref={!r}", index, spine_item.idref)
33
+ continue
34
+ try:
35
+ if spine_item.media_type in XHTML_MEDIA_TYPES:
36
+ root = package.xhtml_part(spine_item.path, allow_external_doctype=True)
37
+ else:
38
+ root = package.xml_part(spine_item.path, allow_external_doctype=True)
39
+ except (EpubEncryptedError, EpubResourceLimitError):
40
+ raise
41
+ except EpubParseError as exc:
42
+ logger.warning("Skipping corrupt EPUB spine item index={} path={!r}: {}", index, spine_item.path, exc)
43
+ root = None
44
+ if root is not None:
45
+ readable_count += 1
46
+ else:
47
+ logger.warning("Skipping unreadable EPUB spine item index={} path={!r}", index, spine_item.path)
48
+ parsed.append((index, spine_item.path, spine_item.media_type, root))
49
+
50
+ if readable_count == 0:
51
+ raise EpubParseError("Malformed EPUB package: no selected spine content could be read")
52
+
53
+ xhtml_chapters = [
54
+ (path, root) for _, path, media_type, root in parsed if root is not None and media_type in XHTML_MEDIA_TYPES
55
+ ]
56
+ anchors = build_anchor_registry(xhtml_chapters, package)
57
+ pages: list[list[dict[str, Any]]] = []
58
+ for index, path, media_type, root in parsed:
59
+ if root is None:
60
+ pages.append([])
61
+ continue
62
+ try:
63
+ if media_type in XHTML_MEDIA_TYPES:
64
+ blocks = EpubChapterConverter(package, path, root, anchors).convert()
65
+ elif media_type == SVG_MEDIA_TYPE:
66
+ blocks = convert_svg_spine(package, path, root)
67
+ else:
68
+ blocks = []
69
+ except (EpubEncryptedError, EpubResourceLimitError):
70
+ raise
71
+ except Exception as exc:
72
+ logger.warning("Skipping unusable EPUB spine item index={} path={!r}: {}", index, path, exc)
73
+ blocks = []
74
+ pages.append(blocks)
75
+ self.pages = pages
76
+ finally:
77
+ package.close()
78
+
79
+
80
+ __all__ = ["EpubConverter"]
@@ -0,0 +1,20 @@
1
+ """EPUB 解析器内部使用的稳定错误类型。"""
2
+
3
+
4
+ class EpubError(ValueError):
5
+ """所有 EPUB 解析错误的共同基类。"""
6
+
7
+
8
+ class EpubParseError(EpubError):
9
+ """表示 EPUB 容器或正文结构不可用。"""
10
+
11
+
12
+ class EpubEncryptedError(EpubError):
13
+ """表示解析所需的 EPUB 资源已加密。"""
14
+
15
+
16
+ class EpubResourceLimitError(EpubError):
17
+ """表示 EPUB 输入超过固定资源预算。"""
18
+
19
+
20
+ __all__ = ["EpubEncryptedError", "EpubError", "EpubParseError", "EpubResourceLimitError"]
@@ -0,0 +1,31 @@
1
+ """从 EPUB OPF 提取 doclib 使用的基础元数据。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import BinaryIO
6
+
7
+ from .package import EpubPackage
8
+
9
+
10
+ def _epub_output_page_count(package: EpubPackage) -> int:
11
+ """按 OPF spine 项数量返回稳定的逻辑页数。"""
12
+ return len(package.spine)
13
+
14
+
15
+ def extract_epub_metadata(file_binary: BinaryIO) -> dict[str, object | None]:
16
+ """读取 EPUB 标题、作者、主题、关键词和 spine 逻辑页数。"""
17
+ package = EpubPackage(file_binary.read())
18
+ try:
19
+ metadata = package.metadata
20
+ return {
21
+ "page_count": _epub_output_page_count(package),
22
+ "title": metadata.title,
23
+ "author": metadata.author,
24
+ "subject": metadata.subject,
25
+ "keywords": metadata.keywords,
26
+ }
27
+ finally:
28
+ package.close()
29
+
30
+
31
+ __all__ = ["extract_epub_metadata"]