From 3f652ea0797a576224bd9f34ebed56c0f144cf27 Mon Sep 17 00:00:00 2001 From: IllusionMan1212 Date: Sat, 24 May 2025 03:06:42 +0200 Subject: [PATCH] initial --- blurhash.odin | 306 ++++++++++++++++++++++++++++++++++++++++++++++ example/main.odin | 74 +++++++++++ 2 files changed, 380 insertions(+) create mode 100644 blurhash.odin create mode 100644 example/main.odin diff --git a/blurhash.odin b/blurhash.odin new file mode 100644 index 0000000..73ff962 --- /dev/null +++ b/blurhash.odin @@ -0,0 +1,306 @@ +package blurhash + +import "core:strings" +import "core:math" + +@(rodata, private) +ENC_TABLE := [83]byte { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', + 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', + 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', + 'y', 'z', '#', '$', '%', '*', '+', ',', '-', '.', + ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', + '|', '}', '~', +} + +@(rodata, private) +DEC_TABLE := [128]int { + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, 62, 63, 64, -1, -1, + -1, -1, 65, 66, 67, 68, 69, -1, + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 70, 71, -1, 72, -1, 73, + 74, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + 33, 34, 35, 75, -1, 76, 77, 78, + -1, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 53, 54, 55, 56, 57, 58, + 59, 60, 61, 79, 80, 81, 82, -1, +} + +Error :: enum { + None, + + Invalid_BlurHash, + Invalid_Component_Count, +} + +@(private) +base83_decode_byte :: proc(r: byte) -> int { + return DEC_TABLE[r] +} + +@(private) +base83_decode_str :: proc(str: string) -> int { + value := 0 + for r in str { + value = value * 83 + DEC_TABLE[r] + } + return value +} + +@(private) +base83_decode :: proc{base83_decode_str, base83_decode_byte} + +@(private) +base83_encode :: proc(value, length: int, allocator := context.allocator) -> string { + b := strings.builder_make_none(allocator) + + pow :: proc(base, exp: int) -> int { + base := base + exp := exp + result := 1 + for exp > 0 { + if exp % 2 == 1 { + result *= base + } + base *= base + exp /= 2 + } + return result + } + + for i in 1..=length { + digit := (value / pow(83, length - i)) % 83 + strings.write_byte(&b, ENC_TABLE[digit]) + } + + return strings.to_string(b) +} + +@(private) +is_valid_blurhash :: proc(blurhash: string) -> bool { + if len(blurhash) < 6 { + return false + } + + size_flag := base83_decode(blurhash[0]) + num_y := (size_flag / 9) + 1 + num_x := (size_flag % 9) + 1 + + if len(blurhash) != 4 + 2 * num_x * num_y { + return false + } + + return true +} + +@(private) +linear_to_srgb :: proc(value: f32) -> int { + v := max(0, min(1, value)) + if v <= 0.0031308 { + return int(v * 12.92 * 255 + 0.5) + } + + return int((1.055 * math.pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) +} + +@(private) +srgb_to_linear :: proc(value: int) -> f32 { + v := f32(value) / 255.0 + if v <= 0.04045 { + return v / 12.92 + } + + return math.pow((v + 0.055) / 1.055, 2.4) +} + +@(private) +decode_dc :: proc(value: int) -> (r, g, b: f32) { + return srgb_to_linear(value >> 16), srgb_to_linear((value >> 8) & 0xFF), srgb_to_linear(value & 0xFF) +} + +@(private) +sign_pow :: proc(value, exp: f32) -> f32 { + return math.copy_sign(math.pow(abs(value), exp), value) +} + +@(private) +decode_ac :: proc(value: int, max_value: f32) -> (r, g, b: f32) { + quant_r := int(math.floor(f32(value) / (19 * 19))) + quant_g := int(math.floor(f32(value) / 19)) % 19 + quant_b := value % 19 + + r = sign_pow((f32(quant_r) - 9) / 9, 2.0) * max_value + g = sign_pow((f32(quant_g) - 9) / 9, 2.0) * max_value + b = sign_pow((f32(quant_b) - 9) / 9, 2.0) * max_value + + return +} + +decode :: proc(blurhash: string, width, height: int, punch := 1.0, allocator := context.allocator) -> ([]byte, Error) { + context.allocator = allocator + + if !is_valid_blurhash(blurhash) { + return nil, .Invalid_BlurHash + } + punch := punch + if punch < 1 { + punch = 1 + } + + size_flag := base83_decode(blurhash[0]) + num_y := (size_flag / 9) + 1 + num_x := (size_flag % 9) + 1 + + r, g, b := 0, 0, 0 + + quantized_max_value := base83_decode(blurhash[1]) + max_value := f32(quantized_max_value + 1) / 166.0 + + colors_size := num_x * num_y + colors := make([][3]f32, colors_size) + + for iter in 0.. (r, g, b: f32) { + normalization := x_component == 0 && y_component == 0 ? 1 : 2 + + for y in 0.. int { + rounded_r := linear_to_srgb(dc.r) + rounded_g := linear_to_srgb(dc.g) + rounded_b := linear_to_srgb(dc.b) + + return (rounded_r << 16) + (rounded_g << 8) + rounded_b +} + +@(private) +encode_ac :: proc(ac: [3]f32, max_value: f32) -> int { + quant_r := int(max(0, min(18, math.floor(sign_pow(ac.r / max_value, 0.5) * 9 + 9.5)))) + quant_g := int(max(0, min(18, math.floor(sign_pow(ac.g / max_value, 0.5) * 9 + 9.5)))) + quant_b := int(max(0, min(18, math.floor(sign_pow(ac.b / max_value, 0.5) * 9 + 9.5)))) + + return quant_r * 19 * 19 + quant_g * 19 + quant_b +} + +encode :: proc(pixels: []byte, x_components, y_components, width, height, bytes_per_row: int, allocator := context.allocator) -> (string, Error) { + context.allocator = allocator + + if x_components < 1 || x_components > 9 { + return "", .Invalid_Component_Count + } + if y_components < 1 || y_components > 9 { + return "", .Invalid_Component_Count + } + + builder := strings.builder_make_len_cap(0, 2 + 4 + (9 * 9 - 1) * 2) + factors := make([dynamic][3]f32, 0, x_components * y_components) + + for y in 0.. 0 { + actual_max_value: f32 = 0 + for v in ac { + actual_max_value = max(actual_max_value, max(abs(v[0]), abs(v[1]), abs(v[2]))) + } + + quantized_max_value := int(max(0, min(82, math.floor(actual_max_value * 166 - 0.5)))) + max_value = f32(quantized_max_value + 1) / 166.0 + strings.write_string(&builder, base83_encode(quantized_max_value, 1)) + } else { + max_value = 1 + strings.write_string(&builder, base83_encode(0, 1)) + } + + strings.write_string(&builder, base83_encode(encode_dc(dc), 4)) + + for factor in ac { + strings.write_string(&builder, base83_encode(encode_ac(factor, max_value), 2)) + } + + return strings.to_string(builder), nil +} diff --git a/example/main.odin b/example/main.odin new file mode 100644 index 0000000..d3915a8 --- /dev/null +++ b/example/main.odin @@ -0,0 +1,74 @@ +package main + +import "core:fmt" +import "core:image" +import "core:image/qoi" +import "core:image/png" +import "core:slice" +import "core:os/os2" + +import blurhash "../" + +decode_example :: proc() { + if len(os2.args) < 3 { + fmt.eprintln("Please the blurhash as an arg") + return + } + + width := 400 + height := 240 + pixels, err := blurhash.decode(os2.args[2], width, height, 1.2) + defer delete(pixels) + if err != nil { + fmt.eprintln(err) + return + } + + img, ok := image.pixels_to_image(slice.reinterpret([]image.RGB_Pixel, pixels), width, height) + if !ok { + fmt.eprintln("Couldn't create an image") + return + } + + save_err := qoi.save_to_file("yup.qoi", &img) + if save_err != nil { + fmt.eprintln(save_err) + return + } +} + +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 + } + + bhash, hash_err := blurhash.encode(img.pixels.buf[:], 5, 4, img.width, img.height, img.width*img.channels) + if hash_err != nil { + fmt.eprintln(hash_err) + return + } + + fmt.println(bhash) +} + +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") + } +}