package pdf // The most colorants a colour value can have. /DeviceN is capped at 32 // colorants by the format (ISO 32000-2, 8.6.6.5), which is the widest case. MAX_COLOR_COMPONENTS :: 32 // --------------------------------------------------------------------------- // Stroking // --------------------------------------------------------------------------- Line_Cap :: enum u8 { Butt = 0, Round = 1, Square = 2, } Line_Join :: enum u8 { Miter = 0, Round = 1, Bevel = 2, } // MAX_DASH_ENTRIES bounds a /D array from the file. The format sets no limit; // eight is the most any real design uses and the array is copied by value into // every saved graphics state. MAX_DASH_ENTRIES :: 16 Stroke_State :: struct { line_width: f64, line_cap: Line_Cap, line_join: Line_Join, miter_limit: f64, // Dash pattern in user space, with `dash_count` valid entries. An entry of // zero is legal and means a dot; an all-zero array means "no dashing", // because a pattern that never advances would loop forever. dash: [MAX_DASH_ENTRIES]f64, dash_count: int, dash_phase: f64, } STROKE_STATE_DEFAULT :: Stroke_State{ line_width = 1, line_cap = .Butt, line_join = .Miter, miter_limit = 10, } // stroke_has_dash reports whether the pattern actually breaks the line. An // array of all zeros, or one whose entries sum to zero, must be treated as // solid or the dasher never advances (ISO 32000-2, 8.4.3.6). stroke_has_dash :: proc(s: ^Stroke_State) -> bool { if s == nil || s.dash_count <= 0 { return false } total := 0.0 for i in 0 ..< min(s.dash_count, MAX_DASH_ENTRIES) { if s.dash[i] < 0 { return false } total += s.dash[i] } return total > 0 } // --------------------------------------------------------------------------- // Transparency // --------------------------------------------------------------------------- // Blend_Mode is the set from ISO 32000-2, 11.3.5. `Compatible` is a legacy // spelling of Normal and behaves identically. Blend_Mode :: enum u8 { Normal = 0, Multiply, Screen, Overlay, Darken, Lighten, Color_Dodge, Color_Burn, Hard_Light, Soft_Light, Difference, Exclusion, // The four non-separable modes operate on all three RGB components at once. Hue, Saturation, Color, Luminosity, } // blend_mode_from_name maps a /BM name. An unrecognised name is Normal, which // the format requires (11.3.5.2) rather than treating as an error. blend_mode_from_name :: proc(name: string) -> Blend_Mode { switch name { case "Normal", "Compatible": return .Normal case "Multiply": return .Multiply case "Screen": return .Screen case "Overlay": return .Overlay case "Darken": return .Darken case "Lighten": return .Lighten case "ColorDodge": return .Color_Dodge case "ColorBurn": return .Color_Burn case "HardLight": return .Hard_Light case "SoftLight": return .Soft_Light case "Difference": return .Difference case "Exclusion": return .Exclusion case "Hue": return .Hue case "Saturation": return .Saturation case "Color": return .Color case "Luminosity": return .Luminosity } return .Normal } blend_mode_is_separable :: proc(m: Blend_Mode) -> bool { return m < .Hue } // --------------------------------------------------------------------------- // Colour // --------------------------------------------------------------------------- // Color is a colour value in some colour space: `n` components, each in the // range that space defines (which is not always 0..1 -- Lab and Indexed both // use other ranges). Color :: struct { n: int, v: [MAX_COLOR_COMPONENTS]f64, } color_make :: proc(values: ..f64) -> (c: Color) { c.n = min(len(values), MAX_COLOR_COMPONENTS) for i in 0 ..< c.n { c.v[i] = values[i] } return } // Paint_Source is one half of the graphics state's colour: what a fill (or a // stroke) paints with. A pattern takes precedence over the colour components, // which for an uncoloured tiling pattern still carry the underlying colour. Paint_Source :: struct { cs: ^Colorspace, color: Color, pattern: ^Pattern, } // Paint is the fully-resolved instruction handed to a Device for one drawing // operation: everything it needs and nothing it has to look up. Paint :: struct { cs: ^Colorspace, color: Color, // The colour converted to RGB once, so a device that only wants RGB need // not carry a colour-management path. rgb: [3]f64, pattern: ^Pattern, alpha: f64, blend: Blend_Mode, soft_mask: ^Soft_Mask, // Set when the operation paints a shape rather than a colour: image masks // and text clipping. Devices that ignore colour can skip the conversion. is_mask: bool, } // --------------------------------------------------------------------------- // Text state (ISO 32000-2, 9.3) // --------------------------------------------------------------------------- Text_Render_Mode :: enum u8 { Fill = 0, Stroke = 1, Fill_Stroke = 2, Invisible = 3, Fill_Clip = 4, Stroke_Clip = 5, Fill_Stroke_Clip = 6, Clip = 7, } text_render_mode_fills :: proc(m: Text_Render_Mode) -> bool { return m == .Fill || m == .Fill_Stroke || m == .Fill_Clip || m == .Fill_Stroke_Clip } text_render_mode_strokes :: proc(m: Text_Render_Mode) -> bool { return m == .Stroke || m == .Fill_Stroke || m == .Stroke_Clip || m == .Fill_Stroke_Clip } text_render_mode_clips :: proc(m: Text_Render_Mode) -> bool { return m >= .Fill_Clip } Text_State :: struct { font: ^Font, size: f64, char_spacing: f64, // Tc, unscaled text-space units word_spacing: f64, // Tw, applied only to single-byte code 32 // Th: the horizontal scaling operator gives a percentage; this holds the // fraction, so the default is 1, not 100. horiz_scale: f64, leading: f64, // TL rise: f64, // Ts render_mode: Text_Render_Mode, knockout: bool, } TEXT_STATE_DEFAULT :: Text_State{ size = 0, horiz_scale = 1, knockout = true, } // --------------------------------------------------------------------------- // Graphics state (ISO 32000-2, 8.4) // --------------------------------------------------------------------------- Graphics_State :: struct { ctm: Matrix, fill: Paint_Source, stroke: Paint_Source, fill_alpha: f64, stroke_alpha: f64, stroke_state: Stroke_State, blend: Blend_Mode, soft_mask: ^Soft_Mask, text: Text_State, // Depth of the device clip stack at the time this state was saved, so `Q` // can pop exactly the clips that `q` was holding. clip_depth: int, // Parsed but not honoured by the software renderer; kept so a display-list // consumer and any future colour-managed path can see them. render_intent: string, flatness: f64, smoothness: f64, stroke_adjust: bool, alpha_is_shape: bool, black_point_compensation: bool, overprint_fill: bool, overprint_stroke: bool, overprint_mode: int, } // gstate_default is the initial state at the start of a page or form, before // any operator runs (8.4.1). Colour spaces are filled in by the interpreter, // which needs the Document to resolve DeviceGray. gstate_default :: proc(ctm: Matrix) -> Graphics_State { return Graphics_State{ ctm = ctm, fill_alpha = 1, stroke_alpha = 1, stroke_state = STROKE_STATE_DEFAULT, blend = .Normal, text = TEXT_STATE_DEFAULT, render_intent = "RelativeColorimetric", flatness = 1, smoothness = 0, } } // Gstate_Stack is the `q`/`Q` stack. It is depth-capped because a content // stream can push without ever popping, and each entry is a fat value. MAX_GSTATE_DEPTH :: 64 Gstate_Stack :: struct { items: [dynamic]Graphics_State, } gstate_stack_make :: proc(allocator := context.allocator) -> Gstate_Stack { return Gstate_Stack{items = make([dynamic]Graphics_State, 0, 16, allocator)} } gstate_stack_destroy :: proc(s: ^Gstate_Stack) { if s == nil { return } delete(s.items) s^ = {} } // gstate_push saves a copy of `gs`. Returns false when the stack is full, in // which case the caller must not pop for this `q` -- otherwise the stack // underflows on the matching `Q` and the rest of the page draws in the wrong // state. gstate_push :: proc(s: ^Gstate_Stack, gs: Graphics_State) -> bool { if len(s.items) >= MAX_GSTATE_DEPTH { return false } append(&s.items, gs) return true } gstate_pop :: proc(s: ^Gstate_Stack) -> (gs: Graphics_State, ok: bool) { if len(s.items) == 0 { return {}, false } gs = s.items[len(s.items) - 1] pop(&s.items) return gs, true } gstate_depth :: proc(s: ^Gstate_Stack) -> int { return len(s.items) }