dsh-office-kit 0.2.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/README.md +3 -1
- 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 +1 -1
- package/postinstall.mjs +92 -82
- package/python/office_cli.py +171 -246
package/python/office_cli.py
CHANGED
|
@@ -93,53 +93,121 @@ def find_soffice() -> str | None:
|
|
|
93
93
|
return found
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
#: Automation entry points per suite and format: (executable, COM ProgID).
|
|
97
|
+
RENDER_ENGINES: dict[str, dict[str, tuple[str, str]]] = {
|
|
98
|
+
"microsoft": {
|
|
99
|
+
"docx": ("WINWORD.EXE", "Word.Application"),
|
|
100
|
+
"pptx": ("POWERPNT.EXE", "PowerPoint.Application"),
|
|
101
|
+
"xlsx": ("EXCEL.EXE", "Excel.Application"),
|
|
102
|
+
},
|
|
103
|
+
"kingsoft": {
|
|
104
|
+
"docx": ("wps.exe", "KWps.Application"),
|
|
105
|
+
"pptx": ("wpp.exe", "KWpp.Application"),
|
|
106
|
+
"xlsx": ("et.exe", "Ket.Application"),
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _kingsoft_dirs() -> list[Path]:
|
|
112
|
+
"""WPS Office hides its binaries under a per-version directory."""
|
|
113
|
+
bases = [
|
|
114
|
+
os.environ.get("WPS_OFFICE_HOME"),
|
|
115
|
+
os.path.join(os.environ.get("ProgramFiles", r"C:\Program Files"), "Kingsoft", "WPS Office"),
|
|
116
|
+
os.path.join(os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)"), "Kingsoft", "WPS Office"),
|
|
117
|
+
os.path.join(os.environ.get("LOCALAPPDATA", ""), "Kingsoft", "WPS Office"),
|
|
118
|
+
]
|
|
119
|
+
found: list[Path] = []
|
|
120
|
+
for base in bases:
|
|
121
|
+
if not base:
|
|
122
|
+
continue
|
|
123
|
+
root = Path(base)
|
|
124
|
+
if (root / "office6").is_dir():
|
|
125
|
+
found.append(root / "office6")
|
|
126
|
+
continue
|
|
127
|
+
if not root.is_dir():
|
|
128
|
+
continue
|
|
129
|
+
versions = [child for child in root.iterdir() if (child / "office6").is_dir()]
|
|
130
|
+
versions.sort(key=lambda child: [int(part) if part.isdigit() else part for part in child.name.replace("-", ".").split(".")], reverse=True)
|
|
131
|
+
for child in versions:
|
|
132
|
+
found.append(child / "office6")
|
|
133
|
+
return found
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def office_engine(kind: str, family: str | None = None) -> tuple[str, str] | None:
|
|
137
|
+
"""Return (suite, executable) for an installed Office-compatible application."""
|
|
97
138
|
if sys.platform != "win32":
|
|
98
139
|
return None
|
|
99
|
-
|
|
100
|
-
name
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
140
|
+
families = [family] if family else ["microsoft", "kingsoft"]
|
|
141
|
+
for name in families:
|
|
142
|
+
executable = RENDER_ENGINES[name][kind][0]
|
|
143
|
+
if name == "microsoft":
|
|
144
|
+
roots = [
|
|
145
|
+
r"C:\Program Files\Microsoft Office\root\Office16",
|
|
146
|
+
r"C:\Program Files (x86)\Microsoft Office\root\Office16",
|
|
147
|
+
r"C:\Program Files\Microsoft Office\Office16",
|
|
148
|
+
r"C:\Program Files (x86)\Microsoft Office\Office16",
|
|
149
|
+
]
|
|
150
|
+
for root in roots:
|
|
151
|
+
candidate = Path(root) / executable
|
|
152
|
+
if candidate.is_file():
|
|
153
|
+
return name, str(candidate)
|
|
154
|
+
continue
|
|
155
|
+
for directory in _kingsoft_dirs():
|
|
156
|
+
candidate = directory / executable
|
|
157
|
+
if candidate.is_file():
|
|
158
|
+
return name, str(candidate)
|
|
111
159
|
return None
|
|
112
160
|
|
|
113
161
|
|
|
162
|
+
def office_binary(kind: str) -> str | None:
|
|
163
|
+
engine = office_engine(kind)
|
|
164
|
+
return engine[1] if engine else None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _engine_path(kind: str, family: str) -> str | None:
|
|
168
|
+
engine = office_engine(kind, family)
|
|
169
|
+
return engine[1] if engine else None
|
|
170
|
+
|
|
171
|
+
|
|
114
172
|
def _powershell() -> str | None:
|
|
115
173
|
return shutil.which("powershell") or shutil.which("pwsh")
|
|
116
174
|
|
|
117
175
|
|
|
118
|
-
def _office_script(kind: str) -> str:
|
|
176
|
+
def _office_script(kind: str, family: str) -> str:
|
|
177
|
+
progid = RENDER_ENGINES[family][kind][1]
|
|
119
178
|
if kind == "docx":
|
|
120
|
-
|
|
179
|
+
if family == "kingsoft":
|
|
180
|
+
return rf"""
|
|
181
|
+
$ErrorActionPreference = 'Stop'
|
|
182
|
+
$app = New-Object -ComObject {progid}
|
|
183
|
+
$app.Visible = $false
|
|
184
|
+
$app.DisplayAlerts = 0
|
|
185
|
+
$doc = $app.Documents.Open($env:DSH_INPUT, $false, $true)
|
|
186
|
+
try {{ $doc.SaveAs([ref]$env:DSH_PDF, [ref]17) }} finally {{ $doc.Close(0); $app.Quit() }}
|
|
187
|
+
"""
|
|
188
|
+
return rf"""
|
|
121
189
|
$ErrorActionPreference = 'Stop'
|
|
122
|
-
$app = New-Object -ComObject
|
|
190
|
+
$app = New-Object -ComObject {progid}
|
|
123
191
|
$app.Visible = $false
|
|
124
192
|
$app.DisplayAlerts = 0
|
|
125
193
|
$doc = $app.Documents.Open($env:DSH_INPUT, $false, $true)
|
|
126
|
-
try { $doc.ExportAsFixedFormat($env:DSH_PDF, 17) } finally { $doc.Close(0); $app.Quit() }
|
|
194
|
+
try {{ $doc.ExportAsFixedFormat($env:DSH_PDF, 17) }} finally {{ $doc.Close(0); $app.Quit() }}
|
|
127
195
|
"""
|
|
128
196
|
if kind == "pptx":
|
|
129
|
-
return
|
|
197
|
+
return rf"""
|
|
130
198
|
$ErrorActionPreference = 'Stop'
|
|
131
|
-
$app = New-Object -ComObject
|
|
199
|
+
$app = New-Object -ComObject {progid}
|
|
132
200
|
$app.DisplayAlerts = 1
|
|
133
201
|
$pres = $app.Presentations.Open($env:DSH_INPUT, $true, $false, $false)
|
|
134
|
-
try { $pres.SaveAs($env:DSH_PDF, 32) } finally { $pres.Close(); $app.Quit() }
|
|
202
|
+
try {{ $pres.SaveAs($env:DSH_PDF, 32) }} finally {{ $pres.Close(); $app.Quit() }}
|
|
135
203
|
"""
|
|
136
|
-
return
|
|
204
|
+
return rf"""
|
|
137
205
|
$ErrorActionPreference = 'Stop'
|
|
138
|
-
$app = New-Object -ComObject
|
|
206
|
+
$app = New-Object -ComObject {progid}
|
|
139
207
|
$app.Visible = $false
|
|
140
208
|
$app.DisplayAlerts = $false
|
|
141
209
|
$wb = $app.Workbooks.Open($env:DSH_INPUT)
|
|
142
|
-
try { $wb.ExportAsFixedFormat(0, $env:DSH_PDF) } finally { $wb.Close($false); $app.Quit() }
|
|
210
|
+
try {{ $wb.ExportAsFixedFormat(0, $env:DSH_PDF) }} finally {{ $wb.Close($false); $app.Quit() }}
|
|
143
211
|
"""
|
|
144
212
|
|
|
145
213
|
|
|
@@ -151,28 +219,33 @@ def export_pdf(input_path: Path, kind: str, out_dir: Path) -> tuple[str, str]:
|
|
|
151
219
|
pdf.unlink()
|
|
152
220
|
|
|
153
221
|
power_shell = _powershell()
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
222
|
+
failures: list[str] = []
|
|
223
|
+
if power_shell:
|
|
224
|
+
for family, renderer in (("microsoft", "microsoft-office"), ("kingsoft", "kingsoft-wps")):
|
|
225
|
+
engine = office_engine(kind, family)
|
|
226
|
+
if engine is None:
|
|
227
|
+
continue
|
|
228
|
+
script = Path(tempfile.gettempdir()) / f"dsh-office-{kind}-{os.getpid()}.ps1"
|
|
229
|
+
script.write_text(_office_script(kind, family), encoding="utf-8")
|
|
230
|
+
env = dict(os.environ)
|
|
231
|
+
env["DSH_INPUT"] = str(input_path.resolve())
|
|
232
|
+
env["DSH_PDF"] = str(pdf.resolve())
|
|
233
|
+
try:
|
|
234
|
+
completed = subprocess.run(
|
|
235
|
+
[power_shell, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", str(script)],
|
|
236
|
+
capture_output=True,
|
|
237
|
+
text=True,
|
|
238
|
+
timeout=240,
|
|
239
|
+
env=env,
|
|
240
|
+
)
|
|
241
|
+
except subprocess.TimeoutExpired:
|
|
242
|
+
failures.append(f"{family}: timed out")
|
|
243
|
+
continue
|
|
244
|
+
finally:
|
|
245
|
+
script.unlink(missing_ok=True)
|
|
246
|
+
if completed.returncode == 0 and pdf.is_file():
|
|
247
|
+
return renderer, str(pdf)
|
|
248
|
+
failures.append(f"{family}: " + ((completed.stderr or completed.stdout).strip() or "no PDF produced"))
|
|
176
249
|
|
|
177
250
|
soffice = find_soffice()
|
|
178
251
|
if soffice:
|
|
@@ -187,7 +260,10 @@ def export_pdf(input_path: Path, kind: str, out_dir: Path) -> tuple[str, str]:
|
|
|
187
260
|
fail(f"LibreOffice export failed for {input_path.name}", detail=detail or None)
|
|
188
261
|
return "libreoffice", str(pdf)
|
|
189
262
|
|
|
190
|
-
fail(
|
|
263
|
+
fail(
|
|
264
|
+
"no document renderer found: install LibreOffice, Microsoft Office, or WPS Office",
|
|
265
|
+
detail="; ".join(failures) or None,
|
|
266
|
+
)
|
|
191
267
|
|
|
192
268
|
|
|
193
269
|
def rasterize_pdf(pdf: str, out_dir: Path, dpi: int = 110) -> list[str]:
|
|
@@ -424,38 +500,6 @@ def _replace_in_paragraph(paragraph, find: str, replacement: str) -> int:
|
|
|
424
500
|
return count
|
|
425
501
|
|
|
426
502
|
|
|
427
|
-
def docx_edit(spec: dict[str, Any], input_path: Path, out: Path) -> dict[str, Any]:
|
|
428
|
-
from docx import Document
|
|
429
|
-
|
|
430
|
-
document = Document(str(input_path))
|
|
431
|
-
replace_count = 0
|
|
432
|
-
for item in spec.get("replace", []):
|
|
433
|
-
find = str(item.get("find", ""))
|
|
434
|
-
replacement = str(item.get("replace", ""))
|
|
435
|
-
if not find:
|
|
436
|
-
continue
|
|
437
|
-
for paragraph in _docx_iter_paragraphs(document):
|
|
438
|
-
replace_count += _replace_in_paragraph(paragraph, find, replacement)
|
|
439
|
-
counters = {key: 0 for key in ("headings", "paragraphs", "list_items", "tables", "images", "equations", "equation_fallbacks")}
|
|
440
|
-
_docx_add_blocks(document, spec.get("blocks", []), counters)
|
|
441
|
-
core = spec.get("core")
|
|
442
|
-
if isinstance(core, dict):
|
|
443
|
-
for key in ("title", "author", "subject", "keywords", "comments"):
|
|
444
|
-
if key in core:
|
|
445
|
-
setattr(document.core_properties, key, str(core[key]))
|
|
446
|
-
ensure_parent(str(out))
|
|
447
|
-
document.save(str(out))
|
|
448
|
-
return {
|
|
449
|
-
"ok": True,
|
|
450
|
-
"kind": "docx",
|
|
451
|
-
"command": "edit",
|
|
452
|
-
"input": str(input_path),
|
|
453
|
-
"output": str(out),
|
|
454
|
-
"replacements": replace_count,
|
|
455
|
-
"counts": counters,
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
|
|
459
503
|
def _docx_has_equation(paragraph) -> bool:
|
|
460
504
|
tag = "{http://schemas.openxmlformats.org/officeDocument/2006/math}oMath"
|
|
461
505
|
return bool(paragraph._p.findall(f".//{tag}"))
|
|
@@ -667,45 +711,6 @@ def pdf_read(input_path: Path) -> dict[str, Any]:
|
|
|
667
711
|
}
|
|
668
712
|
|
|
669
713
|
|
|
670
|
-
def pdf_edit(spec: dict[str, Any], input_path: Path, out: Path) -> dict[str, Any]:
|
|
671
|
-
from pypdf import PdfReader, PdfWriter
|
|
672
|
-
|
|
673
|
-
writer = PdfWriter()
|
|
674
|
-
reader = PdfReader(str(input_path))
|
|
675
|
-
for page in reader.pages:
|
|
676
|
-
writer.add_page(page)
|
|
677
|
-
rotate = int(spec.get("rotate", 0))
|
|
678
|
-
targets = spec.get("pages")
|
|
679
|
-
if rotate:
|
|
680
|
-
for index, page in enumerate(writer.pages):
|
|
681
|
-
if targets is None or index in targets:
|
|
682
|
-
page.rotate(rotate)
|
|
683
|
-
for extra in spec.get("merge", []):
|
|
684
|
-
extra_reader = PdfReader(str(require_file(str(extra), "PDF to merge")))
|
|
685
|
-
for page in extra_reader.pages:
|
|
686
|
-
writer.add_page(page)
|
|
687
|
-
if spec.get("remove_pages"):
|
|
688
|
-
remove = set(int(value) for value in spec["remove_pages"])
|
|
689
|
-
writer = PdfWriter()
|
|
690
|
-
for index, page in enumerate(reader.pages):
|
|
691
|
-
if index not in remove:
|
|
692
|
-
writer.add_page(page)
|
|
693
|
-
for extra in spec.get("merge", []):
|
|
694
|
-
extra_reader = PdfReader(str(require_file(str(extra), "PDF to merge")))
|
|
695
|
-
for page in extra_reader.pages:
|
|
696
|
-
writer.add_page(page)
|
|
697
|
-
if isinstance(spec.get("metadata"), dict):
|
|
698
|
-
metadata = {}
|
|
699
|
-
for key, value in spec["metadata"].items():
|
|
700
|
-
name = str(key)
|
|
701
|
-
metadata[name if name.startswith("/") else f"/{name}"] = str(value)
|
|
702
|
-
writer.add_metadata(metadata)
|
|
703
|
-
ensure_parent(str(out))
|
|
704
|
-
with open(out, "wb") as handle:
|
|
705
|
-
writer.write(handle)
|
|
706
|
-
return {"ok": True, "kind": "pdf", "command": "edit", "input": str(input_path), "output": str(out), "page_count": len(writer.pages)}
|
|
707
|
-
|
|
708
|
-
|
|
709
714
|
def pdf_render(input_path: Path, out_dir: Path, dpi: int) -> dict[str, Any]:
|
|
710
715
|
pages = rasterize_pdf(str(input_path), out_dir, dpi)
|
|
711
716
|
return {"ok": True, "kind": "pdf", "command": "render", "input": str(input_path), "page_count": len(pages), "pages": pages}
|
|
@@ -912,38 +917,6 @@ def pptx_read(input_path: Path) -> dict[str, Any]:
|
|
|
912
917
|
}
|
|
913
918
|
|
|
914
919
|
|
|
915
|
-
def pptx_edit(spec: dict[str, Any], input_path: Path, out: Path) -> dict[str, Any]:
|
|
916
|
-
from pptx import Presentation
|
|
917
|
-
|
|
918
|
-
prs = Presentation(str(input_path))
|
|
919
|
-
replacements = 0
|
|
920
|
-
for item in spec.get("replace", []):
|
|
921
|
-
find = str(item.get("find", ""))
|
|
922
|
-
replacement = str(item.get("replace", ""))
|
|
923
|
-
if not find:
|
|
924
|
-
continue
|
|
925
|
-
for slide in prs.slides:
|
|
926
|
-
for shape in slide.shapes:
|
|
927
|
-
if not shape.has_text_frame:
|
|
928
|
-
continue
|
|
929
|
-
for paragraph in shape.text_frame.paragraphs:
|
|
930
|
-
for run in paragraph.runs:
|
|
931
|
-
if find in run.text:
|
|
932
|
-
run.text = run.text.replace(find, replacement)
|
|
933
|
-
replacements += 1
|
|
934
|
-
counters = {key: 0 for key in ("slides", "bullets", "tables", "charts", "images")}
|
|
935
|
-
for slide_spec in spec.get("slides", []):
|
|
936
|
-
if isinstance(slide_spec, dict):
|
|
937
|
-
_pptx_add_slide(prs, slide_spec, counters)
|
|
938
|
-
if isinstance(spec.get("core"), dict):
|
|
939
|
-
for key in ("title", "author", "subject", "keywords", "comments"):
|
|
940
|
-
if key in spec["core"]:
|
|
941
|
-
setattr(prs.core_properties, key, str(spec["core"][key]))
|
|
942
|
-
ensure_parent(str(out))
|
|
943
|
-
prs.save(str(out))
|
|
944
|
-
return {"ok": True, "kind": "pptx", "command": "edit", "input": str(input_path), "output": str(out), "replacements": replacements, "added": counters}
|
|
945
|
-
|
|
946
|
-
|
|
947
920
|
# ---------------------------------------------------------------------------
|
|
948
921
|
# XLSX
|
|
949
922
|
# ---------------------------------------------------------------------------
|
|
@@ -1113,86 +1086,26 @@ def xlsx_read(input_path: Path, data_only: bool = False) -> dict[str, Any]:
|
|
|
1113
1086
|
}
|
|
1114
1087
|
|
|
1115
1088
|
|
|
1116
|
-
def xlsx_edit(spec: dict[str, Any], input_path: Path, out: Path) -> dict[str, Any]:
|
|
1117
|
-
from openpyxl import load_workbook
|
|
1118
|
-
from openpyxl.drawing.image import Image as XLImage
|
|
1119
|
-
|
|
1120
|
-
workbook = load_workbook(str(input_path))
|
|
1121
|
-
changed = 0
|
|
1122
|
-
for operation in spec.get("operations", []):
|
|
1123
|
-
if not isinstance(operation, dict):
|
|
1124
|
-
continue
|
|
1125
|
-
action = str(operation.get("action", "set"))
|
|
1126
|
-
if action == "add_sheet":
|
|
1127
|
-
title = str(operation.get("name", "Sheet"))
|
|
1128
|
-
worksheet = workbook.create_sheet(title)
|
|
1129
|
-
elif action == "delete_sheet":
|
|
1130
|
-
name = str(operation.get("name", ""))
|
|
1131
|
-
if name not in workbook.sheetnames:
|
|
1132
|
-
fail(f"sheet not found: {name}")
|
|
1133
|
-
workbook.remove(workbook[name])
|
|
1134
|
-
continue
|
|
1135
|
-
elif action == "rename_sheet":
|
|
1136
|
-
old = str(operation.get("from", ""))
|
|
1137
|
-
new = str(operation.get("to", ""))
|
|
1138
|
-
if old not in workbook.sheetnames:
|
|
1139
|
-
fail(f"sheet not found: {old}")
|
|
1140
|
-
workbook[old].title = new
|
|
1141
|
-
continue
|
|
1142
|
-
else:
|
|
1143
|
-
name = str(operation.get("sheet", workbook.sheetnames[0]))
|
|
1144
|
-
if name not in workbook.sheetnames:
|
|
1145
|
-
fail(f"sheet not found: {name}")
|
|
1146
|
-
worksheet = workbook[name]
|
|
1147
|
-
if action == "set_cells":
|
|
1148
|
-
for address, value in (operation.get("cells") or {}).items():
|
|
1149
|
-
worksheet[str(address)] = _xlsx_value(value)
|
|
1150
|
-
changed += 1
|
|
1151
|
-
elif action == "append_rows":
|
|
1152
|
-
for row in operation.get("rows", []):
|
|
1153
|
-
worksheet.append([_xlsx_value(value) for value in row])
|
|
1154
|
-
changed += 1
|
|
1155
|
-
elif action == "style":
|
|
1156
|
-
for address, style in (operation.get("cells") or {}).items():
|
|
1157
|
-
if isinstance(style, dict):
|
|
1158
|
-
for cell in _xlsx_cells(worksheet, str(address)):
|
|
1159
|
-
_xlsx_apply_style(cell, style)
|
|
1160
|
-
changed += 1
|
|
1161
|
-
elif action == "chart":
|
|
1162
|
-
_xlsx_add_chart(worksheet, operation)
|
|
1163
|
-
changed += 1
|
|
1164
|
-
elif action == "image":
|
|
1165
|
-
path = require_file(str(operation.get("path", "")), "image")
|
|
1166
|
-
worksheet.add_image(XLImage(str(path)), str(operation.get("anchor", "A1")))
|
|
1167
|
-
changed += 1
|
|
1168
|
-
elif action == "rows":
|
|
1169
|
-
for row in operation.get("rows", []):
|
|
1170
|
-
worksheet.append([_xlsx_value(value) for value in row])
|
|
1171
|
-
changed += 1
|
|
1172
|
-
ensure_parent(str(out))
|
|
1173
|
-
workbook.save(str(out))
|
|
1174
|
-
workbook.close()
|
|
1175
|
-
return {"ok": True, "kind": "xlsx", "command": "edit", "input": str(input_path), "output": str(out), "changes": changed}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
1089
|
def xlsx_recalc(input_path: Path, out: Path) -> dict[str, Any]:
|
|
1179
|
-
"""Force
|
|
1180
|
-
if sys.platform
|
|
1181
|
-
|
|
1090
|
+
"""Force a spreadsheet application to recalculate and store cached formula values."""
|
|
1091
|
+
engine = office_engine("xlsx") if sys.platform == "win32" else None
|
|
1092
|
+
if engine is None or _powershell() is None:
|
|
1093
|
+
fail("formula recalculation requires Microsoft Excel or WPS Spreadsheets on Windows; openpyxl cannot evaluate formulas")
|
|
1094
|
+
family, executable = engine
|
|
1095
|
+
progid = RENDER_ENGINES[family]["xlsx"][1]
|
|
1096
|
+
ensure_parent(str(out))
|
|
1097
|
+
if out.resolve() != input_path.resolve():
|
|
1098
|
+
shutil.copyfile(input_path, out)
|
|
1182
1099
|
script = Path(tempfile.gettempdir()) / f"dsh-office-recalc-{os.getpid()}.ps1"
|
|
1183
|
-
script.write_text(
|
|
1100
|
+
script.write_text(rf"""
|
|
1184
1101
|
$ErrorActionPreference = 'Stop'
|
|
1185
|
-
$app = New-Object -ComObject
|
|
1102
|
+
$app = New-Object -ComObject {progid}
|
|
1186
1103
|
$app.Visible = $false
|
|
1187
1104
|
$app.DisplayAlerts = $false
|
|
1188
|
-
$wb = $app.Workbooks.Open($env:
|
|
1189
|
-
try {
|
|
1190
|
-
$app.CalculateFull()
|
|
1191
|
-
$wb.SaveAs($env:DSH_OUT, 51)
|
|
1192
|
-
} finally { $wb.Close($false); $app.Quit() }
|
|
1105
|
+
$wb = $app.Workbooks.Open($env:DSH_OUT)
|
|
1106
|
+
try {{ $app.CalculateFull(); $wb.Save() }} finally {{ $wb.Close($true); $app.Quit() }}
|
|
1193
1107
|
""", encoding="utf-8")
|
|
1194
1108
|
env = dict(os.environ)
|
|
1195
|
-
env["DSH_INPUT"] = str(input_path.resolve())
|
|
1196
1109
|
env["DSH_OUT"] = str(out.resolve())
|
|
1197
1110
|
try:
|
|
1198
1111
|
completed = subprocess.run(
|
|
@@ -1205,8 +1118,28 @@ try {
|
|
|
1205
1118
|
finally:
|
|
1206
1119
|
script.unlink(missing_ok=True)
|
|
1207
1120
|
if completed.returncode != 0 or not out.is_file():
|
|
1208
|
-
fail("
|
|
1209
|
-
|
|
1121
|
+
fail("formula recalculation failed", detail=(completed.stderr or completed.stdout).strip() or None)
|
|
1122
|
+
cached = 0
|
|
1123
|
+
try:
|
|
1124
|
+
import openpyxl
|
|
1125
|
+
|
|
1126
|
+
workbook = openpyxl.load_workbook(str(out), data_only=True)
|
|
1127
|
+
for sheet in workbook.worksheets:
|
|
1128
|
+
for row in sheet.iter_rows():
|
|
1129
|
+
cached += sum(1 for cell in row if cell.value is not None)
|
|
1130
|
+
workbook.close()
|
|
1131
|
+
except Exception:
|
|
1132
|
+
cached = 0
|
|
1133
|
+
return {
|
|
1134
|
+
"ok": True,
|
|
1135
|
+
"kind": "xlsx",
|
|
1136
|
+
"command": "recalc",
|
|
1137
|
+
"input": str(input_path),
|
|
1138
|
+
"output": str(out),
|
|
1139
|
+
"renderer": f"{family}-office" if family == "microsoft" else "kingsoft-wps",
|
|
1140
|
+
"executable": executable,
|
|
1141
|
+
"cached_values": cached,
|
|
1142
|
+
}
|
|
1210
1143
|
|
|
1211
1144
|
|
|
1212
1145
|
# ---------------------------------------------------------------------------
|
|
@@ -1246,9 +1179,20 @@ def status() -> dict[str, Any]:
|
|
|
1246
1179
|
"renderers": {
|
|
1247
1180
|
"libreoffice": find_soffice(),
|
|
1248
1181
|
"microsoft_office": {
|
|
1249
|
-
"word":
|
|
1250
|
-
"powerpoint":
|
|
1251
|
-
"excel":
|
|
1182
|
+
"word": _engine_path("docx", "microsoft"),
|
|
1183
|
+
"powerpoint": _engine_path("pptx", "microsoft"),
|
|
1184
|
+
"excel": _engine_path("xlsx", "microsoft"),
|
|
1185
|
+
},
|
|
1186
|
+
"kingsoft_wps": {
|
|
1187
|
+
"writer": _engine_path("docx", "kingsoft"),
|
|
1188
|
+
"presentation": _engine_path("pptx", "kingsoft"),
|
|
1189
|
+
"spreadsheet": _engine_path("xlsx", "kingsoft"),
|
|
1190
|
+
},
|
|
1191
|
+
"can_render": {
|
|
1192
|
+
"docx": bool(office_engine("docx") or find_soffice()),
|
|
1193
|
+
"pptx": bool(office_engine("pptx") or find_soffice()),
|
|
1194
|
+
"xlsx": bool(office_engine("xlsx") or find_soffice()),
|
|
1195
|
+
"pdf": True,
|
|
1252
1196
|
},
|
|
1253
1197
|
},
|
|
1254
1198
|
}
|
|
@@ -1280,13 +1224,6 @@ def main() -> None:
|
|
|
1280
1224
|
render.add_argument("--dpi", type=int, default=110)
|
|
1281
1225
|
render.add_argument("--json-out")
|
|
1282
1226
|
|
|
1283
|
-
if kind in {"docx", "pdf", "pptx", "xlsx"}:
|
|
1284
|
-
edit = commands.add_parser("edit")
|
|
1285
|
-
edit.add_argument("--input", required=True)
|
|
1286
|
-
edit.add_argument("--spec", required=True)
|
|
1287
|
-
edit.add_argument("--out", required=True)
|
|
1288
|
-
edit.add_argument("--json-out")
|
|
1289
|
-
|
|
1290
1227
|
if kind == "xlsx":
|
|
1291
1228
|
recalc = commands.add_parser("recalc")
|
|
1292
1229
|
recalc.add_argument("--input", required=True)
|
|
@@ -1321,18 +1258,6 @@ def main() -> None:
|
|
|
1321
1258
|
result = pptx_read(input_path)
|
|
1322
1259
|
else:
|
|
1323
1260
|
result = xlsx_read(input_path, data_only=bool(getattr(args, "data_only", False)))
|
|
1324
|
-
elif args.command == "edit":
|
|
1325
|
-
input_path = require_file(args.input)
|
|
1326
|
-
spec = load_json(args.spec)
|
|
1327
|
-
out = Path(args.out)
|
|
1328
|
-
if args.format == "docx":
|
|
1329
|
-
result = docx_edit(spec, input_path, out)
|
|
1330
|
-
elif args.format == "pdf":
|
|
1331
|
-
result = pdf_edit(spec, input_path, out)
|
|
1332
|
-
elif args.format == "pptx":
|
|
1333
|
-
result = pptx_edit(spec, input_path, out)
|
|
1334
|
-
else:
|
|
1335
|
-
result = xlsx_edit(spec, input_path, out)
|
|
1336
1261
|
elif args.command == "render":
|
|
1337
1262
|
input_path = require_file(args.input)
|
|
1338
1263
|
out_dir = Path(args.out_dir)
|