initial commit
This commit is contained in:
+866
@@ -0,0 +1,866 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user