initial commit

This commit is contained in:
2026-09-25 23:32:29 +02:00
commit fff8b677f8
114 changed files with 84233 additions and 0 deletions
+296
View File
@@ -0,0 +1,296 @@
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)
}