Skip to content

Rendering Invariant Tests Design & Coverage

Marc Jones edited this page Apr 29, 2026 · 1 revision

Rendering Invariant Tests - Design & Coverage

Rendering Invariant Tests for SkiaRenderer

Overview

Property-based rendering invariant test harness implemented in Pdfe.Rendering.Tests/RenderingInvariantTests.cs. These tests verify mathematical correctness of the PDF rendering pipeline by asserting invariants that must hold across varied random inputs.

Test Count

32 passing tests covering 10 rendering invariants:

  • 5 determinism tests (different seeds)
  • 3 DPI scaling tests (72↔144, 100↔200, 150↔300)
  • 4 background fill tests (white, red, green, blue)
  • 2 translation invariance tests (horizontal, vertical)
  • 1 identity transform test
  • 4 empty page invariant tests (612x792, 595x842, 100x100, 2400x3000)
  • 1 page-size proportionality test
  • 5 RGB color round-trip tests (red, green, blue, gray, yellow)
  • 4 CMYK color tests
  • 1 stroke width visibility test
  • 1 anti-aliasing existence test
  • 1 scaling transform test

Invariants Verified

1. Determinism (5 tests)

  • Render same PDF twice with same DPI, compare byte-for-byte.
  • Tests: seeds 42, 123, 456, 789, 999
  • Verifies: Random rectangles + text render identically across renders.

2. DPI Scaling (3 tests)

  • Render page at DPI1 and DPI2, verify bitmap dimensions scale correctly.
  • Tests: 72↔144, 100↔200, 150↔300
  • Verifies: (bitmap_at_DPI2.width / bitmap_at_DPI1.width) ≈ (DPI2 / DPI1)

3. Background Fill (4 tests)

  • Empty content stream with different background colors.
  • Tests: white (0xFFFFFFFF), red (0xFFFF0000), green (0xFF00FF00), blue (0xFF0000FF)
  • Verifies: All pixels match the specified background color.

4. Translation Invariance (2 tests)

  • Render rectangle with/without CTM translation.
  • Tests: horizontal (50pt), vertical (75pt)
  • Verifies: Content renders with translation applied (CTM math correctness).

5. Identity Transform (1 test)

  • Compare rendering with explicit identity matrix (1 0 0 1 0 0 cm) vs. no transform.
  • Verifies: Identity transformation is a no-op, produces byte-identical output.

6. Empty Page Invariant (4 tests)

  • Empty pages of different sizes, verify bitmap dimensions match formula.
  • Tests: 612x792 (Letter), 595x842 (A4), 100x100 (small), 2400x3000 (large)
  • Verifies: bitmap_width = ⌊page_width * dpi / 72⌋, bitmap_height = ⌊page_height * dpi / 72⌋

7. Page-Size Proportionality (1 test)

  • Two pages: (612, 792) and (612, 396) — half-height.
  • Verifies: Half-height page renders to half bitmap height.

8. RGB Color Round-Trip (5 tests)

  • Render PDF with RGB colors, sample pixel from rendered region.
  • Tests: red (1,0,0), green (0,1,0), blue (0,0,1), gray (0.5,0.5,0.5), yellow (1,1,0)
  • Verifies: Rendered pixels have dominant color channels matching the fill color.

9. CMYK Color (4 tests)

  • Render PDF with CMYK colors, verify non-background pixels.
  • Tests: red (0,1,1,0), cyan (1,0,0,0), magenta (0,1,0,0), yellow (0,0,1,0)
  • Verifies: CMYK colors render to non-background pixels (color space conversion works).

10. Stroke Width Visibility (1 test)

  • Render same line with 10pt and 1pt strokes, compare pixel counts.
  • Verifies: Thicker stroke produces significantly more pixels (10× scale factor).

11. Anti-Aliasing Existence (1 test)

  • Render text at 100 DPI with AA enabled.
  • Verifies: Scan line through text edge contains ≥3 distinct intermediate gray values.

12. Scaling Transform (1 test)

  • Small rectangle with 1.5× scaling.
  • Verifies: Both unscaled and scaled versions render, scaled has more pixel coverage.

Helper Classes

RandomPdfBuilder

Static class that generates synthetic single-page PDFs with:

  • N random-colored, random-positioned rectangles
  • M random text lines
  • Seeded Random for reproducibility

Used by determinism tests to verify rendering consistency.

BitmapCompare

Static utility class with three methods:

  • AreIdentical(bitmap1, bitmap2): Byte-for-byte equality check.
  • PixelDifferenceCount(bitmap, expectedColor, tolerance): Count non-matching pixels.
  • SampleColor(bitmap, x, y): Sample RGBA at position with bounds checking.

Test Execution

dotnet test Pdfe.Rendering.Tests --filter "FullyQualifiedName~RenderingInvariant"

All 32 tests complete in ~5 seconds.

Why These Invariants Matter

These invariants catch subtle bugs that hand-written tests miss:

  • Determinism: Catches non-deterministic rendering (e.g., uninitialized state, timing-dependent code).
  • DPI Scaling: Catches CTM (Current Transformation Matrix) scaling bugs.
  • Color Round-Trip: Catches color space conversion errors (RGB↔CMYK).
  • Transform Invariants: Catches coordinate system confusion (PDF bottom-left vs. Avalonia top-left).
  • Anti-Aliasing: Catches rendering correctness (edge pixel sharpness).

By testing across random inputs rather than hand-crafted cases, we discover edge cases and regressions before they reach users.

Limitations & Unverified Invariants

Could Not Verify

  1. Inline images (BI/ID/EI) - Parser support incomplete, content rendering may not reflect parsed positions.
  2. Clipping paths with rotation - Rotation operator (/Rotate) not fully supported.
  3. Gradient shading - Shading operators not yet implemented in coverage test suite.
  4. PDF transparency groups - Group state and blending modes require deeper operator support.

Why Not Verified

  • Inline images: BI...ID...EI operators are low-priority for the current redaction-focused feature set.
  • Rotation: Page rotation is tracked but not applied during rendering; requires coordinate flip in canvas setup.
  • Gradients/Shading: Would require significant SkiaSharp shader plumbing.
  • Transparency Groups: Requires tracking GS stack state and group scope; orthogonal to current invariant focus.

These can be added as future test enhancements without modifying production code.

Integration with CI/CD

Tests run in normal xUnit test suite (not separate benchmark project), capturing trends on every CI run. Thresholds are loose enough to avoid flakiness but tight enough to catch 2-3× regressions.

Clone this wiki locally