docxide-pdf 0.16.3

Library and CLI for converting DOCX files to PDF, matching Microsoft Word's output as closely as possible
Documentation
<!-- Generated by the codebase-extraction-audit workflow on 2026-06-21.
     13 reviewers/hunters + adversarial verification per candidate.
     71 candidates verified, 41 dropped, 30 survivors. -->

# docxside-pdf Extraction/Refactor Audit

> **Status: ALL 30 survivors applied (2026-06-21), 11 commits on `main`.** No
> rendering regressions — 208/208 scores unchanged throughout. Two behavior
> changes landed: A13 (SmartArt fontRef tint/shade — latent, no fixture exercises
> it) and C5 (cell textboxes use the shared renderer — only `japanese_land`'s
> pixels shifted, by noise; its visual hash is intentionally left unaccepted).
> The "leave alone" list at the bottom was not touched, by design.

Verified 71 candidates. **Dropped 41** where `real:false`, `net_simplification:false`, or score ≤2 — these were either non-duplications (different ST_ enums, parse/render mirrors), already-DRY code (text-width via `FontEntry::word_width`, ResolvedCommand emission), giant functions whose "clean seams" actually straddle pervasive `&mut` state (`render_paragraph_block`, `parse_table_node`, three table row loops), or cosmetic file-splits/renames with no logic removed. The 30 survivors below are real, net-negative-LOC, and low-coupling.

After dedup, several agents reported the same issues under different source tags (color helpers in comments.rs reported twice; `resolve_w14_color`/scheme-color reported three times; EMF COLORREF + emf.rs color helpers; `v=="1"||"true"` reported twice). Merged into single entries citing all locations.

---

## (A) Cross-module duplication worth unifying

| # | What + where | Fix | Effort |
|---|---|---|---|
| A1 | **comments.rs reimplements `fill_rgb`/`stroke_rgb`** (plus an identical-twin `fill_rgb_in_text`) — `src/pdf/comments.rs:282-304` vs canonical `src/pdf/color.rs:4-11`. *(reported 3×: review:pdf-render, dup:color, dup:units-shapefill — merged)* | Delete the 3 local fns, `use super::color::{fill_rgb, stroke_rgb};`, rewrite the 3 `fill_rgb_in_text` calls (134/143/156) to `fill_rgb`. ~20 lines gone. | **S** |
| A2 | **`char_to_gid` encode dispatch duplicated 5×** — `encode_against`/`encode_for`/`encode_label` (`list_label.rs:51`, `comments.rs:275`, `table.rs:788`) + inline in `charts.rs:291` + wrapped in `layout.rs:1355`. | Add `impl FontEntry { pub(crate) fn encode(&self, text) -> Vec<u8> }` into the **existing** impl block at `fonts/mod.rs:52-118`; delete the 3 named fns, route callers through it. | **S** |
| A3 | **`rFonts` ascii/hAnsi name extraction copy-pasted 4×** — `numbering.rs:86`, `paragraph.rs:63`, `styles.rs:828`, `styles.rs:867`. Deliberately ignores theme fonts (so `resolve_font_from_node_opt` cannot replace it). | Add `pub(super) fn rfonts_ascii_name(rpr) -> Option<String>` in styles.rs; take the **rPr** node so all 4 sites collapse to one call. | **S** |
| A4 | **Half-point font-size parse reinlined 4×** despite `parse_font_size` already existing — `styles.rs:228` (the helper) vs inlined at `runs.rs:311`, `runs.rs:524`, `paragraph.rs:59`, `numbering.rs:93`. | Move `parse_font_size` to `pub(super)` in `docx/mod.rs`; replace inlined `sz/2.0` sites with `.and_then(parse_font_size)`. | **S** |
| A5 | **`resolve_w14_color` DML-fallback is a verbatim copy of `resolve_dml_color`** — `wordart.rs:252-261` vs `color.rs:95-104`. *(reported 3×: review:docx-content, dup:color, dup:units-shapefill — merged)* | Replace the fallback block with `.or_else(\|\| color::resolve_dml_color(fill_node, theme))`; add `resolve_dml_color` to the existing `use super::color`. Free prstClr support as a bonus. Keep the w14-namespaced head. | **S** |
| A6 | **Inline `wml_attr` re-implementation (`wml(n,name).attribute(WML_NS,"val")`) ~10×** — `styles.rs:244/250/256/594/698`, `embedded_fonts.rs:98/101/105`, `sections.rs:68`, `runs.rs:387`. Helper already exists at `mod.rs:245` and is used 20+ times in the same files. | Replace the inline chains with `wml_attr(node, name)`. Add `wml_attr` to embedded_fonts.rs's import. Leave the tables.rs `.and_then(wml(...))` chains. | **S** |
| A7 | **JPEG image-XObject emit block duplicated 3×** (incl. the load-bearing `match jpeg_components {1=>gray,4=>cmyk,_=>rgb}`) — `images.rs:226`, `images.rs:468`, `images.rs:499`. *(merges the standalone "color_space dispatch" sub-finding)* | Add `fn write_jpeg_xobject(pdf, ref, data, w, h, components:u8, smask:Option<Ref>)` next to existing `write_image_xobject`; route all 3 sites (site 3 passes components=3). | **M** |
| A8 | **pdf/emf.rs `rgb_op` reinvents color.rs `fill_rgb`/`stroke_rgb`** — `pdf/emf.rs:227` + 4 manual call sites vs `color.rs:4-11`. | `use super::color::{fill_rgb, stroke_rgb};`, replace `rgb_op`+manual calls with `fill_rgb/stroke_rgb`, delete `rgb_op`. Output byte-identical. | **S** |
| A9 | **`is_wml` name+namespace predicate defined twice + inlined ~12×** — `tables.rs:60` (`is_wml`) and `headers_footers.rs:16` (`is_wml_element`), same body. | Add one `pub(super) fn is_wml(node, name) -> bool` to mod.rs; delete both locals, update 12 call sites (drop the `&` at tables.rs sites). Skip the hoisted-`ns` loops in runs.rs/images.rs — helper reads worse there. | **S** |
| A10 | **`v=="1"\|\|v=="true"` truthiness copied 9×** — `sections.rs:86/90`, `mod.rs:455/459`, `styles.rs:614/616`, `tables.rs:311`, `images.rs:480`, `wordart.rs:31`. Also fixes the latent missing-`"on"` ST_OnOff gap. *(reported 2×: review:docx-parse-core, dup:xml-helpers — merged)* | Add `pub(super) fn parse_on_off(v:&str) -> bool { matches!(v,"1"\|"true"\|"on") }` to mod.rs; apply point-free at all 9 sites. Use the `&str` form, not `attr_bool` (doesn't fit tables.rs:311's hex bitmask). | **S** |
| A11 | **`char_spacing` parse duplicated** — `runs.rs:386` inlines what `styles.rs:255 parse_char_spacing` already does (and styles.rs:641 already uses the target idiom). | Make `parse_char_spacing` `pub(super)`, add to runs.rs's `use super::styles`, replace with `rpr.and_then(parse_char_spacing).unwrap_or(...)`. | **S** |
| A12 | **`twips_attr` re-implementation for dxaOrig/dyaOrig** — `images.rs:810-815` byte-for-byte = `twips_attr` body (`mod.rs:249`). | Replace both blocks with `twips_attr(obj,"dxaOrig")`/`"dyaOrig"`; add `twips_attr` to images.rs import. | **S** |
| A13 | **scheme-color resolution sequence repeated 3 (clean) sites** — `color.rs:99`, `wordart.rs:256` (DML fallback), `smartart.rs:194` (fontRef, drops transforms). *(W14 site excluded — needs namespaced `val`)* | Add `resolve_scheme_clr(node, theme)` in color.rs; use in resolve_dml_color + wordart DML-fallback. smartart fontRef switch is a behavior change (starts honoring tint/shade) — re-run SmartArt fixtures before accepting. | **S** |
| A14 | **Identical 4-cubic circle emitter** — `charts.rs::draw_circle` (239) vs inlined ArrowEnd::Oval arm `positioning.rs:360`; differing kappa spelling. | Move `draw_circle` to `pdf/helpers.rs` as `pub(super)` (not charts.rs — avoids positioning→charts dep), single kappa `0.5522847498`, call from both. Output unchanged. | **S** |
| A15 | **VML style `key:val;pt` parsing duplicated** — `wordart.rs:302`, `textbox.rs:688`, `textbox.rs:806`, `images.rs:782`, plus a 6th at `runs.rs:1342`. | Add one `pub(super) fn parse_pt(&str) -> f32` to mod.rs (canonical trim/parse) — dedups AND fixes inconsistent variants. The bigger win the original missed: factor the two near-identical textbox.rs blocks into `parse_vml_box_style(&str) -> VmlBoxStyle` (~25 lines). Skip the iterator helper. | **M** |
| A16 | **Per-module `emu_to_pt`/`emu_attr` closures re-wrap shared `emu_attr`** — `textbox.rs:308`, `smartart.rs:262` shadow the already-imported `super::emu_attr` (`mod.rs:61`). | Delete the redundant closures, call `super::emu_attr` directly (call sites already match). Drop the proposed `emu_attr_or` — one consumer only; leave textbox.rs:289's non-zero-default closure. | **S** |
| A17 | **`ln` stroke parsing (solidFill+width+noFill) repeated** — `textbox.rs:381`, `smartart.rs:170`. *(images.rs:141 excluded — different srgbClr-only, no-theme semantics; folding it in is a behavior change)* | Extract `parse_line_stroke(ln_node, theme) -> Option<([u8;3],f32)>` near `parse_solid_fill`; replace smartart wholesale, use in textbox's parse_solid_fill arm (keep its lnRef fallback). | **S** |

---

## (B) Over-long files with a clean seam to split

| # | What + where | Fix | Effort |
|---|---|---|---|
| B1 | **`render_chart` is a ~715-line god function** — `pdf/charts.rs:312-1026`. The 260-line data-rendering match (588-848) is a genuinely clean seam (reads only computed locals, emits to content). | Extract the match into in-file free fns: `draw_bars_vertical`/`_horizontal`, `draw_line`/`_area`/`_scatter`/`_bubble`, passing a `PlotRect{x,y,w,h}` + scalars; `draw_bubble` also needs `&mut HashSet<u8> alpha_states`. Leave axis/legend orchestration. **No new file.** Optionally collapse the 4 bar arms to 2. | **M** |

*(Note: the bigger god-functions — `render_paragraph_block` 1780 lines, `parse_table_node` 715 lines, `render()` 680 lines, `render_paragraph_lines` 670 lines — were all dropped: their proposed seams straddle pervasive `&mut state`/page-break logic and would add 13-30-arg signatures without removing any duplication. See "leave alone".)*

---

## (C) Other structural wins / dead code

| # | What + where | Fix | Effort |
|---|---|---|---|
| C1 | **Identical 3-pass box-blur loop duplicated 2×** — `pdf/color.rs:72-124` (shadow) vs `224-269` (inner-shadow), ~45 lines of error-prone pixel arithmetic, byte-identical bar comments + a loop-var rename. | Extract `fn box_blur_3pass(buf:&mut [u8], px_w, px_h, box_r)`, guard `if box_r>0`, call from both. | **M** |
| C2 | **Grayscale mask XObject emit repeated 5×** — `images.rs:182/324/395/456/561`. | Add `fn write_gray_mask_xobject(pdf, ref, compressed, w, h)` (caller-allocs Ref, like `write_image_xobject`); always `interpolate(true)` — fixes the latent omission at the PNG-alpha site (324). | **M** |
| C3 | **Solid-color + gray-mask XObject pattern duplicated** — `embed_shadow` (`images.rs:387`) vs inline inner_shadow branch (`images.rs:555`); mask half also at reflection (456) + alpha-SMask (322). | Extract `write_solid_color_with_gray_mask(...)`; de-inline inner_shadow into its own `embed_inner_shadow` fn paralleling `embed_shadow`. *(Overlaps C2; do together.)* | **S** |
| C4 | **`Run::default()` test boilerplate duplicated** — `fonts/mod.rs:566`, `layout.rs:2143`. Every new Run field forces synced edits to both. | Collapse each to struct-update over `Run::default()` (set only the 2-4 fields each test reads). ~70 lines gone. Helpers stay independent. | **S** |
| C5 | **`table.rs::render_simple_textbox` is a cut-down 3rd textbox renderer** — `table.rs:478-550` vs `textbox_render.rs:306-443`; Solid-fill-only, no tabs/images/labels/indents. | Replace its text loop with `render_textbox_paragraphs(...)` and its fill block with `render_shape_fill`; thread a real `&mut Vec<LinkAnnotation>` (restores cell hyperlinks). Both helpers already `pub(super)`. | **M** |
| C6 | **`table.rs` render_nested_table ↔ render_header_footer_table near-identical** — `table.rs:575-654` vs `1617-1695` (sole body diff: a var name). *(narrowed from the dropped 3-way row-loop claim — body path is a genuinely different 3rd impl)* | Unify only these two into one helper taking `col_widths/cm/table_left/merge_spans` + `&mut f32` cursor. Leave `render_table_row` alone. | **M** |
| C7 | **Inline `/20.0` twips conversions bypass `twips_to_pts`** — `mod.rs:363/373/377`, `images.rs:812/815`, `styles.rs:462-463`. | Swap the 6 magic `/20.0` for `twips_to_pts(...)`. **Do NOT touch styles.rs:464** (`/240.0`, a different unit). | **S** |
| C8 | **EMF COLORREF: dead `color_at` closure + open-coded 3×** — `docx/emf.rs:126-130` (dead, live compiler warning), open-coded at 210/228. *(reported 2×: review:docx-content, dup:image-vector — merged)* | Delete the dead closure (silences a warning); add `fn colorref(v:u32)->[u8;3]` and call from `decode_brush`/`decode_extcreatepen`. | **S** |
| C9 | **Dead `parse_tab_stops` wrapper** kept alive only by its own test — `docx/mod.rs:387-391` (`#[allow(dead_code)]`). | Delete the wrapper; change the test (mod.rs:1032) to `parse_tab_stops_with_clears(node).0`. | **S** |
| C10 | **Dead `FontEntry::char_width_1000_with_fallback`** — `fonts/mod.rs:69-82`, `#[allow(dead_code)]`, zero callers anywhere. | Delete the method + doc comment. | **S** |
| C11 | **Tab-stop merge algorithm (0.5pt-tolerance) duplicated** — `styles.rs:1009-1027` vs `paragraph.rs:273-288`; load-bearing magic constant + override rule. | Extract `pub(super) fn merge_tab_stops(dst:&mut Vec<TabStop>, clears:&[f32], incoming:Vec<TabStop>)` in mod.rs; styles.rs passes `.clone()`. Drop the now-redundant trailing sort at styles.rs:1027. | **S** |
| C12 | **Dead `SampledBoundary::min_y` / `x_range`** — `pdf/wordart.rs:379-385` + `394-400`, `#[allow(dead_code)]`, no callers. *(narrowed: the other items in this finding — `is_filled`, `EvaluatedPath.fill` — are used by unit tests, NOT dead)* | Delete the two methods (~14 lines). Leave `is_filled`/`.fill` (exercised by tests at `definitions.rs:15330/15332/15362`). | **S** |

---

## Top 3 do-first

1. **A2 — `FontEntry::encode` method** (S). Highest-leverage single dedup: collapses 5 scattered copies of the WinAnsi-fallback branch onto the type that owns `char_to_gid`, and lets 4 modules drop imports. The branch is exactly the kind that drifts (one site forgets the fallback).
2. **C1 + C2 + C3 — the images.rs/color.rs mask + XObject family** (M, do as one pass). ~90 lines of error-prone PDF-emission boilerplate (box-blur, gray-mask, solid+mask) across 5-7 sites that must stay lockstep with pdf-writer's API; one already diverged (PNG-alpha missing `interpolate`).
3. **A1 + A8 + comments fix — the `fill_rgb`/`stroke_rgb` reinventions** (S). Pure deletion, zero behavior change, follows the pattern 6+ sibling modules already use. Knock both holdout modules (comments.rs, emf.rs) out in one go.

*(Honorable mention: the cluster A3/A4/A6/A10/A11/A12 are all S-effort docx/ helper consolidations that share the same `pub(super)`-helper-in-mod.rs pattern — batch them in a single sweep.)*

## Leave alone

The genuine god-functions look like the obvious targets but aren't: **`render_paragraph_block`** (1780 lines), **`parse_table_node`** (715), **`render_paragraph_lines`** (670), **`render()`** (680), and the **three table row-rendering loops** all thread pervasive `&mut LayoutState`/page-break state or per-cell mutable parse-state through their phases — every proposed "clean seam" actually needs a 13-30-argument signature (or fails the borrow checker outright, e.g. the 6× `render_paragraph_lines` closure idea) and removes **zero** duplication. They're long-but-cohesive, not mixed-concern. Also leave the generated data tables (`definitions.rs` 15k lines, `text_warp_definitions.rs`), the **three structurally-identical path-command enums** (`PathCommandDef`/`Cmd<'a>`/`CustomPathCommand` — duplication is the deliberate price of the static-`&str`-vs-owned-`String` split; collapsing pessimizes the hot static path or rewrites 15k generated lines), `model/mod.rs` (cohesive IR root, already split), and the `pdf/color.rs` misnaming (cosmetic, ~15 import sites of churn for no logic change).