ZPL Diff Auto-Fix Skill
Purpose
Automatically detect labels with high rendering diff percentages, isolate the problematic ZPL elements into standalone test files for focused debugging, and iteratively fix the rendering logic until the diff drops below a target threshold (default 1%).
Critical Rule — Fix Rendering Code, Never Tests
DO NOT modify diff tests, golden test infrastructure, reference images, tolerance thresholds
(to hide regressions), or test comparison logic to make diffs pass. The goal is to improve
rendering accuracy, not to weaken the test harness.
DO fix the root cause in rendering code and logic:
- Parsers:
src/parsers/
- Elements:
src/elements/
- Barcode encoders:
src/barcodes/
- Renderer / drawing:
src/drawers/
- Font handling:
src/assets/
Threshold updates in docs/DIFF_THRESHOLDS.md are only allowed after a rendering fix
genuinely lowers the diff percentage — set the new tolerance slightly above the new measured
diff, never raise it to paper over a regression.
When to Use
- A label has a diff percentage higher than desired
- You want to systematically reduce rendering differences across all labels
- You need to extract standalone ZPL snippets to isolate a rendering bug
- After implementing a rendering change, you want to verify and fine-tune it
Prerequisites
Before starting, ensure:
- The diff report is up to date:
cargo test --test e2e_diff_report -- --nocapture
- All current tests pass:
cargo test
- You have reference PNG images in
testdata/ for the target label(s)
Procedure
Phase 1: Scan — Identify High-Diff Labels
Read the diff report from testdata/diffs/diff_report.txt
Read the tolerance thresholds from docs/DIFF_THRESHOLDS.md
Identify target labels — either a specific label from the user's request, or all labels with diff > 1%:
- List labels sorted by diff percentage (highest first)
- For each, note the current diff%, tolerance, and primary diff source
Pick the highest-impact label to fix first (highest diff% that is potentially fixable — skip MaxiCode/PDF417 unless specifically requested)
Phase 2: Analyze — Find Problematic Elements
Read the ZPL source from testdata/labels/<name>.zpl or testdata/unit/<name>.zpl
Parse the ZPL to identify all commands and elements:
Use src/skill/snippet_extractor.rs → split_zpl_commands() to list all commands
Use src/skill/snippet_extractor.rs → group_commands_into_spans() to group into element spans
Inspect the diff image at testdata/diffs/<name>_diff.png — red pixels show where rendering differs from Labelary
Correlate diff regions with elements using src/skill/element_analyzer.rs:
- Parse the ZPL to get
LabelInfo
- Compute bounding boxes for each element via
compute_element_bbox()
- Load the diff image and count red pixels in each bbox via
correlate_diff_regions()
- Report which elements contribute most to the total diff
Rank elements by diff contribution — focus on the top 3-5 contributors
9b. Classify each element's diff as ContentDiff, PositionDiff, or Mixed using
src/skill/diff_classifier.rs → classify_element_diffs():
```
// Full pipeline with classification:
Use element_analyzer::analyze_label_with_classification(label, diff_path, zpl, use_labelary)
```
The classifier renders each element in isolation, optionally fetches the Labelary reference
for the same snippet, and compares the two renders:
| Isolated snippet diff | Full-label bbox diff | Classification |
|----------------------|----------------------|----------------|
| Low (< 2%) | High (> 2%) | **PositionDiff** — element placed wrong |
| High (> 2%) | Any | **ContentDiff** — element renders wrong |
| High (> 2%) | High (> 2%) | **Mixed** — both |
| Low (< 2%) | Low (< 2%) | No classification (no meaningful diff) |
For **PositionDiff** elements, the classifier also attempts to detect the offset vector (dx, dy):
- Shadow detection: two clusters of red pixels → direct offset measurement
- Centroid shift: centroid of diff pixels vs. expected element center
Phase 3: Extract — Create Standalone Snippets
Extract each high-diff element into a standalone ZPL file:
Use src/skill/snippet_extractor.rs → extract_element()
This creates testdata/unit/<label>_<index>.zpl with:
^XA / ^XZ wrapper
- Global state commands (
^PW, ^CF, ^BY, ^CI, ^FW, ^PO, ^LR) preserved
- The specific element's command span (
^FO/^FT through ^FS)
Render each snippet to verify it renders correctly in isolation:
cargo run -- convert testdata/unit/<label>_<index>.zpl
Both unit tests (testdata/unit/) and carrier labels (testdata/labels/) use the full
Labelary canvas (101.625×203.25 mm at 8 dpmm → 813×1626 px). Unit golden PNGs are
Labelary references at 812×1624 px (Labelary rounds by 1 px due to float precision).
Get Labelary reference for each snippet — post the snippet ZPL to:
POST http://api.labelary.com/v1/printers/8dpmm/labels/4.005x8.01/0/
Save the reference as testdata/unit/<label>_<index>_ref.png.
Note: Labelary renders at 812×1624 for our canvas size (1 px smaller than the local
renderer's 813×1626 due to floating-point rounding of the inch dimensions). This causes
a ~0.25% systematic diff; direct pixel comparison is done over the overlapping region.
Compare snippet renders — this isolates the rendering difference to a single element
Phase 4: Fix — Iterative Rendering Fixes
For each high-diff element, starting with the highest contributor:
Identify the element type, diff classification, and fix category:
Use FixCategory::from_classification(element_type, classification) to select the fix strategy:
| Classification |
Fix Strategy |
Target Source Files |
| ContentDiff + Text |
FontMetrics |
src/drawers/renderer.rs, src/elements/font.rs |
| ContentDiff + Barcode128 |
BarcodeEncoding |
src/barcodes/code128.rs, src/drawers/renderer.rs |
| ContentDiff + BarcodeEan13 |
BarcodeEncoding |
src/barcodes/ean13.rs |
| ContentDiff + Barcode2of5 |
BarcodeEncoding |
src/barcodes/interleaved2of5.rs |
| ContentDiff + Barcode39 |
BarcodeEncoding |
src/barcodes/code39.rs |
| ContentDiff + BarcodePdf417 |
BarcodeEncoding |
src/barcodes/pdf417.rs |
| ContentDiff + BarcodeAztec |
BarcodeEncoding |
src/barcodes/aztec.rs |
| ContentDiff + BarcodeDatamatrix |
BarcodeEncoding |
src/barcodes/datamatrix.rs |
| ContentDiff + BarcodeQr |
BarcodeEncoding |
src/barcodes/qrcode.rs |
| ContentDiff + Maxicode |
BarcodeEncoding |
src/barcodes/maxicode.rs |
| ContentDiff + GraphicBox/Circle/Line/Field |
GraphicRendering |
src/drawers/renderer.rs |
| PositionDiff (any element) |
PositionOffset |
src/parsers/zpl_parser.rs, src/elements/field_alignment.rs, src/elements/drawer_options.rs |
| Mixed (any element) |
PositionOffset first, then ContentDiff fix |
Both sets above |
For PositionDiff elements, use the detected offset vector (dx, dy) from PositionOffsetInfo
to guide the fix — look for coordinate calculation that differs by that many pixels.
Fetch the official Zebra documentation for the suspect ZPL command using the zpl-reference skill:
https://docs.zebra.com/us/en/printers/software/zpl-pg/c-zpl-zpl-commands/r-zpl-<slug>.html
Compare spec vs implementation using the fix-zpl-render skill's Phase 3 comparison table
Apply the fix to the relevant source file
Verify the fix:
# Quick check — does it compile?
cargo build
# Render the snippet
cargo run -- convert testdata/unit/<label>_<index>.zpl
# Run the specific golden test
cargo test --test e2e_golden -- <label_name> --nocapture
# Check for regressions on ALL labels
cargo test --test e2e_golden
Measure improvement:
cargo test --test e2e_diff_report -- --nocapture
Compare the new diff% against the previous value.
If diff improved without regressions: Keep the change and proceed to the next element.
If diff regressed on other labels: Revert the change and try a different approach.
Repeat from step 14 for the next highest-contributing element.
Phase 5: Finalize — Update Thresholds and Report
Update diff thresholds in docs/DIFF_THRESHOLDS.md:
- Set new diff% values for improved labels
- Set new tolerance ceilings slightly above the new diff%
Regenerate diff images:
cargo test --test e2e_diff_report -- --nocapture
Run full test suite:
cargo test
Commit all changes including:
- Source code fixes
- Updated
docs/DIFF_THRESHOLDS.md
- Updated diff images in
testdata/diffs/
- New snippet files in
testdata/unit/ (if useful for ongoing debugging)
Auto-Fix Loop Strategy
When running in fully automated mode ("fix all labels above 1%"):
1. Generate diff report
2. Sort labels by diff% descending
3. For each label above 1%:
a. Analyze element contributions
b. For each element (highest contribution first):
i. Extract standalone snippet
ii. Identify fix category
iii. Fetch Zebra spec for the command
iv. Compare spec vs implementation
v. Apply fix
vi. Build & test
vii. If regression: revert, try next element
viii.If improved: keep, re-measure
c. After all elements tried:
- If diff ≤ 1%: SUCCESS, move to next label
- If diff > 1% but improved: PARTIAL, record improvement
- If no improvement possible: SKIP (e.g., font metrics, library limitations)
4. Generate final report
Known Limitations
These labels cannot reach <1% diff due to fundamental limitations:
| Label |
Blocker |
Reason |
| ups, ups_surepost |
MaxiCode encoder |
Proprietary encoding, no compliant open-source encoder |
| fedex |
PDF417 encoding |
rxing encoder uses different compaction modes (valid but visually different) |
| aztec_ec |
Aztec encoding |
rxing Aztec encoder differs in symbol sizing |
| pnldpd |
Multiple (Aztec + GFA + font) |
Compound issues from barcode encoding + font metrics |
| Most text-heavy labels |
Font metrics |
Helvetica Bold vs Zebra proprietary fonts |
Utility Code Reference
The skill uses Rust utilities in src/skill/:
diff_scanner.rs — Parse diff report, find high-diff labels, suggest closest label names
element_analyzer.rs — Compute element bounding boxes, correlate diff pixels with elements
snippet_extractor.rs — Split ZPL commands, group into spans, extract standalone snippets
models.rs — Data structures: DiffReport, ElementBBox, ZplSnippet, etc.
error.rs — Error types: ScanError, AnalyzeError, ExtractError
Phase 6: Commit — Generate Summary
Generate a commit message summarizing the changes:
- List labels improved with before/after diff percentages
- Summarize which rendering modules were modified
- Note any threshold updates
- Format:
fix(render): reduce diff for <labels> — <brief description>
Example:
fix(render): reduce diff for amazon, dhl — fix ^FO baseline offset and Code128 quiet zone
Labels improved:
- amazon: 4.2% → 0.8%
- dhl: 3.1% → 0.6%
Changes:
- src/parsers/zpl_parser.rs: correct ^FO y-offset calculation for rotated fields
- src/barcodes/code128.rs: remove extra quiet zone module on right side
- docs/DIFF_THRESHOLDS.md: update tolerances for amazon (1.0%), dhl (0.8%)
References
Source: GOODBOY008/labelize — distributed by TomeVault.
1---2name: goodboy008-labelize-labelize3description: ZPL Diff Auto-Fix Skill4---56# ZPL Diff Auto-Fix Skill78## Purpose910Automatically detect labels with high rendering diff percentages, isolate the problematic ZPL elements into standalone test files for focused debugging, and iteratively fix the rendering logic until the diff drops below a target threshold (default 1%).1112## Critical Rule — Fix Rendering Code, Never Tests1314**DO NOT** modify diff tests, golden test infrastructure, reference images, tolerance thresholds15(to hide regressions), or test comparison logic to make diffs pass. The goal is to improve16rendering accuracy, not to weaken the test harness.1718**DO** fix the root cause in rendering code and logic:1920- Parsers: `src/parsers/`21- Elements: `src/elements/`22- Barcode encoders: `src/barcodes/`23- Renderer / drawing: `src/drawers/`24- Font handling: `src/assets/`2526Threshold updates in `docs/DIFF_THRESHOLDS.md` are only allowed **after** a rendering fix27genuinely lowers the diff percentage — set the new tolerance slightly above the new measured28diff, never raise it to paper over a regression.2930## When to Use3132- A label has a diff percentage higher than desired33- You want to systematically reduce rendering differences across all labels34- You need to extract standalone ZPL snippets to isolate a rendering bug35- After implementing a rendering change, you want to verify and fine-tune it3637## Prerequisites3839Before starting, ensure:401. The diff report is up to date: `cargo test --test e2e_diff_report -- --nocapture`412. All current tests pass: `cargo test`423. You have reference PNG images in `testdata/` for the target label(s)4344## Procedure4546### Phase 1: Scan — Identify High-Diff Labels47481. **Read the diff report** from [testdata/diffs/diff_report.txt](../../testdata/diffs/diff_report.txt)49502. **Read the tolerance thresholds** from [docs/DIFF_THRESHOLDS.md](../../docs/DIFF_THRESHOLDS.md)51523. **Identify target labels** — either a specific label from the user's request, or all labels with diff > 1%:53 - List labels sorted by diff percentage (highest first)54 - For each, note the current diff%, tolerance, and primary diff source55564. **Pick the highest-impact label** to fix first (highest diff% that is potentially fixable — skip MaxiCode/PDF417 unless specifically requested)5758### Phase 2: Analyze — Find Problematic Elements59605. **Read the ZPL source** from `testdata/labels/<name>.zpl` or `testdata/unit/<name>.zpl`61626. **Parse the ZPL** to identify all commands and elements:63 ```64 Use src/skill/snippet_extractor.rs → split_zpl_commands() to list all commands65 Use src/skill/snippet_extractor.rs → group_commands_into_spans() to group into element spans66 ```67687. **Inspect the diff image** at `testdata/diffs/<name>_diff.png` — red pixels show where rendering differs from Labelary69708. **Correlate diff regions with elements** using `src/skill/element_analyzer.rs`:71 - Parse the ZPL to get `LabelInfo`72 - Compute bounding boxes for each element via `compute_element_bbox()`73 - Load the diff image and count red pixels in each bbox via `correlate_diff_regions()`74 - Report which elements contribute most to the total diff75769. **Rank elements by diff contribution** — focus on the top 3-5 contributors77789b. **Classify each element's diff** as ContentDiff, PositionDiff, or Mixed using79 `src/skill/diff_classifier.rs → classify_element_diffs()`:8081 ```82 // Full pipeline with classification:83 Use element_analyzer::analyze_label_with_classification(label, diff_path, zpl, use_labelary)84 ```8586 The classifier renders each element in isolation, optionally fetches the Labelary reference87 for the same snippet, and compares the two renders:8889 | Isolated snippet diff | Full-label bbox diff | Classification |90 |----------------------|----------------------|----------------|91 | Low (< 2%) | High (> 2%) | **PositionDiff** — element placed wrong |92 | High (> 2%) | Any | **ContentDiff** — element renders wrong |93 | High (> 2%) | High (> 2%) | **Mixed** — both |94 | Low (< 2%) | Low (< 2%) | No classification (no meaningful diff) |9596 For **PositionDiff** elements, the classifier also attempts to detect the offset vector (dx, dy):97 - Shadow detection: two clusters of red pixels → direct offset measurement98 - Centroid shift: centroid of diff pixels vs. expected element center99100### Phase 3: Extract — Create Standalone Snippets10110210. **Extract each high-diff element** into a standalone ZPL file:103 ```104 Use src/skill/snippet_extractor.rs → extract_element()105 ```106 This creates `testdata/unit/<label>_<index>.zpl` with:107 - `^XA` / `^XZ` wrapper108 - Global state commands (`^PW`, `^CF`, `^BY`, `^CI`, `^FW`, `^PO`, `^LR`) preserved109 - The specific element's command span (`^FO`/`^FT` through `^FS`)11011111. **Render each snippet** to verify it renders correctly in isolation:112 ```bash113 cargo run -- convert testdata/unit/<label>_<index>.zpl114 ```115 Both unit tests (`testdata/unit/`) and carrier labels (`testdata/labels/`) use the full116 Labelary canvas (`101.625×203.25 mm` at 8 dpmm → `813×1626 px`). Unit golden PNGs are117 Labelary references at `812×1624 px` (Labelary rounds by 1 px due to float precision).11811912. **Get Labelary reference** for each snippet — post the snippet ZPL to:120 ```121 POST http://api.labelary.com/v1/printers/8dpmm/labels/4.005x8.01/0/122 ```123 Save the reference as `testdata/unit/<label>_<index>_ref.png`.124 Note: Labelary renders at 812×1624 for our canvas size (1 px smaller than the local125 renderer's 813×1626 due to floating-point rounding of the inch dimensions). This causes126 a ~0.25% systematic diff; direct pixel comparison is done over the overlapping region.12712813. **Compare snippet renders** — this isolates the rendering difference to a single element129130### Phase 4: Fix — Iterative Rendering Fixes131132For each high-diff element, starting with the highest contributor:13313414. **Identify the element type, diff classification, and fix category:**135136 Use `FixCategory::from_classification(element_type, classification)` to select the fix strategy:137138 | Classification | Fix Strategy | Target Source Files |139 |---------------|-------------|-------------------|140 | **ContentDiff** + Text | FontMetrics | `src/drawers/renderer.rs`, `src/elements/font.rs` |141 | **ContentDiff** + Barcode128 | BarcodeEncoding | `src/barcodes/code128.rs`, `src/drawers/renderer.rs` |142 | **ContentDiff** + BarcodeEan13 | BarcodeEncoding | `src/barcodes/ean13.rs` |143 | **ContentDiff** + Barcode2of5 | BarcodeEncoding | `src/barcodes/interleaved2of5.rs` |144 | **ContentDiff** + Barcode39 | BarcodeEncoding | `src/barcodes/code39.rs` |145 | **ContentDiff** + BarcodePdf417 | BarcodeEncoding | `src/barcodes/pdf417.rs` |146 | **ContentDiff** + BarcodeAztec | BarcodeEncoding | `src/barcodes/aztec.rs` |147 | **ContentDiff** + BarcodeDatamatrix | BarcodeEncoding | `src/barcodes/datamatrix.rs` |148 | **ContentDiff** + BarcodeQr | BarcodeEncoding | `src/barcodes/qrcode.rs` |149 | **ContentDiff** + Maxicode | BarcodeEncoding | `src/barcodes/maxicode.rs` |150 | **ContentDiff** + GraphicBox/Circle/Line/Field | GraphicRendering | `src/drawers/renderer.rs` |151 | **PositionDiff** (any element) | PositionOffset | `src/parsers/zpl_parser.rs`, `src/elements/field_alignment.rs`, `src/elements/drawer_options.rs` |152 | **Mixed** (any element) | PositionOffset first, then ContentDiff fix | Both sets above |153154 For **PositionDiff** elements, use the detected offset vector (dx, dy) from `PositionOffsetInfo`155 to guide the fix — look for coordinate calculation that differs by that many pixels.15615715. **Fetch the official Zebra documentation** for the suspect ZPL command using the `zpl-reference` skill:158 ```159 https://docs.zebra.com/us/en/printers/software/zpl-pg/c-zpl-zpl-commands/r-zpl-<slug>.html160 ```16116216. **Compare spec vs implementation** using the `fix-zpl-render` skill's Phase 3 comparison table16316417. **Apply the fix** to the relevant source file16516618. **Verify the fix:**167 ```bash168 # Quick check — does it compile?169 cargo build170171 # Render the snippet172 cargo run -- convert testdata/unit/<label>_<index>.zpl173174 # Run the specific golden test175 cargo test --test e2e_golden -- <label_name> --nocapture176177 # Check for regressions on ALL labels178 cargo test --test e2e_golden179 ```18018119. **Measure improvement:**182 ```bash183 cargo test --test e2e_diff_report -- --nocapture184 ```185 Compare the new diff% against the previous value.18618720. **If diff improved without regressions:** Keep the change and proceed to the next element.18818921. **If diff regressed on other labels:** Revert the change and try a different approach.19019122. **Repeat** from step 14 for the next highest-contributing element.192193### Phase 5: Finalize — Update Thresholds and Report19419523. **Update diff thresholds** in [docs/DIFF_THRESHOLDS.md](../../docs/DIFF_THRESHOLDS.md):196 - Set new diff% values for improved labels197 - Set new tolerance ceilings slightly above the new diff%19819924. **Regenerate diff images:**200 ```bash201 cargo test --test e2e_diff_report -- --nocapture202 ```20320425. **Run full test suite:**205 ```bash206 cargo test207 ```20820926. **Commit all changes** including:210 - Source code fixes211 - Updated `docs/DIFF_THRESHOLDS.md`212 - Updated diff images in `testdata/diffs/`213 - New snippet files in `testdata/unit/` (if useful for ongoing debugging)214215## Auto-Fix Loop Strategy216217When running in fully automated mode ("fix all labels above 1%"):218219```2201. Generate diff report2212. Sort labels by diff% descending2223. For each label above 1%:223 a. Analyze element contributions224 b. For each element (highest contribution first):225 i. Extract standalone snippet226 ii. Identify fix category227 iii. Fetch Zebra spec for the command228 iv. Compare spec vs implementation229 v. Apply fix230 vi. Build & test231 vii. If regression: revert, try next element232 viii.If improved: keep, re-measure233 c. After all elements tried:234 - If diff ≤ 1%: SUCCESS, move to next label235 - If diff > 1% but improved: PARTIAL, record improvement236 - If no improvement possible: SKIP (e.g., font metrics, library limitations)2374. Generate final report238```239240## Known Limitations241242These labels cannot reach <1% diff due to fundamental limitations:243244| Label | Blocker | Reason |245|-------|---------|--------|246| ups, ups_surepost | MaxiCode encoder | Proprietary encoding, no compliant open-source encoder |247| fedex | PDF417 encoding | rxing encoder uses different compaction modes (valid but visually different) |248| aztec_ec | Aztec encoding | rxing Aztec encoder differs in symbol sizing |249| pnldpd | Multiple (Aztec + GFA + font) | Compound issues from barcode encoding + font metrics |250| Most text-heavy labels | Font metrics | Helvetica Bold vs Zebra proprietary fonts |251252## Utility Code Reference253254The skill uses Rust utilities in `src/skill/`:255256- **`diff_scanner.rs`** — Parse diff report, find high-diff labels, suggest closest label names257- **`element_analyzer.rs`** — Compute element bounding boxes, correlate diff pixels with elements258- **`snippet_extractor.rs`** — Split ZPL commands, group into spans, extract standalone snippets259- **`models.rs`** — Data structures: `DiffReport`, `ElementBBox`, `ZplSnippet`, etc.260- **`error.rs`** — Error types: `ScanError`, `AnalyzeError`, `ExtractError`261262### Phase 6: Commit — Generate Summary26326427. **Generate a commit message** summarizing the changes:265 - List labels improved with before/after diff percentages266 - Summarize which rendering modules were modified267 - Note any threshold updates268 - Format: `fix(render): reduce diff for <labels> — <brief description>`269270 Example:271 ```272 fix(render): reduce diff for amazon, dhl — fix ^FO baseline offset and Code128 quiet zone273274 Labels improved:275 - amazon: 4.2% → 0.8%276 - dhl: 3.1% → 0.6%277278 Changes:279 - src/parsers/zpl_parser.rs: correct ^FO y-offset calculation for rotated fields280 - src/barcodes/code128.rs: remove extra quiet zone module on right side281 - docs/DIFF_THRESHOLDS.md: update tolerances for amazon (1.0%), dhl (0.8%)282 ```283284## References285286- [Fix ZPL Render Skill](../fix-zpl-render/SKILL.md) — Detailed diagnostic procedures287- [ZPL Reference Skill](../zpl-reference/SKILL.md) — Official Zebra documentation lookup288- [Diff Thresholds](../../docs/DIFF_THRESHOLDS.md) — Per-label tolerance configuration289- [Rendering Fixes History](/memories/repo/labelize-rendering-fixes.md) — Previously applied fixes290291---292> Source: [GOODBOY008/labelize](https://github.com/GOODBOY008/labelize) — distributed by [TomeVault](https://tomevault.io).293<!-- tomevault:4.0:skill_md:2026-06-22 -->