aboutsummaryrefslogtreecommitdiff
path: root/tools/compile_assets/aseprite/utils
diff options
context:
space:
mode:
authorXander Swan <[hidden email]>2026-01-07 23:12:22 -0500
committerXander Swan <[hidden email]>2026-01-07 23:12:22 -0500
commit0988ab832bfc7a1b1c851125b6172cf68c6d9cb9 (patch)
tree460bc2d9f0bce463af273d6b2b2c20faa880ac29 /tools/compile_assets/aseprite/utils
parentade0dc4d257d053b7064184f193f8168c496e308 (diff)
doesn't compile but i'm commiting anywya
Diffstat (limited to 'tools/compile_assets/aseprite/utils')
-rw-r--r--tools/compile_assets/aseprite/utils/animation.odin181
-rw-r--r--tools/compile_assets/aseprite/utils/blend.odin727
-rw-r--r--tools/compile_assets/aseprite/utils/common.odin303
-rw-r--r--tools/compile_assets/aseprite/utils/extract.odin613
-rw-r--r--tools/compile_assets/aseprite/utils/image.odin443
-rw-r--r--tools/compile_assets/aseprite/utils/sprite_sheet.odin382
-rw-r--r--tools/compile_assets/aseprite/utils/types.odin306
-rw-r--r--tools/compile_assets/aseprite/utils/utils_internal.odin110
8 files changed, 3065 insertions, 0 deletions
diff --git a/tools/compile_assets/aseprite/utils/animation.odin b/tools/compile_assets/aseprite/utils/animation.odin
new file mode 100644
index 0000000..7fe5059
--- /dev/null
+++ b/tools/compile_assets/aseprite/utils/animation.odin
@@ -0,0 +1,181 @@
+package aseprite_file_handler_utility
+
+
+import "core:time"
+import "core:slice"
+
+@(require) import "core:fmt"
+@(require) import "core:log"
+
+import ase ".."
+
+get_animation_from_doc :: proc(
+ doc: ^ase.Document, anim: ^Animation,
+ use_tag := "", alloc := context.allocator
+) -> (err: Errors) {
+ info: Info
+ get_info(doc, &info, alloc) or_return
+ defer destroy(&info)
+ return get_animation_from_info(info, anim, use_tag)
+}
+
+
+get_animation_from_info :: proc (
+ info: Info, anim: ^Animation, use_tag := "",
+) -> (err: Errors) {
+ context.allocator = info.allocator
+
+ s, f := 0, len(info.frames)
+ tag: Tag
+
+ if use_tag != "" {
+ for t in info.tags {
+ if t.name == use_tag {
+ if len(info.frames) < t.to || len(info.frames) < t.from {
+ return Animation_Error.Tag_Index_Out_Of_Bounds
+ }
+ tag = t
+ s = t.from
+ f = t.to+1
+ break
+ }
+ }
+
+ if tag == {} {
+ return Animation_Error.Tag_Not_Found
+ }
+ }
+
+ if anim.fps == 0 {
+ anim.fps = 30
+ }
+
+ anim.md = info.md
+ anim_frames := make([dynamic][]byte) or_return
+ defer if err != nil { delete(anim_frames) }
+
+ pos: int
+
+ for frame in info.frames[s:f] {
+ img := get_image_bytes_from_frame(frame, info) or_return
+ defer delete(img)
+
+ to_add := f64(frame.duration) * f64(anim.fps) / 1000
+ for _ in 0..<to_add {
+ append_elem(&anim_frames, slice.clone(img) or_return) or_return
+ pos += 1
+ }
+ }
+
+ if tag.direction == .Reverse || tag.direction == .Ping_Pong_Reverse {
+ slice.reverse(anim_frames[:])
+ }
+
+ if tag.direction == .Ping_Pong || tag.direction == .Ping_Pong_Reverse {
+ rev := slice.clone(anim_frames[:])
+ defer delete(rev)
+
+ slice.reverse(rev)
+ append_elems(&anim_frames, ..rev) or_return
+ }
+
+ anim.frames = anim_frames[:]
+ anim.length = time.Second * time.Duration(len(anim_frames) / anim.fps)
+ return
+}
+
+// Assumes 30 FPS & 100ms Frame Duration
+get_animation_from_images :: proc(imgs: []Image, md: Metadata, anim: ^Animation, alloc := context.allocator) -> (err: Errors) {
+ context.allocator = alloc
+ anim.fps = 30
+ anim.md = md
+ anim.length = time.Second * time.Duration(len(imgs) / 10)
+
+ frames := make([dynamic][]byte, 0, 3 * len(imgs)) or_return
+ defer if err != nil { delete(frames) }
+
+ for img in imgs {
+ frame := slice.clone(img.data) or_return
+ append_elem(&frames, frame) or_return
+
+ frame = slice.clone(img.data) or_return
+ append_elem(&frames, frame) or_return
+
+ frame = slice.clone(img.data) or_return
+ append_elem(&frames, frame) or_return
+ }
+
+ anim.frames = frames[:]
+ return
+}
+
+// Assumes 30 FPS & 100ms Frame Duration
+get_animation_from_bytes :: proc(imgs: [][]byte, md: Metadata, anim: ^Animation, alloc := context.allocator) -> (err: Errors) {
+ context.allocator = alloc
+ anim.fps = 30
+ anim.md = md
+ anim.length = time.Second * time.Duration(len(imgs) / 10)
+
+ frames := make([dynamic][]byte, 0, 3 * len(imgs)) or_return
+ defer if err != nil { delete(frames) }
+
+ for img in imgs {
+ append_elem(&frames, slice.clone(img) or_return) or_return
+ append_elem(&frames, slice.clone(img) or_return) or_return
+ append_elem(&frames, slice.clone(img) or_return) or_return
+ }
+
+ anim.frames = frames[:]
+ return
+}
+
+
+get_animation :: proc {
+ get_animation_from_doc,
+ get_animation_from_info,
+ get_animation_from_images,
+ get_animation_from_bytes,
+}
+
+
+// Returns a new image set with Red/Blue Tint or Merge Onion Skin
+apply_onion_skin :: proc(
+ imgs: []Image, opacity := 69, merge := false,
+ alloc := context.allocator
+) -> (frames: []Image, err: Errors) {
+ context.allocator = alloc
+
+ frames = make([]Image, len(imgs)) or_return
+ defer if err != nil { delete(frames) }
+
+ blank := make([]byte, len(imgs[0].data)) or_return
+ defer delete(blank)
+
+ cur, next: []byte
+ last := blank
+ defer if cur != nil { delete(cur) }
+
+ for img, pos in imgs {
+ if pos < len(imgs)-1 {
+ next = imgs[pos+1].data
+ } else {
+ next = blank
+ }
+
+ cur = slice.clone(img.data) or_return
+ if merge {
+ blend_bytes(last, cur, opacity, .Merge) or_return
+ blend_bytes(next, cur, opacity, .Merge) or_return
+ } else {
+ blend_bytes(last, cur, opacity, .Red_Tint) or_return
+ blend_bytes(next, cur, opacity, .Blue_Tint) or_return
+ }
+
+
+ frames[pos] = img
+ frames[pos].data = cur
+ last = cur
+ }
+
+ return
+}
diff --git a/tools/compile_assets/aseprite/utils/blend.odin b/tools/compile_assets/aseprite/utils/blend.odin
new file mode 100644
index 0000000..9bf0f7e
--- /dev/null
+++ b/tools/compile_assets/aseprite/utils/blend.odin
@@ -0,0 +1,727 @@
+package aseprite_file_handler_utility
+
+import "core:math"
+
+@(require) import "core:fmt"
+@(require) import "core:log"
+
+
+ASE_USE_BUGGED_SAT :: #config(ASE_USE_BUGGED_SAT, false)
+
+
+// https://printtechnologies.org/standards/files/pdf-reference-1.6-addendum-blend-modes.pdf
+
+@(private)
+slow_alpha :: proc(a: int, b: ..int) -> (res: int) {
+ // α = α1 * α2 *..αn / 255^(n-1)
+ if len(b) == 0 { return a }
+ if len(b) == 1 { return a * b[0] / 255}
+ res = a
+ d := 255
+ for i in b {
+ res *= i
+ d *= 255
+ }
+ return res / d
+}
+
+
+// Modifies current Image (`cur`)
+blend_images :: proc(last, cur: Image, opacity: int, mode: Blend_Mode) -> (err: Blend_Error) {
+ if len(last.data) != len(cur.data) {
+ return Blend_Error.Unequal_Image_Sizes
+ }
+ return blend_bytes(last.data, cur.data, opacity, mode)
+}
+
+// Modifies current Image (`cur`)
+blend_bytes :: proc(last, cur: []byte, opacity: int, mode: Blend_Mode) -> (err: Blend_Error) {
+ if len(last) != len(cur) {
+ return Blend_Error.Unequal_Image_Sizes
+ }
+
+ for pix in 0..<len(cur)/4 {
+ pos := pix * 4
+ l_pix, c_pix: [4]u8
+
+ copy(l_pix[:], last[pos:pos+4])
+ copy(c_pix[:], cur[pos:pos+4])
+
+ r_pix := blend(l_pix, c_pix, i32(opacity), mode) or_return
+
+ copy(cur[pos:pos+4], r_pix[:])
+ }
+ return
+}
+
+
+alpha :: mul
+mul :: proc{mul_u8, mul_i32, mul_int, mul_vec4, mul_vec3}
+
+mul_vec4 :: proc(a, b: [4]i32) -> [4]i32 {
+ t := a * b + 128
+ return {
+ ((t.r >> 8) + t.r) >> 8,
+ ((t.g >> 8) + t.g) >> 8,
+ ((t.b >> 8) + t.b) >> 8,
+ ((t.a >> 8) + t.a) >> 8,
+ }
+}
+
+mul_vec3 :: proc(a, b: [3]i32) -> [3]i32 {
+ t := a * b + 128
+ return {
+ ((t.r >> 8) + t.r) >> 8,
+ ((t.g >> 8) + t.g) >> 8,
+ ((t.b >> 8) + t.b) >> 8,
+ }
+}
+
+
+mul_i32 :: proc(a, b: i32) -> i32 {
+ // License `.\3rd party licenses\libpixman license`
+ // https://github.com/libpixman/pixman/blob/master/pixman/pixman-combine32.h#L67
+
+ t := a * b + 128
+ return ((t >> 8 ) + t ) >> 8
+}
+
+mul_int :: proc(a, b: int) -> i32 {
+ // License `.\3rd party licenses\libpixman license`
+ // https://github.com/libpixman/pixman/blob/master/pixman/pixman-combine32.h#L67
+
+ t := a * b + 128
+ return i32(((t >> 8 ) + t ) >> 8)
+}
+
+mul_u8 :: proc(a, b: byte) -> i32 {
+ // License `.\3rd party licenses\libpixman license`
+ // https://github.com/libpixman/pixman/blob/master/pixman/pixman-combine32.h#L67
+
+ t := a * b + 128
+ return i32(((t >> 8 ) + t ) >> 8)
+}
+
+
+div :: proc( #any_int a, b: u16) -> u16 {
+ // License `.\3rd party licenses\libpixman license`
+ // https://github.com/libpixman/pixman/blob/master/pixman/pixman-combine32.h#L70
+ return (a * 255 + (b / 2)) / b
+}
+
+blend :: proc(last, cur: Pixel, opacity: i32, mode: Blend_Mode) -> (res: Pixel, err: Blend_Error) {
+ // https://github.com/aseprite/aseprite/blob/main/src/doc/blend_funcs.cpp
+ if last.a == 0 {
+ res.rgb = cur.rgb
+ res.a = byte(mul(i32(cur.a), opacity))
+ return
+ }
+
+ back: [4]i32 = {i32(last.r), i32(last.g), i32(last.b), i32(last.a)}
+ pix: [4]i32 = {i32(cur.r), i32(cur.g), i32(cur.b), i32(cur.a)}
+
+ blen: [4]i32
+ switch mode {
+ case .Src: blen = blend_src(back, pix, opacity)
+ case .Merge: blen = blend_merge(back, pix, opacity)
+ case .Neg_BW: blen = blend_neg_bw(back, pix, opacity)
+ case .Red_Tint: blen = blend_red_tint(back, pix, opacity)
+ case .Blue_Tint: blen = blend_blue_tint(back, pix, opacity)
+ case .Dst_Over: blen = blend_normal_dst_over(back, pix, opacity)
+ case .Addition: blen = blend_addition(back, pix, opacity)
+ case .Color: blen = blend_color(back, pix, opacity)
+ case .Color_Burn: blen = blend_color_burn(back, pix, opacity)
+ case .Color_Dodge: blen = blend_color_dodge(back, pix, opacity)
+ case .Darken: blen = blend_darken(back, pix, opacity)
+ case .Difference: blen = blend_difference(back, pix, opacity)
+ case .Divide: blen = blend_divide(back, pix, opacity)
+ case .Exclusion: blen = blend_exclusion(back, pix, opacity)
+ case .Hard_Light: blen = blend_hard_light(back, pix, opacity)
+ case .Hue: blen = blend_hue(back, pix, opacity)
+ case .Lighten: blen = blend_lighten(back, pix, opacity)
+ case .Luminosity: blen = blend_luminosity(back, pix, opacity)
+ case .Multiply: blen = blend_multiply(back, pix, opacity)
+ case .Normal: blen = blend_normal(back, pix, opacity)
+ case .Overlay: blen = blend_overlay(back, pix, opacity)
+ case .Saturation: blen = blend_saturation(back, pix, opacity)
+ case .Screen: blen = blend_screen(back, pix, opacity)
+ case .Soft_Light: blen = blend_soft_light(back, pix, opacity)
+ case .Subtract: blen = blend_subtract(back, pix, opacity)
+ case .Unspecified: fallthrough
+ case:
+ log.error("Invalid Bland Mode provided:", mode)
+ return last, .Invalid_Mode
+ }
+
+ normal := blend_normal(back, pix, opacity)
+ norm_merge := blend_merge(normal, blen, back.a)
+ blen = blend_merge(norm_merge, blen, alpha(back.a, alpha(pix.a, opacity)))
+
+ return {u8(blen.r), u8(blen.g), u8(blen.b), u8(blen.a)}, nil
+}
+
+
+/* ------------------------------------------------------------------- */
+// RGB Blenders
+blend_normal :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ if last.a == 0 {
+ res = cur
+ res.a = alpha(cur.a, opacity)
+ } else if cur.a == 0 {
+ return last
+ }
+
+ cur := cur
+ cur.a = alpha(cur.a, opacity)
+
+ res.a = cur.a + last.a - alpha(last.a, cur.a)
+ res.rgb = last.rgb + (cur.rgb - last.rgb) * cur.a / res.a
+
+ return res
+}
+
+blend_src :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ return last
+}
+
+blend_merge :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+
+ if last.a == 0 {
+ res.rgb = cur.rgb
+
+ } else if cur.a == 0 {
+ res.rgb = last.rgb
+
+ } else {
+ op: [4]i32 = opacity
+ res = last + mul((cur - last), op)
+ }
+
+ res.a = last.a + mul((cur.a - last.a), opacity)
+ if res.a == 0 {
+ res.rgb = 0
+ }
+
+ return
+}
+
+blend_neg_bw :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ if rgba_luma(last) < 128{
+ return { 255, 255, 255, 255 }
+ }
+ return { 0, 0, 0, 255 }
+}
+
+blend_red_tint :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ cur := cur
+ luma := rgba_luma(cur)
+ cur = { (255 + luma) >> 1, luma >> 1, luma >> 1, last.a }
+ return blend_normal(last, cur, opacity)
+}
+
+blend_blue_tint :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ luma := rgba_luma(cur)
+ res = { luma >> 1, luma >> 1, (255 + luma) >> 1, last.a }
+ return blend_normal(last, res, opacity)
+}
+
+blend_normal_dst_over :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ res.a = alpha(cur.a, opacity)
+ return blend_normal(last, res, opacity)
+}
+
+
+blend_multiply :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ res = mul(last, cur)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_screen :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ res = last + cur - mul(last, cur)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_overlay :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ hl :: proc(b, s: i32) -> i32 {
+ if s < 128 {
+ return mul(b, s<<1)
+ }
+ return b + ((s<<1)-255) - mul(b, (s<<1)-255)
+ }
+
+ res.r = hl(cur.r, last.r)
+ res.b = hl(cur.b, last.b)
+ res.g = hl(cur.g, last.g)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_darken :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ res.r = min(last.r, cur.r)
+ res.b = min(last.b, cur.b)
+ res.g = min(last.g, cur.g)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_lighten :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ res.r = max(last.r, cur.r)
+ res.b = max(last.b, cur.b)
+ res.g = max(last.g, cur.g)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_color_dodge :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ cd :: proc( #any_int b, s: u32) -> i32 {
+ if b == 0 {
+ return 0
+ }
+ s1 := 255 - s
+ if b >= s1 {
+ return 255
+ }
+ return i32(div(b, s1))
+ }
+
+ res.r = cd(last.r, cur.r)
+ res.b = cd(last.b, cur.b)
+ res.g = cd(last.g, cur.g)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_color_burn :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ cb :: proc( #any_int b, s: u32) -> i32 {
+ if b == 255 {
+ return 255
+ }
+ b1 := 255 - b
+ if b1 >= s {
+ return 0
+ }
+ return i32(255 - div(b1, s))
+ }
+
+ res.r = cb(last.r, cur.r)
+ res.b = cb(last.b, cur.b)
+ res.g = cb(last.g, cur.g)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_hard_light :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ hl :: proc(b, s: i32) -> i32 {
+ if s < 128 {
+ return mul(b, s<<1)
+ }
+ return b + ((s<<1)-255) - mul(b, (s<<1)-255)
+ }
+
+ res.r = hl(last.r, cur.r)
+ res.b = hl(last.b, cur.b)
+ res.g = hl(last.g, cur.g)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_soft_light :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ sl :: proc(b, s: i32) -> i32 {
+ b := f64(b) / 255
+ s := f64(s) / 255
+ r, d: f64
+
+ if b <= 0.25 {
+ d = ((16 * b - 12) * b + 4) * b
+ } else {
+ d = math.sqrt(b)
+ }
+
+ if s <= 0.5 {
+ r = b - (1 - 2 * s) * b * (1 - b)
+ } else {
+ r = b + (2 * s - 1) * (d - b)
+ }
+ return i32(r * 255 + 0.5)
+ }
+
+ res.r = sl(last.r, cur.r)
+ res.b = sl(last.b, cur.b)
+ res.g = sl(last.g, cur.g)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_difference :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ res.r = abs(last.r - cur.r)
+ res.b = abs(last.b - cur.b)
+ res.g = abs(last.g - cur.g)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_exclusion :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ ex :: proc(b, s: i32) -> i32 {
+ return b + s - (mul(b, s) << 1)
+ }
+
+ res.r = ex(last.r, cur.r)
+ res.b = ex(last.b, cur.b)
+ res.g = ex(last.g, cur.g)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+
+
+/* ------------------------------------------------------------------- */
+// HSV Blenders
+blend_hue :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ /*
+ https://github.com/alpine-alpaca/asefile/blob/main/src/blend.rs#L392
+ https://drafts.fxtf.org/compositing-1/#blendinghue
+ https://printtechnologies.org/standards/files/pdf-reference-1.6-addendum-blend-modes.pdf
+ https://github.com/aseprite/aseprite/blob/main/src/doc/blend_funcs.cpp#L425
+ https://gitlab.freedesktop.org/pixman/pixman/-/blob/master/pixman/pixman-combine-float.c?ref_type=heads#L908
+ */
+
+ lpix := [3]f64{f64(last.r), f64(last.g), f64(last.b)} / 255
+ sat := hsv_sat(lpix)
+ lum := hsv_luma(lpix)
+
+ cpix := [3]f64{f64(cur.r), f64(cur.g), f64(cur.b)} / 255
+ cpix = set_sat(cpix, sat)
+ cpix = set_luma(cpix, lum)
+ cpix *= 255
+
+ return blend_normal(last, {i32(cpix.r), i32(cpix.g), i32(cpix.b), cur.a}, opacity)
+}
+
+
+blend_saturation :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ pix := [3]f64{ f64(cur.r), f64(cur.g), f64(cur.b) }
+ pix /= 255
+ s := hsv_sat(pix)
+
+ pix = { f64(last.r), f64(last.g), f64(last.b) }
+ pix /= 255
+ l := hsv_luma(pix)
+
+ pix = set_luma(set_sat(pix, s), l)
+
+ pix *= 255
+ res = { i32(pix.r), i32(pix.g), i32(pix.b), cur.a }
+
+ return blend_normal(last, res, opacity)
+}
+
+
+blend_color :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ pix := [3]f64{ f64(last.r), f64(last.g), f64(last.b) }
+ pix /= 255
+ l := hsv_luma(pix)
+
+ pix = { f64(cur.r), f64(cur.g), f64(cur.b)}
+ pix /= 255
+ pix = set_luma(pix, l)
+
+ pix *= 255
+ res = { i32(pix.r), i32(pix.g), i32(pix.b), cur.a }
+
+ return blend_normal(last, res, opacity)
+}
+
+
+blend_luminosity :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ pix := [3]f64{ f64(cur.r), f64(cur.g), f64(cur.b) }
+ pix /= 255
+ l := hsv_luma(pix)
+
+ pix = { f64(last.r), f64(last.g), f64(last.b) }
+ pix /= 255
+ pix = set_luma(pix, l)
+
+ pix *= 255
+ res = { i32(pix.r), i32(pix.g), i32(pix.b), cur.a }
+
+ return blend_normal(last, res, opacity)
+}
+
+
+blend_addition :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ res.r = min(last.r + cur.r, 255)
+ res.g = min(last.g + cur.g, 255)
+ res.b = min(last.b + cur.b, 255)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_subtract :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ res.r = max(last.r - cur.r, 0)
+ res.g = max(last.g - cur.g, 0)
+ res.b = max(last.b - cur.b, 0)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_divide :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ bd :: proc( #any_int b, s: u32) -> i32 {
+ if b == 0 {
+ return 0
+ } else if b >= s {
+ return 255
+ }
+ return i32(div(b, s))
+ }
+
+ res.r = bd( last.r, cur.r )
+ res.g = bd( last.g, cur.g )
+ res.b = bd( last.b, cur.b )
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+
+
+/* ------------------------------------------------------------------- */
+// RGB Helpers
+
+rgba_luma :: proc(pix: B_Pixel) -> i32 {
+ return rgb_luma(pix.r, pix.b, pix.g)
+}
+
+rgb_luma :: proc(#any_int r, b, g: int) -> i32 {
+ return i32(( r*2126 + g*7152 + b*722 ) / 10000 )
+}
+
+/* ------------------------------------------------------------------- */
+// HSV Helpers
+
+hsv_luma :: proc(p: [3]f64) -> f64 {
+ return 0.3*p.r + 0.59*p.g + 0.11*p.b
+}
+
+hsv_sat :: proc(p: [3]f64) -> f64 {
+ return max(p.r, p.g, p.b) - min(p.r, p.g, p.b)
+}
+
+clip_color :: proc(pix: [3]f64) -> [3]f64 {
+ p := pix
+ lum := hsv_luma(p)
+ n := min(p.r, p.g, p.b)
+ x := max(p.r, p.g, p.b)
+
+ if n < 0 {
+ p = lum + (((p - lum) * lum) / (lum - n))
+ }
+
+ if x > 1 {
+ p = lum + (((p - lum) * (1 - lum)) / (x - lum))
+ }
+
+ return p
+}
+
+set_luma :: proc(p: [3]f64, l: f64) -> [3]f64 {
+ d := l - hsv_luma(p)
+ return clip_color(p + d)
+}
+
+
+set_sat :: proc(p: [3]f64, s: f64) -> (res: [3]f64) {
+ // https://github.com/aseprite/aseprite/blob/main/src/doc/blend_funcs.cpp#L400
+ // The chosen solution is a mix a asefile & pixman
+ // https://github.com/alpine-alpaca/asefile/blob/main/src/blend.rs#L522
+ // https://gitlab.freedesktop.org/pixman/pixman/-/blob/master/pixman/pixman-combine-float.c#L831
+
+
+ when ASE_USE_BUGGED_SAT == true {
+ res = p
+ MIN :: proc(x, y: ^f64) -> ^f64 {
+ return (x^ < y^) ? x : y
+ }
+ MAX :: proc(x, y: ^f64) -> ^f64 {
+ return (x^ > y^) ? x : y
+ }
+
+ r, g, b := &res.r, &res.g, &res.b
+ min := MIN(r, MIN(g, b))
+ max := MAX(r, MAX(g, b))
+ mid := r > g ? (g > b ? g : (r > b ? b : r)) : (g > b ? (b > r ? b : r): g)
+
+ if max > min {
+ mid^ = ((mid^ - min^) * s) / (max^ - min^)
+ max^ = s
+ } else {
+ mid^ = 0
+ max^ = 0
+ }
+ min^ = 0
+
+ } else {
+ val: [3]struct{v: f64, p: int} = {
+ {p.r, 0}, {p.g, 1}, {p.b, 2},
+ }
+ val.rg = val.r.v < val.g.v ? val.rg : val.gr
+ val.rb = val.r.v < val.b.v ? val.rb : val.br
+ val.gb = val.g.v < val.b.v ? val.gb : val.bg
+ min, mid, max := val.r.p, val.g.p, val.b.p
+
+ if (p[max] - p[min]) != 0 {
+ res[mid] = ( (p[mid] - p[min]) * s ) / ( p[max] - p[min] )
+ res[max] = s
+ }
+ }
+
+ return
+}
+
+
+/*
+ The following is for mine (blob) record keeping:
+
+ The way aseprite does set_set is very much bugged.
+ First the sorting doesn't work when if r == g and g < b
+
+ MIN :: proc(x, y: ^f64) -> ^f64 {
+ return (x^ < y^) ? x : y
+ }
+ MAX :: proc(x, y: ^f64) -> ^f64 {
+ return (y^ < x^) ? x : y
+ }
+
+ r, g, b := &pix.r, &pix.g, &pix.b
+ min := MIN(r, MIN(g, b))
+ max := MAX(r, MAX(g, b))
+ mid := r > g ? (g > b ? g : (r > b ? b : r)) : (g > b ? (b > r ? b : r): g)
+
+ Second the way it's value calculations is also wrong & only leaves the blue channel
+ if max > min {
+ mid^ = ((mid^ - min^) * s) / (max^ - min^)
+ max^ = s
+ } else {
+ mid^ = 0
+ max^ = 0
+ }
+
+ The sullotion griven by asefile
+ // r --*--*----- min
+ // | |
+ // g --*--|--*-- mid
+ // | |
+ // b -----*--*-- max
+
+ res = p
+ val := [3][2]f64{{p.r, 0}, {p.g, 1}, {p.b, 2}}
+
+ val.rg = val.r[0] < val.g[0] ? val.rg : val.gr
+ val.rb = val.r[0] < val.b[0] ? val.rb : val.br
+ val.gb = val.g[0] < val.b[0] ? val.gb : val.bg
+ min, mid, max := int(val.r[1]), int(val.g[1]), int(val.b[1])
+
+
+ if max > min {
+ res[mid] = ((res[mid] - res[min]) * s) / (res[max] - res[min])
+ res[max] = s
+ } else {
+ res[mid], res[max] = 0, 0
+ }
+ res[min] = 0
+
+ Pointer based version og working sullotion from pixman
+ res = p
+ min, mid, max: ^f64
+ if p.r > p.g {
+ if p.r > p.b {
+ max = &res.r
+ if p.g > p.b {
+ mid = &res.g
+ min = &res.b
+
+ } else {
+ mid = &res.b
+ min = &res.g
+ }
+
+ } else {
+ max = &res.b
+ mid = &res.r
+ min = &res.g
+ }
+
+ } else {
+ if p.r > p.b {
+ max = &res.g
+ mid = &res.r
+ min = &res.b
+
+ } else {
+ min = &res.r
+ if p.g > p.b {
+ max = &res.g
+ mid = &res.b
+
+ } else {
+ max = &res.b
+ mid = &res.g
+ }
+ }
+ }
+
+
+ if (max^ - min^) == 0 {
+ mid^ = 0
+ max^ = 0
+ } else {
+ mid^ = ((mid^ - min^) * s) / (max^ - min^)
+ max^ = s
+ }
+ min^ = 0
+
+
+ Pointerless pixman
+ min, mid, max: int
+ if p.r > p.g {
+ if p.r > p.b {
+ max = 0
+ if p.g > p.b {
+ mid = 1
+ min = 2
+
+ } else {
+ mid = 2
+ min = 1
+ }
+
+ } else {
+ max = 2
+ mid = 0
+ min = 1
+ }
+
+ } else {
+ if p.r > p.b {
+ max = 1
+ mid = 0
+ min = 2
+
+ } else {
+ min = 0
+ if p.g > p.b {
+ max = 1
+ mid = 2
+
+ } else {
+ max = 2
+ mid = 1
+ }
+ }
+ }
+
+ if (p[max] - p[min]) != 0 {
+ res[mid] = ((p[mid] - p[min]) * s) / (p[max] - p[min])
+ res[max] = s
+ }
+*/
diff --git a/tools/compile_assets/aseprite/utils/common.odin b/tools/compile_assets/aseprite/utils/common.odin
new file mode 100644
index 0000000..69d7c71
--- /dev/null
+++ b/tools/compile_assets/aseprite/utils/common.odin
@@ -0,0 +1,303 @@
+package aseprite_file_handler_utility
+
+import "base:runtime"
+import "core:mem"
+import "core:slice"
+import "core:image"
+
+@(require) import "core:fmt"
+@(require) import "core:log"
+
+import ase ".."
+
+
+UTILS_DEBUG_MODE :: #config(ASE_UTILS_DEBUG, ODIN_DEBUG)
+
+
+/* ================================== Destruction procs ================================== */
+destroy_frame :: proc(frame: Frame, alloc := context.allocator) -> runtime.Allocator_Error {
+ for &cel in frame.cels {
+ destroy(&cel, alloc) or_return
+ }
+ return delete(frame.cels, alloc)
+}
+
+destroy_frames :: proc(frames: []Frame, alloc := context.allocator) -> runtime.Allocator_Error {
+ for frame in frames {
+ for &cel in frame.cels {
+ destroy(&cel, alloc) or_return
+ }
+ delete(frame.cels, alloc) or_return
+ }
+ return delete(frames, alloc)
+}
+
+destroy_image :: proc(img: Image, alloc := context.allocator) -> runtime.Allocator_Error {
+ return delete(img.data, alloc)
+}
+
+destroy_images :: proc(imgs: []Image, alloc := context.allocator) -> runtime.Allocator_Error {
+ for img in imgs {
+ delete(img.data, alloc) or_return
+ }
+ return delete(imgs, alloc)
+}
+
+destroy_image_bytes :: proc(imgs: [][]byte, alloc := context.allocator) -> runtime.Allocator_Error {
+ for img in imgs {
+ delete(img, alloc) or_return
+ }
+ return delete(imgs, alloc)
+}
+
+destroy_animation :: proc(anim: ^Animation, alloc := context.allocator) -> runtime.Allocator_Error {
+ for frame in anim.frames {
+ delete(frame, alloc) or_return
+ }
+ return delete(anim.frames, alloc)
+}
+
+destroy_info :: proc(info: ^Info) -> runtime.Allocator_Error {
+ context.allocator = info.allocator
+ destroy_frames(info.frames) or_return
+ delete(info.layers) or_return
+ delete(info.palette) or_return
+ delete(info.tags) or_return
+ delete(info.tilesets) or_return
+ destroy_slices(info.slices) or_return
+ return nil
+}
+
+destroy_cel :: proc(cel: ^Cel, alloc := context.allocator) -> runtime.Allocator_Error {
+ return delete(cel.tilemap.tiles, alloc)
+}
+
+destroy_layers :: proc(lay: []Layer, alloc := context.allocator) -> runtime.Allocator_Error {
+ return delete(lay, alloc)
+}
+
+destroy_palette :: proc(pal: Palette, alloc := context.allocator) -> runtime.Allocator_Error {
+ return delete(pal)
+}
+
+destroy_slices :: proc(sls: []Slice, alloc := context.allocator) -> runtime.Allocator_Error {
+ for sl in sls {
+ destroy_slice(sl, alloc) or_return
+ }
+ return delete(sls)
+}
+
+destroy_slice :: proc(sl: Slice, alloc := context.allocator) -> runtime.Allocator_Error {
+ return delete(sl.keys)
+}
+
+destroy_sheet :: proc(s: Sprite_Sheet, alloc := context.allocator) -> runtime.Allocator_Error {
+ return destroy_image(s.img, alloc)
+}
+
+destroy :: proc {
+ destroy_frames,
+ destroy_image,
+ destroy_animation,
+ destroy_image_bytes,
+ destroy_images,
+ destroy_info,
+ destroy_cel,
+ destroy_frame,
+ destroy_layers,
+ destroy_palette,
+ destroy_slices,
+ destroy_slice,
+ destroy_sheet,
+}
+/* ======================================================================================= */
+
+
+get_metadata :: proc(header: ase.File_Header) -> (md: Metadata) {
+ return {
+ int(header.width),
+ int(header.height),
+ Pixel_Depth(header.color_depth),
+ u8(header.transparent_index),
+ }
+}
+
+
+// Use with slice.sort_by, .sort_by_with_indices, .stable_sort_by, .is_sorted_by & .reverse_sort_by
+cel_less :: proc(i, j: Cel) -> bool {
+ // https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md#note5
+ ior := i.layer + i.z_index
+ jor := j.layer + j.z_index
+ return ior < jor || (ior == jor && i.z_index < j.z_index )
+}
+
+// Use with slice.sort_by_cmp, .stable_sort_by_cmp, .is_sorted_cmp & .reverse_sort_by_cmp
+cel_cmp :: proc(i, j: Cel) -> slice.Ordering {
+ // https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md#note5
+ iz := i.z_index
+ jz := j.z_index
+ ior := i.layer + iz
+ jor := j.layer + jz
+
+ if ior < jor { return .Less }
+ else if ior > jor { return .Greater }
+ else if iz < jz { return .Less }
+ else if iz > jz { return .Greater }
+ return .Equal
+}
+
+
+has_new_palette :: proc(doc: ^ase.Document) -> bool {
+ for f in doc.frames {
+ for c in f.chunks {
+ _ = c.(ase.Palette_Chunk) or_continue
+ return true
+ }
+ }
+ return false
+}
+
+
+has_tileset :: proc(doc: ^ase.Document) -> bool {
+ for f in doc.frames {
+ for c in f.chunks {
+ _ = c.(ase.Tileset_Chunk) or_continue
+ return true
+ }
+ }
+ return false
+}
+
+
+// Uses Nearest-neighbor Upscaling
+upscale_image_from_bytes :: proc(img: []byte, md: Metadata, factor := 10, alloc := context.allocator) -> (res: []byte, res_md: Metadata, err: Errors) {
+ ch := int(md.bpp) >> 3
+ if len(img) != (md.width * md.height * ch) {
+ fast_log(.Error, "image size doesn't match metadata")
+ err = .Buffer_Size_Not_Match_Metadata
+ return
+ }
+
+ res = make([]byte, len(img) * factor * factor, alloc) or_return
+ res_md = {md.width*factor, md.height*factor, md.bpp, md.trans_idx}
+
+ for h in 0..<md.height {
+ for w in 0..<md.width {
+ start := (h*md.width*factor + w) * factor * ch
+ first := res[start:start + factor * ch]
+
+ copy(first[:ch], img[(h*md.width + w) * ch:])
+
+ for x in 1..<factor {
+ copy(res[start + x * ch:][:ch], first)
+ }
+
+ for y in 1..<factor {
+ copy(res[start + (y*md.width*factor*ch):], first)
+ }
+ }
+ }
+
+ return
+}
+
+// Uses Nearest-neighbor Upscaling
+upscale_image_from_img :: proc(img: Image, factor := 10, alloc := context.allocator) -> (res: Image, err: Errors) {
+ res.data, res.md = upscale_image_from_bytes(img.data, img.md, factor, alloc) or_return
+ return
+}
+
+// Uses Nearest-neighbor Upscaling
+upscale_image :: proc { upscale_image_from_img, upscale_image_from_bytes }
+
+
+upscale_all_from_imgs :: proc(imgs: []Image, factor := 10, alloc := context.allocator) -> (res: []Image, err: Errors) {
+ res = make([]Image, len(imgs), alloc) or_return
+ for img, pos in imgs {
+ res[pos] = upscale_image_from_img(img, factor, alloc) or_return
+ }
+ return
+}
+
+upscale_all_from_byte:: proc(imgs: [][]byte, md: Metadata, factor := 10, alloc := context.allocator) -> (res: [][]byte, res_md: Metadata, err: Errors) {
+ res = make([][]byte, len(imgs), alloc) or_return
+ if len(imgs) == 0 { return }
+ res[0], res_md = upscale_image_from_bytes(imgs[0], md, factor, alloc) or_return
+
+ for img, pos in imgs[1:] {
+ res[pos], _ = upscale_image_from_bytes(img, md, factor, alloc) or_return
+ }
+ return
+}
+
+upscale_all :: proc{ upscale_all_from_imgs, upscale_all_from_byte }
+
+
+compute_alpha :: proc(img: []u8, alloc := context.allocator) -> (res: []u8, err: Errors) {
+ if len(img) % 4 != 0 {
+ fast_log(.Error, "Given buffer isn't RGBA")
+ return nil, .Buffer_Not_RGBA
+ }
+
+ img_buf := mem.slice_data_cast([][4]u8, img)
+ buf := make([][3]u8, len(img_buf), alloc) or_return
+
+ for p, pos in img_buf {
+ bp := [4]i32{ i32(p.r), i32(p.g), i32(p.b), i32(p.a) }
+ bp.rgb = bp.rgb * bp.a / (255 + bp.a - alpha(bp.a, 255))
+ buf[pos] = { u8(bp.r), u8(bp.g), u8(bp.b) }
+ }
+
+ return mem.slice_data_cast([]u8, buf), nil
+}
+
+
+remove_alpha :: proc(img: []u8, alloc := context.allocator) -> (res: []u8, err: Errors) {
+ if len(img) % 4 != 0 {
+ fast_log(.Error, "Given buffer isn't RGBA")
+ return nil, .Buffer_Not_RGBA
+ }
+
+ img_buf := mem.slice_data_cast([][4]u8, img)
+ buf := make([][3]u8, len(img_buf), alloc) or_return
+
+ for p, pos in img_buf {
+ buf[pos] = p.rgb
+ }
+
+ return mem.slice_data_cast([]u8, buf), nil
+}
+
+
+// Converts `utils.Image` to a `core:image.Image` with allocation
+to_core_image :: proc(buf: []byte, md: Metadata, alloc := context.allocator) -> (img: image.Image, err: runtime.Allocator_Error) {
+ img.width = md.width
+ img.height = md.height
+ img.depth = 8
+ img.channels = 4
+ img.pixels.buf = make([dynamic]byte, len(buf), alloc) or_return
+
+ copy(img.pixels.buf[:], buf)
+ return
+}
+
+// Converts `utils.Image` to a `core:image.Image` with no allocation
+to_core_image_non_alloc :: proc(buf: []byte, md: Metadata) -> (img: image.Image) {
+ img.width = md.width
+ img.height = md.height
+ img.depth = 8
+ img.channels = 4
+
+ raw := runtime.Raw_Dynamic_Array {
+ data = raw_data(buf),
+ len = len(buf),
+ cap = len(buf),
+ allocator = runtime.nil_allocator(),
+ }
+
+ img.pixels.buf = transmute([dynamic]byte)raw
+ return
+}
+
+
+
diff --git a/tools/compile_assets/aseprite/utils/extract.odin b/tools/compile_assets/aseprite/utils/extract.odin
new file mode 100644
index 0000000..f1b009d
--- /dev/null
+++ b/tools/compile_assets/aseprite/utils/extract.odin
@@ -0,0 +1,613 @@
+package aseprite_file_handler_utility
+
+import "base:runtime"
+import "core:math/fixed"
+
+@(require) import "core:fmt"
+@(require) import "core:log"
+
+import ase ".."
+
+
+cels_from_doc :: proc(doc: ^ase.Document, alloc := context.allocator) -> (res: []Cel, err: runtime.Allocator_Error) {
+ cels := make([dynamic]Cel, alloc) or_return
+ defer if err != nil { delete(cels) }
+
+ for frame in doc.frames {
+ f_cels := get_cels(frame, alloc) or_return
+ for &c in f_cels {
+ if c.raw == nil && c.tilemap.tiles == nil {
+ for l in cels[c.link:] {
+ if l.layer == c.layer {
+ c.height = l.height
+ c.width = l.width
+ c.raw = l.raw
+ }
+ }
+ }
+ }
+
+ append(&cels, ..f_cels) or_return
+ delete(f_cels, alloc) or_return
+ }
+
+ return cels[:], nil
+}
+
+cels_from_doc_frame :: proc(frame: ase.Frame, alloc := context.allocator) -> (res: []Cel, err: runtime.Allocator_Error) {
+ cels := make([dynamic]Cel, alloc) or_return
+ defer if err != nil { delete(cels) }
+
+ for chunk in frame.chunks {
+ #partial switch c in chunk {
+ case ase.Cel_Chunk:
+ cel := Cel {
+ pos = { int(c.x), int(c.y) },
+ opacity = int(c.opacity_level),
+ z_index = int(c.z_index),
+ layer = int(c.layer_index),
+ }
+
+ switch v in c.cel {
+ case ase.Com_Image_Cel:
+ cel.width = int(v.width)
+ cel.height = int(v.height)
+ cel.raw = v.pixels
+
+ case ase.Raw_Cel:
+ cel.width = int(v.width)
+ cel.height = int(v.height)
+ cel.raw = v.pixels
+
+ case ase.Linked_Cel:
+ cel.link = int(v)
+
+ case ase.Com_Tilemap_Cel:
+
+ cel.tilemap = Tilemap {
+ width = int(v.width),
+ height = int(v.height),
+ x_flip = uint(v.bitmask_x), // Bitmask for X flip
+ y_flip = uint(v.bitmask_y), // Bitmask for Y flip
+ diag_flip = uint(v.bitmask_diagonal), // Bitmask for diagonal flip (swap X/Y axis)
+ tiles = make([]int, len(v.tiles), alloc) or_return,
+ }
+
+ for &n, p in cel.tilemap.tiles {
+ switch t in v.tiles[p] {
+ case ase.BYTE: n = int(t)
+ case ase.WORD: n = int(t)
+ case ase.DWORD: n = int(t)
+ }
+ }
+
+ }
+ append(&cels, cel) or_return
+
+ case ase.Cel_Extra_Chunk:
+ if ase.Cel_Extra_Flag.Precise in c.flags {
+ extra := Precise_Bounds {
+ fixed.to_f64(c.x), fixed.to_f64(c.y),
+ fixed.to_f64(c.width), fixed.to_f64(c.height),
+ }
+ cels[len(cels)-1].extra = extra
+ }
+ }
+ }
+
+ return cels[:], nil
+}
+
+get_cels :: proc{ cels_from_doc_frame, cels_from_doc }
+
+
+layers_from_doc :: proc(doc: ^ase.Document, alloc := context.allocator) -> (res: []Layer, err: runtime.Allocator_Error) {
+ layers := make([dynamic]Layer, alloc) or_return
+ defer if err != nil { delete(layers) }
+
+ for frame in doc.frames {
+ f_lays := get_layers(frame, .Layer_Opacity in doc.header.flags) or_return
+ append(&layers, ..f_lays) or_return
+ delete(f_lays, alloc) or_return
+ }
+
+ return layers[:], nil
+}
+
+layers_from_doc_frame :: proc(frame: ase.Frame, layer_valid_opacity := false, alloc := context.allocator) -> (res: []Layer, err: runtime.Allocator_Error) {
+ layers := make([dynamic]Layer, alloc) or_return
+ defer if err != nil { delete(layers) }
+
+ all_lays := make([dynamic]^ase.Layer_Chunk) or_return
+ defer delete(all_lays)
+
+ for chunk in frame.chunks {
+ #partial switch &v in chunk {
+ case ase.Layer_Chunk:
+ lay := Layer {
+ name = v.name,
+ opacity = int(v.opacity) if layer_valid_opacity else 255,
+ index = len(layers),
+ blend_mode = Blend_Mode(v.blend_mode),
+ visiable = .Visiable in v.flags,
+ tileset = int(v.tileset_index),
+ is_background = .Background in v.flags,
+ }
+
+ #reverse for l in all_lays {
+ if l.type == .Group {
+ if .Visiable not_in l.flags {
+ lay.visiable = false
+ break
+ }
+ if l.child_level == 0 {
+ break
+ }
+ }
+ }
+
+ append(&all_lays, &v) or_return
+ append(&layers, lay) or_return
+ }
+ }
+
+ return layers[:], nil
+}
+
+get_layers :: proc{ layers_from_doc_frame, layers_from_doc }
+
+
+tags_from_doc :: proc(doc: ^ase.Document, alloc := context.allocator) -> (res: []Tag, err: runtime.Allocator_Error) {
+ tags := make([dynamic]Tag, alloc)
+ defer if err != nil { delete(tags) }
+
+ for frame in doc.frames {
+ f_tags := get_tags(frame, alloc) or_return
+ append(&tags, ..f_tags) or_return
+ delete(f_tags, alloc) or_return
+ }
+
+ return tags[:], nil
+}
+
+tags_from_doc_frame :: proc(frame: ase.Frame, alloc := context.allocator) -> (res: []Tag, err: runtime.Allocator_Error) {
+ tags := make([dynamic]Tag, alloc) or_return
+ defer if err != nil { delete(tags) }
+
+ for chunk in frame.chunks {
+ #partial switch v in chunk {
+ case ase.Tags_Chunk:
+ for t in v {
+ tag := Tag {
+ from = int(t.from_frame),
+ to = int(t.to_frame),
+ direction = t.loop_direction,
+ name = t.name,
+ }
+ append(&tags, tag) or_return
+ }
+ }
+ }
+
+ return tags[:], nil
+}
+
+get_tags :: proc{ tags_from_doc_frame, tags_from_doc }
+
+
+frames_from_doc :: proc(doc: ^ase.Document, alloc := context.allocator) -> (frames: []Frame, err: runtime.Allocator_Error) {
+ return get_frames(doc.frames, alloc)
+}
+
+frames_from_doc_frames :: proc(data: []ase.Frame, alloc := context.allocator) -> (frames: []Frame, err: runtime.Allocator_Error) {
+ res := make([dynamic]Frame, alloc) or_return
+ defer if err != nil { delete(res) }
+
+ for frame in data {
+ append(&res, get_frame(frame) or_return) or_return
+ }
+ return
+}
+
+get_frames :: proc {
+ frames_from_doc,
+ frames_from_doc_frames,
+}
+
+get_frame :: proc(data: ase.Frame, alloc := context.allocator) -> (frame: Frame, err: runtime.Allocator_Error) {
+ frame.duration = i64(data.header.duration)
+ frame.cels = get_cels(data, alloc) or_return
+ return
+}
+
+
+palette_from_doc :: proc(doc: ^ase.Document, alloc := context.allocator) -> (palette: Palette, err: Errors) {
+ pal := make([dynamic]Color, alloc) or_return
+ defer if err != nil { delete(pal) }
+
+ for frame in doc.frames {
+ get_palette(frame, &pal, has_new_palette(doc)) or_return
+ }
+
+ return pal[:], nil
+}
+
+palette_from_doc_frame:: proc(frame: ase.Frame, pal: ^[dynamic]Color, has_new: bool) -> (err: Errors) {
+ for chunk in frame.chunks {
+ #partial switch c in chunk {
+ case ase.Palette_Chunk:
+ if int(c.last_index) >= len(pal) {
+ resize(pal, int(c.last_index)+1) or_return
+ }
+
+ for i in c.first_index..=c.last_index {
+ if int(i) > len(pal) {
+ return Palette_Error.Color_Index_Out_of_Bounds
+ }
+
+ if n, ok := c.entries[i].name.(string); ok {
+ pal[i].name = n
+ }
+ pal[i].color = c.entries[i].color
+
+ }
+
+ case ase.Old_Palette_256_Chunk:
+ if has_new { continue }
+ for p in c {
+ first := len(pal) + int(p.entries_to_skip)
+ last := first + len(p.colors)
+ if last >= len(pal) {
+ resize(pal, last) or_return
+ }
+
+ for i in first..<last {
+ if i >= len(pal) {
+ return Palette_Error.Color_Index_Out_of_Bounds
+ }
+ pal[i].color.rgb = p.colors[i]
+ if p.colors[i] != 0 {
+ pal[i].color.a = 255
+ }
+
+ }
+ }
+
+ case ase.Old_Palette_64_Chunk:
+ if has_new { continue }
+ for p in c {
+ first := len(pal) + int(p.entries_to_skip)
+ last := first + len(p.colors)
+ if last >= len(pal) {
+ resize(pal, last) or_return
+ }
+
+ for i in first..<last {
+ if i >= len(pal) {
+ return Palette_Error.Color_Index_Out_of_Bounds
+ }
+
+ pal[i].color.rgb = p.colors[i]
+ if p.colors[i] != 0 {
+ pal[i].color.a = 255
+ }
+ }
+ }
+ }
+ }
+
+ return
+}
+
+get_palette :: proc{ palette_from_doc, palette_from_doc_frame }
+
+
+tileset_from_doc :: proc(doc: ^ase.Document, alloc := context.allocator) -> (ts: []Tileset, err: runtime.Allocator_Error) {
+ buf := make([dynamic]Tileset, alloc) or_return
+ for frame in doc.frames {
+ err = get_tileset(frame, &buf, alloc)
+ if err != nil {
+ return buf[:], err
+ }
+ }
+ if len(buf) > 0 {
+ log.warn("Tilemaps & Tilesets currently only work for RGBA colour space.")
+ }
+ return buf[:], nil
+}
+
+tileset_from_doc_frame :: proc(frame: ase.Frame, buf: ^[dynamic]Tileset, alloc := context.allocator) -> (err: runtime.Allocator_Error) {
+ for chunk in frame.chunks {
+ #partial switch v in chunk {
+ case ase.Tileset_Chunk:
+ ts: Tileset = {
+ int(v.id),
+ int(v.width),
+ int(v.height),
+ int(v.num_of_tiles),
+ v.name,
+ nil,
+ }
+
+ if t, ok := v.compressed.(ase.Tileset_Compressed); ok {
+ ts.tiles = (Pixels)(t)
+ }
+
+ append(buf, ts) or_return
+
+ case ase.User_Data_Chunk:
+ }
+ }
+
+ return
+}
+
+get_tileset :: proc{ tileset_from_doc, tileset_from_doc_frame }
+
+
+get_info :: proc(doc: ^ase.Document, info: ^Info, alloc := context.allocator) -> (err: Errors) {
+ context.allocator = alloc
+ info.allocator = alloc
+
+ layer_valid_opacity := .Layer_Opacity in doc.header.flags
+ has_new := has_new_palette(doc)
+
+ frames := make([dynamic]Frame) or_return
+ lays := make([dynamic]Layer) or_return
+ tags := make([dynamic]Tag) or_return
+ all_ts := make([dynamic]Tileset) or_return
+ pal := make([dynamic]Color) or_return
+ sls := make([dynamic]Slice) or_return
+ md := get_metadata(doc.header)
+
+ all_lays := make([dynamic]^ase.Layer_Chunk) or_return
+ defer delete(all_lays)
+
+ hue_sat_warn: bool
+
+ // TODO: Make big assumption that only Cel Chunks appear after first frame.
+
+ for doc_frame in doc.frames {
+ frame: Frame
+ frame.duration = i64(doc_frame.header.duration)
+ cels := make([dynamic]Cel) or_return
+
+ for &chunk in doc_frame.chunks {
+
+ #partial switch &c in chunk {
+ case ase.Cel_Chunk:
+ cel := Cel {
+ pos = {int(c.x), int(c.y)},
+ opacity = int(c.opacity_level),
+ z_index = int(c.z_index),
+ layer = int(c.layer_index),
+ }
+
+ switch v in c.cel {
+ case ase.Com_Image_Cel:
+ cel.width = int(v.width)
+ cel.height = int(v.height)
+ cel.raw = v.pixels
+
+ case ase.Raw_Cel:
+ cel.width = int(v.width)
+ cel.height = int(v.height)
+ cel.raw = v.pixels
+
+ case ase.Linked_Cel:
+ for l in frames[v].cels {
+ if l.layer == cel.layer {
+ cel.height = l.height
+ cel.width = l.width
+ cel.raw = l.raw
+ cel.link = int(v)
+ }
+ }
+
+ case ase.Com_Tilemap_Cel:
+ cel.tilemap = Tilemap {
+ width = int(v.width),
+ height = int(v.height),
+ x_flip = uint(v.bitmask_x), // Bitmask for X flip
+ y_flip = uint(v.bitmask_y), // Bitmask for Y flip
+ diag_flip = uint(v.bitmask_diagonal), // Bitmask for diagonal flip (swap X/Y axis)
+ tiles = make([]int, len(v.tiles), alloc) or_return,
+ }
+
+ for &n, p in cel.tilemap.tiles {
+ switch t in v.tiles[p] {
+ case ase.BYTE: n = int(t)
+ case ase.WORD: n = int(t)
+ case ase.DWORD: n = int(t)
+ }
+ }
+ }
+
+ append(&cels, cel) or_return
+
+ case ase.Cel_Extra_Chunk:
+ if ase.Cel_Extra_Flag.Precise in c.flags {
+ extra := Precise_Bounds {
+ fixed.to_f64(c.x), fixed.to_f64(c.y),
+ fixed.to_f64(c.width), fixed.to_f64(c.height),
+ }
+ cels[len(cels)-1].extra = extra
+ }
+
+ case ase.Layer_Chunk:
+ lay := Layer {
+ name = c.name,
+ opacity = int(c.opacity) if layer_valid_opacity else 255,
+ index = len(lays),
+ blend_mode = Blend_Mode(c.blend_mode),
+ visiable = .Visiable in c.flags,
+ tileset = int(c.tileset_index),
+ is_background = .Background in c.flags,
+ }
+
+ when !ASE_USE_BUGGED_SAT {
+ if !hue_sat_warn && (lay.blend_mode == .Saturation || lay.blend_mode == .Hue) {
+ log.infof("Layer: \"%v\"; \"%v\" blend mode is bugged in Aseprite, in ways we can't replicate.", lay.name, lay.blend_mode)
+ log.info("By default we use a fixed version. Compile with `ASE_USE_BUGGED_SAT=true` to use a bugged version.")
+ hue_sat_warn = true
+ }
+ }
+
+
+ if c.child_level != 0 {
+ #reverse for l in all_lays {
+ if l.type == .Group {
+ if .Visiable not_in l.flags {
+ lay.visiable = false
+ break
+ }
+ if l.child_level == 0 {
+ break
+ }
+ }
+ }
+ }
+
+ append(&lays, lay) or_return
+ append(&all_lays, &c) or_return
+
+ case ase.Tags_Chunk:
+ for t in c {
+ tag := Tag {
+ int(t.from_frame),
+ int(t.to_frame),
+ t.loop_direction,
+ t.name,
+ }
+ append(&tags, tag) or_return
+ }
+
+ case ase.Palette_Chunk:
+ if int(c.last_index) >= len(pal) {
+ resize(&pal, int(c.last_index)+1) or_return
+ }
+
+ for i in c.first_index..=c.last_index {
+ if int(i) >= len(pal) {
+ err = Palette_Error.Color_Index_Out_of_Bounds
+ return
+ }
+
+ if n, ok := c.entries[i].name.(string); ok {
+ pal[i].name = n
+ }
+ pal[i].color = c.entries[i].color
+ }
+
+ case ase.Old_Palette_256_Chunk:
+ if has_new { continue }
+ for p in c {
+ first := len(pal) + int(p.entries_to_skip)
+ last := first + len(p.colors)
+ if last >= len(pal) {
+ resize(&pal, last) or_return
+ }
+
+ for i in first..<last {
+ if i >= len(pal) {
+ err = Palette_Error.Color_Index_Out_of_Bounds
+ return
+ }
+ pal[i].color.rgb = p.colors[i]
+ if p.colors[i] != 0 {
+ pal[i].color.a = 255
+ }
+ }
+ }
+
+ case ase.Old_Palette_64_Chunk:
+ if has_new { continue }
+ for p in c {
+ first := len(pal) + int(p.entries_to_skip)
+ last := first + len(p.colors)
+ if last >= len(pal) {
+ resize(&pal, last) or_return
+ }
+
+ for i in first..<last {
+ if i >= len(pal) {
+ err = Palette_Error.Color_Index_Out_of_Bounds
+ return
+ }
+ if max(p.colors[i].r, p.colors[i].b, p.colors[i].g) > 63 {
+ err = Palette_Error.Color_Index_Out_of_Bounds
+ return
+ }
+
+ // https://github.com/alpine-alpaca/asefile/blob/2274c354cea6764f85597252a0d2228e64709348/src/palette.rs#L134
+ // Scale such that 0 -> 0 & 63 -> 255
+ pal[i].color.r = p.colors[i].r << 2 | p.colors[i].r >> 4
+ pal[i].color.g = p.colors[i].g << 2 | p.colors[i].g >> 4
+ pal[i].color.b = p.colors[i].b << 2 | p.colors[i].b >> 4
+ if p.colors[i] != 0 {
+ pal[i].color.a = 255
+ }
+ }
+ }
+
+ case ase.Tileset_Chunk:
+ ts: Tileset
+ ts.id = int(c.id)
+ ts.width = int(c.width)
+ ts.height = int(c.height)
+ ts.name = c.name
+
+ if t, ok := c.compressed.(ase.Tileset_Compressed); ok {
+ ts.tiles = (Pixels)(t)
+ }
+
+ append(&all_ts, ts) or_return
+
+ case ase.Slice_Chunk:
+ sl: Slice
+ sl.name = c.name
+ sl.flags = c.flags
+ sl.keys = make([]Slice_Key, len(c.keys))
+
+ for &key, pos in sl.keys {
+ c_key := c.keys[pos]
+ key.frame = int(c_key.frame_num)
+
+ key.x = int(c_key.x)
+ key.y = int(c_key.y)
+ key.w = int(c_key.width)
+ key.h = int(c_key.height)
+
+ if center, ok := c_key.center.(ase.Slice_Center); ok {
+ key.center = {
+ int(center.x), int(center.y),
+ int(center.width), int(center.height),
+ }
+ }
+
+ if pivot, ok := c_key.pivot.(ase.Slice_Pivot); ok {
+ key.pivot = { int(pivot.x), int(pivot.y) }
+ }
+ }
+ append(&sls, sl)
+ }
+ }
+
+ frame.cels = cels[:]
+ append(&frames, frame) or_return
+ }
+
+ info^ = {
+ frames = frames[:],
+ layers = lays[:],
+ tags = tags[:],
+ tilesets = all_ts[:],
+ slices = sls[:],
+ palette = pal[:],
+ md = md,
+ allocator = alloc,
+ }
+
+ return nil
+}
+
diff --git a/tools/compile_assets/aseprite/utils/image.odin b/tools/compile_assets/aseprite/utils/image.odin
new file mode 100644
index 0000000..ded33b7
--- /dev/null
+++ b/tools/compile_assets/aseprite/utils/image.odin
@@ -0,0 +1,443 @@
+package aseprite_file_handler_utility
+
+import "base:runtime"
+import ir "base:intrinsics"
+
+import "core:slice"
+import "core:mem"
+
+@(require) import "core:fmt"
+@(require) import "core:log"
+
+import ase ".."
+
+
+// Only Uses the first frame
+get_image_from_doc :: proc(doc: ^ase.Document, frame := 0, alloc := context.allocator) -> (img: Image, err: Errors) {
+ context.allocator = alloc
+
+ if len(doc.frames) <= frame {
+ return {}, Image_Error.Frame_Index_Out_Of_Bounds
+ }
+
+ info: Info
+ get_info(doc, &info) or_return
+ defer destroy(&info)
+
+ return get_image_from_frame(info.frames[frame], info)
+}
+
+get_image_from_doc_frame :: proc(
+ frame: ase.Frame, info: Info,
+) -> (img: Image, err: Errors) {
+
+ raw_frame := get_frame(frame, info.allocator) or_return
+ defer delete(raw_frame.cels, info.allocator)
+
+ return get_image_from_frame(raw_frame, info)
+}
+
+get_image_from_cel :: proc(
+ cel: Cel, layer: Layer, info: Info,
+) -> (img: Image, err: Errors) {
+ img.width = cel.width
+ img.height = cel.height
+ img.bpp = .RGBA
+
+ if cel.tilemap.tiles != nil {
+ ts := info.tilesets[layer.tileset]
+ c := cel_from_tileset(cel, ts, info.md.bpp, info.allocator) or_return
+ defer delete(c.raw)
+
+ img.data = make([]byte, c.width * c.height * 4, info.allocator) or_return
+ if !layer.is_background && info.md.bpp == .Indexed {
+ img_p := mem.slice_data_cast([]Pixel, img.data)
+ c := info.palette[info.md.trans_idx].color
+ c.a = 0
+ slice.fill(img_p, c)
+ }
+ write_cel(img.data[:], c, layer, info.md, info.palette) or_return
+
+ } else {
+ img.data = make([]byte, cel.width * cel.height * 4, info.allocator) or_return
+ if !layer.is_background && info.md.bpp == .Indexed {
+ img_p := mem.slice_data_cast([]Pixel, img.data)
+ c := info.palette[info.md.trans_idx].color
+ c.a = 0
+ slice.fill(img_p, c)
+ }
+ write_cel(img.data[:], cel, layer, info.md, info.palette) or_return
+ }
+
+ return
+}
+
+get_image_from_frame :: proc(
+ frame: Frame, info: Info,
+) -> (img: Image, err: Errors) {
+
+ img.md = info.md
+ img.bpp = .RGBA
+ img.data = get_image_bytes(frame, info) or_return
+ return
+}
+
+get_image :: proc {
+ get_image_from_doc,
+ get_image_from_doc_frame,
+ get_image_from_frame,
+ get_image_from_cel,
+}
+
+
+// Only Uses the First Frame
+get_image_bytes_from_doc :: proc(doc: ^ase.Document, frame := 0, alloc := context.allocator) -> (img: []byte, err: Errors) {
+ context.allocator = alloc
+ md := get_metadata(doc.header)
+
+ raw_frame := get_frame(doc.frames[frame]) or_return
+ defer delete(raw_frame.cels)
+
+ layers := get_layers(doc) or_return
+ defer delete(layers)
+
+ palette := get_palette(doc) or_return
+ defer delete(palette)
+
+ ts := get_tileset(doc) or_return
+ defer delete(ts)
+
+ info := Info{layers=layers, palette=palette, tilesets=ts, allocator=alloc, md=md}
+
+ return get_image_bytes(raw_frame, info)
+}
+
+get_image_bytes_from_doc_frame :: proc(
+ frame: ase.Frame, info: Info,
+) -> (img: []byte, err: Errors) {
+
+ raw_frame := get_frame(frame, info.allocator) or_return
+ defer destroy(raw_frame, info.allocator)
+ return get_image_bytes(raw_frame, info)
+}
+
+get_image_bytes_from_cel :: proc (
+ cel: Cel, layer: Layer, info: Info,
+) -> (img: []byte, err: Errors) {
+ if cel.tilemap.tiles != nil {
+ ts := info.tilesets[layer.tileset]
+ c := cel_from_tileset(cel, ts, info.md.bpp, info.allocator) or_return
+ defer delete(c.raw)
+
+ img = make([]byte, c.width * c.height * 4, info.allocator) or_return
+ if !layer.is_background && info.md.bpp == .Indexed {
+ img_p := mem.slice_data_cast([]Pixel, img)
+ c := info.palette[info.md.trans_idx].color
+ c.a = 0
+ slice.fill(img_p, c)
+ }
+ write_cel(img, c, layer, info.md, info.palette) or_return
+
+ } else {
+ img = make([]byte, cel.width * cel.height * 4, info.allocator) or_return
+ if !layer.is_background && info.md.bpp == .Indexed {
+ img_p := mem.slice_data_cast([]Pixel, img)
+ c := info.palette[info.md.trans_idx].color
+ c.a = 0
+ slice.fill(img_p, c)
+ }
+ write_cel(img, cel, layer, info.md, info.palette) or_return
+ }
+
+ return
+}
+
+get_image_bytes_from_frame :: proc(
+ frame: Frame, info: Info,
+) -> (img: []byte, err: Errors) {
+ context.allocator = info.allocator
+
+ img = make([]byte, info.md.width * info.md.height * 4) or_return
+ if len(frame.cels) == 0 { return }
+
+ if !slice.is_sorted_by(frame.cels, cel_less) {
+ slice.sort_by(frame.cels, cel_less)
+ }
+
+ if !info.layers[0].is_background && info.md.bpp == .Indexed {
+ img_p := mem.slice_data_cast([]Pixel, img)
+ c := info.palette[info.md.trans_idx].color
+ c.a = 0
+ slice.fill(img_p, c)
+ }
+
+ for cel in frame.cels {
+ lay := info.layers[cel.layer]
+ if !lay.visiable { continue }
+
+ if cel.tilemap.tiles != nil {
+ ts := info.tilesets[lay.tileset]
+ c := cel_from_tileset(cel, ts, info.md.bpp, info.allocator) or_return
+ defer delete(c.raw)
+
+ write_cel(img, c, lay, info.md, info.palette) or_return
+
+ } else {
+ write_cel(img, cel, lay, info.md, info.palette) or_return
+ }
+ }
+
+ return
+}
+
+get_image_bytes :: proc {
+ get_image_bytes_from_doc,
+ get_image_bytes_from_doc_frame,
+ get_image_bytes_from_cel,
+ get_image_bytes_from_frame,
+}
+
+
+get_all_images :: proc(doc: ^ase.Document, alloc := context.allocator) -> (imgs: []Image, err: Errors) {
+ context.allocator = alloc
+ imgs = make([]Image, len(doc.frames)) or_return
+ defer if err != nil { destroy(imgs)}
+
+ info: Info
+ get_info(doc, &info) or_return
+ defer destroy(&info)
+
+ for frame, p in info.frames {
+ imgs[p] = get_image(frame, info) or_return
+ }
+
+ return
+}
+
+get_all_images_bytes :: proc(doc: ^ase.Document, alloc := context.allocator) -> (imgs: [][]byte, err: Errors) {
+ context.allocator = alloc
+ imgs = make([][]byte, len(doc.frames)) or_return
+ defer if err != nil { destroy(imgs) }
+
+ md := get_metadata(doc.header)
+
+ layers := get_layers(doc) or_return
+ defer delete(layers)
+
+ palette := get_palette(doc) or_return
+ defer delete(palette)
+
+ ts := get_tileset(doc) or_return
+ defer delete(ts)
+
+ info := Info{layers=layers, palette=palette, tilesets=ts, allocator=alloc, md=md}
+
+ for frame, p in doc.frames {
+ imgs[p] = get_image_bytes(frame, info) or_return
+ }
+
+ return
+}
+
+
+get_cels_as_imgs :: proc(doc: ^ase.Document, frame_idx := 0, alloc := context.allocator) -> (res: []Image, err: Errors) {
+ context.allocator = alloc
+
+ info: Info
+ get_info(doc, &info) or_return
+ defer destroy(&info)
+
+ if len(info.frames) < frame_idx {
+ return nil, .Frame_Index_Out_Of_Bounds
+ }
+
+ frame := info.frames[frame_idx]
+
+ res = make([]Image, len(frame.cels)) or_return
+ if len(frame.cels) == 0 { return }
+
+ if !slice.is_sorted_by(frame.cels, cel_less) {
+ slice.sort_by(frame.cels, cel_less)
+ }
+
+ for cel, i in frame.cels {
+ lay := info.layers[cel.layer]
+ if !lay.visiable { continue }
+
+ res[i] = get_image_from_cel(cel, lay, info) or_return
+ }
+
+ return
+}
+
+
+get_all_cels_as_imgs :: proc(doc: ^ase.Document, alloc := context.allocator) -> (res: []Image, err: Errors) {
+ context.allocator = alloc
+
+ info: Info
+ get_info(doc, &info) or_return
+ defer destroy(&info)
+
+ imgs := make([dynamic]Image) or_return
+
+ for frame in info.frames {
+ if len(frame.cels) == 0 { continue }
+
+ if !slice.is_sorted_by(frame.cels, cel_less) {
+ slice.sort_by(frame.cels, cel_less)
+ }
+
+ for cel in frame.cels {
+ lay := info.layers[cel.layer]
+ if !lay.visiable { continue }
+
+ img := get_image_from_cel(cel, lay, info) or_return
+ append(&imgs, img)
+ }
+ }
+
+ return imgs[:], nil
+}
+
+cel_from_tileset :: proc(cel: Cel, ts: Tileset, chans: Pixel_Depth, alloc: runtime.Allocator) -> (c: Cel, err: Errors) {
+ c = cel
+ c.width = cel.tilemap.width * ts.width
+ c.height = cel.tilemap.height * ts.height
+ ch := int(chans)/8
+
+ if (ts.height * ts.width * len(c.tilemap.tiles) * ch) != (c.width * c.height * ch) {
+ return {}, .Tileset_Cel_Sizes_Mismatch
+ }
+
+ c.raw = make([]byte, ts.height * ts.width * len(c.tilemap.tiles) * ch, alloc) or_return
+
+ for h in 0..<c.tilemap.height {
+ for w in 0..<c.tilemap.width {
+ s := (h * c.tilemap.width * ts.height + w) * ts.width * ch
+ s1 := c.tilemap.tiles[h * c.tilemap.width + w] * ts.width * ts.height * ch
+
+ for y in 0..<ts.height {
+ copy(c.raw[s+(y*ts.width*c.tilemap.width*ch):][:ch*ts.width], ts.tiles[s1+(y*ts.width*ch):])
+ }
+ }
+ }
+
+ return
+}
+
+// Write a cel to an image's data. Assumes tilemaps & linked cels have already been handled.
+write_cel :: proc (
+ buf: []byte, cel: Cel, layer: Layer, md: Metadata,
+ pal: Palette = nil,
+) -> (err: Errors) {
+ // TODO: Allow for both arbitrary reads & writes, i.e. cropping.
+
+ if len(cel.raw) <= 0 {
+ fast_log(.Debug, "No Cel data to write.")
+ return
+ }
+
+ if len(buf) < (md.height * md.width * 4) {
+ fast_log(.Error, "Image buffer size is smaller than Metadata.")
+ return .Buffer_To_Small
+ }
+
+ when UTILS_DEBUG_MODE {
+ switch md.bpp {
+ case .Indexed:
+ if pal == nil {
+ fast_log(.Error, "Indexed Color Mode. No Palette")
+ return .Indexed_BPP_No_Palette
+ }
+ fallthrough
+ case .Grayscale, .RGBA:
+ if len(cel.raw) % (int(md.bpp) / 8) != 0 {
+ fast_log(.Error, "Size of cel not a multipule of channels. ")
+ return .Cel_Size_Not_Of_BPP
+ }
+ case:
+ fast_log(.Error, "Invalid Color Mode: ", md.bpp)
+ return .Invalid_BPP
+ }
+ }
+
+ /*offset := [2]int {
+ abs(cel.x) if cel.x < 0 else 0,
+ abs(cel.y) if cel.y < 0 else 0,
+ }*/
+ offset := [2]int {
+ abs(min(0, cel.x)),
+ abs(min(0, cel.y)),
+ }
+
+ bounds := Bounds {
+ x = clamp(cel.x, 0, md.width),
+ y = clamp(cel.y, 0, md.height),
+ width = clamp(cel.width, 0, md.width),
+ height = clamp(cel.height, 0, md.height),
+ }
+
+ when UTILS_DEBUG_MODE {
+ if !( bounds.x <= md.width && bounds.y <= md.height \
+ && bounds.width <= md.width && bounds.height <= md.height \
+ && offset.x <= md.width && offset.y <= md.height \
+ && bounds.x >= 0 && bounds.y >= 0 \
+ && bounds.width >= 0 && bounds.height >= 0 \
+ && offset.x >= 0 && offset.y >= 0 ) {
+ fast_log(.Error, "Cel out of bounds of Image bounds.")
+ return .Cel_Out_Of_Bounds,
+ }
+ }
+
+ y_loop: for y in 0..<bounds.height {
+ for x in 0..<bounds.width {
+ pix: [4]byte
+ idx := (y + offset.y) * cel.width + x + offset.x
+
+ // Convert to RGBA
+ switch md.bpp {
+ case .Indexed:
+ if cel.raw[idx] == md.trans_idx {
+ continue
+ }
+ pix = pal[cel.raw[idx]].color
+
+ case .Grayscale:
+ pix.rgb = cel.raw[idx * 2]
+ pix.a = cel.raw[idx * 2 + 1]
+
+ case .RGBA:
+ // Note(blob):
+ // This is comparable to slice casting before & idxing in to that.
+ // `mem.slice_data_cast()` or `slice.reinterpret()`
+ // On average is slightly faster but more variable in optimized builds
+ pix = ir.unaligned_load((^[4]byte)(&cel.raw[idx * 4]))
+ }
+
+ if pix.a != 0 {
+ iidx := ((y + bounds.y) * md.width + x + bounds.x) * 4
+ if len(buf) <= iidx {
+ break y_loop
+ }
+
+ ipix := (^[4]byte)(&buf[iidx])
+
+ if ipix.a != 0 {
+ // Blend pixels)
+ p := ir.unaligned_load(ipix)
+ a := alpha(cel.opacity, layer.opacity)
+ pix = blend(p, pix, a, layer.blend_mode) or_return
+
+ } else {
+ // Merge Alpha & Opacities
+ pix.a = u8(alpha(i32(pix.a), alpha(cel.opacity, layer.opacity)))
+ }
+
+ ir.unaligned_store(ipix, pix)
+ }
+ }
+ }
+
+ return
+}
+
diff --git a/tools/compile_assets/aseprite/utils/sprite_sheet.odin b/tools/compile_assets/aseprite/utils/sprite_sheet.odin
new file mode 100644
index 0000000..ceb4b76
--- /dev/null
+++ b/tools/compile_assets/aseprite/utils/sprite_sheet.odin
@@ -0,0 +1,382 @@
+package aseprite_file_handler_utility
+
+import "base:runtime"
+import "core:slice"
+
+@require import "core:fmt"
+@require import "core:log"
+
+import ase ".."
+
+
+create_sprite_sheet :: proc {
+ create_sprite_sheet_from_doc,
+ create_sprite_sheet_from_info,
+}
+
+
+/*
+Creates internal allocation on `context.temp_allocator`, will attempt to clean up after itself.
+*/
+create_sprite_sheet_from_doc :: proc (
+ doc: ^ase.Document, s_info: Sprite_Info,
+ write_rules := DEFAULT_SPRITE_WRITE_RULES, alloc := context.allocator
+) -> (res: Sprite_Sheet, err: Errors) {
+
+ runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD(context.allocator == context.temp_allocator)
+ info: Info
+ get_info(doc, &info, context.temp_allocator) or_return
+
+ return create_sprite_sheet_from_info(info, s_info, write_rules, alloc)
+}
+
+
+/*
+Creates internal allocation on `context.temp_allocator`, will attempt to clean up after itself.
+*/
+create_sprite_sheet_from_info :: proc (
+ info: Info, s_info: Sprite_Info,
+ write_rules := DEFAULT_SPRITE_WRITE_RULES, alloc := context.allocator
+) -> (res: Sprite_Sheet, err: Errors) {
+
+ switch {
+ case write_rules.align < min(Sprite_Alignment) || max(Sprite_Alignment) < write_rules.align:
+ err = .Invalid_Alignment
+ return
+
+ case s_info.size.x < write_rules.offset.x || s_info.size.y < write_rules.offset.y:
+ err = .Invalid_Offset
+ return
+
+ case s_info.spacing.x < 0 || s_info.spacing.y < 0:
+ err = .Invalid_Spacing
+ return
+
+ case s_info.boarder.x < 0 || s_info.boarder.y < 0:
+ err = .Invalid_Boarder
+ return
+
+ case s_info.count <= 0:
+ err = .Invalid_Count
+ return
+
+ case (s_info.size.x * s_info.size.y) < (info.md.width * info.md.height):
+ if !write_rules.ingore_sprite_size {
+ err = .Sprite_Size_to_Small
+ return
+ }
+
+ // If `shrink_to_pixels` is set the new bounds may fit just fine.
+ // However it's not reasonable to check them all, instead we just assume they will be.
+ if !write_rules.shrink_to_pixels {
+ fast_log(.Warning, "Sprite smaller than Frame. Ingoring & continuing.")
+ }
+ }
+
+ // Note(blob):
+ // Gets the clostest multiple of `s_info.count` that's `>=` to `len(info.frames)`.
+ // Allows for `len(info.frames)` to not be a multiple of `s_info.count`;
+ // and still make a valid grid.
+ frame_count := len(info.frames) + (s_info.count - ((len(info.frames) - 1) %% s_info.count + 1))
+
+ y_count := max( 1, frame_count / s_info.count )
+ width := ( s_info.count * s_info.size.x ) + ( (s_info.count - 1) * s_info.spacing.x )
+ height := ( y_count * s_info.size.y ) + ( (y_count - 1) * s_info.spacing.y )
+
+
+ img_width := width + (s_info.boarder.x * 2)
+ img_height := height + (s_info.boarder.y * 2)
+ img_size := img_width * img_height * 4
+
+ res.info = s_info
+ res.img = {
+ width = img_width,
+ height = img_height,
+ bpp = .RGBA,
+ data = make([]u8, img_size, alloc) or_return,
+ }
+
+ defer {
+ if err != nil {
+ delete(res.img.data, alloc)
+ }
+ }
+
+
+ if write_rules.use_index_bg_colour && info.md.bpp == .Indexed && !info.layers[0].is_background {
+ img_p := slice.reinterpret([]Pixel, res.img.data)
+ c := info.palette[info.md.trans_idx].color
+ c.a = 0
+ slice.fill(img_p, c)
+
+ } else {
+ fill_colour(res.img.data, write_rules.background_colour)
+ }
+
+ runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD(context.allocator == context.temp_allocator)
+ tileset_alloc := context.temp_allocator
+
+ sprite_pos: [2]int
+ sw, sh := s_info.size.x, s_info.size.y
+
+ for frame in info.frames {
+ defer {
+ sprite_pos.x += s_info.size.x + s_info.spacing.x
+ if width <= sprite_pos.x {
+ sprite_pos.x = 0
+ sprite_pos.y += s_info.size.y + s_info.spacing.y
+ }
+
+ if height + s_info.spacing.y < sprite_pos.y {
+ panic("Sprite Y Pos is OOB. This shouldn't happen... send help.")
+ }
+ }
+
+ if len(frame.cels) == 0 {
+ continue
+ }
+
+ if !slice.is_sorted_by(frame.cels, cel_less) {
+ slice.sort_by(frame.cels, cel_less)
+ }
+
+ fw, fh := info.md.width, info.md.height
+ fp: [2]int = 0
+
+ if write_rules.shrink_to_pixels {
+
+ min_pos := [2]int{fw, fh}
+ max_pos: [2]int
+
+ for cel in frame.cels {
+ layer := info.layers[cel.layer]
+ if !layer.visiable || layer.is_background { continue }
+
+ size := [2]int{cel.width, cel.height}
+ if cel.tilemap.tiles != nil {
+ ts := info.tilesets[layer.tileset]
+ size = {ts.width, ts.height}
+ }
+
+ min_pos = {
+ min(min_pos.x, cel.pos.x),
+ min(min_pos.y, cel.pos.y),
+ }
+ max_pos = {
+ max(max_pos.x, cel.pos.x + size.x),
+ max(max_pos.y, cel.pos.y + size.y),
+ }
+ }
+
+ frame_size := max_pos - min_pos
+
+ fw, fh = frame_size.x, frame_size.y
+ fp = min_pos
+ }
+
+ cel_offset := write_rules.offset + s_info.boarder + sprite_pos
+
+ alignment: [2]int
+ // Sprite Sheet Aligment https://www.desmos.com/geometry/miqzk9ijus
+ switch write_rules.align {
+ case .Top_Left: // Default Alignment
+ case .Top_Center: alignment.x = (sw - fw) / 2
+ case .Top_Right: alignment.x = (sw - fw)
+
+ case .Mid_Left: alignment.y = (sh - fh) / 2
+ case .Mid_Center: alignment = { (sw - fw) / 2, (sh - fh) / 2 }
+ case .Mid_Right: alignment = { (sw - fw), (sh - fh) / 2 }
+
+ case .Bot_Left: alignment.y = sh - fh
+ case .Bot_Center: alignment = { (sw - fw) / 2, sh - fh }
+ case .Bot_Right: alignment = { (sw - fw), sh - fh }
+
+ case:
+ err = .Invalid_Alignment
+ return
+ }
+
+
+ for cel in frame.cels {
+ layer := info.layers[cel.layer]
+ skip := (!layer.visiable) || (write_rules.ingore_bg_layers && layer.is_background)
+ if skip { continue }
+
+ s_cel := cel
+ if cel.tilemap.tiles != nil {
+ ts := info.tilesets[layer.tileset]
+ s_cel = cel_from_tileset(cel, ts, info.md.bpp, tileset_alloc) or_return
+ }
+
+ if layer.is_background {
+ // NOTE(blob):
+ // This isn't truly right & only for really works for solid colours.
+ // `write_cel` would need to rewrite support both arbitrary reads & writes.
+ s_cel.pos += cel_offset
+ s_cel.width = s_info.size.x
+ s_cel.height = s_info.size.y
+ write_cel(res.img.data, s_cel, layer, res.img.md, info.palette) or_return
+ continue
+ }
+
+ s_cel.pos += alignment + cel_offset - fp
+ // Make sure we don't pass a negitive position.
+ s_cel.pos = { max(0, s_cel.pos.x), max(0, s_cel.pos.y) }
+
+ write_cel(res.img.data, s_cel, layer, res.img.md, info.palette) or_return
+ }
+ }
+
+ return
+}
+
+
+// Finds the smallest Sprite size need to fit all visable pixels.
+// Ingores Background Layers
+find_min_sprite_size :: proc(info: Info, make_square := true) -> (res: [2]int) {
+
+ for frame in info.frames {
+ for cel in frame.cels {
+ layer := info.layers[cel.layer]
+ if !layer.visiable || layer.is_background { continue }
+
+ size := [2]int{ cel.width, cel.height }
+ if cel.tilemap.tiles != nil {
+ ts := info.tilesets[layer.tileset]
+ size = {ts.width, ts.height}
+ }
+
+ res.x = max(res.x, size.x)
+ res.y = max(res.y, size.y)
+ }
+ }
+
+ if make_square {
+ res = max(res.x, res.y)
+ }
+
+ return
+}
+
+
+draw_sheet_grid :: proc(sheet: ^Sprite_Sheet, colour: [4]u8) {
+ draw_sheet_spacing(sheet, colour, true)
+}
+
+
+draw_sheet_spacing :: proc(sheet: ^Sprite_Sheet, colour: [4]u8, always_draw: bool) {
+ img := sheet.img
+ info := sheet.info
+ assert(img.bpp == .RGBA)
+
+ raw := slice.reinterpret([][4]u8, img.data)
+
+ row_count := (img.height - info.size.y - (info.boarder.y * 2)) / ( info.size.y + info.spacing.y )
+ if 0 < row_count {
+ row_block := img.width * info.size.y
+ row_space := img.width * info.spacing.y
+ row_step := row_block + row_space
+ row_fill := always_draw ? max(img.width, row_space) : row_space
+
+ row_offset := row_block + img.width * info.boarder.x
+ base := raw[row_offset:][:row_fill]
+
+ slice.fill(base, colour)
+
+ for row in 1..<row_count {
+ start := row_step * row + row_offset
+ copy(raw[start:], base)
+ }
+ }
+
+ col_count := info.count - 1
+ if 0 < col_count {
+ col_block := info.size.x + info.spacing.x
+ col_fill := always_draw ? max(1, info.spacing.y) : info.spacing.y
+ col_offset := info.size.x + info.boarder.x
+
+ base := raw[col_offset:][:col_fill]
+ slice.fill(base, colour)
+
+ for col in 1..<col_count {
+ pos := col_block * col + col_offset
+ copy(raw[pos:], base)
+ }
+
+ for y in 1..<img.height {
+ start := img.width * y + col_offset
+ for col in 0..<col_count {
+ pos := col_block * col + start
+ copy(raw[pos:], base)
+ }
+ }
+ }
+
+ return
+}
+
+
+draw_sheet_boarder :: proc(sheet: ^Sprite_Sheet, colour: [4]u8) {
+ img := sheet.img
+ info := sheet.info
+ assert(img.bpp == .RGBA)
+
+ raw := slice.reinterpret([][4]u8, img.data)
+
+ if 0 < info.boarder.y {
+ base := raw[:img.width * info.boarder.y]
+ slice.fill(base, colour)
+ copy(raw[(img.height - info.boarder.y) * img.width:], base)
+ }
+
+ if 0 < info.boarder.x {
+ base_start := img.width * info.boarder.y
+ base := raw[base_start:][:info.boarder.x]
+ count := img.height - (info.boarder.y * 2)
+
+ slice.fill(base, colour)
+ copy(raw[base_start + img.width - info.boarder.x:], base)
+
+ for pos in 0..<count {
+ start := base_start + (img.width * pos)
+ copy(raw[start:], base)
+ copy(raw[start + img.width - info.boarder.x:], base)
+ }
+ }
+
+ return
+}
+
+
+find_pixel_bounds :: proc(img: Image, bg_colour: [4]u8 = 0, check_trans := true) -> (bounds: Bounds) {
+ assert(img.md.bpp == .RGBA)
+ raw := slice.reinterpret([][4]u8, img.data)
+
+ min_pos, max_pos := find_pixel_bounds_min_max(raw, img.width, img.height, bg_colour, check_trans)
+
+ return { pos = min_pos, width = max_pos.x - min_pos.x, height = max_pos.y - min_pos.y }
+}
+
+
+find_pixel_bounds_min_max :: proc(img: [][4]u8, width, height: int, bg_colour: [4]u8, check_trans: bool) -> (min_pos, max_pos: [2]int) {
+
+ min_pos = { width, height }
+ max_pos = { 0, 0 }
+
+ for y in 0..<height {
+ for x in 0..<width {
+ pix := img[y * width + x]
+ if (pix.a == 0 && check_trans) || pix == bg_colour {
+ continue
+ }
+
+ min_pos = { min(x, min_pos.x), min(y, min_pos.y) }
+ max_pos = { max(x, max_pos.x), max(y, max_pos.y) }
+ }
+ }
+
+ max_pos += 1
+
+ return
+}
+
diff --git a/tools/compile_assets/aseprite/utils/types.odin b/tools/compile_assets/aseprite/utils/types.odin
new file mode 100644
index 0000000..bf2cd0c
--- /dev/null
+++ b/tools/compile_assets/aseprite/utils/types.odin
@@ -0,0 +1,306 @@
+package aseprite_file_handler_utility
+
+import "base:runtime"
+import "core:time"
+
+import ase ".."
+
+
+// Errors
+Palette_Error :: enum {
+ None,
+ Color_Index_Out_of_Bounds,
+}
+Blend_Error :: enum {
+ None,
+ Invalid_Mode,
+ Unequal_Image_Sizes,
+}
+Image_Error :: enum {
+ None,
+ Frame_Index_Out_Of_Bounds,
+ Indexed_BPP_No_Palette,
+ Invalid_BPP,
+ Cel_Out_Of_Bounds,
+ Cel_Size_Not_Of_BPP,
+ Buffer_To_Small,
+ Buffer_Size_Not_Match_Metadata,
+ Buffer_Not_RGBA,
+}
+Animation_Error :: enum {
+ None,
+ Tag_Not_Found,
+ Tag_Index_Out_Of_Bounds,
+}
+
+Tileset_Error :: enum {
+ None,
+ Tileset_Cel_Sizes_Mismatch,
+}
+
+Sprite_Sheet_Error :: enum {
+ None,
+ Sprite_Size_to_Small,
+ Invalid_Alignment,
+ Invalid_Offset,
+ Invalid_Count,
+ Invalid_Spacing,
+ Invalid_Boarder,
+}
+
+Errors :: union #shared_nil {
+ runtime.Allocator_Error,
+ Image_Error,
+ Animation_Error,
+ Tileset_Error,
+ Blend_Error,
+ Palette_Error,
+ Sprite_Sheet_Error,
+}
+
+// Raw Types
+B_Pixel :: [4]i32
+Pixel :: [4]byte
+Pixels :: []byte
+
+Vec2 :: [2]int
+
+// ASE Document types
+
+Precise_Bounds :: struct {
+ // Truely fixpoint but I anit dealing with that shit
+ x, y, width, height: f64,
+}
+
+Bounds :: struct {
+ using pos: Vec2,
+ width, height: int,
+}
+
+Cel :: struct {
+ using bounds: Bounds,
+ opacity: int,
+ link: int,
+ layer: int,
+ z_index: int, // https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md#note5
+ raw: Pixels `fmt:"-"`,
+ tilemap: Tilemap,
+ extra: Maybe(Precise_Bounds),
+}
+
+Tilemap :: struct {
+ width: int,
+ height: int,
+ x_flip: uint,
+ y_flip: uint,
+ diag_flip: uint,
+ tiles: []int,
+}
+
+Layer :: struct {
+ name: string,
+ opacity: int,
+ visiable: bool,
+ is_background: bool,
+ index: int,
+ blend_mode: Blend_Mode,
+ tileset: int,
+}
+
+Frame :: struct {
+ duration: i64, // in milliseconds
+ cels: []Cel,
+}
+
+Tag :: struct {
+ from: int,
+ to: int,
+ direction: ase.Tag_Loop_Dir,
+ name: string,
+}
+
+Palette :: []Color
+
+Color :: struct {
+ using color: Pixel,
+ name: string,
+}
+
+// Bits per pixel
+Pixel_Depth :: enum {
+ Indexed = 8,
+ Grayscale = 16,
+ RGBA = 32,
+}
+
+// Not needed RN. We'll only ever handle sRGB.
+Color_Space :: enum {
+ None,
+ sRGB,
+ ICC,
+}
+
+Metadata :: struct {
+ width: int,
+ height: int,
+ bpp: Pixel_Depth,
+ trans_idx: u8,
+}
+
+Slice_Key :: struct {
+ frame: int,
+ x, y: int,
+ w, h: int,
+ center: [4]int,
+ pivot: [2]int,
+}
+
+Slice :: struct {
+ flags: ase.Slice_Flags,
+ name: string,
+ keys: []Slice_Key,
+}
+
+Tileset :: struct {
+ id: int,
+ width: int,
+ height: int,
+ num: int,
+ name: string,
+ tiles: Pixels,
+}
+
+
+Info :: struct {
+ frames: []Frame,
+ layers: []Layer,
+ tags: []Tag,
+ tilesets: []Tileset,
+ slices: []Slice,
+ palette: Palette,
+
+ md: Metadata,
+ allocator: runtime.Allocator,
+}
+
+
+
+// Sprite Sheet
+Sprite_Info :: struct {
+ size: [2]int, // Size of a sprite.
+ spacing: [2]int, // Spacing between each sprite.
+ boarder: [2]int, // Boarder between sheet & image edge.
+ count: int, // Sprites per row.
+}
+
+// Govern's how Frames are writen a Sprite
+Sprite_Write_Rules :: struct {
+
+ // What point on the Frame & Sprite to align.
+ // Effective when Frame.size < Sprite.size.
+ align: Sprite_Alignment,
+
+ // Offset from the alignment point.
+ offset: [2]int,
+
+ // Resulting Sheet's Background Colour
+ background_colour: [4]u8,
+
+ // Shrinks Frame to the bounds of its visable pixels.
+ // NOTE(blob):
+ // Using this may slighly change the positioning.
+ // This is expected & intended behaviour.
+ shrink_to_pixels: bool,
+
+ // Whether to use the Background Colour
+ // of the ase file or use `background_colour`;
+ // Only used for `Indexed` colour mode.
+ use_index_bg_colour: bool,
+
+ // Whether to ignore Background layers.
+ ingore_bg_layers: bool,
+
+ // Whether to ingore the Sprite size being
+ // smaller then the Frame frame.
+ ingore_sprite_size: bool,
+}
+
+// Sprite Sheet Alignment https://www.desmos.com/geometry/miqzk9ijus
+Sprite_Alignment :: enum {
+ Top_Left, Top_Center, Top_Right,
+ Mid_Left, Mid_Center, Mid_Right,
+ Bot_Left, Bot_Center, Bot_Right,
+
+ // Default Alignment
+ Base = Top_Left,
+
+ // Some helpers... cause why not.
+ Top = Top_Center,
+ Middle = Mid_Center,
+ Bottom = Bot_Center,
+ Left = Mid_Left,
+ Right = Mid_Right,
+}
+
+
+// Precomputed Types. They own all their data.
+Image :: struct {
+ using md: Metadata,
+ data: Pixels `fmt:"-"`,
+}
+
+Animation :: struct {
+ using md: Metadata,
+ fps: int,
+ length: time.Duration,
+ frames: []Pixels,
+}
+
+Sprite_Sheet :: struct {
+ using img: Image,
+ info: Sprite_Info,
+}
+
+
+
+Blend_Mode :: enum {
+ Unspecified = -1,
+ Src = -2,
+ Merge = -3,
+ Neg_BW = -4,
+ Red_Tint = -5,
+ Blue_Tint = -6,
+ Dst_Over = -7,
+
+ Normal = 00,
+ Multiply = 01,
+ Screen = 02,
+ Overlay = 03,
+ Darken = 04,
+ Lighten = 05,
+ Color_Dodge = 06,
+ Color_Burn = 07,
+ Hard_Light = 08,
+ Soft_Light = 09,
+ Difference = 10,
+ Exclusion = 11,
+ Hue = 12,
+ Saturation = 13,
+ Color = 14,
+ Luminosity = 15,
+ Addition = 16,
+ Subtract = 17,
+ Divide = 18,
+}
+
+
+DEFAULT_SPRITE_WRITE_RULES :: Sprite_Write_Rules {
+ align = .Mid_Center,
+ offset = {0, 0},
+ background_colour = {0, 0, 0, 0},
+ shrink_to_pixels = false,
+ use_index_bg_colour = true,
+ ingore_bg_layers = false,
+ ingore_sprite_size = false,
+}
+
diff --git a/tools/compile_assets/aseprite/utils/utils_internal.odin b/tools/compile_assets/aseprite/utils/utils_internal.odin
new file mode 100644
index 0000000..6915fe5
--- /dev/null
+++ b/tools/compile_assets/aseprite/utils/utils_internal.odin
@@ -0,0 +1,110 @@
+package aseprite_file_handler_utility
+
+import ir "base:intrinsics"
+import "base:runtime"
+import "core:reflect"
+import "core:strconv"
+import "core:slice"
+
+@(require) import "core:fmt"
+@(require) import "core:log"
+
+_ :: reflect
+
+
+@(private)
+fast_log_str :: proc(lvl: log.Level, str: string, loc := #caller_location) {
+ logger := context.logger
+ if logger.procedure == nil { return }
+ if lvl < logger.lowest_level { return }
+ logger.procedure(logger.data, lvl, str, logger.options, loc)
+}
+
+@(private)
+fast_log_str_enum :: proc(lvl: log.Level, str: string, val: $T, sep := " ", loc := #caller_location) where ir.type_is_enum(T) {
+ logger := context.logger
+ if logger.procedure == nil { return }
+ if lvl < logger.lowest_level { return }
+
+ s := reflect.enum_string(val)
+ buf := make([]u8, len(str) + len(sep) + len(s))
+ defer delete(buf)
+
+ n := copy(buf[:], str)
+ n += copy(buf[n:], sep)
+ copy(buf[n:], s)
+
+ logger.procedure(logger.data, lvl, string(buf), logger.options, loc)
+}
+
+@(private)
+fast_log_str_num :: proc(lvl: log.Level, str: string, val: $T, sep := " ", loc := #caller_location) where ir.type_is_integer(T) {
+ logger := context.logger
+ if logger.procedure == nil { return }
+ if lvl < logger.lowest_level { return }
+
+ nb: [32]u8
+ s := strconv.append_int(nb[:], i64(val), 10)
+ buf := make([]u8, len(str) + len(sep) + len(s))
+ defer delete(buf)
+
+ n := copy(buf[:], str)
+ n += copy(buf[n:], sep)
+ copy(buf[n:], s)
+
+ logger.procedure(logger.data, lvl, string(buf), logger.options, loc)
+}
+
+@(private)
+fast_log :: proc {fast_log_str, fast_log_str_enum, fast_log_str_num}
+
+
+// Internal Debugging Tool.
+format_pixels :: proc(img: Image, x := 4, y := 4, alloc := context.allocator) -> (str: string, err: runtime.Allocator_Error) #optional_allocator_error {
+ ch := int(img.bpp) >> 3
+
+ size := len(img.data) * 3 /*pixel in bytes*/ \
+ + (len(img.data)/ch - 1) /*comas*/ \
+ + (len(img.data)*2/ch) /*<space>|*/ \
+ + (img.width*img.height*4) /*tile gap*/
+
+ sb := make([dynamic]byte, 0, size) or_return
+ buf: [3]byte
+
+ for n in 0..<len(img.data)/ch {
+ if n %% (img.width) == 0 {
+ append(&sb, '|') or_return
+ }
+
+ s := strconv.write_int(buf[:], i64(img.data[n*ch]), 10)
+ for _ in 0..<3-len(s) {
+ append(&sb, ' ') or_return
+ }
+ append(&sb, s) or_return
+
+ for i in 1..<ch {
+ append(&sb, ',')
+ s = strconv.write_int(buf[:], i64(img.data[n*ch+i]), 10)
+ for _ in 0..<3-len(s) {
+ append(&sb, ' ') or_return
+ }
+ append(&sb, s) or_return
+ }
+ append(&sb, '|') or_return
+
+ if (n+1) %% img.width == 0 {
+ append(&sb, '\n') or_return
+ }else if (n+1) %% x == 0 {
+ append(&sb, " |") or_return
+ }
+ if (n+1) %% (img.width * y) == 0 {
+ append(&sb, '\n') or_return
+ }
+ }
+
+ return string(sb[:]), nil
+}
+
+fill_colour :: proc(data: []u8, colour: [4]u8) {
+ slice.fill(slice.reinterpret([][4]u8, data), colour)
+}