package pdf import "core:math" // Colour spaces (ISO 32000-2, 8.6). // // Every space is reduced to RGB for output. That is not colour management and // does not pretend to be: an ICCBased space renders through its /Alternate (or // through the device space its /N implies), which is what every // non-colour-managed viewer does and what the format explicitly permits. Colorspace_Kind :: enum u8 { Device_Gray, Device_RGB, Device_CMYK, Cal_Gray, Cal_RGB, Lab, ICC_Based, Indexed, Separation, Device_N, Pattern, } Colorspace :: struct { kind: Colorspace_Kind, // Components a colour value in this space carries. 1 for Indexed (the // index itself), not the base space's count. n: int, name: string, // Indexed: `base` is the space the lookup table's entries live in, and // `lookup` holds (hival+1) * base.n bytes. base: ^Colorspace, lookup: []byte, hival: int, // Separation, DeviceN and ICCBased all render through `alt`. For the first // two, `tint` maps the colorant values into it. alt: ^Colorspace, tint: ^Function, colorant_names: []string, // The /None colorant marks nothing at all on the page (8.6.6.4). is_none: bool, // The /All colorant marks every separation. is_all: bool, // CalGray, CalRGB and Lab. white_point: [3]f64, black_point: [3]f64, gamma: [3]f64, // CalRGB's /Matrix. For a Lab space this holds something else the caller // never sees: the 3x3 that takes XYZ under this space's own white point to // linear sRGB, chromatic adaptation folded in, computed once at load time // so that converting a colour is nine multiplies and no matrix building. mtx: [9]f64, // Lab's a and b component ranges, defaulting to [-100 100 -100 100]. range: [4]f64, // Pattern's underlying space, present only for an uncoloured tiling // pattern, whose colour is supplied by the content that paints it. under: ^Colorspace, } // Nesting of colour spaces: an Indexed whose base is a Separation whose // alternate is an ICCBased, and so on. Four is the deepest anything // meaningful goes; the limit exists because /Resources can be made to name a // space that names itself. @(private) MAX_COLORSPACE_DEPTH :: 8 // The most entries an Indexed lookup table can address. /HiVal is capped at // 255 by the format (8.6.6.3), which is what makes an index a single byte. @(private) MAX_INDEXED_HIVAL :: 255 // --------------------------------------------------------------------------- // The device spaces // // These are shared, immutable and outlive every Document: they hold no // document memory and nothing ever writes to them, so handing the same pointer // to two Documents (or to two threads rendering different files) is safe. It // also means the overwhelmingly common `/DeviceRGB cs` costs nothing at all. // --------------------------------------------------------------------------- @(private) cs_gray := Colorspace{kind = .Device_Gray, n = 1, name = "DeviceGray"} @(private) cs_rgb := Colorspace{kind = .Device_RGB, n = 3, name = "DeviceRGB"} @(private) cs_cmyk := Colorspace{kind = .Device_CMYK, n = 4, name = "DeviceCMYK"} // A /Pattern space with no underlying space: the pattern carries its own // colour, so a colour value in it has no components at all. `n` is therefore // 0, not 1 -- `n` is what tells the content interpreter how many numeric // operands `scn` takes, and a coloured pattern is set with `/P1 scn`, a name // and nothing else. An uncoloured pattern is the case with components, and it // gets them from its underlying space in cs_load_pattern. @(private) cs_pattern := Colorspace{kind = .Pattern, n = 0, name = "Pattern"} colorspace_device_gray :: proc(doc: ^Document) -> ^Colorspace { return &cs_gray } colorspace_device_rgb :: proc(doc: ^Document) -> ^Colorspace { return &cs_rgb } colorspace_device_cmyk :: proc(doc: ^Document) -> ^Colorspace { return &cs_cmyk } // colorspace_device_n returns the device space with `n` components, which is // how an ICCBased space with no /Alternate and an inline image with no space // at all are resolved (8.6.5.5). @(private) colorspace_device_n :: proc(n: int) -> ^Colorspace { switch n { case 1: return &cs_gray case 4: return &cs_cmyk } return &cs_rgb } // --------------------------------------------------------------------------- // Loading // --------------------------------------------------------------------------- // colorspace_load builds the colour space `obj` names or describes. // // `res` is the /Resources dictionary of the page or form whose content stream // named it, and is where a bare name that is not a device space is looked up // (through /Resources /ColorSpace); it may be nil, and a caller that has only // the /ColorSpace sub-dictionary may pass that instead. // // The result is owned by the Document and must not be freed. Spaces are cached // by the object they were built from, because a page that names the same space // in four hundred XObjects must build it once. colorspace_load :: proc(doc: ^Document, obj: Object, res: ^Dict) -> (^Colorspace, Error) { return cs_load(doc, obj, res, 0) } @(private) cs_load :: proc(doc: ^Document, obj: Object, res: ^Dict, depth: int) -> (^Colorspace, Error) { if doc == nil { return nil, .Invalid_Argument } if depth > MAX_COLORSPACE_DEPTH { add_repair(doc, .Recursion_Limit_Hit, 0) return nil, .Recursion_Limit } o := resolve(doc, obj) key := object_identity(o) if key != 0 { if cached, ok := doc.cs_cache[key]; ok { return cached, .None } } cs: ^Colorspace err := Error.Bad_Colorspace #partial switch v in o { case Name: cs, err = cs_load_name(doc, string(v), res, depth) case ^Array: cs, err = cs_load_array(doc, v, res, depth) } if err != .None || cs == nil { return nil, err == .None ? .Bad_Colorspace : err } if key != 0 { if doc.cs_cache == nil { doc.cs_cache = make(map[uintptr]^Colorspace, 16, doc.allocator) } doc.cs_cache[key] = cs } return cs, .None } @(private) cs_load_name :: proc(doc: ^Document, name: string, res: ^Dict, depth: int) -> (^Colorspace, Error) { // The device spaces cannot be redefined by a resource dictionary // (8.6.3), so they are answered before it is consulted. switch name { case "DeviceGray": return &cs_gray, .None case "DeviceRGB": return &cs_rgb, .None case "DeviceCMYK": return &cs_cmyk, .None case "Pattern": return &cs_pattern, .None } if res != nil { // `res` is the /Resources dictionary, so a name is looked up in its // /ColorSpace sub-dictionary (8.6.3). A caller holding only that // sub-dictionary may pass it instead: it is tried second, so it cannot // shadow a genuine /ColorSpace entry. if csdict := dict_dict(doc, res, "ColorSpace"); csdict != nil { if entry := dict_get_raw(csdict, name); entry != nil { return cs_load(doc, entry, res, depth + 1) } } if entry := dict_get_raw(res, name); entry != nil { return cs_load(doc, entry, res, depth + 1) } } // The inline-image abbreviations (Table 93). They are only legal inside an // inline image dictionary, so they are tried after the resource lookup: a // resource genuinely called /G has to win. switch name { case "G": return &cs_gray, .None case "RGB": return &cs_rgb, .None case "CMYK": return &cs_cmyk, .None } return nil, .Bad_Colorspace } @(private) cs_load_array :: proc(doc: ^Document, a: ^Array, res: ^Dict, depth: int) -> (^Colorspace, Error) { if array_len(a) == 0 { return nil, .Bad_Colorspace } family := array_name(doc, a, 0) // `[/DeviceRGB]` and `[/Pattern]` are both legal spellings. if array_len(a) == 1 { return cs_load_name(doc, family, res, depth) } switch family { case "DeviceGray", "G": return &cs_gray, .None case "DeviceRGB", "RGB": return &cs_rgb, .None case "DeviceCMYK", "CMYK": return &cs_cmyk, .None case "CalGray": return cs_load_calgray(doc, a) case "CalRGB": return cs_load_calrgb(doc, a) case "Lab": return cs_load_lab(doc, a) case "ICCBased": return cs_load_icc(doc, a, res, depth) case "Indexed", "I": return cs_load_indexed(doc, a, res, depth) case "Separation": return cs_load_separation(doc, a, res, depth) case "DeviceN": return cs_load_devicen(doc, a, res, depth) case "Pattern": return cs_load_pattern(doc, a, res, depth) } return nil, .Bad_Colorspace } @(private) cs_new :: proc(doc: ^Document, kind: Colorspace_Kind, n: int, name: string) -> ^Colorspace { cs := new(Colorspace, doc.allocator) cs.kind = kind cs.n = n cs.name = name cs.white_point = {0.9642, 1.0, 0.8249} // D50, the format's own default cs.gamma = {1, 1, 1} cs.range = {-100, 100, -100, 100} return cs } // cs_read_white_point reads /WhitePoint, which must be a positive XYZ triple // with Y = 1. A file that gives something else gets D50, because a zero or // negative component divides by zero further down. @(private) cs_read_white_point :: proc(doc: ^Document, d: ^Dict, cs: ^Colorspace) { wp := dict_array(doc, d, "WhitePoint") if array_len(wp) < 3 { return } x := clamp_finite(array_num(doc, wp, 0, 0), 0) y := clamp_finite(array_num(doc, wp, 1, 0), 0) z := clamp_finite(array_num(doc, wp, 2, 0), 0) if x <= 0 || y <= 0 || z <= 0 { add_repair(doc, .Bad_Resource, 0) return } cs.white_point = {x, y, z} bp := dict_array(doc, d, "BlackPoint") if array_len(bp) >= 3 { cs.black_point = { clamp_finite(array_num(doc, bp, 0, 0), 0), clamp_finite(array_num(doc, bp, 1, 0), 0), clamp_finite(array_num(doc, bp, 2, 0), 0), } } } @(private) cs_load_calgray :: proc(doc: ^Document, a: ^Array) -> (^Colorspace, Error) { d := array_dict(doc, a, 1) cs := cs_new(doc, .Cal_Gray, 1, "CalGray") cs_read_white_point(doc, d, cs) g := clamp_finite(dict_num(doc, d, "Gamma", 1), 1) if g <= 0 { g = 1 } cs.gamma = {g, g, g} return cs, .None } @(private) cs_load_calrgb :: proc(doc: ^Document, a: ^Array) -> (^Colorspace, Error) { d := array_dict(doc, a, 1) cs := cs_new(doc, .Cal_RGB, 3, "CalRGB") cs_read_white_point(doc, d, cs) if ga := dict_array(doc, d, "Gamma"); array_len(ga) >= 3 { for i in 0 ..< 3 { g := clamp_finite(array_num(doc, ga, i, 1), 1) cs.gamma[i] = g > 0 ? g : 1 } } if ma := dict_array(doc, d, "Matrix"); array_len(ma) >= 9 { for i in 0 ..< 9 { cs.mtx[i] = clamp_finite(array_num(doc, ma, i, 0), 0) } } return cs, .None } @(private) cs_load_lab :: proc(doc: ^Document, a: ^Array) -> (^Colorspace, Error) { d := array_dict(doc, a, 1) cs := cs_new(doc, .Lab, 3, "Lab") cs_read_white_point(doc, d, cs) if r := dict_array(doc, d, "Range"); array_len(r) >= 4 { for i in 0 ..< 4 { cs.range[i] = clamp_finite(array_num(doc, r, i, 0), 0) } // An inverted range would clamp every value to nothing. if cs.range[0] > cs.range[1] { cs.range[0], cs.range[1] = cs.range[1], cs.range[0] } if cs.range[2] > cs.range[3] { cs.range[2], cs.range[3] = cs.range[3], cs.range[2] } } cs.mtx = lab_to_srgb_matrix(cs.white_point) return cs, .None } @(private) cs_load_icc :: proc(doc: ^Document, a: ^Array, res: ^Dict, depth: int) -> (^Colorspace, Error) { stm := to_stream(array_get(doc, a, 1)) or_else nil d, _ := to_dict(array_get(doc, a, 1)) n := int(dict_int(doc, d, "N", 0)) if n != 1 && n != 3 && n != 4 { n = 0 } alt: ^Colorspace if entry := dict_get_raw(d, "Alternate"); entry != nil { loaded, err := cs_load(doc, entry, res, depth + 1) if err == .None && loaded != nil && loaded.kind != .Pattern { // An /Alternate that disagrees with /N is the file contradicting // itself; /N is what the sample data is laid out by. if n == 0 || loaded.n == n { alt = loaded } } } if alt == nil { if n == 0 { // Neither /N nor a usable /Alternate. The profile itself would say, // but reading ICC profiles is exactly what this library does not // do, so the commonest case is assumed. add_repair(doc, .Bad_Resource, 0, stm != nil ? stm.ref : Ref{}) n = 3 } alt = colorspace_device_n(n) } // Rendering happens entirely through the alternate: no ICC profile is ever // parsed and no colour management is performed. Every non-colour-managed // viewer does the same, and 8.6.5.5 allows it. cs := cs_new(doc, .ICC_Based, alt.n, "ICCBased") cs.alt = alt return cs, .None } @(private) cs_load_indexed :: proc(doc: ^Document, a: ^Array, res: ^Dict, depth: int) -> (^Colorspace, Error) { base, berr := cs_load(doc, array_get_raw(a, 1), res, depth + 1) if berr != .None || base == nil { return nil, .Bad_Colorspace } // The base of an Indexed space may not itself be Indexed or Pattern // (8.6.6.3); either would make an index mean nothing. if base.kind == .Indexed || base.kind == .Pattern { return nil, .Bad_Colorspace } cs := cs_new(doc, .Indexed, 1, "Indexed") cs.base = base cs.hival = int(clamp(array_int(doc, a, 2, 0), 0, MAX_INDEXED_HIVAL)) lookup := array_get(doc, a, 3) #partial switch v in lookup { case String: cs.lookup = transmute([]byte)v case ^Stream: data, derr := stream_data(doc, v) if derr == .None { cs.lookup = data } } if len(cs.lookup) < (cs.hival + 1) * base.n { // A short table is common in files that were subset or edited. The // entries past its end read as zero rather than past the buffer. add_repair(doc, .Bad_Resource, 0) } return cs, .None } @(private) cs_load_separation :: proc(doc: ^Document, a: ^Array, res: ^Dict, depth: int) -> (^Colorspace, Error) { cs := cs_new(doc, .Separation, 1, "Separation") colorant := array_name(doc, a, 1) names := make([]string, 1, doc.allocator) names[0] = colorant cs.colorant_names = names // /None marks nothing on the page at all; /All marks every separation // (8.6.6.4). cs.is_none = colorant == "None" cs.is_all = colorant == "All" cs_load_tint(doc, cs, a, res, depth) return cs, .None } @(private) cs_load_devicen :: proc(doc: ^Document, a: ^Array, res: ^Dict, depth: int) -> (^Colorspace, Error) { list := to_array(array_get(doc, a, 1)) or_else nil n := array_len(list) if n < 1 { return nil, .Bad_Colorspace } // 8.6.6.5 caps a DeviceN space at 32 colorants, which is also the width of // a Color. n = min(n, MAX_COLOR_COMPONENTS) cs := cs_new(doc, .Device_N, n, "DeviceN") names := make([]string, n, doc.allocator) none_count := 0 for i in 0 ..< n { names[i] = array_name(doc, list, i) if names[i] == "None" { none_count += 1 } } cs.colorant_names = names // A DeviceN marks nothing only when every one of its colorants is /None. cs.is_none = none_count == n cs_load_tint(doc, cs, a, res, depth) return cs, .None } // cs_load_tint fills in the alternate space and tint transform shared by // Separation and DeviceN. Both may be missing or broken, and neither failure // is a reason to reject the space: `colorspace_to_rgb` falls back to treating // the colorant as ink coverage, which keeps a spot colour dark instead of // dropping the artwork that uses it. @(private) cs_load_tint :: proc(doc: ^Document, cs: ^Colorspace, a: ^Array, res: ^Dict, depth: int) { // 8.6.6.4 and 8.6.6.5: the alternate space "shall be any device or // CIE-based colour space but shall not be another special colour space // (Pattern, Indexed, Separation, or DeviceN)". All four are refused, and // for the same reason -- none of them can be handed the output of a tint // transform and give a colour back. The components of a tint transform's // output run 0..1; an Indexed component is a palette index, so following // one collapses the whole ramp onto two entries, and a Pattern has no // colour value at all. pdfium refuses the whole space at this point; // dropping to the ink-coverage fallback below is gentler and keeps the // artwork on the page. refused := false if alt, err := cs_load(doc, array_get_raw(a, 2), res, depth + 1); err == .None && alt != nil { #partial switch alt.kind { case .Pattern, .Indexed, .Separation, .Device_N: refused = true case: cs.alt = alt } } if tint, err := function_load(doc, array_get_raw(a, 3)); err == .None && tint != nil && tint.n_out >= 1 { if cs.alt == nil && !refused { // A tint transform with no alternate space still says how many // components it produces, and a device space with that many is a // far better guess than giving up on the colour entirely. That // does not extend to an alternate that was named and refused: its // outputs mean whatever that space's components mean, which is // exactly what cannot be read as a device colour. cs.alt = colorspace_device_n(tint.n_out) } if cs.alt != nil && tint.n_out == cs.alt.n { cs.tint = tint } } if cs.tint == nil || cs.alt == nil { add_repair(doc, .Bad_Resource, 0) } } @(private) cs_load_pattern :: proc(doc: ^Document, a: ^Array, res: ^Dict, depth: int) -> (^Colorspace, Error) { under, err := cs_load(doc, array_get_raw(a, 1), res, depth + 1) if err != .None || under == nil || under.kind == .Pattern { return &cs_pattern, .None } cs := cs_new(doc, .Pattern, under.n, "Pattern") cs.under = under return cs, .None } // --------------------------------------------------------------------------- // Conversion to RGB // --------------------------------------------------------------------------- // colorspace_to_rgb converts one colour value to RGB in 0..1. It allocates // nothing and never fails: an unconvertible colour comes back as black, since // a page that draws the wrong colour is still a page and a page that stops // drawing is not. colorspace_to_rgb :: proc(cs: ^Colorspace, c: Color) -> [3]f64 { return cs_to_rgb(cs, c, 0) } @(private) cs_to_rgb :: proc(cs: ^Colorspace, c: Color, depth: int) -> [3]f64 { if cs == nil || depth > MAX_COLORSPACE_DEPTH { return {0, 0, 0} } switch cs.kind { case .Device_Gray: g := cs_comp(c, 0) return {g, g, g} case .Device_RGB: return {cs_comp(c, 0), cs_comp(c, 1), cs_comp(c, 2)} case .Device_CMYK: // The naive conversion, subtracting each ink and the black from full // intensity. It is not colorimetric and no non-colour-managed viewer // does anything else; a real CMYK profile is the only way to do // better and this library reads none. cy, m, y, k := cs_comp(c, 0), cs_comp(c, 1), cs_comp(c, 2), cs_comp(c, 3) return { 1 - min(1, cy + k), 1 - min(1, m + k), 1 - min(1, y + k), } case .Cal_Gray: g := cs_cal_component(cs_comp(c, 0), cs.gamma[0]) return {g, g, g} case .Cal_RGB: return { cs_cal_component(cs_comp(c, 0), cs.gamma[0]), cs_cal_component(cs_comp(c, 1), cs.gamma[1]), cs_cal_component(cs_comp(c, 2), cs.gamma[2]), } case .Lab: return cs_lab_to_rgb(cs, c) case .ICC_Based: return cs_to_rgb(cs.alt, c, depth + 1) case .Indexed: return cs_indexed_to_rgb(cs, c, depth) case .Separation, .Device_N: return cs_tint_to_rgb(cs, c, depth) case .Pattern: // An uncoloured pattern carries the colour it is painted in, in the // underlying space. A coloured one supplies its own, and this value is // never asked for. if cs.under != nil { return cs_to_rgb(cs.under, c, depth + 1) } return {0, 0, 0} } return {0, 0, 0} } // cs_comp reads component i, clamped to 0..1. Used for the spaces whose // components are all 0..1, which is every one of them but Lab and Indexed. @(private) cs_comp :: proc(c: Color, i: int) -> f64 { if i < 0 || i >= c.n || i >= MAX_COLOR_COMPONENTS { return 0 } return clamp(clamp_finite(c.v[i], 0), 0, 1) } // cs_cal_component decodes one CalGray or CalRGB component. // // The full colorimetry -- white point, /Matrix, adaptation -- is deliberately // not implemented: the format allows a reader to treat these as DeviceGray and // DeviceRGB and viewers do. /Gamma is honoured, because it is one pow: the // component is decoded to linear light and re-encoded for an sRGB output // device. At the usual /Gamma 2.2 that round trip is within half a level of // leaving the value alone, so the common case still matches every other // viewer; a gamma of exactly 1 is the default and is skipped entirely. @(private) cs_cal_component :: proc(v: f64, gamma: f64) -> f64 { x := clamp(clamp_finite(v, 0), 0, 1) if gamma == 1 { return x } return cs_srgb_encode(math.pow(x, gamma)) } // cs_srgb_encode is the sRGB transfer function (IEC 61966-2-1), taking linear // light to a display value. @(private) cs_srgb_encode :: proc(v: f64) -> f64 { x := clamp(clamp_finite(v, 0), 0, 1) if x <= 0.0031308 { return x * 12.92 } return 1.055 * math.pow(x, 1.0 / 2.4) - 0.055 } // --------------------------------------------------------------------------- // Lab // --------------------------------------------------------------------------- // lab_to_srgb_matrix builds XYZ-under-`wp` to linear sRGB. // // sRGB is defined against D65 and PDF's Lab spaces are almost always D50, so // the two are joined by a Bradford chromatic adaptation. Without it a D50 // white -- L* 100, the commonest colour in any Lab image -- renders as a // yellow cast rather than as white. @(private) lab_to_srgb_matrix :: proc(wp: [3]f64) -> [9]f64 { // Bradford cone response and its inverse. ma := [9]f64{ 0.8951, 0.2664, -0.1614, -0.7502, 1.7135, 0.0367, 0.0389, -0.0685, 1.0296, } ma_inv := [9]f64{ 0.9869929, -0.1470543, 0.1599627, 0.4323053, 0.5183603, 0.0492912, -0.0085287, 0.0400428, 0.9684867, } // XYZ (D65) to linear sRGB. to_rgb := [9]f64{ 3.2404542, -1.5371385, -0.4985314, -0.9692660, 1.8760108, 0.0415560, 0.0556434, -0.2040259, 1.0572252, } d65 := [3]f64{0.95047, 1.0, 1.08883} src := mat3_mul_vec(ma, wp) dst := mat3_mul_vec(ma, d65) scale: [9]f64 for i in 0 ..< 3 { // A white point component of zero was rejected at load time; this is // belt and braces, since a division by zero here would poison every // colour the space produces. scale[i * 3 + i] = src[i] != 0 ? dst[i] / src[i] : 1 } adapt := mat3_mul(ma_inv, mat3_mul(scale, ma)) return mat3_mul(to_rgb, adapt) } @(private) mat3_mul :: proc(a, b: [9]f64) -> (m: [9]f64) { for r in 0 ..< 3 { for c in 0 ..< 3 { sum := 0.0 for k in 0 ..< 3 { sum += a[r * 3 + k] * b[k * 3 + c] } m[r * 3 + c] = sum } } return } @(private) mat3_mul_vec :: proc(a: [9]f64, v: [3]f64) -> (o: [3]f64) { for r in 0 ..< 3 { o[r] = a[r * 3] * v[0] + a[r * 3 + 1] * v[1] + a[r * 3 + 2] * v[2] } return } // cs_lab_g inverts the CIE L* nonlinearity. @(private) cs_lab_g :: proc(x: f64) -> f64 { // 6/29, below which the curve is linear so that its slope stays finite. if x >= 6.0 / 29.0 { return x * x * x } return (108.0 / 841.0) * (x - 4.0 / 29.0) } @(private) cs_lab_to_rgb :: proc(cs: ^Colorspace, c: Color) -> [3]f64 { l := clamp(clamp_finite(c.n > 0 ? c.v[0] : 0, 0), 0, 100) a := clamp(clamp_finite(c.n > 1 ? c.v[1] : 0, 0), cs.range[0], cs.range[1]) b := clamp(clamp_finite(c.n > 2 ? c.v[2] : 0, 0), cs.range[2], cs.range[3]) m := (l + 16) / 116 xw := cs.white_point[0] * cs_lab_g(m + a / 500) yw := cs.white_point[1] * cs_lab_g(m) zw := cs.white_point[2] * cs_lab_g(m - b / 200) out: [3]f64 for i in 0 ..< 3 { lin := cs.mtx[i * 3] * xw + cs.mtx[i * 3 + 1] * yw + cs.mtx[i * 3 + 2] * zw // Colours outside the sRGB gamut -- most of the Lab space is -- are // clipped per channel, which is what every viewer without a gamut // mapping engine does. out[i] = cs_srgb_encode(lin) } return out } // --------------------------------------------------------------------------- // Indexed // --------------------------------------------------------------------------- @(private) cs_indexed_to_rgb :: proc(cs: ^Colorspace, c: Color, depth: int) -> [3]f64 { base := cs.base if base == nil { return {0, 0, 0} } // An index out of range is clamped, never followed: the table is exactly // (hival+1) entries and a file is free to name entry 300 of a 4-entry one. idx := int(clamp(clamp_finite(math.round(c.n > 0 ? c.v[0] : 0), 0), 0, f64(cs.hival))) comp: Color comp.n = min(base.n, MAX_COLOR_COMPONENTS) for j in 0 ..< comp.n { off := idx * base.n + j raw := off >= 0 && off < len(cs.lookup) ? f64(cs.lookup[off]) : 0 lo, hi := cs_component_range(base, j) comp.v[j] = lo + raw * (hi - lo) / 255 } return cs_to_rgb(base, comp, depth + 1) } // cs_component_range is the interval component j of a colour in this space // runs over. Everything is 0..1 except Lab, whose L is 0..100 and whose a and // b carry their own /Range. @(private) cs_component_range :: proc(cs: ^Colorspace, j: int) -> (lo, hi: f64) { if cs != nil && cs.kind == .Lab { switch j { case 0: return 0, 100 case 1: return cs.range[0], cs.range[1] case 2: return cs.range[2], cs.range[3] } } return 0, 1 } // --------------------------------------------------------------------------- // Separation and DeviceN // --------------------------------------------------------------------------- @(private) cs_tint_to_rgb :: proc(cs: ^Colorspace, c: Color, depth: int) -> [3]f64 { if cs.is_none { // Nothing is painted in a /None space at all. The interpreter checks // `is_none` before drawing; this value only decides what a caller that // does not sees, and white is the least destructive answer. return {1, 1, 1} } if cs.tint != nil && cs.alt != nil { in_: [MAX_COLOR_COMPONENTS]f64 out_: [MAX_COLOR_COMPONENTS]f64 n := min(cs.n, MAX_COLOR_COMPONENTS) for i in 0 ..< n { in_[i] = clamp_finite(i < c.n ? c.v[i] : 0, 0) } alt_n := min(cs.alt.n, MAX_COLOR_COMPONENTS) function_eval(cs.tint, in_[:n], out_[:alt_n]) alt_color: Color alt_color.n = alt_n for i in 0 ..< alt_n { alt_color.v[i] = out_[i] } return cs_to_rgb(cs.alt, alt_color, depth + 1) } // No usable tint transform. Treating the darkest colorant as ink coverage // keeps a spot colour dark and a light tint light, which is much closer // than either black or white and never loses the artwork. ink := 0.0 for i in 0 ..< min(cs.n, c.n) { ink = max(ink, clamp(clamp_finite(c.v[i], 0), 0, 1)) } g := 1 - ink return {g, g, g} } // --------------------------------------------------------------------------- // Initial values and image decode arrays // --------------------------------------------------------------------------- // colorspace_default_color is the colour a space starts at when it is selected // with `cs` or `CS` (ISO 32000-2, 8.6.8): black for the device and CIE spaces, // the first entry for Indexed, full tint for Separation and DeviceN. colorspace_default_color :: proc(cs: ^Colorspace) -> (c: Color) { if cs == nil { return color_make(0) } n := min(cs.n, MAX_COLOR_COMPONENTS) switch cs.kind { case .Device_CMYK: // 0 0 0 1: black, not the white that four zero components would be. c.n = 4 c.v[3] = 1 case .Separation, .Device_N: c.n = n for i in 0 ..< n { c.v[i] = 1 } case .Pattern: // Until a pattern is selected there is nothing to paint with. An // uncoloured pattern's components come from the underlying space. if cs.under != nil { return colorspace_default_color(cs.under) } c.n = 0 case .Device_Gray, .Device_RGB, .Cal_Gray, .Cal_RGB, .Lab, .ICC_Based, .Indexed: c.n = n } return } // colorspace_default_decode is the /Decode array an image in this space gets // when it does not supply one (ISO 32000-2, Table 89). `bpc` matters only for // an Indexed space, whose default maps the raw sample straight onto the // lookup table. // // The result is allocated with `allocator` and belongs to the caller. colorspace_default_decode :: proc(cs: ^Colorspace, bpc: int, allocator := context.allocator) -> []f64 { if cs == nil { return nil } n := min(cs.n, MAX_COLOR_COMPONENTS) if n < 1 { return nil } out := make([]f64, 2 * n, allocator) if cs.kind == .Indexed { bits := clamp(bpc, 1, 16) out[0] = 0 out[1] = f64(int(1) << uint(bits)) - 1 return out } for j in 0 ..< n { out[2 * j], out[2 * j + 1] = cs_component_range(cs, j) } return out }