1630 lines
64 KiB
Odin
1630 lines
64 KiB
Odin
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")
|
|
}
|
|
}
|
|
}
|