Files

1630 lines
64 KiB
Odin
Raw Permalink Normal View History

2026-09-25 23:32:29 +02:00
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 <FF000000FF000000FFFFFFFF>] >>",
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 <FF000000FF00>] >>",
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 <FF000000FF000000FFFFFFFF>] >>",
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 <FF000000FF000000FFFFFFFF>] >>",
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 <FF000000FF00>] >>"))
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")
}
}
}