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 ]") 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 ]") 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 ]" + RAMP + "]", "[/Separation /Spot [/Separation /Other /DeviceGray" + RAMP + "]" + RAMP + "]", "[/Separation /Spot [/DeviceN [/A] /DeviceGray" + RAMP + "]" + RAMP + "]", "[/Separation /Spot /Pattern" + RAMP + "]", "[/DeviceN [/A] [/Indexed /DeviceRGB 3 ]" + 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 ] /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") }