diff --git a/changes/lab-png-16-bit.md b/changes/lab-png-16-bit.md new file mode 100644 index 00000000..41af6d5a --- /dev/null +++ b/changes/lab-png-16-bit.md @@ -0,0 +1,3 @@ +bump: patch +type: feature +**ludic.lab: 16-bit PNGs** — `lab_png_write16_from(st, path, w, h, channels, px, at)` writes 16-bit samples (two bytes each, most significant first) the way `lab_png_write_from` writes 8-bit ones, so a test can write a height map render3d's decoder reads back as R16; `lp_png_bytes(stride, h)` is the size of one. diff --git a/packages/ludic.lab/README.md b/packages/ludic.lab/README.md index 34ca8069..c7c7ae22 100644 --- a/packages/ludic.lab/README.md +++ b/packages/ludic.lab/README.md @@ -61,7 +61,7 @@ showing its own models - adds them from its own module with `def LabScenes int { # the header, then the pixels, then the end, into one buffer; the length written, or -1 @alloc_ok("a bake's preview or a lab shot: one image written, once") -function lp_encode(table: []long, w: int, h: int, channels: int, px: []byte, at: int, out: []byte) -> int { +function lp_encode(table: []long, w: int, h: int, channels: int, px: []byte, at: int, out: []byte) -> int { return lp_encode_depth(table, w, h, channels, 8, px, at, out) } +# ... with `depth` bits a sample (8, or 16: two bytes each, most significant first, as PNG stores them) +@alloc_ok("a bake's preview or a lab shot: one image written, once") +function lp_encode_depth(table: []long, w: int, h: int, channels: int, depth: int, px: []byte, at: int, out: []byte) -> int { let sig = [137, 80, 78, 71, 13, 10, 26, 10] for i in 0 .. 8 { out[i] = sig[i] } lp_put32(out, 8, long(13)) lp_tag(out, 12, "IHDR") lp_put32(out, 16, long(w)) lp_put32(out, 20, long(h)) - out[24] = 8 + out[24] = depth out[25] = lp_colour_type(channels) out[26] = 0 out[27] = 0 out[28] = 0 lp_put32(out, 29, lp_crc_of(table, out, 12, 17)) - let o = lp_idat(table, w, h, channels, px, at, out, 33) + let o = lp_idat(table, w * channels * depth / 8, h, px, at, out, 33) lp_put32(out, o, long(0)) lp_tag(out, o + 4, "IEND") lp_put32(out, o + 8, lp_crc_of(table, out, o + 4, 4)) return o + 12 } -# the IDAT chunk at `at0`: every row behind a filter byte of 0, in stored blocks; where it ends -function lp_idat(table: []long, w: int, h: int, channels: int, px: []byte, at: int, out: []byte, at0: int) -> int { - let stride = w * channels +# the IDAT chunk at `at0`: every row (`stride` bytes) behind a filter byte of 0, in stored blocks; where it ends +function lp_idat(table: []long, stride: int, h: int, px: []byte, at: int, out: []byte, at0: int) -> int { let row = stride + 1 let raw = row * h let blocks = (raw + 65534) / 65535 @@ -112,8 +114,10 @@ function lp_idat(table: []long, w: int, h: int, channels: int, px: []byte, at: i } # the bytes a PNG of this size takes, stored -function lp_png_size(w: int, h: int, channels: int) -> int { - let raw = (w * channels + 1) * h +function lp_png_size(w: int, h: int, channels: int) -> int { return lp_png_bytes(w * channels, h) } +# ... of rows `stride` bytes long +function lp_png_bytes(stride: int, h: int) -> int { + let raw = (stride + 1) * h return 8 + 25 + 12 + 2 + ((raw + 65534) / 65535) * 5 + raw + 4 + 12 } @@ -131,3 +135,14 @@ export function lab_png_write_from(lab_png_st: LabPngState, path: string, w: int export function lab_png_write(lab_png_st: LabPngState, path: string, w: int, h: int, channels: int, px: []byte) -> bool { return lab_png_write_from(lab_png_st, path, w, h, channels, px, 0) } +# w x h pixels of `channels` 16-bit samples each, two bytes a sample most significant first (a height map: +# 1 channel, read back by render3d's decoder as R16), rows top down, starting at px[at] +@alloc_ok("a bake or a test's fixture: one image written, once") +export function lab_png_write16_from(lab_png_st: LabPngState, path: string, w: int, h: int, channels: int, px: []byte, at: int) -> bool { + if channels < 1 or channels > 4 or w <= 0 or h <= 0 or at + w * h * channels * 2 > len(px) { return false } + let out = buffer(lp_png_bytes(w * channels * 2, h)) + let n = lp_encode_depth(lab_png_st.table, w, h, channels, 16, px, at, out) + let ok = Fs.write_bytes(path, out, n) + free(out) + return ok +} diff --git a/packages/ludic.lab/tests/lab_test.ludic b/packages/ludic.lab/tests/lab_test.ludic index ffa25792..23e01155 100644 --- a/packages/ludic.lab/tests/lab_test.ludic +++ b/packages/ludic.lab/tests/lab_test.ludic @@ -47,6 +47,18 @@ program LabTest { expect(not lab_png_write(lab_png_st, `{dir}/lab_test_short.png`, 9, 9, 4, px)) } + test "16-bit samples become a 16-bit PNG, two bytes a sample" (lab_png_st: LabPngState) { + let dir = Os.temp_dir() + let px = buffer(3 * 2 * 2) + for i in 0 .. 12 { px[i] = i * 20 } + expect(lab_png_write16_from(lab_png_st, `{dir}/lab_test_r16.png`, 3, 2, 1, px, 0)) + let p = Fs.read_bytes(`{dir}/lab_test_r16.png`) + expect_eq(p[24], 16) # bit depth + expect_eq(p[25], 0) # colour type grey + expect_eq(len(p), lp_png_bytes(3 * 2, 2)) + expect(not lab_png_write16_from(lab_png_st, `{dir}/lab_test_r16_short.png`, 3, 3, 1, px, 0)) + } + test "a half float and a float read as the numbers they store" { let b = buffer(8) b[0] = 0; b[1] = 60 # half 1.0 = 0x3C00