feat: decoding and encoding
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,130 @@
|
||||
package main
|
||||
|
||||
import "core:image"
|
||||
import "core:image/png"
|
||||
import "core:image/qoi"
|
||||
import "core:os/os2"
|
||||
import "core:fmt"
|
||||
import "core:slice"
|
||||
import "core:mem"
|
||||
|
||||
import thumbhash "../"
|
||||
|
||||
decode_example :: proc() {
|
||||
t: mem.Tracking_Allocator
|
||||
mem.tracking_allocator_init(&t, context.allocator)
|
||||
alloc := mem.tracking_allocator(&t)
|
||||
|
||||
// The hardcoded hash is that of the flower.png file
|
||||
w, h, pixels, err := thumbhash.decode({0x93, 0x4A, 0x6, 0x2D, 0x6, 0x92, 0x56, 0xC3, 0x74, 0x5, 0x58, 0x67, 0xDA, 0x8A, 0xB6, 0x67, 0x94, 0x90, 0x51, 0x7, 0x19}, alloc)
|
||||
if err != nil {
|
||||
fmt.eprintln(err)
|
||||
return
|
||||
}
|
||||
|
||||
img, ok := image.pixels_to_image(slice.reinterpret([]image.RGBA_Pixel, pixels[:]), cast(int)w, cast(int)h)
|
||||
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
|
||||
}
|
||||
|
||||
delete(pixels)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
convert_rgb_to_rgba :: proc(rgb_data: []u8) -> []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\" <arg>")
|
||||
return
|
||||
}
|
||||
|
||||
if os2.args[1] == "decode" {
|
||||
decode_example()
|
||||
} else if os2.args[1] == "encode" {
|
||||
encode_example()
|
||||
} else {
|
||||
fmt.eprintln("Unknown command")
|
||||
}
|
||||
}
|
||||
+335
@@ -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..<ny {
|
||||
cx: uint = cy > 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..<height {
|
||||
for x in 0..<width {
|
||||
l := l_dc
|
||||
p := p_dc
|
||||
q := q_dc
|
||||
a := a_dc
|
||||
|
||||
for cx in 0..<max(lx, has_alpha ? 5 : 3) {
|
||||
fx[cx] = math.cos(math.PI / f32(width) * (f32(x) + 0.5) * f32(cx))
|
||||
}
|
||||
for cy in 0..<max(ly, has_alpha ? 5 : 3) {
|
||||
fy[cy] = math.cos(math.PI / f32(height) * (f32(y) + 0.5) * f32(cy))
|
||||
}
|
||||
|
||||
j := 0
|
||||
for cy in 0..<ly {
|
||||
cx: uint = cy > 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..<ny {
|
||||
cx: uint = 0
|
||||
|
||||
for cx * ny < nx * (ny - cy) {
|
||||
f := f32(0.0)
|
||||
|
||||
for x in 0..<width{
|
||||
fx[x] = math.cos(math.PI / f32(width) * f32(cx) * (f32(x) + 0.5))
|
||||
}
|
||||
|
||||
for y in 0..<height {
|
||||
fy := math.cos(math.PI / f32(height) * f32(cy) * (f32(y) + 0.5))
|
||||
for x in 0..<width {
|
||||
f += channel[x + y * width] * fx[x] * fy
|
||||
}
|
||||
}
|
||||
|
||||
f /= f32(width * height)
|
||||
|
||||
if cx > 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..<width*height {
|
||||
alpha := f32(pixels[j + 3]) / 255.0
|
||||
avg_r += alpha / 255.0 * f32(pixels[j])
|
||||
avg_g += alpha / 255.0 * f32(pixels[j + 1])
|
||||
avg_b += alpha / 255.0 * f32(pixels[j + 2])
|
||||
avg_a += alpha
|
||||
|
||||
j += 4
|
||||
}
|
||||
|
||||
if avg_a > 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
|
||||
}
|
||||
Reference in New Issue
Block a user