dsh-office-kit 0.2.0 → 0.3.1
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 +99 -0
- package/assets/skills/office-documents/scripts/a11y_audit.py +201 -0
- package/assets/skills/office-documents/scripts/accept_tracked_changes.py +158 -0
- package/assets/skills/office-documents/scripts/add_tracked_replacements.py +155 -0
- package/assets/skills/office-documents/scripts/append_blocks.py +115 -0
- package/assets/skills/office-documents/scripts/apply_template_styles.py +111 -0
- package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +109 -0
- package/assets/skills/office-documents/scripts/comments_add.py +143 -0
- package/assets/skills/office-documents/scripts/comments_apply_patch.py +121 -0
- package/assets/skills/office-documents/scripts/comments_extract.py +92 -0
- package/assets/skills/office-documents/scripts/comments_strip.py +81 -0
- package/assets/skills/office-documents/scripts/content_controls.py +199 -0
- package/assets/skills/office-documents/scripts/core_properties.py +131 -0
- package/assets/skills/office-documents/scripts/docx_kit.py +594 -0
- package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +232 -0
- package/assets/skills/office-documents/scripts/docx_table_to_csv.py +106 -0
- package/assets/skills/office-documents/scripts/fields_materialize.py +73 -0
- package/assets/skills/office-documents/scripts/fields_report.py +66 -0
- package/assets/skills/office-documents/scripts/flatten_ref_fields.py +69 -0
- package/assets/skills/office-documents/scripts/footnotes_report.py +84 -0
- package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +103 -0
- package/assets/skills/office-documents/scripts/heading_audit.py +88 -0
- package/assets/skills/office-documents/scripts/images_audit.py +87 -0
- package/assets/skills/office-documents/scripts/insert_note.py +133 -0
- package/assets/skills/office-documents/scripts/insert_ref_fields.py +97 -0
- package/assets/skills/office-documents/scripts/insert_toc.py +91 -0
- package/assets/skills/office-documents/scripts/internal_nav.py +153 -0
- package/assets/skills/office-documents/scripts/make_fixtures.py +176 -0
- package/assets/skills/office-documents/scripts/merge_docx_append.py +246 -0
- package/assets/skills/office-documents/scripts/privacy_scrub.py +138 -0
- package/assets/skills/office-documents/scripts/redact_docx.py +120 -0
- package/assets/skills/office-documents/scripts/render_and_diff.py +108 -0
- package/assets/skills/office-documents/scripts/replace_text.py +161 -0
- package/assets/skills/office-documents/scripts/section_audit.py +98 -0
- package/assets/skills/office-documents/scripts/set_protection.py +99 -0
- package/assets/skills/office-documents/scripts/style_lint.py +118 -0
- package/assets/skills/office-documents/scripts/style_normalize.py +110 -0
- package/assets/skills/office-documents/scripts/table_geometry.py +174 -0
- package/assets/skills/office-documents/scripts/watermark_add.py +55 -0
- package/assets/skills/office-documents/scripts/watermark_audit_remove.py +74 -0
- package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +124 -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 +143 -0
- package/assets/skills/office-presentations/container_tools/core_properties.py +127 -0
- package/assets/skills/office-presentations/container_tools/create_montage.py +133 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +292 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +164 -0
- package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +151 -0
- package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +206 -0
- package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +219 -0
- package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +352 -0
- package/assets/skills/office-presentations/container_tools/preview_render.py +382 -0
- package/assets/skills/office-presentations/container_tools/render_slides.py +146 -0
- package/assets/skills/office-presentations/container_tools/replace_text.py +175 -0
- package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +236 -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 +183 -0
- package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +161 -0
- package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +118 -0
- package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +246 -0
- package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +192 -0
- package/assets/skills/office-spreadsheets/SKILL.md +105 -101
- package/assets/skills/office-spreadsheets/container_tools/add_chart.py +243 -0
- package/assets/skills/office-spreadsheets/container_tools/add_image.py +114 -0
- package/assets/skills/office-spreadsheets/container_tools/append_rows.py +129 -0
- package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +313 -0
- package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +405 -0
- package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +287 -0
- package/assets/skills/office-spreadsheets/container_tools/set_cells.py +142 -0
- package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +141 -0
- package/assets/skills/office-spreadsheets/container_tools/style_cells.py +140 -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 +260 -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 +246 -89
- package/package.json +1 -1
- package/postinstall.mjs +92 -82
- package/python/office_cli.py +213 -254
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Read text, tables, and page images
|
|
2
|
+
|
|
3
|
+
Text extraction reads the page content stream: glyph runs carry position and a font, and a library groups them into words and lines. A page with no text operators (a scan or a photo export) yields nothing no matter which extractor is used — that is a property of the file, not of the tool.
|
|
4
|
+
|
|
5
|
+
## Fast path: plain text and structure
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
"<python>" "<cli>" pdf read --input "<file.pdf>" --json-out "<read.json>"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Returns `page_count`, `word_count`, document metadata, per-page text (with character counts), and AcroForm field identifiers. `scripts/render_pages.py --input "<file.pdf>"` adds geometry: page size, rotation, drawings, annotations, widgets, and the characters inside an optional crop.
|
|
12
|
+
|
|
13
|
+
## Tables and layout with pdfplumber
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
"<python>" "<skill>/scripts/extract_text.py" --input "<file.pdf>" --json-out "<tables.json>"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Each page reports its characters and detected tables as rows of cell strings. The detection uses the page's ruling lines and word spacing, so a borderless table often comes back as running text; `--layout` keeps the spacing of the original lines for a manual look.
|
|
20
|
+
|
|
21
|
+
When the detection looks wrong, inspect rather than guess:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
"<python>" - <<'PY'
|
|
25
|
+
import pdfplumber
|
|
26
|
+
with pdfplumber.open("in.pdf") as pdf:
|
|
27
|
+
page = pdf.pages[0]
|
|
28
|
+
print("chars", len(page.chars), "lines", len(page.lines), "rects", len(page.rects))
|
|
29
|
+
for table in page.find_tables():
|
|
30
|
+
print(table.bbox, table.extract()[:3])
|
|
31
|
+
print(page.extract_text()[:400])
|
|
32
|
+
PY
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`chars`, `lines`, and `rects` tell you whether the table has rules at all. A table with no lines is best read from `extract_text` with `--layout`, where the column gaps survive as spaces.
|
|
36
|
+
|
|
37
|
+
## Page images
|
|
38
|
+
|
|
39
|
+
A page may contain embedded rasters that are invisible in text extraction. List them with PyMuPDF:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
"<python>" - <<'PY'
|
|
43
|
+
import pymupdf
|
|
44
|
+
doc = pymupdf.open("in.pdf")
|
|
45
|
+
for number, page in enumerate(doc, 1):
|
|
46
|
+
for image in page.get_images(full=True):
|
|
47
|
+
print(number, image[0], image[2], "x", image[3])
|
|
48
|
+
PY
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Extract one with `page.get_pixmap(clip=...)` or `doc.extract_image(xref)` when the user needs the picture itself. Rendering the page at 150–200 dpi and cropping with `read_image` is the reliable way to *see* it.
|
|
52
|
+
|
|
53
|
+
## What fails and how to resolve it
|
|
54
|
+
|
|
55
|
+
- **Scanned page, no text**: there is no text layer to read. Say so; the render shows the image of the words. Resolutions: work from a text-based copy of the file, or read the pages visually and type the content into a rebuild. Do not claim extraction succeeded on an empty result.
|
|
56
|
+
- **Empty string from a page that shows text**: the page may use a font with a broken ToUnicode map, or the text may be drawn as vector outlines. Check the render before concluding; if it is outlines, extraction cannot recover the words.
|
|
57
|
+
- **Wrong reading order in multi-column layouts**: raw extraction follows the operator order. Crop each column with a rect and extract per column, `get_textbox(rect)` on a PyMuPDF page is the simplest split.
|
|
58
|
+
- **Ligatures and soft hyphens**: extraction may glue or split words; quote extracted text only after reading the same passage on the render.
|
|
59
|
+
|
|
60
|
+
## Verify
|
|
61
|
+
|
|
62
|
+
Rendering the page is the check for every extraction: the characters you return must be visible in that image. Watch for a page whose extracted text is suspiciously short (a missing text layer), duplicated (headers repeated per page), or full of replacement glyphs (broken encoding) before you build anything on top of it.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Remove a region of a page
|
|
2
|
+
|
|
3
|
+
Redaction here means *destruction*, not a black box: the glyphs and vector paths inside a rectangle are deleted from the content stream, so no text extractor can recover them afterwards. A rectangle that only covers them visually is not a redaction, and "black out then flatten" leaks the original text.
|
|
4
|
+
|
|
5
|
+
## Fast path: the region script
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
"<python>" "<skill>/scripts/redact_region.py" --input "<file.pdf>" --out "<file-redacted.pdf>" \
|
|
9
|
+
--annotations '{"page":1,"region":{"left":0.064,"top":0.318,"width":0.891,"height":0.153}}' \
|
|
10
|
+
--json-out "<redact.json>"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The payload is the annotation shape: 1-based page plus a region normalised 0–1 against the rendered page. The script converts it, expands the rectangle by `--pad` points (default 2.5), calls `add_redact_annot(rect, fill=None)` then `apply_redactions(images=PDF_REDACT_IMAGE_NONE, graphics=PDF_REDACT_LINE_ART_REMOVE_IF_TOUCHED, text=PDF_REDACT_TEXT_REMOVE)`, and reports `removed_chars`, `removed_drawings`, the per-page before/after counts, and `leftover_chars_in_rect`. `--region`/`--page` do the same for a single value without JSON. Multiple annotations in one payload are applied page by page.
|
|
14
|
+
|
|
15
|
+
## Why the rectangle needs padding
|
|
16
|
+
|
|
17
|
+
A line of text sits on a baseline and its glyphs can overhang the geometric box by a fraction of a point; selection boxes from a preview are approximate too. Expanding by 2–3 points catches ascenders, descenders, rules that touch the text, and the anti-aliased edge, while staying clear of neighbours. Measure, do not guess: `get_textbox(rect)` before the redaction lists what the rectangle will take, and `leftover_chars_in_rect` after it must be zero.
|
|
18
|
+
|
|
19
|
+
Do it by hand when you need a different shape or a dry run:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
"<python>" - <<'PY'
|
|
23
|
+
import pymupdf
|
|
24
|
+
doc = pymupdf.open("in.pdf")
|
|
25
|
+
page = doc[0]
|
|
26
|
+
r = page.rect
|
|
27
|
+
rect = pymupdf.Rect(r.x0 + 0.064 * r.width, r.y0 + 0.318 * r.height,
|
|
28
|
+
r.x0 + 0.955 * r.width, r.y0 + 0.471 * r.height)
|
|
29
|
+
rect = (rect + (-2.5, -2.5, 2.5, 2.5)) & page.rect
|
|
30
|
+
print("will remove:", repr(page.get_textbox(rect)[:200]))
|
|
31
|
+
before = len(page.get_text()), len(page.get_drawings())
|
|
32
|
+
page.add_redact_annot(rect, fill=None)
|
|
33
|
+
page.apply_redactions(images=pymupdf.PDF_REDACT_IMAGE_NONE,
|
|
34
|
+
graphics=pymupdf.PDF_REDACT_LINE_ART_REMOVE_IF_TOUCHED)
|
|
35
|
+
after = len(page.get_text()), len(page.get_drawings())
|
|
36
|
+
print("chars/drawings", before, "->", after)
|
|
37
|
+
doc.save("out.pdf", garbage=3, deflate=True)
|
|
38
|
+
PY
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## The flags are not interchangeable
|
|
42
|
+
|
|
43
|
+
- `fill=None` erases without painting. A fill colour (`fill=(1, 1, 1)`) paints a rectangle, which itself becomes a new drawing and hides the page's own background tint; the drawing count then goes up, not down.
|
|
44
|
+
- `images=PDF_REDACT_IMAGE_NONE` leaves pictures untouched: a scanned region stays visible. Use `PDF_REDACT_IMAGE_PIXELS` to blank the pixels a rectangle covers inside an image, and only use a stronger setting when the user wants the whole image gone.
|
|
45
|
+
- `graphics=PDF_REDACT_LINE_ART_REMOVE_IF_TOUCHED` deletes paths that intersect the rectangle; paths entirely inside it are always removed. `PDF_REDACT_LINE_ART_NONE` keeps all line art, which is right when only text should disappear.
|
|
46
|
+
- Text removal is the default (`PDF_REDACT_TEXT_REMOVE`); keeping the text while erasing its surroundings is almost never what the user means.
|
|
47
|
+
|
|
48
|
+
## Counting the result
|
|
49
|
+
|
|
50
|
+
`removed_chars` is the page-level difference in extracted text; `region_chars_removed` is the text that was inside the rectangle. They differ by the newline or space that joins the removed block to its neighbours, so expect them to be equal or one apart. Document-level totals are the sum over the changed pages. A run that removes fewer characters than the region held means the rectangle missed part of the block — enlarge `--pad` or convert the geometry again.
|
|
51
|
+
|
|
52
|
+
Cross-check on the sample document: page 1, region `{left: 0.064, top: 0.318, width: 0.891, height: 0.153}` deletes the "Latest News" block and nothing else. Measured: 16 drawings → 5, page text 1049 → 879 characters (the block held 169), `leftover_chars_in_rect` 0, and the body paragraph, header, and footer still extract and render.
|
|
53
|
+
|
|
54
|
+
## Leftovers redaction does not cover
|
|
55
|
+
|
|
56
|
+
- **Metadata and embedded files**: the info dictionary and attachments are not page content. Clear them when the user asks for a clean file (see `scripts/metadata.py`), and note that a document ID/xref may still fingerprint the file.
|
|
57
|
+
- **Previous revisions**: incrementally saved files can retain old objects. Saving with `garbage=3, deflate=True` compacts the file; when the user needs forensic certainty, say that the output was rebuilt and confirm with a text search that the words are gone.
|
|
58
|
+
- **Images inside the region**: by default they stay. If the region is a screenshot of the sensitive text, switch to `--images pixels` or rebuild the page.
|
|
59
|
+
|
|
60
|
+
## Verify
|
|
61
|
+
|
|
62
|
+
Render after the redaction and look at the page. The region must be empty, the surrounding text intact, and any ruled boxes that belonged to the block gone rather than half-erased. Then search the output for the removed words:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
"<python>" - <<'PY'
|
|
66
|
+
import pymupdf
|
|
67
|
+
text = pymupdf.open("out.pdf")[0].get_text()
|
|
68
|
+
print("latest news" in text.lower(), len(text))
|
|
69
|
+
PY
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If they still appear, the rectangle missed them — fix the geometry, do not hand the file over.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Stamp a footer, header, or label
|
|
2
|
+
|
|
3
|
+
An overlay is a tiny one-page PDF drawn with reportlab and merged onto the target page, so the original content stream is untouched and existing widgets keep working. Position is in points measured from the bottom-left of the page box; a stamp on an A4 page has 842 points of height to work with.
|
|
4
|
+
|
|
5
|
+
## Fast path: the stamp script
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
"<python>" "<skill>/scripts/stamp_overlay.py" --input "<file.pdf>" --out "<file-stamped.pdf>" \
|
|
9
|
+
--text "Confidential - page {page} of {pages}" --position bottom-center --render-dir "<render>"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`{page}` and `{pages}` are substituted per page. Positions: `bottom-left`, `bottom-center`, `bottom-right`, `top-left`, `top-center`, `top-right`; `--pages "1,3-5"` limits the stamp; `--font-size`, `--font`, `--color`, `--margin`, and `--opacity` control the look. The script merges page by page with pypdf, refuses to overwrite the input, and can render the stamped pages so you can look at them.
|
|
13
|
+
|
|
14
|
+
## Draw it yourself
|
|
15
|
+
|
|
16
|
+
Use reportlab directly when the stamp is a bar, a rotated line, or a block of text:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
"<python>" - <<'PY'
|
|
20
|
+
import io
|
|
21
|
+
from pypdf import PdfReader, PdfWriter
|
|
22
|
+
from reportlab.lib.pagesizes import A4
|
|
23
|
+
from reportlab.lib.colors import HexColor
|
|
24
|
+
from reportlab.pdfgen import canvas
|
|
25
|
+
|
|
26
|
+
reader = PdfReader("in.pdf")
|
|
27
|
+
writer = PdfWriter()
|
|
28
|
+
for index, page in enumerate(reader.pages):
|
|
29
|
+
width = float(page.mediabox.width)
|
|
30
|
+
height = float(page.mediabox.height)
|
|
31
|
+
buffer = io.BytesIO()
|
|
32
|
+
c = canvas.Canvas(buffer, pagesize=(width, height))
|
|
33
|
+
c.setFillColor(HexColor("#c0392b"))
|
|
34
|
+
c.setFont("Helvetica-Bold", 10)
|
|
35
|
+
c.saveState()
|
|
36
|
+
c.translate(width / 2, 18)
|
|
37
|
+
c.drawCentredString(0, 0, f"CONFIDENTIAL | page {index + 1} of {len(reader.pages)}")
|
|
38
|
+
c.restoreState()
|
|
39
|
+
c.save()
|
|
40
|
+
buffer.seek(0)
|
|
41
|
+
page.merge_page(PdfReader(buffer).pages[0])
|
|
42
|
+
writer.add_page(page)
|
|
43
|
+
with open("out.pdf", "wb") as handle:
|
|
44
|
+
writer.write(handle)
|
|
45
|
+
PY
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use the page's own `mediabox` rather than a hard-coded A4 size when the source may mix page sizes; a stamp drawn for A4 lands in the wrong place on a landscape page. `merge_page` paints over the page, so a solid bar hides whatever it covers — place it in a margin or inside a band you intend to cover.
|
|
49
|
+
|
|
50
|
+
## Pitfalls
|
|
51
|
+
|
|
52
|
+
- **Collisions with existing footers**: check the bottom margin first; the render is the proof, not the coordinate math. If the document already has a page number, either skip the stamp or put it on the opposite side.
|
|
53
|
+
- **Rotated pages**: a page with `/Rotate 90` renders sideways; text drawn in the unrotated coordinate system will appear rotated in the viewer. Either rotate the stamp with `c.rotate(90)` and adjust the origin, or rotate the page back first.
|
|
54
|
+
- **Repeated stamping**: merging the same overlay twice doubles the opacity and positions identically; re-run from the original input when iterating.
|
|
55
|
+
- **Fonts**: Helvetica handles Latin; a CJK stamp needs a registered TTF, otherwise the text renders as blank boxes. Check the render.
|
|
56
|
+
- **Opacity**: `setFillAlpha` is applied by viewers that honour transparency groups; a stamp that must be visible on any viewer is safer with full opacity.
|
|
57
|
+
|
|
58
|
+
## Verify
|
|
59
|
+
|
|
60
|
+
Render every stamped page and check: the text sits inside the margin, does not overlap body text or an existing footer, is the size the user asked for, and appears on exactly the selected pages. Then run `"<python>" "<cli>" pdf read --input "<file-stamped.pdf>"` — the stamp's text will join the page text, so a search for the label confirms it landed once, not twice.
|
|
@@ -1,110 +1,164 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: Build, inspect, restyle, and verify PowerPoint .pptx slide decks with python-pptx plus the bundled Office helper CLI.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# Presentations (.pptx)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
A deck is a zip of XML parts: slides, layouts, masters, charts with embedded
|
|
8
|
+
workbooks, notes, media. Everything the audience sees comes from those parts, so
|
|
9
|
+
this pack teaches you to (a) author those parts correctly and (b) read them back
|
|
10
|
+
before delivering.
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
Two layers:
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
- **The helper CLI** — a deterministic accelerator for common cases:
|
|
15
|
+
`<python> "<cli>" pptx {create|read|render}`, plus `office_status`. It creates
|
|
16
|
+
from a JSON spec, reads structure back, and renders. It does not mutate.
|
|
17
|
+
- **This pack** — the capability layer. Recipes for anything the CLI does not
|
|
18
|
+
cover ([`recipes/`](recipes/README.md)), runnable tools for mutation and gates
|
|
19
|
+
in [`container_tools/`](container_tools/), template-following in
|
|
20
|
+
[`template_following_scripts/`](template_following_scripts/).
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
Authoring is `python-pptx`. Not a page-description format, not a screenshot: real
|
|
23
|
+
shapes, real chart parts, real notes.
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
"<python>" "<cli>" pptx create --spec "<spec.json>" --out "<output.pptx>"
|
|
19
|
-
```
|
|
25
|
+
## Before any file work
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
"layout": "blank",
|
|
30
|
-
"title": "Revenue by quarter",
|
|
31
|
-
"table": { "headers": ["Quarter", "Revenue"], "rows": [["Q1", 100], ["Q2", 128]] },
|
|
32
|
-
"chart": {
|
|
33
|
-
"type": "column",
|
|
34
|
-
"title": "Quarterly revenue",
|
|
35
|
-
"left": 3.6,
|
|
36
|
-
"top": 3.5,
|
|
37
|
-
"width": 8.8,
|
|
38
|
-
"height": 3.0,
|
|
39
|
-
"categories": ["Q1", "Q2"],
|
|
40
|
-
"series": [{ "name": "Revenue", "values": [100, 128] }]
|
|
41
|
-
},
|
|
42
|
-
"image": { "path": "<absolute image path>", "left": 0.8, "top": 3.9, "width": 2.2 }
|
|
43
|
-
}
|
|
44
|
-
]
|
|
45
|
-
}
|
|
46
|
-
```
|
|
27
|
+
1. Call the `office_status` tool.
|
|
28
|
+
2. Read the `## Office runtime` JSON block appended to this file and use the
|
|
29
|
+
absolute `python`, `cli`, `skill` and `shared` values from it. Quote paths.
|
|
30
|
+
3. If that block reports no working runtime, stop: tell the user to run the
|
|
31
|
+
`node "..."` remedy command it names, then call `office_status` again. Never
|
|
32
|
+
claim a deck was created or verified without a runtime to create and verify
|
|
33
|
+
it with.
|
|
47
34
|
|
|
48
|
-
|
|
35
|
+
## Delivery
|
|
49
36
|
|
|
50
|
-
|
|
51
|
-
- `blank` / `title_only` place content with a vertical cursor starting at 1.5 in. A table advances the cursor; a chart without `top` starts at the cursor and gets a fitted height; an image without `top` starts at the cursor. Explicit `left`/`top`/`width`/`height` in inches are honored, so use them when combining elements side by side.
|
|
52
|
-
- A blank slide with a `title` gets a generated title textbox (32 pt bold).
|
|
53
|
-
- Chart types: `column`, `line`, `pie` (`bar` is accepted as a column alias). Each series needs a `name` and numeric `values`.
|
|
37
|
+
Always hand the finished file to the user with the `present` tool, then summarise in one sentence what changed. A bare path is not delivery: the chat transcript renders `.docx`/`.pptx`/`.xlsx`/`.pdf` links as "二进制文件,无法显示改动" and the user cannot open them.
|
|
54
38
|
|
|
55
|
-
|
|
39
|
+
- Creating a file: write it into the user's workspace, verify it, then `present`.
|
|
40
|
+
- Editing an existing file: never overwrite the original. Write `<original>-edited.<ext>` (or a name the user asked for), verify it, `present` it, and state both paths.
|
|
56
41
|
|
|
57
|
-
|
|
58
|
-
"<python>" "<cli>" pptx read --input "<file.pptx>"
|
|
59
|
-
```
|
|
42
|
+
## Annotations
|
|
60
43
|
|
|
61
|
-
|
|
44
|
+
The user may reply with an `office-annotations` block: `[{ page, text, requirement, region: { left, top, width, height } }]`. `region` is normalised 0–1 against the rendered preview page the user drew on; `requirement` is free-form natural language. Treat it as a change request, not as editor state: locate what the region covers, apply the smallest edit that satisfies `requirement`, re-render, and confirm the region now looks right.
|
|
62
45
|
|
|
63
|
-
|
|
46
|
+
When the payload cannot be mapped to a concrete object (the region is empty or the file has no geometry), do not guess silently — say what you could not locate.
|
|
64
47
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"core": { "title": "Annual Review (final)" }
|
|
74
|
-
}
|
|
75
|
-
```
|
|
48
|
+
**pptx mapping.** `page` is the preview slide index (1-based, the `slide-NN.png`
|
|
49
|
+
you rendered). The region selects shapes whose bounding box overlaps it:
|
|
50
|
+
`left_in = region.left * slide_width_in`, and likewise for top/width/height.
|
|
51
|
+
`text` disambiguates overlapping shapes (a title over a background panel).
|
|
52
|
+
Resolve candidates from
|
|
53
|
+
`container_tools/inspect_presentation_layout_geometry.py --json-out geometry.json`,
|
|
54
|
+
then apply the edit through a mutation tool or a recipe. Full procedure:
|
|
55
|
+
[tasks/apply-annotations.md](tasks/apply-annotations.md).
|
|
76
56
|
|
|
77
|
-
|
|
57
|
+
## Verify
|
|
78
58
|
|
|
79
|
-
|
|
59
|
+
Render, look at every page, then deliver — in that order, with nothing skipped.
|
|
80
60
|
|
|
81
61
|
```bash
|
|
82
|
-
"<python>" "<
|
|
62
|
+
"<python>" "<skill>/container_tools/render_slides.py" --input "<file.pptx>" --out-dir "<render-dir>"
|
|
83
63
|
```
|
|
84
64
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
65
|
+
`render_slides.py` uses the CLI's `pptx render` — which drives Microsoft
|
|
66
|
+
PowerPoint, WPS Presentation, or LibreOffice, whichever is installed — and falls
|
|
67
|
+
back to a draft preview when none is, so this one command is always the right
|
|
68
|
+
entry point. The JSON names the engine it used in `engine`.
|
|
69
|
+
|
|
70
|
+
Then open each PNG with `read_image` (or build one sheet with
|
|
71
|
+
`"<skill>/template_following_scripts/make_contact_sheet.py"`). Check: titles
|
|
72
|
+
one or two lines, body ≥ 18 pt, nothing clipped or outside the slide, no empty
|
|
73
|
+
placeholder, table and chart not overlapping, chart readable at thumbnail size.
|
|
74
|
+
|
|
75
|
+
If no office suite is installed, `render_slides.py` falls back to
|
|
76
|
+
`--engine draft`, a PyMuPDF geometry preview. It proves placement, not
|
|
77
|
+
typography — say which one you used.
|
|
78
|
+
|
|
79
|
+
Gates that print JSON with a `passed` field, all blocking when they say `false`:
|
|
80
|
+
`inspect_presentation_package_integrity.py`,
|
|
81
|
+
`inspect_presentation_layout_geometry.py`, `native_chart_title_gate.py`,
|
|
82
|
+
`native_quantitative_chart_gate.py`, `native_table_arithmetic_gate.py`,
|
|
83
|
+
`template_source_fidelity_gate.py`. Details:
|
|
84
|
+
[tasks/inspect-and-verify.md](tasks/inspect-and-verify.md).
|
|
85
|
+
|
|
86
|
+
Never present a deck you have not rendered and looked at.
|
|
87
|
+
|
|
88
|
+
## Tasks
|
|
89
|
+
|
|
90
|
+
One file per job:
|
|
91
|
+
|
|
92
|
+
| Job | Start here |
|
|
93
|
+
|---|---|
|
|
94
|
+
| Create a deck | [tasks/create-deck.md](tasks/create-deck.md) |
|
|
95
|
+
| Edit an existing deck | [tasks/edit-deck.md](tasks/edit-deck.md) |
|
|
96
|
+
| Inspect, gate and verify | [tasks/inspect-and-verify.md](tasks/inspect-and-verify.md) |
|
|
97
|
+
| Follow a supplied template | [tasks/template-following.md](tasks/template-following.md) |
|
|
98
|
+
| Charts, tables, speaker notes | [tasks/charts-tables-notes.md](tasks/charts-tables-notes.md) |
|
|
99
|
+
| Apply an annotation payload | [tasks/apply-annotations.md](tasks/apply-annotations.md) |
|
|
100
|
+
|
|
101
|
+
## Tools and recipes
|
|
102
|
+
|
|
103
|
+
Mutations (each prints one JSON object, exits non-zero on failure, defaults to
|
|
104
|
+
`<original>-edited.pptx`):
|
|
105
|
+
|
|
106
|
+
| Need | Command |
|
|
107
|
+
|---|---|
|
|
108
|
+
| Find/replace text, run-level | `"<skill>/container_tools/replace_text.py" --input "<deck>" --find "old" --replace "new"` |
|
|
109
|
+
| Append slides | `"<skill>/container_tools/add_slides.py" --input "<deck>" --spec "<slides.json>"` |
|
|
110
|
+
| Title/author/subject/keywords | `"<skill>/container_tools/core_properties.py" --input "<deck>" --title "..."` |
|
|
111
|
+
| Render (office, else draft) | `"<skill>/container_tools/render_slides.py" --input "<deck>" --out-dir "<dir>"` |
|
|
112
|
+
| Contact sheet / montage | `"<skill>/template_following_scripts/make_contact_sheet.py" --dir "<dir>" --out "<sheet.png>"` |
|
|
113
|
+
|
|
114
|
+
Template flow: `inspect_template_deck.py` → write a frame map →
|
|
115
|
+
`validate_template_plan.py` → `prepare_template_starter_deck.py` →
|
|
116
|
+
`check_template_fidelity.py`.
|
|
117
|
+
|
|
118
|
+
Recipes (paste-runnable, each with the pitfall it avoids):
|
|
119
|
+
[README](recipes/README.md) ·
|
|
120
|
+
[textbox at a coordinate](recipes/textbox-at-coordinate.md) ·
|
|
121
|
+
[auto-layout](recipes/autolayout.md) ·
|
|
122
|
+
[tables](recipes/tables.md) ·
|
|
123
|
+
[native charts](recipes/native-charts.md) ·
|
|
124
|
+
[masters and layouts](recipes/master-and-layouts.md) ·
|
|
125
|
+
[speaker notes](recipes/speaker-notes.md) ·
|
|
126
|
+
[theme tokens](recipes/theme-tokens.md) ·
|
|
127
|
+
[rich text runs](recipes/rich-text-runs.md) ·
|
|
128
|
+
[images](recipes/images.md) ·
|
|
129
|
+
[connectors](recipes/connectors.md).
|
|
130
|
+
|
|
131
|
+
Guidance: [authoring workflow](references/authoring-workflow.md) ·
|
|
132
|
+
[style](references/style-guidelines.md) ·
|
|
133
|
+
[bullets](references/native-bullets.md) ·
|
|
134
|
+
[evidence](references/native-evidence.md) ·
|
|
135
|
+
[templates](references/templates.md) ·
|
|
136
|
+
[finalization](references/finalization.md) ·
|
|
137
|
+
[finance decks](domain_guidance/finance.md) ·
|
|
138
|
+
[Google Slides hand-off](routing/google_slides.md).
|
|
139
|
+
|
|
140
|
+
## Non-negotiables
|
|
141
|
+
|
|
142
|
+
- One idea per slide; a title is a claim, not a label.
|
|
143
|
+
- Body ≥ 18 pt, title ≥ 28 pt, captions ≥ 12 pt.
|
|
144
|
+
- Native charts and tables, never a picture of a spreadsheet.
|
|
145
|
+
- Every placeholder filled or deleted; no "Chart Title", no trailing full stop
|
|
146
|
+
on a heading.
|
|
147
|
+
- Numbers in the deck must agree with the numbers that were supplied.
|
|
148
|
+
- New output lives in the user's workspace; an edit is a new file.
|
|
149
|
+
|
|
150
|
+
## Genuine gaps
|
|
151
|
+
|
|
152
|
+
- No Microsoft Office, WPS Office, or LibreOffice installed → no true render;
|
|
153
|
+
use `--engine draft` and say the check was geometric. Installing any of them
|
|
154
|
+
fixes it.
|
|
155
|
+
- Fonts the user's machine lacks are substituted by any renderer; the pack
|
|
156
|
+
cannot install fonts.
|
|
157
|
+
- Google Slides import fidelity is out of our control; see
|
|
158
|
+
[routing/google_slides.md](routing/google_slides.md).
|
|
159
|
+
|
|
160
|
+
## Shared workflow
|
|
161
|
+
|
|
162
|
+
Use `"<shared>/common-workflow.md"` for the cross-format delivery and
|
|
163
|
+
verification loop, and `"<shared>/annotation-recipes.md"` when turning an
|
|
164
|
+
annotation payload into an edit.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Append slides to an existing deck using the shared slide builder.
|
|
2
|
+
|
|
3
|
+
The slide builder lives in `python/office_cli.py` (`_pptx_add_slide`), so this
|
|
4
|
+
tool imports it instead of reimplementing titles, bullet bodies, tables, charts
|
|
5
|
+
and pictures. Anything `pptx create --spec` accepts inside `slides[]` is
|
|
6
|
+
accepted here.
|
|
7
|
+
|
|
8
|
+
Usage
|
|
9
|
+
-----
|
|
10
|
+
<python> add_slides.py --input deck.pptx --spec extra-slides.json
|
|
11
|
+
<python> add_slides.py --input deck.pptx --slide '{"layout":"bullets","title":"Next"}' --out deck-2.pptx
|
|
12
|
+
|
|
13
|
+
Exit codes
|
|
14
|
+
----------
|
|
15
|
+
0 slides appended
|
|
16
|
+
2 bad arguments, invalid spec, or a slide that the builder rejected
|
|
17
|
+
3 the output file could not be written
|
|
18
|
+
|
|
19
|
+
The JSON report on stdout is `{ok, input, output, added, slides, counts}`.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import json
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
# Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
|
|
29
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
30
|
+
try:
|
|
31
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
32
|
+
except (AttributeError, ValueError):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
PACKAGE_ROOT = Path(__file__).resolve().parents[4]
|
|
36
|
+
CLI_DIR = PACKAGE_ROOT / "python"
|
|
37
|
+
COUNTERS = ("slides", "bullets", "tables", "charts", "images")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _emit(payload: dict, code: int = 0) -> int:
|
|
41
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
42
|
+
return code
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _load_slides(spec_path: str | None, inline: list[str]) -> list[dict]:
|
|
46
|
+
slides: list[dict] = []
|
|
47
|
+
if spec_path:
|
|
48
|
+
data = json.loads(Path(spec_path).read_text(encoding="utf-8"))
|
|
49
|
+
if isinstance(data, dict):
|
|
50
|
+
slides.extend(data.get("slides") or [])
|
|
51
|
+
elif isinstance(data, list):
|
|
52
|
+
slides.extend(data)
|
|
53
|
+
else:
|
|
54
|
+
raise ValueError("spec must be an object with 'slides' or a list of slides")
|
|
55
|
+
for raw in inline:
|
|
56
|
+
slides.append(json.loads(raw))
|
|
57
|
+
for slide in slides:
|
|
58
|
+
if not isinstance(slide, dict):
|
|
59
|
+
raise ValueError(f"slide must be an object: {slide!r}")
|
|
60
|
+
return slides
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def main() -> int:
|
|
64
|
+
parser = argparse.ArgumentParser(
|
|
65
|
+
description="Append slides to a .pptx with the same builder the helper CLI uses.",
|
|
66
|
+
epilog="Exit codes: 0 appended, 2 bad input, 3 write failure.",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument("--input", required=True, help="deck to extend")
|
|
69
|
+
parser.add_argument("--spec", help="JSON file: {slides:[...]} or a list of slides")
|
|
70
|
+
parser.add_argument("--slide", action="append", default=[], help="one slide as inline JSON (repeatable)")
|
|
71
|
+
parser.add_argument("--out", help="output deck (default: <input>-edited.pptx)")
|
|
72
|
+
args = parser.parse_args()
|
|
73
|
+
|
|
74
|
+
source = Path(args.input)
|
|
75
|
+
if not source.is_file():
|
|
76
|
+
return _emit({"ok": False, "error": f"no such file: {source}"}, 2)
|
|
77
|
+
output = Path(args.out) if args.out else source.with_name(f"{source.stem}-edited{source.suffix}")
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
slides = _load_slides(args.spec, args.slide)
|
|
81
|
+
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
82
|
+
return _emit({"ok": False, "error": f"bad spec: {error}"}, 2)
|
|
83
|
+
if not slides:
|
|
84
|
+
return _emit({"ok": False, "error": "nothing to add: pass --spec or --slide"}, 2)
|
|
85
|
+
|
|
86
|
+
if not CLI_DIR.is_dir():
|
|
87
|
+
return _emit({"ok": False, "error": f"helper CLI not found at {CLI_DIR}"}, 2)
|
|
88
|
+
sys.path.insert(0, str(CLI_DIR))
|
|
89
|
+
try:
|
|
90
|
+
import office_cli # noqa: PLC0415 - optional dependency, resolved at run time
|
|
91
|
+
except ImportError as error:
|
|
92
|
+
return _emit({"ok": False, "error": f"cannot import the slide builder: {error}"}, 2)
|
|
93
|
+
|
|
94
|
+
from pptx import Presentation # noqa: PLC0415
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
prs = Presentation(str(source))
|
|
98
|
+
except Exception as error: # noqa: BLE001
|
|
99
|
+
return _emit({"ok": False, "error": f"cannot open {source}: {error}"}, 2)
|
|
100
|
+
|
|
101
|
+
before = len(prs.slides)
|
|
102
|
+
counters = {key: 0 for key in COUNTERS}
|
|
103
|
+
added: list[dict] = []
|
|
104
|
+
for slide_spec in slides:
|
|
105
|
+
try:
|
|
106
|
+
slide = office_cli._pptx_add_slide(prs, slide_spec, counters) # noqa: SLF001
|
|
107
|
+
except Exception as error: # noqa: BLE001 - report which slide failed
|
|
108
|
+
return _emit(
|
|
109
|
+
{"ok": False, "error": f"slide {len(added) + 1} rejected: {error}", "added": added},
|
|
110
|
+
2,
|
|
111
|
+
)
|
|
112
|
+
added.append(
|
|
113
|
+
{
|
|
114
|
+
"position": len(prs.slides),
|
|
115
|
+
"layout": str(slide_spec.get("layout", "bullets")),
|
|
116
|
+
"title": slide_spec.get("title"),
|
|
117
|
+
"placeholder_count": len(slide.placeholders),
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
prs.save(str(output))
|
|
124
|
+
except Exception as error: # noqa: BLE001
|
|
125
|
+
return _emit({"ok": False, "error": f"cannot write {output}: {error}"}, 3)
|
|
126
|
+
|
|
127
|
+
return _emit(
|
|
128
|
+
{
|
|
129
|
+
"schema": "add_slides/1",
|
|
130
|
+
"ok": True,
|
|
131
|
+
"input": str(source),
|
|
132
|
+
"output": str(output),
|
|
133
|
+
"added": len(added),
|
|
134
|
+
"before": before,
|
|
135
|
+
"slides": len(prs.slides),
|
|
136
|
+
"counts": counters,
|
|
137
|
+
"details": added,
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
sys.exit(main())
|