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.
- anyconvert/__init__.py +75 -0
- anyconvert/__main__.py +10 -0
- anyconvert/api.py +229 -0
- anyconvert/cli.py +134 -0
- anyconvert/common/__init__.py +55 -0
- anyconvert/common/color.py +291 -0
- anyconvert/common/geometry.py +508 -0
- anyconvert/common/logging.py +65 -0
- anyconvert/common/reader.py +673 -0
- anyconvert/emitters/__init__.py +24 -0
- anyconvert/emitters/base.py +123 -0
- anyconvert/emitters/docx/__init__.py +5 -0
- anyconvert/emitters/docx/emitter.py +832 -0
- anyconvert/emitters/docx/numbering.py +94 -0
- anyconvert/emitters/docx/styles.py +183 -0
- anyconvert/emitters/odp/__init__.py +5 -0
- anyconvert/emitters/odp/emitter.py +461 -0
- anyconvert/emitters/odt/__init__.py +5 -0
- anyconvert/emitters/odt/emitter.py +710 -0
- anyconvert/emitters/pptx/__init__.py +5 -0
- anyconvert/emitters/pptx/emitter.py +489 -0
- anyconvert/emitters/pptx/theme.py +149 -0
- anyconvert/emitters/txt/__init__.py +42 -0
- anyconvert/emitters/txt/canvas.py +246 -0
- anyconvert/emitters/txt/flow.py +194 -0
- anyconvert/exceptions.py +198 -0
- anyconvert/ir/__init__.py +41 -0
- anyconvert/ir/builder.py +505 -0
- anyconvert/ir/model.py +190 -0
- anyconvert/ir/validator.py +216 -0
- anyconvert/layout/__init__.py +101 -0
- anyconvert/layout/cluster.py +345 -0
- anyconvert/layout/flow.py +196 -0
- anyconvert/layout/headings.py +123 -0
- anyconvert/layout/lists.py +135 -0
- anyconvert/layout/paragraph.py +269 -0
- anyconvert/layout/spatial.py +300 -0
- anyconvert/layout/table.py +435 -0
- anyconvert/layout/xycut.py +363 -0
- anyconvert/packaging/__init__.py +137 -0
- anyconvert/packaging/odf.py +415 -0
- anyconvert/packaging/opc.py +686 -0
- anyconvert/pdf/__init__.py +47 -0
- anyconvert/pdf/content/__init__.py +19 -0
- anyconvert/pdf/content/interpreter.py +876 -0
- anyconvert/pdf/crypto/__init__.py +29 -0
- anyconvert/pdf/crypto/aes.py +450 -0
- anyconvert/pdf/crypto/handler.py +367 -0
- anyconvert/pdf/crypto/rc4.py +90 -0
- anyconvert/pdf/document.py +318 -0
- anyconvert/pdf/filters/__init__.py +148 -0
- anyconvert/pdf/filters/ascii.py +128 -0
- anyconvert/pdf/filters/flate.py +232 -0
- anyconvert/pdf/filters/lzw.py +109 -0
- anyconvert/pdf/filters/runlength.py +49 -0
- anyconvert/pdf/graphics/__init__.py +34 -0
- anyconvert/pdf/graphics/image.py +724 -0
- anyconvert/pdf/graphics/path.py +207 -0
- anyconvert/pdf/graphics/state.py +116 -0
- anyconvert/pdf/lexer.py +351 -0
- anyconvert/pdf/parser.py +452 -0
- anyconvert/pdf/typography/__init__.py +56 -0
- anyconvert/pdf/typography/cff.py +408 -0
- anyconvert/pdf/typography/cmap.py +371 -0
- anyconvert/pdf/typography/composite.py +372 -0
- anyconvert/pdf/typography/encodings.py +425 -0
- anyconvert/pdf/typography/font.py +142 -0
- anyconvert/pdf/typography/sfnt.py +308 -0
- anyconvert/pdf/typography/type1.py +356 -0
- anyconvert/pdf/xref.py +550 -0
- anyconvert/py.typed +1 -0
- anyconvert/utils/__init__.py +33 -0
- anyconvert/utils/png.py +369 -0
- anyconvert-0.1.0.dist-info/METADATA +254 -0
- anyconvert-0.1.0.dist-info/RECORD +78 -0
- anyconvert-0.1.0.dist-info/WHEEL +4 -0
- anyconvert-0.1.0.dist-info/entry_points.txt +3 -0
- 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
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
|
+
]
|