package pdf import "core:testing" // The fixtures -- `cf_open`, `cf_parse`, `cf_stream`, `cf_close` -- live in // function_test.odin. The JPEG byte literals `JPEG_T_CMYK` and `JPEG_T_GREY8` // live in filter_dct_test.odin; using the decoder's own vectors here is what // makes the DCT tests below about /Decode and colour rather than about // re-testing the JPEG decoder. // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- @(private = "file") im_load :: proc(t: ^testing.T, doc: ^Document, dict_src: string, payload: []byte, res: ^Dict = nil, loc := #caller_location) -> ^Image { img, err := image_load(doc, cf_stream(doc, dict_src, payload), res) testing.expectf(t, err == .None, "image_load: %v", err, loc = loc) return img } @(private = "file") im_pixels :: proc(t: ^testing.T, doc: ^Document, img: ^Image, loc := #caller_location) -> ^Bitmap { testing.expect(t, img != nil, "no image to decode", loc = loc) if img == nil { return nil } bmp, err := image_decode(doc, img) testing.expectf(t, err == .None, "image_decode: %v", err, loc = loc) testing.expect(t, bmp != nil, "image_decode returned no bitmap", loc = loc) return bmp } // im_decode loads and decodes in one step, which is what most of these want. @(private = "file") im_decode :: proc(t: ^testing.T, doc: ^Document, dict_src: string, payload: []byte, res: ^Dict = nil, loc := #caller_location) -> ^Bitmap { return im_pixels(t, doc, im_load(t, doc, dict_src, payload, res, loc), loc) } @(private = "file") im_rgba :: proc(bmp: ^Bitmap, x, y: int) -> [4]u8 { if bmp == nil || bmp.n != 4 { return {} } off, ok := bitmap_pixel_offset(bmp, x, y) if !ok { return {} } return {bmp.pixels[off], bmp.pixels[off + 1], bmp.pixels[off + 2], bmp.pixels[off + 3]} } @(private = "file") expect_rgba :: proc(t: ^testing.T, bmp: ^Bitmap, x, y: int, want: [4]u8, what: string, loc := #caller_location) { got := im_rgba(bmp, x, y) if got != want { testing.expectf(t, false, "%s at (%d,%d): got %v, want %v", what, x, y, got, want, loc = loc) } } @(private = "file") expect_rgba_near :: proc(t: ^testing.T, bmp: ^Bitmap, x, y: int, want: [4]u8, tol: int, what: string, loc := #caller_location) { got := im_rgba(bmp, x, y) worst := 0 for i in 0 ..< 4 { worst = max(worst, abs(int(got[i]) - int(want[i]))) } if worst > tol { testing.expectf(t, false, "%s at (%d,%d): got %v, want %v (+-%d)", what, x, y, got, want, tol, loc = loc) } } // expect_gray reads a one-channel coverage plane, which is what an /ImageMask // decodes to. @(private = "file") expect_cov :: proc(t: ^testing.T, bmp: ^Bitmap, x, y: int, want: u8, what: string, loc := #caller_location) { if bmp == nil || bmp.n != 1 { testing.expectf(t, false, "%s: expected a 1-channel plane, got n=%d", what, bmp == nil ? -1 : bmp.n, loc = loc) return } off, ok := bitmap_pixel_offset(bmp, x, y) if !ok { testing.expectf(t, false, "%s: (%d,%d) out of range", what, x, y, loc = loc) return } if bmp.pixels[off] != want { testing.expectf(t, false, "%s at (%d,%d): got %d, want %d", what, x, y, bmp.pixels[off], want, loc = loc) } } // im_arena copies bytes into the Document's arena, so a payload built in a // test outlives the Stream that borrows it. @(private = "file") im_arena :: proc(doc: ^Document, src: []byte) -> []byte { b := make([]byte, len(src), doc.allocator) copy(b, src) return b } @(private = "file") im_arena_fill :: proc(doc: ^Document, n: int, fill: u8) -> []byte { b := make([]byte, n, doc.allocator) for &v in b { v = fill } return b } // --------------------------------------------------------------------------- // Sample unpacking, one test per bit depth // // Expected bytes are hand-computed: a raw sample s of a /DeviceGray image maps // to round(255 * s / (2^bpc - 1)). // --------------------------------------------------------------------------- @(private = "file", rodata) GRAY8_DATA := [?]byte{0, 64, 128, 255, 10, 20, 30, 40} @(test) test_image_bpc_8 :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) bmp := im_decode(t, doc, "<< /Width 4 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", GRAY8_DATA[:]) if bmp == nil { return } testing.expect_value(t, bmp.width, 4) testing.expect_value(t, bmp.height, 2) testing.expect_value(t, bmp.n, 4) // Straight alpha out, never premultiplied: blend_span_rgba8 says so. testing.expect(t, !bmp.premultiplied, "image_decode must produce straight alpha") for x in 0 ..< 4 { v := GRAY8_DATA[x] expect_rgba(t, bmp, x, 0, {v, v, v, 255}, "row 0") } for x in 0 ..< 4 { v := GRAY8_DATA[4 + x] expect_rgba(t, bmp, x, 1, {v, v, v, 255}, "row 1") } } @(private = "file", rodata) // 1 0 1 1 0 0 0 1 / 0 1 0 0 1 1 1 0 GRAY1_DATA := [?]byte{0b1011_0001, 0b0100_1110} @(test) test_image_bpc_1 :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) bmp := im_decode(t, doc, "<< /Width 8 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 1 >>", GRAY1_DATA[:]) if bmp == nil { return } want_row0 := [8]u8{255, 0, 255, 255, 0, 0, 0, 255} want_row1 := [8]u8{0, 255, 0, 0, 255, 255, 255, 0} for x in 0 ..< 8 { expect_rgba(t, bmp, x, 0, {want_row0[x], want_row0[x], want_row0[x], 255}, "row 0") expect_rgba(t, bmp, x, 1, {want_row1[x], want_row1[x], want_row1[x], 255}, "row 1") } } @(private = "file", rodata) // 00 01 10 11 -> samples 0 1 2 3, and 11 10 01 00 on the second row. GRAY2_DATA := [?]byte{0b00_01_10_11, 0b11_10_01_00} @(test) test_image_bpc_2 :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) bmp := im_decode(t, doc, "<< /Width 4 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 2 >>", GRAY2_DATA[:]) if bmp == nil { return } // 255 * s / 3: 0, 85, 170, 255. want := [4]u8{0, 85, 170, 255} for x in 0 ..< 4 { expect_rgba(t, bmp, x, 0, {want[x], want[x], want[x], 255}, "row 0") expect_rgba(t, bmp, x, 1, {want[3 - x], want[3 - x], want[3 - x], 255}, "row 1") } } @(private = "file", rodata) // Samples 0, 15 / 5, 10. GRAY4_DATA := [?]byte{0x0F, 0x5A} @(test) test_image_bpc_4 :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) bmp := im_decode(t, doc, "<< /Width 2 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 4 >>", GRAY4_DATA[:]) if bmp == nil { return } // 255 * s / 15: 0, 255, 85, 170. expect_rgba(t, bmp, 0, 0, {0, 0, 0, 255}, "s=0") expect_rgba(t, bmp, 1, 0, {255, 255, 255, 255}, "s=15") expect_rgba(t, bmp, 0, 1, {85, 85, 85, 255}, "s=5") expect_rgba(t, bmp, 1, 1, {170, 170, 170, 255}, "s=10") } @(private = "file", rodata) // 0x0000, 0x8000, 0xFFFF, 0x4000, 0x00FF. GRAY16_DATA := [?]byte{0x00, 0x00, 0x80, 0x00, 0xFF, 0xFF, 0x40, 0x00, 0x00, 0xFF} @(test) test_image_bpc_16 :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) bmp := im_decode(t, doc, "<< /Width 5 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 16 >>", GRAY16_DATA[:]) if bmp == nil { return } // round(255 * s / 65535): 0, 128, 255, 64, 1. The last one is the sample // that keeps this honest -- 0x00FF is 1, not the 0 that dropping the low // byte and calling it a 16-bit decode would give. expect_rgba(t, bmp, 0, 0, {0, 0, 0, 255}, "0x0000") expect_rgba(t, bmp, 1, 0, {128, 128, 128, 255}, "0x8000") expect_rgba(t, bmp, 2, 0, {255, 255, 255, 255}, "0xFFFF") expect_rgba(t, bmp, 3, 0, {64, 64, 64, 255}, "0x4000") expect_rgba(t, bmp, 4, 0, {1, 1, 1, 255}, "0x00FF") } @(private = "file", rodata) // 16-bit RGB, one pixel per row: (0x1234, 0x5678, 0x9ABC) and (0,0,0). RGB16_DATA := [?]byte{0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0, 0, 0, 0, 0, 0} @(test) test_image_16bit_rgb :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) bmp := im_decode(t, doc, "<< /Width 1 /Height 2 /ColorSpace /DeviceRGB /BitsPerComponent 16 >>", RGB16_DATA[:]) if bmp == nil { return } // 0x1234 = 4660 -> 4660*255/65535 = 18.13 -> 18 // 0x5678 = 22136 -> 22136*255/65535 = 86.13 -> 86 // 0x9ABC = 39612 -> 39612*255/65535 = 154.13 -> 154 expect_rgba(t, bmp, 0, 0, {18, 86, 154, 255}, "16-bit RGB") expect_rgba(t, bmp, 0, 1, {0, 0, 0, 255}, "16-bit black") } // --------------------------------------------------------------------------- // Row padding // --------------------------------------------------------------------------- @(private = "file", rodata) // Three 1-bit pixels per row, each row padded out to a whole byte: // 101 00000 / 010 00000. Packing the rows continuously instead would put // row 1 at bit 3 of byte 0, and both rows would read 1,0,1. PAD1_DATA := [?]byte{0b101_00000, 0b010_00000} @(test) test_image_row_padding_1bpc :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) bmp := im_decode(t, doc, "<< /Width 3 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 1 >>", PAD1_DATA[:]) if bmp == nil { return } expect_rgba(t, bmp, 0, 0, {255, 255, 255, 255}, "row 0 px 0") expect_rgba(t, bmp, 1, 0, {0, 0, 0, 255}, "row 0 px 1") expect_rgba(t, bmp, 2, 0, {255, 255, 255, 255}, "row 0 px 2") expect_rgba(t, bmp, 0, 1, {0, 0, 0, 255}, "row 1 px 0") expect_rgba(t, bmp, 1, 1, {255, 255, 255, 255}, "row 1 px 1") expect_rgba(t, bmp, 2, 1, {0, 0, 0, 255}, "row 1 px 2") } @(private = "file", rodata) // Three 4-bit pixels is 12 bits, so each row occupies two bytes with the last // four bits padding: samples 0,15,5 then 10,5,0. PAD4_DATA := [?]byte{0x0F, 0x50, 0xA5, 0x00} @(test) test_image_row_padding_4bpc :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) bmp := im_decode(t, doc, "<< /Width 3 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 4 >>", PAD4_DATA[:]) if bmp == nil { return } expect_rgba(t, bmp, 0, 0, {0, 0, 0, 255}, "row 0 px 0") expect_rgba(t, bmp, 1, 0, {255, 255, 255, 255}, "row 0 px 1") expect_rgba(t, bmp, 2, 0, {85, 85, 85, 255}, "row 0 px 2") expect_rgba(t, bmp, 0, 1, {170, 170, 170, 255}, "row 1 px 0") expect_rgba(t, bmp, 1, 1, {85, 85, 85, 255}, "row 1 px 1") expect_rgba(t, bmp, 2, 1, {0, 0, 0, 255}, "row 1 px 2") } // --------------------------------------------------------------------------- // /Decode // --------------------------------------------------------------------------- @(private = "file", rodata) DEC_DATA := [?]byte{0, 128, 255} @(test) test_image_decode_inverts_gray :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) plain := im_decode(t, doc, "<< /Width 3 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", DEC_DATA[:]) expect_rgba(t, plain, 0, 0, {0, 0, 0, 255}, "no /Decode") expect_rgba(t, plain, 2, 0, {255, 255, 255, 255}, "no /Decode") inverted := im_decode(t, doc, "<< /Width 3 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /Decode [1 0] >>", DEC_DATA[:]) expect_rgba(t, inverted, 0, 0, {255, 255, 255, 255}, "/Decode [1 0]") expect_rgba(t, inverted, 1, 0, {127, 127, 127, 255}, "/Decode [1 0]") expect_rgba(t, inverted, 2, 0, {0, 0, 0, 255}, "/Decode [1 0]") } @(test) test_image_decode_wrong_length_is_dropped :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // A three-component space with a two-entry /Decode. Half an applied decode // is worse than none, so the default is used and a repair recorded. before := len(doc.repairs) bmp := im_decode(t, doc, "<< /Width 1 /Height 1 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Decode [1 0] >>", im_arena(doc, {255, 0, 0})) expect_rgba(t, bmp, 0, 0, {255, 0, 0, 255}, "short /Decode ignored") testing.expect(t, len(doc.repairs) > before, "a dropped /Decode must record a repair") } @(test) test_image_decode_partial_range :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // /Decode [0.25 0.75] compresses the range: sample 0 -> 0.25 -> 64, // sample 255 -> 0.75 -> 191, sample 128 -> 0.25 + 128/255*0.5 = 0.5010 -> 128. bmp := im_decode(t, doc, "<< /Width 3 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /Decode [0.25 0.75] >>", DEC_DATA[:]) expect_rgba(t, bmp, 0, 0, {64, 64, 64, 255}, "0.25") expect_rgba(t, bmp, 1, 0, {128, 128, 128, 255}, "midpoint") expect_rgba(t, bmp, 2, 0, {191, 191, 191, 255}, "0.75") } // --------------------------------------------------------------------------- // Indexed, whose default /Decode is [0 2^bpc - 1] and not [0 1] // --------------------------------------------------------------------------- @(private = "file", rodata) // Four 2-bit samples: 0, 1, 2, 3 -- red, green, blue, white. INDEXED_DATA := [?]byte{0b00_01_10_11} @(test) test_image_indexed_default_decode :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) bmp := im_decode(t, doc, "<< /Width 4 /Height 1 /BitsPerComponent 2 " + "/ColorSpace [/Indexed /DeviceRGB 3 ] >>", INDEXED_DATA[:]) if bmp == nil { return } // The sample IS the index. Assuming the [0 1] default that every other // space has would scale it to 0, 0.33, 0.67, 1, which rounds to indices // 0, 0, 1, 1 -- red, red, green, green. expect_rgba(t, bmp, 0, 0, {255, 0, 0, 255}, "index 0") expect_rgba(t, bmp, 1, 0, {0, 255, 0, 255}, "index 1") expect_rgba(t, bmp, 2, 0, {0, 0, 255, 255}, "index 2") expect_rgba(t, bmp, 3, 0, {255, 255, 255, 255}, "index 3") } @(test) test_image_indexed_out_of_range_index :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // /HiVal 1 with an 8-bit sample: index 200 is past the end of the table // and must clamp to the last entry, never read past it. bmp := im_decode(t, doc, "<< /Width 2 /Height 1 /BitsPerComponent 8 " + "/ColorSpace [/Indexed /DeviceRGB 1 ] >>", im_arena(doc, {0, 200})) expect_rgba(t, bmp, 0, 0, {255, 0, 0, 255}, "index 0") expect_rgba(t, bmp, 1, 0, {0, 255, 0, 255}, "index clamped to hival") } @(test) test_image_indexed_explicit_decode :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // /Decode [3 0] reverses the palette order at 2 bits per component. bmp := im_decode(t, doc, "<< /Width 4 /Height 1 /BitsPerComponent 2 /Decode [3 0] " + "/ColorSpace [/Indexed /DeviceRGB 3 ] >>", INDEXED_DATA[:]) expect_rgba(t, bmp, 0, 0, {255, 255, 255, 255}, "sample 0 -> index 3") expect_rgba(t, bmp, 3, 0, {255, 0, 0, 255}, "sample 3 -> index 0") } // --------------------------------------------------------------------------- // Lab, whose default /Decode is [0 100 amin amax bmin bmax] // --------------------------------------------------------------------------- @(test) test_image_lab_default_decode :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // L* = 100, a* = b* = 0 is white. Scaling the samples onto 0..1 instead -- // the default every other space has -- would give L* = 1, which is black. bmp := im_decode(t, doc, "<< /Width 1 /Height 1 /BitsPerComponent 8 " + "/ColorSpace [/Lab << /WhitePoint [0.9505 1.0 1.089] /Range [-100 100 -100 100] >>] >>", im_arena(doc, {255, 128, 128})) got := im_rgba(bmp, 0, 0) testing.expectf(t, got[0] > 240 && got[1] > 240 && got[2] > 240, "Lab (100, ~0, ~0) should be white, got %v", got) } // --------------------------------------------------------------------------- // Separation: one component, so it goes through the sample lookup table // --------------------------------------------------------------------------- @(test) test_image_separation :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // Object 5 of the fixture is [/Separation /Spot /DeviceCMYK 6 0 R], whose // full tint is 0 1 1 0 -- red. bmp := im_decode(t, doc, "<< /Width 3 /Height 1 /BitsPerComponent 8 /ColorSpace 5 0 R >>", im_arena(doc, {0, 128, 255})) expect_rgba(t, bmp, 0, 0, {255, 255, 255, 255}, "no tint") expect_rgba(t, bmp, 2, 0, {255, 0, 0, 255}, "full tint") // Half tint: c = 0, m = y = 128/255, k = 0. expect_rgba_near(t, bmp, 1, 0, {255, 127, 127, 255}, 1, "half tint") } // --------------------------------------------------------------------------- // CMYK // --------------------------------------------------------------------------- @(test) test_image_cmyk_8bit :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) bmp := im_decode(t, doc, "<< /Width 3 /Height 1 /BitsPerComponent 8 /ColorSpace /DeviceCMYK >>", im_arena(doc, {0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0})) expect_rgba(t, bmp, 0, 0, {255, 255, 255, 255}, "no ink") expect_rgba(t, bmp, 1, 0, {0, 0, 0, 255}, "full black") expect_rgba(t, bmp, 2, 0, {0, 255, 255, 255}, "full cyan") } // --------------------------------------------------------------------------- // /ImageMask: a stencil, both polarities // --------------------------------------------------------------------------- @(test) test_image_mask_both_ways :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // Bits 1 0 1 1 0 0 0 1. Sample 0 paints under the default /Decode [0 1]. plain := im_decode(t, doc, "<< /Width 8 /Height 1 /ImageMask true >>", GRAY1_DATA[:]) if plain == nil { return } testing.expect_value(t, plain.n, 1) want := [8]u8{0, 255, 0, 0, 255, 255, 255, 0} for x in 0 ..< 8 { expect_cov(t, plain, x, 0, want[x], "default /Decode") } flipped := im_decode(t, doc, "<< /Width 8 /Height 1 /ImageMask true /Decode [1 0] >>", GRAY1_DATA[:]) for x in 0 ..< 8 { expect_cov(t, flipped, x, 0, 255 - want[x], "/Decode [1 0]") } } @(test) test_image_mask_forces_one_bit :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // 8.9.6.2: an /ImageMask has no colour space and /BitsPerComponent shall // be 1. Producers write 8 anyway; the format wins. img := im_load(t, doc, "<< /Width 8 /Height 1 /ImageMask true /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", GRAY1_DATA[:]) if img == nil { return } testing.expect_value(t, img.bpc, 1) testing.expect(t, img.cs == nil, "a stencil has no colour space") bmp := im_pixels(t, doc, img) expect_cov(t, bmp, 1, 0, 255, "still read as 1 bit") } // --------------------------------------------------------------------------- // /SMask // --------------------------------------------------------------------------- @(private = "file", rodata) // 2x2 DeviceRGB: red, green / blue, white. SMASK_BASE := [?]byte{255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255} @(test) test_image_smask_matching_dimensions :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) alpha := cf_stream(doc, "<< /Width 2 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", im_arena(doc, {0, 64, 192, 255})) d, _ := to_dict(cf_parse(doc, "<< /Width 2 /Height 2 /ColorSpace /DeviceRGB /BitsPerComponent 8 >>")) dict_set(d, "SMask", alpha) img, err := image_load(doc, new_stream(d, SMASK_BASE[:], doc.allocator), nil) testing.expect_value(t, err, Error.None) testing.expect(t, img != nil && img.smask != nil, "the /SMask must have loaded") bmp := im_pixels(t, doc, img) expect_rgba(t, bmp, 0, 0, {255, 0, 0, 0}, "alpha 0") expect_rgba(t, bmp, 1, 0, {0, 255, 0, 64}, "alpha 64") expect_rgba(t, bmp, 0, 1, {0, 0, 255, 192}, "alpha 192") expect_rgba(t, bmp, 1, 1, {255, 255, 255, 255}, "alpha 255") } @(test) test_image_smask_different_dimensions :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // A 4x4 image with a 2x2 mask: nearest-neighbour, so columns 0-1 take mask // column 0 and columns 2-3 take mask column 1. alpha := cf_stream(doc, "<< /Width 2 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", im_arena(doc, {0, 255, 128, 64})) d, _ := to_dict(cf_parse(doc, "<< /Width 4 /Height 4 /ColorSpace /DeviceGray /BitsPerComponent 8 >>")) dict_set(d, "SMask", alpha) img, _ := image_load(doc, new_stream(d, im_arena_fill(doc, 16, 200), doc.allocator), nil) bmp := im_pixels(t, doc, img) if bmp == nil { return } testing.expect_value(t, bmp.width, 4) for y in 0 ..< 4 { for x in 0 ..< 4 { want: u8 = y < 2 ? (x < 2 ? 0 : 255) : (x < 2 ? 128 : 64) expect_rgba(t, bmp, x, y, {200, 200, 200, want}, "resampled /SMask") } } } @(test) test_image_smask_matte :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // /Matte [1 1 1]: the colour was stored pre-blended against white, as // c' = m + a*(c - m). At alpha 0.5 a stored 191 came from // 255 + 0.5*(c - 255), i.e. c = 127. Undoing the blend must recover it. alpha := cf_stream(doc, "<< /Width 2 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /Matte [1 1 1] >>", im_arena(doc, {128, 255})) d, _ := to_dict(cf_parse(doc, "<< /Width 2 /Height 1 /ColorSpace /DeviceRGB /BitsPerComponent 8 >>")) dict_set(d, "SMask", alpha) img, _ := image_load(doc, new_stream(d, im_arena(doc, {191, 191, 191, 10, 20, 30}), doc.allocator), nil) testing.expect(t, img != nil && img.smask != nil && img.smask.has_matte, "/Matte must be read from the soft mask's dictionary") bmp := im_pixels(t, doc, img) // 255 + (191 - 255) * 255/128 = 255 - 127.5 = 127.5 -> 127 or 128. expect_rgba_near(t, bmp, 0, 0, {128, 128, 128, 128}, 2, "unblended against white") // A fully opaque pixel is not pre-blended at all and must not be touched. expect_rgba(t, bmp, 1, 0, {10, 20, 30, 255}, "opaque pixel is untouched") } // --------------------------------------------------------------------------- // /Mask as a stencil // --------------------------------------------------------------------------- @(test) test_image_stencil_mask :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // 1 masks OUT by default. Bits 1 0 1 1 0 0 0 1 over an 8x1 grey image. stencil := cf_stream(doc, "<< /Width 8 /Height 1 /ImageMask true >>", GRAY1_DATA[:]) d, _ := to_dict(cf_parse(doc, "<< /Width 8 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 >>")) dict_set(d, "Mask", stencil) img, _ := image_load(doc, new_stream(d, im_arena_fill(doc, 8, 100), doc.allocator), nil) testing.expect(t, img != nil && img.mask != nil, "the stencil /Mask must have loaded") bmp := im_pixels(t, doc, img) want := [8]u8{0, 255, 0, 0, 255, 255, 255, 0} for x in 0 ..< 8 { expect_rgba(t, bmp, x, 0, {100, 100, 100, want[x]}, "stencil /Mask") } } // --------------------------------------------------------------------------- // Colour-key masking, which compares SOURCE samples // --------------------------------------------------------------------------- @(test) test_image_color_key_uses_source_samples :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // /Decode [1 0] and a key range of [0 0]. Sample 0 is in the range and is // masked out; after the decode that same pixel is 1.0, and a colour-key // test done on the decoded value would instead mask sample 255. The two // answers are exact opposites, so this cannot pass by accident. bmp := im_decode(t, doc, "<< /Width 3 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 " + "/Decode [1 0] /Mask [0 0] >>", DEC_DATA[:]) expect_rgba(t, bmp, 0, 0, {255, 255, 255, 0}, "sample 0 is masked out") expect_rgba(t, bmp, 1, 0, {127, 127, 127, 255}, "sample 128 is kept") expect_rgba(t, bmp, 2, 0, {0, 0, 0, 255}, "sample 255 is kept") } @(test) test_image_color_key_all_components_must_match :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // Key range green-ish: every component must fall inside its own range for // the pixel to be masked. bmp := im_decode(t, doc, "<< /Width 3 /Height 1 /ColorSpace /DeviceRGB /BitsPerComponent 8 " + "/Mask [0 10 200 255 0 10] >>", im_arena(doc, {0, 255, 0, 5, 210, 8, 0, 255, 200})) expect_rgba(t, bmp, 0, 0, {0, 255, 0, 0}, "exactly in range") expect_rgba(t, bmp, 1, 0, {5, 210, 8, 0}, "inside every range") expect_rgba(t, bmp, 2, 0, {0, 255, 200, 255}, "blue is outside its range") } @(test) test_image_color_key_indexed :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // On an Indexed image the key range is a range of INDICES. bmp := im_decode(t, doc, "<< /Width 4 /Height 1 /BitsPerComponent 2 /Mask [2 2] " + "/ColorSpace [/Indexed /DeviceRGB 3 ] >>", INDEXED_DATA[:]) expect_rgba(t, bmp, 1, 0, {0, 255, 0, 255}, "index 1 kept") expect_rgba(t, bmp, 2, 0, {0, 0, 255, 0}, "index 2 masked out") expect_rgba(t, bmp, 3, 0, {255, 255, 255, 255}, "index 3 kept") } // --------------------------------------------------------------------------- // DCTDecode // --------------------------------------------------------------------------- @(test) test_image_cmyk_jpeg_needs_its_decode_array :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // JPEG_T_CMYK is a 16x16 Adobe CMYK JPEG whose samples are stored // complemented: (255-16x, 255-16y, 55, 225). filter_dct.odin deliberately // does not un-complement them, because the file expresses the inversion in // /Decode [1 0 1 0 1 0 1 0]. Applying it recovers the source image // (16x, 16y, 200, 30), and forgetting it turns the photograph into its own // negative -- which the second half of this test pins down. bmp := im_decode(t, doc, "<< /Width 16 /Height 16 /BitsPerComponent 8 /ColorSpace /DeviceCMYK " + "/Filter /DCTDecode /Decode [1 0 1 0 1 0 1 0] >>", JPEG_T_CMYK[:]) if bmp == nil { return } testing.expect_value(t, bmp.width, 16) // c = 16x/255, m = 16y/255, y = 200/255, k = 30/255, then // r = 1 - min(1, c+k), g = 1 - min(1, m+k), b = 1 - min(1, y+k). expect_rgba_near(t, bmp, 0, 0, {225, 225, 25, 255}, 8, "top-left") expect_rgba_near(t, bmp, 4, 2, {161, 193, 25, 255}, 8, "interior") expect_rgba_near(t, bmp, 15, 15, {0, 0, 25, 255}, 8, "bottom-right") // The same JPEG with no /Decode: every component is complemented, so // c = m = 1 at the top-left and the pixel goes black. naive := im_decode(t, doc, "<< /Width 16 /Height 16 /BitsPerComponent 8 /ColorSpace /DeviceCMYK " + "/Filter /DCTDecode >>", JPEG_T_CMYK[:]) got := im_rgba(naive, 0, 0) testing.expectf(t, got[0] < 40 && got[1] < 40, "without /Decode the corner must stay dark, got %v", got) } @(test) test_image_grey_jpeg :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // JPEG_T_GREY8 is an 8x8 grey ramp; JPEG_T_GREY8_PIXELS is what the // decoder's own test expects out of it. bmp := im_decode(t, doc, "<< /Width 8 /Height 8 /BitsPerComponent 8 /ColorSpace /DeviceGray /Filter /DCTDecode >>", JPEG_T_GREY8[:]) if bmp == nil { return } for y in 0 ..< 8 { for x in 0 ..< 8 { v := JPEG_T_GREY8_PIXELS[y * 8 + x] expect_rgba_near(t, bmp, x, y, {v, v, v, 255}, 4, "grey JPEG") } } } @(test) test_image_jpeg_dimensions_win_over_the_dictionary :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // The frame header says 8x8. A /Width that disagrees would shear every row // if it were believed, so the frame header is taken and a repair recorded. before := len(doc.repairs) img := im_load(t, doc, "<< /Width 40 /Height 3 /BitsPerComponent 8 /ColorSpace /DeviceGray /Filter /DCTDecode >>", JPEG_T_GREY8[:]) bmp := im_pixels(t, doc, img) if bmp == nil { return } testing.expect_value(t, bmp.width, 8) testing.expect_value(t, bmp.height, 8) w, h := image_size(img) testing.expect_value(t, w, 8) testing.expect_value(t, h, 8) testing.expect(t, len(doc.repairs) > before, "a size disagreement must record a repair") } @(test) test_image_jpeg_component_count_wins_over_the_colour_space :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // A one-component JPEG declared /DeviceRGB. Believing the dictionary would // read three consecutive grey pixels as one RGB pixel. bmp := im_decode(t, doc, "<< /Width 8 /Height 8 /BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter /DCTDecode >>", JPEG_T_GREY8[:]) if bmp == nil { return } v := JPEG_T_GREY8_PIXELS[0] expect_rgba_near(t, bmp, 0, 0, {v, v, v, 255}, 4, "read as grey") } // --------------------------------------------------------------------------- // CCITTFaxDecode // // G4_IMAGE is the 8-column, 12-row Group 4 vector filter_ccitt_test.odin // checks the codec against; it was produced by libtiff. Decoded it is // G4_ROWS, one byte per row, and with /BlackIs1 false a 1 bit is white. // --------------------------------------------------------------------------- @(private = "file", rodata) G4_IMAGE := [?]byte{ 0x93, 0x51, 0x59, 0x43, 0x94, 0x38, 0x4A, 0xAE, 0xC4, 0x38, 0x2A, 0x3D, 0x8C, 0xDB, 0x07, 0x09, 0x46, 0x00, 0x20, 0x02, } @(private = "file", rodata) G4_ROWS := [?]byte{0xFF, 0x00, 0xAA, 0x33, 0xFC, 0xE0, 0xCF, 0xFF, 0xF0, 0xFE, 0xF9, 0xFF} @(private = "file") g4_bit :: proc(row: int, x: int) -> u8 { return (G4_ROWS[row] >> uint(7 - x)) & 1 } @(test) test_image_ccitt :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) bmp := im_decode(t, doc, "<< /Width 8 /Height 12 /BitsPerComponent 1 /ColorSpace /DeviceGray " + "/Filter /CCITTFaxDecode /DecodeParms << /K -1 /Columns 8 /Rows 12 >> >>", G4_IMAGE[:]) if bmp == nil { return } testing.expect_value(t, bmp.width, 8) testing.expect_value(t, bmp.height, 12) for y in 0 ..< 12 { for x in 0 ..< 8 { // /BlackIs1 defaults to false, so a 1 bit is white, which // DeviceGray maps to 1.0. The filter's convention and the image's // are the same one, so nothing is inverted between them. v: u8 = g4_bit(y, x) == 1 ? 255 : 0 expect_rgba(t, bmp, x, y, {v, v, v, 255}, "CCITT G4") } } } @(test) test_image_ccitt_black_is_1 :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // /BlackIs1 exchanges the bits in the SAMPLE data. Nothing in the image // layer compensates, so the picture comes out inverted -- which is exactly // what the format says happens, and why a producer that sets it also // writes /Decode [1 0]. bmp := im_decode(t, doc, "<< /Width 8 /Height 12 /BitsPerComponent 1 /ColorSpace /DeviceGray " + "/Filter /CCITTFaxDecode /DecodeParms << /K -1 /Columns 8 /Rows 12 /BlackIs1 true >> >>", G4_IMAGE[:]) for y in 0 ..< 12 { for x in 0 ..< 8 { v: u8 = g4_bit(y, x) == 1 ? 0 : 255 expect_rgba(t, bmp, x, y, {v, v, v, 255}, "/BlackIs1 true") } } } @(test) test_image_ccitt_missing_columns_uses_width :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // /Columns is absent. Its format default is 1728, but inside a PDF the two // are required to agree, so /Width is what the producer meant; taking 1728 // would lay the rows out 216 bytes apart and show nothing at all. bmp := im_decode(t, doc, "<< /Width 8 /Height 12 /BitsPerComponent 1 /ColorSpace /DeviceGray " + "/Filter /CCITTFaxDecode /DecodeParms << /K -1 >> >>", G4_IMAGE[:]) for x in 0 ..< 8 { v: u8 = g4_bit(2, x) == 1 ? 255 : 0 expect_rgba(t, bmp, x, 2, {v, v, v, 255}, "row 2 with no /Columns") } } @(test) test_image_ccitt_unusable_columns_uses_width :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // /Columns is present but is not a width the codec will honour as // written: it reads a non-positive one as the format's 1728 default and // clamps anything past MAX_CCITT_COLUMNS. Left alone, the decoder lays its // rows out 216 (or 8192) bytes apart while the image reads them 1 byte // apart, and every row after the first starts at the wrong byte -- the // picture is gone with nothing to say so. /Width is what both sides use. HEAD :: "<< /Width 8 /Height 12 /BitsPerComponent 1 /ColorSpace /DeviceGray " + "/Filter /CCITTFaxDecode /DecodeParms << /K -1 /Rows 12 /Columns " srcs := [?]string{ HEAD + "0 >> >>", HEAD + "-8 >> >>", HEAD + "70000 >> >>", } for src in srcs { bmp := im_decode(t, doc, src, G4_IMAGE[:]) if bmp == nil { continue } for y in 0 ..< 12 { for x in 0 ..< 8 { v: u8 = g4_bit(y, x) == 1 ? 255 : 0 expect_rgba(t, bmp, x, y, {v, v, v, 255}, src) } } } } @(test) test_image_ccitt_as_stencil :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // The same fax data as an /ImageMask: sample 0 (black) paints. bmp := im_decode(t, doc, "<< /Width 8 /Height 12 /ImageMask true " + "/Filter /CCITTFaxDecode /DecodeParms << /K -1 /Columns 8 /Rows 12 >> >>", G4_IMAGE[:]) if bmp == nil { return } testing.expect_value(t, bmp.n, 1) for x in 0 ..< 8 { expect_cov(t, bmp, x, 0, 0, "row 0 is white, so it paints nothing") expect_cov(t, bmp, x, 1, 255, "row 1 is black, so it paints") } } // --------------------------------------------------------------------------- // Formats that are not decoded // --------------------------------------------------------------------------- @(test) test_image_jpx_is_a_transparent_placeholder :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) sources := [?]string{ "<< /Width 4 /Height 3 /BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter /JPXDecode >>", "<< /Width 4 /Height 3 /BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter /JBIG2Decode >>", } for src in sources { before := len(doc.repairs) img := im_load(t, doc, src, im_arena_fill(doc, 32, 0x42)) bmp, err := image_decode(doc, img) testing.expectf(t, err == .None, "%s must not fail the page: %v", src, err) if bmp == nil { continue } testing.expect_value(t, bmp.width, 4) testing.expect_value(t, bmp.height, 3) for y in 0 ..< 3 { for x in 0 ..< 4 { expect_rgba(t, bmp, x, y, {0, 0, 0, 0}, "placeholder is transparent") } } testing.expect(t, len(doc.repairs) > before, "an undecoded format must record a repair") testing.expect(t, img.failed, "the image must be marked failed") } } @(test) test_image_broken_stream_degrades :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // Flate data that is not flate data at all. The chain fails outright, so // this takes the same route as a format that is not implemented: a // transparent image of the declared size, a repair, and no error. before := len(doc.repairs) img := im_load(t, doc, "<< /Width 2 /Height 2 /BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter /FlateDecode >>", im_arena(doc, {0xDE, 0xAD, 0xBE, 0xEF})) bmp, err := image_decode(doc, img) testing.expect_value(t, err, Error.None) if bmp == nil { return } testing.expect_value(t, bmp.width, 2) testing.expect_value(t, bmp.height, 2) expect_rgba(t, bmp, 0, 0, {0, 0, 0, 0}, "unreadable stream") expect_rgba(t, bmp, 1, 1, {0, 0, 0, 0}, "unreadable stream") testing.expect(t, img.failed, "the image must be marked failed") testing.expect(t, len(doc.repairs) > before, "and record a repair") } // --------------------------------------------------------------------------- // Malformed dictionaries // --------------------------------------------------------------------------- @(test) test_image_short_data_pads :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // 16 samples are needed and 5 are supplied. bmp := im_decode(t, doc, "<< /Width 4 /Height 4 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", im_arena(doc, {10, 20, 30, 40, 50})) if bmp == nil { return } testing.expect_value(t, bmp.width, 4) testing.expect_value(t, bmp.height, 4) expect_rgba(t, bmp, 0, 0, {10, 10, 10, 255}, "supplied") expect_rgba(t, bmp, 3, 0, {40, 40, 40, 255}, "supplied") expect_rgba(t, bmp, 0, 1, {50, 50, 50, 255}, "last supplied byte") expect_rgba(t, bmp, 1, 1, {0, 0, 0, 255}, "past the data") expect_rgba(t, bmp, 3, 3, {0, 0, 0, 255}, "past the data") } @(test) test_image_absurd_dimensions_are_refused :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) bad := [?]string{ "<< /Width 0 /Height 4 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", "<< /Width 4 /Height 0 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", "<< /Width -5 /Height 4 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", "<< /Width 100000 /Height 4 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", "<< /Width 1000000000 /Height 1000000000 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", // 2^26 is the pixel cap; 20000 x 20000 is 4x10^8 and well past it. "<< /Width 20000 /Height 20000 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", "<< /Width 1e300 /Height 1e300 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", "<< /ColorSpace /DeviceGray /BitsPerComponent 8 >>", } for src in bad { img, err := image_load(doc, cf_stream(doc, src, GRAY8_DATA[:]), nil) testing.expectf(t, err == .Bad_Image, "%s should be refused, got %v", src, err) testing.expect(t, img == nil, "a refused image must not be returned") } } @(test) test_image_dimensions_are_checked_against_the_stream :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // 65536 x 1024 is exactly MAX_IMAGE_PIXELS, so the dimension caps pass it // and nothing else ever related it to the bytes that arrived: about 150 // bytes of dictionary bought a 256 MiB allocation and a colour conversion // over 67 million pixels, and a page may name many such XObjects. bad := [?]string{ "<< /Width 65536 /Height 1024 /BitsPerComponent 8 /ColorSpace /DeviceGray >>", "<< /Width 65536 /Height 1024 /BitsPerComponent 8 /ColorSpace /DeviceCMYK >>", "<< /Width 65536 /Height 1024 /BitsPerComponent 16 /ColorSpace /DeviceRGB >>", "<< /Width 65536 /Height 1024 /ImageMask true >>", } for src in bad { img, err := image_load(doc, cf_stream(doc, src, nil), nil) testing.expectf(t, err == .Bad_Image, "%s should be refused, got %v", src, err) testing.expect(t, img == nil, "a refused image must not be returned") } testing.expect(t, len(doc.repairs) > 0, "refusing one should record a repair") } @(test) test_image_badly_truncated_but_plausible_still_loads :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // The stream check is about the RATIO, not about the shortfall: 600 bytes // for a 2 MiB image is a 3500-fold truncation and still loads, because a // blank page under CCITT G4 or a flat one under Flate really does compress // that far and neither is a broken file. img := im_load(t, doc, "<< /Width 2048 /Height 1024 /BitsPerComponent 8 /ColorSpace /DeviceGray >>", im_arena_fill(doc, 600, 0x40)) testing.expect(t, img != nil, "a truncated image must still load") if img == nil { return } testing.expect_value(t, img.width, 2048) testing.expect_value(t, img.height, 1024) // And a small image is never looked at at all, however empty its stream. tiny, err := image_load(doc, cf_stream(doc, "<< /Width 64 /Height 64 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", nil), nil) testing.expect_value(t, err, Error.None) testing.expect(t, tiny != nil, "a small image with an empty stream still loads") } @(test) test_image_rows_past_the_data_are_the_zero_sample :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // A row the samples do not reach is one colour from end to end, so it is // computed once and filled. That shortcut has to produce exactly what the // per-pixel path would: in CMYK the all-zero sample is WHITE, not the // black that assuming a zeroed RGB pixel would give. bmp := im_decode(t, doc, "<< /Width 2 /Height 4 /BitsPerComponent 8 /ColorSpace /DeviceCMYK >>", im_arena(doc, {0, 0, 0, 255, 0, 0, 0, 255})) if bmp == nil { return } expect_rgba(t, bmp, 0, 0, {0, 0, 0, 255}, "the one supplied row: K=255 is black") expect_rgba(t, bmp, 0, 1, {255, 255, 255, 255}, "past the data: zero CMYK is white") expect_rgba(t, bmp, 1, 3, {255, 255, 255, 255}, "past the data, last row") // Colour-key masking is decided on the raw samples, so an absent row can // be transparent as well -- the filled row has to carry that too. keyed := im_decode(t, doc, "<< /Width 2 /Height 4 /BitsPerComponent 8 /ColorSpace /DeviceRGB " + "/Mask [0 0 0 0 0 0] >>", im_arena(doc, {9, 9, 9, 9, 9, 9})) if keyed == nil { return } expect_rgba(t, keyed, 0, 0, {9, 9, 9, 255}, "the supplied row is not in the key range") expect_rgba(t, keyed, 0, 2, {0, 0, 0, 0}, "an absent row is all zeroes, which is masked") } @(test) test_image_no_dict_is_refused :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) _, err := image_load(doc, new_stream(nil, GRAY8_DATA[:], doc.allocator), nil) testing.expect_value(t, err, Error.Bad_Image) _, nerr := image_load(doc, nil, nil) testing.expect_value(t, nerr, Error.Invalid_Argument) _, derr := image_decode(nil, nil) testing.expect_value(t, derr, Error.Invalid_Argument) w, h := image_size(nil) testing.expect_value(t, w, 0) testing.expect_value(t, h, 0) } @(test) test_image_missing_colorspace_is_inferred :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // No /ColorSpace at all, and 12 bytes for 2x2 pixels: three components. bmp := im_decode(t, doc, "<< /Width 2 /Height 2 /BitsPerComponent 8 >>", im_arena(doc, {255, 0, 0, 0, 255, 0, 0, 0, 255, 9, 9, 9})) expect_rgba(t, bmp, 0, 0, {255, 0, 0, 255}, "inferred DeviceRGB") expect_rgba(t, bmp, 1, 1, {9, 9, 9, 255}, "inferred DeviceRGB") // One byte per pixel: grey. grey := im_decode(t, doc, "<< /Width 2 /Height 2 /BitsPerComponent 8 >>", im_arena(doc, {1, 2, 3, 4})) expect_rgba(t, grey, 0, 0, {1, 1, 1, 255}, "inferred DeviceGray") } @(test) test_image_bad_bpc_falls_back_to_8 :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) img := im_load(t, doc, "<< /Width 4 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 7 >>", GRAY8_DATA[:]) if img == nil { return } testing.expect_value(t, img.bpc, 8) expect_rgba(t, im_pixels(t, doc, img), 1, 0, {64, 64, 64, 255}, "read at 8 bits") } @(test) test_image_self_referential_smask_terminates :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // A stream whose /SMask is itself. The nesting cap is what stops this. d, _ := to_dict(cf_parse(doc, "<< /Width 2 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 >>")) stm := new_stream(d, im_arena(doc, {10, 200}), doc.allocator) dict_set(d, "SMask", stm) img, err := image_load(doc, stm, nil) testing.expect_value(t, err, Error.None) bmp := im_pixels(t, doc, img) if bmp == nil { return } // The mask is the image itself, so its own samples become the alpha. expect_rgba(t, bmp, 0, 0, {10, 10, 10, 10}, "self mask") expect_rgba(t, bmp, 1, 0, {200, 200, 200, 200}, "self mask") } @(test) test_image_broken_smask_leaves_the_image_opaque :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // The /SMask is a JPEG 2000 codestream, which this library does not // decode. Losing the alpha is bad; losing the picture would be worse. alpha := cf_stream(doc, "<< /Width 2 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /JPXDecode >>", im_arena_fill(doc, 4, 0)) d, _ := to_dict(cf_parse(doc, "<< /Width 2 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 >>")) dict_set(d, "SMask", alpha) img, _ := image_load(doc, new_stream(d, im_arena(doc, {40, 90}), doc.allocator), nil) bmp := im_pixels(t, doc, img) expect_rgba(t, bmp, 0, 0, {40, 40, 40, 255}, "opaque despite the broken mask") expect_rgba(t, bmp, 1, 0, {90, 90, 90, 255}, "opaque despite the broken mask") } // --------------------------------------------------------------------------- // Inline images // --------------------------------------------------------------------------- @(test) test_image_inline_abbreviated_keys :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // Every key and the colour space in their abbreviated spellings. d, ok := to_dict(cf_parse(doc, "<< /W 4 /H 2 /BPC 8 /CS /G /D [1 0] /IM false >>")) testing.expect(t, ok, "inline dictionary did not parse") img, err := image_load_inline(doc, d, GRAY8_DATA[:], nil) testing.expect_value(t, err, Error.None) if img == nil { return } testing.expect_value(t, img.width, 4) testing.expect_value(t, img.height, 2) testing.expect_value(t, img.bpc, 8) testing.expect(t, img.is_inline, "an inline image must know that it is one") testing.expect(t, img.cs != nil && img.cs.kind == .Device_Gray, "/G is DeviceGray") bmp := im_pixels(t, doc, img) for x in 0 ..< 4 { v := 255 - GRAY8_DATA[x] expect_rgba(t, bmp, x, 0, {v, v, v, 255}, "inline /D [1 0]") } } @(test) test_image_inline_data_is_copied :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // The content interpreter's buffer is scratch; an Image outlives it. scratch := make([]byte, 4, context.allocator) scratch[0], scratch[1], scratch[2], scratch[3] = 1, 2, 3, 4 d, _ := to_dict(cf_parse(doc, "<< /W 2 /H 2 /BPC 8 /CS /G >>")) img, _ := image_load_inline(doc, d, scratch, nil) for &b in scratch { b = 0xFF } delete(scratch, context.allocator) bmp := im_pixels(t, doc, img) expect_rgba(t, bmp, 0, 0, {1, 1, 1, 255}, "copied, not borrowed") expect_rgba(t, bmp, 1, 1, {4, 4, 4, 255}, "copied, not borrowed") } @(test) test_image_inline_rgb_and_indexed :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) d, _ := to_dict(cf_parse(doc, "<< /W 2 /H 1 /BPC 8 /CS /RGB >>")) img, _ := image_load_inline(doc, d, im_arena(doc, {255, 0, 0, 0, 0, 255}), nil) expect_rgba(t, im_pixels(t, doc, img), 0, 0, {255, 0, 0, 255}, "/RGB") // /I is the abbreviation for Indexed inside a colour space array. di, _ := to_dict(cf_parse(doc, "<< /W 2 /H 1 /BPC 8 /CS [/I /RGB 1 ] >>")) imi, _ := image_load_inline(doc, di, im_arena(doc, {1, 0}), nil) bi := im_pixels(t, doc, imi) expect_rgba(t, bi, 0, 0, {0, 255, 0, 255}, "/I palette entry 1") expect_rgba(t, bi, 1, 0, {255, 0, 0, 255}, "/I palette entry 0") // /CMYK, the fourth abbreviation. dc, _ := to_dict(cf_parse(doc, "<< /W 1 /H 1 /BPC 8 /CS /CMYK >>")) imc, _ := image_load_inline(doc, dc, im_arena(doc, {0, 0, 0, 0}), nil) expect_rgba(t, im_pixels(t, doc, imc), 0, 0, {255, 255, 255, 255}, "/CMYK") } @(test) test_image_inline_named_colorspace_from_resources :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // A name that is not one of the abbreviations resolves through the page's // /Resources /ColorSpace. res, _ := to_dict(cf_parse(doc, "<< /ColorSpace << /Cs1 /DeviceRGB >> >>")) d, _ := to_dict(cf_parse(doc, "<< /W 2 /H 1 /BPC 8 /CS /Cs1 >>")) img, err := image_load_inline(doc, d, im_arena(doc, {255, 0, 0, 0, 255, 0}), res) testing.expect_value(t, err, Error.None) testing.expect(t, img != nil && img.cs != nil && img.cs.kind == .Device_RGB, "/Cs1 must resolve through /Resources") expect_rgba(t, im_pixels(t, doc, img), 1, 0, {0, 255, 0, 255}, "resource colour space") } @(test) test_image_inline_stencil_with_filter :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // /IM true and /F, the abbreviated /Filter, on the same inline image. // "8 %-fdt" is <2A 2A 2A> in ASCII85 terms; use ASCIIHexDecode instead, // which is unambiguous: B1 is 1011 0001. d, _ := to_dict(cf_parse(doc, "<< /W 8 /H 1 /IM true /F /AHx >>")) img, err := image_load_inline(doc, d, transmute([]byte)string("B1>"), nil) testing.expect_value(t, err, Error.None) bmp := im_pixels(t, doc, img) if bmp == nil { return } testing.expect_value(t, bmp.n, 1) want := [8]u8{0, 255, 0, 0, 255, 255, 255, 0} for x in 0 ..< 8 { expect_cov(t, bmp, x, 0, want[x], "inline stencil through /F /AHx") } } // --------------------------------------------------------------------------- // Caching // --------------------------------------------------------------------------- @(test) test_image_load_is_cached_by_stream :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) stm := cf_stream(doc, "<< /Width 4 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", GRAY8_DATA[:]) a, _ := image_load(doc, stm, nil) b, _ := image_load(doc, stm, nil) testing.expect(t, a != nil && a == b, "the same stream must give the same Image") first := im_pixels(t, doc, a) bytes_after_first := doc.img_cache.bytes second := im_pixels(t, doc, b) testing.expect(t, first == second, "the decoded bitmap must be reused") testing.expect_value(t, doc.img_cache.bytes, bytes_after_first) testing.expect_value(t, len(doc.img_cache.live), 1) } @(test) test_image_cache_evicts_least_recently_used :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // Three 100x100 grey images: 40000 bytes of RGBA each. A budget of 90000 // holds two. doc.img_cache.budget = 90000 imgs: [3]^Image for i in 0 ..< 3 { imgs[i] = im_load(t, doc, "<< /Width 100 /Height 100 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", im_arena_fill(doc, 10000, u8(i * 30))) } im_pixels(t, doc, imgs[0]) im_pixels(t, doc, imgs[1]) testing.expect_value(t, doc.img_cache.bytes, 80000) testing.expect(t, imgs[0].decoded && imgs[1].decoded, "both fit in the budget") im_pixels(t, doc, imgs[2]) testing.expect(t, doc.img_cache.bytes <= 90000, "the budget must be respected") testing.expect(t, !imgs[0].decoded, "the least recently used image must be evicted") testing.expect(t, imgs[1].decoded && imgs[2].decoded, "the two newest must survive") testing.expect_value(t, len(doc.img_cache.live), 2) // An evicted image decodes again on demand, with the same pixels. again := im_pixels(t, doc, imgs[0]) testing.expect(t, imgs[0].decoded, "re-decoded after eviction") expect_rgba(t, again, 0, 0, {0, 0, 0, 255}, "re-decoded content") } @(test) test_image_cache_keeps_the_image_being_decoded :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // A budget smaller than a single image. It is still decoded and returned: // throwing it away before the caller has drawn it would be worse than // being briefly over budget. doc.img_cache.budget = 16 img := im_load(t, doc, "<< /Width 100 /Height 100 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", im_arena_fill(doc, 10000, 77)) bmp := im_pixels(t, doc, img) expect_rgba(t, bmp, 50, 50, {77, 77, 77, 255}, "over-budget image still decodes") testing.expect(t, img.decoded, "and is still the one held") } @(test) test_image_touch_orders_the_cache :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) doc.img_cache.budget = 90000 imgs: [3]^Image for i in 0 ..< 3 { imgs[i] = im_load(t, doc, "<< /Width 100 /Height 100 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", im_arena_fill(doc, 10000, u8(i * 30))) } im_pixels(t, doc, imgs[0]) im_pixels(t, doc, imgs[1]) // Re-reading image 0 makes image 1 the least recently used one. im_pixels(t, doc, imgs[0]) im_pixels(t, doc, imgs[2]) testing.expect(t, imgs[0].decoded, "the re-read image must survive") testing.expect(t, !imgs[1].decoded, "the untouched image must be the victim") } // --------------------------------------------------------------------------- // Fast paths must agree with the general one // --------------------------------------------------------------------------- @(test) test_image_iccbased_rgb_matches_devicergb :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // ICCBased renders through its /Alternate, so this is DeviceRGB by another // name and must give byte-identical pixels. `img_cs_is_identity_rgb` // probes for exactly this. icc := cf_stream(doc, "<< /N 3 /Alternate /DeviceRGB >>", im_arena_fill(doc, 8, 0)) d, _ := to_dict(cf_parse(doc, "<< /Width 3 /Height 1 /BitsPerComponent 8 >>")) a := new_array(doc.allocator, 2) array_push(a, Name("ICCBased")) array_push(a, icc) dict_set(d, "ColorSpace", a) payload := im_arena(doc, {255, 0, 0, 0, 128, 200, 3, 3, 3}) img, _ := image_load(doc, new_stream(d, payload, doc.allocator), nil) got := im_pixels(t, doc, img) plain := im_decode(t, doc, "<< /Width 3 /Height 1 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", payload) for x in 0 ..< 3 { testing.expectf(t, im_rgba(got, x, 0) == im_rgba(plain, x, 0), "ICCBased pixel %d: %v vs DeviceRGB %v", x, im_rgba(got, x, 0), im_rgba(plain, x, 0)) } } @(test) test_image_calrgb_with_gamma_is_not_the_identity :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // A CalRGB with /Gamma 2.2 does change its components, so the direct-copy // fast path must not be taken for it. If the probe were replaced by a // "kind is Cal_RGB, close enough" test this pixel would come back as 128. bmp := im_decode(t, doc, "<< /Width 1 /Height 1 /BitsPerComponent 8 /ColorSpace " + "[/CalRGB << /WhitePoint [0.9505 1.0 1.089] /Gamma [2.2 2.2 2.2] >>] >>", im_arena(doc, {128, 128, 128})) got := im_rgba(bmp, 0, 0) testing.expectf(t, got[0] != 128, "CalRGB gamma must be applied, got %v", got) } @(test) test_image_general_path_matches_the_lut :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // 16 bits per component takes the general per-pixel path; 8 bits takes the // 256-entry table. Feeding both the same values must give the same answer. lut := im_decode(t, doc, "<< /Width 3 /Height 1 /BitsPerComponent 8 /ColorSpace 5 0 R >>", im_arena(doc, {0, 128, 255})) // 0x0000, 0x8080, 0xFFFF are the same three tints at 16 bits. general := im_decode(t, doc, "<< /Width 3 /Height 1 /BitsPerComponent 16 /ColorSpace 5 0 R >>", im_arena(doc, {0, 0, 0x80, 0x80, 0xFF, 0xFF})) for x in 0 ..< 3 { a := im_rgba(lut, x, 0) b := im_rgba(general, x, 0) worst := 0 for i in 0 ..< 4 { worst = max(worst, abs(int(a[i]) - int(b[i]))) } testing.expectf(t, worst <= 1, "pixel %d: table %v vs general %v", x, a, b) } } // --------------------------------------------------------------------------- // Odds and ends // --------------------------------------------------------------------------- @(test) test_image_interpolate_is_recorded :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) off := im_load(t, doc, "<< /Width 4 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", GRAY8_DATA[:]) testing.expect(t, off != nil && !off.interpolate, "/Interpolate defaults to false") on := im_load(t, doc, "<< /Width 4 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 /Interpolate true >>", GRAY8_DATA[:]) testing.expect(t, on != nil && on.interpolate, "/Interpolate true") // The inline spelling is /I. d, _ := to_dict(cf_parse(doc, "<< /W 4 /H 2 /BPC 8 /CS /G /I true >>")) inl, _ := image_load_inline(doc, d, GRAY8_DATA[:], nil) testing.expect(t, inl != nil && inl.interpolate, "/I true") } @(test) test_image_size_reports_pixels :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) img := im_load(t, doc, "<< /Width 4 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", GRAY8_DATA[:]) w, h := image_size(img) testing.expect_value(t, w, 4) testing.expect_value(t, h, 2) } @(test) test_image_real_valued_dimensions :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // "/Width 4.0" occurs; rejecting it would lose the image over nothing. img := im_load(t, doc, "<< /Width 4.0 /Height 2.0 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", GRAY8_DATA[:]) testing.expect(t, img != nil, "a real-valued /Width must be accepted") if img == nil { return } testing.expect_value(t, img.width, 4) expect_rgba(t, im_pixels(t, doc, img), 3, 1, {40, 40, 40, 255}, "real /Width") } @(test) test_image_non_stencil_mask_is_taken_as_one :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // 8.9.6.4 requires the stencil to be an /ImageMask; producers write a // 1-bit DeviceGray. The samples mean the same thing, so it is read anyway. stencil := cf_stream(doc, "<< /Width 8 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 1 >>", GRAY1_DATA[:]) d, _ := to_dict(cf_parse(doc, "<< /Width 8 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 >>")) dict_set(d, "Mask", stencil) img, _ := image_load(doc, new_stream(d, im_arena_fill(doc, 8, 60), doc.allocator), nil) testing.expect(t, img != nil && img.mask != nil, "the mask must still load") bmp := im_pixels(t, doc, img) expect_rgba(t, bmp, 0, 0, {60, 60, 60, 0}, "bit 1 masks out") expect_rgba(t, bmp, 1, 0, {60, 60, 60, 255}, "bit 0 paints") } @(test) test_image_non_stencil_mask_leaves_the_shared_image_alone :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // One stream used both ways, which is what a page holding Im1 and an Im2 // whose /Mask names Im1 does. Reading it as a stencil for the masking use // is right; writing that decision back onto the Image the by-stream cache // holds is what makes the direct `Do` of it decode as a one-channel // stencil instead of the greyscale picture it is. shared := cf_stream(doc, "<< /Width 8 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", im_arena(doc, {0b1010_1010, 0, 255, 0x11, 0x22, 0x33, 0x44, 0x55})) pic, _ := image_load(doc, shared, nil) testing.expect(t, pic != nil, "the picture must load") if pic == nil { return } first := im_pixels(t, doc, pic) testing.expect_value(t, first.n, 4) expect_rgba(t, first, 0, 0, {170, 170, 170, 255}, "greyscale before the mask loads") expect_rgba(t, first, 2, 0, {255, 255, 255, 255}, "greyscale before the mask loads") d, _ := to_dict(cf_parse(doc, "<< /Width 8 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 >>")) dict_set(d, "Mask", shared) masked, _ := image_load(doc, new_stream(d, im_arena_fill(doc, 8, 60), doc.allocator), nil) testing.expect(t, masked != nil && masked.mask != nil, "the stencil /Mask must load") if masked == nil || masked.mask == nil { return } testing.expect(t, masked.mask != pic, "the stencil reading is a second Image") testing.expect(t, masked.mask.is_mask, "and it IS read as a stencil") testing.expect_value(t, masked.mask.bpc, 1) testing.expect(t, !pic.is_mask, "the picture must not have become a stencil") testing.expect_value(t, pic.bpc, 8) // Read one bit deep the first byte is the whole 8-pixel row: 0b10101010, // so a 1 bit masks out and a 0 bit paints, alternately. mbmp := im_pixels(t, doc, masked) expect_rgba(t, mbmp, 0, 0, {60, 60, 60, 0}, "bit 1 masks out") expect_rgba(t, mbmp, 1, 0, {60, 60, 60, 255}, "bit 0 paints") // And the picture still decodes as colour when it is decoded again from // scratch, which is where a flipped is_mask shows up even if the cached // bitmap happened to survive. doc.img_cache.budget = 1 other := im_load(t, doc, "<< /Width 4 /Height 4 /ColorSpace /DeviceGray /BitsPerComponent 8 >>", im_arena_fill(doc, 16, 9)) im_pixels(t, doc, other) testing.expect(t, !pic.decoded, "the picture should have been evicted") again := im_pixels(t, doc, pic) testing.expect_value(t, again.n, 4) expect_rgba(t, again, 0, 0, {170, 170, 170, 255}, "still greyscale after re-decoding") expect_rgba(t, again, 2, 0, {255, 255, 255, 255}, "still greyscale after re-decoding") } @(test) test_image_color_key_wrong_length_is_dropped :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // Three entries for a three-component space needs six. img := im_load(t, doc, "<< /Width 1 /Height 1 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Mask [0 0 0] >>", im_arena(doc, {0, 0, 0})) testing.expect(t, img != nil && !img.has_color_key, "a short /Mask array is ignored") expect_rgba(t, im_pixels(t, doc, img), 0, 0, {0, 0, 0, 255}, "still opaque") } @(test) test_image_flate_encoded_samples :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // A stored (uncompressed) deflate block wrapped in a zlib header, so the // byte-filter half of stream_image_data is exercised too. // Built by hand: 78 01, then BFINAL=1 BTYPE=00, LEN=4, NLEN=~4, payload, // then the Adler-32 of {10,20,30,40}. raw := [?]byte{ 0x78, 0x01, 0x01, 0x04, 0x00, 0xFB, 0xFF, 10, 20, 30, 40, 0x00, 0x63, 0x00, 0x1B, } bmp := im_decode(t, doc, "<< /Width 2 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode >>", im_arena(doc, raw[:])) expect_rgba(t, bmp, 0, 0, {10, 10, 10, 255}, "flate") expect_rgba(t, bmp, 1, 0, {20, 20, 20, 255}, "flate") expect_rgba(t, bmp, 0, 1, {30, 30, 30, 255}, "flate") expect_rgba(t, bmp, 1, 1, {40, 40, 40, 255}, "flate") } @(test) test_image_filter_chain_stops_at_the_image_filter :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // /Filter [/ASCIIHexDecode /DCTDecode]: the hex is a byte filter and is // applied here, the JPEG is an image filter and is handed to the decoder // with the image dictionary. Doing it the other way round -- running the // whole chain first -- would leave nothing but a bag of bytes. hex := make([]byte, len(JPEG_T_GREY8) * 2 + 1, doc.allocator) digits := "0123456789ABCDEF" for b, i in JPEG_T_GREY8 { hex[i * 2] = digits[b >> 4] hex[i * 2 + 1] = digits[b & 15] } hex[len(hex) - 1] = '>' bmp := im_decode(t, doc, "<< /Width 8 /Height 8 /BitsPerComponent 8 /ColorSpace /DeviceGray " + "/Filter [/ASCIIHexDecode /DCTDecode] >>", hex) if bmp == nil { return } for y in 0 ..< 8 { for x in 0 ..< 8 { v := JPEG_T_GREY8_PIXELS[y * 8 + x] expect_rgba_near(t, bmp, x, y, {v, v, v, 255}, 4, "hex over JPEG") } } }