867 lines
30 KiB
Odin
867 lines
30 KiB
Odin
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
|
||
|
|
}
|