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,53 @@
|
|
|
1
|
+
# Templates and style packs
|
|
2
|
+
|
|
3
|
+
**Goal.** Make a new or existing document look like the house template, without copying the template's content into it.
|
|
4
|
+
|
|
5
|
+
A `.dotx` is a `.docx` whose package is a template: `styles.xml` defines the look, `numbering.xml` defines lists, and the body is empty. Copying the body would import placeholder text; what you actually need is the styles part, the numbering definitions they reference, and the theme.
|
|
6
|
+
|
|
7
|
+
## Ask before guessing
|
|
8
|
+
|
|
9
|
+
Do not invent a house style. Get from the user: heading fonts and sizes, body font and size, accent colour, margins, table style, and whether a logo or cover page is required. `references/template-elicitation.md` has the question set that produces a spec.
|
|
10
|
+
|
|
11
|
+
## Apply a template
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
"<python>" "<skill>/scripts/apply_template_styles.py" --template "<house.dotx>" --target "<draft.docx>" --out "<draft-styled.docx>"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The script merges the template's style definitions by `styleId` (replacing same-id definitions, appending missing ones, taking the template's `docDefaults`), and copies `numbering.xml` with its relationship and content-type override. The target's body is untouched, so the content survives and the look changes.
|
|
18
|
+
|
|
19
|
+
Then normalize so direct formatting does not override the template:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
"<python>" "<skill>/scripts/style_normalize.py" "<draft-styled.docx>" --out "<draft-final.docx>" --clear_paragraph_format
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Restyle without a template
|
|
26
|
+
|
|
27
|
+
When the user describes the style instead of supplying a file, set it on the styles rather than on runs:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
"<python>" - <<'PY'
|
|
31
|
+
from docx import Document
|
|
32
|
+
from docx.shared import Pt, RGBColor
|
|
33
|
+
d = Document(r"<draft.docx>")
|
|
34
|
+
normal = d.styles["Normal"]
|
|
35
|
+
normal.font.name = "Calibri"
|
|
36
|
+
normal.font.size = Pt(11)
|
|
37
|
+
for level, size in ((1, 18), (2, 14), (3, 12)):
|
|
38
|
+
style = d.styles[f"Heading {level}"]
|
|
39
|
+
style.font.size = Pt(size)
|
|
40
|
+
style.font.color.rgb = RGBColor(0, 0, 0)
|
|
41
|
+
d.save(r"<draft-styled.docx>")
|
|
42
|
+
PY
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Verify
|
|
46
|
+
|
|
47
|
+
Render and compare against the template's own preview page by page: heading sizes, body leading, margins, table borders. A style page that looks right in a style list but wrong on the page usually means a direct-formatting run left over — run the lint.
|
|
48
|
+
|
|
49
|
+
## Pitfalls
|
|
50
|
+
|
|
51
|
+
- A template applied over content that references missing styles produces the fallback look, not an error; run `style_lint.py` to find undefined references.
|
|
52
|
+
- Themes (`word/theme/theme1.xml`) carry fonts and accent colours; a style that names a theme font changes when the theme does.
|
|
53
|
+
- Keeping the template's `numbering.xml` without its `abstractNum` ids produces lists that all share one counter.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Table of contents
|
|
2
|
+
|
|
3
|
+
**Goal.** Put a navigable contents list at the front of the document, built from heading styles rather than typed by hand.
|
|
4
|
+
|
|
5
|
+
The TOC is a `TOC` field. Word fills it in when it recalculates; the file stores the last result. A hand-typed list looks identical in a screenshot and then goes wrong on the first edit.
|
|
6
|
+
|
|
7
|
+
## Insert
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<skill>/scripts/insert_toc.py" "<file.docx>" --out "<file-toc.docx>" --levels 1-3
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The script looks for a placeholder paragraph containing `[[TOC]]` (override with `--placeholder`). If there is none it inserts the field at the start of the body. It writes `TOC \o "1-3" \h \z \u`, sets `w:updateFields`, and adds a short cached hint so the page is not blank before recalculation.
|
|
14
|
+
|
|
15
|
+
Check that every heading you want in the list uses a heading style: `heading_audit.py` finds headings formatted by hand, which the TOC will silently skip.
|
|
16
|
+
|
|
17
|
+
## Rebuild after edits
|
|
18
|
+
|
|
19
|
+
If headings changed, the cached list is stale. Either ask the user to update the field in Word (right-click → Update Field), or regenerate: strip the old field with `fields_materialize.py`? — no, that freezes it. Instead delete the field paragraphs and run `insert_toc.py` again. Freezing a TOC is almost never what a reader wants.
|
|
20
|
+
|
|
21
|
+
## Static contents list
|
|
22
|
+
|
|
23
|
+
Sometimes the requirement really is a fixed list — a PDF export that will not be recalculated, or a template with a hand-built contents page. `internal_nav.py` builds one: a heading plus hyperlink entries to every heading, with optional back-links.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
"<python>" "<skill>/scripts/internal_nav.py" "<file.docx>" --out "<file-nav.docx>" --levels 1 2
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
See `tasks/navigation_internal_links.md`.
|
|
30
|
+
|
|
31
|
+
## Verify
|
|
32
|
+
|
|
33
|
+
Render the first pages and read them. A TOC that shows one line of grey hint text means the field never recalculated in that renderer; a TOC that shows the old headings means the document was rendered before the field updated. Say which one happened instead of claiming the TOC is correct.
|
|
34
|
+
|
|
35
|
+
## Pitfalls
|
|
36
|
+
|
|
37
|
+
- TOC entries come from outline levels, not from text that merely looks like a heading.
|
|
38
|
+
- A TOC inside a header or a text box does not work; it belongs in the body.
|
|
39
|
+
- Level 4+ headings are excluded unless you widen `--levels`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Visual verification
|
|
2
|
+
|
|
3
|
+
**Goal.** See the pages before the user does. A document that parses and a document that looks right are different claims.
|
|
4
|
+
|
|
5
|
+
## Render
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
"<python>" "<cli>" docx render --input "<file.docx>" --out-dir "<render-dir>"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Writes a PDF and one PNG per page, and prints their paths plus the renderer it used (Word via COM, or LibreOffice). Use the same command through `render_docx.py` when you want the JSON report and page list:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
"<python>" "<skill>/render_docx.py" "<file.docx>" --output_dir "<render-dir>" --dpi 150
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Inspect
|
|
18
|
+
|
|
19
|
+
Open every PNG with `read_image` and check, in this order:
|
|
20
|
+
|
|
21
|
+
1. **Hierarchy** — does the page show the structure the user asked for, at a glance?
|
|
22
|
+
2. **Fit** — nothing past the right margin, no clipped table column, no text over a watermark.
|
|
23
|
+
3. **Breaks** — no heading alone at the bottom, no orphaned caption, no half-empty page before a section break.
|
|
24
|
+
4. **Text** — the words that were meant to change did change, and nothing else moved.
|
|
25
|
+
5. **Glyphs** — dashes, quotes, CJK, currency symbols and maths render, not as boxes.
|
|
26
|
+
6. **Header and footer** — present, correct, not repeated on a first page that should not have it.
|
|
27
|
+
|
|
28
|
+
## When no renderer exists
|
|
29
|
+
|
|
30
|
+
The runtime reports `renderers.libreoffice: null` and no Office installation. `render` then exits non-zero with a JSON error naming the missing renderer, and `render_docx.py` exits 3 with `reason: no_renderer`.
|
|
31
|
+
|
|
32
|
+
In that state:
|
|
33
|
+
|
|
34
|
+
- Verify structure with `docx read`, `docx_kit.py parts|text|xml` and the audit scripts.
|
|
35
|
+
- Tell the user plainly that layout was not visually verified, and offer to install Microsoft Office, WPS Office, or LibreOffice instead of describing a page you did not see.
|
|
36
|
+
- Do not claim a file "looks good" from XML evidence alone.
|
|
37
|
+
|
|
38
|
+
## After any script edit
|
|
39
|
+
|
|
40
|
+
Re-render. Package-valid is not page-valid: a caption inserted before the paragraph it describes, a table grown past the margin, a style merge that changed heading sizes — all are invisible to the XML and obvious in the image.
|
|
41
|
+
|
|
42
|
+
## Comparing revisions
|
|
43
|
+
|
|
44
|
+
`render_and_diff.py` renders two files and writes a heatmapped difference page. Read the diff images yourself; a ratio is not a conclusion.
|
|
45
|
+
|
|
46
|
+
## Pitfalls
|
|
47
|
+
|
|
48
|
+
- Different renderers paginate differently. Compare like with like, and do not promise Word-perfect pagination from a LibreOffice render.
|
|
49
|
+
- A render fails silently into a blank page when a font is missing; a blank page is a finding, not a rendering bug.
|
|
50
|
+
- Render into a fresh directory; stale PNGs from an earlier run are the classic false pass.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Watermarks and background text
|
|
2
|
+
|
|
3
|
+
**Goal.** A diagonal DRAFT / CONFIDENTIAL mark behind the page content, and the ability to find and remove one that a template left behind.
|
|
4
|
+
|
|
5
|
+
A Word watermark is not a document feature with its own element: it is a VML shape (`v:shape` with `v:textpath`) in a header part, positioned behind the text. That is why it disappears if the header is replaced, and why it can appear on some pages and not others when sections have their own headers.
|
|
6
|
+
|
|
7
|
+
## Add
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<skill>/scripts/watermark_add.py" "<file.docx>" --out "<file-draft.docx>" --text "DRAFT"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Writes the shape into the default header, light grey, rotated -45°, behind the body. The script refuses to add a second watermark when it finds one — remove the old one first, or you get two overlapping marks.
|
|
14
|
+
|
|
15
|
+
For a per-page mark across a document with several sections, each section's header needs the shape; that is a loop over sections, not one insertion.
|
|
16
|
+
|
|
17
|
+
## Find and remove
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
"<python>" "<skill>/scripts/watermark_audit_remove.py" "<file.docx>" --mode report
|
|
21
|
+
"<python>" "<skill>/scripts/watermark_audit_remove.py" "<file.docx>" --mode remove --out "<file-clean.docx>"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The report lists the text of every VML text path and the part it lives in, and exits 1 when any watermark exists. `--contains` narrows to a substring, which is the case "the template says Company Confidential and it should not". Remove deletes the enclosing paragraph from the header.
|
|
25
|
+
|
|
26
|
+
## Verify
|
|
27
|
+
|
|
28
|
+
Render and read the pages: the mark sits behind the text (not over it), stays inside the page, and appears on every page the user expects. A watermark that vanishes on page two means that section has a different header.
|
|
29
|
+
|
|
30
|
+
## Pitfalls
|
|
31
|
+
|
|
32
|
+
- A watermark in a "different first page" header only shows on page one.
|
|
33
|
+
- Removing the header paragraph can leave an empty header that adds vertical space; check the top margin on the rendered page.
|
|
34
|
+
- An image watermark is an `v:imagedata` shape, not a text path — the report will not name it, so inspect the header XML when the visual mark is a logo.
|
|
35
|
+
- Watermarks are not security. See `tasks/protection_restrict_editing.md` for what actually restricts editing.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Rendering setup and failures
|
|
2
|
+
|
|
3
|
+
## First: does this machine render
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
"<python>" "<cli>" status
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Look at `renderers.libreoffice` and `microsoft_office`. Both null means `docx render` will fail — not with a corrupt file, but with `no document renderer found`. That is a setup problem, not a document problem.
|
|
10
|
+
|
|
11
|
+
## LibreOffice
|
|
12
|
+
|
|
13
|
+
Install LibreOffice (Windows: winget `TheDocumentFoundation.LibreOffice`), then check:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
soffice --headless --version
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
If the binary is not on `PATH`, the runtime cannot find it; either add its `program` directory to `PATH` or install into the default location the runtime probes. On Windows, `soffice.com` is the console wrapper — use that one, not `soffice.exe`, when a script needs to wait for exit.
|
|
20
|
+
|
|
21
|
+
## Microsoft Office
|
|
22
|
+
|
|
23
|
+
When Word is installed, the CLI can drive it through COM and does not need LibreOffice. Office must be activated and able to launch without a first-run dialog; a pending activation or a modal "sign in" blocks the render forever.
|
|
24
|
+
|
|
25
|
+
## Common failures
|
|
26
|
+
|
|
27
|
+
| Message | Cause | Fix |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `no document renderer found` | Neither installed / not on PATH | Install one, re-check `status` |
|
|
30
|
+
| Render hangs | A dialog is waiting in the headless process | Kill the leftover process, add the `--headless --norestore` flags the CLI uses |
|
|
31
|
+
| Blank pages | Missing font | Install the font or substitute it |
|
|
32
|
+
| Text shifted | Font substitution | Compare with a render from the same renderer the user will use |
|
|
33
|
+
| `soffice already running` | A user profile lock from a crashed run | Remove the stale lock file under the LibreOffice user profile, or wait for the old process to exit |
|
|
34
|
+
|
|
35
|
+
## Working without a renderer
|
|
36
|
+
|
|
37
|
+
Structure, text and metadata are all verifiable headlessly (`docx read`, the audit scripts, `docx_kit.py parts|text|xml`). Layout is not. In that state:
|
|
38
|
+
|
|
39
|
+
1. Do the structural verification.
|
|
40
|
+
2. Tell the user that page layout was not visually checked, and why.
|
|
41
|
+
3. Offer the install, or ask them to open the file and say what looks wrong.
|
|
42
|
+
|
|
43
|
+
Never describe a page you did not render.
|
|
44
|
+
|
|
45
|
+
## Keep renders disposable
|
|
46
|
+
|
|
47
|
+
Render into a fresh temp directory each time (`--out-dir`). A stale PNG from a previous run reads as a pass and is the most common false confirmation.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Splitting a long job
|
|
2
|
+
|
|
3
|
+
A twenty-section document is not one command. Splitting it into bounded steps keeps each change verifiable and stops one failure from hiding inside a large edit.
|
|
4
|
+
|
|
5
|
+
## One step per structural change
|
|
6
|
+
|
|
7
|
+
1. **Plan** — the outline (headings and what each section must establish), written down before any file work.
|
|
8
|
+
2. **Skeleton** — create the file with the outline only: heading blocks, no body text.
|
|
9
|
+
3. **Content, in batches** — a batch per top-level section, appended with `append_blocks.py`.
|
|
10
|
+
4. **Numbers** — captions, cross-references, TOC once the structure is final, never while sections are still moving.
|
|
11
|
+
5. **Verify** — render, inspect, fix.
|
|
12
|
+
|
|
13
|
+
## Why this order
|
|
14
|
+
|
|
15
|
+
- Captions and TOC are fields over structure; running them after every batch means renumbering noise and a TOC that points at deleted headings.
|
|
16
|
+
- A batch of one section is small enough to redo when the layout is wrong.
|
|
17
|
+
- The skeleton proves the heading hierarchy before the text makes it expensive to change.
|
|
18
|
+
|
|
19
|
+
## Batch boundaries
|
|
20
|
+
|
|
21
|
+
- One section per file: `section-01.docx`, `section-02.docx`, then `merge_docx_append.py`. This is the only sane way to draft in parallel or across sessions.
|
|
22
|
+
- Keep a `manifest.json` listing section files in order. Merging is then mechanical and the order is not remembered from chat.
|
|
23
|
+
- Merge before numbering, not after.
|
|
24
|
+
|
|
25
|
+
## When a batch fails
|
|
26
|
+
|
|
27
|
+
Do not patch a large edit inside a large document. Discard the failed batch's output, fix the batch, re-apply it to the previous good file. Keep the previous good file until the batch is verified.
|
|
28
|
+
|
|
29
|
+
## Context hygiene
|
|
30
|
+
|
|
31
|
+
- Long sessions lose the document design. Keep the plan and the design defaults (page size, font sizes, table borders) in a file in the workspace and re-read it, rather than trusting recollection.
|
|
32
|
+
- When the user changes a decision mid-job (page size, heading style), record it at the point they said it, and apply it to batches already written.
|
|
33
|
+
|
|
34
|
+
## Smoke test after the join
|
|
35
|
+
|
|
36
|
+
`examples/end_to_end_smoke_test.md` is the checklist to run on the merged file before it becomes a deliverable.
|
|
@@ -1,86 +1,103 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Create,
|
|
2
|
+
description: Create, inspect, edit, redact, stamp, and render PDF files with the bundled Office helper CLI and self-contained Python scripts.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# PDF
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
A PDF page is a content stream of text-showing and path-painting operators behind a page box. Every job in this pack either reads that stream (PyMuPDF, pdfplumber, pypdf), writes a new one (reportlab), or rasterises the result so you can look at it. Work from the mechanics outward: inspect first, choose the smallest operation that changes what the user asked for, then prove the change on a render. The helper CLI and the scripts are fast paths, not limits — each task file carries a runnable snippet for jobs they do not cover yet.
|
|
8
|
+
|
|
9
|
+
| User job | What actually happens | Fast path |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Look at a page, crop, or measure geometry | Rasterise the page box with PyMuPDF at a DPI, read page size, rotation, text, drawings, widgets | `scripts/render_pages.py`, `pdf render` |
|
|
12
|
+
| Extract text or tables | pdfplumber reads characters and clusters table rules per page | `scripts/extract_text.py`, `pdf read` |
|
|
13
|
+
| Inspect or fill an AcroForm | pypdf walks `/AcroForm` widgets, then rebuilds each `/AP` appearance from `/V` | `scripts/forms.py`, snippet in `tasks/forms.md` |
|
|
14
|
+
| Remove a region (redaction) | PyMuPDF redaction annotations plus `apply_redactions` strip text and line art inside a rect | `scripts/redact_region.py`, snippet in `tasks/redact.md` |
|
|
15
|
+
| Read or rewrite metadata | The document info dictionary; producer keys cannot change page content | `scripts/metadata.py` |
|
|
16
|
+
| Merge, split, reorder, rotate, delete pages | Page-tree rearrangement; content is copied, not re-flowed | `scripts/page_ops.py`, `scripts/split_pages.py`, snippet in `tasks/edit-pages.md` |
|
|
17
|
+
| Stamp a footer, header, or banner | reportlab draws an overlay page, pypdf merges it over the target pages | `scripts/stamp_overlay.py` |
|
|
18
|
+
| Build a new document | reportlab draws primitives and text into a page | `pdf create`, `tasks/create.md` |
|
|
8
19
|
|
|
9
20
|
## Before any file work
|
|
10
21
|
|
|
11
22
|
1. Call the `office_status` tool.
|
|
12
|
-
2.
|
|
13
|
-
3. If
|
|
23
|
+
2. Read the `## Office runtime` JSON block at the end of this file and use its `python`, `cli`, `skill`, and `shared` values verbatim, quoted: `"<python>"`, `"<cli>"`, `"<skill>/scripts/<name>.py"`.
|
|
24
|
+
3. If that block reports no working runtime, stop. Give the user the `node "..."` remedy command it names, then call `office_status` again. Never fabricate a PDF result you could not verify.
|
|
14
25
|
|
|
15
|
-
|
|
26
|
+
Scripts under `"<skill>/scripts/"` are standalone argparse CLIs. Run `<python> <script> --help` before using one you have not called in this session.
|
|
16
27
|
|
|
17
|
-
|
|
18
|
-
"<python>" "<cli>" pdf create --spec "<spec.json>" --out "<output.pdf>"
|
|
19
|
-
```
|
|
28
|
+
## Route selection
|
|
20
29
|
|
|
21
|
-
|
|
22
|
-
{
|
|
23
|
-
"title": "Product Brief",
|
|
24
|
-
"author": "DSH Office Suite",
|
|
25
|
-
"page_size": "A4",
|
|
26
|
-
"blocks": [
|
|
27
|
-
{ "type": "heading", "level": 1, "text": "Product Brief" },
|
|
28
|
-
{ "type": "paragraph", "text": "This PDF contains Chinese, English, numbers, and punctuation." },
|
|
29
|
-
{ "type": "bullets", "items": ["Fast", "Local", "Verifiable"] },
|
|
30
|
-
{ "type": "table", "headers": ["Item", "Value"], "rows": [["Version", "1.0"], ["Status", "Draft"]] },
|
|
31
|
-
{ "type": "image", "path": "<absolute image path>", "width": 4.5 },
|
|
32
|
-
{ "type": "page_break" }
|
|
33
|
-
]
|
|
34
|
-
}
|
|
35
|
-
```
|
|
30
|
+
Decide the route before you type a command. Every route ends in the same render check.
|
|
36
31
|
|
|
37
|
-
|
|
32
|
+
- **The file does not exist** → create it from a `pdf create` spec, or with reportlab when the layout does not fit the block model (`tasks/create.md`).
|
|
33
|
+
- **The file exists and must change** → read it first (`pdf read`, and a render), then mutate it with the script named in the task file. The helper CLI has no edit verb: it creates, reads, renders, and reports status only; every change is a `scripts/*.py` call.
|
|
34
|
+
- **A script covers the job** → run it, then verify its JSON report and its rendered pages. Scripts refuse to overwrite their input and exit non-zero on a failed self-check; treat both as hard stops.
|
|
35
|
+
- **A read/render verb covers it** → `pdf read`, `pdf render`, or `pdf status`, then verify in the task's own way. Never route a mutation through the CLI.
|
|
36
|
+
- **Neither covers it** → run the runnable snippet in the matching `tasks/*.md` file, or adapt it. Do not invent a CLI verb, and do not tell the user the job is impossible because no verb exists.
|
|
37
|
+
- **Choose the smallest operation** that satisfies the request: metadata change before rebuild, page move before re-creation, one-line patch before re-typing the page.
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
## Failure handling
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
| Symptom | Meaning | Next step |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| `{"ok": false, ...}` and non-zero exit | Invalid argument, missing file, or a refused overwrite | Read the `error` field, fix what it names, re-run. Never strip a step to make it pass. |
|
|
44
|
+
| `encrypted` in the error | The file needs a password | Ask the user for a decrypted copy; do not guess passwords or write one into a command. |
|
|
45
|
+
| `no document renderer found: install LibreOffice, Microsoft Office, or WPS Office` | Media conversion for docx/pptx/xlsx is unavailable | PDF rendering still works (PyMuPDF); for a PDF this is harmless. For other formats report that appearance could not be checked. |
|
|
46
|
+
| `leftover_chars_in_rect` greater than 0 | The redaction rectangle missed text | Enlarge `--pad` or reconvert the region; re-render before delivering. |
|
|
47
|
+
| `not_applied` is not empty | A form value did not persist after save | Treat as a failure: check the field name, its allowed states, and whether the widget has an appearance stream. |
|
|
48
|
+
| `chars: 0` on a page that shows text | Scanned page, no text layer | Say so; follow `references/limits.md` for the OCR situation. Do not return an empty extraction as success. |
|
|
49
|
+
| Render shows the change, `pdf read` does not | The edit landed in pixels only | Fix the content stream or note the limitation; a visible-only change usually means a cover-up, not an edit. |
|
|
42
50
|
|
|
43
|
-
|
|
44
|
-
"<python>" "<cli>" pdf read --input "<file.pdf>"
|
|
45
|
-
```
|
|
51
|
+
## Quality rules
|
|
46
52
|
|
|
47
|
-
|
|
53
|
+
- One heading scale, one body size, and one margin system per document (`references/layout-quality.md`).
|
|
54
|
+
- Prove every structural change on a render of the changed page, not only on a text dump.
|
|
55
|
+
- Keep page geometry consistent when merging or stamping: use each page's own box.
|
|
56
|
+
- Do not claim redaction while the words are still extractable.
|
|
57
|
+
- Preserve the user's original: write `<original>-edited.pdf`, never over it.
|
|
58
|
+
- Mark deliberate shortcuts (a rebuild instead of a reflow, a stamped patch instead of a content edit) in the reply, so the user knows what kind of change they received.
|
|
59
|
+
- Deliver with `present`, then one sentence about what changed and where the file is.
|
|
48
60
|
|
|
49
|
-
##
|
|
61
|
+
## Delivery
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
"<python>" "<cli>" pdf edit --input "<file.pdf>" --spec "<edit.json>" --out "<file-edited.pdf>"
|
|
53
|
-
```
|
|
63
|
+
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
64
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
```
|
|
65
|
+
- Creating a file: write it into the user's workspace, verify it, then `present`.
|
|
66
|
+
- 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.
|
|
67
|
+
|
|
68
|
+
## Annotations
|
|
69
|
+
|
|
70
|
+
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.
|
|
71
|
+
|
|
72
|
+
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
73
|
|
|
65
|
-
- `
|
|
66
|
-
- `rotate` is degrees clockwise; `pages` limits it to zero-based page indexes.
|
|
67
|
-
- `remove_pages` is zero-based and applied to the original pages; merged pages are appended after.
|
|
68
|
-
- Metadata keys may be plain (`Title`) or PDF names (`/Title`); the CLI normalizes them.
|
|
74
|
+
For PDF the `page` value is the 1-based page of the file itself, and `region × page.rect` is the real place to operate on: convert once with PyMuPDF and use the device rect directly. `text` is the words under the mark and wins over the region when they disagree; `imageName`, when present, is a screenshot of the marked area that you can open with `read_image`. Full resolution recipe: `tasks/annotations.md` and `"<shared>/annotation-recipes.md"`.
|
|
69
75
|
|
|
70
|
-
|
|
76
|
+
## Verify
|
|
71
77
|
|
|
72
|
-
|
|
78
|
+
Render before you present, and again after every fix:
|
|
73
79
|
|
|
74
80
|
```bash
|
|
75
81
|
"<python>" "<cli>" pdf render --input "<file.pdf>" --out-dir "<render-dir>" --dpi 110
|
|
76
82
|
```
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
Open each PNG with `read_image`, and compare it with the same page rendered before the change. Look for text clipped at the page box, overlapping stamps or form fields, fonts replaced by boxes, rotated pages that now sit sideways, tables spilling past the margin, and scanned pages whose text is an image. Then run `"<python>" "<cli>" pdf read --input "<file.pdf>"` and confirm the text, page count, and form fields match what you edited. Two or three render/patch passes are normal; if a defect survives three, say what still looks wrong instead of calling it fixed.
|
|
85
|
+
|
|
86
|
+
## Tasks
|
|
87
|
+
|
|
88
|
+
| Task | Read |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| Create a PDF from a spec or reportlab | `tasks/create.md` |
|
|
91
|
+
| Extract text, tables, or page images | `tasks/read.md` |
|
|
92
|
+
| Inspect, fill, or flatten an AcroForm | `tasks/forms.md` |
|
|
93
|
+
| Remove a region of a page | `tasks/redact.md` |
|
|
94
|
+
| Merge, split, reorder, rotate, delete pages | `tasks/edit-pages.md` |
|
|
95
|
+
| Stamp a footer, header, or label | `tasks/stamp.md` |
|
|
96
|
+
| Resolve an `office-annotations` region | `tasks/annotations.md` |
|
|
97
|
+
| Decide what a PDF cannot do | `references/limits.md` |
|
|
98
|
+
|
|
99
|
+
`references/geometry.md` explains page boxes and normalised regions, `references/layout-quality.md` is the pre-delivery visual checklist, `references/form-fields.md` covers field flags, appearance streams, and flattening.
|
|
79
100
|
|
|
80
|
-
##
|
|
101
|
+
## Shared workflow
|
|
81
102
|
|
|
82
|
-
-
|
|
83
|
-
- Filling, creating, or flattening AcroForms (field identifiers are reported for inspection only).
|
|
84
|
-
- Digital signatures, encryption/password handling, redaction, annotations, bookmarks editing.
|
|
85
|
-
- PDF/A, tagged-PDF accessibility repair, or exact reflow of an existing document.
|
|
86
|
-
- Google Drive or other online PDF services.
|
|
103
|
+
Follow `"<shared>/common-workflow.md"` for the create-or-edit decision, the failure ladder, and the edit naming rule, and `"<shared>/annotation-recipes.md"` for the payload shapes the client sends.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Form field mechanics
|
|
2
|
+
|
|
3
|
+
## Field tree
|
|
4
|
+
|
|
5
|
+
`/AcroForm` in the catalog holds `/Fields`, the root field array. A field dictionary has:
|
|
6
|
+
|
|
7
|
+
- `/T` — partial name; the full name is the partial names of ancestors joined with dots.
|
|
8
|
+
- `/FT` — field type, inherited: `Tx` text, `Btn` button, `Ch` choice, `Sig` signature.
|
|
9
|
+
- `/V` — current value. Text fields store a string; buttons store the selected state name (`/Yes`, `/Off`, `/basic`); choice fields store a string or an array of strings; signature fields store a dictionary.
|
|
10
|
+
- `/Ff` — flags bitmask, inherited. Bit 1 (`1`) read-only, bit 2 (`2`) required, bit 13 (`4096`) multiselect for choices, bits 15–17 (`65536`/`131072`/`262144`) button behaviour (no-toggle, radio, pushbutton).
|
|
11
|
+
- `/DA` — default appearance string; controls the font and colour an appearance generator uses.
|
|
12
|
+
- `/Kids` — child fields or widgets.
|
|
13
|
+
- `/AP` — appearance dictionary: `/N` normal, `/R` rollover, `/D` down. For a button, `/N` maps each state name to a form XObject; for text and choice fields, `/N` is one stream.
|
|
14
|
+
- `/Rect`, `/P`, `/AS` — widget geometry, owning page, and the button state a widget currently shows.
|
|
15
|
+
- `/NeedAppearances` in `/AcroForm` — documented flag that asks viewers to regenerate appearances; many viewers ignore it, which is why a file can hold a correct `/V` and show nothing.
|
|
16
|
+
|
|
17
|
+
## What a fill must satisfy
|
|
18
|
+
|
|
19
|
+
1. Field `/V` equals the requested value, in the field's expected representation (boolean → state name, radio → that button's on-state, choice → string or list).
|
|
20
|
+
2. The widget's `/AP /N` exists and is a stream or state map with a `/BBox` and `/Resources`; the stream's content paints the value.
|
|
21
|
+
3. The widget's `/AS` names a state present in `/AP /N`; for a radio group every widget's `/AS` must be the group's chosen state or `/Off`.
|
|
22
|
+
4. The value is visible on the page: rendered inside `/Rect`, not clipped, not hidden behind the page content.
|
|
23
|
+
|
|
24
|
+
Updating `/V` alone satisfies 1 and fails 2–4. That is the "filled but blank" failure. Rebuilding the appearance and forgetting to remove a stale `/AS` satisfies 1–3 and shows two selected radio buttons. Reopening and checking 1–3 after save, then rendering for 4, is the whole discipline.
|
|
25
|
+
|
|
26
|
+
pypdf's `update_page_form_field_values(writer, page, values, flags, auto_regenerate, flatten)` handles 1–3 for common field types when the writer cloned the document properly (and `writer.reattach_fields(page)` was called where needed). Button groups need the extra `/AS` synchronisation described in `tasks/forms.md`.
|
|
27
|
+
|
|
28
|
+
## Flattening
|
|
29
|
+
|
|
30
|
+
Two meanings, both destructive in different ways:
|
|
31
|
+
|
|
32
|
+
- `flatten=True` in `update_page_form_field_values` paints each widget's current appearance into the page content and removes the interactive field.
|
|
33
|
+
- Post-process: remove `/Widget` annotations with `writer.remove_annotations(subtypes="/Widget")` and pop `/AcroForm` from the catalog. This alone leaves the widget's appearance in place, so the page looks unchanged while the fields stop responding.
|
|
34
|
+
|
|
35
|
+
Do both when the user wants a non-editable form. Verify: zero `/Widget` annotations remain, no `/AcroForm` in the catalog, and the rendered page still shows the values.
|
|
36
|
+
|
|
37
|
+
## Appearance streams
|
|
38
|
+
|
|
39
|
+
An appearance stream is a form XObject.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
/Type /XObject
|
|
43
|
+
/Subtype /Form
|
|
44
|
+
/BBox [0 0 200 14]
|
|
45
|
+
/Resources << /Font << /Helv 5 0 R >> >>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`/BBox` is the drawing area in the widget's own coordinates; the widget's `/Rect` maps that area onto the page. A generated stream usually sets the font from `/DA`, positions text at a small inset, and clips to `/BBox`. When a field must look right and its own appearance is missing, drawing a replacement with reportlab at `/Rect` coordinates and merging it into the page produces the same visual result without lying about the field value.
|
|
49
|
+
|
|
50
|
+
## Signature fields
|
|
51
|
+
|
|
52
|
+
`/FT /Sig` requires a certificate and a signing implementation. Filling text into a signature field, or editing any page of an already-signed document, breaks the signature. Say that plainly and offer the unsigned rebuild; do not present an edited document as signed.
|
|
53
|
+
|
|
54
|
+
## Choice fields
|
|
55
|
+
|
|
56
|
+
`/Opt` lists the allowed options; `/V` is the chosen value or values. A single-select choice takes a string; multi-select takes an array. If the value is not in `/Opt`, viewers show blank — validate against `/Opt` before writing, and report the allowed options when the requested value is missing.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Page geometry
|
|
2
|
+
|
|
3
|
+
PDF units are points: 1/72 inch. A4 is 595 × 842 points, Letter 612 × 792. Numbers in a spec, a region, or a stamp are points unless stated otherwise.
|
|
4
|
+
|
|
5
|
+
## Boxes
|
|
6
|
+
|
|
7
|
+
- **MediaBox** — the physical page, the outermost boundary.
|
|
8
|
+
- **CropBox** — the visible area; defaults to MediaBox. A viewer shows the CropBox.
|
|
9
|
+
- **BleedBox / TrimBox / ArtBox** — print-production hints, usually absent in office output.
|
|
10
|
+
|
|
11
|
+
PyMuPDF's `page.rect` is the CropBox (or MediaBox when no CropBox exists), with `x0, y0` normally 0 and `x1, y1` the width and height. pypdf exposes `.mediabox` and `.cropbox` directly. When you move content between documents, use the page's own box, not a constant: mixed-size sources are common after a merge.
|
|
12
|
+
|
|
13
|
+
## Coordinates
|
|
14
|
+
|
|
15
|
+
The origin is the bottom-left of the page box, x to the right, y upward. Library calls that take a rectangle (`page.get_textbox(rect)`, `add_redact_annot`, `get_pixmap(clip=...)`) use that system. Library calls that report word boxes (`page.get_text("words")`, `search_for`) report rectangles in the same system — a word's box is not a baseline, so a text redaction rect is close but may clip the line.
|
|
16
|
+
|
|
17
|
+
reportlab's `drawString(x, y, text)` places a *baseline*, measured from the bottom. Converting a top-down measurement `top` to that system: `y = page_height - top - font_size * 0.8` puts the glyph body roughly where it was.
|
|
18
|
+
|
|
19
|
+
## Rotation
|
|
20
|
+
|
|
21
|
+
`/Rotate` is a multiple of 90 stored per page; viewers apply it on top of the content. `page.rect` in PyMuPDF is already the rotated (visible) geometry, while `page.mediabox` stays unrotated. Redactions and text boxes use the unrotated content stream, so a rotated page needs the rectangle transformed: simplest is `page.set_rotation(0)`, do the work, then set the rotation back — the render proves whether you got it right.
|
|
22
|
+
|
|
23
|
+
## Normalised regions
|
|
24
|
+
|
|
25
|
+
`office-annotations` regions are fractions of the rendered preview page:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
x0 = page.x0 + left * page.width
|
|
29
|
+
x1 = page.x0 + (left + width) * page.width
|
|
30
|
+
y0 = page.y0 + top * page.height
|
|
31
|
+
y1 = page.y0 + (top + height) * page.height
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
That is the whole conversion for an unrotated page. For a rotated page, convert against the preview's own orientation and map back; the words under the converted rect tell you immediately if the mapping is off.
|
|
35
|
+
|
|
36
|
+
## Pixels and DPI
|
|
37
|
+
|
|
38
|
+
A raster at `dpi` has `pixels = points * dpi / 72`. A 110 dpi A4 page is roughly 909 × 1287 pixels. When the user draws a region on a rendered image, the fraction is DPI-independent — you never need the pixel size to convert it. For image quality on print, 150 dpi is the minimum for a screenshot, 300 dpi for artwork.
|
|
39
|
+
|
|
40
|
+
## Text measurements
|
|
41
|
+
|
|
42
|
+
`page.get_text("dict")` gives blocks → lines → spans, each span with `size` (font size in points), `font`, `origin` (the baseline point), and the text. Use the span measurements, not the bounding box, when you redraw a line: the box includes ascenders and descenders that change the apparent size.
|
|
43
|
+
|
|
44
|
+
`page.get_textbox(rect)` returns the characters whose boxes intersect `rect`, in reading order. That is the correct way to confirm what a region covers; `page.get_text("words", clip=rect)` returns word tuples when you need the boxes too.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Layout and delivery quality
|
|
2
|
+
|
|
3
|
+
Run this list on the rendered pages before presenting a file. It is the difference between "the command exited 0" and "the document is right".
|
|
4
|
+
|
|
5
|
+
## Page level
|
|
6
|
+
|
|
7
|
+
- Page count matches the request; no stray blank page at the end from a trailing `page_break` or an empty `showPage`.
|
|
8
|
+
- Every page has the intended size and orientation — a merged section with Letter pages inside an A4 report is visible in the render.
|
|
9
|
+
- Margins are consistent; nothing is clipped at the page box edge, including stamps and footers.
|
|
10
|
+
- The page box is the CropBox the user expects; a crop that hides a footer is a bug, not a clean-up.
|
|
11
|
+
|
|
12
|
+
## Text
|
|
13
|
+
|
|
14
|
+
- Body size, leading, and font are consistent; a patched line that uses a different size or face is a visible patch job.
|
|
15
|
+
- Headings descend in a sensible scale and do not collide with the body.
|
|
16
|
+
- No overflow past the right margin, no hyphen-only line breaks, no replacement boxes for missing glyphs.
|
|
17
|
+
- Non-Latin text renders as characters, not blanks — this needs a registered font with the right coverage.
|
|
18
|
+
- Extracted text matches what is visible: run `pdf read` on the final file and spot-check the edited region.
|
|
19
|
+
|
|
20
|
+
## Tables and figures
|
|
21
|
+
|
|
22
|
+
- Table columns fit the text column; long cell text wraps rather than clipping.
|
|
23
|
+
- A table that splits across pages repeats its header row.
|
|
24
|
+
- Images keep their aspect ratio; a stretched logo is a defect.
|
|
25
|
+
- Ruled lines and fills from a redacted block are gone, not half-erased.
|
|
26
|
+
- Charts and screenshots are legible at print size; text inside an image is not searchable, so keep the numbers in the body text too.
|
|
27
|
+
|
|
28
|
+
## Stamps and annotations
|
|
29
|
+
|
|
30
|
+
- Footer/header text sits in the margin, not over body copy.
|
|
31
|
+
- Page numbers are correct on the first and last page (off-by-one is the classic overlay bug).
|
|
32
|
+
- A stamp appears once per page; repeated merges double it.
|
|
33
|
+
- Filled form values sit inside their widget boxes, are not clipped by the box border, and use the field's own font where the appearance stream was preserved.
|
|
34
|
+
|
|
35
|
+
## Fonts and links
|
|
36
|
+
|
|
37
|
+
`page.get_fonts(full=True)` lists embedded fonts. A subset font is fine for display; it means the file is not intended for text-heavy editing. Check that no font is missing for the glyphs you wrote — the render shows boxes where coverage is absent. Links and bookmarks that pointed at reordered or deleted pages may dangle; the render shows clickable areas only, so trust the original page order rather than assuming the links moved with the content.
|
|
38
|
+
|
|
39
|
+
## Structural sanity
|
|
40
|
+
|
|
41
|
+
Open the output one more time with the same route the user will use: `"<python>" "<cli>" pdf read --input "<file>"`. It confirms the file parses, the page count, the metadata, and that text is extractable. A file that renders but fails to parse is worse than an obvious error, because it fails later in someone else's viewer.
|