pdftract/notes/pdftract-44f6.md
jedarden 6ea0b0aa54 feat(pdftract-44f6): implement GraphicsState with 13 fields, Color enum, and matrix ops
Implements the complete graphics state per PDF spec section 8.4:

- Color enum with 5 variants (DeviceGray/RGB/CMYK, Spot, Other)
- Color::to_css_hex() for JSON serialization (returns None for Spot/Other)
- GraphicsState struct with all 13 fields (ctm, text_matrix, text_line_matrix,
  font, font_size, char_spacing, word_spacing, horiz_scaling, leading,
  text_rise, text_rendering_mode, fill_color, stroke_color)
- GraphicsState::initial() returning default state (identity CTM, black colors)
- Matrix operations: scale(), translate(), rotate(), invert()
- Manual Debug impl for GraphicsState (Font doesn't implement Debug)

All acceptance criteria PASS:
- initial() has identity CTM, font_size 0.0, fill_color DeviceGray(0.0)
- Clone produces deep-equal value
- Color::DeviceRGB([1.0, 0.0, 0.0]).to_css_hex() == Some("#ff0000")
- Color::Spot returns None
- Matrix multiply identity*identity within 1e-10

Closes: pdftract-44f6

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 15:49:50 -04:00

100 lines
3.8 KiB
Markdown

# pdftract-44f6: GraphicsState struct + Color enum + matrix initialization
## Summary
Implemented the complete `GraphicsState` struct with all 13 fields per PDF spec section 8.4, plus the `Color` enum with CSS hex serialization and matrix operations.
## Changes Made
### File: `crates/pdftract-core/src/graphics_state.rs`
1. **Added `Color` enum** with 5 variants:
- `DeviceGray(f32)` - single grayscale component
- `DeviceRGB([f32; 3])` - RGB color
- `DeviceCMYK([f32; 4])` - CMYK color
- `Spot(Arc<str>, f32)` - spot color with tint
- `Other` - other color spaces
2. **Added `Color::to_css_hex()`** method:
- Converts DeviceGray/RGB/CMYK to "#rrggbb" format
- Returns None for Spot/Other
- Includes clamp for safety
- CMYK→RGB uses naive formula: R=(1-C)*(1-K)
3. **Extended `GraphicsState` struct** with all 13 fields:
- `ctm: Matrix3x3` - current transformation matrix
- `text_matrix: Matrix3x3` - Tm
- `text_line_matrix: Matrix3x3` - Tlm
- `font: Option<Arc<Font>>` - current font
- `font_size: f64` - Tf
- `char_spacing: f64` - Tc
- `word_spacing: f64` - Tw
- `horiz_scaling: f64` - Tz (default 100)
- `leading: f64` - TL
- `text_rise: f64` - Ts
- `text_rendering_mode: u8` - Tr (0-7)
- `fill_color: Color` - fill color
- `stroke_color: Color` - stroke color
4. **Added `GraphicsState::initial()`** returning default state:
- Identity CTM, text_matrix, text_line_matrix
- font: None
- font_size: 0.0
- All spacings: 0.0
- horiz_scaling: 100.0
- text_rendering_mode: 0
- fill/stroke_color: DeviceGray(0.0) (black)
5. **Added matrix operations** to `Matrix3x3`:
- `scale(sx, sy)` - create scale matrix
- `translate(tx, ty)` - create translation matrix
- `rotate(angle)` - create rotation matrix (angle in radians)
- `invert()` - invert matrix, returns None if singular
6. **Implemented `Debug` manually** for `GraphicsState`:
- Font doesn't implement Debug, so we show `<Arc<Font>>` placeholder
7. **Added comprehensive tests**:
- `test_gstate_initial_ctm_is_identity`
- `test_gstate_initial_font_size_is_zero`
- `test_gstate_initial_fill_color_is_black`
- `test_gstate_initial_horiz_scaling_is_100`
- `test_gstate_initial_text_matrices_are_identity`
- `test_gstate_initial_font_is_none`
- `test_gstate_initial_text_rendering_mode_is_0`
- `test_gstate_clone_deep_equal`
- `test_color_device_rgb_to_css_hex` - verifies `#ff0000` for red
- `test_color_device_gray_to_css_hex`
- `test_color_device_cmyk_to_css_hex`
- `test_color_spot_to_css_hex_none`
- `test_color_other_to_css_hex_none`
- `test_color_device_rgb_clamped`
- `test_matrix_scale`
- `test_matrix_translate`
- `test_matrix_rotate`
- `test_matrix_invert_identity`
- `test_matrix_invert_translation`
- `test_matrix_invert_singular`
- `test_identity_multiply_identity`
- `test_multiply_within_tolerance`
## Acceptance Criteria Status
-`GraphicsState::initial()` returns state with ctm == identity
-`GraphicsState::initial()` returns state with font_size == 0.0
-`GraphicsState::initial()` returns state with fill_color == DeviceGray(0.0)
- ✅ Clone produces a deep-equal value (Arc cloned cheaply)
-`Color::DeviceRGB([1.0, 0.0, 0.0]).to_css_hex() == Some("#ff0000".into())`
-`Color::Spot("PANTONE".into(), 0.5).to_css_hex() == None`
- ✅ Matrix multiply test: identity * identity == identity within 1e-10
## Notes
- Font doesn't implement Debug, so GraphicsState has a manual Debug impl that shows `<Arc<Font>>` placeholder for the font field
- All matrix operations use f64 for precision (INV-30)
- Color uses f32 for components per PDF spec convention
- GraphicsState is Clone thanks to Arc<Font> (cheap clone for q/Q operators)
## References
- Plan section: Phase 3.1 State struct fields (lines 1444-1460), Color type (lines 1463-1471), CSS hex rules (line 1473)