office-export 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.
- office_export/__init__.py +3 -0
- office_export/__main__.py +6 -0
- office_export/cli.py +577 -0
- office_export/core.py +863 -0
- office_export/doctor.py +154 -0
- office_export/errors.py +70 -0
- office_export/naming.py +92 -0
- office_export/office_adapters.py +599 -0
- office_export/publishing.py +103 -0
- office_export/rasterizer.py +264 -0
- office_export/results.py +85 -0
- office_export/selectors.py +97 -0
- office_export/skill.py +150 -0
- office_export/worker.py +532 -0
- office_export/worker_protocol.py +191 -0
- office_export-0.1.0.dist-info/METADATA +263 -0
- office_export-0.1.0.dist-info/RECORD +21 -0
- office_export-0.1.0.dist-info/WHEEL +4 -0
- office_export-0.1.0.dist-info/entry_points.txt +2 -0
- office_export-0.1.0.dist-info/licenses/LICENSE +21 -0
- office_export-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +21 -0
office_export/cli.py
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, TextIO
|
|
9
|
+
|
|
10
|
+
from office_export import __version__
|
|
11
|
+
from office_export.core import ExportOptions, default_output_for, export_document, format_capabilities, inspect_document
|
|
12
|
+
from office_export.doctor import run_doctor
|
|
13
|
+
from office_export.errors import EXIT_INTERNAL, OfficeExportError, UsageError
|
|
14
|
+
from office_export.naming import ensure_distinct_paths
|
|
15
|
+
from office_export.results import SCHEMA_VERSION, base_result, write_manifest
|
|
16
|
+
from office_export.skill import install_skill, remove_skill
|
|
17
|
+
|
|
18
|
+
PROGRAM_NAME = "office-export"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CliArgumentParser(argparse.ArgumentParser):
|
|
22
|
+
def error(self, message: str) -> None:
|
|
23
|
+
raise UsageError(message)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_root_help() -> str:
|
|
27
|
+
return f"""{PROGRAM_NAME} {__version__}
|
|
28
|
+
|
|
29
|
+
Export Microsoft Office documents through desktop Office, or rasterize PDFs.
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
{PROGRAM_NAME} INPUT --to pdf|png|jpeg [OPTIONS]
|
|
33
|
+
{PROGRAM_NAME} inspect INPUT [--json]
|
|
34
|
+
{PROGRAM_NAME} doctor [--json]
|
|
35
|
+
{PROGRAM_NAME} formats [--json]
|
|
36
|
+
{PROGRAM_NAME} batch PATH --to pdf|png|jpeg [OPTIONS]
|
|
37
|
+
{PROGRAM_NAME} skill install [--skills-dir DIR] [--force] [--json]
|
|
38
|
+
{PROGRAM_NAME} skill remove [--skills-dir DIR] [--force] [--json]
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
{PROGRAM_NAME} report.docx --to pdf
|
|
42
|
+
{PROGRAM_NAME} deck.pptx --to png --slides 1,3-5 --dpi 200
|
|
43
|
+
{PROGRAM_NAME} model.xlsx --to jpeg --chart "Dashboard!Revenue"
|
|
44
|
+
{PROGRAM_NAME} document.pdf --to png --pages 1-4
|
|
45
|
+
|
|
46
|
+
Run `{PROGRAM_NAME} INPUT --to FORMAT --help` for export options.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_export_parser() -> CliArgumentParser:
|
|
51
|
+
parser = CliArgumentParser(prog=PROGRAM_NAME, add_help=False)
|
|
52
|
+
parser.add_argument("source", type=Path)
|
|
53
|
+
parser.add_argument("--to", dest="output_format", choices=("pdf", "png", "jpeg"), required=True)
|
|
54
|
+
parser.add_argument("--output", type=Path)
|
|
55
|
+
parser.add_argument("--force", action="store_true")
|
|
56
|
+
parser.add_argument("--dpi", type=int)
|
|
57
|
+
parser.add_argument("--jpeg-quality", type=int)
|
|
58
|
+
parser.add_argument("--background")
|
|
59
|
+
parser.add_argument("--quality", choices=("screen", "print"), default="print")
|
|
60
|
+
parser.add_argument("--timeout", type=float, default=120.0)
|
|
61
|
+
parser.add_argument("--keep-intermediate", action="store_true")
|
|
62
|
+
parser.add_argument("--json", action="store_true")
|
|
63
|
+
parser.add_argument("--manifest", type=Path)
|
|
64
|
+
parser.add_argument("--verbose", action="store_true")
|
|
65
|
+
parser.add_argument("--image-engine", choices=("pdfium", "office"), default="pdfium")
|
|
66
|
+
parser.add_argument("--max-megapixels", type=float, default=50.0)
|
|
67
|
+
parser.add_argument("--pages")
|
|
68
|
+
parser.add_argument("--include-markup", action="store_true")
|
|
69
|
+
parser.add_argument("--bookmarks", choices=("none", "headings", "word"), default="headings")
|
|
70
|
+
parser.add_argument("--pdf-a", action="store_true")
|
|
71
|
+
parser.add_argument("--no-update-toc", dest="update_toc", action="store_false", default=True)
|
|
72
|
+
parser.add_argument("--slides")
|
|
73
|
+
parser.add_argument("--include-hidden", action="store_true")
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--output-type",
|
|
76
|
+
choices=("slides", "notes", "outline", "handout1", "handout2", "handout3", "handout4", "handout6", "handout9"),
|
|
77
|
+
default="slides",
|
|
78
|
+
)
|
|
79
|
+
parser.add_argument("--frame-slides", action="store_true")
|
|
80
|
+
parser.add_argument("--sheet", dest="sheets", action="append", default=[])
|
|
81
|
+
parser.add_argument("--range", dest="range_value")
|
|
82
|
+
parser.add_argument("--chart", dest="charts", action="append", default=[])
|
|
83
|
+
parser.add_argument("--charts", dest="charts_mode", choices=("all",))
|
|
84
|
+
parser.add_argument("--ignore-print-area", action="store_true")
|
|
85
|
+
parser.add_argument("--show-formulas", action="store_true")
|
|
86
|
+
parser.add_argument("--show-headings", action="store_true")
|
|
87
|
+
parser.add_argument("--recalculate", choices=("never", "auto", "full"), default="never")
|
|
88
|
+
parser.add_argument("--update-links", action="store_true")
|
|
89
|
+
parser.add_argument("--refresh-data", action="store_true")
|
|
90
|
+
parser.add_argument("--exclude-annotations", action="store_true")
|
|
91
|
+
return parser
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def build_export_help() -> str:
|
|
95
|
+
return f"""Usage: {PROGRAM_NAME} INPUT --to pdf|png|jpeg [OPTIONS]
|
|
96
|
+
|
|
97
|
+
Common options:
|
|
98
|
+
--output PATH Override the deterministic default output path.
|
|
99
|
+
--force Replace only planned output collisions.
|
|
100
|
+
--dpi INTEGER Image resolution from 36 through 2400. Default: 150.
|
|
101
|
+
--jpeg-quality INTEGER JPEG quality from 1 through 100. Default: 92.
|
|
102
|
+
--background COLOR JPEG transparency background. Default: white.
|
|
103
|
+
--quality screen|print Native Office PDF quality. Default: print.
|
|
104
|
+
--timeout SECONDS Office worker timeout. Default: 120.
|
|
105
|
+
--keep-intermediate Keep the Office PDF used for image output.
|
|
106
|
+
--image-engine pdfium|office Office is valid only for PowerPoint slides.
|
|
107
|
+
--max-megapixels NUMBER Per-page image safety limit. Default: 50.
|
|
108
|
+
--manifest PATH Persist the JSON conversion result.
|
|
109
|
+
--json Write the JSON conversion result to stdout.
|
|
110
|
+
--verbose Include internal exception details for unexpected failures.
|
|
111
|
+
|
|
112
|
+
Word options:
|
|
113
|
+
--pages LIST One-based pages such as 1,3-5.
|
|
114
|
+
--include-markup
|
|
115
|
+
--bookmarks none|headings|word
|
|
116
|
+
--pdf-a
|
|
117
|
+
--no-update-toc
|
|
118
|
+
|
|
119
|
+
PowerPoint options:
|
|
120
|
+
--slides LIST One-based slides such as 1,3-5.
|
|
121
|
+
--include-hidden
|
|
122
|
+
--output-type TYPE Slides, notes, outline, or a handout layout.
|
|
123
|
+
--frame-slides
|
|
124
|
+
--pdf-a
|
|
125
|
+
|
|
126
|
+
Excel options:
|
|
127
|
+
--sheet NAME_OR_INDEX Repeat to select sheets.
|
|
128
|
+
--range SHEET!A1:H40
|
|
129
|
+
--chart SHEET!NAME Repeat to select charts.
|
|
130
|
+
--charts all
|
|
131
|
+
--ignore-print-area
|
|
132
|
+
--show-formulas
|
|
133
|
+
--show-headings
|
|
134
|
+
--recalculate never|auto|full
|
|
135
|
+
--update-links
|
|
136
|
+
--refresh-data
|
|
137
|
+
|
|
138
|
+
PDF input options:
|
|
139
|
+
--pages LIST
|
|
140
|
+
--exclude-annotations
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def main(
|
|
145
|
+
argv: list[str] | None = None,
|
|
146
|
+
*,
|
|
147
|
+
stdout: TextIO | None = None,
|
|
148
|
+
stderr: TextIO | None = None,
|
|
149
|
+
) -> int:
|
|
150
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
151
|
+
stdout = stdout or sys.stdout
|
|
152
|
+
stderr = stderr or sys.stderr
|
|
153
|
+
json_mode = "--json" in args
|
|
154
|
+
verbose = "--verbose" in args
|
|
155
|
+
try:
|
|
156
|
+
if not args:
|
|
157
|
+
stdout.write(build_root_help())
|
|
158
|
+
return 0
|
|
159
|
+
if args == ["--version"]:
|
|
160
|
+
stdout.write(f"{PROGRAM_NAME} {__version__}\n")
|
|
161
|
+
return 0
|
|
162
|
+
if args == ["--about"]:
|
|
163
|
+
stdout.write(_about_text())
|
|
164
|
+
return 0
|
|
165
|
+
if args[0] in {"-h", "--help"}:
|
|
166
|
+
stdout.write(build_root_help())
|
|
167
|
+
return 0
|
|
168
|
+
if args[0] == "skill":
|
|
169
|
+
return _run_skill(args[1:], stdout=stdout)
|
|
170
|
+
if args[0] == "inspect":
|
|
171
|
+
return _run_inspect(args[1:], stdout=stdout)
|
|
172
|
+
if args[0] == "doctor":
|
|
173
|
+
return _run_doctor(args[1:], stdout=stdout)
|
|
174
|
+
if args[0] == "formats":
|
|
175
|
+
return _run_formats(args[1:], stdout=stdout)
|
|
176
|
+
if args[0] == "batch":
|
|
177
|
+
return _run_batch(args[1:], stdout=stdout)
|
|
178
|
+
if "--help" in args or "-h" in args:
|
|
179
|
+
stdout.write(build_export_help())
|
|
180
|
+
return 0
|
|
181
|
+
parsed = build_export_parser().parse_args(args)
|
|
182
|
+
options = _options_from_args(parsed)
|
|
183
|
+
result = export_document(options)
|
|
184
|
+
_write_export_result(result, json_mode=parsed.json, stdout=stdout)
|
|
185
|
+
return 0
|
|
186
|
+
except OfficeExportError as exc:
|
|
187
|
+
_write_error(exc, json_mode=json_mode, stdout=stdout, stderr=stderr)
|
|
188
|
+
return exc.context.exit_code
|
|
189
|
+
except Exception as exc:
|
|
190
|
+
message = f"Unexpected internal error ({type(exc).__name__})."
|
|
191
|
+
details = {"exception": repr(exc)} if verbose else None
|
|
192
|
+
payload: dict[str, Any] = {
|
|
193
|
+
"ok": False,
|
|
194
|
+
"schema_version": SCHEMA_VERSION,
|
|
195
|
+
"error": {"code": "internal_error", "message": message},
|
|
196
|
+
}
|
|
197
|
+
if details:
|
|
198
|
+
payload["error"]["details"] = details
|
|
199
|
+
if json_mode:
|
|
200
|
+
stdout.write(json.dumps(payload, indent=2) + "\n")
|
|
201
|
+
else:
|
|
202
|
+
stderr.write(f"error: {message}\n")
|
|
203
|
+
if verbose:
|
|
204
|
+
stderr.write(f"details: {exc!r}\n")
|
|
205
|
+
return EXIT_INTERNAL
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _run_skill(args: list[str], *, stdout: TextIO) -> int:
|
|
209
|
+
if not args or args[0] in {"-h", "--help"}:
|
|
210
|
+
stdout.write(
|
|
211
|
+
f"Usage:\n {PROGRAM_NAME} skill install [--skills-dir DIR] [--force] [--json]\n"
|
|
212
|
+
f" {PROGRAM_NAME} skill remove [--skills-dir DIR] [--force] [--json]\n"
|
|
213
|
+
)
|
|
214
|
+
return 0
|
|
215
|
+
parser = CliArgumentParser(prog=f"{PROGRAM_NAME} skill", add_help=False)
|
|
216
|
+
parser.add_argument("action", choices=("install", "remove"))
|
|
217
|
+
parser.add_argument("--skills-dir", type=Path)
|
|
218
|
+
parser.add_argument("--force", action="store_true")
|
|
219
|
+
parser.add_argument("--json", action="store_true")
|
|
220
|
+
parsed = parser.parse_args(args)
|
|
221
|
+
root = parsed.skills_dir.expanduser().resolve() if parsed.skills_dir else None
|
|
222
|
+
result = (
|
|
223
|
+
install_skill(root, force=parsed.force)
|
|
224
|
+
if parsed.action == "install"
|
|
225
|
+
else remove_skill(root, force=parsed.force)
|
|
226
|
+
)
|
|
227
|
+
payload = {"ok": True, "schema_version": SCHEMA_VERSION, "mode": f"skill_{parsed.action}", **result}
|
|
228
|
+
if parsed.json:
|
|
229
|
+
stdout.write(json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
|
|
230
|
+
else:
|
|
231
|
+
verb = "Installed" if parsed.action == "install" else "Removed"
|
|
232
|
+
if parsed.action == "remove" and not result["removed"]:
|
|
233
|
+
stdout.write(f"Skill is not installed at {result['path']}\n")
|
|
234
|
+
else:
|
|
235
|
+
stdout.write(f"{verb} {result['path']}\n")
|
|
236
|
+
return 0
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _run_inspect(args: list[str], *, stdout: TextIO) -> int:
|
|
240
|
+
if not args or args[0] in {"-h", "--help"}:
|
|
241
|
+
stdout.write(f"Usage: {PROGRAM_NAME} inspect INPUT [--timeout SECONDS] [--json]\n")
|
|
242
|
+
return 0
|
|
243
|
+
parser = CliArgumentParser(prog=f"{PROGRAM_NAME} inspect", add_help=False)
|
|
244
|
+
parser.add_argument("source", type=Path)
|
|
245
|
+
parser.add_argument("--timeout", type=float, default=120.0)
|
|
246
|
+
parser.add_argument("--json", action="store_true")
|
|
247
|
+
parsed = parser.parse_args(args)
|
|
248
|
+
if parsed.timeout <= 0:
|
|
249
|
+
raise UsageError("--timeout must be greater than zero.")
|
|
250
|
+
result = inspect_document(parsed.source, timeout=parsed.timeout)
|
|
251
|
+
if parsed.json:
|
|
252
|
+
stdout.write(json.dumps(result, indent=2, ensure_ascii=False) + "\n")
|
|
253
|
+
else:
|
|
254
|
+
stdout.write(_inspection_text(result))
|
|
255
|
+
return 0
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _run_doctor(args: list[str], *, stdout: TextIO) -> int:
|
|
259
|
+
if args and args[0] in {"-h", "--help"}:
|
|
260
|
+
stdout.write(
|
|
261
|
+
f"Usage: {PROGRAM_NAME} doctor [--timeout SECONDS] [--smoke-word FILE] "
|
|
262
|
+
"[--smoke-excel FILE] [--smoke-powerpoint FILE] [--json]\n"
|
|
263
|
+
)
|
|
264
|
+
return 0
|
|
265
|
+
parser = CliArgumentParser(prog=f"{PROGRAM_NAME} doctor", add_help=False)
|
|
266
|
+
parser.add_argument("--timeout", type=float, default=30.0)
|
|
267
|
+
parser.add_argument("--smoke-word", type=Path)
|
|
268
|
+
parser.add_argument("--smoke-excel", type=Path)
|
|
269
|
+
parser.add_argument("--smoke-powerpoint", type=Path)
|
|
270
|
+
parser.add_argument("--json", action="store_true")
|
|
271
|
+
parsed = parser.parse_args(args)
|
|
272
|
+
if parsed.timeout <= 0:
|
|
273
|
+
raise UsageError("--timeout must be greater than zero.")
|
|
274
|
+
payload = base_result(mode="doctor")
|
|
275
|
+
payload["diagnostics"] = run_doctor(timeout=parsed.timeout)
|
|
276
|
+
smoke_sources = {
|
|
277
|
+
"word": parsed.smoke_word,
|
|
278
|
+
"excel": parsed.smoke_excel,
|
|
279
|
+
"powerpoint": parsed.smoke_powerpoint,
|
|
280
|
+
}
|
|
281
|
+
if any(smoke_sources.values()):
|
|
282
|
+
payload["diagnostics"]["smoke_tests"] = _doctor_smoke(smoke_sources, timeout=parsed.timeout)
|
|
283
|
+
if parsed.json:
|
|
284
|
+
stdout.write(json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
|
|
285
|
+
else:
|
|
286
|
+
stdout.write(_doctor_text(payload["diagnostics"]))
|
|
287
|
+
return 0
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _run_formats(args: list[str], *, stdout: TextIO) -> int:
|
|
291
|
+
if args and args[0] in {"-h", "--help"}:
|
|
292
|
+
stdout.write(f"Usage: {PROGRAM_NAME} formats [--json]\n")
|
|
293
|
+
return 0
|
|
294
|
+
parser = CliArgumentParser(prog=f"{PROGRAM_NAME} formats", add_help=False)
|
|
295
|
+
parser.add_argument("--json", action="store_true")
|
|
296
|
+
parsed = parser.parse_args(args)
|
|
297
|
+
payload = base_result(mode="formats")
|
|
298
|
+
payload["formats"] = format_capabilities()
|
|
299
|
+
if parsed.json:
|
|
300
|
+
stdout.write(json.dumps(payload, indent=2) + "\n")
|
|
301
|
+
else:
|
|
302
|
+
stdout.write("Input Application Outputs\n")
|
|
303
|
+
for name, detail in payload["formats"]["inputs"].items():
|
|
304
|
+
stdout.write(f"{name:<6} {(detail['application'] or 'none'):<12} {', '.join(detail['outputs'])}\n")
|
|
305
|
+
return 0
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _run_batch(args: list[str], *, stdout: TextIO) -> int:
|
|
309
|
+
if not args or args[0] in {"-h", "--help"}:
|
|
310
|
+
stdout.write(
|
|
311
|
+
f"Usage: {PROGRAM_NAME} batch PATH --to pdf|png|jpeg "
|
|
312
|
+
"[--output-dir DIR] [--recursive] [--continue-on-error] [--json]\n"
|
|
313
|
+
)
|
|
314
|
+
return 0
|
|
315
|
+
parser = CliArgumentParser(prog=f"{PROGRAM_NAME} batch", add_help=False)
|
|
316
|
+
parser.add_argument("path", type=Path)
|
|
317
|
+
parser.add_argument("--to", dest="output_format", choices=("pdf", "png", "jpeg"), required=True)
|
|
318
|
+
parser.add_argument("--output-dir", type=Path)
|
|
319
|
+
parser.add_argument("--recursive", action="store_true")
|
|
320
|
+
parser.add_argument("--continue-on-error", action="store_true")
|
|
321
|
+
parser.add_argument("--jobs", type=int, default=1)
|
|
322
|
+
parser.add_argument("--force", action="store_true")
|
|
323
|
+
parser.add_argument("--dpi", type=int)
|
|
324
|
+
parser.add_argument("--jpeg-quality", type=int)
|
|
325
|
+
parser.add_argument("--background")
|
|
326
|
+
parser.add_argument("--quality", choices=("screen", "print"), default="print")
|
|
327
|
+
parser.add_argument("--timeout", type=float, default=120.0)
|
|
328
|
+
parser.add_argument("--keep-intermediate", action="store_true")
|
|
329
|
+
parser.add_argument("--image-engine", choices=("pdfium", "office"), default="pdfium")
|
|
330
|
+
parser.add_argument("--max-megapixels", type=float, default=50.0)
|
|
331
|
+
parser.add_argument("--manifest", type=Path)
|
|
332
|
+
parser.add_argument("--json", action="store_true")
|
|
333
|
+
parsed = parser.parse_args(args)
|
|
334
|
+
if parsed.jobs != 1:
|
|
335
|
+
raise UsageError("Version 0.1.0 supports only --jobs 1.")
|
|
336
|
+
sources = _batch_sources(parsed.path, recursive=parsed.recursive)
|
|
337
|
+
output_root = parsed.output_dir.expanduser().resolve() if parsed.output_dir else None
|
|
338
|
+
if output_root:
|
|
339
|
+
output_root.mkdir(parents=True, exist_ok=True)
|
|
340
|
+
manifest_target = parsed.manifest.expanduser().resolve() if parsed.manifest else None
|
|
341
|
+
if manifest_target is not None:
|
|
342
|
+
if manifest_target.exists() and manifest_target.is_dir():
|
|
343
|
+
raise UsageError(f"Manifest path is a directory: {manifest_target}.", code="manifest_is_directory")
|
|
344
|
+
if manifest_target.exists() and not parsed.force:
|
|
345
|
+
raise UsageError(
|
|
346
|
+
f"Manifest already exists: {manifest_target}. Use --force to replace it.",
|
|
347
|
+
code="manifest_exists",
|
|
348
|
+
)
|
|
349
|
+
for source in sources:
|
|
350
|
+
ensure_distinct_paths(source, manifest_target)
|
|
351
|
+
results: list[dict[str, Any]] = []
|
|
352
|
+
failures: list[dict[str, Any]] = []
|
|
353
|
+
for source in sources:
|
|
354
|
+
destination = None
|
|
355
|
+
if output_root:
|
|
356
|
+
destination = output_root / (
|
|
357
|
+
f"{source.stem}.pdf"
|
|
358
|
+
if parsed.output_format == "pdf"
|
|
359
|
+
else f"{source.stem} - {'PNG' if parsed.output_format == 'png' else 'JPEG'} export"
|
|
360
|
+
)
|
|
361
|
+
if manifest_target is not None:
|
|
362
|
+
ensure_distinct_paths(destination or default_output_for(source, parsed.output_format), manifest_target)
|
|
363
|
+
options = ExportOptions(
|
|
364
|
+
source=source,
|
|
365
|
+
output_format=parsed.output_format,
|
|
366
|
+
output=destination,
|
|
367
|
+
force=parsed.force,
|
|
368
|
+
dpi=parsed.dpi,
|
|
369
|
+
jpeg_quality=parsed.jpeg_quality,
|
|
370
|
+
background=parsed.background,
|
|
371
|
+
quality=parsed.quality,
|
|
372
|
+
timeout=parsed.timeout,
|
|
373
|
+
keep_intermediate=parsed.keep_intermediate,
|
|
374
|
+
image_engine=parsed.image_engine,
|
|
375
|
+
max_megapixels=parsed.max_megapixels,
|
|
376
|
+
)
|
|
377
|
+
try:
|
|
378
|
+
results.append(export_document(options))
|
|
379
|
+
except OfficeExportError as exc:
|
|
380
|
+
failure = {"source": str(source), "error": exc.context.to_dict()}
|
|
381
|
+
failures.append(failure)
|
|
382
|
+
if not parsed.continue_on_error:
|
|
383
|
+
raise
|
|
384
|
+
payload = base_result(mode="batch")
|
|
385
|
+
payload.update(
|
|
386
|
+
{
|
|
387
|
+
"ok": not failures,
|
|
388
|
+
"path": str(parsed.path.expanduser().resolve()),
|
|
389
|
+
"jobs": 1,
|
|
390
|
+
"converted": len(results),
|
|
391
|
+
"failed": len(failures),
|
|
392
|
+
"results": results,
|
|
393
|
+
"failures": failures,
|
|
394
|
+
}
|
|
395
|
+
)
|
|
396
|
+
if manifest_target is not None:
|
|
397
|
+
payload["manifest"] = str(manifest_target)
|
|
398
|
+
write_manifest(manifest_target, payload, force=parsed.force)
|
|
399
|
+
if parsed.json:
|
|
400
|
+
stdout.write(json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
|
|
401
|
+
else:
|
|
402
|
+
stdout.write(f"Converted {len(results)} file(s). Failed: {len(failures)}.\n")
|
|
403
|
+
for result in results:
|
|
404
|
+
for output in result["outputs"]:
|
|
405
|
+
stdout.write(f"{output['path']}\n")
|
|
406
|
+
return 0 if not failures else 6
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _options_from_args(args: argparse.Namespace) -> ExportOptions:
|
|
410
|
+
return ExportOptions(
|
|
411
|
+
source=args.source,
|
|
412
|
+
output_format=args.output_format,
|
|
413
|
+
output=args.output,
|
|
414
|
+
force=args.force,
|
|
415
|
+
dpi=args.dpi,
|
|
416
|
+
jpeg_quality=args.jpeg_quality,
|
|
417
|
+
background=args.background,
|
|
418
|
+
quality=args.quality,
|
|
419
|
+
timeout=args.timeout,
|
|
420
|
+
keep_intermediate=args.keep_intermediate,
|
|
421
|
+
manifest=args.manifest,
|
|
422
|
+
verbose=args.verbose,
|
|
423
|
+
image_engine=args.image_engine,
|
|
424
|
+
pages=args.pages,
|
|
425
|
+
include_markup=args.include_markup,
|
|
426
|
+
bookmarks=args.bookmarks,
|
|
427
|
+
pdf_a=args.pdf_a,
|
|
428
|
+
update_toc=args.update_toc,
|
|
429
|
+
slides=args.slides,
|
|
430
|
+
include_hidden=args.include_hidden,
|
|
431
|
+
output_type=args.output_type,
|
|
432
|
+
frame_slides=args.frame_slides,
|
|
433
|
+
sheets=args.sheets,
|
|
434
|
+
range_value=args.range_value,
|
|
435
|
+
charts=args.charts,
|
|
436
|
+
charts_all=args.charts_mode == "all",
|
|
437
|
+
ignore_print_area=args.ignore_print_area,
|
|
438
|
+
show_formulas=args.show_formulas,
|
|
439
|
+
show_headings=args.show_headings,
|
|
440
|
+
recalculate=args.recalculate,
|
|
441
|
+
update_links=args.update_links,
|
|
442
|
+
refresh_data=args.refresh_data,
|
|
443
|
+
exclude_annotations=args.exclude_annotations,
|
|
444
|
+
max_megapixels=args.max_megapixels,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _write_export_result(result: dict[str, Any], *, json_mode: bool, stdout: TextIO) -> None:
|
|
449
|
+
if json_mode:
|
|
450
|
+
stdout.write(json.dumps(result, indent=2, ensure_ascii=False) + "\n")
|
|
451
|
+
return
|
|
452
|
+
for output in result["outputs"]:
|
|
453
|
+
stdout.write(f"{output['path']}\n")
|
|
454
|
+
for warning in result.get("warnings", []):
|
|
455
|
+
stdout.write(f"warning: {warning['message']}\n")
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _write_error(exc: OfficeExportError, *, json_mode: bool, stdout: TextIO, stderr: TextIO) -> None:
|
|
459
|
+
if json_mode:
|
|
460
|
+
stdout.write(
|
|
461
|
+
json.dumps(
|
|
462
|
+
{
|
|
463
|
+
"ok": False,
|
|
464
|
+
"schema_version": SCHEMA_VERSION,
|
|
465
|
+
"tool": {"name": PROGRAM_NAME, "version": __version__},
|
|
466
|
+
"error": exc.context.to_dict(),
|
|
467
|
+
},
|
|
468
|
+
indent=2,
|
|
469
|
+
ensure_ascii=False,
|
|
470
|
+
)
|
|
471
|
+
+ "\n"
|
|
472
|
+
)
|
|
473
|
+
else:
|
|
474
|
+
stderr.write(f"error [{exc.context.code}]: {exc.context.message}\n")
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _inspection_text(payload: dict[str, Any]) -> str:
|
|
478
|
+
source = payload["source"]
|
|
479
|
+
detail = payload["inspection"]
|
|
480
|
+
lines = [f"Source: {source['path']}", f"Format: {source['format']}"]
|
|
481
|
+
if "page_count" in detail:
|
|
482
|
+
lines.append(f"Pages: {detail['page_count']}")
|
|
483
|
+
if "slide_count" in detail:
|
|
484
|
+
lines.append(f"Slides: {detail['slide_count']}")
|
|
485
|
+
if "sheets" in detail:
|
|
486
|
+
lines.append(f"Sheets: {len(detail['sheets'])}")
|
|
487
|
+
for sheet in detail["sheets"]:
|
|
488
|
+
lines.append(f" {sheet['position']}: {sheet['name']} ({sheet['kind']}, {sheet['visibility']})")
|
|
489
|
+
if detail.get("charts"):
|
|
490
|
+
lines.append(f"Charts: {len(detail['charts'])}")
|
|
491
|
+
for warning in detail.get("warnings", []):
|
|
492
|
+
lines.append(f"Warning: {warning['message']}")
|
|
493
|
+
return "\n".join(lines) + "\n"
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _doctor_text(diagnostics: dict[str, Any]) -> str:
|
|
497
|
+
lines = [
|
|
498
|
+
f"office-export {diagnostics['tool_version']}",
|
|
499
|
+
f"Platform: {diagnostics['platform']['system']} {diagnostics['platform']['release']}",
|
|
500
|
+
f"Python: {diagnostics['platform']['python']}",
|
|
501
|
+
f"PDF rasterization: {'available' if diagnostics['pdf_rasterization']['available'] else 'unavailable'}",
|
|
502
|
+
]
|
|
503
|
+
for name, item in diagnostics["office_export"]["applications"].items():
|
|
504
|
+
if item.get("available"):
|
|
505
|
+
lines.append(f"{name.capitalize()}: {item['application'].get('version') or 'available'}")
|
|
506
|
+
else:
|
|
507
|
+
lines.append(f"{name.capitalize()}: unavailable")
|
|
508
|
+
printing = diagnostics["printing"]
|
|
509
|
+
lines.append(f"Print Spooler: {printing.get('spooler') or 'not applicable'}")
|
|
510
|
+
lines.append(f"Printers: {len(printing.get('printers', []))}")
|
|
511
|
+
for warning in diagnostics["warnings"]:
|
|
512
|
+
lines.append(f"Warning: {warning['message']}")
|
|
513
|
+
for name, result in diagnostics.get("smoke_tests", {}).items():
|
|
514
|
+
lines.append(f"{name.capitalize()} smoke: {'passed' if result['ok'] else 'failed'}")
|
|
515
|
+
return "\n".join(lines) + "\n"
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _doctor_smoke(sources: dict[str, Path | None], *, timeout: float) -> dict[str, Any]:
|
|
519
|
+
results: dict[str, Any] = {}
|
|
520
|
+
with tempfile.TemporaryDirectory(prefix="office-export-smoke-") as directory:
|
|
521
|
+
root = Path(directory)
|
|
522
|
+
for application, source in sources.items():
|
|
523
|
+
if source is None:
|
|
524
|
+
continue
|
|
525
|
+
try:
|
|
526
|
+
conversion = export_document(
|
|
527
|
+
ExportOptions(
|
|
528
|
+
source=source,
|
|
529
|
+
output_format="pdf",
|
|
530
|
+
output=root / f"{application}.pdf",
|
|
531
|
+
timeout=timeout,
|
|
532
|
+
)
|
|
533
|
+
)
|
|
534
|
+
results[application] = {
|
|
535
|
+
"ok": True,
|
|
536
|
+
"source": str(source.expanduser().resolve()),
|
|
537
|
+
"size": conversion["outputs"][0]["size"],
|
|
538
|
+
"warnings": conversion["warnings"],
|
|
539
|
+
}
|
|
540
|
+
except OfficeExportError as exc:
|
|
541
|
+
results[application] = {
|
|
542
|
+
"ok": False,
|
|
543
|
+
"source": str(source.expanduser().resolve()),
|
|
544
|
+
"error": exc.context.to_dict(),
|
|
545
|
+
}
|
|
546
|
+
return results
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _batch_sources(path: Path, *, recursive: bool) -> list[Path]:
|
|
550
|
+
target = path.expanduser().resolve()
|
|
551
|
+
if target.is_file():
|
|
552
|
+
return [target]
|
|
553
|
+
if not target.exists():
|
|
554
|
+
raise UsageError(f"Batch path does not exist: {target}", code="batch_path_not_found")
|
|
555
|
+
if not target.is_dir():
|
|
556
|
+
raise UsageError(f"Batch path is not a directory or file: {target}")
|
|
557
|
+
iterator = target.rglob("*") if recursive else target.iterdir()
|
|
558
|
+
supported = {".docx", ".doc", ".xlsx", ".xls", ".pptx", ".ppt", ".pdf"}
|
|
559
|
+
sources = sorted(
|
|
560
|
+
(item for item in iterator if item.is_file() and item.suffix.lower() in supported),
|
|
561
|
+
key=lambda p: str(p).casefold(),
|
|
562
|
+
)
|
|
563
|
+
if not sources:
|
|
564
|
+
raise UsageError(f"No supported input files were found in: {target}", code="batch_empty")
|
|
565
|
+
return sources
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _about_text() -> str:
|
|
569
|
+
return f"""{PROGRAM_NAME} {__version__}
|
|
570
|
+
Export Office documents through installed desktop Microsoft Office.
|
|
571
|
+
Repository: https://github.com/pseudosavant/office-export
|
|
572
|
+
License: MIT
|
|
573
|
+
"""
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
if __name__ == "__main__":
|
|
577
|
+
raise SystemExit(main())
|