dsh-office-kit 0.4.1 → 0.4.3
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/assets/skills/excel-live-control/SKILL.md +120 -148
- package/assets/skills/excel-live-control/container_tools/live_excel.py +9 -0
- package/assets/skills/excel-live-control/domain_guidance/financial_models.md +42 -58
- package/assets/skills/excel-live-control/domain_guidance/healthcare.md +36 -42
- package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +33 -34
- package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +34 -36
- package/assets/skills/excel-live-control/references/office-automation.md +127 -153
- package/assets/skills/excel-live-control/references/read_only_qna.md +40 -45
- package/assets/skills/excel-live-control/style_guidelines.md +56 -64
- package/assets/skills/excel-live-control/workflows/create_workflows.md +40 -47
- package/assets/skills/excel-live-control/workflows/edit_workflows.md +37 -45
- package/assets/skills/office-documents/SKILL.md +24 -60
- package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +5 -5
- package/assets/skills/office-documents/ooxml/comments.md +8 -8
- package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +10 -10
- package/assets/skills/office-documents/ooxml/rels_and_content_types.md +7 -7
- package/assets/skills/office-documents/ooxml/tracked_changes.md +12 -12
- package/assets/skills/office-documents/references/docx-structure.md +11 -13
- package/assets/skills/office-documents/references/template-elicitation.md +11 -13
- package/assets/skills/office-documents/references/writing-quality.md +20 -20
- package/assets/skills/office-documents/tasks/accessibility_a11y.md +16 -22
- package/assets/skills/office-documents/tasks/captions_crossrefs.md +23 -26
- package/assets/skills/office-documents/tasks/clean_tracked_changes.md +17 -22
- package/assets/skills/office-documents/tasks/comments_manage.md +9 -11
- package/assets/skills/office-documents/tasks/compare_diff.md +10 -16
- package/assets/skills/office-documents/tasks/create_edit.md +14 -16
- package/assets/skills/office-documents/tasks/equations.md +17 -28
- package/assets/skills/office-documents/tasks/fields_update.md +14 -16
- package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +19 -33
- package/assets/skills/office-documents/tasks/footnotes_endnotes.md +12 -14
- package/assets/skills/office-documents/tasks/forms_content_controls.md +16 -20
- package/assets/skills/office-documents/tasks/headings_numbering.md +17 -22
- package/assets/skills/office-documents/tasks/images_figures.md +15 -25
- package/assets/skills/office-documents/tasks/multi_doc_merge.md +13 -23
- package/assets/skills/office-documents/tasks/navigation_internal_links.md +18 -21
- package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +16 -22
- package/assets/skills/office-documents/tasks/protection_restrict_editing.md +19 -21
- package/assets/skills/office-documents/tasks/read_review.md +9 -20
- package/assets/skills/office-documents/tasks/redaction_anonymization.md +17 -18
- package/assets/skills/office-documents/tasks/sections_layout.md +16 -23
- package/assets/skills/office-documents/tasks/style_lint_normalize.md +18 -22
- package/assets/skills/office-documents/tasks/tables_spreadsheets.md +21 -31
- package/assets/skills/office-documents/tasks/templates_style_packs.md +17 -21
- package/assets/skills/office-documents/tasks/toc_workflow.md +10 -14
- package/assets/skills/office-documents/tasks/verify_render.md +11 -23
- package/assets/skills/office-documents/tasks/watermarks_background.md +11 -15
- package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +9 -9
- package/assets/skills/office-documents/troubleshooting/run_splitting.md +12 -12
- package/assets/skills/office-pdf/SKILL.md +36 -51
- package/assets/skills/office-pdf/references/form-fields.md +20 -20
- package/assets/skills/office-pdf/references/geometry.md +10 -10
- package/assets/skills/office-pdf/references/layout-quality.md +13 -13
- package/assets/skills/office-pdf/references/limits.md +12 -56
- package/assets/skills/office-pdf/tasks/annotations.md +5 -5
- package/assets/skills/office-pdf/tasks/create.md +6 -9
- package/assets/skills/office-pdf/tasks/edit-pages.md +14 -14
- package/assets/skills/office-pdf/tasks/forms.md +15 -15
- package/assets/skills/office-pdf/tasks/read.md +9 -9
- package/assets/skills/office-pdf/tasks/redact.md +18 -18
- package/assets/skills/office-pdf/tasks/stamp.md +10 -10
- package/assets/skills/office-presentations/SKILL.md +71 -91
- package/assets/skills/office-presentations/container_tools/render_slides.py +1 -1
- package/assets/skills/office-presentations/domain_guidance/finance.md +16 -16
- package/assets/skills/office-presentations/recipes/README.md +24 -35
- package/assets/skills/office-presentations/recipes/autolayout.md +12 -13
- package/assets/skills/office-presentations/recipes/connectors.md +11 -12
- package/assets/skills/office-presentations/recipes/images.md +11 -12
- package/assets/skills/office-presentations/recipes/master-and-layouts.md +18 -18
- package/assets/skills/office-presentations/recipes/native-charts.md +8 -8
- package/assets/skills/office-presentations/recipes/rich-text-runs.md +13 -13
- package/assets/skills/office-presentations/recipes/speaker-notes.md +7 -7
- package/assets/skills/office-presentations/recipes/tables.md +14 -14
- package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +9 -9
- package/assets/skills/office-presentations/recipes/theme-tokens.md +7 -8
- package/assets/skills/office-presentations/references/authoring-workflow.md +38 -43
- package/assets/skills/office-presentations/references/finalization.md +10 -12
- package/assets/skills/office-presentations/references/native-bullets.md +14 -18
- package/assets/skills/office-presentations/references/native-evidence.md +18 -32
- package/assets/skills/office-presentations/references/style-guidelines.md +27 -32
- package/assets/skills/office-presentations/references/templates.md +31 -60
- package/assets/skills/office-presentations/routing/google_slides.md +4 -4
- package/assets/skills/office-presentations/tasks/apply-annotations.md +17 -17
- package/assets/skills/office-presentations/tasks/charts-tables-notes.md +20 -23
- package/assets/skills/office-presentations/tasks/create-deck.md +19 -19
- package/assets/skills/office-presentations/tasks/edit-deck.md +14 -16
- package/assets/skills/office-presentations/tasks/inspect-and-verify.md +31 -34
- package/assets/skills/office-presentations/tasks/template-following.md +23 -28
- package/assets/skills/office-spreadsheets/SKILL.md +32 -39
- package/assets/skills/office-spreadsheets/recipes/charts.md +8 -26
- package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +9 -26
- package/assets/skills/office-spreadsheets/recipes/data-tables.md +7 -23
- package/assets/skills/office-spreadsheets/recipes/named-ranges.md +8 -21
- package/assets/skills/office-spreadsheets/recipes/number-formats.md +7 -23
- package/assets/skills/office-spreadsheets/recipes/sparklines.md +7 -20
- package/assets/skills/office-spreadsheets/references/charts.md +15 -34
- package/assets/skills/office-spreadsheets/references/domain/financial-models.md +10 -25
- package/assets/skills/office-spreadsheets/references/domain/healthcare.md +8 -19
- package/assets/skills/office-spreadsheets/references/domain/marketing.md +8 -22
- package/assets/skills/office-spreadsheets/references/domain/research.md +8 -19
- package/assets/skills/office-spreadsheets/references/formula-patterns.md +20 -50
- package/assets/skills/office-spreadsheets/references/style-guidelines.md +19 -44
- package/assets/skills/office-spreadsheets/references/workbook-structure.md +12 -35
- package/assets/skills/office-spreadsheets/routing/google-sheets.md +11 -26
- package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +10 -25
- package/assets/skills/office-spreadsheets/tasks/annotations.md +12 -30
- package/assets/skills/office-spreadsheets/tasks/create-workbook.md +5 -5
- package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +2 -2
- package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +5 -5
- package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +9 -9
- package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +10 -30
- package/assets/skills/shared/annotation-recipes.md +98 -120
- package/assets/skills/shared/common-workflow.md +70 -80
- package/dist/index.js +105 -29
- package/package.json +1 -1
- package/postinstall.mjs +58 -17
- package/python/office_cli.py +70 -7
|
@@ -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
|
|
8
|
-
- `/FT` —
|
|
9
|
-
- `/V` — current value
|
|
10
|
-
- `/Ff` — flags bitmask
|
|
11
|
-
- `/DA` — default appearance string;
|
|
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
|
|
15
|
-
- `/NeedAppearances` in `/AcroForm` —
|
|
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
|
|
20
|
-
2. The widget's `/AP /N` exists
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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`
|
|
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.
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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` (
|
|
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
|
|
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
|
|
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
|
|
8
|
-
- Every page has the intended size and orientation —
|
|
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
|
|
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
|
|
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 —
|
|
18
|
-
- Extracted text matches
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
pypdf
|
|
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
|
|
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`.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
62
|
+
- Full layout checklist: `references/layout-quality.md`.
|
|
66
63
|
|
|
67
64
|
## Verify
|
|
68
65
|
|
|
69
|
-
Render
|
|
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
|
|
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
|
|
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
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- `--order` keeps and reorders; pages
|
|
15
|
-
- `--remove` drops indices from the list
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
68
|
-
- for merges, confirm
|
|
69
|
-
- for deletions of numbered pages, tell the user which numbers were removed, because the remaining page labels
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
31
|
-
3.
|
|
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`
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
- **
|
|
61
|
-
- **A signature field (`/FT /Sig`)**:
|
|
62
|
-
- **
|
|
63
|
-
- **A flat, scanned form**: no field tree
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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**:
|
|
56
|
-
- **Empty string from a page that shows text**:
|
|
57
|
-
- **Wrong reading order in multi-column layouts**:
|
|
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
|
-
|
|
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).
|