anyconvert 0.1.0__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 (78) hide show
  1. anyconvert/__init__.py +75 -0
  2. anyconvert/__main__.py +10 -0
  3. anyconvert/api.py +229 -0
  4. anyconvert/cli.py +134 -0
  5. anyconvert/common/__init__.py +55 -0
  6. anyconvert/common/color.py +291 -0
  7. anyconvert/common/geometry.py +508 -0
  8. anyconvert/common/logging.py +65 -0
  9. anyconvert/common/reader.py +673 -0
  10. anyconvert/emitters/__init__.py +24 -0
  11. anyconvert/emitters/base.py +123 -0
  12. anyconvert/emitters/docx/__init__.py +5 -0
  13. anyconvert/emitters/docx/emitter.py +832 -0
  14. anyconvert/emitters/docx/numbering.py +94 -0
  15. anyconvert/emitters/docx/styles.py +183 -0
  16. anyconvert/emitters/odp/__init__.py +5 -0
  17. anyconvert/emitters/odp/emitter.py +461 -0
  18. anyconvert/emitters/odt/__init__.py +5 -0
  19. anyconvert/emitters/odt/emitter.py +710 -0
  20. anyconvert/emitters/pptx/__init__.py +5 -0
  21. anyconvert/emitters/pptx/emitter.py +489 -0
  22. anyconvert/emitters/pptx/theme.py +149 -0
  23. anyconvert/emitters/txt/__init__.py +42 -0
  24. anyconvert/emitters/txt/canvas.py +246 -0
  25. anyconvert/emitters/txt/flow.py +194 -0
  26. anyconvert/exceptions.py +198 -0
  27. anyconvert/ir/__init__.py +41 -0
  28. anyconvert/ir/builder.py +505 -0
  29. anyconvert/ir/model.py +190 -0
  30. anyconvert/ir/validator.py +216 -0
  31. anyconvert/layout/__init__.py +101 -0
  32. anyconvert/layout/cluster.py +345 -0
  33. anyconvert/layout/flow.py +196 -0
  34. anyconvert/layout/headings.py +123 -0
  35. anyconvert/layout/lists.py +135 -0
  36. anyconvert/layout/paragraph.py +269 -0
  37. anyconvert/layout/spatial.py +300 -0
  38. anyconvert/layout/table.py +435 -0
  39. anyconvert/layout/xycut.py +363 -0
  40. anyconvert/packaging/__init__.py +137 -0
  41. anyconvert/packaging/odf.py +415 -0
  42. anyconvert/packaging/opc.py +686 -0
  43. anyconvert/pdf/__init__.py +47 -0
  44. anyconvert/pdf/content/__init__.py +19 -0
  45. anyconvert/pdf/content/interpreter.py +876 -0
  46. anyconvert/pdf/crypto/__init__.py +29 -0
  47. anyconvert/pdf/crypto/aes.py +450 -0
  48. anyconvert/pdf/crypto/handler.py +367 -0
  49. anyconvert/pdf/crypto/rc4.py +90 -0
  50. anyconvert/pdf/document.py +318 -0
  51. anyconvert/pdf/filters/__init__.py +148 -0
  52. anyconvert/pdf/filters/ascii.py +128 -0
  53. anyconvert/pdf/filters/flate.py +232 -0
  54. anyconvert/pdf/filters/lzw.py +109 -0
  55. anyconvert/pdf/filters/runlength.py +49 -0
  56. anyconvert/pdf/graphics/__init__.py +34 -0
  57. anyconvert/pdf/graphics/image.py +724 -0
  58. anyconvert/pdf/graphics/path.py +207 -0
  59. anyconvert/pdf/graphics/state.py +116 -0
  60. anyconvert/pdf/lexer.py +351 -0
  61. anyconvert/pdf/parser.py +452 -0
  62. anyconvert/pdf/typography/__init__.py +56 -0
  63. anyconvert/pdf/typography/cff.py +408 -0
  64. anyconvert/pdf/typography/cmap.py +371 -0
  65. anyconvert/pdf/typography/composite.py +372 -0
  66. anyconvert/pdf/typography/encodings.py +425 -0
  67. anyconvert/pdf/typography/font.py +142 -0
  68. anyconvert/pdf/typography/sfnt.py +308 -0
  69. anyconvert/pdf/typography/type1.py +356 -0
  70. anyconvert/pdf/xref.py +550 -0
  71. anyconvert/py.typed +1 -0
  72. anyconvert/utils/__init__.py +33 -0
  73. anyconvert/utils/png.py +369 -0
  74. anyconvert-0.1.0.dist-info/METADATA +254 -0
  75. anyconvert-0.1.0.dist-info/RECORD +78 -0
  76. anyconvert-0.1.0.dist-info/WHEEL +4 -0
  77. anyconvert-0.1.0.dist-info/entry_points.txt +3 -0
  78. anyconvert-0.1.0.dist-info/licenses/LICENSE +674 -0
anyconvert/__init__.py ADDED
@@ -0,0 +1,75 @@
1
+ """anyconvert: Enterprise document conversion engine in pure Python.
2
+
3
+ High-performance, zero-dependency PDF document conversion to DOCX, PPTX, ODT, ODP,
4
+ and TXT with mathematical precision and dual-mode layout reconstruction.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from anyconvert.api import (
10
+ ConversionMode,
11
+ convert,
12
+ convert_bytes,
13
+ pdf_to_document_ir,
14
+ )
15
+ from anyconvert.exceptions import (
16
+ AnyConvertError,
17
+ EmitterError,
18
+ IRBuilderError,
19
+ IRError,
20
+ IRValidationError,
21
+ LayoutError,
22
+ PackagingError,
23
+ PDFFontError,
24
+ PDFError,
25
+ PDFObjectError,
26
+ PDFPasswordRequiredError,
27
+ PDFSecurityError,
28
+ PDFStreamError,
29
+ PDFSyntaxError,
30
+ PDFUnsupportedFilterError,
31
+ SerializationError,
32
+ SpatialIndexError,
33
+ TableReconstructionError,
34
+ UnsupportedFormatError,
35
+ XYCutError,
36
+ )
37
+ from anyconvert.ir.model import DocumentIR, DocumentPage
38
+ from anyconvert.pdf.document import PDFDocument
39
+
40
+ __version__ = "0.1.0"
41
+ __author__ = "anyconvert contributors"
42
+ __license__ = "GPL-3.0-or-later"
43
+
44
+ __all__ = [
45
+ "__version__",
46
+ "__author__",
47
+ "__license__",
48
+ "convert",
49
+ "convert_bytes",
50
+ "pdf_to_document_ir",
51
+ "ConversionMode",
52
+ "DocumentIR",
53
+ "DocumentPage",
54
+ "PDFDocument",
55
+ "AnyConvertError",
56
+ "PDFError",
57
+ "PDFSyntaxError",
58
+ "PDFObjectError",
59
+ "PDFSecurityError",
60
+ "PDFPasswordRequiredError",
61
+ "PDFUnsupportedFilterError",
62
+ "PDFFontError",
63
+ "PDFStreamError",
64
+ "LayoutError",
65
+ "SpatialIndexError",
66
+ "XYCutError",
67
+ "TableReconstructionError",
68
+ "IRError",
69
+ "IRValidationError",
70
+ "IRBuilderError",
71
+ "EmitterError",
72
+ "PackagingError",
73
+ "SerializationError",
74
+ "UnsupportedFormatError",
75
+ ]
anyconvert/__main__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Package invocation entrypoint for python -m anyconvert."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from anyconvert.cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())
anyconvert/api.py ADDED
@@ -0,0 +1,229 @@
1
+ """High-level conversion API for anyconvert.
2
+
3
+ Provides zero-dependency unified conversion functions:
4
+ - convert(): Convert from file path, Path, or bytes to target format file or bytes.
5
+ - convert_bytes(): Convert from in-memory PDF bytes to target format bytes.
6
+ - pdf_to_document_ir(): Parse a PDF into a validated DocumentIR hierarchy.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import pathlib
12
+ from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Union
13
+
14
+ from anyconvert.common.reader import ByteReader
15
+ from anyconvert.emitters.base import BaseEmitter, ConversionMode
16
+ from anyconvert.emitters.docx import DocxEmitter
17
+ from anyconvert.emitters.odp import OdpEmitter
18
+ from anyconvert.emitters.odt import OdtEmitter
19
+ from anyconvert.emitters.pptx import PptxEmitter
20
+ from anyconvert.emitters.txt import TxtEmitter
21
+ from anyconvert.exceptions import (
22
+ AnyConvertError,
23
+ PDFSyntaxError,
24
+ UnsupportedFormatError,
25
+ )
26
+ from anyconvert.ir.builder import DocumentIRBuilder
27
+ from anyconvert.ir.model import DocumentIR, DocumentPage
28
+ from anyconvert.layout.cluster import cluster_characters_to_words, cluster_words_to_lines
29
+ from anyconvert.layout.flow import detect_repeating_headers_footers
30
+ from anyconvert.pdf.content.interpreter import ContentInterpreter, InterpreterOutput
31
+ from anyconvert.pdf.document import PDFDocument
32
+ from anyconvert.pdf.parser import PDFArray, PDFDict, PDFIndirectRef
33
+
34
+ SUPPORTED_FORMATS: Set[str] = {"docx", "pptx", "odt", "odp", "txt"}
35
+
36
+
37
+ def _resolve_conversion_mode(mode: Union[ConversionMode, str]) -> ConversionMode:
38
+ """Normalize and validate ConversionMode from string or enum."""
39
+ if isinstance(mode, ConversionMode):
40
+ return mode
41
+ if isinstance(mode, str):
42
+ normalized = mode.lower().strip()
43
+ if normalized == "flow":
44
+ return ConversionMode.FLOW
45
+ elif normalized == "canvas":
46
+ return ConversionMode.CANVAS
47
+ raise ValueError(
48
+ f"Invalid conversion mode: '{mode}'. Must be 'flow' or 'canvas'."
49
+ )
50
+ raise TypeError(f"Invalid mode type: {type(mode).__name__}. Must be ConversionMode or str.")
51
+
52
+
53
+ def _get_emitter(output_format: str) -> BaseEmitter:
54
+ """Instantiate and return the appropriate emitter for the requested format."""
55
+ fmt = output_format.lower().strip().lstrip(".")
56
+ if fmt == "docx":
57
+ return DocxEmitter()
58
+ elif fmt == "pptx":
59
+ return PptxEmitter()
60
+ elif fmt == "odt":
61
+ return OdtEmitter()
62
+ elif fmt == "odp":
63
+ return OdpEmitter()
64
+ elif fmt == "txt":
65
+ return TxtEmitter()
66
+ raise UnsupportedFormatError(
67
+ format_name=output_format,
68
+ supported_formats=sorted(list(SUPPORTED_FORMATS)),
69
+ )
70
+
71
+
72
+ def pdf_to_document_ir(
73
+ source: Union[str, pathlib.Path, bytes, bytearray, memoryview, ByteReader],
74
+ password: str = "",
75
+ mode: Union[ConversionMode, str] = ConversionMode.FLOW,
76
+ ) -> DocumentIR:
77
+ """Parse a PDF document source into a fully validated DocumentIR hierarchy.
78
+
79
+ Args:
80
+ source: File path, Path, raw bytes, or ByteReader.
81
+ password: Optional decryption password for encrypted PDFs.
82
+ mode: Layout conversion mode (ConversionMode.FLOW or ConversionMode.CANVAS).
83
+
84
+ Returns:
85
+ DocumentIR: Validated intermediate representation of the document.
86
+
87
+ Raises:
88
+ PDFSyntaxError: If the PDF structure is corrupted or contains no pages.
89
+ PDFPasswordRequiredError: If the document is password-protected and an incorrect or empty password was given.
90
+ """
91
+ conv_mode = _resolve_conversion_mode(mode)
92
+ reader: ByteReader
93
+ if isinstance(source, (str, pathlib.Path)):
94
+ raw_bytes = pathlib.Path(source).read_bytes()
95
+ reader = ByteReader(raw_bytes)
96
+ elif isinstance(source, ByteReader):
97
+ reader = source
98
+ elif isinstance(source, (bytes, bytearray, memoryview)):
99
+ reader = ByteReader(source)
100
+ else:
101
+ raise TypeError(f"Unsupported source type: {type(source).__name__}")
102
+
103
+ doc = PDFDocument(reader, password=password)
104
+
105
+ if doc.page_count == 0:
106
+ raise PDFSyntaxError("PDF contains no valid pages")
107
+
108
+ ir_builder = DocumentIRBuilder(resolver=doc.resolver)
109
+ pages: List[DocumentPage] = []
110
+
111
+ interpreted_pages: List[Tuple[InterpreterOutput, float, float]] = []
112
+ pages_lines: List[List[Any]] = []
113
+
114
+ for page_idx in range(doc.page_count):
115
+ page_dict = doc.get_page(page_idx)
116
+ x0, y0, x1, y1 = doc.get_page_box(page_dict)
117
+ page_w = max(1.0, abs(x1 - x0))
118
+ page_h = max(1.0, abs(y1 - y0))
119
+
120
+ # Resolve resources dictionary
121
+ raw_res = page_dict.get("Resources")
122
+ if isinstance(raw_res, PDFIndirectRef):
123
+ raw_res = doc.resolver.dereference(raw_res)
124
+ page_res = raw_res if isinstance(raw_res, PDFDict) else PDFDict()
125
+
126
+ # Decompress / decrypt content streams
127
+ content_bytes = doc.get_page_content_bytes(page_dict)
128
+
129
+ # Interpret content streams
130
+ interpreter = ContentInterpreter(resources=page_res, resolver=doc.resolver)
131
+ output = interpreter.interpret(content_bytes)
132
+ interpreted_pages.append((output, page_w, page_h))
133
+
134
+ words = cluster_characters_to_words(output.text_elements, page_height=page_h)
135
+ lines = cluster_words_to_lines(words)
136
+ pages_lines.append(lines)
137
+
138
+ known_headers: Set[str] = set()
139
+ known_footers: Set[str] = set()
140
+ if len(pages_lines) > 1:
141
+ known_headers, known_footers = detect_repeating_headers_footers(pages_lines)
142
+
143
+ for page_idx, (output, page_w, page_h) in enumerate(interpreted_pages):
144
+ built_page = ir_builder.build_page(
145
+ output=output,
146
+ page_width=page_w,
147
+ page_height=page_h,
148
+ page_number=page_idx + 1,
149
+ known_headers=known_headers,
150
+ known_footers=known_footers,
151
+ mode=conv_mode,
152
+ )
153
+ pages.append(built_page)
154
+
155
+ metadata = doc.get_metadata()
156
+ return ir_builder.build_document(pages, metadata=metadata)
157
+
158
+
159
+ def convert(
160
+ input_path: Union[str, pathlib.Path, bytes, bytearray, memoryview, ByteReader],
161
+ output_format: str = "docx",
162
+ output_path: Optional[Union[str, pathlib.Path]] = None,
163
+ mode: Union[ConversionMode, str] = ConversionMode.FLOW,
164
+ password: str = "",
165
+ ) -> bytes:
166
+ """Convert a PDF document into a target document format (DOCX, PPTX, ODT, ODP, TXT).
167
+
168
+ Args:
169
+ input_path: File path, Path object, or raw PDF bytes.
170
+ output_format: Target format ('docx', 'pptx', 'odt', 'odp', 'txt'). Defaults to 'docx'.
171
+ output_path: Optional file path to write output. If None, output is only returned as bytes.
172
+ mode: Layout mode, either ConversionMode.FLOW / 'flow' or ConversionMode.CANVAS / 'canvas'.
173
+ password: Optional password if the PDF document is encrypted.
174
+
175
+ Returns:
176
+ bytes: Converted document binary payload (ZIP archive for DOCX/PPTX/ODT/ODP, UTF-8 for TXT).
177
+
178
+ Raises:
179
+ UnsupportedFormatError: If the requested output format is not supported.
180
+ ValueError: If mode is not 'flow' or 'canvas'.
181
+ AnyConvertError: If parsing, layout, or serialization fails.
182
+ """
183
+ conv_mode = _resolve_conversion_mode(mode)
184
+ emitter = _get_emitter(output_format)
185
+
186
+ doc_ir = pdf_to_document_ir(input_path, password=password, mode=conv_mode)
187
+ result_bytes = emitter.emit(doc_ir, mode=conv_mode)
188
+
189
+ if output_path is not None:
190
+ target_path = pathlib.Path(output_path)
191
+ target_path.parent.mkdir(parents=True, exist_ok=True)
192
+ target_path.write_bytes(result_bytes)
193
+
194
+ return result_bytes
195
+
196
+
197
+ def convert_bytes(
198
+ pdf_bytes: Union[bytes, bytearray, memoryview],
199
+ output_format: str = "docx",
200
+ mode: Union[ConversionMode, str] = ConversionMode.FLOW,
201
+ password: str = "",
202
+ ) -> bytes:
203
+ """Convert raw in-memory PDF bytes into target format bytes.
204
+
205
+ Args:
206
+ pdf_bytes: Raw bytes of the PDF file.
207
+ output_format: Target format ('docx', 'pptx', 'odt', 'odp', 'txt').
208
+ mode: Layout mode ('flow' or 'canvas').
209
+ password: Optional password if the PDF document is encrypted.
210
+
211
+ Returns:
212
+ bytes: Converted document binary payload.
213
+ """
214
+ return convert(
215
+ input_path=pdf_bytes,
216
+ output_format=output_format,
217
+ output_path=None,
218
+ mode=mode,
219
+ password=password,
220
+ )
221
+
222
+
223
+ __all__ = [
224
+ "SUPPORTED_FORMATS",
225
+ "ConversionMode",
226
+ "convert",
227
+ "convert_bytes",
228
+ "pdf_to_document_ir",
229
+ ]
anyconvert/cli.py ADDED
@@ -0,0 +1,134 @@
1
+ """Command Line Interface (CLI) entrypoint for anyconvert."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import pathlib
7
+ import sys
8
+ from typing import Sequence
9
+
10
+
11
+ def create_parser() -> argparse.ArgumentParser:
12
+ """Create and configure the command-line argument parser.
13
+
14
+ Returns:
15
+ argparse.ArgumentParser: Configured argument parser instance.
16
+ """
17
+ from anyconvert import __version__
18
+
19
+ parser = argparse.ArgumentParser(
20
+ prog="anyconvert",
21
+ description=(
22
+ "anyconvert: Pure-Python enterprise-grade document conversion engine.\n"
23
+ "Converts PDF documents into DOCX, PPTX, ODT, ODP, and TXT."
24
+ ),
25
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
26
+ )
27
+
28
+ parser.add_argument(
29
+ "input",
30
+ nargs="?",
31
+ help="Path to the input PDF file to convert.",
32
+ )
33
+ parser.add_argument(
34
+ "-f",
35
+ "--format",
36
+ choices=["docx", "pptx", "odt", "odp", "txt"],
37
+ default="docx",
38
+ help="Target document format.",
39
+ )
40
+ parser.add_argument(
41
+ "-o",
42
+ "--output",
43
+ help="Path to save the converted output file. Defaults to input base name with target extension.",
44
+ )
45
+ parser.add_argument(
46
+ "-m",
47
+ "--mode",
48
+ choices=["flow", "canvas"],
49
+ default="flow",
50
+ help="Conversion layout mode: 'flow' (semantic reflowable) or 'canvas' (fixed absolute coordinates).",
51
+ )
52
+ parser.add_argument(
53
+ "-p",
54
+ "--password",
55
+ default="",
56
+ help="Decryption password if the PDF is password-protected.",
57
+ )
58
+ parser.add_argument(
59
+ "-v",
60
+ "--verbose",
61
+ action="store_true",
62
+ help="Enable verbose diagnostic output.",
63
+ )
64
+ parser.add_argument(
65
+ "--version",
66
+ action="version",
67
+ version=f"%(prog)s {__version__}",
68
+ help="Show program version and exit.",
69
+ )
70
+
71
+ return parser
72
+
73
+
74
+ def main(argv: Sequence[str] | None = None) -> int:
75
+ """CLI execution entrypoint.
76
+
77
+ Args:
78
+ argv: Optional list of command-line arguments. If None, uses sys.argv[1:].
79
+
80
+ Returns:
81
+ int: Exit status code (0 for success, non-zero for error).
82
+ """
83
+ parser = create_parser()
84
+ args = parser.parse_args(argv)
85
+
86
+ if not args.input:
87
+ parser.print_help(sys.stderr)
88
+ return 1
89
+
90
+ input_path = pathlib.Path(args.input)
91
+ if not input_path.exists():
92
+ sys.stderr.write(f"Error: Input file '{args.input}' not found.\n")
93
+ return 1
94
+
95
+ # Determine output path
96
+ if args.output:
97
+ output_path = pathlib.Path(args.output)
98
+ else:
99
+ output_path = input_path.with_suffix(f".{args.format.lower()}")
100
+
101
+ if args.verbose:
102
+ sys.stderr.write(
103
+ f"[anyconvert] Input: {input_path}, Output: {output_path}, "
104
+ f"Format: {args.format}, Mode: {args.mode}\n"
105
+ )
106
+
107
+ try:
108
+ from anyconvert.api import convert
109
+ from anyconvert.exceptions import AnyConvertError
110
+
111
+ res_bytes = convert(
112
+ input_path=input_path,
113
+ output_format=args.format,
114
+ output_path=output_path,
115
+ mode=args.mode,
116
+ password=args.password,
117
+ )
118
+
119
+ if args.verbose:
120
+ sys.stderr.write(
121
+ f"[anyconvert] Successfully converted {len(res_bytes)} bytes to '{output_path}'.\n"
122
+ )
123
+ return 0
124
+
125
+ except AnyConvertError as err:
126
+ sys.stderr.write(f"Conversion error: {err}\n")
127
+ return 1
128
+ except Exception as err:
129
+ sys.stderr.write(f"Error: {err}\n")
130
+ return 1
131
+
132
+
133
+ if __name__ == "__main__":
134
+ sys.exit(main())
@@ -0,0 +1,55 @@
1
+ """Common primitives, geometry, color models, and binary readers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from anyconvert.common.color import (
6
+ BLACK,
7
+ BLUE,
8
+ DARK_GRAY,
9
+ GRAY,
10
+ GREEN,
11
+ LIGHT_GRAY,
12
+ RED,
13
+ TRANSPARENT,
14
+ WHITE,
15
+ Color,
16
+ )
17
+ from anyconvert.common.geometry import (
18
+ BoundingBox,
19
+ Matrix3x3,
20
+ Point,
21
+ Size,
22
+ )
23
+ from anyconvert.common.logging import (
24
+ configure_logging,
25
+ get_logger,
26
+ )
27
+ from anyconvert.common.reader import (
28
+ PDF_DELIMITERS,
29
+ PDF_WHITESPACE,
30
+ BitReader,
31
+ ByteReader,
32
+ )
33
+
34
+ __all__ = [
35
+ "Point",
36
+ "Size",
37
+ "BoundingBox",
38
+ "Matrix3x3",
39
+ "Color",
40
+ "BLACK",
41
+ "WHITE",
42
+ "TRANSPARENT",
43
+ "RED",
44
+ "GREEN",
45
+ "BLUE",
46
+ "GRAY",
47
+ "LIGHT_GRAY",
48
+ "DARK_GRAY",
49
+ "get_logger",
50
+ "configure_logging",
51
+ "ByteReader",
52
+ "BitReader",
53
+ "PDF_WHITESPACE",
54
+ "PDF_DELIMITERS",
55
+ ]