dsh-office-kit 0.4.2 → 0.4.4

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.
Files changed (123) hide show
  1. package/assets/skills/excel-live-control/SKILL.md +119 -152
  2. package/assets/skills/excel-live-control/domain_guidance/financial_models.md +42 -58
  3. package/assets/skills/excel-live-control/domain_guidance/healthcare.md +36 -42
  4. package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +33 -34
  5. package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +34 -36
  6. package/assets/skills/excel-live-control/references/office-automation.md +127 -153
  7. package/assets/skills/excel-live-control/references/read_only_qna.md +40 -45
  8. package/assets/skills/excel-live-control/style_guidelines.md +56 -64
  9. package/assets/skills/excel-live-control/workflows/create_workflows.md +40 -47
  10. package/assets/skills/excel-live-control/workflows/edit_workflows.md +37 -45
  11. package/assets/skills/office-documents/SKILL.md +24 -60
  12. package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +5 -5
  13. package/assets/skills/office-documents/ooxml/comments.md +8 -8
  14. package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +10 -10
  15. package/assets/skills/office-documents/ooxml/rels_and_content_types.md +7 -7
  16. package/assets/skills/office-documents/ooxml/tracked_changes.md +12 -12
  17. package/assets/skills/office-documents/references/docx-structure.md +11 -13
  18. package/assets/skills/office-documents/references/template-elicitation.md +11 -13
  19. package/assets/skills/office-documents/references/writing-quality.md +20 -20
  20. package/assets/skills/office-documents/scripts/add_tracked_replacements.py +4 -3
  21. package/assets/skills/office-documents/scripts/docx_kit.py +68 -27
  22. package/assets/skills/office-documents/scripts/heading_audit.py +3 -4
  23. package/assets/skills/office-documents/scripts/internal_nav.py +3 -1
  24. package/assets/skills/office-documents/scripts/merge_docx_append.py +104 -0
  25. package/assets/skills/office-documents/scripts/table_geometry.py +13 -3
  26. package/assets/skills/office-documents/scripts/watermark_add.py +6 -5
  27. package/assets/skills/office-documents/scripts/watermark_audit_remove.py +6 -4
  28. package/assets/skills/office-documents/tasks/accessibility_a11y.md +16 -22
  29. package/assets/skills/office-documents/tasks/captions_crossrefs.md +23 -26
  30. package/assets/skills/office-documents/tasks/clean_tracked_changes.md +17 -22
  31. package/assets/skills/office-documents/tasks/comments_manage.md +9 -11
  32. package/assets/skills/office-documents/tasks/compare_diff.md +10 -16
  33. package/assets/skills/office-documents/tasks/create_edit.md +14 -16
  34. package/assets/skills/office-documents/tasks/equations.md +17 -28
  35. package/assets/skills/office-documents/tasks/fields_update.md +14 -16
  36. package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +19 -33
  37. package/assets/skills/office-documents/tasks/footnotes_endnotes.md +12 -14
  38. package/assets/skills/office-documents/tasks/forms_content_controls.md +16 -20
  39. package/assets/skills/office-documents/tasks/headings_numbering.md +17 -22
  40. package/assets/skills/office-documents/tasks/images_figures.md +15 -25
  41. package/assets/skills/office-documents/tasks/multi_doc_merge.md +13 -23
  42. package/assets/skills/office-documents/tasks/navigation_internal_links.md +18 -21
  43. package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +16 -22
  44. package/assets/skills/office-documents/tasks/protection_restrict_editing.md +19 -21
  45. package/assets/skills/office-documents/tasks/read_review.md +9 -20
  46. package/assets/skills/office-documents/tasks/redaction_anonymization.md +17 -18
  47. package/assets/skills/office-documents/tasks/sections_layout.md +16 -23
  48. package/assets/skills/office-documents/tasks/style_lint_normalize.md +18 -22
  49. package/assets/skills/office-documents/tasks/tables_spreadsheets.md +21 -31
  50. package/assets/skills/office-documents/tasks/templates_style_packs.md +17 -21
  51. package/assets/skills/office-documents/tasks/toc_workflow.md +10 -14
  52. package/assets/skills/office-documents/tasks/verify_render.md +11 -23
  53. package/assets/skills/office-documents/tasks/watermarks_background.md +10 -14
  54. package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +6 -6
  55. package/assets/skills/office-documents/troubleshooting/run_splitting.md +12 -12
  56. package/assets/skills/office-pdf/SKILL.md +36 -51
  57. package/assets/skills/office-pdf/references/form-fields.md +20 -20
  58. package/assets/skills/office-pdf/references/geometry.md +10 -10
  59. package/assets/skills/office-pdf/references/layout-quality.md +13 -13
  60. package/assets/skills/office-pdf/references/limits.md +12 -56
  61. package/assets/skills/office-pdf/tasks/annotations.md +5 -5
  62. package/assets/skills/office-pdf/tasks/create.md +6 -9
  63. package/assets/skills/office-pdf/tasks/edit-pages.md +14 -14
  64. package/assets/skills/office-pdf/tasks/forms.md +15 -15
  65. package/assets/skills/office-pdf/tasks/read.md +9 -9
  66. package/assets/skills/office-pdf/tasks/redact.md +18 -18
  67. package/assets/skills/office-pdf/tasks/stamp.md +10 -10
  68. package/assets/skills/office-presentations/SKILL.md +71 -91
  69. package/assets/skills/office-presentations/container_tools/preview_render.py +4 -1
  70. package/assets/skills/office-presentations/container_tools/render_slides.py +1 -1
  71. package/assets/skills/office-presentations/domain_guidance/finance.md +16 -16
  72. package/assets/skills/office-presentations/recipes/README.md +24 -35
  73. package/assets/skills/office-presentations/recipes/autolayout.md +12 -13
  74. package/assets/skills/office-presentations/recipes/connectors.md +11 -12
  75. package/assets/skills/office-presentations/recipes/images.md +11 -12
  76. package/assets/skills/office-presentations/recipes/master-and-layouts.md +18 -18
  77. package/assets/skills/office-presentations/recipes/native-charts.md +8 -8
  78. package/assets/skills/office-presentations/recipes/rich-text-runs.md +13 -13
  79. package/assets/skills/office-presentations/recipes/speaker-notes.md +7 -7
  80. package/assets/skills/office-presentations/recipes/tables.md +14 -14
  81. package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +9 -9
  82. package/assets/skills/office-presentations/recipes/theme-tokens.md +7 -8
  83. package/assets/skills/office-presentations/references/authoring-workflow.md +38 -43
  84. package/assets/skills/office-presentations/references/finalization.md +10 -12
  85. package/assets/skills/office-presentations/references/native-bullets.md +14 -18
  86. package/assets/skills/office-presentations/references/native-evidence.md +18 -32
  87. package/assets/skills/office-presentations/references/style-guidelines.md +27 -32
  88. package/assets/skills/office-presentations/references/templates.md +31 -60
  89. package/assets/skills/office-presentations/routing/google_slides.md +4 -4
  90. package/assets/skills/office-presentations/tasks/apply-annotations.md +17 -17
  91. package/assets/skills/office-presentations/tasks/charts-tables-notes.md +20 -23
  92. package/assets/skills/office-presentations/tasks/create-deck.md +19 -19
  93. package/assets/skills/office-presentations/tasks/edit-deck.md +14 -16
  94. package/assets/skills/office-presentations/tasks/inspect-and-verify.md +31 -34
  95. package/assets/skills/office-presentations/tasks/template-following.md +23 -28
  96. package/assets/skills/office-spreadsheets/SKILL.md +32 -39
  97. package/assets/skills/office-spreadsheets/recipes/charts.md +8 -26
  98. package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +9 -26
  99. package/assets/skills/office-spreadsheets/recipes/data-tables.md +7 -23
  100. package/assets/skills/office-spreadsheets/recipes/named-ranges.md +8 -21
  101. package/assets/skills/office-spreadsheets/recipes/number-formats.md +7 -23
  102. package/assets/skills/office-spreadsheets/recipes/sparklines.md +7 -20
  103. package/assets/skills/office-spreadsheets/references/charts.md +15 -34
  104. package/assets/skills/office-spreadsheets/references/domain/financial-models.md +10 -25
  105. package/assets/skills/office-spreadsheets/references/domain/healthcare.md +8 -19
  106. package/assets/skills/office-spreadsheets/references/domain/marketing.md +8 -22
  107. package/assets/skills/office-spreadsheets/references/domain/research.md +8 -19
  108. package/assets/skills/office-spreadsheets/references/formula-patterns.md +20 -50
  109. package/assets/skills/office-spreadsheets/references/style-guidelines.md +19 -44
  110. package/assets/skills/office-spreadsheets/references/workbook-structure.md +12 -35
  111. package/assets/skills/office-spreadsheets/routing/google-sheets.md +11 -26
  112. package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +10 -25
  113. package/assets/skills/office-spreadsheets/tasks/annotations.md +12 -30
  114. package/assets/skills/office-spreadsheets/tasks/create-workbook.md +5 -5
  115. package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +2 -2
  116. package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +5 -5
  117. package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +9 -9
  118. package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +10 -30
  119. package/assets/skills/shared/annotation-recipes.md +98 -120
  120. package/assets/skills/shared/common-workflow.md +70 -80
  121. package/dist/index.js +1 -1
  122. package/package.json +1 -1
  123. package/python/office_cli.py +38 -4
@@ -4,39 +4,39 @@
4
4
 
5
5
  `/AcroForm` in the catalog holds `/Fields`, the root field array. A field dictionary has:
6
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.
7
+ - `/T` — partial name; the full name joins ancestor partial names with dots.
8
+ - `/FT` — type, inherited: `Tx` text, `Btn` button, `Ch` choice, `Sig` signature.
9
+ - `/V` — current value: a string for text; the selected state name (`/Yes`, `/Off`, `/basic`) for buttons; a string or array for choice fields; a dictionary for signatures.
10
+ - `/Ff` — inherited flags bitmask: bit 1 (`1`) read-only, bit 2 (`2`) required, bit 13 (`4096`) multiselect, bits 15–17 (`65536`/`131072`/`262144`) button behaviour (no-toggle, radio, pushbutton).
11
+ - `/DA` — default appearance string; font and colour an appearance generator uses.
12
12
  - `/Kids` — child fields or widgets.
13
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.
14
+ - `/Rect`, `/P`, `/AS` — widget geometry, owning page, and the state the widget shows.
15
+ - `/NeedAppearances` in `/AcroForm` — asks viewers to regenerate appearances; many ignore it, which is why a file can hold a correct `/V` and show nothing.
16
16
 
17
17
  ## What a fill must satisfy
18
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.
19
+ 1. Field `/V` equals the requested value in the field's representation (boolean → state name, radio → that button's on-state, choice → string or list).
20
+ 2. The widget's `/AP /N` exists as a stream or state map with a `/BBox` and `/Resources`, and its 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 page content.
23
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.
24
+ Updating `/V` alone satisfies 1 and fails 2–4 — the "filled but blank" failure. Rebuilding the appearance with a stale `/AS` satisfies 1–3 and shows two selected radios. Check 1–3 after saving, then render for 4.
25
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`.
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 in `tasks/forms.md`.
27
27
 
28
28
  ## Flattening
29
29
 
30
- Two meanings, both destructive in different ways:
30
+ Two destructive meanings:
31
31
 
32
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.
33
+ - Post-process: remove `/Widget` annotations with `writer.remove_annotations(subtypes="/Widget")` and pop `/AcroForm` from the catalog. On its own this leaves the appearance, so the page looks unchanged while the fields stop responding.
34
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.
35
+ Do both for a non-editable form. Verify zero `/Widget` annotations, no `/AcroForm` in the catalog, and the values still rendered.
36
36
 
37
37
  ## Appearance streams
38
38
 
39
- An appearance stream is a form XObject.
39
+ An appearance stream is a form XObject:
40
40
 
41
41
  ```
42
42
  /Type /XObject
@@ -45,12 +45,12 @@ An appearance stream is a form XObject.
45
45
  /Resources << /Font << /Helv 5 0 R >> >>
46
46
  ```
47
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.
48
+ `/BBox` is the drawing area in the widget's own coordinates; the widget's `/Rect` maps it 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 appearance is missing, draw a replacement with reportlab at `/Rect` coordinates and merge it into the page — same visual result, without lying about the field value.
49
49
 
50
50
  ## Signature fields
51
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.
52
+ `/FT /Sig` needs a certificate and a signing implementation. Filling text into a signature field, or editing any page of a signed document, breaks the signature. Say so plainly and offer the unsigned rebuild; never present an edited document as signed.
53
53
 
54
54
  ## Choice fields
55
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.
56
+ `/Opt` lists the allowed options; `/V` is the chosen value or values. Single-select takes a string, multi-select an array. A value outside `/Opt` renders blank — validate against `/Opt`, and report the allowed options when the requested value is missing.
@@ -1,24 +1,24 @@
1
1
  # Page geometry
2
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.
3
+ PDF units are points, 1/72 inch: A4 is 595 × 842, Letter 612 × 792. Numbers in a spec, a region, or a stamp are points unless stated otherwise.
4
4
 
5
5
  ## Boxes
6
6
 
7
7
  - **MediaBox** — the physical page, the outermost boundary.
8
- - **CropBox** — the visible area; defaults to MediaBox. A viewer shows the CropBox.
8
+ - **CropBox** — the visible area, defaulting to MediaBox; viewers show the CropBox.
9
9
  - **BleedBox / TrimBox / ArtBox** — print-production hints, usually absent in office output.
10
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.
11
+ PyMuPDF's `page.rect` is the CropBox (MediaBox when no CropBox exists), with `x0, y0` normally 0 and `x1, y1` the width and height; pypdf exposes `.mediabox` and `.cropbox`. When moving content between documents, use the page's own box, never a constant: mixed-size sources are common after a merge.
12
12
 
13
13
  ## Coordinates
14
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.
15
+ The origin is the bottom-left of the page box, x right, y up. Rectangle-taking calls (`page.get_textbox(rect)`, `add_redact_annot`, `get_pixmap(clip=...)`) and word-box calls (`page.get_text("words")`, `search_for`) all use that system — a word's box is not a baseline, so a text redaction rect may clip the line.
16
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.
17
+ reportlab's `drawString(x, y, text)` places a *baseline* measured from the bottom. Convert a top-down measurement: `y = page_height - top - font_size * 0.8` puts the glyph body roughly where it was.
18
18
 
19
19
  ## Rotation
20
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.
21
+ `/Rotate` is a per-page multiple of 90 applied by viewers on top of the content. `page.rect` in PyMuPDF is the rotated (visible) geometry; `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, set the rotation back, and let the render prove it.
22
22
 
23
23
  ## Normalised regions
24
24
 
@@ -31,14 +31,14 @@ y0 = page.y0 + top * page.height
31
31
  y1 = page.y0 + (top + height) * page.height
32
32
  ```
33
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.
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 show at once if the mapping is off.
35
35
 
36
36
  ## Pixels and DPI
37
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.
38
+ A raster at `dpi` has `pixels = points * dpi / 72`; a 110 dpi A4 page is roughly 909 × 1287 pixels. A region drawn on a rendered image is a DPI-independent fraction, so you never need the pixel size to convert it. For print, 150 dpi is the minimum for a screenshot, 300 dpi for artwork.
39
39
 
40
40
  ## Text measurements
41
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.
42
+ `page.get_text("dict")` gives blocks → lines → spans, each span with `size` (points), `font`, `origin` (baseline point), and text. When redrawing a line, use the span measurements, not the bounding box: the box includes ascenders and descenders and changes the apparent size.
43
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.
44
+ `page.get_textbox(rect)` returns the characters whose boxes intersect `rect`, in reading order — the correct way to confirm what a region covers. `page.get_text("words", clip=rect)` returns word tuples when you need the boxes too.
@@ -1,41 +1,41 @@
1
1
  # Layout and delivery quality
2
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".
3
+ Run this list on the rendered pages before presenting a file: it separates "the command exited 0" from "the document is right".
4
4
 
5
5
  ## Page level
6
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.
7
+ - Page count matches the request; no stray blank page from a trailing `page_break` or an empty `showPage`.
8
+ - Every page has the intended size and orientation — Letter pages inside an A4 merge show in the render.
9
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.
10
+ - The page box is the CropBox the user expects; a crop that hides a footer is a bug.
11
11
 
12
12
  ## Text
13
13
 
14
- - Body size, leading, and font are consistent; a patched line that uses a different size or face is a visible patch job.
14
+ - Body size, leading, and font are consistent; a patched line in another size or face is a visible patch job.
15
15
  - Headings descend in a sensible scale and do not collide with the body.
16
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.
17
+ - Non-Latin text renders as characters, not blanks — it needs a registered font with the right coverage.
18
+ - Extracted text matches the visible text: `pdf read` the final file and spot-check the edited region.
19
19
 
20
20
  ## Tables and figures
21
21
 
22
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.
23
+ - A table split across pages repeats its header row.
24
24
  - Images keep their aspect ratio; a stretched logo is a defect.
25
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.
26
+ - Charts and screenshots are legible at print size; text inside an image is not searchable, so keep the numbers in body text too.
27
27
 
28
28
  ## Stamps and annotations
29
29
 
30
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).
31
+ - Page numbers are right on the first and last page (off-by-one is the classic overlay bug).
32
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.
33
+ - Filled form values sit inside their widget boxes, are not clipped by the box border, and keep the field's own font where the appearance stream was preserved.
34
34
 
35
35
  ## Fonts and links
36
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.
37
+ `page.get_fonts(full=True)` lists embedded fonts; a subset font is fine for display and 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 links moved with the content.
38
38
 
39
39
  ## Structural sanity
40
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.
41
+ Open the output one more time with the route the user will use: `"<python>" "<cli>" pdf read --input "<file>"`. It confirms the file parses, the page count, the metadata, and that text extracts. A file that renders but fails to parse is worse than an obvious error, because it fails later in someone else's viewer.
@@ -1,58 +1,14 @@
1
1
  # When a PDF job is beyond this pack
2
2
 
3
- Every entry names the missing capability, what you can still deliver, and how the user resolves it. Never answer with a bare "unsupported": say what is missing, what the floor is, and give the next step.
4
-
5
- ## Scanned pages with no text layer
6
-
7
- The words exist only as pixels; extraction and search cannot see them. `render_pages.py` reports `chars: 0` for such a page, and the render shows the text as an image.
8
-
9
- - Still possible: render, crop with `read_image`, and transcribe the visible text; redact the image pixels with `--images pixels`; rebuild the page as a created PDF from the transcription.
10
- - Resolution: the user supplies a text-based copy of the file, or runs OCR where they have it (this runtime has no OCR engine, so do not promise the words will be extracted here).
11
-
12
- ## Encrypted or password-protected files
13
-
14
- pypdf and PyMuPDF can open with a password only if it is provided; every script here exits with "encrypted" and does no work.
15
-
16
- - Resolution: ask the user to decrypt the file first, then re-run. Never write a password into a command line that will be logged.
17
-
18
- ## Signing and certificates
19
-
20
- Creating a digital signature needs a certificate, a signer, and a signature field; the runtime has none of them. Editing a signed document invalidates its signature.
21
-
22
- - Still possible: fill unsigned fields, rebuild the content, deliver an unsigned file — and say clearly that the signature is gone.
23
- - Resolution: signing must happen in a tool with a certificate; the user applies it after the content is final.
24
-
25
- ## Paragraph-level re-editing
26
-
27
- PDF has no paragraph model: the content stream is positioned glyphs and paths. "Rewrite this paragraph" becomes a block-level operation — redact the old block, stamp the new text, and check the reflow yourself. Truly reflowing a page means rebuilding it from extracted content.
28
-
29
- - Still possible: replace a line or a block in place (see `tasks/annotations.md`), remove a block, stamp a note.
30
- - Resolution for reflow: rebuild the document from a spec with the corrected text, and say that it was rebuilt rather than patched.
31
-
32
- ## Lexical repair of broken files
33
-
34
- A file with a damaged cross-reference table may still render (viewers rebuild the xref) while strict parsers refuse it. If PyMuPDF or pypdf cannot open the file, do not silently produce an empty document.
35
-
36
- - Still possible: report the parse error, ask the user to open and re-save the file in a reader (which repairs it), then re-run.
37
- - Resolution: whichever tool originally produced the file should be told about the corruption.
38
-
39
- ## Tagged PDF and accessibility remediation
40
-
41
- Reading order, alternative text, and tags live in a structure tree that the libraries here can read but not author reliably. A visual edit does not add tags.
42
-
43
- - Still possible: keep the visual result correct and say the file is untagged.
44
- - Resolution: accessibility remediation needs an authoring tool that writes the structure tree.
45
-
46
- ## PDF/A and archival conformance
47
-
48
- Conformance requires font embedding rules, metadata schemas, and colour profiles that these libraries do not validate.
49
-
50
- - Still possible: many PDF/A inputs will survive a page-level edit and remain close, but claiming conformance would be a guess.
51
- - Resolution: validate with a dedicated PDF/A checker before delivery.
52
-
53
- ## Creating or restructuring form fields
54
-
55
- pypdf can fill existing fields; creating a new interactive field with a correct `/AP` state map is much more than a fill. reportlab can draw a form-like appearance, but without a widget tree it is not a form.
56
-
57
- - Still possible: stamp labels and values at coordinates so the printed result looks filled.
58
- - Resolution: an authoring tool for interactive forms, or accept a non-interactive output and say so.
3
+ State the missing capability, what you can still deliver, and how the user resolves it. Never answer with a bare "unsupported".
4
+
5
+ | Missing capability | Still possible | Resolution |
6
+ | --- | --- | --- |
7
+ | **Scanned pages with no text layer** — words are pixels only; `render_pages.py` reports `chars: 0` | Render, crop with `read_image`, transcribe; redact image pixels with `--images pixels`; rebuild the page from the transcription | The user supplies a text-based copy, or runs OCR where they have it — this runtime has no OCR engine |
8
+ | **Encrypted or password-protected files** — pypdf/PyMuPDF need the password; every script here exits with "encrypted" and does no work | Nothing until the file opens | Ask the user to decrypt first, then re-run. Never write a password into a command line that will be logged. |
9
+ | **Signing and certificates** — no certificate, signer, or signature field; editing a signed file invalidates its signature | Fill unsigned fields, rebuild the content, deliver unsigned — and say the signature is gone | Signing happens in a tool with a certificate, after the content is final |
10
+ | **Paragraph-level re-editing** — no paragraph model; the content stream is positioned glyphs and paths | Replace a line or block in place (`tasks/annotations.md`), remove a block, stamp a note | Reflow means rebuilding from a spec with the corrected text — say it was rebuilt, not patched |
11
+ | **Lexical repair of broken files** — a damaged xref may render in viewers but fail strict parsers; never emit an empty document | Report the parse error; ask the user to open and re-save in a reader (which repairs it), then re-run | Tell whichever tool produced the file about the corruption |
12
+ | **Tagged PDF and accessibility remediation** — the structure tree can be read but not authored reliably; a visual edit adds no tags | Keep the visual result correct and say the file is untagged | Remediation needs an authoring tool that writes the structure tree |
13
+ | **PDF/A and archival conformance** — font embedding, metadata schemas, and colour profiles are not validated here | Many PDF/A inputs survive a page-level edit and stay close, but claiming conformance would be a guess | Validate with a dedicated PDF/A checker before delivery |
14
+ | **Creating or restructuring form fields** — pypdf fills existing fields; a new interactive field needs a correct `/AP` state map, far beyond a fill | Stamp labels and values at coordinates so the printed result looks filled | An authoring tool for interactive forms, or accept non-interactive output and say so |
@@ -1,6 +1,6 @@
1
1
  # Resolve an `office-annotations` region on a PDF
2
2
 
3
- `page` is the 1-based page of the file, and `region × page.rect` is the real rectangle in the page's coordinate system. Convert once; then everything you do — read, redact, stamp, fill — uses that rect.
3
+ `page` is the 1-based page of the file, and `region × page.rect` is the real rectangle in the page's coordinate system. Convert once; then every operation — read, redact, stamp, fill — uses that rect.
4
4
 
5
5
  ## 1. Reproduce the page first
6
6
 
@@ -8,7 +8,7 @@
8
8
  "<python>" "<cli>" pdf render --input "<file.pdf>" --out-dir "<render>" --dpi 110
9
9
  ```
10
10
 
11
- Open `"<render>/page-<N>.png"` with `read_image`. The geometry is only meaningful against the page the user saw, so never resolve a region on a page you have not rendered.
11
+ Open `"<render>/page-<N>.png"` with `read_image`. Geometry is only meaningful against the page the user saw, so never resolve a region on a page you have not rendered.
12
12
 
13
13
  ## 2. Convert the region and read what it covers
14
14
 
@@ -43,7 +43,7 @@ Compare the words with the annotation's `text`. If `text` is present and those w
43
43
  | A single word or sentence to change | redact the old line, then stamp the new text (below) |
44
44
  | A picture to replace | overlay a cover rect and draw the new image with reportlab |
45
45
 
46
- A requirement like "make this sentence shorter" is a content edit; do not rebuild the whole page for it when the block is one line.
46
+ A requirement like "make this sentence shorter" is a content edit; do not rebuild the whole page when the block is one line.
47
47
 
48
48
  ## 4. Patch one line in place
49
49
 
@@ -91,8 +91,8 @@ Line height and font size must match the original text; read them from the origi
91
91
 
92
92
  ## 5. When the payload cannot be tied to an object
93
93
 
94
- Say which part you could not locate: the render page, the words you found under the region, and the item that is missing. Then ask for the text of the target or a clearer region. Genuinely blocked cases and their resolutions are in `references/limits.md` — name the missing capability and what the user can do about it, never a bare "unsupported".
94
+ Say which part you could not locate — render page, the words found under the region, the missing item — and ask for the target text or a clearer region. Blocked cases are in `references/limits.md`: name the missing capability and the way out, never a bare "unsupported".
95
95
 
96
96
  ## 6. Re-render and confirm
97
97
 
98
- Apply the edit, render the same page, and `read_image` it again. The region must show the requested result, the neighbouring text must be untouched, and the layout must still hold. Then check extraction (`pdf read`) so the change is real in the text layer, not only in pixels. Report what you resolved back to the user in the same terms they annotated with: page number and a one-line description of the change.
98
+ Apply the edit, render the same page, and `read_image` it again: the region must show the requested result, neighbouring text must be untouched, and the layout must hold. Then confirm with `pdf read` that the change is in the text layer, not only in pixels. Report in the user's own terms: page number plus a one-line description of the change.
@@ -1,7 +1,5 @@
1
1
  # Create a PDF
2
2
 
3
- A new PDF is a set of pages, each holding a content stream plus fonts and resources. Two routes exist here, and both end with the same verification.
4
-
5
3
  ## Fast path: the helper CLI spec
6
4
 
7
5
  ```bash
@@ -27,7 +25,7 @@ Blocks: `title`, `heading` (`level` 1–2), `paragraph`, `quote`, `bullets`, `nu
27
25
 
28
26
  ## Direct path: reportlab
29
27
 
30
- Reach for reportlab when the layout does not fit the block model — columns, absolute placement, custom rules, rotated text.
28
+ Reach for reportlab when the layout does not fit the block model: columns, absolute placement, custom rules, rotated text.
31
29
 
32
30
  ```bash
33
31
  "<python>" - <<'PY'
@@ -50,26 +48,25 @@ c.save()
50
48
  PY
51
49
  ```
52
50
 
53
- `drawString` takes a baseline point measured from the bottom-left corner, not the top. Convert top-down coordinates once (for a page height `h`, baseline `y = h - top - font_size`), or every following measurement will drift.
51
+ `drawString` takes a baseline point measured from the bottom-left corner, not the top. Convert top-down coordinates once (for a page height `h`, baseline `y = h - top - font_size`), or every later measurement drifts.
54
52
 
55
53
  ## Fonts
56
54
 
57
- Base-14 fonts (`Helvetica`, `Times-Roman`, `Courier`) need no embedding and cover Latin text. Non-Latin text needs a real font file registered with `pdfmetrics.registerFont(TTFont("Body", "<path>.ttf"))`; a font that cannot render a glyph produces a blank box in the output, which the render step will show. The CLI tries the first available CJK system font, and says so when none exists.
55
+ Base-14 fonts (`Helvetica`, `Times-Roman`, `Courier`) need no embedding and cover Latin text. Non-Latin text needs a real font file registered with `pdfmetrics.registerFont(TTFont("Body", "<path>.ttf"))`; a font that cannot render a glyph produces a blank box, which the render step shows. The CLI tries the first available CJK system font and says so when none exists.
58
56
 
59
57
  ## Quality rules
60
58
 
61
- - One heading scale, one body size, one margin system per document.
62
59
  - Keep body text inside 12–16 pt leading; below 9 pt text becomes unreliable on print.
63
60
  - ASCII hyphens only in prose; do not paste typographic dashes into generated copy unless the source used them.
64
61
  - Give every page a consistent header/footer treatment, or none at all.
65
- - Do not put a table wider than the text column; wrap long cell text instead of letting it clip.
62
+ - Full layout checklist: `references/layout-quality.md`.
66
63
 
67
64
  ## Verify
68
65
 
69
- Render and look at every page; a created PDF has no text layer surprises, so problems are layout problems: text past the margin, blocks overlapping, table columns squeezed, a `page_break` landing on an empty page, or a CJK line silently blank.
66
+ Render every page: a created PDF has no text-layer surprises, so problems are layout problems — text past the margin, blocks overlapping, table columns squeezed, a `page_break` landing on an empty page, a CJK line silently blank.
70
67
 
71
68
  ```bash
72
69
  "<python>" "<cli>" pdf render --input "out.pdf" --out-dir "render" --dpi 110
73
70
  ```
74
71
 
75
- Then confirm the structure you intended with `"<python>" "<cli>" pdf read --input "out.pdf"` (page count and per-page text) before `present`.
72
+ Then confirm the page count and per-page text with `"<python>" "<cli>" pdf read --input "out.pdf"` before `present`.
@@ -1,6 +1,6 @@
1
1
  # Merge, split, reorder, rotate, delete pages
2
2
 
3
- Pages are objects referenced from a page tree; changing the order or removing a page changes references, not content. Nothing is re-flowed, so a page moved into a document with a different page size keeps its own box and fonts.
3
+ Pages are referenced from a page tree; reordering or removing one changes references, not content. Nothing is re-flowed, so a page moved into a differently sized document keeps its own box and fonts.
4
4
 
5
5
  ## Fast path: `scripts/page_ops.py`
6
6
 
@@ -9,14 +9,14 @@ Pages are objects referenced from a page tree; changing the order or removing a
9
9
  --rotate 90 --pages 0,2 --remove 3 --merge "<other.pdf>" --order 2,0,1
10
10
  ```
11
11
 
12
- - Operations run in a fixed order: merge, then order, then remove, then rotate — a command means the same thing every time.
13
- - Every index is zero-based and addresses the combined page list (input pages first, appended pages after).
14
- - `--order` keeps and reorders; pages it does not list are dropped, which also makes it the duplicate/select tool when you repeat an index.
15
- - `--remove` drops indices from the list left by `--order`.
12
+ - Fixed operation order: merge, then order, then remove, then rotate — a command always means the same thing.
13
+ - Indices are zero-based over the combined page list (input pages first, appended pages after).
14
+ - `--order` keeps and reorders; unlisted pages are dropped, and a repeated index duplicates it.
15
+ - `--remove` drops indices from the list `--order` leaves.
16
16
  - `--rotate` takes 90, 180, or 270 clockwise and defaults to every remaining page; `--pages` limits it.
17
17
  - `--out` defaults to `<input>-edited.pdf`; the input is never overwritten.
18
18
 
19
- The script carries page content streams over as they are and reopens the result to report page count, rotation, and page size per page. When one call cannot express the operation, chain two calls into two output files rather than reusing a name.
19
+ The script carries content streams over unchanged and reopens the result to report page count, rotation, and page size per page. When one call cannot express the operation, chain two calls into two output files rather than reusing a name.
20
20
 
21
21
  ## Metadata
22
22
 
@@ -24,7 +24,7 @@ The script carries page content streams over as they are and reopens the result
24
24
  "<python>" "<skill>/scripts/metadata.py" --input "<file.pdf>" --set "/Title=Field report" --set "/Author=Ada" --out "<file-edited.pdf>"
25
25
  ```
26
26
 
27
- Keys are the document information fields (`Title`, `Author`, `Subject`, `Keywords`, `Creator`, `Producer`, `CreationDate`, `ModDate`), plain or `/Name`, in any case. The script writes a new file and reopens it, so `applied` and `not_applied` tell you what actually persisted.
27
+ Keys are the document information fields (`Title`, `Author`, `Subject`, `Keywords`, `Creator`, `Producer`, `CreationDate`, `ModDate`), plain or `/Name`, in any case. The script writes a new file and reopens it; `applied`/`not_applied` show what persisted.
28
28
 
29
29
  ## Split
30
30
 
@@ -32,7 +32,7 @@ Keys are the document information fields (`Title`, `Author`, `Subject`, `Keyword
32
32
  "<python>" "<skill>/scripts/split_pages.py" --input "<file.pdf>" --out-dir "<parts>" --ranges "1-2,3,5-"
33
33
  ```
34
34
 
35
- 1-based inclusive ranges, `--every N` for equal chunks, default one file per page. The script copies the source metadata, refuses overlapping or out-of-range ranges, and fails if the parts do not add up to the source page count — that check is what catches a typo in the range string. `--prefix` sets the filename stem.
35
+ 1-based inclusive ranges, or `--every N` for equal chunks; the default is one file per page. The script copies the source metadata, refuses overlapping or out-of-range ranges, and fails when the parts do not add up to the source page count — the check that catches a typo in the range string. `--prefix` sets the filename stem.
36
36
 
37
37
  ## Direct page-tree work
38
38
 
@@ -53,19 +53,19 @@ print("pages", len(writer.pages))
53
53
  PY
54
54
  ```
55
55
 
56
- `add_page` copies the page and its resources into the new file; annotations that point across pages (links) can break when their target is removed. After a reorder or deletion, check the links and bookmarks on the render, not only the text.
56
+ `add_page` copies the page and its resources; annotations pointing across pages (links) can break when their target is removed. After a reorder or deletion, check links and bookmarks on the render, not only the text.
57
57
 
58
58
  ## Rotate
59
59
 
60
- Page rotation is a `/Rotate` entry (multiples of 90) applied by viewers; it does not transpose the content stream. `page.rotate(90)` in pypdf, or `page_ops.py --rotate 90 --pages ...`, is enough. If a page was scanned sideways and also carries a stale `/Rotate`, the two add up — the render shows whether you are looking at the wrong one, and `scripts/render_pages.py` reports each page's rotation.
60
+ Page `/Rotate` (multiples of 90) is applied by viewers and does not transpose the content stream. `page.rotate(90)` in pypdf, or `page_ops.py --rotate 90 --pages ...`, is enough. A page scanned sideways that also carries a stale `/Rotate` accumulates both — the render shows which one you are looking at, and `scripts/render_pages.py` reports each page's rotation.
61
61
 
62
62
  ## Verify
63
63
 
64
64
  After any structural edit:
65
65
 
66
66
  - compare page counts (`pdf read` or the split report) — one missing page is easy to miss in a long render;
67
- - render at least the first and last page of every part, and every page you rotated or reordered;
68
- - for merges, confirm the page size and orientation of the appended pages did not change, and that page numbers or bates stamps do not collide;
69
- - for deletions of numbered pages, tell the user which numbers were removed, because the remaining page labels will shift.
67
+ - render the first and last page of every part, plus every page you rotated or reordered;
68
+ - for merges, confirm appended pages kept their size and orientation, and that page numbers or bates stamps do not collide;
69
+ - for deletions of numbered pages, tell the user which numbers were removed, because the remaining page labels shift.
70
70
 
71
- `scripts/render_pages.py --input "<file.pdf>" --region 0,0,1,1 --json-out ...` prints per-page character and drawing counts, which is a cheap way to confirm a page is not blank before you deliver.
71
+ `scripts/render_pages.py --input "<file.pdf>" --region 0,0,1,1 --json-out ...` prints per-page character and drawing counts — a cheap blank-page check before delivery.
@@ -1,6 +1,6 @@
1
1
  # AcroForm inspection and filling
2
2
 
3
- A form PDF carries `/AcroForm` in the catalog. Its `/Fields` array holds field dictionaries: `/T` the partial name, `/FT` the type (`Tx` text, `Btn` button, `Ch` choice, `Sig` signature), `/V` the value, `/Ff` flags, `/DA` the default appearance, and `/AP` appearance streams. A widget is the field's visual rectangle (`/Rect`, `/P` parent page, `/AS` its currently selected state name). A field can own several widgets (radio groups), and a widget can be a separate object from its field, reached through `/Parent`. Text in a filled field is usually not in the page content stream — it lives in the widget's `/AP /N` appearance, which is why "the value is in `/V`" and "the page shows the value" are two different facts to check.
3
+ A form PDF carries `/AcroForm` in the catalog. `/Fields` holds field dictionaries: `/T` name, `/FT` type, `/V` value, `/Ff` flags, `/DA` default appearance, `/AP` appearance stream. Widgets carry `/Rect`, `/P`, `/AS`, and a widget can be a separate object reached through `/Parent`. Full field mechanics: `references/form-fields.md`. Text in a filled field usually lives in the widget's `/AP /N`, not the page content stream, so "value in `/V`" and "page shows the value" are two different facts.
4
4
 
5
5
  ## Inspect first
6
6
 
@@ -8,7 +8,7 @@ A form PDF carries `/AcroForm` in the catalog. Its `/Fields` array holds field d
8
8
  "<python>" "<skill>/scripts/forms.py" inspect --input "<file.pdf>"
9
9
  ```
10
10
 
11
- Reports, per field: id/name, type, value, read-only/required, widget count, page numbers, current states for buttons, and whether each widget has an `/AP` stream. Read the whole report before touching values: duplicate names, missing appearance streams, and read-only flags are the usual reason a fill "does nothing".
11
+ Reports per field: name, type, value, read-only/required, widget count, pages, button states, and whether each widget has `/AP`. Read it all before touching values: duplicate names, missing appearances, and read-only flags are why a fill "does nothing".
12
12
 
13
13
  ## Fill
14
14
 
@@ -20,20 +20,20 @@ Reports, per field: id/name, type, value, read-only/required, widget count, page
20
20
  { "full_name": "Ada Lovelace", "email": "ada@example.org", "agree": true, "plan": "/basic" }
21
21
  ```
22
22
 
23
- Text fields take strings, checkboxes take booleans or the state name, radio groups take the state you want selected, choice fields take a string or list. The dictionary is keyed by field name (`/T`, nested names joined with dots). The script fails with a non-zero exit when a name does not exist instead of silently skipping it, when a value is not one of the allowed states, and when the saved file fails the verification below. Pass `--flatten` to bake the values into the page and drop the form.
23
+ Text fields take strings; checkboxes booleans or the state name; radio groups the state to select; choice fields a string or list. Keys are field names (`/T`, nested names joined with dots). `--flatten` bakes the values in and drops the form. The script exits non-zero on an unknown name (it never skips silently), a value outside the allowed states, or a saved file that fails the verification below.
24
24
 
25
25
  ## The discipline that makes a fill real
26
26
 
27
- pypdf writes `/V` and rebuilds the widget appearance. Both must be re-checked after saving, because a writer can update one and not the other:
27
+ pypdf writes `/V` and rebuilds the widget appearance; a writer can do one without the other, so check both after saving.
28
28
 
29
29
  1. Reopen the saved file.
30
- 2. For every field you set, read `/V` from the field dictionary and compare it with the requested value.
31
- 3. For every widget, confirm `/AP /N` exists and is a non-empty stream with a `/BBox`; for radio and checkbox groups, confirm `/AS` names a state that exists in `/AP /N`.
30
+ 2. Read `/V` for every field you set and compare it with the requested value.
31
+ 3. Confirm every widget has a non-empty `/AP /N` stream with a `/BBox`; for radio and checkbox groups, `/AS` must name a state present in `/AP /N`.
32
32
  4. Render the page and look at the field.
33
33
 
34
- Steps 1–3 catch a stale or missing appearance; step 4 catches a `/V` that is right while the page still shows the old text. The script does 1–3 itself and prints `applied`/`not_applied`; treat `not_applied` as a failure to fix, never as a warning.
34
+ Steps 1–3 catch a stale or missing appearance; step 4 catches a right `/V` over old text. The script does 1–3 and prints `applied`/`not_applied`; `not_applied` is a failure to fix, not a warning.
35
35
 
36
- When you build the check by hand:
36
+ By hand:
37
37
 
38
38
  ```bash
39
39
  "<python>" - <<'PY'
@@ -49,19 +49,19 @@ for name, field in fields.items():
49
49
  PY
50
50
  ```
51
51
 
52
- Radio groups are the common failure: pypdf updates the group `/V`, but the widget-level `/AS` for the previously selected button can stay behind, so two buttons look selected. The script resynchronises every widget's `/AS` from the group value; if you fill by hand, do the same.
52
+ Radio groups fail most: pypdf updates the group `/V` but leaves the old button's `/AS`, so two look selected. The script resyncs every widget's `/AS` from the group value; do the same by hand.
53
53
 
54
54
  ## Flatten
55
55
 
56
- Flattening is for files that must no longer be editable. Update with `flatten=True` (the value is painted into the page content), then remove every widget annotation (`subtypes="/Widget"`) and drop `/AcroForm` from the catalog. Verify afterwards: no `/Widget` annotations remain and the catalog has no `/AcroForm`. If widgets remain, the file still looks editable in readers even though the content carries the value.
56
+ Flatten when the file must stay uneditable: fill with `flatten=True` (the value is painted into page content), remove every widget annotation (`subtypes="/Widget"`), and drop `/AcroForm` from the catalog. Then confirm no `/Widget` and no `/AcroForm` remain; otherwise readers still treat the file as editable.
57
57
 
58
58
  ## What cannot be filled, and the fix
59
59
 
60
- - **A field with no appearance stream and a broken `/DA`**: the reader may refuse to show a new value. Render after filling; if the value is invisible, rebuild that widget's appearance with reportlab and merge it over the page.
61
- - **A signature field (`/FT /Sig`)**: signing requires a certificate and a signer implementation that this runtime does not ship. Adding text to a signed field invalidates the signature. Tell the user the signature cannot be created here and what would (a certificate plus a signing tool).
62
- - **A form where the values are drawn as static text, not widgets**: the file is not a form. Inspect with `page.get_text()` — if the labels and lines are plain content and `reader.get_fields()` is empty, treat it as an edit job, not a fill job.
63
- - **A flat, scanned form**: no field tree exists; filling means drawing text at coordinates (a stamp overlay) and the render decides whether it lines up.
60
+ - **No appearance stream and a broken `/DA`**: the reader may not show a new value. Render after filling; if invisible, rebuild that widget's appearance with reportlab and merge it over the page.
61
+ - **A signature field (`/FT /Sig`)**: needs a certificate and a signer this runtime does not ship, and text on a signed field invalidates the signature. Say it cannot be created here; it needs a certificate plus a signing tool.
62
+ - **Values drawn as static text**: not a form. If `page.get_text()` shows the labels and lines and `reader.get_fields()` is empty, treat it as an edit job.
63
+ - **A flat, scanned form**: no field tree; draw text at coordinates (a stamp overlay) and let the render decide whether it lines up.
64
64
 
65
65
  ## Verify
66
66
 
67
- Always: reopen, check `/V`, check `/AP`, then `"<python>" "<cli>" pdf render --input "<file-filled.pdf>" --out-dir "render"` and `read_image` the page. The filled value must be visible inside its widget box, without clipping or overlapping the box border.
67
+ Reopen, check `/V`, check `/AP`, then `"<python>" "<cli>" pdf render --input "<file-filled.pdf>" --out-dir "render"` and `read_image`. The value must be visible inside its widget box, not clipped or over the border.
@@ -1,6 +1,6 @@
1
1
  # Read text, tables, and page images
2
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.
3
+ Extraction reads the page content stream; glyph runs carry position and font, which a library groups into words and lines. A page without text operators (a scan, a photo export) yields nothing, whatever the extractor — a property of the file, not of the tool.
4
4
 
5
5
  ## Fast path: plain text and structure
6
6
 
@@ -16,7 +16,7 @@ Returns `page_count`, `word_count`, document metadata, per-page text (with chara
16
16
  "<python>" "<skill>/scripts/extract_text.py" --input "<file.pdf>" --json-out "<tables.json>"
17
17
  ```
18
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.
19
+ Detection uses the page's ruling lines and word spacing, so a borderless table often comes back as running text; `--layout` keeps the original line spacing for a manual look.
20
20
 
21
21
  When the detection looks wrong, inspect rather than guess:
22
22
 
@@ -32,11 +32,11 @@ with pdfplumber.open("in.pdf") as pdf:
32
32
  PY
33
33
  ```
34
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.
35
+ `chars`, `lines`, and `rects` tell you whether the table has rules at all. With no lines, read it from `extract_text` with `--layout`, where column gaps survive as spaces.
36
36
 
37
37
  ## Page images
38
38
 
39
- A page may contain embedded rasters that are invisible in text extraction. List them with PyMuPDF:
39
+ A page may embed rasters that text extraction does not show. List them with PyMuPDF:
40
40
 
41
41
  ```bash
42
42
  "<python>" - <<'PY'
@@ -48,15 +48,15 @@ for number, page in enumerate(doc, 1):
48
48
  PY
49
49
  ```
50
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.
51
+ Extract one with `page.get_pixmap(clip=...)` or `doc.extract_image(xref)` when the user needs the picture itself. To *see* it, render the page at 150–200 dpi and crop with `read_image`.
52
52
 
53
53
  ## What fails and how to resolve it
54
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.
55
+ - **Scanned page, no text**: no text layer exists; say so, with resolutions from `references/limits.md`. Never claim extraction succeeded on an empty result.
56
+ - **Empty string from a page that shows text**: broken ToUnicode map, or text drawn as vector outlines. Check the render; outlines cannot be recovered by extraction.
57
+ - **Wrong reading order in multi-column layouts**: extraction follows operator order. Crop each column with a rect and extract per column; `get_textbox(rect)` on a PyMuPDF page is the simplest split.
58
58
  - **Ligatures and soft hyphens**: extraction may glue or split words; quote extracted text only after reading the same passage on the render.
59
59
 
60
60
  ## Verify
61
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.
62
+ The render is the check: the characters you return must be visible in it. Before building on extracted text, check for a page whose text is suspiciously short (missing text layer), duplicated (headers repeated per page), or full of replacement glyphs (broken encoding).