From d2a2c1e74e62ab3fdb11d93f4ef8f74e4727bcda Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Sun, 9 Jun 2024 16:10:06 +0200 Subject: [PATCH 1/3] Image: Add improved blending method and test it. --- core/image/common.odin | 17 +++++++++++++ core/image/png/png.odin | 20 +++++++-------- tests/core/image/test_core_image.odin | 36 +++++++++++++++++++++++++++ tests/core/normal.odin | 1 - tests/core/speed.odin | 1 + 5 files changed, 63 insertions(+), 12 deletions(-) diff --git a/core/image/common.odin b/core/image/common.odin index fe75371b3..15c5bbbce 100644 --- a/core/image/common.odin +++ b/core/image/common.odin @@ -42,6 +42,23 @@ GA_Pixel :: [2]u8 G_Pixel_16 :: [1]u16 GA_Pixel_16 :: [2]u16 +blend_single_channel :: #force_inline proc(fg, alpha, bg: $T) -> (res: T) where T == u8 || T == u16 { + MAX :: 256 when T == u8 else 65536 + + c := u32(fg) * (MAX - u32(alpha)) + u32(bg) * (1 + u32(alpha)) + return T(c & (MAX - 1)) +} + +blend_pixel :: #force_inline proc(fg: [$N]$T, alpha: T, bg: [N]T) -> (res: [N]T) where (T == u8 || T == u16), N >= 1 && N <= 4 { + MAX :: 256 when T == u8 else 65536 + + r_a := u32(fg) * (MAX - u32(alpha)) + u32(bg) * (1 + u32(alpha)) + + return +} + +blend :: proc{blend_single_channel, blend_pixel} + Image :: struct { width: int, height: int, diff --git a/core/image/png/png.odin b/core/image/png/png.odin index 4bb070da8..9a91575a8 100644 --- a/core/image/png/png.odin +++ b/core/image/png/png.odin @@ -597,7 +597,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a dsc := depth_scale_table scale := dsc[info.header.bit_depth] if scale != 1 { - key := mem.slice_data_cast([]u16be, c.data)[0] * u16be(scale) + key := (^u16be)(raw_data(c.data))^ * u16be(scale) c.data = []u8{0, u8(key & 255)} } } @@ -762,19 +762,18 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a for w := 0; w < int(img.width); w += 1 { index := temp.buf[i] - c := _plte.entries[index] - a := int(index) < len(trns.data) ? trns.data[index] : 255 - alpha := f32(a) / 255.0 + c := _plte.entries[index] + a := int(index) < len(trns.data) ? trns.data[index] : 255 if blend_background { - c.r = u8((1.0 - alpha) * bg[0] + f32(c.r) * alpha) - c.g = u8((1.0 - alpha) * bg[1] + f32(c.g) * alpha) - c.b = u8((1.0 - alpha) * bg[2] + f32(c.b) * alpha) + c.r = image.blend(c.r, a, u8(u32(bg.r))) + c.g = image.blend(c.g, a, u8(u32(bg.g))) + c.b = image.blend(c.b, a, u8(u32(bg.b))) a = 255 } else if premultiply { - c.r = u8(f32(c.r) * alpha) - c.g = u8(f32(c.g) * alpha) - c.b = u8(f32(c.b) * alpha) + c.r = image.blend(u8(0), a, c.r) + c.g = image.blend(u8(0), a, c.g) + c.b = image.blend(u8(0), a, c.b) } t.buf[j ] = c.r @@ -1627,7 +1626,6 @@ defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^image.PNG_IH return nil } - @(init, private) _register :: proc() { image.register(.PNG, load_from_bytes, destroy) diff --git a/tests/core/image/test_core_image.odin b/tests/core/image/test_core_image.odin index 857022b81..0cd118497 100644 --- a/tests/core/image/test_core_image.odin +++ b/tests/core/image/test_core_image.odin @@ -2358,4 +2358,40 @@ run_bmp_suite :: proc(t: ^testing.T, suite: []Test) { } } return +} + +@test +will_it_blend :: proc(t: ^testing.T) { + Pixel :: image.RGB_Pixel + Pixel_16 :: image.RGB_Pixel_16 + + { + bg := Pixel{255, 255, 0} + fg := Pixel{ 0, 0, 255} + + for a in 0..=255 { + blended := Pixel{ + image.blend(fg.r, u8(a), bg.r), + image.blend(fg.g, u8(a), bg.g), + image.blend(fg.b, u8(a), bg.b), + } + testing.expectf(t, blended.r == bg.r - u8(a), "Expected blend(%v, %3d, %v) = %v, got %v", fg.r, a, bg.r, bg.r - u8(a), blended.r) + testing.expectf(t, blended.b == 255 - blended.r, "Expected blend(%v, %3d, %v) = %v, got %v", fg.b, a, bg.b, 255 - blended.r, blended.b) + } + } + + { + bg := Pixel_16{65535, 65535, 0} + fg := Pixel_16{ 0, 0, 65535} + + for a in 0..=65535 { + blended := Pixel_16{ + image.blend(fg.r, u16(a), bg.r), + image.blend(fg.g, u16(a), bg.g), + image.blend(fg.b, u16(a), bg.b), + } + testing.expectf(t, blended.r == bg.r - u16(a), "Expected blend(%v, %3d, %v) = %v, got %v", fg.r, a, bg.r, bg.r - u16(a), blended.r) + testing.expectf(t, blended.b == 65535 - blended.r, "Expected blend(%v, %3d, %v) = %v, got %v", fg.b, a, bg.b, 65535 - blended.r, blended.b) + } + } } \ No newline at end of file diff --git a/tests/core/normal.odin b/tests/core/normal.odin index f23763a59..29f4e9b80 100644 --- a/tests/core/normal.odin +++ b/tests/core/normal.odin @@ -20,7 +20,6 @@ download_assets :: proc() { @(require) import "encoding/varint" @(require) import "encoding/xml" @(require) import "fmt" -@(require) import "image" @(require) import "math" @(require) import "math/big" @(require) import "math/linalg/glsl" diff --git a/tests/core/speed.odin b/tests/core/speed.odin index 555d30f5e..a4b2b6a69 100644 --- a/tests/core/speed.odin +++ b/tests/core/speed.odin @@ -3,3 +3,4 @@ package tests_core @(require) import "crypto" @(require) import "hash" +@(require) import "image" \ No newline at end of file From 6b88d0a8206e78f2bf84bba39dc2261f906d0a58 Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Sun, 9 Jun 2024 16:37:27 +0200 Subject: [PATCH 2/3] Use new blend helper --- core/image/common.odin | 53 +++++++++++++++++++++++++++-------------- core/image/png/png.odin | 16 +++++-------- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/core/image/common.odin b/core/image/common.odin index 15c5bbbce..fed2c1470 100644 --- a/core/image/common.odin +++ b/core/image/common.odin @@ -42,23 +42,6 @@ GA_Pixel :: [2]u8 G_Pixel_16 :: [1]u16 GA_Pixel_16 :: [2]u16 -blend_single_channel :: #force_inline proc(fg, alpha, bg: $T) -> (res: T) where T == u8 || T == u16 { - MAX :: 256 when T == u8 else 65536 - - c := u32(fg) * (MAX - u32(alpha)) + u32(bg) * (1 + u32(alpha)) - return T(c & (MAX - 1)) -} - -blend_pixel :: #force_inline proc(fg: [$N]$T, alpha: T, bg: [N]T) -> (res: [N]T) where (T == u8 || T == u16), N >= 1 && N <= 4 { - MAX :: 256 when T == u8 else 65536 - - r_a := u32(fg) * (MAX - u32(alpha)) + u32(bg) * (1 + u32(alpha)) - - return -} - -blend :: proc{blend_single_channel, blend_pixel} - Image :: struct { width: int, height: int, @@ -1276,6 +1259,40 @@ apply_palette_rgba :: proc(img: ^Image, palette: [256]RGBA_Pixel, allocator := c } apply_palette :: proc{apply_palette_rgb, apply_palette_rgba} +blend_single_channel :: #force_inline proc(fg, alpha, bg: $T) -> (res: T) where T == u8 || T == u16 { + MAX :: 256 when T == u8 else 65536 + + c := u32(fg) * (MAX - u32(alpha)) + u32(bg) * (1 + u32(alpha)) + return T(c & (MAX - 1)) +} + +blend_pixel :: #force_inline proc(fg: [$N]$T, alpha: T, bg: [N]T) -> (res: [N]T) where (T == u8 || T == u16), N >= 1 && N <= 4 { + MAX :: 256 when T == u8 else 65536 + + when N == 1 { + r := u32(fg.r) * (MAX - u32(alpha)) + u32(bg.r) * (1 + u32(alpha)) + return {T(r & (MAX - 1))} + } + when N == 2 { + r := u32(fg.r) * (MAX - u32(alpha)) + u32(bg.r) * (1 + u32(alpha)) + g := u32(fg.g) * (MAX - u32(alpha)) + u32(bg.g) * (1 + u32(alpha)) + return {T(r & (MAX - 1)), T(g & (MAX - 1))} + } + when N == 3 || N == 4 { + r := u32(fg.r) * (MAX - u32(alpha)) + u32(bg.r) * (1 + u32(alpha)) + g := u32(fg.g) * (MAX - u32(alpha)) + u32(bg.g) * (1 + u32(alpha)) + b := u32(fg.b) * (MAX - u32(alpha)) + u32(bg.b) * (1 + u32(alpha)) + + when N == 3 { + return {T(r & (MAX - 1)), T(g & (MAX - 1)), T(b & (MAX - 1))} + } else { + return {T(r & (MAX - 1)), T(g & (MAX - 1)), T(b & (MAX - 1)), MAX - 1} + } + } + unreachable() +} +blend :: proc{blend_single_channel, blend_pixel} + // Replicates grayscale values into RGB(A) 8- or 16-bit images as appropriate. // Returns early with `false` if already an RGB(A) image. @@ -1388,4 +1405,4 @@ write_bytes :: proc(buf: ^bytes.Buffer, data: []u8) -> (err: compress.General_Er return .Resize_Failed } return nil -} +} \ No newline at end of file diff --git a/core/image/png/png.odin b/core/image/png/png.odin index 9a91575a8..56a939682 100644 --- a/core/image/png/png.odin +++ b/core/image/png/png.odin @@ -749,10 +749,10 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a } } } else if add_alpha || .alpha_drop_if_present in options { - bg := [3]f32{0, 0, 0} + bg := PLTE_Entry{0, 0, 0} if premultiply && seen_bkgd { c16 := img.background.([3]u16) - bg = [3]f32{f32(c16.r), f32(c16.g), f32(c16.b)} + bg = {u8(c16.r), u8(c16.g), u8(c16.b)} } no_alpha := (.alpha_drop_if_present in options || premultiply) && .alpha_add_if_missing not_in options @@ -766,14 +766,10 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a a := int(index) < len(trns.data) ? trns.data[index] : 255 if blend_background { - c.r = image.blend(c.r, a, u8(u32(bg.r))) - c.g = image.blend(c.g, a, u8(u32(bg.g))) - c.b = image.blend(c.b, a, u8(u32(bg.b))) + c = image.blend(c, a, bg) a = 255 } else if premultiply { - c.r = image.blend(u8(0), a, c.r) - c.g = image.blend(u8(0), a, c.g) - c.b = image.blend(u8(0), a, c.b) + c = image.blend(PLTE_Entry{}, a, c) } t.buf[j ] = c.r @@ -1014,8 +1010,8 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a return {}, .Unable_To_Allocate_Or_Resize } - p := mem.slice_data_cast([]u8, temp.buf[:]) - o := mem.slice_data_cast([]u8, t.buf[:]) + p := temp.buf[:] + o := t.buf[:] switch raw_image_channels { case 1: From 5be7d8e32d7f299c74df39368395a0505b39ab13 Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Sun, 9 Jun 2024 17:59:59 +0200 Subject: [PATCH 3/3] Clean up PNG code. --- core/image/png/png.odin | 46 ++++++++++++--------------- tests/core/image/test_core_image.odin | 1 - 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/core/image/png/png.odin b/core/image/png/png.odin index 56a939682..aa1c5f781 100644 --- a/core/image/png/png.odin +++ b/core/image/png/png.odin @@ -735,18 +735,11 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a return {}, .Unable_To_Allocate_Or_Resize } - i := 0; j := 0 - // If we don't have transparency or drop it without applying it, we can do this: if (!seen_trns || (seen_trns && .alpha_drop_if_present in options && .alpha_premultiply not_in options)) && .alpha_add_if_missing not_in options { - for h := 0; h < int(img.height); h += 1 { - for w := 0; w < int(img.width); w += 1 { - c := _plte.entries[temp.buf[i]] - t.buf[j ] = c.r - t.buf[j+1] = c.g - t.buf[j+2] = c.b - i += 1; j += 3 - } + output := mem.slice_data_cast([]image.RGB_Pixel, t.buf[:]) + for pal_idx, idx in temp.buf { + output[idx] = _plte.entries[pal_idx] } } else if add_alpha || .alpha_drop_if_present in options { bg := PLTE_Entry{0, 0, 0} @@ -758,12 +751,23 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a no_alpha := (.alpha_drop_if_present in options || premultiply) && .alpha_add_if_missing not_in options blend_background := seen_bkgd && .blend_background in options - for h := 0; h < int(img.height); h += 1 { - for w := 0; w < int(img.width); w += 1 { - index := temp.buf[i] + if no_alpha { + output := mem.slice_data_cast([]image.RGB_Pixel, t.buf[:]) + for orig, idx in temp.buf { + c := _plte.entries[orig] + a := int(orig) < len(trns.data) ? trns.data[orig] : 255 - c := _plte.entries[index] - a := int(index) < len(trns.data) ? trns.data[index] : 255 + if blend_background { + output[idx] = image.blend(c, a, bg) + } else if premultiply { + output[idx] = image.blend(PLTE_Entry{}, a, c) + } + } + } else { + output := mem.slice_data_cast([]image.RGBA_Pixel, t.buf[:]) + for orig, idx in temp.buf { + c := _plte.entries[orig] + a := int(orig) < len(trns.data) ? trns.data[orig] : 255 if blend_background { c = image.blend(c, a, bg) @@ -772,17 +776,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a c = image.blend(PLTE_Entry{}, a, c) } - t.buf[j ] = c.r - t.buf[j+1] = c.g - t.buf[j+2] = c.b - i += 1 - - if no_alpha { - j += 3 - } else { - t.buf[j+3] = u8(a) - j += 4 - } + output[idx] = {c.r, c.g, c.b, u8(a)} } } } else { diff --git a/tests/core/image/test_core_image.odin b/tests/core/image/test_core_image.odin index 0cd118497..899596229 100644 --- a/tests/core/image/test_core_image.odin +++ b/tests/core/image/test_core_image.odin @@ -1479,7 +1479,6 @@ run_png_suite :: proc(t: ^testing.T, suite: []Test) { png_hash := hash.crc32(pixels) testing.expectf(t, test.hash == png_hash, "%v test %v hash is %08x, expected %08x with %v", file.file, count, png_hash, test.hash, test.options) - passed &= test.hash == png_hash if passed {