From d9a327eefc46d08a1a1860321d696de410ece1ae Mon Sep 17 00:00:00 2001 From: IllusionMan1212 Date: Sun, 25 May 2025 22:30:41 +0200 Subject: [PATCH] feat: decoding and encoding --- example/flower.png | Bin 0 -> 14304 bytes example/main.odin | 130 ++++++++++++++++++ thumbhash.odin | 335 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 465 insertions(+) create mode 100644 example/flower.png create mode 100644 example/main.odin create mode 100644 thumbhash.odin diff --git a/example/flower.png b/example/flower.png new file mode 100644 index 0000000000000000000000000000000000000000..07a17223bb6a87a0197f77972bbb337e4f47e96b GIT binary patch literal 14304 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 []u8 { + assert(len(rgb_data) % 3 == 0, "RGB data length must be a multiple of 3") + + rgba_data := make([dynamic]u8, 0, (len(rgb_data) / 3) * 4) + + for i := 0; i < len(rgb_data); i += 3 { + r := rgb_data[i + 0] + g := rgb_data[i + 1] + b := rgb_data[i + 2] + a := u8(255) // Fully opaque + + append(&rgba_data, r) + append(&rgba_data, g) + append(&rgba_data, b) + append(&rgba_data, a) + } + + return rgba_data[:] +} + +encode_example :: proc() { + if len(os2.args) < 3 { + fmt.eprintln("Please provide a path to a png") + return + } + + img, err := image.load_from_file(os2.args[2]) + if err != nil { + fmt.eprintln(err) + return + } + + buf := img.pixels.buf[:] + if img.channels == 3 { + buf = convert_rgb_to_rgba(img.pixels.buf[:]) + } + + t: mem.Tracking_Allocator + mem.tracking_allocator_init(&t, context.allocator) + alloc := mem.tracking_allocator(&t) + + thash, hash_err := thumbhash.encode(img.width, img.height, buf, alloc) + defer delete(thash) + if hash_err != nil { + fmt.eprintln(hash_err) + return + } + + fmt.printfln("Hash: %X", thash) + + delete(thash) + + for _, leak in t.allocation_map { + fmt.printfln("%v leaked %m", leak.location, leak.size) + } + for bad_free in t.bad_free_array { + fmt.printfln("%v allocation %p was freed badly", bad_free.location, bad_free.memory) + } + fmt.printfln("Peak Mem: %m", t.peak_memory_allocated) + fmt.printfln("Total Mem: %m", t.total_memory_allocated) + fmt.printfln("Current Mem: %m", t.current_memory_allocated) + fmt.printfln("Total Allocs: %v, Total Frees: %v:", t.total_allocation_count, t.total_free_count) + fmt.printfln("Total Mem Free'd: %m", t.total_memory_freed) +} + +main :: proc() { + if len(os2.args) < 2 { + fmt.eprintln("Usage: program \"decode|encode\" ") + return + } + + if os2.args[1] == "decode" { + decode_example() + } else if os2.args[1] == "encode" { + encode_example() + } else { + fmt.eprintln("Unknown command") + } +} diff --git a/thumbhash.odin b/thumbhash.odin new file mode 100644 index 0000000..3534367 --- /dev/null +++ b/thumbhash.odin @@ -0,0 +1,335 @@ +package thumbhash + +import "core:math" + +Error :: enum { + None, + Dimensions_Too_Large, + Invalid_RGBA_Image, + Hash_Too_Short, +} + +hash_to_approximate_aspect_ratio :: proc(hash: []byte) -> (ratio: f32, err: Error) { + if len(hash) < 5 { + return 0.0, .Hash_Too_Short + } + + has_alpha := hash[2] & 0x80 != 0 + l_max := has_alpha ? 5 : 7 + l_min := hash[3] & 7 + is_landscape := hash[4] & 0x80 != 0 + lx := is_landscape ? u8(l_max) : l_min + ly := is_landscape ? l_min : u8(l_max) + + return f32(lx) / f32(ly), nil +} + +@(private) +decode_channel :: proc(hash: []byte, nx, ny: uint, scale: f32, ac_start: int, ac_index: ^int) -> [dynamic]f32 { + ac := make([dynamic]f32, 0, nx * ny) + + for cy in 0.. 0 ? 0 : 1 + + for cx * ny < nx * (ny - cy) { + fac := f32((hash[ac_start + (ac_index^ >> 1)] >> uint((ac_index^ & 1) << 2)) & 15) + + append(&ac, (fac / 7.5 - 1.0) * scale) + ac_index^ += 1 + cx += 1 + } + } + + return ac +} + +decode :: proc(hash: []byte, allocator := context.allocator) -> (w: uint, h: uint, pixels: [dynamic]byte, err: Error) { + context.allocator = allocator + + ratio := hash_to_approximate_aspect_ratio(hash) or_return + + header24 := cast(u32)hash[0] | + (cast(u32)hash[1] << 8) | + (cast(u32)hash[2] << 16) + + header16 := cast(u16)hash[3] | (cast(u16)hash[4] << 8) + l_dc := f32(header24 & 63) / 63.0 + p_dc := f32((header24 >> 6) & 63) / 31.5 - 1.0 + q_dc := f32((header24 >> 12) & 63) / 31.5 - 1.0 + l_scale := f32((header24 >> 18) & 31) / 31.0 + has_alpha := (header24 >> 23) != 0 + p_scale := f32((header16 >> 3) & 63) / 63.0 + q_scale := f32((header16 >> 9) & 63) / 63.0 + is_landscape := (header16 >> 15) != 0 + l_max := has_alpha ? 5 : 7 + lx := cast(uint)max(3, is_landscape ? l_max : int(header16 & 7)) + ly := cast(uint)max(3, is_landscape ? int(header16 & 7) : l_max) + a_dc, a_scale: f32 = 1.0, 1.0 + if has_alpha { + header8 := hash[5] + a_dc = f32(header8 & 15) / 15.0 + a_scale = f32(header8 >> 4) / 15.0 + } + + ac_start := has_alpha ? 6 : 5 + ac_index := 0 + l_ac := decode_channel(hash, lx, ly, l_scale, ac_start, &ac_index) + p_ac := decode_channel(hash, 3, 3, p_scale * 1.25, ac_start, &ac_index) + q_ac := decode_channel(hash, 3, 3, q_scale * 1.25, ac_start, &ac_index) + a_ac: [dynamic]f32 + if has_alpha { + a_ac = decode_channel(hash, 5, 5, a_scale, ac_start, &ac_index) + } + + defer delete(l_ac) + defer delete(p_ac) + defer delete(q_ac) + defer delete(a_ac) + + width := cast(uint)math.round(ratio > 1.0 ? 32 : 32 * ratio) + height := cast(uint)math.round(ratio > 1.0 ? 32 / ratio : 32) + + pixels = make([dynamic]byte, 0, width * height * 4) + fx := []f32{0, 0, 0, 0, 0, 0, 0} + fy := []f32{0, 0, 0, 0, 0, 0, 0} + + for y in 0.. 0 ? 0 : 1 + fy2 := fy[cy] * 2.0 + for cx * ly < lx * (ly - cy) { + l += l_ac[j] * fx[cx] * fy2 + j += 1 + cx += 1 + } + } + + j = 0 + for cy in 0..<3 { + cx := cy > 0 ? 0 : 1 + fy2 := fy[cy] * 2.0 + for cx < 3 - cy { + f := fx[cx] *fy2 + p += p_ac[j] * f + q += q_ac[j] * f + j += 1 + cx += 1 + } + } + + if has_alpha { + j = 0 + for cy in 0..<5 { + cx := cy > 0 ? 0 : 1 + fy2 := fy[cy] * 2.0 + for cx < 5 - cy { + a += a_ac[j] * fx[cx] * fy2 + j += 1 + cx += 1 + } + } + } + + b := l - 2.0 / 3.0 * p + r := (3.0 * l - b + q) / 2.0 + g := r - q + + append(&pixels, byte(clamp(r, 0.0, 1.0) * 255.0)) + append(&pixels, byte(clamp(g, 0.0, 1.0) * 255.0)) + append(&pixels, byte(clamp(b, 0.0, 1.0) * 255.0)) + append(&pixels, byte(clamp(a, 0.0, 1.0) * 255.0)) + } + } + + return width, height, pixels, nil +} + +@(private) +encode_channel :: proc(channel: []f32, nx, ny: uint, width, height: int, allocator := context.allocator) -> (f32, [dynamic]f32, f32) { + context.allocator = allocator + + dc := f32(0.0) + ac := make([dynamic]f32, 0, nx * ny / 2) + scale := f32(0.0) + fx := make([]f32, width) + defer delete(fx) + + for cy in 0.. 0 || cy > 0 { + append(&ac, f) + scale = max(abs(f), scale) + } else { + dc = f + } + + cx += 1 + } + } + + if scale > 0.0 { + for &_ac in ac { + _ac = 0.5 + 0.5 / scale * _ac + } + } + + return dc, ac, scale +} + +encode :: proc(width, height: int, pixels: []byte, allocator := context.allocator) -> ([dynamic]byte, Error) { + context.allocator = allocator + + if width > 100 || height > 100 { + return nil, .Dimensions_Too_Large + } + + if len(pixels) != width * height * 4 { + return nil, .Invalid_RGBA_Image + } + + avg_r, avg_g, avg_b, avg_a: f32 = 0.0, 0.0, 0.0, 0.0 + + j := 0 + for i in 0.. 0.0 { + avg_r /= avg_a + avg_g /= avg_a + avg_b /= avg_a + } + + has_alpha := avg_a < f32(width * height) + l_limit := has_alpha ? 5 : 7 + lx := max(cast(uint)math.round(f32(l_limit * width) / f32(max(width, height))), 1) + ly := max(cast(uint)math.round(f32(l_limit * height) / f32(max(width, height))), 1) + + l := make([dynamic]f32, 0, width * height) + p := make([dynamic]f32, 0, width * height) + q := make([dynamic]f32, 0, width * height) + a := make([dynamic]f32, 0, width * height) + + defer delete(l) + defer delete(p) + defer delete(q) + defer delete(a) + + for i := 0; i < len(pixels); i += 4 { + alpha := f32(pixels[i + 3]) / 255.0 + r := avg_r * (1.0 - alpha) + alpha / 255.0 * f32(pixels[i]) + g := avg_g * (1.0 - alpha) + alpha / 255.0 * f32(pixels[i + 1]) + b := avg_b * (1.0 - alpha) + alpha / 255.0 * f32(pixels[i + 2]) + append(&l, (r + g + b) / 3.0) + append(&p, (r + g) / 2.0 - b) + append(&q, r - g) + append(&a, alpha) + } + + l_dc, l_ac, l_scale := encode_channel(l[:], max(lx, 3), max(ly, 3), width, height) + p_dc, p_ac, p_scale := encode_channel(p[:], 3, 3, width, height) + q_dc, q_ac, q_scale := encode_channel(q[:], 3, 3, width, height) + a_dc, a_ac, a_scale := f32(1.0), [dynamic]f32{}, f32(1.0) + + defer delete(l_ac) + defer delete(p_ac) + defer delete(q_ac) + defer delete(a_ac) + + if has_alpha { + a_dc, a_ac, a_scale = encode_channel(a[:], 5, 5, width, height) + } + + is_landscape := width > height + header24: u32 = cast(u32)math.round(63.0 * l_dc) | + ((cast(u32)math.round(31.5 + 31.5 * p_dc)) << 6) | + ((cast(u32)math.round(31.5 + 31.5 * q_dc)) << 12) | + ((cast(u32)math.round(31.0 * l_scale)) << 18) + + if has_alpha { + header24 |= 1 << 23 + } + + header16: u16 = cast(u16)(is_landscape ? ly : lx) | + ((cast(u16)math.round(63.0 * p_scale)) << 3) | + ((cast(u16)math.round(63.0 * q_scale)) << 9) + + if is_landscape { + header16 |= 1 << 15 + } + + hash := make([dynamic]byte, 0, 25) + append(&hash, byte(header24 & 0xFF)) + append(&hash, byte((header24 >> 8) & 0xFF)) + append(&hash, byte(header24 >> 16)) + append(&hash, byte(header16 & 0xFF)) + append(&hash, byte(header16 >> 8)) + + is_odd := false + if has_alpha { + append(&hash, cast(byte)math.round(15.0 * a_dc) | ((cast(byte)math.round(15.0 * a_scale)) << 4)) + } + + for ac in ([][]f32{l_ac[:], p_ac[:], q_ac[:]}) { + for f in ac { + u := cast(byte)math.round(15.0 * f) + if is_odd { + hash[len(hash) - 1] |= u << 4 + } else { + append(&hash, u) + } + is_odd = !is_odd + } + } + + if has_alpha { + for f in a_ac { + u := cast(byte)math.round(15.0 * f) + if is_odd { + hash[len(hash) - 1] |= u << 4 + } else { + append(&hash, u) + } + is_odd = !is_odd + } + } + + return hash, nil +}