815 lines
36 KiB
Odin
815 lines
36 KiB
Odin
package pdf
|
|||
|
|
|
||
|
|
import "core:testing"
|
||
|
|
|
||
|
|
// The fixtures -- `cf_open`, `cf_parse`, `cf_stream`, `cf_bytes` -- live in
|
||
|
|
// function_test.odin, because a colour space is not testable without the
|
||
|
|
// functions its tint transforms are made of.
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
TOL :: 1e-9
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
expect_rgb :: proc(t: ^testing.T, got: [3]f64, r, g, b: f64, tol: f64, what: string,
|
||
|
|
loc := #caller_location) {
|
||
|
|
if !(abs(got[0] - r) <= tol && abs(got[1] - g) <= tol && abs(got[2] - b) <= tol) {
|
||
|
|
testing.expectf(t, false, "%s: got (%f %f %f), want (%f %f %f)",
|
||
|
|
what, got[0], got[1], got[2], r, g, b, loc = loc)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
load_cs :: proc(t: ^testing.T, doc: ^Document, src: string, res: ^Dict = nil) -> ^Colorspace {
|
||
|
|
cs, err := colorspace_load(doc, cf_parse(doc, src), res)
|
||
|
|
testing.expect_value(t, err, Error.None)
|
||
|
|
return cs
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
cf_array :: proc(doc: ^Document, items: ..Object) -> ^Array {
|
||
|
|
a := new_array(doc.allocator, max(len(items), 1))
|
||
|
|
for it in items {
|
||
|
|
array_push(a, it)
|
||
|
|
}
|
||
|
|
return a
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// The device spaces
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_colorspace_device_gray :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
cs := colorspace_device_gray(doc)
|
||
|
|
testing.expect_value(t, cs.kind, Colorspace_Kind.Device_Gray)
|
||
|
|
testing.expect_value(t, cs.n, 1)
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0)), 0, 0, 0, TOL, "black")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 1, 1, 1, TOL, "white")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0.25)), 0.25, 0.25, 0.25, TOL, "quarter")
|
||
|
|
|
||
|
|
// Components outside 0..1 are clamped, not wrapped.
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(2)), 1, 1, 1, TOL, "too bright")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(-1)), 0, 0, 0, TOL, "negative")
|
||
|
|
|
||
|
|
// Naming it produces the very same space, not a copy.
|
||
|
|
named := load_cs(t, doc, "/DeviceGray")
|
||
|
|
testing.expect(t, named == cs, "/DeviceGray must be the shared space")
|
||
|
|
testing.expect(t, load_cs(t, doc, "[/DeviceGray]") == cs, "and so must [/DeviceGray]")
|
||
|
|
testing.expect(t, load_cs(t, doc, "/G") == cs, "and the inline-image abbreviation")
|
||
|
|
}
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_colorspace_device_rgb :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
cs := colorspace_device_rgb(doc)
|
||
|
|
testing.expect_value(t, cs.n, 3)
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0, 0, 0)), 0, 0, 0, TOL, "black")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(1, 1, 1)), 1, 1, 1, TOL, "white")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(1, 0.5, 0)), 1, 0.5, 0, TOL, "orange")
|
||
|
|
// A colour with too few components reads the missing ones as zero.
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 1, 0, 0, TOL, "short colour")
|
||
|
|
|
||
|
|
testing.expect(t, load_cs(t, doc, "/DeviceRGB") == cs, "/DeviceRGB")
|
||
|
|
testing.expect(t, load_cs(t, doc, "/RGB") == cs, "/RGB abbreviation")
|
||
|
|
}
|
||
|
|
|
||
|
|
// The naive conversion: subtract each ink and the black from full intensity.
|
||
|
|
// It is what every non-colour-managed viewer does.
|
||
|
|
@(test)
|
||
|
|
test_colorspace_device_cmyk :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
cs := colorspace_device_cmyk(doc)
|
||
|
|
testing.expect_value(t, cs.n, 4)
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0, 0, 0, 0)), 1, 1, 1, TOL, "no ink is white")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0, 0, 0, 1)), 0, 0, 0, TOL, "black ink")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(1, 0, 0, 0)), 0, 1, 1, TOL, "cyan")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0, 1, 0, 0)), 1, 0, 1, TOL, "magenta")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0, 0, 1, 0)), 1, 1, 0, TOL, "yellow")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0, 1, 1, 0)), 1, 0, 0, TOL, "red")
|
||
|
|
// Half a cyan under a quarter black: 1 - (0.5 + 0.25) against 1 - 0.25.
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0.5, 0, 0, 0.25)),
|
||
|
|
0.25, 0.75, 0.75, TOL, "ink plus black")
|
||
|
|
// Ink and black together past 1 saturate rather than going negative.
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(1, 1, 1, 1)), 0, 0, 0, TOL, "everything")
|
||
|
|
|
||
|
|
testing.expect(t, load_cs(t, doc, "/DeviceCMYK") == cs, "/DeviceCMYK")
|
||
|
|
testing.expect(t, load_cs(t, doc, "/CMYK") == cs, "/CMYK abbreviation")
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// CalGray and CalRGB
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
// With the default /Gamma these behave exactly as the device spaces, which is
|
||
|
|
// what the format permits and what viewers do.
|
||
|
|
@(test)
|
||
|
|
test_colorspace_cal_defaults_to_device :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
gray := load_cs(t, doc, "[/CalGray << /WhitePoint [0.9505 1 1.089] >>]")
|
||
|
|
if gray == nil { return }
|
||
|
|
testing.expect_value(t, gray.kind, Colorspace_Kind.Cal_Gray)
|
||
|
|
testing.expect_value(t, gray.n, 1)
|
||
|
|
expect_rgb(t, colorspace_to_rgb(gray, color_make(0.25)), 0.25, 0.25, 0.25, TOL, "gamma 1 gray")
|
||
|
|
|
||
|
|
rgb := load_cs(t, doc, "[/CalRGB << /WhitePoint [0.9505 1 1.089] >>]")
|
||
|
|
if rgb == nil { return }
|
||
|
|
testing.expect_value(t, rgb.kind, Colorspace_Kind.Cal_RGB)
|
||
|
|
testing.expect_value(t, rgb.n, 3)
|
||
|
|
expect_rgb(t, colorspace_to_rgb(rgb, color_make(1, 0.5, 0)), 1, 0.5, 0, TOL, "gamma 1 rgb")
|
||
|
|
|
||
|
|
// A white point that would divide by zero is refused; the space still
|
||
|
|
// loads, with the format's default.
|
||
|
|
bad := load_cs(t, doc, "[/CalGray << /WhitePoint [0 0 0] /Gamma 1 >>]")
|
||
|
|
if bad != nil {
|
||
|
|
testing.expect(t, bad.white_point[0] > 0, "a zero white point is replaced")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// /Gamma is honoured by decoding to linear light and re-encoding for sRGB. At
|
||
|
|
// the usual 2.2 that round trip lands within half a level of 255 of leaving
|
||
|
|
// the value alone, so the common case still agrees with every other viewer.
|
||
|
|
@(test)
|
||
|
|
test_colorspace_calgray_gamma :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
cs := load_cs(t, doc, "[/CalGray << /WhitePoint [0.9505 1 1.089] /Gamma 2.2 >>]")
|
||
|
|
if cs == nil { return }
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0)), 0, 0, 0, TOL, "black stays black")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 1, 1, 1, TOL, "white stays white")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0.5)),
|
||
|
|
0.5038667822745281, 0.5038667822745281, 0.5038667822745281, 1e-9,
|
||
|
|
"gamma 2.2 round trip at 0.5")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0.25)),
|
||
|
|
0.2410493652415971, 0.2410493652415971, 0.2410493652415971, 1e-9,
|
||
|
|
"gamma 2.2 round trip at 0.25")
|
||
|
|
// Which is to say: within one level of 255 of the DeviceGray answer.
|
||
|
|
got := colorspace_to_rgb(cs, color_make(0.5))
|
||
|
|
testing.expect(t, abs(got[0] - 0.5) < 1.0 / 255, "still what a viewer shows")
|
||
|
|
|
||
|
|
// A per-component /Gamma on CalRGB.
|
||
|
|
rgb := load_cs(t, doc,
|
||
|
|
"[/CalRGB << /WhitePoint [0.9505 1 1.089] /Gamma [2.2 1 2.2] >>]")
|
||
|
|
if rgb == nil { return }
|
||
|
|
out := colorspace_to_rgb(rgb, color_make(0.5, 0.5, 0.5))
|
||
|
|
testing.expect(t, abs(out[0] - 0.5038667822745281) < 1e-9, "red honours its gamma")
|
||
|
|
testing.expect(t, abs(out[1] - 0.5) < 1e-12, "green is left alone at gamma 1")
|
||
|
|
testing.expect(t, abs(out[2] - 0.5038667822745281) < 1e-9, "blue honours its gamma")
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Lab
|
||
|
|
//
|
||
|
|
// The reference values are the published sRGB primaries expressed in L*a*b*
|
||
|
|
// under a D50 white point, which is the white point PDF Lab spaces almost
|
||
|
|
// always carry. Getting back to within a level of 255 of pure red, green and
|
||
|
|
// blue is what says the conversion -- including the Bradford adaptation from
|
||
|
|
// D50 to sRGB's D65 -- is right.
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_colorspace_lab_known_colours :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
cs := load_cs(t, doc,
|
||
|
|
"[/Lab << /WhitePoint [0.9642 1 0.8249] /Range [-128 127 -128 127] >>]")
|
||
|
|
if cs == nil { return }
|
||
|
|
testing.expect_value(t, cs.kind, Colorspace_Kind.Lab)
|
||
|
|
testing.expect_value(t, cs.n, 3)
|
||
|
|
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(100, 0, 0)), 1, 1, 1, 1e-6, "L* 100 is white")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0, 0, 0)), 0, 0, 0, 1e-9, "L* 0 is black")
|
||
|
|
// L* 53.585 is the mid grey of sRGB: 128/255.
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(53.5850, 0, 0)),
|
||
|
|
128.0 / 255, 128.0 / 255, 128.0 / 255, 1e-5, "L* 53.585 is sRGB 128")
|
||
|
|
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(54.2917, 80.8125, 69.8911)),
|
||
|
|
1, 0, 0, 0.002, "sRGB red")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(87.8181, -79.2718, 80.9946)),
|
||
|
|
0, 1, 0, 0.002, "sRGB green")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(29.5676, 68.2986, -112.0294)),
|
||
|
|
0, 0, 1, 0.002, "sRGB blue")
|
||
|
|
}
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_colorspace_lab_d65_and_clamping :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// The same conversion under a D65 white point, where sRGB red has
|
||
|
|
// different coordinates.
|
||
|
|
d65 := load_cs(t, doc, "[/Lab << /WhitePoint [0.9505 1 1.089] >>]")
|
||
|
|
if d65 == nil { return }
|
||
|
|
expect_rgb(t, colorspace_to_rgb(d65, color_make(53.2408, 80.0925, 67.2032)),
|
||
|
|
1, 0, 0, 0.002, "sRGB red under D65")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(d65, color_make(100, 0, 0)), 1, 1, 1, 1e-6, "white under D65")
|
||
|
|
|
||
|
|
// /Range clamps a and b before anything else happens, so a wild value
|
||
|
|
// cannot walk out of the gamut and take the whole colour with it.
|
||
|
|
narrow := load_cs(t, doc,
|
||
|
|
"[/Lab << /WhitePoint [0.9642 1 0.8249] /Range [-10 10 -10 10] >>]")
|
||
|
|
if narrow == nil { return }
|
||
|
|
testing.expect_value(t, narrow.range[0], -10)
|
||
|
|
a := colorspace_to_rgb(narrow, color_make(50, 100, 0))
|
||
|
|
b := colorspace_to_rgb(narrow, color_make(50, 10, 0))
|
||
|
|
expect_rgb(t, a, b[0], b[1], b[2], 1e-12, "a is clamped to /Range")
|
||
|
|
|
||
|
|
// L* outside 0..100 is clamped too.
|
||
|
|
hi := colorspace_to_rgb(narrow, color_make(500, 0, 0))
|
||
|
|
expect_rgb(t, hi, 1, 1, 1, 1e-6, "L* above 100")
|
||
|
|
lo := colorspace_to_rgb(narrow, color_make(-500, 0, 0))
|
||
|
|
expect_rgb(t, lo, 0, 0, 0, 1e-9, "L* below 0")
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// ICCBased
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_colorspace_iccbased :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// A colour space is a name or an array, never a bare stream: ICCBased is
|
||
|
|
// always written [/ICCBased stream] (ISO 32000-2, 8.6.5.5). Handing the
|
||
|
|
// stream over on its own must be refused rather than guessed at, or a
|
||
|
|
// stream that happens to carry an /N would be read as a colour space
|
||
|
|
// wherever one is expected.
|
||
|
|
_, bare_err := colorspace_load(doc, cf_stream(doc, "<< /N 3 >>", nil), nil)
|
||
|
|
testing.expect_value(t, bare_err, Error.Bad_Colorspace)
|
||
|
|
|
||
|
|
// No /Alternate: the device space /N implies.
|
||
|
|
icc3_arr := cf_array(doc, Name("ICCBased"), cf_stream(doc, "<< /N 3 >>", nil))
|
||
|
|
icc3, icc3_err := colorspace_load(doc, icc3_arr, nil)
|
||
|
|
testing.expect_value(t, icc3_err, Error.None)
|
||
|
|
if icc3 == nil {
|
||
|
|
testing.expect(t, false, "an ICCBased space with /N 3 must load")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
testing.expect_value(t, icc3.n, 3)
|
||
|
|
expect_rgb(t, colorspace_to_rgb(icc3, color_make(1, 0, 0)), 1, 0, 0, TOL,
|
||
|
|
"three components render as RGB")
|
||
|
|
// The array form is what a file actually writes; the stream alone is what
|
||
|
|
// cf_stream can build, so both are exercised.
|
||
|
|
arr := cf_array(doc, Name("ICCBased"), cf_stream(doc, "<< /N 4 >>", nil))
|
||
|
|
icc4, err4 := colorspace_load(doc, arr, nil)
|
||
|
|
testing.expect_value(t, err4, Error.None)
|
||
|
|
if icc4 == nil { return }
|
||
|
|
testing.expect_value(t, icc4.kind, Colorspace_Kind.ICC_Based)
|
||
|
|
testing.expect_value(t, icc4.n, 4)
|
||
|
|
expect_rgb(t, colorspace_to_rgb(icc4, color_make(0, 1, 1, 0)), 1, 0, 0, TOL,
|
||
|
|
"four components render as CMYK")
|
||
|
|
|
||
|
|
// An /Alternate is used when it agrees with /N.
|
||
|
|
alt := cf_array(doc, Name("ICCBased"),
|
||
|
|
cf_stream(doc, "<< /N 3 /Alternate [/Lab << /WhitePoint [0.9642 1 0.8249] >>] >>", nil))
|
||
|
|
lab_icc, _ := colorspace_load(doc, alt, nil)
|
||
|
|
if lab_icc == nil { return }
|
||
|
|
testing.expect_value(t, lab_icc.n, 3)
|
||
|
|
expect_rgb(t, colorspace_to_rgb(lab_icc, color_make(100, 0, 0)), 1, 1, 1, 1e-6,
|
||
|
|
"rendered through the alternate")
|
||
|
|
|
||
|
|
// An /Alternate that contradicts /N is ignored in favour of /N.
|
||
|
|
bad := cf_array(doc, Name("ICCBased"),
|
||
|
|
cf_stream(doc, "<< /N 1 /Alternate /DeviceCMYK >>", nil))
|
||
|
|
gray_icc, _ := colorspace_load(doc, bad, nil)
|
||
|
|
if gray_icc == nil { return }
|
||
|
|
testing.expect_value(t, gray_icc.n, 1)
|
||
|
|
expect_rgb(t, colorspace_to_rgb(gray_icc, color_make(0.5)), 0.5, 0.5, 0.5, TOL,
|
||
|
|
"one component is grey whatever the alternate said")
|
||
|
|
|
||
|
|
// Neither /N nor /Alternate: three components assumed, and a repair.
|
||
|
|
guess := cf_array(doc, Name("ICCBased"), cf_stream(doc, "<< >>", nil))
|
||
|
|
guessed, _ := colorspace_load(doc, guess, nil)
|
||
|
|
if guessed != nil {
|
||
|
|
testing.expect_value(t, guessed.n, 3)
|
||
|
|
testing.expect(t, doc_has_repair(doc, .Bad_Resource), "guessing /N is a repair")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Indexed
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_colorspace_indexed :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
cs, err := colorspace_load(doc, Ref{10, 0}, nil)
|
||
|
|
testing.expect_value(t, err, Error.None)
|
||
|
|
if cs == nil { return }
|
||
|
|
testing.expect_value(t, cs.kind, Colorspace_Kind.Indexed)
|
||
|
|
// An Indexed colour has one component -- the index -- whatever the base is.
|
||
|
|
testing.expect_value(t, cs.n, 1)
|
||
|
|
testing.expect_value(t, cs.hival, 3)
|
||
|
|
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0)), 1, 0, 0, TOL, "entry 0")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 0, 1, 0, TOL, "entry 1")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(2)), 0, 0, 1, TOL, "entry 2")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(3)), 1, 1, 1, TOL, "entry 3")
|
||
|
|
}
|
||
|
|
|
||
|
|
// An index outside 0..hival is clamped. It must never read past the table.
|
||
|
|
@(test)
|
||
|
|
test_colorspace_indexed_clamps :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
cs, _ := colorspace_load(doc, Ref{10, 0}, nil)
|
||
|
|
if cs == nil { return }
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(4)), 1, 1, 1, TOL, "one past the end")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(9999)), 1, 1, 1, TOL, "far past the end")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(-1)), 1, 0, 0, TOL, "below the start")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(-9999)), 1, 0, 0, TOL, "far below")
|
||
|
|
|
||
|
|
// A table shorter than /HiVal claims: the entries that are not there read
|
||
|
|
// as zero rather than off the end of the buffer.
|
||
|
|
short := load_cs(t, doc, "[/Indexed /DeviceRGB 5 <FF0000>]")
|
||
|
|
if short == nil { return }
|
||
|
|
testing.expect_value(t, short.hival, 5)
|
||
|
|
expect_rgb(t, colorspace_to_rgb(short, color_make(0)), 1, 0, 0, TOL, "the entry that exists")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(short, color_make(5)), 0, 0, 0, TOL, "one that does not")
|
||
|
|
|
||
|
|
// /HiVal above 255 is not a table this format can index.
|
||
|
|
big := load_cs(t, doc, "[/Indexed /DeviceRGB 99999 <FF0000>]")
|
||
|
|
if big != nil {
|
||
|
|
testing.expect_value(t, big.hival, 255)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_colorspace_indexed_variants :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// The lookup table may be a stream instead of a string.
|
||
|
|
stm := cf_stream(doc, "<< >>", []byte{0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF})
|
||
|
|
cs, err := colorspace_load(doc,
|
||
|
|
cf_array(doc, Name("Indexed"), Name("DeviceRGB"), i64(1), stm), nil)
|
||
|
|
testing.expect_value(t, err, Error.None)
|
||
|
|
if cs == nil { return }
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0)), 0, 0, 0, TOL, "stream entry 0")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 1, 1, 1, TOL, "stream entry 1")
|
||
|
|
|
||
|
|
// A base whose components are not 0..1: each table byte spans the base
|
||
|
|
// component's own range, so 255 in the L* slot is L* 100.
|
||
|
|
lab := load_cs(t, doc,
|
||
|
|
"[/Indexed [/Lab << /WhitePoint [0.9642 1 0.8249] /Range [-128 127] >>] " +
|
||
|
|
"1 <008080 FF8080>]")
|
||
|
|
if lab == nil { return }
|
||
|
|
// 0x80 is the middle of [-128 127], which is a* = b* = -0.5: near neutral.
|
||
|
|
expect_rgb(t, colorspace_to_rgb(lab, color_make(0)), 0, 0, 0, 0.01, "L* 0 entry")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(lab, color_make(1)), 1, 1, 1, 0.01, "L* 100 entry")
|
||
|
|
|
||
|
|
// An Indexed space over another Indexed space, or over a Pattern, means
|
||
|
|
// nothing and is refused.
|
||
|
|
_, e1 := colorspace_load(doc, cf_parse(doc, "[/Indexed [/Indexed /DeviceRGB 1 <0000>] 1 <0001>]"), nil)
|
||
|
|
testing.expect_value(t, e1, Error.Bad_Colorspace)
|
||
|
|
_, e2 := colorspace_load(doc, cf_parse(doc, "[/Indexed /Pattern 1 <0001>]"), nil)
|
||
|
|
testing.expect_value(t, e2, Error.Bad_Colorspace)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Separation and DeviceN
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_colorspace_separation :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// A spot colour whose tint transform is a type 2 function into DeviceCMYK:
|
||
|
|
// full tint is magenta plus yellow, which is red.
|
||
|
|
cs, err := colorspace_load(doc, Ref{5, 0}, nil)
|
||
|
|
testing.expect_value(t, err, Error.None)
|
||
|
|
if cs == nil { return }
|
||
|
|
testing.expect_value(t, cs.kind, Colorspace_Kind.Separation)
|
||
|
|
testing.expect_value(t, cs.n, 1)
|
||
|
|
testing.expect(t, cs.tint != nil, "the tint transform loaded")
|
||
|
|
testing.expect(t, cs.alt != nil && cs.alt.kind == .Device_CMYK, "the alternate space")
|
||
|
|
testing.expect(t, len(cs.colorant_names) == 1 && cs.colorant_names[0] == "Spot", "colorant")
|
||
|
|
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0)), 1, 1, 1, TOL, "no tint is white")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 1, 0, 0, TOL, "full tint is red")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0.5)), 1, 0.5, 0.5, TOL, "half tint")
|
||
|
|
|
||
|
|
// The initial colour in a Separation is full tint, not zero (8.6.8).
|
||
|
|
c := colorspace_default_color(cs)
|
||
|
|
testing.expect_value(t, c.n, 1)
|
||
|
|
testing.expect_value(t, c.v[0], 1)
|
||
|
|
}
|
||
|
|
|
||
|
|
// /None marks nothing at all on the page.
|
||
|
|
@(test)
|
||
|
|
test_colorspace_separation_none :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
cs := load_cs(t, doc,
|
||
|
|
"[/Separation /None /DeviceCMYK << /FunctionType 2 /Domain [0 1] " +
|
||
|
|
"/C0 [0 0 0 0] /C1 [0 0 0 1] /N 1 >>]")
|
||
|
|
if cs == nil { return }
|
||
|
|
testing.expect(t, cs.is_none, "/None paints nothing")
|
||
|
|
testing.expect(t, !cs.is_all, "and is not /All")
|
||
|
|
// Even at full tint, which would otherwise be solid black.
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 1, 1, 1, TOL, "nothing is painted")
|
||
|
|
|
||
|
|
all := load_cs(t, doc,
|
||
|
|
"[/Separation /All /DeviceCMYK << /FunctionType 2 /Domain [0 1] " +
|
||
|
|
"/C0 [0 0 0 0] /C1 [0 0 0 1] /N 1 >>]")
|
||
|
|
if all == nil { return }
|
||
|
|
testing.expect(t, all.is_all, "/All marks every separation")
|
||
|
|
testing.expect(t, !all.is_none, "and is not /None")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(all, color_make(1)), 0, 0, 0, TOL, "full tint is black")
|
||
|
|
}
|
||
|
|
|
||
|
|
// A tint transform that is missing or unusable must not lose the artwork: the
|
||
|
|
// colorant is treated as ink coverage instead.
|
||
|
|
@(test)
|
||
|
|
test_colorspace_separation_broken_tint :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
cs := load_cs(t, doc, "[/Separation /Spot /DeviceCMYK << /FunctionType 9 >>]")
|
||
|
|
if cs == nil { return }
|
||
|
|
testing.expect(t, cs.tint == nil, "the tint transform did not load")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0)), 1, 1, 1, TOL, "no ink")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 0, 0, 0, TOL, "full ink")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0.25)), 0.75, 0.75, 0.75, TOL, "a quarter")
|
||
|
|
testing.expect(t, doc_has_repair(doc, .Bad_Resource), "a broken tint is a repair")
|
||
|
|
|
||
|
|
// The alternate space missing entirely is survivable in the same way.
|
||
|
|
no_alt := load_cs(t, doc, "[/Separation /Spot]")
|
||
|
|
if no_alt != nil {
|
||
|
|
expect_rgb(t, colorspace_to_rgb(no_alt, color_make(1)), 0, 0, 0, TOL, "no alternate")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 8.6.6.4 and 8.6.6.5: the alternate "shall not be another special colour
|
||
|
|
// space (Pattern, Indexed, Separation, or DeviceN)", and the four are excluded
|
||
|
|
// for one reason -- a tint transform's output cannot be read as a colour in
|
||
|
|
// any of them. Indexed is the one that shows: its single component is a
|
||
|
|
// palette index, so a tint running 0..1 lands on entry 0 or entry 1 and the
|
||
|
|
// whole ramp collapses onto two colours. Refusing the alternate drops the
|
||
|
|
// space onto the ink-coverage fallback, which is monotone in the tint and
|
||
|
|
// keeps the artwork.
|
||
|
|
@(test)
|
||
|
|
test_colorspace_separation_special_alternate :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
RAMP :: " << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >>"
|
||
|
|
for src in ([]string{
|
||
|
|
"[/Separation /Spot [/Indexed /DeviceRGB 3 <FF000000FF000000FFFFFFFF>]" + RAMP + "]",
|
||
|
|
"[/Separation /Spot [/Separation /Other /DeviceGray" + RAMP + "]" + RAMP + "]",
|
||
|
|
"[/Separation /Spot [/DeviceN [/A] /DeviceGray" + RAMP + "]" + RAMP + "]",
|
||
|
|
"[/Separation /Spot /Pattern" + RAMP + "]",
|
||
|
|
"[/DeviceN [/A] [/Indexed /DeviceRGB 3 <FF000000FF000000FFFFFFFF>]" + RAMP + "]",
|
||
|
|
}) {
|
||
|
|
cs := load_cs(t, doc, src)
|
||
|
|
if cs == nil { continue }
|
||
|
|
testing.expectf(t, cs.alt == nil, "%s: the alternate must be refused", src)
|
||
|
|
testing.expectf(t, cs.tint == nil, "%s: and its tint transform with it", src)
|
||
|
|
// Ink coverage, not two palette entries: the ramp runs the right way
|
||
|
|
// and takes every value in between.
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0)), 1, 1, 1, TOL, "no ink")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0.25)), 0.75, 0.75, 0.75, TOL, "a quarter")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0.5)), 0.5, 0.5, 0.5, TOL, "half")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 0, 0, 0, TOL, "full ink")
|
||
|
|
}
|
||
|
|
testing.expect(t, doc_has_repair(doc, .Bad_Resource), "an unusable alternate is a repair")
|
||
|
|
|
||
|
|
// The control: a device alternate is what the rule allows, and it still
|
||
|
|
// goes through the tint transform.
|
||
|
|
good := load_cs(t, doc,
|
||
|
|
"[/Separation /Spot /DeviceGray << /FunctionType 2 /Domain [0 1] " +
|
||
|
|
"/C0 [1] /C1 [0] /N 1 >>]")
|
||
|
|
if good == nil { return }
|
||
|
|
testing.expect(t, good.alt != nil && good.alt.kind == .Device_Gray, "a device alternate loads")
|
||
|
|
testing.expect(t, good.tint != nil, "and keeps its tint transform")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(good, color_make(0.25)), 0.75, 0.75, 0.75, TOL, "through the tint")
|
||
|
|
}
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_colorspace_devicen :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// Two colorants into DeviceRGB. The program throws the second away and
|
||
|
|
// copies the first into all three outputs.
|
||
|
|
tint := cf_stream(doc,
|
||
|
|
"<< /FunctionType 4 /Domain [0 1 0 1] /Range [0 1 0 1 0 1] >>",
|
||
|
|
cf_bytes("{ pop dup dup }"))
|
||
|
|
names := cf_array(doc, Name("Ink1"), Name("Ink2"))
|
||
|
|
cs, err := colorspace_load(doc,
|
||
|
|
cf_array(doc, Name("DeviceN"), names, Name("DeviceRGB"), tint), nil)
|
||
|
|
testing.expect_value(t, err, Error.None)
|
||
|
|
if cs == nil { return }
|
||
|
|
testing.expect_value(t, cs.kind, Colorspace_Kind.Device_N)
|
||
|
|
testing.expect_value(t, cs.n, 2)
|
||
|
|
testing.expect(t, cs.tint != nil, "the tint transform loaded")
|
||
|
|
testing.expect(t, len(cs.colorant_names) == 2 && cs.colorant_names[1] == "Ink2", "colorants")
|
||
|
|
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0.25, 0.9)),
|
||
|
|
0.25, 0.25, 0.25, TOL, "the first colorant drives all three outputs")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(1, 0)), 1, 1, 1, TOL, "full first colorant")
|
||
|
|
|
||
|
|
// The initial colour is 1.0 in every component.
|
||
|
|
c := colorspace_default_color(cs)
|
||
|
|
testing.expect_value(t, c.n, 2)
|
||
|
|
testing.expect_value(t, c.v[0], 1)
|
||
|
|
testing.expect_value(t, c.v[1], 1)
|
||
|
|
}
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_colorspace_devicen_degrades :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
// No tint transform at all: the darkest colorant becomes ink coverage.
|
||
|
|
names := cf_array(doc, Name("Ink1"), Name("Ink2"))
|
||
|
|
cs, err := colorspace_load(doc,
|
||
|
|
cf_array(doc, Name("DeviceN"), names, Name("DeviceRGB")), nil)
|
||
|
|
testing.expect_value(t, err, Error.None)
|
||
|
|
if cs == nil { return }
|
||
|
|
testing.expect_value(t, cs.n, 2)
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0, 0)), 1, 1, 1, TOL, "no ink")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0.25, 0.75)),
|
||
|
|
0.25, 0.25, 0.25, TOL, "the darkest colorant")
|
||
|
|
|
||
|
|
// Every colorant /None: nothing is marked.
|
||
|
|
nones := cf_array(doc, Name("None"), Name("None"))
|
||
|
|
all_none, _ := colorspace_load(doc,
|
||
|
|
cf_array(doc, Name("DeviceN"), nones, Name("DeviceRGB")), nil)
|
||
|
|
if all_none != nil {
|
||
|
|
testing.expect(t, all_none.is_none, "a DeviceN of nothing but /None marks nothing")
|
||
|
|
}
|
||
|
|
// One real colorant among them still marks.
|
||
|
|
mixed := cf_array(doc, Name("None"), Name("Ink"))
|
||
|
|
some, _ := colorspace_load(doc,
|
||
|
|
cf_array(doc, Name("DeviceN"), mixed, Name("DeviceRGB")), nil)
|
||
|
|
if some != nil {
|
||
|
|
testing.expect(t, !some.is_none, "one real colorant is still painted")
|
||
|
|
}
|
||
|
|
|
||
|
|
// A DeviceN with no colorants at all is not a colour space.
|
||
|
|
_, e := colorspace_load(doc, cf_parse(doc, "[/DeviceN [] /DeviceRGB]"), nil)
|
||
|
|
testing.expect_value(t, e, Error.Bad_Colorspace)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Pattern
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_colorspace_pattern :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
plain := load_cs(t, doc, "/Pattern")
|
||
|
|
if plain == nil { return }
|
||
|
|
testing.expect_value(t, plain.kind, Colorspace_Kind.Pattern)
|
||
|
|
testing.expect(t, plain.under == nil, "a coloured pattern has no underlying space")
|
||
|
|
// Nothing has been selected yet, so there are no components to carry --
|
||
|
|
// `/P1 scn` is a name and no numbers, which is what cs.n has to say.
|
||
|
|
testing.expect_value(t, plain.n, 0)
|
||
|
|
testing.expect_value(t, colorspace_default_color(plain).n, 0)
|
||
|
|
// And no image can be in a Pattern space (8.9.5.1), so it has no default
|
||
|
|
// /Decode either.
|
||
|
|
testing.expect(t, colorspace_default_decode(plain, 8, context.allocator) == nil,
|
||
|
|
"a Pattern space has no image decode array")
|
||
|
|
|
||
|
|
// An uncoloured pattern carries the colour it is painted in.
|
||
|
|
under := load_cs(t, doc, "[/Pattern /DeviceCMYK]")
|
||
|
|
if under == nil { return }
|
||
|
|
testing.expect_value(t, under.kind, Colorspace_Kind.Pattern)
|
||
|
|
testing.expect_value(t, under.n, 4)
|
||
|
|
testing.expect(t, under.under != nil && under.under.kind == .Device_CMYK, "underlying space")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(under, color_make(0, 1, 1, 0)), 1, 0, 0, TOL,
|
||
|
|
"painted through the underlying space")
|
||
|
|
// Whose initial colour is the underlying space's.
|
||
|
|
c := colorspace_default_color(under)
|
||
|
|
testing.expect_value(t, c.n, 4)
|
||
|
|
testing.expect_value(t, c.v[3], 1)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Resource lookup, caching and malformed input
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_colorspace_named_through_resources :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
res, _ := to_dict(cf_parse(doc,
|
||
|
|
"<< /ColorSpace << /CS0 [/Indexed /DeviceRGB 1 <FF000000FF00>] /CS1 /DeviceCMYK >> >>"))
|
||
|
|
cs0 := load_cs(t, doc, "/CS0", res)
|
||
|
|
if cs0 == nil { return }
|
||
|
|
testing.expect_value(t, cs0.kind, Colorspace_Kind.Indexed)
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs0, color_make(1)), 0, 1, 0, TOL, "resolved through /Resources")
|
||
|
|
|
||
|
|
cs1 := load_cs(t, doc, "/CS1", res)
|
||
|
|
testing.expect(t, cs1 == colorspace_device_cmyk(doc), "a name for a device space")
|
||
|
|
|
||
|
|
// A caller holding only the /ColorSpace dictionary may pass that instead.
|
||
|
|
csdict, _ := to_dict(cf_parse(doc, "<< /CSX /DeviceCMYK >>"))
|
||
|
|
testing.expect(t, load_cs(t, doc, "/CSX", csdict) == colorspace_device_cmyk(doc),
|
||
|
|
"the /ColorSpace dictionary works too")
|
||
|
|
|
||
|
|
// A name that is in no resource dictionary is not a colour space.
|
||
|
|
_, err := colorspace_load(doc, cf_parse(doc, "/Nope"), res)
|
||
|
|
testing.expect_value(t, err, Error.Bad_Colorspace)
|
||
|
|
|
||
|
|
// A device name cannot be redefined by a resource dictionary.
|
||
|
|
shadow, _ := to_dict(cf_parse(doc, "<< /ColorSpace << /DeviceRGB /DeviceCMYK >> >>"))
|
||
|
|
testing.expect(t, load_cs(t, doc, "/DeviceRGB", shadow) == colorspace_device_rgb(doc),
|
||
|
|
"/DeviceRGB is always /DeviceRGB")
|
||
|
|
}
|
||
|
|
|
||
|
|
// A page that names the same space in four hundred XObjects must build it once.
|
||
|
|
@(test)
|
||
|
|
test_colorspace_cached_by_object :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
a, _ := colorspace_load(doc, Ref{10, 0}, nil)
|
||
|
|
b, _ := colorspace_load(doc, Ref{10, 0}, nil)
|
||
|
|
testing.expect(t, a != nil && a == b, "the same object must build one space")
|
||
|
|
|
||
|
|
// Reached by name through a resource dictionary, the space behind the name
|
||
|
|
// is the same object and so is cached as well.
|
||
|
|
res, _ := to_dict(cf_parse(doc, "<< /ColorSpace << /CS0 10 0 R >> >>"))
|
||
|
|
c := load_cs(t, doc, "/CS0", res)
|
||
|
|
testing.expect(t, c == a, "the name resolves onto the cached space")
|
||
|
|
|
||
|
|
// A Separation loaded twice reuses the loaded tint transform, too.
|
||
|
|
s1, _ := colorspace_load(doc, Ref{5, 0}, nil)
|
||
|
|
s2, _ := colorspace_load(doc, Ref{5, 0}, nil)
|
||
|
|
testing.expect(t, s1 != nil && s1 == s2, "a Separation is cached")
|
||
|
|
}
|
||
|
|
|
||
|
|
// A colour space that names itself must stop rather than recursing forever.
|
||
|
|
@(test)
|
||
|
|
test_colorspace_self_reference :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
res, _ := to_dict(cf_parse(doc, "<< /ColorSpace << /Self 9 0 R >> >>"))
|
||
|
|
cs, err := colorspace_load(doc, cf_parse(doc, "/Self"), res)
|
||
|
|
testing.expect(t, cs == nil, "a self-naming space cannot be built")
|
||
|
|
testing.expect_value(t, err, Error.Recursion_Limit)
|
||
|
|
testing.expect(t, doc_has_repair(doc, .Recursion_Limit_Hit), "and it is a repair")
|
||
|
|
}
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_colorspace_rejects_nonsense :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
_, e1 := colorspace_load(doc, nil, nil)
|
||
|
|
testing.expect_value(t, e1, Error.Bad_Colorspace)
|
||
|
|
|
||
|
|
_, e2 := colorspace_load(doc, cf_parse(doc, "42"), nil)
|
||
|
|
testing.expect_value(t, e2, Error.Bad_Colorspace)
|
||
|
|
|
||
|
|
_, e3 := colorspace_load(doc, cf_parse(doc, "[]"), nil)
|
||
|
|
testing.expect_value(t, e3, Error.Bad_Colorspace)
|
||
|
|
|
||
|
|
_, e4 := colorspace_load(doc, cf_parse(doc, "[/NotASpace 1 2 3]"), nil)
|
||
|
|
testing.expect_value(t, e4, Error.Bad_Colorspace)
|
||
|
|
|
||
|
|
// Converting through no space at all is black, not a crash.
|
||
|
|
expect_rgb(t, colorspace_to_rgb(nil, color_make(1, 1, 1)), 0, 0, 0, TOL, "nil space")
|
||
|
|
testing.expect_value(t, colorspace_default_color(nil).n, 1)
|
||
|
|
testing.expect(t, colorspace_default_decode(nil, 8) == nil, "nil space has no decode")
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Initial colours and image decode arrays
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_colorspace_default_color :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
gray := colorspace_default_color(colorspace_device_gray(doc))
|
||
|
|
testing.expect_value(t, gray.n, 1)
|
||
|
|
testing.expect_value(t, gray.v[0], 0)
|
||
|
|
|
||
|
|
rgb := colorspace_default_color(colorspace_device_rgb(doc))
|
||
|
|
testing.expect_value(t, rgb.n, 3)
|
||
|
|
expect_rgb(t, colorspace_to_rgb(colorspace_device_rgb(doc), rgb), 0, 0, 0, TOL, "black")
|
||
|
|
|
||
|
|
// DeviceCMYK starts at 0 0 0 1 -- black -- not at four zeros, which would
|
||
|
|
// be white (8.6.4.4).
|
||
|
|
cmyk := colorspace_default_color(colorspace_device_cmyk(doc))
|
||
|
|
testing.expect_value(t, cmyk.n, 4)
|
||
|
|
testing.expect_value(t, cmyk.v[3], 1)
|
||
|
|
expect_rgb(t, colorspace_to_rgb(colorspace_device_cmyk(doc), cmyk), 0, 0, 0, TOL, "black")
|
||
|
|
|
||
|
|
// Indexed starts at entry zero.
|
||
|
|
idx, _ := colorspace_load(doc, Ref{10, 0}, nil)
|
||
|
|
if idx != nil {
|
||
|
|
c := colorspace_default_color(idx)
|
||
|
|
testing.expect_value(t, c.n, 1)
|
||
|
|
testing.expect_value(t, c.v[0], 0)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Lab starts at 0 0 0, which is black.
|
||
|
|
lab := load_cs(t, doc, "[/Lab << /WhitePoint [0.9642 1 0.8249] >>]")
|
||
|
|
if lab != nil {
|
||
|
|
c := colorspace_default_color(lab)
|
||
|
|
testing.expect_value(t, c.n, 3)
|
||
|
|
expect_rgb(t, colorspace_to_rgb(lab, c), 0, 0, 0, TOL, "black")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@(test)
|
||
|
|
test_colorspace_default_decode :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
gray := colorspace_default_decode(colorspace_device_gray(doc), 8)
|
||
|
|
defer delete(gray)
|
||
|
|
testing.expect_value(t, len(gray), 2)
|
||
|
|
testing.expect_value(t, gray[0], 0)
|
||
|
|
testing.expect_value(t, gray[1], 1)
|
||
|
|
|
||
|
|
cmyk := colorspace_default_decode(colorspace_device_cmyk(doc), 8)
|
||
|
|
defer delete(cmyk)
|
||
|
|
testing.expect_value(t, len(cmyk), 8)
|
||
|
|
testing.expect_value(t, cmyk[7], 1)
|
||
|
|
|
||
|
|
// Indexed maps the raw sample straight onto the table, so its default
|
||
|
|
// depends on the bit depth.
|
||
|
|
idx, _ := colorspace_load(doc, Ref{10, 0}, nil)
|
||
|
|
if idx != nil {
|
||
|
|
Depth :: struct { bpc: int, want: f64 }
|
||
|
|
depths := []Depth{
|
||
|
|
Depth{1, 1}, Depth{2, 3}, Depth{4, 15}, Depth{8, 255}, Depth{16, 65535},
|
||
|
|
}
|
||
|
|
for c in depths {
|
||
|
|
d := colorspace_default_decode(idx, c.bpc)
|
||
|
|
defer delete(d)
|
||
|
|
testing.expect_value(t, len(d), 2)
|
||
|
|
testing.expect_value(t, d[0], 0)
|
||
|
|
testing.expectf(t, d[1] == c.want, "bpc %d: got %f, want %f", c.bpc, d[1], c.want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Lab's default decode is its own component ranges.
|
||
|
|
lab := load_cs(t, doc,
|
||
|
|
"[/Lab << /WhitePoint [0.9642 1 0.8249] /Range [-90 80 -70 60] >>]")
|
||
|
|
if lab != nil {
|
||
|
|
d := colorspace_default_decode(lab, 8)
|
||
|
|
defer delete(d)
|
||
|
|
testing.expect_value(t, len(d), 6)
|
||
|
|
testing.expect_value(t, d[0], 0)
|
||
|
|
testing.expect_value(t, d[1], 100)
|
||
|
|
testing.expect_value(t, d[2], -90)
|
||
|
|
testing.expect_value(t, d[3], 80)
|
||
|
|
testing.expect_value(t, d[4], -70)
|
||
|
|
testing.expect_value(t, d[5], 60)
|
||
|
|
}
|
||
|
|
|
||
|
|
// A Separation's decode is one 0..1 pair, whatever its alternate space is.
|
||
|
|
sep, _ := colorspace_load(doc, Ref{5, 0}, nil)
|
||
|
|
if sep != nil {
|
||
|
|
d := colorspace_default_decode(sep, 8)
|
||
|
|
defer delete(d)
|
||
|
|
testing.expect_value(t, len(d), 2)
|
||
|
|
testing.expect_value(t, d[1], 1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// A colour space nested inside another still converts: an Indexed table whose
|
||
|
|
// entries are spot-colour tints.
|
||
|
|
@(test)
|
||
|
|
test_colorspace_nested :: proc(t: ^testing.T) {
|
||
|
|
doc := cf_open(t)
|
||
|
|
defer cf_close(t, doc)
|
||
|
|
|
||
|
|
cs, err := colorspace_load(doc,
|
||
|
|
cf_array(doc, Name("Indexed"), Ref{5, 0}, i64(1), String(transmute([]byte)string("\x00\xff"))),
|
||
|
|
nil)
|
||
|
|
testing.expect_value(t, err, Error.None)
|
||
|
|
if cs == nil { return }
|
||
|
|
testing.expect(t, cs.base != nil && cs.base.kind == .Separation, "the base is the spot colour")
|
||
|
|
// Entry 0 is no tint at all, entry 1 is full tint: white, then red.
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(0)), 1, 1, 1, TOL, "entry 0")
|
||
|
|
expect_rgb(t, colorspace_to_rgb(cs, color_make(1)), 1, 0, 0, TOL, "entry 1")
|
||
|
|
}
|