dsh-office-kit 0.1.0 → 0.3.0
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.
- package/CAPABILITY.md +147 -68
- package/LICENSE.md +21 -21
- package/README.md +108 -106
- package/assets/skills/excel-live-control/SKILL.md +234 -0
- package/assets/skills/excel-live-control/container_tools/live_excel.py +1589 -0
- package/assets/skills/excel-live-control/domain_guidance/financial_models.md +86 -0
- package/assets/skills/excel-live-control/domain_guidance/healthcare.md +58 -0
- package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +53 -0
- package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +53 -0
- package/assets/skills/excel-live-control/references/office-automation.md +207 -0
- package/assets/skills/excel-live-control/references/read_only_qna.md +59 -0
- package/assets/skills/excel-live-control/style_guidelines.md +87 -0
- package/assets/skills/excel-live-control/workflows/create_workflows.md +78 -0
- package/assets/skills/excel-live-control/workflows/edit_workflows.md +72 -0
- package/assets/skills/office-documents/SKILL.md +90 -77
- package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +86 -0
- package/assets/skills/office-documents/ooxml/comments.md +40 -0
- package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +66 -0
- package/assets/skills/office-documents/ooxml/rels_and_content_types.md +62 -0
- package/assets/skills/office-documents/ooxml/tracked_changes.md +53 -0
- package/assets/skills/office-documents/references/docx-structure.md +83 -0
- package/assets/skills/office-documents/references/template-elicitation.md +49 -0
- package/assets/skills/office-documents/references/writing-quality.md +49 -0
- package/assets/skills/office-documents/render_docx.py +92 -0
- package/assets/skills/office-documents/scripts/a11y_audit.py +194 -0
- package/assets/skills/office-documents/scripts/accept_tracked_changes.py +151 -0
- package/assets/skills/office-documents/scripts/add_tracked_replacements.py +148 -0
- package/assets/skills/office-documents/scripts/append_blocks.py +108 -0
- package/assets/skills/office-documents/scripts/apply_template_styles.py +104 -0
- package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +102 -0
- package/assets/skills/office-documents/scripts/comments_add.py +136 -0
- package/assets/skills/office-documents/scripts/comments_apply_patch.py +114 -0
- package/assets/skills/office-documents/scripts/comments_extract.py +85 -0
- package/assets/skills/office-documents/scripts/comments_strip.py +74 -0
- package/assets/skills/office-documents/scripts/content_controls.py +192 -0
- package/assets/skills/office-documents/scripts/core_properties.py +124 -0
- package/assets/skills/office-documents/scripts/docx_kit.py +587 -0
- package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +225 -0
- package/assets/skills/office-documents/scripts/docx_table_to_csv.py +99 -0
- package/assets/skills/office-documents/scripts/fields_materialize.py +66 -0
- package/assets/skills/office-documents/scripts/fields_report.py +59 -0
- package/assets/skills/office-documents/scripts/flatten_ref_fields.py +62 -0
- package/assets/skills/office-documents/scripts/footnotes_report.py +77 -0
- package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +96 -0
- package/assets/skills/office-documents/scripts/heading_audit.py +81 -0
- package/assets/skills/office-documents/scripts/images_audit.py +80 -0
- package/assets/skills/office-documents/scripts/insert_note.py +126 -0
- package/assets/skills/office-documents/scripts/insert_ref_fields.py +90 -0
- package/assets/skills/office-documents/scripts/insert_toc.py +84 -0
- package/assets/skills/office-documents/scripts/internal_nav.py +146 -0
- package/assets/skills/office-documents/scripts/make_fixtures.py +169 -0
- package/assets/skills/office-documents/scripts/merge_docx_append.py +239 -0
- package/assets/skills/office-documents/scripts/privacy_scrub.py +131 -0
- package/assets/skills/office-documents/scripts/redact_docx.py +113 -0
- package/assets/skills/office-documents/scripts/render_and_diff.py +101 -0
- package/assets/skills/office-documents/scripts/replace_text.py +154 -0
- package/assets/skills/office-documents/scripts/section_audit.py +91 -0
- package/assets/skills/office-documents/scripts/set_protection.py +92 -0
- package/assets/skills/office-documents/scripts/style_lint.py +111 -0
- package/assets/skills/office-documents/scripts/style_normalize.py +103 -0
- package/assets/skills/office-documents/scripts/table_geometry.py +167 -0
- package/assets/skills/office-documents/scripts/watermark_add.py +48 -0
- package/assets/skills/office-documents/scripts/watermark_audit_remove.py +67 -0
- package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +117 -0
- package/assets/skills/office-documents/tasks/accessibility_a11y.md +45 -0
- package/assets/skills/office-documents/tasks/captions_crossrefs.md +49 -0
- package/assets/skills/office-documents/tasks/clean_tracked_changes.md +49 -0
- package/assets/skills/office-documents/tasks/comments_manage.md +59 -0
- package/assets/skills/office-documents/tasks/compare_diff.md +40 -0
- package/assets/skills/office-documents/tasks/create_edit.md +107 -0
- package/assets/skills/office-documents/tasks/equations.md +59 -0
- package/assets/skills/office-documents/tasks/fields_update.md +44 -0
- package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +50 -0
- package/assets/skills/office-documents/tasks/footnotes_endnotes.md +38 -0
- package/assets/skills/office-documents/tasks/forms_content_controls.md +48 -0
- package/assets/skills/office-documents/tasks/headings_numbering.md +55 -0
- package/assets/skills/office-documents/tasks/images_figures.md +63 -0
- package/assets/skills/office-documents/tasks/multi_doc_merge.md +43 -0
- package/assets/skills/office-documents/tasks/navigation_internal_links.md +38 -0
- package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +45 -0
- package/assets/skills/office-documents/tasks/protection_restrict_editing.md +41 -0
- package/assets/skills/office-documents/tasks/read_review.md +50 -0
- package/assets/skills/office-documents/tasks/redaction_anonymization.md +42 -0
- package/assets/skills/office-documents/tasks/sections_layout.md +54 -0
- package/assets/skills/office-documents/tasks/style_lint_normalize.md +44 -0
- package/assets/skills/office-documents/tasks/tables_spreadsheets.md +64 -0
- package/assets/skills/office-documents/tasks/templates_style_packs.md +53 -0
- package/assets/skills/office-documents/tasks/toc_workflow.md +39 -0
- package/assets/skills/office-documents/tasks/verify_render.md +50 -0
- package/assets/skills/office-documents/tasks/watermarks_background.md +35 -0
- package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +47 -0
- package/assets/skills/office-documents/troubleshooting/run_splitting.md +36 -0
- package/assets/skills/office-pdf/SKILL.md +73 -56
- package/assets/skills/office-pdf/references/form-fields.md +56 -0
- package/assets/skills/office-pdf/references/geometry.md +44 -0
- package/assets/skills/office-pdf/references/layout-quality.md +41 -0
- package/assets/skills/office-pdf/references/limits.md +58 -0
- package/assets/skills/office-pdf/scripts/extract_text.py +172 -0
- package/assets/skills/office-pdf/scripts/forms.py +556 -0
- package/assets/skills/office-pdf/scripts/metadata.py +161 -0
- package/assets/skills/office-pdf/scripts/page_ops.py +198 -0
- package/assets/skills/office-pdf/scripts/redact_region.py +347 -0
- package/assets/skills/office-pdf/scripts/render_pages.py +207 -0
- package/assets/skills/office-pdf/scripts/split_pages.py +155 -0
- package/assets/skills/office-pdf/scripts/stamp_overlay.py +199 -0
- package/assets/skills/office-pdf/tasks/annotations.md +98 -0
- package/assets/skills/office-pdf/tasks/create.md +75 -0
- package/assets/skills/office-pdf/tasks/edit-pages.md +71 -0
- package/assets/skills/office-pdf/tasks/forms.md +67 -0
- package/assets/skills/office-pdf/tasks/read.md +62 -0
- package/assets/skills/office-pdf/tasks/redact.md +72 -0
- package/assets/skills/office-pdf/tasks/stamp.md +60 -0
- package/assets/skills/office-presentations/SKILL.md +141 -87
- package/assets/skills/office-presentations/container_tools/add_slides.py +136 -0
- package/assets/skills/office-presentations/container_tools/core_properties.py +120 -0
- package/assets/skills/office-presentations/container_tools/create_montage.py +126 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +285 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +157 -0
- package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +144 -0
- package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +199 -0
- package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +212 -0
- package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +345 -0
- package/assets/skills/office-presentations/container_tools/preview_render.py +375 -0
- package/assets/skills/office-presentations/container_tools/render_slides.py +139 -0
- package/assets/skills/office-presentations/container_tools/replace_text.py +168 -0
- package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +229 -0
- package/assets/skills/office-presentations/domain_guidance/finance.md +66 -0
- package/assets/skills/office-presentations/recipes/README.md +71 -0
- package/assets/skills/office-presentations/recipes/autolayout.md +88 -0
- package/assets/skills/office-presentations/recipes/connectors.md +83 -0
- package/assets/skills/office-presentations/recipes/images.md +80 -0
- package/assets/skills/office-presentations/recipes/master-and-layouts.md +127 -0
- package/assets/skills/office-presentations/recipes/native-charts.md +87 -0
- package/assets/skills/office-presentations/recipes/rich-text-runs.md +86 -0
- package/assets/skills/office-presentations/recipes/speaker-notes.md +65 -0
- package/assets/skills/office-presentations/recipes/tables.md +88 -0
- package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +71 -0
- package/assets/skills/office-presentations/recipes/theme-tokens.md +87 -0
- package/assets/skills/office-presentations/references/authoring-workflow.md +65 -0
- package/assets/skills/office-presentations/references/finalization.md +63 -0
- package/assets/skills/office-presentations/references/native-bullets.md +78 -0
- package/assets/skills/office-presentations/references/native-evidence.md +67 -0
- package/assets/skills/office-presentations/references/style-guidelines.md +66 -0
- package/assets/skills/office-presentations/references/templates.md +76 -0
- package/assets/skills/office-presentations/routing/google_slides.md +45 -0
- package/assets/skills/office-presentations/tasks/apply-annotations.md +83 -0
- package/assets/skills/office-presentations/tasks/charts-tables-notes.md +79 -0
- package/assets/skills/office-presentations/tasks/create-deck.md +106 -0
- package/assets/skills/office-presentations/tasks/edit-deck.md +93 -0
- package/assets/skills/office-presentations/tasks/inspect-and-verify.md +107 -0
- package/assets/skills/office-presentations/tasks/template-following.md +132 -0
- package/assets/skills/office-presentations/template_following_scripts/check_template_fidelity.py +176 -0
- package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +154 -0
- package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +111 -0
- package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +239 -0
- package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +185 -0
- package/assets/skills/office-spreadsheets/SKILL.md +105 -101
- package/assets/skills/office-spreadsheets/container_tools/add_chart.py +236 -0
- package/assets/skills/office-spreadsheets/container_tools/add_image.py +107 -0
- package/assets/skills/office-spreadsheets/container_tools/append_rows.py +122 -0
- package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +306 -0
- package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +398 -0
- package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +275 -0
- package/assets/skills/office-spreadsheets/container_tools/set_cells.py +135 -0
- package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +134 -0
- package/assets/skills/office-spreadsheets/container_tools/style_cells.py +133 -0
- package/assets/skills/office-spreadsheets/recipes/charts.md +66 -0
- package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +69 -0
- package/assets/skills/office-spreadsheets/recipes/data-tables.md +58 -0
- package/assets/skills/office-spreadsheets/recipes/named-ranges.md +41 -0
- package/assets/skills/office-spreadsheets/recipes/number-formats.md +72 -0
- package/assets/skills/office-spreadsheets/recipes/sparklines.md +43 -0
- package/assets/skills/office-spreadsheets/references/charts.md +63 -0
- package/assets/skills/office-spreadsheets/references/domain/financial-models.md +40 -0
- package/assets/skills/office-spreadsheets/references/domain/healthcare.md +22 -0
- package/assets/skills/office-spreadsheets/references/domain/marketing.md +28 -0
- package/assets/skills/office-spreadsheets/references/domain/research.md +22 -0
- package/assets/skills/office-spreadsheets/references/formula-patterns.md +78 -0
- package/assets/skills/office-spreadsheets/references/style-guidelines.md +69 -0
- package/assets/skills/office-spreadsheets/references/workbook-structure.md +60 -0
- package/assets/skills/office-spreadsheets/routing/google-sheets.md +36 -0
- package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +37 -0
- package/assets/skills/office-spreadsheets/tasks/annotations.md +51 -0
- package/assets/skills/office-spreadsheets/tasks/create-workbook.md +66 -0
- package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +38 -0
- package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +69 -0
- package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +38 -0
- package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +57 -0
- package/assets/skills/shared/annotation-recipes.md +252 -0
- package/assets/skills/shared/check_runtime.py +253 -0
- package/assets/skills/shared/common-workflow.md +139 -0
- package/assets/skills/shared/scripts/ensure_raster_image.py +187 -0
- package/dist/client.js +4 -4
- package/dist/client.js.map +1 -1
- package/dist/index.js +239 -89
- package/package.json +146 -149
- package/postinstall.mjs +92 -82
- package/python/office_cli.py +171 -246
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate the Office runtime that the format skills promise.
|
|
3
|
+
|
|
4
|
+
Imports every distribution listed in python/requirements.txt, then asks the
|
|
5
|
+
helper CLI for its own `status` report (packages and renderers) and fails with
|
|
6
|
+
a readable message when anything the skills depend on is missing.
|
|
7
|
+
|
|
8
|
+
Exit codes:
|
|
9
|
+
0 all required packages import and the helper CLI answered
|
|
10
|
+
1 one or more required packages is missing or fails to import
|
|
11
|
+
2 helper CLI missing, crashed, or did not return a status report
|
|
12
|
+
3 bad path or argument
|
|
13
|
+
4 --require-renderer was given and no document renderer was found
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import importlib
|
|
20
|
+
import importlib.metadata
|
|
21
|
+
import json
|
|
22
|
+
import re
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
DEFAULT_ROOT = Path(__file__).resolve().parents[3]
|
|
28
|
+
RENDERER_HINT = (
|
|
29
|
+
"docx/pptx/xlsx rendering needs LibreOffice, Microsoft Office, or WPS Office; "
|
|
30
|
+
"pdf rendering works without any of them (PyMuPDF)"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def canonical(name: str) -> str:
|
|
35
|
+
"""Normalise a distribution name the way packaging does."""
|
|
36
|
+
return re.sub(r"[-_.]+", "-", name).lower()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def parse_requirements(path: Path) -> list[str]:
|
|
40
|
+
"""Return distribution names from a requirements file, one per line."""
|
|
41
|
+
names: list[str] = []
|
|
42
|
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
43
|
+
line = raw.split("#", 1)[0].strip()
|
|
44
|
+
if not line or line.startswith("-"):
|
|
45
|
+
continue
|
|
46
|
+
line = line.split(";", 1)[0].strip()
|
|
47
|
+
name = re.split(r"[<>=!~\[( ]", line, maxsplit=1)[0].strip()
|
|
48
|
+
if name and canonical(name) not in {canonical(n) for n in names}:
|
|
49
|
+
names.append(name)
|
|
50
|
+
return names
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def module_map() -> dict[str, list[str]]:
|
|
54
|
+
"""Map canonical distribution name -> its top-level import modules."""
|
|
55
|
+
mapping: dict[str, list[str]] = {}
|
|
56
|
+
for module, dists in importlib.metadata.packages_distributions().items():
|
|
57
|
+
for dist in dists:
|
|
58
|
+
mapping.setdefault(canonical(dist), []).append(module)
|
|
59
|
+
return mapping
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def check_packages(requirements: list[str]) -> list[dict]:
|
|
63
|
+
"""Import each distribution's modules and report per-package state."""
|
|
64
|
+
mapping = module_map()
|
|
65
|
+
results = []
|
|
66
|
+
for name in requirements:
|
|
67
|
+
key = canonical(name)
|
|
68
|
+
modules = sorted(mapping.get(key, []))
|
|
69
|
+
if "pymupdf" in modules:
|
|
70
|
+
# PyMuPDF ships a legacy `fitz` alias that prints a deprecation warning.
|
|
71
|
+
modules = [m for m in modules if m != "fitz"]
|
|
72
|
+
entry: dict = {"distribution": name, "modules": modules}
|
|
73
|
+
try:
|
|
74
|
+
entry["version"] = importlib.metadata.version(name)
|
|
75
|
+
except importlib.metadata.PackageNotFoundError:
|
|
76
|
+
entry.update(ok=False, error="not installed for this interpreter")
|
|
77
|
+
results.append(entry)
|
|
78
|
+
continue
|
|
79
|
+
failures = []
|
|
80
|
+
for module in modules:
|
|
81
|
+
try:
|
|
82
|
+
importlib.import_module(module)
|
|
83
|
+
except Exception as exc: # noqa: BLE001 - report any import failure verbatim
|
|
84
|
+
failures.append(f"{module}: {type(exc).__name__}: {exc}")
|
|
85
|
+
if failures:
|
|
86
|
+
entry.update(ok=False, error="; ".join(failures))
|
|
87
|
+
else:
|
|
88
|
+
entry.update(ok=True)
|
|
89
|
+
results.append(entry)
|
|
90
|
+
return results
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def cli_status(python: Path, cli: Path, timeout: float) -> dict:
|
|
94
|
+
"""Run `<python> <cli> status` and return the parsed JSON report."""
|
|
95
|
+
proc = subprocess.run(
|
|
96
|
+
[str(python), str(cli), "status"],
|
|
97
|
+
capture_output=True,
|
|
98
|
+
text=True,
|
|
99
|
+
encoding="utf-8",
|
|
100
|
+
errors="replace",
|
|
101
|
+
timeout=timeout,
|
|
102
|
+
cwd=str(cli.parent),
|
|
103
|
+
)
|
|
104
|
+
if proc.returncode != 0:
|
|
105
|
+
raise RuntimeError(
|
|
106
|
+
f"status exited {proc.returncode}: "
|
|
107
|
+
f"{(proc.stderr or proc.stdout).strip()[:800]}"
|
|
108
|
+
)
|
|
109
|
+
try:
|
|
110
|
+
return json.loads(proc.stdout)
|
|
111
|
+
except json.JSONDecodeError as exc:
|
|
112
|
+
raise RuntimeError(f"status did not print JSON: {exc}") from exc
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def renderer_summary(report: dict) -> dict:
|
|
116
|
+
"""Reduce the CLI renderer block to a simple availability answer."""
|
|
117
|
+
renderers = report.get("renderers") or {}
|
|
118
|
+
office = renderers.get("microsoft_office") or {}
|
|
119
|
+
wps = renderers.get("kingsoft_wps") or {}
|
|
120
|
+
available = {
|
|
121
|
+
"libreoffice": renderers.get("libreoffice") or None,
|
|
122
|
+
"microsoft_office": {k: office.get(k) or None for k in ("word", "powerpoint", "excel")},
|
|
123
|
+
"kingsoft_wps": {k: wps.get(k) or None for k in ("writer", "presentation", "spreadsheet")},
|
|
124
|
+
}
|
|
125
|
+
available["any"] = (
|
|
126
|
+
bool(available["libreoffice"])
|
|
127
|
+
or any(available["microsoft_office"].values())
|
|
128
|
+
or any(available["kingsoft_wps"].values())
|
|
129
|
+
)
|
|
130
|
+
return available
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main(argv: list[str] | None = None) -> int:
|
|
134
|
+
parser = argparse.ArgumentParser(
|
|
135
|
+
description="Validate the Office runtime the skills promise (packages + renderer status).",
|
|
136
|
+
)
|
|
137
|
+
parser.add_argument("--python", default=sys.executable, help="interpreter to validate (default: this one)")
|
|
138
|
+
parser.add_argument(
|
|
139
|
+
"--requirements",
|
|
140
|
+
default=str(DEFAULT_ROOT / "python" / "requirements.txt"),
|
|
141
|
+
help="requirements file (default: <plugin>/python/requirements.txt)",
|
|
142
|
+
)
|
|
143
|
+
parser.add_argument(
|
|
144
|
+
"--cli",
|
|
145
|
+
default=str(DEFAULT_ROOT / "python" / "office_cli.py"),
|
|
146
|
+
help="helper CLI path (default: <plugin>/python/office_cli.py)",
|
|
147
|
+
)
|
|
148
|
+
parser.add_argument("--require-renderer", action="store_true", help="exit 4 when no document renderer is installed")
|
|
149
|
+
parser.add_argument("--timeout", type=float, default=180.0, help="seconds allowed for the status call")
|
|
150
|
+
parser.add_argument("--json", action="store_true", help="print one JSON object instead of prose")
|
|
151
|
+
args = parser.parse_args(argv)
|
|
152
|
+
|
|
153
|
+
python = Path(args.python)
|
|
154
|
+
requirements_path = Path(args.requirements)
|
|
155
|
+
cli = Path(args.cli)
|
|
156
|
+
for label, path in (("requirements", requirements_path), ("cli", cli)):
|
|
157
|
+
if not path.is_file():
|
|
158
|
+
print(f"error: {label} file not found: {path}", file=sys.stderr)
|
|
159
|
+
return 3
|
|
160
|
+
if not python.exists():
|
|
161
|
+
print(f"error: interpreter not found: {python}", file=sys.stderr)
|
|
162
|
+
return 3
|
|
163
|
+
|
|
164
|
+
requirements = parse_requirements(requirements_path)
|
|
165
|
+
if not requirements:
|
|
166
|
+
print(f"error: no packages listed in {requirements_path}", file=sys.stderr)
|
|
167
|
+
return 3
|
|
168
|
+
|
|
169
|
+
packages = check_packages(requirements)
|
|
170
|
+
missing = [p["distribution"] for p in packages if not p["ok"]]
|
|
171
|
+
|
|
172
|
+
report: dict = {
|
|
173
|
+
"ok": False,
|
|
174
|
+
"python": {"executable": str(python), "version": sys.version.split()[0] if python == Path(sys.executable) else None},
|
|
175
|
+
"requirements": {"path": str(requirements_path), "distributions": requirements},
|
|
176
|
+
"cli": {"path": str(cli)},
|
|
177
|
+
"packages": packages,
|
|
178
|
+
"missing": missing,
|
|
179
|
+
"warnings": [],
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if missing:
|
|
183
|
+
report["error"] = f"{len(missing)} of {len(requirements)} required package(s) missing: {', '.join(missing)}"
|
|
184
|
+
report["remedy"] = f'"{python}" -m pip install -r "{requirements_path}"'
|
|
185
|
+
emit(args, report)
|
|
186
|
+
return 1
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
status = cli_status(python, cli, args.timeout)
|
|
190
|
+
except (OSError, subprocess.SubprocessError, RuntimeError) as exc:
|
|
191
|
+
report["error"] = f"helper CLI status failed: {exc}"
|
|
192
|
+
emit(args, report)
|
|
193
|
+
return 2
|
|
194
|
+
|
|
195
|
+
if status.get("command") != "status":
|
|
196
|
+
report["error"] = "helper CLI did not identify itself as the status command"
|
|
197
|
+
emit(args, report)
|
|
198
|
+
return 2
|
|
199
|
+
|
|
200
|
+
cli_python = (status.get("python") or {}).get("executable")
|
|
201
|
+
report["python"]["version"] = (status.get("python") or {}).get("version") or report["python"]["version"]
|
|
202
|
+
if cli_python and Path(cli_python).resolve() != python.resolve():
|
|
203
|
+
report["warnings"].append(f"status reported a different interpreter: {cli_python}")
|
|
204
|
+
|
|
205
|
+
cli_missing = [p.get("package") for p in status.get("packages") or [] if not p.get("installed")]
|
|
206
|
+
if cli_missing:
|
|
207
|
+
report["error"] = f"status disagrees with direct imports; still missing: {', '.join(str(p) for p in cli_missing)}"
|
|
208
|
+
emit(args, report)
|
|
209
|
+
return 2
|
|
210
|
+
|
|
211
|
+
renderers = renderer_summary(status)
|
|
212
|
+
report["renderers"] = renderers
|
|
213
|
+
if not renderers["any"]:
|
|
214
|
+
report["warnings"].append(f"no document renderer found: {RENDERER_HINT}")
|
|
215
|
+
if args.require_renderer:
|
|
216
|
+
report["error"] = "no document renderer found (--require-renderer)"
|
|
217
|
+
emit(args, report)
|
|
218
|
+
return 4
|
|
219
|
+
|
|
220
|
+
report["ok"] = True
|
|
221
|
+
emit(args, report)
|
|
222
|
+
return 0
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def emit(args: argparse.Namespace, report: dict) -> None:
|
|
226
|
+
"""Print the machine report or a short human summary."""
|
|
227
|
+
if args.json:
|
|
228
|
+
print(json.dumps(report, indent=2, ensure_ascii=False))
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
print(f"interpreter {report['python']['executable']} (python {report['python']['version'] or '?'})")
|
|
232
|
+
print(f"requirements {report['requirements']['path']} ({len(report['requirements']['distributions'])} distributions)")
|
|
233
|
+
for item in report["packages"]:
|
|
234
|
+
mark = "ok " if item["ok"] else "FAIL"
|
|
235
|
+
detail = item.get("version") or item.get("error", "")
|
|
236
|
+
modules = f" ({', '.join(item['modules'])})" if item["modules"] else ""
|
|
237
|
+
print(f" [{mark}] {item['distribution']:<16} {detail}{modules}")
|
|
238
|
+
if "renderers" in report:
|
|
239
|
+
r = report["renderers"]
|
|
240
|
+
print(f"renderer libreoffice={r['libreoffice'] or 'none'}; "
|
|
241
|
+
f"office={r['microsoft_office']}; wps={r['kingsoft_wps']}")
|
|
242
|
+
for warning in report["warnings"]:
|
|
243
|
+
print(f"warning {warning}")
|
|
244
|
+
if report.get("error"):
|
|
245
|
+
print(f"FAILED {report['error']}", file=sys.stderr)
|
|
246
|
+
if report.get("remedy"):
|
|
247
|
+
print(f"remedy {report['remedy']}", file=sys.stderr)
|
|
248
|
+
return
|
|
249
|
+
print(f"OK {len(report['packages'])}/{len(report['packages'])} packages import; helper CLI status matches")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
if __name__ == "__main__":
|
|
253
|
+
sys.exit(main())
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Shared workflow
|
|
2
|
+
|
|
3
|
+
Every format pack follows this loop. The helper CLI is an accelerator for common
|
|
4
|
+
operations, not the boundary of what is possible: it shortens the common path,
|
|
5
|
+
and the skill teaches the rest. The format router adds the spec keys and the
|
|
6
|
+
commands; the order, the checks and the failure handling are the same.
|
|
7
|
+
|
|
8
|
+
## 0. Read the runtime block first
|
|
9
|
+
|
|
10
|
+
Call the `office_status` tool, then read the `## Office runtime` section that is
|
|
11
|
+
appended to the end of the active `SKILL.md`. It is a fenced JSON block:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{ "python": "...\\python.exe", "cli": "...\\python\\office_cli.py", "skill": "...\\assets\\skills\\office-documents", "shared": "...\\assets\\skills\\shared", "version": "3.13.12" }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- `python` and `cli` are verified absolute paths. Use them verbatim and quote
|
|
18
|
+
them in commands. Never call a bare `python`, `py`, `soffice` or `pdftoppm`.
|
|
19
|
+
- `skill` is the directory of the format pack; its scripts live under
|
|
20
|
+
`<skill>/scripts/`.
|
|
21
|
+
- `shared` is this directory (workflow and annotation recipes).
|
|
22
|
+
- If the block is missing entirely and the skill body instead says *No working
|
|
23
|
+
Python runtime is available*, stop: do not run any helper command and do not
|
|
24
|
+
fabricate a document.
|
|
25
|
+
|
|
26
|
+
`office_status` returns the same facts as JSON with `runtime.state`, the package
|
|
27
|
+
list and `renderers`. Use it when a command fails and you need to know why.
|
|
28
|
+
|
|
29
|
+
## 1. Decide: create or edit
|
|
30
|
+
|
|
31
|
+
- No existing file → create. Write the output into the user's workspace, not a
|
|
32
|
+
temp directory, unless the user names a location.
|
|
33
|
+
- Existing file to change → edit. **Never write back over the original.** The
|
|
34
|
+
output is `<original>-edited.<ext>` next to the original (or the name the user
|
|
35
|
+
asked for): `report.docx` → `report-edited.docx`. State both paths in the
|
|
36
|
+
final summary.
|
|
37
|
+
- "Improve this document" means read it first (`read` verb) and edit the real
|
|
38
|
+
content; do not rebuild it from scratch and drop its formatting.
|
|
39
|
+
|
|
40
|
+
## 2. Understand the file, choose the route, then change it
|
|
41
|
+
|
|
42
|
+
Read or inspect the file before changing it (`read` verb first), then pick the
|
|
43
|
+
cheapest route that actually does the job:
|
|
44
|
+
|
|
45
|
+
1. **CLI verb** — the common case. The helper CLI is spec-driven and prints
|
|
46
|
+
exactly one JSON object on stdout. Failures print
|
|
47
|
+
`{ "ok": false, "error": "..." }` on stderr and exit non-zero.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
"<python>" "<cli>" docx|pptx|xlsx|pdf create --spec "<spec.json>" --out "<file>"
|
|
51
|
+
"<python>" "<cli>" docx|pptx|xlsx|pdf read --input "<file>"
|
|
52
|
+
"<python>" "<cli>" docx|pptx|xlsx|pdf edit --input "<file>" --spec "<spec.json>" --out "<file-edited>"
|
|
53
|
+
"<python>" "<cli>" docx|pptx|xlsx|pdf render --input "<file>" --out-dir "<dir>"
|
|
54
|
+
"<python>" "<cli>" xlsx recalc --input "<file>" --out "<file>"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Write the spec as a JSON file in the workspace; quote absolute paths.
|
|
58
|
+
`xlsx read` accepts `--data-only`; every command accepts `--json-out <path>`
|
|
59
|
+
when you want the report on disk instead of stdout.
|
|
60
|
+
|
|
61
|
+
2. **Pack script** — a specialised job the CLI has no verb for. Use the script
|
|
62
|
+
listed in the format router (`<skill>/scripts/`, or `container_tools/` for
|
|
63
|
+
pptx/xlsx). Scripts are argparse-based, support `--help`, and take absolute
|
|
64
|
+
paths; run them with the same `"<python>"`.
|
|
65
|
+
|
|
66
|
+
3. **Library snippet** — the long tail. When neither the CLI nor a pack script
|
|
67
|
+
covers the change, write a short runnable script in the workspace using the
|
|
68
|
+
installed libraries (`python-docx`, `openpyxl`, `python-pptx`, `pypdf`,
|
|
69
|
+
`PyMuPDF`, `reportlab`, `pdfplumber`), run it with `"<python>"`, then verify
|
|
70
|
+
the file through the same render step. Keep it small and keep the user's
|
|
71
|
+
original untouched.
|
|
72
|
+
|
|
73
|
+
Never answer "the CLI does not support this, so it cannot be done". A missing
|
|
74
|
+
verb is a missing shortcut, not a missing capability: give the library-level
|
|
75
|
+
route, run it, and verify the result like any other change.
|
|
76
|
+
|
|
77
|
+
`check_runtime.py` in this directory validates the interpreter: it imports every
|
|
78
|
+
package from `python/requirements.txt` and runs `<cli> status`. A non-zero exit
|
|
79
|
+
means the environment is broken, not the spec.
|
|
80
|
+
|
|
81
|
+
## 3. Render and look at the result
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
"<python>" "<cli>" docx render --input "<file>" --out-dir "<dir>"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Output: `<dir>/page-1.png`, `page-2.png`, ... (rasterised at `--dpi`, default
|
|
88
|
+
110). For docx/pptx/xlsx the intermediate PDF is kept at `<dir>/<stem>.pdf` —
|
|
89
|
+
use it for annotation geometry. `pdf render` rasterises with PyMuPDF and needs
|
|
90
|
+
no external renderer.
|
|
91
|
+
|
|
92
|
+
Open each page with `read_image`. Look for: text overflowing or clipped at
|
|
93
|
+
margins, overlapping shapes, unexplained blank pages, tables spilling past the
|
|
94
|
+
page, fonts replaced by boxes, missing chart/table content, and numbers that do
|
|
95
|
+
not match what the user asked for.
|
|
96
|
+
|
|
97
|
+
Only after the pages look right do you present the file.
|
|
98
|
+
|
|
99
|
+
## 4. Fix, re-render, present
|
|
100
|
+
|
|
101
|
+
Edit the spec (or the edit spec), re-run the same command, re-render, look
|
|
102
|
+
again. Two or three passes is normal; if a problem survives three passes, say
|
|
103
|
+
what still looks wrong instead of pretending it is fixed.
|
|
104
|
+
|
|
105
|
+
Then call `present` with the final file and reply with one sentence about what
|
|
106
|
+
changed. For an edit, mention the original path and the edited path.
|
|
107
|
+
|
|
108
|
+
## Rasterizing art
|
|
109
|
+
|
|
110
|
+
Decks, workbooks and documents embed raster images only, so an SVG or a chosen
|
|
111
|
+
PDF page has to become a PNG first; the same call downscales an oversized raster.
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
"<python>" "<shared>/scripts/ensure_raster_image.py" --input "<art.svg>" --dpi 300 --out "<art.png>"
|
|
115
|
+
"<python>" "<shared>/scripts/ensure_raster_image.py" --input "<deck.pdf>" --page 2 --width 1600 --out "<slide-2.png>"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
It prints one JSON object with the PNG's `width`/`height` and never overwrites
|
|
119
|
+
the input. Open that PNG with `read_image` and look at it before embedding it:
|
|
120
|
+
a blank, clipped or miscoloured raster is invisible inside the host file.
|
|
121
|
+
|
|
122
|
+
## Failure ladder
|
|
123
|
+
|
|
124
|
+
| Symptom | Meaning | What you do and say |
|
|
125
|
+
| --- | --- | --- |
|
|
126
|
+
| No `## Office runtime` JSON; skill says *No working Python runtime is available* | The plugin cannot install or find a compatible interpreter | Do not run helper commands, do not write a document by hand. Give the user the remedy command from the skill body, ask them to run it, then call `office_status` again. |
|
|
127
|
+
| `office_status` shows `cli.exists: false`, or `python`/`cli` file not found | The bundled helper or its install is incomplete | Report the missing path. Do not substitute another tool that produces a different file. |
|
|
128
|
+
| `render` fails: `no document renderer found: install LibreOffice, Microsoft Office, or WPS Office` | Create/read/edit work, but docx/pptx/xlsx cannot be rasterised here | Do not claim you inspected it. Say the file was built and validated structurally (read verb), but this machine has no renderer so you could not check its appearance; the preview stays unavailable until Microsoft Office, WPS Office, or LibreOffice is installed. `pdf render` still works. If the user needs visual confidence now, offer to rebuild from a spec or to present the unverified file explicitly labelled as such. |
|
|
129
|
+
| Annotation region does not land on anything, or the payload says `unmapped: true` | The drawn region cannot be tied to an object in the file | Do not silently shift the region or guess a cell/shape. Say what you could identify (preview page, text, resolved cells) and which item you could not locate, then ask the user for the text or cell. |
|
|
130
|
+
| `{"ok": false, "error": "..."}` and non-zero exit | The spec was rejected (unknown key, bad type, missing file) | Read the error text, fix the spec key it names, re-run the same command. Never strip a failing block to make the command pass — that silently deletes user content. |
|
|
131
|
+
| `xlsx recalc` fails with *requires Microsoft Excel or WPS Spreadsheets* | openpyxl cannot evaluate formulas | Report formula cells as uncalculated; do not invent computed values. |
|
|
132
|
+
| The helper has no verb for what the requirement needs | The common path is short; the capability is not limited to it | Take the pack script route, or write a short snippet against `python-docx` / `openpyxl` / `python-pptx` / `pypdf` / `PyMuPDF` / `reportlab`, run it with `<python>`, then `read` the file back and render. Never refuse a job because a verb is missing. |
|
|
133
|
+
| `read` returns an empty structure for a file the user supplied | Unsupported or corrupt input | Say what failed and stop; do not overwrite the user's file with an empty one. |
|
|
134
|
+
|
|
135
|
+
## Naming rule for edits (restated)
|
|
136
|
+
|
|
137
|
+
`<original>-edited.<ext>`, in the same directory as the original, unless the
|
|
138
|
+
user asked for a different name. Never overwrite the original, never delete it,
|
|
139
|
+
never "clean up" a temp copy over it. Deliver both paths, original first.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Rasterise SVG artwork, one PDF page, or a raster image to a PNG.
|
|
3
|
+
|
|
4
|
+
Decks, workbooks and documents can only embed raster images, so vector art has
|
|
5
|
+
to become pixels before it can go in, and an oversized raster has to be scaled
|
|
6
|
+
down before it makes a file huge. SVG and PDF go through PyMuPDF in-process;
|
|
7
|
+
raster inputs go through Pillow (first frame, alpha kept or flattened).
|
|
8
|
+
|
|
9
|
+
Success prints exactly one JSON object on stdout. Failures print a JSON error
|
|
10
|
+
object on stderr and exit non-zero. The input file is never overwritten.
|
|
11
|
+
|
|
12
|
+
Exit codes: 0 success, 1 bad arguments, 2 unreadable input or write failure.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import pymupdf
|
|
24
|
+
except ImportError: # pragma: no cover - PyMuPDF is a required dependency
|
|
25
|
+
import fitz as pymupdf # type: ignore[no-redef]
|
|
26
|
+
|
|
27
|
+
from PIL import Image, ImageColor
|
|
28
|
+
|
|
29
|
+
VECTOR_SUFFIXES = {".svg", ".pdf"}
|
|
30
|
+
RASTER_SUFFIXES = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff", ".gif"}
|
|
31
|
+
|
|
32
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
33
|
+
try:
|
|
34
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
35
|
+
except (AttributeError, ValueError):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def fail(message, code=1):
|
|
40
|
+
print(json.dumps({"ok": False, "error": message}, ensure_ascii=False), file=sys.stderr)
|
|
41
|
+
raise SystemExit(code)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def background_color(spec):
|
|
45
|
+
"""Return an RGB tuple, or None for a transparent background."""
|
|
46
|
+
if spec.strip().lower() == "transparent":
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
return ImageColor.getrgb(spec.strip())[:3]
|
|
50
|
+
except ValueError:
|
|
51
|
+
fail(f"--background must be 'transparent' or a colour such as #ffffff, got {spec!r}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def fit_size(width, height, target_width, target_height):
|
|
55
|
+
"""Scale a native pixel size toward the requested box, keeping the aspect ratio.
|
|
56
|
+
|
|
57
|
+
One requested axis sizes the other; with both, the image fits inside the box.
|
|
58
|
+
"""
|
|
59
|
+
if not target_width and not target_height:
|
|
60
|
+
return round(width), round(height)
|
|
61
|
+
scales = [t / n for t, n in ((target_width, width), (target_height, height)) if t]
|
|
62
|
+
scale = min(scales)
|
|
63
|
+
return max(1, round(width * scale)), max(1, round(height * scale))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def flatten(image, color):
|
|
67
|
+
"""Composite an RGBA image over a solid colour and drop the alpha channel."""
|
|
68
|
+
canvas = Image.new("RGB", image.size, color)
|
|
69
|
+
canvas.paste(image, (0, 0), image)
|
|
70
|
+
return canvas
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def vector_page(source, args, background):
|
|
74
|
+
"""Rasterise one SVG/PDF page at --dpi, optionally scaled or flattened."""
|
|
75
|
+
try:
|
|
76
|
+
document = pymupdf.open(str(source))
|
|
77
|
+
except Exception as exc: # noqa: BLE001 - surface the library message verbatim
|
|
78
|
+
fail(f"cannot open input: {exc}", code=2)
|
|
79
|
+
if not 1 <= args.page <= document.page_count:
|
|
80
|
+
fail(f"--page {args.page} is outside 1-{document.page_count} for {source.name}")
|
|
81
|
+
page = document[args.page - 1]
|
|
82
|
+
if page.rect.width <= 0 or page.rect.height <= 0:
|
|
83
|
+
fail(f"{source.name} page {args.page} has no drawable area", code=2)
|
|
84
|
+
|
|
85
|
+
base = args.dpi / 72.0
|
|
86
|
+
natural_width = page.rect.width * base
|
|
87
|
+
natural_height = page.rect.height * base
|
|
88
|
+
out_width, out_height = fit_size(natural_width, natural_height, args.width, args.height)
|
|
89
|
+
zoom = base * (out_width / natural_width)
|
|
90
|
+
try:
|
|
91
|
+
pixmap = page.get_pixmap(
|
|
92
|
+
matrix=pymupdf.Matrix(zoom, zoom),
|
|
93
|
+
colorspace=pymupdf.csRGB,
|
|
94
|
+
alpha=True,
|
|
95
|
+
)
|
|
96
|
+
except Exception as exc: # noqa: BLE001 - rasterisation failure is an input problem
|
|
97
|
+
fail(f"cannot rasterize page {args.page}: {exc}", code=2)
|
|
98
|
+
image = Image.frombytes("RGBA", (pixmap.width, pixmap.height), pixmap.samples)
|
|
99
|
+
return flatten(image, background) if background is not None else image
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def raster_image(source, args, background):
|
|
103
|
+
"""Convert the first frame of a raster input to RGBA, then resize/flatten."""
|
|
104
|
+
try:
|
|
105
|
+
with Image.open(source) as raw:
|
|
106
|
+
raw.seek(0)
|
|
107
|
+
image = raw.convert("RGBA")
|
|
108
|
+
except (OSError, ValueError) as exc:
|
|
109
|
+
fail(f"cannot read raster input: {exc}", code=2)
|
|
110
|
+
if args.width or args.height:
|
|
111
|
+
image = image.resize(fit_size(*image.size, args.width, args.height), Image.Resampling.LANCZOS)
|
|
112
|
+
return flatten(image, background) if background is not None else image
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def main():
|
|
116
|
+
parser = argparse.ArgumentParser(
|
|
117
|
+
description="Rasterise an SVG file, one PDF page, or a raster image to PNG.",
|
|
118
|
+
epilog='Example: ensure_raster_image.py --input logo.svg --dpi 300 --out logo.png',
|
|
119
|
+
)
|
|
120
|
+
parser.add_argument("--input", required=True, help="SVG, PDF, or raster image (.png/.jpg/.webp/.bmp/.tif/.gif)")
|
|
121
|
+
parser.add_argument("--out", help="PNG output path (default: <input>-p<page>.png beside the input)")
|
|
122
|
+
parser.add_argument("--dpi", type=int, default=150, help="resolution for SVG/PDF input (default: 150)")
|
|
123
|
+
parser.add_argument("--page", type=int, default=1, help="1-based page for PDF/SVG input (default: 1)")
|
|
124
|
+
parser.add_argument("--width", type=int, help="target width in pixels; the height follows the aspect ratio")
|
|
125
|
+
parser.add_argument("--height", type=int, help="target height in pixels; the width follows the aspect ratio")
|
|
126
|
+
parser.add_argument(
|
|
127
|
+
"--background",
|
|
128
|
+
default="#ffffff",
|
|
129
|
+
help="colour behind transparent pixels, or 'transparent' to keep alpha (default: #ffffff)",
|
|
130
|
+
)
|
|
131
|
+
args = parser.parse_args()
|
|
132
|
+
|
|
133
|
+
source = Path(args.input).expanduser()
|
|
134
|
+
suffix = source.suffix.lower()
|
|
135
|
+
if not source.is_file():
|
|
136
|
+
fail(f"input not found: {source}", code=2)
|
|
137
|
+
if suffix in VECTOR_SUFFIXES:
|
|
138
|
+
kind = suffix.lstrip(".")
|
|
139
|
+
elif suffix in RASTER_SUFFIXES:
|
|
140
|
+
kind = "raster"
|
|
141
|
+
else:
|
|
142
|
+
fail(f"unsupported input extension {suffix or '(none)'}; expected .svg, .pdf, or a raster image")
|
|
143
|
+
if args.dpi <= 0:
|
|
144
|
+
fail("--dpi must be a positive integer")
|
|
145
|
+
if args.page < 1:
|
|
146
|
+
fail("--page must be 1 or greater")
|
|
147
|
+
for label, value in (("--width", args.width), ("--height", args.height)):
|
|
148
|
+
if value is not None and value <= 0:
|
|
149
|
+
fail(f"{label} must be a positive integer")
|
|
150
|
+
if kind == "raster" and args.page != 1:
|
|
151
|
+
fail("raster inputs have one frame; use --page 1")
|
|
152
|
+
background = background_color(args.background)
|
|
153
|
+
|
|
154
|
+
out = Path(args.out).expanduser() if args.out else source.with_name(f"{source.stem}-p{args.page}.png")
|
|
155
|
+
if out.resolve() == source.resolve():
|
|
156
|
+
fail("--out points at the input file; refusing to overwrite it")
|
|
157
|
+
try:
|
|
158
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
except OSError as exc:
|
|
160
|
+
fail(f"cannot create output directory {out.parent}: {exc}", code=2)
|
|
161
|
+
|
|
162
|
+
image = raster_image(source, args, background) if kind == "raster" else vector_page(source, args, background)
|
|
163
|
+
try:
|
|
164
|
+
image.save(str(out), "PNG")
|
|
165
|
+
except OSError as exc:
|
|
166
|
+
fail(f"cannot write {out}: {exc}", code=2)
|
|
167
|
+
|
|
168
|
+
print(
|
|
169
|
+
json.dumps(
|
|
170
|
+
{
|
|
171
|
+
"ok": True,
|
|
172
|
+
"input": str(source),
|
|
173
|
+
"out": str(out),
|
|
174
|
+
"source_kind": kind,
|
|
175
|
+
"page": args.page,
|
|
176
|
+
"width": image.width,
|
|
177
|
+
"height": image.height,
|
|
178
|
+
"dpi": args.dpi if kind != "raster" else None,
|
|
179
|
+
"bytes": out.stat().st_size,
|
|
180
|
+
},
|
|
181
|
+
ensure_ascii=False,
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
main()
|