├── .gitignore ├── 3rd party licenses ├── aseprite license └── libpixman license ├── LICENSE ├── README.md ├── TODO.md ├── ase.odin ├── common.odin ├── example ├── ase_example.odin ├── example.odin └── utils_example.odin ├── extentions ├── common.odin ├── dithering matrices.odin ├── extentions.odin ├── keys.odin ├── languages.odin ├── pallettes.odin ├── plugins with scriptes.odin └── themes.odin ├── gpl ├── extended_gpl.odin └── utils │ └── palette_conv.odin ├── marshal.odin ├── mod.pkg ├── raw ├── chunk_equal.odin ├── common.odin ├── gpl.odin ├── marshal.odin ├── types.odin ├── unmarshal.odin ├── unmarshal_chunk.odin ├── updating.odin ├── util.odin └── validate.odin ├── read.odin ├── read_chunk.odin ├── tests ├── README.md ├── _utils │ ├── gen_files.odin │ ├── gen_files.py │ └── test.odin ├── ase_test.odin ├── asefile │ ├── 256_color_old_palette_chunk.aseprite │ ├── 256_color_old_palette_chunk.png │ ├── LICENSE │ ├── background.aseprite │ ├── background.png │ ├── basic-16x16.aseprite │ ├── basic-16x16.png │ ├── big.aseprite │ ├── big.png │ ├── blend_addition.aseprite │ ├── blend_addition.png │ ├── blend_color.aseprite │ ├── blend_color.png │ ├── blend_colorburn.aseprite │ ├── blend_colorburn.png │ ├── blend_colordodge.aseprite │ ├── blend_colordodge.png │ ├── blend_darken.aseprite │ ├── blend_darken.png │ ├── blend_difference.aseprite │ ├── blend_difference.png │ ├── blend_divide.aseprite │ ├── blend_divide.png │ ├── blend_exclusion.aseprite │ ├── blend_exclusion.png │ ├── blend_hardlight.aseprite │ ├── blend_hardlight.png │ ├── blend_hue.aseprite │ ├── blend_hue.png │ ├── blend_lighten.aseprite │ ├── blend_lighten.png │ ├── blend_luminosity.aseprite │ ├── blend_luminosity.png │ ├── blend_multiply.aseprite │ ├── blend_multiply.png │ ├── blend_normal.aseprite │ ├── blend_normal.png │ ├── blend_overlay.aseprite │ ├── blend_overlay.png │ ├── blend_saturation.aseprite │ ├── blend_saturation.png │ ├── blend_saturation_bug.aseprite │ ├── blend_saturation_bug.png │ ├── blend_screen.aseprite │ ├── blend_screen.png │ ├── blend_softlight.aseprite │ ├── blend_softlight.png │ ├── blend_subtract.aseprite │ ├── blend_subtract.png │ ├── cel_overflow.aseprite │ ├── clean.sh │ ├── color-curve.aseprite │ ├── grayscale.aseprite │ ├── grayscale.png │ ├── indexed.aseprite │ ├── indexed_01.png │ ├── layers_and_tags.aseprite │ ├── layers_and_tags.png │ ├── layers_and_tags_01.png │ ├── layers_and_tags_02.png │ ├── layers_and_tags_03.png │ ├── layers_and_tags_04.png │ ├── linked_cels.aseprite │ ├── linked_cels_01.png │ ├── linked_cels_02.png │ ├── linked_cels_03.png │ ├── palette.aseprite │ ├── rawcel.aseprite │ ├── single_layer.png │ ├── slice.aseprite │ ├── slice_advanced.aseprite │ ├── tilemap.aseprite │ ├── tilemap.png │ ├── tilemap_empty_edges.aseprite │ ├── tilemap_empty_edges_0_0.png │ ├── tilemap_empty_edges_0_1.png │ ├── tilemap_grayscale.aseprite │ ├── tilemap_grayscale.png │ ├── tilemap_indexed.aseprite │ ├── tilemap_indexed.png │ ├── tilemap_multi.aseprite │ ├── tilemap_multi.png │ ├── tilemap_multi_map1.png │ ├── tilemap_multi_map2.png │ ├── tilemap_single_tile_1.png │ ├── tileset.aseprite │ ├── tileset.png │ ├── tileset_1.png │ ├── transparency.aseprite │ ├── transparency_01.png │ ├── transparency_02.png │ ├── user_data.aseprite │ ├── util_extrude.aseprite │ ├── util_extrude.png │ └── util_indexed.aseprite ├── aseprite │ ├── 1empty3.aseprite │ ├── 2f-index-3x3.aseprite │ ├── 2x2tilemap2x2tile.aseprite │ ├── 4f-index-4x4.aseprite │ ├── CODE_OF_CONDUCT.md │ ├── EULA.md │ ├── abcd.aseprite │ ├── bg-index-3.aseprite │ ├── file-tests-props.aseprite │ ├── groups2.aseprite │ ├── groups3abc.aseprite │ ├── link.aseprite │ ├── point2frames.aseprite │ ├── point4frames.aseprite │ ├── slices-moving.aseprite │ ├── slices.aseprite │ └── tags3.aseprite ├── blob │ ├── black-dot.aseprite │ ├── geralt.aseprite │ ├── hue.aseprite │ ├── indexed.aseprite │ ├── mango-panda.aseprite │ ├── marshmallow.aseprite │ └── saturation.aseprite ├── chunk_equal.odin ├── common.odin ├── community │ ├── 512x512-colinbellino.aseprite │ └── geralt-blob1807.aseprite └── raw_test.odin ├── types.odin ├── unmarshal.odin ├── util.odin ├── utils ├── TODO.md ├── animation.odin ├── blend.odin ├── common.odin ├── extract.odin ├── image.odin ├── sprite_sheet.odin ├── types.odin └── utils_internal.odin └── write.odin /.gitignore: -------------------------------------------------------------------------------- 1 | ols.json 2 | main.odin 3 | 4 | build 5 | _main 6 | tests/_utils/asefile 7 | tests/_utils/aseprite 8 | tests/_utils/blob 9 | tests/_utils/community 10 | 11 | *.exe 12 | *.ll 13 | *.obj 14 | *.a 15 | *.o 16 | *.pdb 17 | *.txt 18 | *.raw 19 | -------------------------------------------------------------------------------- /3rd party licenses/aseprite license: -------------------------------------------------------------------------------- 1 | END-USER LICENSE AGREEMENT FOR ASEPRITE 2 | 3 | IMPORTANT: PLEASE READ THE TERMS AND CONDITIONS OF THIS LICENSE AGREEMENT CAREFULLY BEFORE CONTINUING WITH THIS PROGRAM INSTALL. 4 | 5 | This End-User License Agreement ("EULA") is a legal agreement between you (either an individual or a single entity) and Igara Studio S.A. (hereinafter referred to as "Licensor"), for the software product(s) identified above which may include associated software components, media, printed materials, and "online" or electronic documentation ("SOFTWARE PRODUCT"). By installing, copying, or otherwise using the SOFTWARE PRODUCT, you agree to be bound by the terms of this EULA. This license agreement represents the entire agreement concerning the program between You and the Licensor, and it supersedes any prior proposal, representation, or understanding between the parties. If you do not agree to the terms of this EULA, do not install or use the SOFTWARE PRODUCT. 6 | 7 | The SOFTWARE PRODUCT is protected by copyright laws and international copyright treaties, as well as other intellectual property laws and treaties. The SOFTWARE PRODUCT is licensed, not sold. 8 | 9 | 1. GRANT OF LICENSE. 10 | The SOFTWARE PRODUCT is licensed as follows: 11 | (a) Installation and Use. 12 | The Licensor grants you the right to install and use copies of the SOFTWARE PRODUCT on your computer running a validly licensed copy of the operating system for which the SOFTWARE PRODUCT was designed. 13 | (b) Backup Copies. 14 | You may also make copies of the SOFTWARE PRODUCT as may be necessary for backup and archival purposes. 15 | 16 | 2. DESCRIPTION OF OTHER RIGHTS AND LIMITATIONS. 17 | (a) Maintenance of Copyright Notices. 18 | You must not remove or alter any copyright notices on any and all copies of the SOFTWARE PRODUCT. 19 | (b) Distribution. 20 | You may not distribute copies of the SOFTWARE PRODUCT to third parties. Evaluation versions available for download from the Licensor's websites may be freely distributed. 21 | (c) Prohibition on Reverse Engineering, Decompilation, and Disassembly. 22 | You may not reverse engineer, decompile, or disassemble the SOFTWARE PRODUCT, except and only to the extent that such activity is expressly permitted by applicable law notwithstanding this limitation. 23 | (d) Rental. 24 | You may not rent, lease, or lend the SOFTWARE PRODUCT. 25 | (e) Support Services. 26 | The Licensor may provide you with support services related to the SOFTWARE PRODUCT ("Support Services"). Any supplemental software code provided to you as part of the Support Services shall be considered part of the SOFTWARE PRODUCT and subject to the terms and conditions of this EULA. 27 | (f) Compliance with Applicable Laws. 28 | You must comply with all applicable laws regarding use of the SOFTWARE PRODUCT. 29 | (g) Source code. 30 | You may only compile and modify the source code of the SOFTWARE PRODUCT for your own personal purpose or to propose a contribution to the SOFTWARE PRODUCT. 31 | 32 | 3. TERMINATION 33 | Without prejudice to any other rights, the Licensor may terminate this EULA if you fail to comply with the terms and conditions of this EULA. In such event, you must destroy all copies of the SOFTWARE PRODUCT in your possession. 34 | 35 | 4. COPYRIGHT 36 | All title, including but not limited to copyrights, in and to the SOFTWARE PRODUCT and any copies thereof are owned by the Licensor or its suppliers. All title and intellectual property rights in and to the content which may be accessed through use of the SOFTWARE PRODUCT is the property of the respective content owner and may be protected by applicable copyright or other intellectual property laws and treaties. This EULA grants you no rights to use such content. All rights not expressly granted are reserved by the Licensor. 37 | 38 | 5. NO WARRANTIES 39 | The Licensor expressly disclaims any warranty for the SOFTWARE PRODUCT. The SOFTWARE PRODUCT is provided 'As Is' without any express or implied warranty of any kind, including but not limited to any warranties of merchantability, noninfringement, or fitness of a particular purpose. The Licensor does not warrant or assume responsibility for the accuracy or completeness of any information, text, graphics, links or other items contained within the SOFTWARE PRODUCT. The Licensor makes no warranties respecting any harm that may be caused by the transmission of a computer virus, worm, time bomb, logic bomb, or other such computer program. The Licensor further expressly disclaims any warranty or representation to Authorized Users or to any third party. 40 | 41 | 6. LIMITATION OF LIABILITY 42 | In no event shall the Licensor be liable for any damages (including, without limitation, lost profits, business interruption, or lost information) rising out of 'Authorized Users' use of or inability to use the SOFTWARE PRODUCT, even if the Licensor has been advised of the possibility of such damages. In no event will the Licensor be liable for loss of data or for indirect, special, incidental, consequential (including lost profit), or other damages based in contract, tort or otherwise. The Licensor shall have no liability with respect to the content of the SOFTWARE PRODUCT or any part thereof, including but not limited to errors or omissions contained therein, libel, infringements of rights of publicity, privacy, trademark rights, business interruption, personal injury, loss of privacy, moral rights or the disclosure of confidential information. -------------------------------------------------------------------------------- /3rd party licenses/libpixman license: -------------------------------------------------------------------------------- 1 | The following is the MIT license, agreed upon by most contributors. 2 | Copyright holders of new code should use this license statement where 3 | possible. They may also add themselves to the list below. 4 | 5 | /* 6 | * Copyright 1987, 1988, 1989, 1998 The Open Group 7 | * Copyright 1987, 1988, 1989 Digital Equipment Corporation 8 | * Copyright 1999, 2004, 2008 Keith Packard 9 | * Copyright 2000 SuSE, Inc. 10 | * Copyright 2000 Keith Packard, member of The XFree86 Project, Inc. 11 | * Copyright 2004, 2005, 2007, 2008, 2009, 2010 Red Hat, Inc. 12 | * Copyright 2004 Nicholas Miell 13 | * Copyright 2005 Lars Knoll & Zack Rusin, Trolltech 14 | * Copyright 2005 Trolltech AS 15 | * Copyright 2007 Luca Barbato 16 | * Copyright 2008 Aaron Plattner, NVIDIA Corporation 17 | * Copyright 2008 Rodrigo Kumpera 18 | * Copyright 2008 André Tupinambá 19 | * Copyright 2008 Mozilla Corporation 20 | * Copyright 2008 Frederic Plourde 21 | * Copyright 2009, Oracle and/or its affiliates. All rights reserved. 22 | * Copyright 2009, 2010 Nokia Corporation 23 | * 24 | * Permission is hereby granted, free of charge, to any person obtaining a 25 | * copy of this software and associated documentation files (the "Software"), 26 | * to deal in the Software without restriction, including without limitation 27 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 28 | * and/or sell copies of the Software, and to permit persons to whom the 29 | * Software is furnished to do so, subject to the following conditions: 30 | * 31 | * The above copyright notice and this permission notice (including the next 32 | * paragraph) shall be included in all copies or substantial portions of the 33 | * Software. 34 | * 35 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 36 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 37 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 38 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 39 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 40 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 41 | * DEALINGS IN THE SOFTWARE. 42 | */ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, blob1807 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Odin Aseprite 2 | Handler for Aseprite's `.ase`/`.aseprite`, `.aseprite-extension` & extended `.gpl` files writen in Odin. 3 | 4 | * `.\`: Main un/marshaler for `.ase` 5 | * `.\utils`: Creates Images & Animations from Documents 6 | * `.\raw`: un/marshals `.ase` exactly as given by the spec 7 | * `.\gpl`: extended & normal .gpl 8 | * `.\extensions`: .aseprite-extension. WIP 9 | * `.\tests`: test files 10 | 11 | ## Examples 12 | ### aseprite 13 | ```odin 14 | package main 15 | 16 | import "core:fmt" 17 | import ase "odin-aseprite" 18 | 19 | main :: proc() { 20 | data := #load("geralt.aseprite") 21 | 22 | doc: ase.Document 23 | defer ase.destroy_doc(&doc) 24 | 25 | umerr := ase.unmarshal(data[:], &doc) 26 | if umerr != nil { 27 | fmt.println(umerr) 28 | return 29 | } 30 | 31 | buf: [dynamic]byte 32 | defer delete(buf) 33 | 34 | written, merr := ase.marshal(&buf, &doc) 35 | if merr != nil { 36 | fmt.println(merr) 37 | return 38 | } 39 | } 40 | ``` 41 | 42 | ### utils 43 | ```odin 44 | package main 45 | 46 | import "core:fmt" 47 | import ase "odin-aseprite" 48 | import "odin-aseprite/utils" 49 | 50 | main :: proc() { 51 | data := #load("geralt.aseprite") 52 | 53 | doc: ase.Document 54 | defer ase.destroy_doc(&doc) 55 | 56 | umerr := ase.unmarshal(data[:], &doc) 57 | if umerr != nil { 58 | fmt.println(umerr) 59 | return 60 | } 61 | 62 | imgs, imgs_err := utils.get_all_images(&doc) 63 | defer utils.destroy(imgs) 64 | 65 | if imgs_err != nil { 66 | fmt.println(imgs_err) 67 | return 68 | } 69 | } 70 | ``` 71 | 72 | ### gpl 73 | ```odin 74 | package main 75 | 76 | import "core:fmt" 77 | import "odin-aseprite/gpl" 78 | 79 | main :: proc() { 80 | data := #load("geralt.gpl") 81 | 82 | palette, err := gpl.parse(data[:]) 83 | if err != nil { 84 | fmt.println(err) 85 | return 86 | } 87 | defer destroy_gpl(&palette) 88 | 89 | buf, err2 := gpl.to_bytes(palette) 90 | if err2 != nil { 91 | fmt.println(err2) 92 | return 93 | } 94 | defer delete(buf) 95 | } 96 | ``` 97 | 98 | 99 | ## Warnings 100 | ICC Colour Profiles aren't supported. The raw data will be saved to doc. 101 | 102 | ## Errors 103 | Any errors please make an issue or DM them to me, `blob1807`, on the [Odin Discord](https://discord.com/invite/sVBPHEv). 104 | If you DM me please include the offending file/s. 105 | 106 | If you want to test your own files for errors. Add them to a new folder in `./tests` and run `odin test .` in the `./tests` directory. 107 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Examples 4 | - [X] Utils 5 | - [X] Images 6 | - [X] Animation 7 | 8 | 9 | ## Utils 10 | - [X] Images 11 | - [X] Animation 12 | - [X] Sprite Sheets 13 | 14 | 15 | ## Fixes 16 | - [X] Fix User Data; again - Wasn't Broken I just an idiot 17 | 18 | 19 | ## Tests 20 | - [X] Rewrite tests 21 | 22 | 23 | ## General 24 | - [ ] Replace all simple `log.*` with `fast_log` 25 | 26 | ## ReadMe 27 | - [ ] Add bit about the bug with hue & sat blend modes -------------------------------------------------------------------------------- /ase.odin: -------------------------------------------------------------------------------- 1 | package aseprite_file_handler 2 | 3 | // https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md 4 | // https://github.com/alpine-alpaca/asefile 5 | // https://github.com/AristurtleDev/AsepriteDotNet/blob/main/source/AsepriteDotNet/Document/UserData.cs 6 | -------------------------------------------------------------------------------- /common.odin: -------------------------------------------------------------------------------- 1 | package aseprite_file_handler 2 | 3 | import "base:runtime" 4 | import "core:mem/virtual" 5 | 6 | @(require) import "core:log" 7 | 8 | 9 | @(private) 10 | destroy_value :: proc(p: ^Property_Value, alloc := context.allocator) -> (err: runtime.Allocator_Error) { 11 | context.allocator = alloc 12 | #partial switch &val in p { 13 | case string: 14 | delete(val) or_return 15 | case UD_Vec: 16 | for &v in val { 17 | destroy_value(&v) or_return 18 | } 19 | delete(val) or_return 20 | 21 | case Properties: 22 | for _, &v in val { 23 | destroy_value(&v) or_return 24 | } 25 | delete(val) or_return 26 | } 27 | 28 | return 29 | } 30 | 31 | destroy_doc :: proc(doc: ^Document) { 32 | virtual.arena_destroy(&doc.arena) 33 | } 34 | 35 | 36 | destroy_doc_alloc :: proc(doc: ^Document, alloc := context.allocator) -> (err: runtime.Allocator_Error) { 37 | context.allocator = alloc 38 | for &frame in doc.frames { 39 | for &chunk in frame.chunks { 40 | #partial switch &v in chunk { 41 | case Old_Palette_256_Chunk: 42 | for pack in v { 43 | delete(pack.colors) 44 | } 45 | delete(v) 46 | 47 | case Old_Palette_64_Chunk: 48 | for pack in v { 49 | delete(pack.colors) 50 | } 51 | delete(v) 52 | case Layer_Chunk: 53 | delete(v.name) 54 | 55 | case Cel_Chunk: 56 | switch &cel in v.cel { 57 | case Linked_Cel: 58 | case Raw_Cel: 59 | delete(cel.pixels) 60 | case Com_Image_Cel: 61 | delete(cel.pixels) 62 | case Com_Tilemap_Cel: 63 | delete(cel.tiles) 64 | } 65 | 66 | case Color_Profile_Chunk: 67 | switch icc in v.icc { 68 | case ICC_Profile: 69 | delete(icc) 70 | } 71 | 72 | case External_Files_Chunk: 73 | for &e in v { 74 | delete(e.file_name_or_id) 75 | } 76 | delete(v) 77 | 78 | case Mask_Chunk: 79 | delete(v.name) 80 | delete(v.bit_map_data) 81 | 82 | case Tags_Chunk: 83 | for &t in v { 84 | delete(t.name) 85 | } 86 | delete(v) 87 | 88 | case Palette_Chunk: 89 | for &e in v.entries { 90 | switch &s in e.name { 91 | case string: 92 | delete(s) 93 | } 94 | } 95 | delete(v.entries) 96 | 97 | case User_Data_Chunk: 98 | switch &s in v.text { 99 | case string: 100 | delete(s) 101 | } 102 | 103 | switch &m in v.maps { 104 | case Properties_Map: 105 | for _, &val in m { 106 | destroy_value(&val) 107 | } 108 | delete_map(m) 109 | } 110 | 111 | case Slice_Chunk: 112 | delete(v.name) 113 | delete(v.keys) 114 | 115 | case Tileset_Chunk: 116 | delete(v.name) 117 | switch &c in v.compressed { 118 | case Tileset_Compressed: 119 | delete(c) 120 | } 121 | } 122 | } 123 | delete(frame.chunks) 124 | } 125 | return delete(doc.frames) 126 | } 127 | 128 | 129 | destroy_chunk :: proc { 130 | _destroy_old_256, _destroy_old_64, _destroy_layer, _destroy_cel, 131 | _destroy_cel_extra, _destroy_color_profile, _destroy_external_files, 132 | _destroy_mask, _destroy_path, _destroy_tags, _destroy_palette, 133 | _destroy_user_data, _destroy_slice, _destroy_tileset, 134 | } 135 | 136 | @(private) 137 | _destroy_old_256 :: proc(c: Old_Palette_256_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) { 138 | for pack in c { 139 | delete(pack.colors, alloc) or_return 140 | } 141 | return delete(c, alloc) 142 | } 143 | 144 | @(private) 145 | _destroy_old_64 :: proc(c: Old_Palette_64_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) { 146 | for pack in c { 147 | delete(pack.colors, alloc) or_return 148 | } 149 | return delete(c, alloc) 150 | } 151 | 152 | @(private) 153 | _destroy_layer :: proc(c: Layer_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) { 154 | return delete(c.name, alloc) 155 | } 156 | 157 | @(private) 158 | _destroy_cel :: proc(c: Cel_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) { 159 | switch cel in c.cel { 160 | case Linked_Cel: 161 | case Raw_Cel: 162 | delete(cel.pixels, alloc) or_return 163 | case Com_Image_Cel: 164 | delete(cel.pixels, alloc) or_return 165 | case Com_Tilemap_Cel: 166 | delete(cel.tiles, alloc) or_return 167 | } 168 | return 169 | } 170 | 171 | @(private) 172 | _destroy_cel_extra :: proc(c: Cel_Extra_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) { 173 | return 174 | } 175 | 176 | @(private) 177 | _destroy_color_profile :: proc(c: Color_Profile_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) { 178 | switch icc in c.icc { 179 | case ICC_Profile: 180 | delete(icc, alloc) or_return 181 | } 182 | return 183 | } 184 | 185 | @(private) 186 | _destroy_external_files :: proc(c: External_Files_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) { 187 | for e in c { 188 | delete(e.file_name_or_id, alloc) or_return 189 | } 190 | return delete(c, alloc) 191 | } 192 | 193 | @(private) 194 | _destroy_mask :: proc(c: Mask_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) { 195 | delete(c.name, alloc) or_return 196 | return delete(c.bit_map_data, alloc) 197 | } 198 | 199 | @(private) 200 | _destroy_path :: proc(c: Path_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) { 201 | return 202 | } 203 | 204 | @(private) 205 | _destroy_tags :: proc(c: Tags_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) { 206 | for t in c { 207 | delete(t.name, alloc) or_return 208 | } 209 | return delete(c, alloc) 210 | } 211 | 212 | @(private) 213 | _destroy_palette :: proc(c: Palette_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) { 214 | for e in c.entries { 215 | switch s in e.name { 216 | case string: delete(s, alloc) or_return 217 | } 218 | } 219 | return delete(c.entries, alloc) 220 | } 221 | 222 | @(private) 223 | _destroy_user_data :: proc(c: User_Data_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) { 224 | switch &s in c.text { 225 | case string: 226 | delete(s, alloc) or_return 227 | } 228 | 229 | switch &m in c.maps { 230 | case Properties_Map: 231 | for _, &val in m { 232 | destroy_value(&val, alloc) or_return 233 | } 234 | delete_map(m) or_return 235 | } 236 | return 237 | } 238 | 239 | @(private) 240 | _destroy_slice :: proc(c: Slice_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) { 241 | delete(c.name, alloc) or_return 242 | return delete(c.keys, alloc) 243 | } 244 | 245 | @(private) 246 | _destroy_tileset :: proc(c: Tileset_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) { 247 | switch v in c.compressed { 248 | case Tileset_Compressed: 249 | delete(v, alloc) or_return 250 | } 251 | return delete(c.name, alloc) 252 | } 253 | -------------------------------------------------------------------------------- /example/ase_example.odin: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import "core:io" 4 | import "core:os" 5 | import "core:log" 6 | import "core:fmt" 7 | import "core:slice" 8 | import "core:bytes" 9 | 10 | import ase ".." 11 | 12 | 13 | ase_example :: proc() { 14 | data := #load("../tests/blob/geralt.aseprite") 15 | doc: ase.Document 16 | defer ase.destroy_doc(&doc) 17 | 18 | un_err := ase.unmarshal(&doc, data[:]) 19 | if un_err != nil { 20 | fmt.eprintln("Failed to Unmarshal my beloved, geralt.", un_err) 21 | return 22 | } 23 | 24 | fmt.println("Successfully Unmarshaled my beloved, geralt.") 25 | 26 | buf: [dynamic]byte 27 | defer delete(buf) 28 | 29 | written, m_err := ase.marshal(&doc, &buf) 30 | if m_err != nil { 31 | fmt.eprintln("Failed to Marshal my beloved, geralt.", m_err) 32 | return 33 | } 34 | 35 | fmt.println("Successfully Marshaled my beloved, geralt.") 36 | 37 | sus := os.write_entire_file("./out.aseprite", buf[:]) 38 | if !sus { 39 | fmt.eprintln("Failed to Write my beloved, geralt.") 40 | return 41 | } 42 | 43 | fmt.println("Successfully Wrote my beloved, geralt.") 44 | } 45 | 46 | 47 | read_only :: proc() { 48 | data := #load("../tests/blob/geralt.aseprite") 49 | r: bytes.Reader 50 | bytes.reader_init(&r, data[:]) 51 | ir, ok := io.to_reader(bytes.reader_to_stream(&r)) 52 | 53 | cs_buf := make([dynamic]ase.Cel_Chunk) 54 | defer { 55 | for c in cs_buf { 56 | ase.destroy_chunk(c) 57 | } 58 | delete(cs_buf) 59 | } 60 | err := ase.unmarshal_single_chunk(ir, &cs_buf) 61 | 62 | 63 | cm_buf := make([dynamic]ase.Chunk) 64 | defer { 65 | for c in cm_buf { 66 | #partial switch v in c { 67 | case ase.Cel_Chunk: ase.destroy_chunk(v) 68 | case ase.Cel_Extra_Chunk: ase.destroy_chunk(v) 69 | case ase.Tileset_Chunk: ase.destroy_chunk(v) 70 | } 71 | } 72 | delete(cm_buf) 73 | } 74 | set := ase.Chunk_Set{.cel, .cel_extra, .tileset} 75 | err = ase.unmarshal_multi_chunks(ir, &cm_buf, set) 76 | 77 | 78 | 79 | c_buf := make([dynamic]ase.Layer_Chunk) 80 | defer { 81 | for c in c_buf { 82 | ase.destroy_chunk(c) 83 | } 84 | delete(c_buf) 85 | } 86 | err = ase.unmarshal_chunks(ir, &c_buf) 87 | 88 | 89 | cmc_buf := make([dynamic]ase.Chunk) 90 | defer { 91 | for c in cmc_buf { 92 | #partial switch v in c { 93 | case ase.Cel_Chunk: ase.destroy_chunk(v) 94 | case ase.Cel_Extra_Chunk: ase.destroy_chunk(v) 95 | case ase.Tileset_Chunk: ase.destroy_chunk(v) 96 | } 97 | } 98 | delete(cmc_buf) 99 | } 100 | err = ase.unmarshal_chunks(ir, &cmc_buf, set) 101 | } 102 | -------------------------------------------------------------------------------- /example/example.odin: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import "core:log" 4 | import "core:mem" 5 | import "core:fmt" 6 | 7 | import ase ".." 8 | 9 | main :: proc() { 10 | track: mem.Tracking_Allocator 11 | mem.tracking_allocator_init(&track, context.allocator) 12 | context.allocator = mem.tracking_allocator(&track) 13 | context.logger = log.create_console_logger() 14 | 15 | defer { 16 | log.destroy_console_logger(context.logger) 17 | for _, leak in track.allocation_map { 18 | fmt.printf("%v leaked %v bytes\n", leak.location, leak.size) 19 | } 20 | 21 | for bad_free in track.bad_free_array { 22 | fmt.printf("%v allocation %p was freed badly\n", bad_free.location, bad_free.memory) 23 | } 24 | } 25 | 26 | ase_example() 27 | read_only() 28 | 29 | single_image() 30 | all_images() 31 | nth_image() 32 | 33 | animation() 34 | animation_tag() 35 | animation_images() 36 | 37 | } 38 | -------------------------------------------------------------------------------- /example/utils_example.odin: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import "core:log" 4 | import "core:fmt" 5 | 6 | import ase "../" 7 | import "../utils" 8 | 9 | 10 | // Convert first frame into an Image. 11 | single_image :: proc() { 12 | data := #load("../tests/blob/geralt.aseprite") 13 | doc: ase.Document 14 | defer ase.destroy_doc(&doc) 15 | 16 | doc_err := ase.unmarshal(&doc, data[:]) 17 | if doc_err != nil { 18 | fmt.eprintln("Fail to unmarshal:", doc_err) 19 | return 20 | } 21 | 22 | img, img_err := utils.get_image(&doc) 23 | if img_err != nil { 24 | fmt.eprintln("Fail to get image:", img_err) 25 | return 26 | } 27 | defer utils.destroy(img) 28 | } 29 | 30 | 31 | // Convert all frames into an Image. 32 | all_images :: proc() { 33 | data := #load("../tests/blob/geralt.aseprite") 34 | doc: ase.Document 35 | defer ase.destroy_doc(&doc) 36 | 37 | doc_err := ase.unmarshal(&doc, data[:]) 38 | if doc_err != nil { 39 | fmt.eprintln("Fail to unmarshal:", doc_err) 40 | return 41 | } 42 | 43 | imgs, img_err := utils.get_all_images(&doc) 44 | if img_err != nil { 45 | fmt.eprintln("Fail to get all imags:", img_err) 46 | return 47 | } 48 | defer utils.destroy(imgs) 49 | 50 | } 51 | 52 | 53 | // Convert nth frame into Images. 54 | nth_image :: proc() { 55 | data := #load("../tests/blob/marshmallow.aseprite") 56 | doc: ase.Document 57 | defer ase.destroy_doc(&doc) 58 | 59 | doc_err := ase.unmarshal(&doc, data[:]) 60 | if doc_err != nil { 61 | fmt.eprintln("Fail to unmarshal:", doc_err) 62 | return 63 | } 64 | 65 | img, img_err := utils.get_image(&doc, 7) 66 | if img_err != nil { 67 | fmt.eprintln("Fail to get image:", img_err) 68 | return 69 | } 70 | defer utils.destroy(img) 71 | } 72 | 73 | 74 | // Create animation frames. 75 | animation :: proc() { 76 | data := #load("../tests/blob/marshmallow.aseprite") 77 | doc: ase.Document 78 | defer ase.destroy_doc(&doc) 79 | 80 | doc_err := ase.unmarshal(&doc, data) 81 | if doc_err != nil { 82 | fmt.eprintln("Fail to unmarshal:", doc_err) 83 | return 84 | } 85 | 86 | anim: utils.Animation 87 | anim_err := utils.get_animation(&doc, &anim) 88 | if anim_err != nil { 89 | fmt.eprintln("Fail to make animation:", anim_err) 90 | return 91 | } 92 | defer utils.destroy(&anim) 93 | 94 | } 95 | 96 | 97 | // Create animation frames, only in tag. 98 | animation_tag :: proc() { 99 | data := #load("../tests/blob/marshmallow.aseprite") 100 | doc: ase.Document 101 | defer ase.destroy_doc(&doc) 102 | 103 | doc_err := ase.unmarshal(&doc, data) 104 | if doc_err != nil { 105 | fmt.eprintln("Fail to unmarshal:", doc_err) 106 | return 107 | } 108 | 109 | anim: utils.Animation 110 | anim_err := utils.get_animation(&doc, &anim, "Squish") 111 | if anim_err != nil { 112 | fmt.eprintln("Fail to make animation:", anim_err) 113 | return 114 | } 115 | defer utils.destroy(&anim) 116 | } 117 | 118 | 119 | // Create animation frames from bytes. 120 | animation_images :: proc() { 121 | data := #load("../tests/blob/marshmallow.aseprite") 122 | doc: ase.Document 123 | defer ase.destroy_doc(&doc) 124 | 125 | doc_err := ase.unmarshal(&doc, data) 126 | if doc_err != nil { 127 | fmt.eprintln("Fail to unmarshal:", doc_err) 128 | return 129 | } 130 | 131 | imgs, img_err := utils.get_all_images_bytes(&doc) 132 | if img_err != nil { 133 | fmt.eprintln("Fail to get all imags:", img_err) 134 | return 135 | } 136 | defer utils.destroy(imgs) 137 | 138 | md := utils.get_metadata(doc.header) 139 | 140 | anim: utils.Animation 141 | anim_err := utils.get_animation(imgs, md, &anim) 142 | if anim_err != nil { 143 | fmt.eprintln("Fail to animation:", anim_err) 144 | return 145 | } 146 | defer utils.destroy(&anim) 147 | } 148 | 149 | 150 | upscale_image :: proc() { 151 | data := #load("../tests/blob/geralt.aseprite") 152 | doc: ase.Document 153 | derr := ase.unmarshal(&doc, data[:]) 154 | img, ierr := utils.get_image(&doc) // default frame idx is 0 155 | big_img, bierr := utils.upscale_image(img, 100) // default is 10 156 | } 157 | 158 | upscale_all_images :: proc() { 159 | data := #load("../tests/blob/geralt.aseprite") 160 | doc: ase.Document 161 | derr := ase.unmarshal(&doc, data[:]) 162 | imgs, ierr := utils.get_all_images(&doc) 163 | big_imgs, bierr := utils.upscale_all(imgs) // default is 10 164 | } 165 | -------------------------------------------------------------------------------- /extentions/common.odin: -------------------------------------------------------------------------------- 1 | package aseprite_extentions 2 | 3 | import "core:os" 4 | import "core:fmt" 5 | import "core:c/libc" 6 | import "core:strings" 7 | import "core:reflect" 8 | 9 | 10 | read_file :: proc(path: string, ext: ext_type) { 11 | // From: https://superuser.com/a/1473255 12 | when ODIN_OS == .Windows || ODIN_OS == .Linux || ODIN_OS == .FreeBSD { 13 | cmd := fmt.aprintf("tar -xf \"%v\"", path) 14 | } else when ODIN_OS == .Darwin { 15 | cmd := fmt.aprintf("unzip \"%v\"", path) 16 | } 17 | 18 | dir := fmt.aprintf("./temp_%v", reflect.enum_name_from_value(ext)) 19 | err := os.make_directory(dir) 20 | if err != 0 { 21 | fmt.println(err) 22 | return 23 | } 24 | 25 | cur_dir := os.get_current_directory() 26 | err = os.set_current_directory(dir) 27 | if err != 0 { 28 | fmt.println(err) 29 | return 30 | } 31 | libc.system(strings.clone_to_cstring(cmd)) 32 | } -------------------------------------------------------------------------------- /extentions/dithering matrices.odin: -------------------------------------------------------------------------------- 1 | package aseprite_extentions 2 | -------------------------------------------------------------------------------- /extentions/extentions.odin: -------------------------------------------------------------------------------- 1 | package aseprite_extentions 2 | 3 | import "base:runtime" 4 | import "core:os" 5 | import "core:io" 6 | import "core:fmt" 7 | import "core:slice" 8 | import "core:strconv" 9 | import "core:strings" 10 | import "core:encoding/xml" 11 | import "core:compress/zlib" 12 | import "core:encoding/json" 13 | 14 | import ase ".." 15 | 16 | ext_type :: enum { 17 | None, 18 | Key, 19 | Themes, 20 | Palettes, 21 | Languages, 22 | Dithering_Matrices, 23 | Plugins_with_Scripts, 24 | } 25 | 26 | // https://www.aseprite.org/docs/extensions/ 27 | main :: proc() { 28 | fmt.println("Hellope") 29 | } 30 | -------------------------------------------------------------------------------- /extentions/keys.odin: -------------------------------------------------------------------------------- 1 | package aseprite_extentions 2 | -------------------------------------------------------------------------------- /extentions/languages.odin: -------------------------------------------------------------------------------- 1 | package aseprite_extentions 2 | -------------------------------------------------------------------------------- /extentions/pallettes.odin: -------------------------------------------------------------------------------- 1 | package aseprite_extentions 2 | -------------------------------------------------------------------------------- /extentions/plugins with scriptes.odin: -------------------------------------------------------------------------------- 1 | package aseprite_extentions 2 | -------------------------------------------------------------------------------- /extentions/themes.odin: -------------------------------------------------------------------------------- 1 | package aseprite_extentions 2 | -------------------------------------------------------------------------------- /gpl/extended_gpl.odin: -------------------------------------------------------------------------------- 1 | package extended_gpl_handler 2 | 3 | import "base:runtime" 4 | import "core:io" 5 | import "core:os" 6 | import "core:fmt" 7 | import "core:strings" 8 | import "core:strconv" 9 | 10 | // https://github.com/aseprite/aseprite/blob/main/docs/gpl-palette-extension.md 11 | // https://developer.gimp.org/core/standards/gpl/ 12 | 13 | GPL_Palette :: struct { 14 | raw: string `fmt:"-"`, 15 | name: string, 16 | colums: int, 17 | rgba: bool, 18 | colors: [dynamic]Color 19 | } 20 | 21 | Color :: struct { 22 | using color: [4]byte, 23 | name: string 24 | } 25 | 26 | GPL_Error :: enum { 27 | None, 28 | Invalid_Palette, 29 | Bad_Magic_Number, 30 | Cant_Parse_Columns, 31 | Cant_Parse_Color, 32 | } 33 | 34 | Errors :: union #shared_nil {GPL_Error, runtime.Allocator_Error} 35 | 36 | from_string :: proc(data: string, alloc := context.allocator) -> (parsed: GPL_Palette, err: Errors) { 37 | parsed.colors = make([dynamic]Color, alloc) or_return 38 | 39 | parsed.raw = data 40 | s := parsed.raw 41 | index := strings.index_rune(s, '\n') 42 | if index == -1 || s[:index] != "GIMP Palette" { 43 | return {}, .Bad_Magic_Number 44 | } 45 | 46 | s = s[index+1:] 47 | index = strings.index_rune(s, '\n') 48 | 49 | for s[0] == '#' { 50 | s = s[index+1:] 51 | index = strings.index_rune(s, '\n') 52 | if index == -1 { 53 | return {}, .Invalid_Palette 54 | } 55 | } 56 | 57 | for { 58 | index = strings.index_rune(s, '\n') 59 | if index == -1 { index = len(s)-1 } 60 | if s[0] == '#' {} 61 | else if strings.has_prefix(s, "Name: ") { 62 | i := strings.index(s, " ") 63 | parsed.name = strings.trim_space(s[i:index]) 64 | 65 | } else if strings.has_prefix(s, "Channels: ") { 66 | if strings.has_suffix(s[:index], "RGBA") { 67 | parsed.rgba = true 68 | } 69 | 70 | } else if strings.has_prefix(s, "Colums: ") { 71 | i := strings.index(s, " ") 72 | n, n_ok := strconv.parse_int(strings.trim_space(s[i:index])) 73 | if !n_ok { return {}, .Cant_Parse_Columns } 74 | parsed.colums = n 75 | 76 | } else { 77 | break 78 | } 79 | s = s[index+1:] 80 | } 81 | 82 | for len(s) != 0 && index != len(s) { 83 | index = strings.index_rune(s, '\n') 84 | if index == -1 { index = len(s) } 85 | if s[0] != '#' { 86 | color: Color 87 | color.a = 255 88 | line := strings.trim_left_space(s[:index]) 89 | 90 | i := strings.index_rune(line, ' ') 91 | n, n_ok := strconv.parse_int(line[:i]) 92 | if !n_ok { return {}, .Cant_Parse_Color } 93 | color.r = byte(n) 94 | 95 | line = strings.trim_left_space(line[i:]) 96 | i = strings.index_rune(line, ' ') 97 | n, n_ok = strconv.parse_int(line[:i]) 98 | if !n_ok { return {}, .Cant_Parse_Color } 99 | color.g = byte(n) 100 | 101 | line = strings.trim_left_space(line[i:]) 102 | i = strings.index_rune(line, ' ') 103 | if i == -1 {i = len(line)-1} 104 | n, n_ok = strconv.parse_int(line[:i]) 105 | if !n_ok { return {}, .Cant_Parse_Color } 106 | color.b = byte(n) 107 | 108 | if parsed.rgba { 109 | line = strings.trim_left_space(line[i:]) 110 | i = strings.index_rune(line, ' ') 111 | if i == -1 {i = len(line)} 112 | n, n_ok = strconv.parse_int(line[:i]) 113 | if !n_ok { return {}, .Cant_Parse_Color } 114 | color.a = byte(n) 115 | } else { 116 | color.a = 255 117 | } 118 | color.name = strings.trim_space(line[i:]) 119 | append(&parsed.colors, color) or_return 120 | } 121 | if index == len(s) { 122 | break 123 | } 124 | s = s[index+1:] 125 | } 126 | return 127 | } 128 | 129 | from_bytes :: proc(data: []byte) -> (parsed: GPL_Palette, err: Errors) { 130 | return from_string(string(data)) 131 | } 132 | 133 | parse :: proc {from_string, from_bytes} 134 | 135 | 136 | to_bytes :: proc(pal: GPL_Palette, alloc := context.allocator) -> (data: []byte, err: runtime.Allocator_Error) { 137 | // len("GIMP Palette\nName: \nChannels: RGBA\nColums: 255\n") == 47 138 | sb := strings.builder_make(0, 47 + len(pal.name) + len(pal.colors), alloc) or_return 139 | strings.write_string(&sb, "GIMP Palette\n") 140 | 141 | if len(pal.name) != 0 { 142 | strings.write_string(&sb, "Name: ") 143 | strings.write_string(&sb, pal.name) 144 | strings.write_byte(&sb, '\n') 145 | } 146 | if pal.colums != 0 { 147 | strings.write_string(&sb, "Colums: ") 148 | strings.write_int(&sb, pal.colums) 149 | strings.write_byte(&sb, '\n') 150 | } 151 | 152 | strings.write_string(&sb, "Channels: RGBA\n#\n") 153 | 154 | for color in pal.colors { 155 | strings.write_int(&sb, int(color.r)) 156 | strings.write_byte(&sb, ' ') 157 | strings.write_int(&sb, int(color.g)) 158 | strings.write_byte(&sb, ' ') 159 | strings.write_int(&sb, int(color.b)) 160 | strings.write_byte(&sb, ' ') 161 | strings.write_int(&sb, int(color.a)) 162 | strings.write_byte(&sb, ' ') 163 | strings.write_string(&sb, color.name) 164 | strings.write_byte(&sb, '\n') 165 | 166 | } 167 | 168 | return sb.buf[:], nil 169 | } 170 | 171 | to_string :: proc(pal: GPL_Palette, alloc := context.allocator) -> (data: string, err: runtime.Allocator_Error) { 172 | return string(to_bytes(pal, alloc) or_return), nil 173 | } 174 | 175 | destroy_gpl :: proc(pal: ^GPL_Palette, alloc := context.allocator) -> runtime.Allocator_Error { 176 | delete(pal.colors) or_return 177 | pal.colors = nil 178 | return nil 179 | } -------------------------------------------------------------------------------- /gpl/utils/palette_conv.odin: -------------------------------------------------------------------------------- 1 | package aseprite_extended_gpl_utility 2 | 3 | import ase "../.." 4 | import gpl ".." 5 | import "../../utils" 6 | import "core:slice" 7 | 8 | // TODO: Move to parent folder 9 | 10 | gpl_to_ase :: proc(gpl_pal: gpl.GPL_Palette, alloc := context.allocator) -> (pal: ase.Palette_Chunk, err: gpl.Errors) { 11 | pal.entries = make([]ase.Palette_Entry, len(gpl_pal.colors)) or_return 12 | pal.size = ase.DWORD(len(gpl_pal.colors)) 13 | pal.last_index = pal.size 14 | 15 | for &en, i in pal.entries { 16 | en.color = gpl_pal.colors[i].color 17 | if gpl_pal.colors[i].name != "" { 18 | en.name = gpl_pal.colors[i].name 19 | } 20 | } 21 | 22 | return 23 | } 24 | 25 | gpl_to_old_packet :: proc(gpl_pal: gpl.GPL_Palette, alloc := context.allocator) -> (pack: []ase.Old_Palette_Packet, err: gpl.Errors) { 26 | context.allocator = alloc 27 | buf := make([dynamic]ase.Old_Palette_Packet, 0, len(gpl_pal.colors) / 256 + 1) or_return 28 | 29 | size := len(gpl_pal.colors) 30 | pos: int 31 | for size > 256 { 32 | pal: ase.Old_Palette_Packet 33 | pal.num_colors = 0 34 | pal_buf := make([]ase.Color_RGB, 256) or_return 35 | 36 | for i in 0..<256 { 37 | pal_buf[i] = gpl_pal.colors[pos].color.rgb 38 | pos += 1 39 | } 40 | 41 | pal.colors = pal_buf[:] 42 | 43 | append(&buf, pal) or_return 44 | size -= 256 45 | } 46 | 47 | if size > 0 { 48 | pal: ase.Old_Palette_Packet 49 | pal.num_colors = 0 50 | pal_buf := make([]ase.Color_RGB, size) or_return 51 | 52 | for i in 0.. (res: ase.Old_Palette_256_Chunk, err: gpl.Errors) { 66 | buf := gpl_to_old_packet(gpl_pal, alloc) or_return 67 | res = transmute(ase.Old_Palette_256_Chunk)buf 68 | return 69 | } 70 | 71 | gpl_to_old_64 :: proc(gpl_pal: gpl.GPL_Palette, alloc := context.allocator) -> (res: ase.Old_Palette_64_Chunk, err: gpl.Errors) { 72 | buf := gpl_to_old_packet(gpl_pal, alloc) or_return 73 | res = transmute(ase.Old_Palette_64_Chunk)buf 74 | return 75 | } 76 | 77 | 78 | doc_to_gpl :: proc(doc: ^ase.Document, gpl_pal: ^gpl.GPL_Palette, alloc := context.allocator) -> (err: gpl.Errors) { 79 | has_new := utils.has_new_palette(doc) 80 | for frame in doc.frames { 81 | for chunk in frame.chunks { 82 | #partial switch &cel in chunk { 83 | case ase.Palette_Chunk: 84 | new_to_gpl(&cel, gpl_pal, alloc) or_return 85 | case ase.Old_Palette_256_Chunk: 86 | if !has_new { 87 | old_256_to_gpl(cel, gpl_pal, alloc) or_return 88 | } 89 | case ase.Old_Palette_64_Chunk: 90 | if !has_new { 91 | old_64_to_gpl(cel, gpl_pal, alloc) or_return 92 | } 93 | 94 | } 95 | } 96 | } 97 | return 98 | } 99 | 100 | new_to_gpl :: proc(data: ^ase.Palette_Chunk, pal: ^gpl.GPL_Palette, alloc := context.allocator) -> (err: gpl.Errors) { 101 | pal.rgba = true 102 | reserve(&pal.colors, len(data.entries)) or_return 103 | 104 | for c in data.entries { 105 | append_elem(&pal.colors, gpl.Color{color=c.color}) or_return 106 | } 107 | 108 | return 109 | } 110 | 111 | old_packet_to_gpl :: proc(pack: []ase.Old_Palette_Packet, gpl_pal: ^gpl.GPL_Palette, alloc := context.allocator) -> (err: gpl.Errors) { 112 | for p in pack { 113 | for c in p.colors { 114 | append(&gpl_pal.colors, gpl.Color{color={c.r, c.g, c.b, 255}}) 115 | } 116 | } 117 | return 118 | } 119 | 120 | old_64_to_gpl :: proc(pal: ase.Old_Palette_64_Chunk, gpl_pal: ^gpl.GPL_Palette, alloc := context.allocator) -> (err: gpl.Errors) { 121 | return ase_to_gpl(transmute([]ase.Old_Palette_Packet)pal, gpl_pal, alloc) 122 | } 123 | 124 | old_256_to_gpl :: proc(pal: ase.Old_Palette_256_Chunk, gpl_pal: ^gpl.GPL_Palette, alloc := context.allocator) -> (err: gpl.Errors) { 125 | return ase_to_gpl(transmute([]ase.Old_Palette_Packet)pal, gpl_pal, alloc) 126 | } 127 | 128 | ase_to_gpl :: proc{doc_to_gpl, new_to_gpl, old_64_to_gpl, old_packet_to_gpl, old_256_to_gpl} 129 | 130 | 131 | gpl_to_utils :: proc(gpl_pal: ^gpl.GPL_Palette) -> (pal: utils.Palette) { 132 | return slice.reinterpret(utils.Palette, gpl_pal.colors[:]) 133 | } 134 | 135 | utils_to_gpl :: proc(pal: utils.Palette, gpl_pal: ^gpl.GPL_Palette, name := "", alloc := context.allocator) -> (err: gpl.Errors) { 136 | gpl_pal.rgba = true 137 | if name != "" { 138 | gpl_pal.name = name 139 | } 140 | append(&gpl_pal.colors, ..slice.reinterpret([]gpl.Color, pal)) or_return 141 | return nil 142 | } -------------------------------------------------------------------------------- /mod.pkg: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.1", 3 | "description": "Handler for Aseprite's file formats.", 4 | "url": "https://github.com/blob1807/odin-aseprite", 5 | "readme": "README.md", 6 | "license": "BSD 3-Clause", 7 | "keywords": ["Aseprite", "ase"] 8 | } 9 | -------------------------------------------------------------------------------- /raw/common.odin: -------------------------------------------------------------------------------- 1 | package raw_aseprite_file_handler 2 | 3 | destroy_doc :: proc(doc: ^ASE_Document) { 4 | destroy_map :: proc(m: UD_Properties_Map) { 5 | for p in m.properties { 6 | destroy_value(p.data) 7 | } 8 | } 9 | 10 | destroy_value :: proc(p: UD_Property_Value) { 11 | #partial switch v in p { 12 | case UD_Properties_Map: 13 | destroy_map(p.(UD_Properties_Map)) 14 | delete(v.properties) 15 | case UD_Vec: 16 | if v.type == 0 { 17 | for n in v.data.([]Vec_Diff) { 18 | destroy_value(n.data) 19 | } 20 | delete(v.data.([]Vec_Diff)) 21 | } else { 22 | for n in v.data.([]UD_Property_Value) { 23 | destroy_value(n) 24 | } 25 | delete(v.data.([]UD_Property_Value)) 26 | } 27 | } 28 | } 29 | 30 | for &frame in doc.frames { 31 | for &chunk in frame.chunks { 32 | #partial switch &c in chunk.data { 33 | case Old_Palette_256_Chunk: 34 | for pal in c.packets { 35 | delete(pal.colors) 36 | } 37 | delete(c.packets) 38 | 39 | case Old_Palette_64_Chunk: 40 | for pal in c.packets { 41 | delete(pal.colors) 42 | } 43 | delete(c.packets) 44 | 45 | case Cel_Chunk: 46 | #partial switch &cel in c.cel { 47 | case Com_Tilemap_Cel: 48 | if cel.did_com { 49 | delete(cel.tiles) 50 | } 51 | case Com_Image_Cel: 52 | if cel.did_com { 53 | delete(cel.pixel) 54 | } 55 | case Raw_Cel: 56 | delete(cel.pixel) 57 | } 58 | 59 | case External_Files_Chunk: 60 | delete(c.entries) 61 | 62 | case Mask_Chunk: 63 | delete(c.bit_map_data) 64 | 65 | case Tags_Chunk: 66 | delete(c.tags) 67 | 68 | case Palette_Chunk: 69 | delete(c.entries) 70 | 71 | case User_Data_Chunk: 72 | if (c.flags & 4) == 4 { 73 | for mp in c.properties.properties_map { 74 | destroy_map(mp) 75 | delete(mp.properties) 76 | } 77 | delete(c.properties.properties_map) 78 | } 79 | 80 | case Slice_Chunk: 81 | delete(c.data) 82 | 83 | case Tileset_Chunk: 84 | if (c.flags & 2) == 2 && c.compressed.did_com { 85 | delete(c.compressed.tiles) 86 | } 87 | } 88 | } 89 | delete(frame.chunks) 90 | } 91 | 92 | delete(doc.frames) 93 | } 94 | 95 | upgrade_doc :: proc(doc: ^ASE_Document, allocator := context.allocator) -> (err: Doc_Upgrade_Error) { 96 | for &frame in doc.frames { 97 | frame.header.num_of_chunks = DWORD(frame.header.old_num_of_chunks) 98 | frame.header.old_num_of_chunks = 0xFFFF 99 | 100 | for &chunk in frame.chunks { 101 | #partial switch &v in chunk.data { 102 | case Old_Palette_256_Chunk: 103 | chunk.type = .palette 104 | new_chunk: Palette_Chunk 105 | 106 | entries := make([dynamic]Palette_Entry, allocator=allocator) or_return 107 | defer delete(entries) 108 | 109 | for pak in v.packets { 110 | for c in pak.colors { 111 | pal: Palette_Entry 112 | pal.red = c[0] 113 | pal.green = c[1] 114 | pal.blue = c[2] 115 | pal.alpha = 255 116 | append(&entries) 117 | } 118 | } 119 | 120 | new_chunk.entries = make([]Palette_Entry, len(entries), allocator) or_return 121 | copy(new_chunk.entries[:], entries[:]) 122 | 123 | new_chunk.size = DWORD(len(entries)) 124 | new_chunk.last_index = DWORD(len(entries) - 1) 125 | 126 | chunk.data = new_chunk 127 | 128 | case Old_Palette_64_Chunk: 129 | // https://github.com/alpine-alpaca/asefile/blob/main/src/palette.rs#L134 130 | scale :: proc(c: BYTE) -> (res: BYTE, err: Doc_Upgrade_Error) { 131 | if c > 63 { 132 | return c, .Palette_Color_To_Big 133 | } 134 | return c << 2 | c >> 4, nil 135 | } 136 | 137 | chunk.type = .palette 138 | new_chunk: Palette_Chunk 139 | 140 | entries := make([dynamic]Palette_Entry, allocator=allocator) or_return 141 | defer delete(entries) 142 | 143 | for pak in v.packets { 144 | for c in pak.colors { 145 | pal: Palette_Entry 146 | pal.red = scale(c[0]) or_return 147 | pal.green = scale(c[1]) or_return 148 | pal.blue = scale(c[2]) or_return 149 | pal.alpha = 255 150 | append(&entries) 151 | } 152 | } 153 | 154 | new_chunk.entries = make([]Palette_Entry, len(entries), allocator) or_return 155 | copy(new_chunk.entries[:], entries[:]) 156 | 157 | new_chunk.size = DWORD(len(entries)) 158 | new_chunk.last_index = DWORD(len(entries) - 1) 159 | 160 | chunk.data = new_chunk 161 | 162 | case Color_Profile_Chunk: 163 | if v.type == 0 { 164 | v.type = 1 165 | } 166 | } 167 | } 168 | } 169 | 170 | return 171 | } -------------------------------------------------------------------------------- /raw/gpl.odin: -------------------------------------------------------------------------------- 1 | package raw_aseprite_file_handler 2 | 3 | import "base:runtime" 4 | import "../gpl" 5 | 6 | 7 | from_old_256_to_gpl :: proc(data: Old_Palette_256_Chunk, pal: ^gpl.GPL_Palette) -> (err: runtime.Allocator_Error) { 8 | pal.rgba = true 9 | size: int 10 | for pak in data.packets { 11 | size += int(pak.num_colors) 12 | } 13 | reserve_dynamic_array(&pal.colors, size) or_return 14 | 15 | for pak in data.packets { 16 | for c in pak.colors { 17 | color: gpl.Color 18 | color.r = c[0] 19 | color.g = c[1] 20 | color.b = c[2] 21 | color.a = 255 22 | append(&pal.colors, color) or_return 23 | } 24 | } 25 | return 26 | } 27 | 28 | from_old_64_to_gpl :: proc(data: Old_Palette_64_Chunk, pal: ^gpl.GPL_Palette) -> (err: runtime.Allocator_Error) { 29 | pal.rgba = true 30 | size: int 31 | for pak in data.packets { 32 | size += int(pak.num_colors) 33 | } 34 | reserve_dynamic_array(&pal.colors, size) or_return 35 | 36 | for pak in data.packets { 37 | for c in pak.colors { 38 | color: gpl.Color 39 | color.r = c[0] 40 | color.g = c[1] 41 | color.b = c[2] 42 | color.a = 255 43 | append(&pal.colors, color) or_return 44 | } 45 | } 46 | return 47 | } 48 | from_palette_to_gpl:: proc(data: Palette_Chunk, pal: ^gpl.GPL_Palette) -> (err: runtime.Allocator_Error){ 49 | pal.rgba = true 50 | reserve_dynamic_array(&pal.colors, len(data.entries)) or_return 51 | 52 | for c in data.entries { 53 | color: gpl.Color 54 | color.r = c.red 55 | color.g = c.green 56 | color.b = c.blue 57 | color.a = c.alpha 58 | append(&pal.colors, color) or_return 59 | } 60 | 61 | return 62 | } 63 | 64 | to_gpl :: proc{from_old_256_to_gpl, from_old_64_to_gpl, from_palette_to_gpl} 65 | 66 | 67 | from_gpl_to_old_256 :: proc(data: gpl.GPL_Palette, pal: ^Old_Palette_256_Chunk, allocator := context.allocator)-> (err: runtime.Allocator_Error) { 68 | size := len(data.colors) 69 | pals := size / 256 + 1 70 | cols := size % 256 71 | pos: int 72 | 73 | pal.packets = make_slice([]Old_Palette_Packet, pals, allocator) or_return 74 | pal.size = WORD(pals) 75 | 76 | for &pak in pal.packets { 77 | pak.colors = make_slice([][3]BYTE, cols, allocator) or_return 78 | pak.num_colors = BYTE(cols) 79 | 80 | for &c in pak.colors { 81 | c[0] = BYTE(data.colors[pos].r) 82 | c[1] = BYTE(data.colors[pos].g) 83 | c[2] = BYTE(data.colors[pos].b) 84 | pos += 1 85 | } 86 | size -= 256 87 | cols = size % 256 88 | } 89 | 90 | return 91 | } 92 | 93 | from_gpl_to_old_64 :: proc(data: gpl.GPL_Palette, pal: ^Old_Palette_64_Chunk, allocator := context.allocator) -> (err: runtime.Allocator_Error) { 94 | size := len(data.colors) 95 | pals := size / 256 + 1 96 | cols := size % 256 97 | pos: int 98 | 99 | pal.packets = make_slice([]Old_Palette_Packet, pals, allocator) or_return 100 | pal.size = WORD(pals) 101 | 102 | for &pak in pal.packets { 103 | pak.colors = make_slice([][3]BYTE, cols, allocator) or_return 104 | pak.num_colors = BYTE(cols) 105 | 106 | for &c in pak.colors { 107 | c[0] = BYTE(data.colors[pos].r) 108 | c[1] = BYTE(data.colors[pos].g) 109 | c[2] = BYTE(data.colors[pos].b) 110 | pos += 1 111 | } 112 | size -= 256 113 | cols = size % 256 114 | } 115 | 116 | return 117 | } 118 | from_gpl_to_palette :: proc(data: gpl.GPL_Palette, pal: ^Palette_Chunk, allocator := context.allocator) -> (err: runtime.Allocator_Error) { 119 | pal.entries = make_slice([]Palette_Entry, len(data.colors), allocator) or_return 120 | pal.size = DWORD(len(data.colors)) 121 | 122 | for c, pos in data.colors { 123 | pal.entries[pos].red = BYTE(c.r) 124 | pal.entries[pos].green = BYTE(c.g) 125 | pal.entries[pos].blue = BYTE(c.b) 126 | pal.entries[pos].alpha = BYTE(c.a) 127 | } 128 | 129 | return 130 | } 131 | 132 | from_gpl :: proc{from_gpl_to_old_256, from_gpl_to_old_64, from_gpl_to_palette} -------------------------------------------------------------------------------- /raw/types.odin: -------------------------------------------------------------------------------- 1 | package raw_aseprite_file_handler 2 | 3 | import "base:runtime" 4 | 5 | ASE_Unmarshal_Errors :: enum { 6 | Bad_File_Magic_Number, 7 | Bad_Frame_Magic_Number, 8 | Bad_User_Data_Type, 9 | } 10 | ASE_Unmarshal_Error :: union #shared_nil {ASE_Unmarshal_Errors, runtime.Allocator_Error} 11 | 12 | ASE_Marshal_Errors :: enum { 13 | Buffer_Not_Big_Enough, 14 | Invalid_Chunk_Type, 15 | } 16 | ASE_Marshal_Error :: union #shared_nil {ASE_Marshal_Errors, runtime.Allocator_Error} 17 | 18 | Doc_Upgrade_Errors :: enum { 19 | Palette_Color_To_Big, 20 | } 21 | Doc_Upgrade_Error :: union #shared_nil {Doc_Upgrade_Errors, runtime.Allocator_Error} 22 | 23 | //https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md 24 | 25 | // all write in le 26 | BYTE :: u8 27 | WORD :: u16 28 | SHORT :: i16 29 | DWORD :: u32 30 | LONG :: i32 31 | FIXED :: distinct i32 // 16.16 32 | FLOAT :: f32 33 | DOUBLE :: f64 34 | QWORD :: u64 35 | LONG64 :: i64 36 | 37 | BYTE_N :: [dynamic]BYTE 38 | 39 | // https://odin-lang.org/docs/overview/#packed 40 | STRING :: struct { 41 | length: WORD, 42 | data: []u8 43 | } 44 | POINT :: struct { 45 | x: LONG, 46 | y: LONG 47 | } 48 | SIZE :: struct { 49 | w: LONG, 50 | h: LONG 51 | } 52 | RECT :: struct { 53 | origin: POINT, 54 | size: SIZE, 55 | } 56 | 57 | PIXEL_RGBA :: [4]BYTE 58 | PIXEL_GRAYSCALE :: [2]BYTE 59 | PIXEL_INDEXED :: BYTE 60 | 61 | // PIXEL :: union {PIXEL_RGBA, PIXEL_GRAYSCALE, PIXEL_INDEXED} 62 | PIXEL :: BYTE 63 | TILE :: union {BYTE, WORD, DWORD} 64 | 65 | UUID :: [16]BYTE 66 | 67 | ASE_Document :: struct { 68 | header: File_Header, 69 | frames: []Frame 70 | } 71 | 72 | Frame :: struct { 73 | header: Frame_Header, 74 | chunks: []Chunk, 75 | } 76 | 77 | Chunk :: struct { 78 | size: DWORD, 79 | type: Chunk_Types, 80 | data: Chunk_Data, 81 | } 82 | 83 | FILE_HEADER_SIZE :: 128 84 | File_Header :: struct { 85 | size: DWORD, 86 | magic: WORD, // always \xA5E0 87 | frames: WORD, 88 | width: WORD, 89 | height: WORD, 90 | color_depth: WORD, // 32=RGBA, 16=Grayscale, 8=Indexed 91 | flags: DWORD, // 1=Layer opacity has valid value 92 | speed: WORD, // Not longer in use 93 | transparent_index: BYTE, // for Indexed sprites only 94 | num_of_colors: WORD, // 0 == 256 for old sprites 95 | ratio_width: BYTE, // "pixel width/pixel height" if 0 ratio == 1:1 96 | ratio_height: BYTE, // "pixel width/pixel height" if 0 ratio == 1:1 97 | x: SHORT, 98 | y: SHORT, 99 | grid_width: WORD, // 0 if no grid 100 | grid_height: WORD, // 0 if no grid 101 | } 102 | 103 | FRAME_HEADER_SIZE :: 16 104 | Frame_Header :: struct { 105 | size: DWORD, 106 | magic: WORD, // always \xF1FA 107 | old_num_of_chunks: WORD, // if \xFFFF use new 108 | duration: WORD, // in milliseconds 109 | num_of_chunks: DWORD, // if 0 use old 110 | } 111 | 112 | Chunk_Data :: union{ 113 | Old_Palette_256_Chunk, Old_Palette_64_Chunk, Layer_Chunk, Cel_Chunk, 114 | Cel_Extra_Chunk, Color_Profile_Chunk, External_Files_Chunk, Mask_Chunk, 115 | Path_Chunk, Tags_Chunk, Palette_Chunk, User_Data_Chunk, Slice_Chunk, 116 | Tileset_Chunk, 117 | } 118 | 119 | Chunk_Types :: enum(WORD) { 120 | none, 121 | old_palette_256 = 0x0004, 122 | old_palette_64 = 0x0011, 123 | layer = 0x2004, 124 | cel = 0x2005, 125 | cel_extra = 0x2006, 126 | color_profile = 0x2007, 127 | external_files = 0x2008, 128 | mask = 0x2016, // no longer in use 129 | path = 0x2017, // not in use 130 | tags = 0x2018, 131 | palette = 0x2019, 132 | user_data = 0x2020, 133 | slice = 0x2022, 134 | tileset = 0x2023, 135 | } 136 | 137 | Old_Palette_Packet :: struct { 138 | entries_to_skip: BYTE, // start from 0 139 | num_colors: BYTE, // 0 == 256 140 | colors: [][3]BYTE 141 | } 142 | 143 | Old_Palette_256_Chunk :: struct { 144 | size: WORD, 145 | packets: []Old_Palette_Packet 146 | } 147 | 148 | Old_Palette_64_Chunk :: struct { 149 | size: WORD, 150 | packets: []Old_Palette_Packet 151 | } 152 | 153 | Layer_Chunk :: struct { 154 | flags: WORD, // to WORD -> transmute(WORD)layer_chunk.flags 155 | type: WORD, 156 | child_level: WORD, 157 | default_width: WORD, // Ignored 158 | default_height: WORD, // Ignored 159 | blend_mode: WORD, 160 | opacity: BYTE, // set when header flag is 1 161 | name: STRING, 162 | tileset_index: DWORD, // set if type == Tilemap 163 | } 164 | 165 | Raw_Cel :: struct{ 166 | width: WORD, 167 | height: WORD, 168 | pixel: []PIXEL 169 | } 170 | Linked_Cel :: distinct WORD 171 | Com_Image_Cel :: struct{ 172 | width: WORD, 173 | height: WORD, 174 | pixel: []PIXEL, 175 | did_com: bool, 176 | } // raw cel ZLIB compressed 177 | 178 | Com_Tilemap_Cel :: struct{ 179 | width: WORD, 180 | height: WORD, 181 | bits_per_tile: WORD, // always 32 182 | bitmask_id: DWORD, 183 | bitmask_x: DWORD, 184 | bitmask_y: DWORD, 185 | bitmask_diagonal: DWORD, 186 | //tiles: []TILE, // ZLIB compressed 187 | tiles: []BYTE, 188 | did_com: bool, 189 | } 190 | Cel_Type :: union{ Raw_Cel, Linked_Cel, Com_Image_Cel, Com_Tilemap_Cel } 191 | Cel_Chunk :: struct { 192 | layer_index: WORD, 193 | x: SHORT, 194 | y: SHORT, 195 | opacity_level: BYTE, 196 | type: WORD, 197 | z_index: SHORT, //0=default, pos=show n layers later, neg=back 198 | cel: Cel_Type 199 | } 200 | 201 | Cel_Extra_Chunk :: struct { 202 | flags: WORD, 203 | x: FIXED, 204 | y: FIXED, 205 | width: FIXED, 206 | height: FIXED, 207 | } 208 | 209 | Color_Profile_Chunk :: struct { 210 | type: WORD, 211 | flags: WORD, 212 | fixed_gamma: FIXED, 213 | icc: struct { // TODO: Yay more libs to make, https://www.color.org/icc1v42.pdf 214 | length: DWORD, 215 | data: []BYTE, 216 | }, 217 | } 218 | 219 | External_Files_Entry :: struct{ 220 | id: DWORD, 221 | type: BYTE, 222 | file_name_or_id: STRING, 223 | } 224 | 225 | External_Files_Chunk :: struct { 226 | length: DWORD, 227 | entries: []External_Files_Entry 228 | } 229 | 230 | Mask_Chunk :: struct { 231 | x: SHORT, 232 | y: SHORT, 233 | width: WORD, 234 | height: WORD, 235 | name: STRING, 236 | bit_map_data: []BYTE, //size = height*((width+7)/8) 237 | } 238 | 239 | Path_Chunk :: struct{} // never used 240 | 241 | Tag :: struct{ 242 | from_frame: WORD, 243 | to_frame: WORD, 244 | loop_direction: BYTE, 245 | repeat: WORD, 246 | tag_color: [3]BYTE, 247 | name: STRING, 248 | } 249 | 250 | Tags_Chunk :: struct { 251 | number: WORD, 252 | tags: []Tag, 253 | } 254 | 255 | Palette_Entry :: struct { 256 | flags: WORD, 257 | red: BYTE, 258 | green: BYTE, 259 | blue: BYTE, 260 | alpha: BYTE, 261 | name: STRING, 262 | } 263 | 264 | Palette_Chunk :: struct { 265 | size: DWORD, 266 | first_index: DWORD, 267 | last_index: DWORD, 268 | entries: []Palette_Entry, 269 | } 270 | 271 | Vec_Diff :: struct{type: WORD, data: UD_Property_Value} 272 | Vec_Type :: union {[]UD_Property_Value, []Vec_Diff} 273 | 274 | 275 | UD_Vec :: struct { 276 | num: DWORD, 277 | type: WORD, 278 | data: Vec_Type 279 | } 280 | 281 | UD_Property_Value :: union { 282 | BYTE, SHORT, WORD, LONG, DWORD, LONG64, QWORD, FIXED, FLOAT, 283 | DOUBLE, STRING, SIZE, POINT, RECT, UUID, 284 | UD_Vec, UD_Properties_Map, 285 | } 286 | 287 | UD_Property :: struct { 288 | name: STRING, 289 | type: WORD, 290 | data: UD_Property_Value, 291 | } 292 | 293 | UD_Properties_Map :: struct { 294 | key: DWORD, 295 | num: DWORD, 296 | properties: []UD_Property 297 | } 298 | 299 | UD_Bit_4 :: struct { 300 | size: DWORD, 301 | num: DWORD, 302 | properties_map: []UD_Properties_Map 303 | } 304 | 305 | UB_Bit_2 :: [4]BYTE 306 | 307 | User_Data_Chunk :: struct { 308 | flags: DWORD, 309 | text: STRING, 310 | color: UB_Bit_2, 311 | properties: UD_Bit_4, 312 | } 313 | 314 | Slice_Center :: struct{ 315 | x: LONG, 316 | y: LONG, 317 | width: DWORD, 318 | height: DWORD, 319 | } 320 | 321 | Slice_Pivot :: struct{ 322 | x: LONG, 323 | y: LONG 324 | } 325 | 326 | Slice_Key :: struct{ 327 | frame_num: DWORD, 328 | x: LONG, 329 | y: LONG, 330 | width: DWORD, 331 | height: DWORD, 332 | center: Slice_Center, 333 | pivot: Slice_Pivot, 334 | } 335 | 336 | Slice_Chunk :: struct { 337 | num_of_keys: DWORD, 338 | flags: DWORD, 339 | name: STRING, 340 | data: []Slice_Key, 341 | } 342 | 343 | Tileset_External :: struct{ 344 | file_id: DWORD, 345 | tileset_id: DWORD 346 | } 347 | 348 | Tileset_Compressed :: struct{ 349 | length: DWORD, 350 | tiles: []PIXEL, 351 | did_com: bool, 352 | } 353 | 354 | Tileset_Chunk :: struct { 355 | id: DWORD, 356 | flags: DWORD, 357 | num_of_tiles: DWORD, 358 | width: WORD, 359 | height: WORD, 360 | base_index: SHORT, 361 | name: STRING, 362 | external: Tileset_External, 363 | compressed: Tileset_Compressed, 364 | } -------------------------------------------------------------------------------- /raw/unmarshal_chunk.odin: -------------------------------------------------------------------------------- 1 | package raw_aseprite_file_handler 2 | 3 | import "core:fmt" 4 | import "core:log" 5 | import "core:bytes" 6 | import "core:slice" 7 | import "core:compress/zlib" 8 | import "core:encoding/endian" 9 | import "base:intrinsics" 10 | 11 | 12 | get_chunk_from_type :: proc($T: typeid) -> (c: Chunk_Types) 13 | where intrinsics.type_is_variant_of(Chunk_Data, T) { 14 | switch typeid_of(T) { 15 | case Old_Palette_256_Chunk: c = .old_palette_256 16 | case Old_Palette_64_Chunk: c = .old_palette_64 17 | case Layer_Chunk: c = .layer 18 | case Cel_Chunk: c = .cel 19 | case Cel_Extra_Chunk: c = .cel_extra 20 | case Color_Profile_Chunk: c = .color_profile 21 | case External_Files_Chunk: c = .external_files 22 | case Mask_Chunk: c = .mask 23 | case Path_Chunk: c = .path 24 | case Tags_Chunk: c = .tags 25 | case Palette_Chunk: c = .palette 26 | case User_Data_Chunk: c = .user_data 27 | case Slice_Chunk: c = .slice 28 | case Tileset_Chunk: c = .tileset 29 | case nil: 30 | case: 31 | unreachable() 32 | } 33 | return 34 | } 35 | 36 | 37 | unmarshal_chunks :: proc(data: []byte, buf: ^[dynamic]$T, allocator := context.allocator) -> (err: ASE_Unmarshal_Error) 38 | where intrinsics.type_is_variant_of(Chunk_Data, T) { 39 | chunk := get_chunk_from_type(T) 40 | pos := size_of(DWORD) 41 | next := size_of(DWORD) + size_of(WORD) 42 | magic, _ := endian.get_u16(data[pos:next], .Little) 43 | 44 | if magic != 0xA5E0 { 45 | return .Bad_File_Magic_Number 46 | } 47 | 48 | pos = next 49 | next += size_of(WORD) 50 | frames, _ := endian.get_u16(data[pos:next], .Little) 51 | 52 | next += size_of(WORD) + size_of(WORD) 53 | 54 | pos = next 55 | next += size_of(WORD) 56 | color_depth, _ := endian.get_u16(data[pos:next], .Little) 57 | 58 | pos = next 59 | next += size_of(DWORD) 60 | flags, _ := endian.get_u32(data[pos:next], .Little) 61 | 62 | next += 110 63 | 64 | for frame in 0.. ( 112 | frames, color_depth: u16, 113 | flags: u32, 114 | err: ASE_Unmarshal_Error 115 | ) 116 | { 117 | pos := size_of(DWORD) 118 | next := size_of(DWORD) + size_of(WORD) 119 | magic, _ := endian.get_u16(data[pos:next], .Little) 120 | 121 | if magic != 0xA5E0 { 122 | err = .Bad_File_Magic_Number 123 | return 124 | } 125 | 126 | pos = next 127 | next += size_of(WORD) 128 | frames, _ = endian.get_u16(data[pos:next], .Little) 129 | 130 | next += size_of(WORD) + size_of(WORD) 131 | 132 | pos = next 133 | next += size_of(WORD) 134 | color_depth, _ = endian.get_u16(data[pos:next], .Little) 135 | 136 | pos = next 137 | next += size_of(DWORD) 138 | flags, _ = endian.get_u32(data[pos:next], .Little) 139 | 140 | next += 110 141 | 142 | return 143 | } 144 | 145 | read_frame_header :: proc(data: []byte) -> ( 146 | frame_count: int, err: ASE_Unmarshal_Error 147 | ) { 148 | pos: int 149 | next := size_of(DWORD) + size_of(WORD) 150 | frame_magic, _ := endian.get_u16(data[pos:next], .Little) 151 | 152 | if frame_magic != 0xF1FA { 153 | err = .Bad_Frame_Magic_Number 154 | return 155 | } 156 | 157 | pos = next 158 | next += size_of(WORD) 159 | old_num, _ := endian.get_u16(data[pos:next], .Little) 160 | 161 | next += 4 162 | pos = next 163 | next += size_of(DWORD) 164 | new_num, _ := endian.get_u32(data[pos:next], .Little) 165 | 166 | if new_num == 0 { 167 | frame_count = int(old_num) 168 | } else { 169 | frame_count = int(new_num) 170 | } 171 | 172 | return 173 | } 174 | 175 | read_old_palette_256 :: proc(data: []byte, buf: ^[dynamic]Old_Palette_256_Chunk, allocator := context.allocator) -> (err: ASE_Unmarshal_Error) { 176 | pos, next: int 177 | frames, _, _ := read_file_header(data[:FILE_HEADER_SIZE]) or_return 178 | 179 | return 180 | } 181 | read_old_palette_64 :: proc(data: []byte, buf: ^[dynamic]Old_Palette_64_Chunk, allocator := context.allocator) -> (err: ASE_Unmarshal_Error) { 182 | pos, next: int 183 | frames, _, _ := read_file_header(data[:FILE_HEADER_SIZE]) or_return 184 | 185 | return 186 | } 187 | read_layer :: proc(data: []byte, buf: ^[dynamic]Layer_Chunk, allocator := context.allocator) -> (err: ASE_Unmarshal_Error) { 188 | pos, next: int 189 | frames, _, _ := read_file_header(data[:FILE_HEADER_SIZE]) or_return 190 | 191 | return 192 | } 193 | read_cel :: proc(data: []byte, buf: ^[dynamic]Cel_Chunk, allocator := context.allocator) -> (err: ASE_Unmarshal_Error) { 194 | pos, next: int 195 | frames, _, _ := read_file_header(data[:FILE_HEADER_SIZE]) or_return 196 | 197 | return 198 | } 199 | read_cel_extra :: proc(data: []byte, buf: ^[dynamic]Cel_Extra_Chunk, allocator := context.allocator) -> (err: ASE_Unmarshal_Error) { 200 | pos, next: int 201 | frames, _, _ := read_file_header(data[:FILE_HEADER_SIZE]) or_return 202 | 203 | return 204 | } 205 | read_color_profile :: proc(data: []byte, buf: ^[dynamic]Color_Profile_Chunk, allocator := context.allocator) -> (err: ASE_Unmarshal_Error) { 206 | pos, next: int 207 | frames, _, _ := read_file_header(data[:FILE_HEADER_SIZE]) or_return 208 | 209 | return 210 | } 211 | read_external_files :: proc(data: []byte, buf: ^[dynamic]External_Files_Chunk, allocator := context.allocator) -> (err: ASE_Unmarshal_Error) { 212 | pos, next: int 213 | frames, _, _ := read_file_header(data[:FILE_HEADER_SIZE]) or_return 214 | 215 | return 216 | } 217 | read_mask :: proc(data: []byte, buf: ^[dynamic]Mask_Chunk, allocator := context.allocator) -> (err: ASE_Unmarshal_Error) { 218 | pos, next: int 219 | frames, _, _ := read_file_header(data[:FILE_HEADER_SIZE]) or_return 220 | 221 | return 222 | } 223 | read_path :: proc(data: []byte, buf: ^[dynamic]Path_Chunk, allocator := context.allocator) -> (err: ASE_Unmarshal_Error) { 224 | pos, next: int 225 | frames, _, _ := read_file_header(data[:FILE_HEADER_SIZE]) or_return 226 | 227 | return 228 | } 229 | read_tags :: proc(data: []byte, buf: ^[dynamic]Tags_Chunk, allocator := context.allocator) -> (err: ASE_Unmarshal_Error) { 230 | pos, next: int 231 | frames, _, _ := read_file_header(data[:FILE_HEADER_SIZE]) or_return 232 | 233 | return 234 | } 235 | read_palette :: proc(data: []byte, buf: ^[dynamic]Palette_Chunk, allocator := context.allocator) -> (err: ASE_Unmarshal_Error) { 236 | pos, next: int 237 | frames, _, _ := read_file_header(data[:FILE_HEADER_SIZE]) or_return 238 | 239 | return 240 | } 241 | read_user_data :: proc(data: []byte, buf: ^[dynamic]User_Data_Chunk, allocator := context.allocator) -> (err: ASE_Unmarshal_Error) { 242 | pos, next: int 243 | frames, _, _ := read_file_header(data[:FILE_HEADER_SIZE]) or_return 244 | 245 | return 246 | } 247 | read_slice :: proc(data: []byte, buf: ^[dynamic]Slice_Chunk, allocator := context.allocator) -> (err: ASE_Unmarshal_Error) { 248 | pos, next: int 249 | frames, _, _ := read_file_header(data[:FILE_HEADER_SIZE]) or_return 250 | 251 | return 252 | } 253 | read_tileset :: proc(data: []byte, buf: ^[dynamic]Tileset_Chunk, allocator := context.allocator) -> (err: ASE_Unmarshal_Error) { 254 | pos, next: int 255 | frames, _, _ := read_file_header(data[:FILE_HEADER_SIZE]) or_return 256 | 257 | return 258 | } 259 | 260 | read_chunks :: proc{read_old_palette_256, read_old_palette_64, read_layer, read_cel, read_cel_extra, read_color_profile, read_external_files} -------------------------------------------------------------------------------- /raw/updating.odin: -------------------------------------------------------------------------------- 1 | package raw_aseprite_file_handler 2 | 3 | // FIXME: Needs to be finished before use. 4 | update_doc :: proc(doc: ^ASE_Document, assume_flags := false) { 5 | if assume_flags { update_flags(doc) } 6 | update_types(doc) 7 | //update_sizes(doc) 8 | // Compiler Assertion Failure on WIN11. Can't use. 9 | } 10 | 11 | update_flags :: proc(doc: ^ASE_Document) { 12 | for &frame in doc.frames { 13 | for &chunk in frame.chunks { 14 | #partial switch &v in chunk.data { 15 | case Palette_Chunk: 16 | for &entry in v.entries { 17 | if len(entry.name.data) != 0 { 18 | entry.flags = 1 19 | } else { 20 | entry.flags = 0 21 | } 22 | } 23 | 24 | case User_Data_Chunk: 25 | if len(v.text.data) != 0 { 26 | v.flags |= 1 27 | } else { 28 | v.flags &~= 1 29 | } 30 | 31 | if len(v.properties.properties_map) != 0 { 32 | v.flags |= 4 33 | } else { 34 | v.flags &~= 4 35 | } 36 | 37 | case Tileset_Chunk: 38 | if len(v.compressed.tiles) != 0 { 39 | v.flags |= 2 40 | } else { 41 | v.flags &~= 2 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | update_types :: proc(doc: ^ASE_Document) { 49 | update_value :: proc(value: ^UD_Property_Value) -> (prop_type: WORD) { 50 | switch &pt in value { 51 | case BYTE: prop_type = 0x0003 52 | case SHORT: prop_type = 0x0004 53 | case WORD: prop_type = 0x0005 54 | case LONG: prop_type = 0x0006 55 | case DWORD: prop_type = 0x0007 56 | case LONG64: prop_type = 0x0008 57 | case QWORD: prop_type = 0x0009 58 | case FIXED: prop_type = 0x000A 59 | case FLOAT: prop_type = 0x000B 60 | case DOUBLE: prop_type = 0x000C 61 | case STRING: prop_type = 0x000D 62 | case POINT: prop_type = 0x000E 63 | case SIZE: prop_type = 0x000F 64 | case RECT: prop_type = 0x0010 65 | case UUID: prop_type = 0x0013 66 | 67 | case UD_Vec: 68 | prop_type = 0x0011 69 | switch &vt in pt.data { 70 | case []Vec_Diff: 71 | for &dt in vt { 72 | dt.type = update_value(&dt.data) 73 | } 74 | case []UD_Property_Value: 75 | if len(vt) != 0 { 76 | pt.type = update_value(&vt[0]) 77 | } 78 | } 79 | 80 | case UD_Properties_Map: 81 | prop_type = 0x0012 82 | for &prop in pt.properties { 83 | prop.type = update_value(&prop.data) 84 | } 85 | } 86 | return 87 | } 88 | 89 | for &frame in doc.frames { 90 | for &chunk in frame.chunks { 91 | switch &v in chunk.data { 92 | case Old_Palette_256_Chunk: chunk.type = .old_palette_256 93 | case Old_Palette_64_Chunk: chunk.type = .old_palette_64 94 | case Layer_Chunk: chunk.type = .layer 95 | case Cel_Extra_Chunk: chunk.type = .cel_extra 96 | case External_Files_Chunk: chunk.type = .external_files 97 | case Mask_Chunk: chunk.type = .mask 98 | case Path_Chunk: chunk.type = .path 99 | case Tags_Chunk: chunk.type = .tags 100 | case Palette_Chunk: chunk.type = .palette 101 | case Slice_Chunk: chunk.type = .slice 102 | case Tileset_Chunk: chunk.type = .tileset 103 | 104 | case Cel_Chunk: 105 | chunk.type = .cel 106 | switch cel in v.cel { 107 | case Raw_Cel: v.type = 0 108 | case Linked_Cel: v.type = 1 109 | case Com_Image_Cel: v.type = 2 110 | case Com_Tilemap_Cel: v.type = 3 111 | } 112 | 113 | case Color_Profile_Chunk: 114 | chunk.type = .color_profile 115 | if len(v.icc.data) != 0 { 116 | v.type = 2 117 | } else { 118 | v.type = 0 119 | } 120 | 121 | case User_Data_Chunk: 122 | chunk.type = .user_data 123 | 124 | if (v.flags & 4) == 4 { 125 | for &pmap in v.properties.properties_map { 126 | for &prop in pmap.properties { 127 | prop.type = update_value(&prop.data) 128 | } 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | 137 | // doc: raw.ASE_Document to update 138 | // size: New total size in bytes 139 | update_sizes :: proc(doc: ^ASE_Document) -> (size: int) { 140 | // Compiler Assertion Failure. Can't use. 141 | update_value :: proc(value: ^UD_Property_Value) -> (size: DWORD) { 142 | switch &pt in value { 143 | case BYTE: size = 1 144 | case SHORT, WORD: size = 2 145 | case LONG, DWORD, FIXED, FLOAT: size = 4 146 | case LONG64, QWORD, DOUBLE: size = 8 147 | case STRING: size = 2 + DWORD(len(pt.data)) 148 | case POINT, SIZE: size = 4+4 149 | case RECT: size = 4*4 150 | case UUID: size = 16 151 | 152 | case UD_Vec: 153 | size += 4 + 2 154 | switch &vt in pt.data { 155 | case []Vec_Diff: 156 | pt.num = DWORD(len(vt)) 157 | for &dt in vt { 158 | size += update_value(&dt.data) 159 | } 160 | case []UD_Property_Value: 161 | pt.num = DWORD(len(vt)) 162 | if len(vt) != 0 { 163 | size += update_value(&vt[0]) * pt.num 164 | } 165 | } 166 | 167 | case UD_Properties_Map: 168 | size = 4 169 | pt.num = DWORD(len(pt.properties)) 170 | for &prop in pt.properties { 171 | size += update_value(&prop.data) 172 | } 173 | } 174 | return 175 | } 176 | 177 | size += FILE_HEADER_SIZE 178 | doc.header.frames = WORD(len(doc.frames)) 179 | 180 | for &frame in doc.frames { 181 | if frame.header.num_of_chunks == 0 && len(frame.chunks) < 0xFFFF { 182 | frame.header.old_num_of_chunks = WORD(len(frame.chunks)) 183 | frame.header.num_of_chunks = 0 184 | } else { 185 | frame.header.old_num_of_chunks = 0xFFFF 186 | frame.header.num_of_chunks = DWORD(len(frame.chunks)) 187 | } 188 | frame.header.size = DWORD(FRAME_HEADER_SIZE) 189 | 190 | for &chunk in frame.chunks { 191 | chunk.size = DWORD(size_of(DWORD)) 192 | 193 | switch &v in chunk.data { 194 | case Old_Palette_256_Chunk: 195 | chunk.size += DWORD(size_of(WORD)) 196 | v.size = WORD(len(v.packets)) 197 | for &pal in v.packets{ 198 | if len(pal.colors) == 256 { 199 | pal.num_colors = 0 200 | chunk.size += 256 * 3 + 2 201 | } else { 202 | pal.num_colors = BYTE(len(pal.colors)) 203 | chunk.size += DWORD(pal.num_colors) * 3 + 2 204 | } 205 | } 206 | 207 | case Old_Palette_64_Chunk: 208 | chunk.size += DWORD(size_of(WORD)) 209 | v.size = WORD(len(v.packets)) 210 | for &pal in v.packets{ 211 | if len(pal.colors) == 256 { 212 | pal.num_colors = 0 213 | chunk.size += 256 * 3 + 2 214 | } else { 215 | pal.num_colors = BYTE(len(pal.colors)) 216 | chunk.size += DWORD(pal.num_colors) * 3 + 2 217 | } 218 | } 219 | 220 | case Layer_Chunk: 221 | v.name.length = WORD(len(v.name.data)) 222 | chunk.size += 2*6 + 1*4 + 2 + DWORD(len(v.name.data)) + 4 223 | 224 | case Cel_Chunk: 225 | chunk.size += 2*5 + 1 + 5 226 | switch cel in v.cel { 227 | case Raw_Cel: 228 | chunk.size += 2*2 + DWORD(len(cel.pixel)) 229 | case Linked_Cel: 230 | chunk.size += 2 231 | case Com_Image_Cel: 232 | chunk.size += 2*2 + DWORD(len(cel.pixel)) 233 | case Com_Tilemap_Cel: 234 | chunk.size += 2*3 + 4*4 + 10 + DWORD(len(cel.tiles)) 235 | } 236 | 237 | case Cel_Extra_Chunk: 238 | chunk.size += 2 + 4*4 + 16 239 | 240 | case Color_Profile_Chunk: 241 | v.icc.length = DWORD(len(v.icc.data)) 242 | chunk.size += 2*2 + 4 + 8 + 4 + v.icc.length 243 | 244 | case External_Files_Chunk: 245 | v.length = DWORD(len(v.entries)) 246 | chunk.size += 4 + 8 247 | for &e in v.entries { 248 | e.file_name_or_id.length = WORD(len(e.file_name_or_id.data)) 249 | chunk.size += 4 + 1 + 7 + 2 + DWORD(e.file_name_or_id.length) 250 | } 251 | 252 | case Mask_Chunk: 253 | v.name.length = WORD(len(v.name.data)) 254 | chunk.size += 4*4 + 8 + 2 + + DWORD(v.name.length) 255 | 256 | case Path_Chunk: 257 | 258 | case Tags_Chunk: 259 | v.number = WORD(len(v.tags)) 260 | chunk.size += 2 + 8 261 | for &tag in v.tags { 262 | tag.name.length = WORD(len(tag.name.data)) 263 | chunk.size += 2*2 + 1 + 2 + 6 + 3 + 1 + 2 + DWORD(tag.name.length) 264 | } 265 | 266 | case Palette_Chunk: 267 | v.size = DWORD(len(v.entries)) 268 | chunk.size += 4*3 + 8 269 | for &e in v.entries { 270 | e.name.length = WORD(len(e.name.data)) 271 | chunk.size += 2 + 1*4 272 | if (e.flags * 1) == 1 { 273 | chunk.size += 2 + DWORD(len(e.name.data)) 274 | } 275 | } 276 | 277 | case User_Data_Chunk: 278 | if (v.flags & 1) == 1 { 279 | v.text.length = WORD(len(v.text.data)) 280 | chunk.size += 2 + DWORD(len(v.text.data)) 281 | } 282 | if (v.flags & 2) == 2 { 283 | chunk.size += 1*4 284 | } 285 | if (v.flags & 4) == 4 { 286 | v.properties.num = DWORD(len(v.properties.properties_map)) 287 | v.properties.size += 4*2 288 | 289 | for &pmap in v.properties.properties_map { 290 | v.properties.size += 4*2 291 | pmap.num += DWORD(len(pmap.properties)) 292 | for &prop in pmap.properties { 293 | prop.name.length = WORD(len(prop.name.data)) 294 | v.properties.size += 2 + DWORD(len(prop.name.data)) + 2 295 | v.properties.size += update_value(&prop.data) 296 | } 297 | } 298 | chunk.size += v.properties.size 299 | 300 | } 301 | 302 | case Slice_Chunk: 303 | v.num_of_keys = DWORD(len(v.data)) 304 | v.name.length = WORD(len(v.name.data)) 305 | chunk.size += 4*3 + 2 + DWORD(len(v.name.data)) 306 | 307 | for key in v.data { 308 | chunk.size += 4*5 309 | if (v.flags & 1) == 1 { 310 | chunk.size += 4*4 311 | } 312 | if (v.flags & 2) == 2 { 313 | chunk.size += 4+4 314 | } 315 | } 316 | 317 | case Tileset_Chunk: 318 | v.name.length = WORD(len(v.name.data)) 319 | chunk.size += 4*3 + 2*3 + 14 + 2 + DWORD(len(v.name.data)) 320 | if (v.flags & 1) == 1 { 321 | chunk.size += 4+4 322 | } 323 | // Can only update compressed.length when failed to uncompress 324 | if (v.flags & 2) == 2 && !v.compressed.did_com { 325 | v.compressed.length = DWORD(len(v.compressed.tiles)) 326 | chunk.size += 4 + v.compressed.length 327 | } 328 | } 329 | frame.header.size += chunk.size 330 | } 331 | size += int(frame.header.size) 332 | } 333 | doc.header.size = DWORD(size) 334 | return 335 | } -------------------------------------------------------------------------------- /raw/util.odin: -------------------------------------------------------------------------------- 1 | package raw_aseprite_file_handler 2 | 3 | import "core:reflect" 4 | 5 | chunk_equal :: proc(x, y: Chunk) -> (a: any, b: any, c: typeid, eq: bool) { 6 | if x.type != y.type { 7 | return x.type, y.type, typeid_of(Chunk), false 8 | } 9 | if x.size != y.size { 10 | return x.size, y.size, typeid_of(Chunk), false 11 | } 12 | 13 | switch xv in x.data { 14 | case Old_Palette_256_Chunk: 15 | yv, ok := y.data.(Old_Palette_256_Chunk) 16 | if !ok { 17 | return typeid_of(type_of(xv)), reflect.union_variant_typeid(y), typeid_of(Chunk_Data), false 18 | } 19 | return _chunk_equal(xv, yv) 20 | 21 | case Old_Palette_64_Chunk: 22 | yv, ok := y.data.(Old_Palette_64_Chunk) 23 | if !ok { 24 | return typeid_of(type_of(xv)), reflect.union_variant_typeid(y), typeid_of(Chunk_Data), false 25 | } 26 | return _chunk_equal(xv, yv) 27 | 28 | case Layer_Chunk: 29 | yv, ok := y.data.(Layer_Chunk) 30 | if !ok { 31 | return typeid_of(type_of(xv)), reflect.union_variant_typeid(y), typeid_of(Chunk_Data), false 32 | } 33 | return _chunk_equal(xv, yv) 34 | 35 | case Cel_Chunk: 36 | yv, ok := y.data.(Cel_Chunk) 37 | if !ok { 38 | return typeid_of(type_of(xv)), reflect.union_variant_typeid(y), typeid_of(Chunk_Data), false 39 | } 40 | return _chunk_equal(xv, yv) 41 | 42 | case Cel_Extra_Chunk: 43 | yv, ok := y.data.(Cel_Extra_Chunk) 44 | if !ok { 45 | return typeid_of(type_of(xv)), reflect.union_variant_typeid(y), typeid_of(Chunk_Data), false 46 | } 47 | return _chunk_equal(xv, yv) 48 | 49 | case External_Files_Chunk: 50 | yv, ok := y.data.(External_Files_Chunk) 51 | if !ok { 52 | return typeid_of(type_of(xv)), reflect.union_variant_typeid(y), typeid_of(Chunk_Data), false 53 | } 54 | return _chunk_equal(xv, yv) 55 | 56 | case Mask_Chunk: 57 | yv, ok := y.data.(Mask_Chunk) 58 | if !ok { 59 | return typeid_of(type_of(xv)), reflect.union_variant_typeid(y), typeid_of(Chunk_Data), false 60 | } 61 | return _chunk_equal(xv, yv) 62 | 63 | case Path_Chunk: 64 | yv, ok := y.data.(Path_Chunk) 65 | if !ok { 66 | return typeid_of(type_of(xv)), reflect.union_variant_typeid(y), typeid_of(Chunk_Data), false 67 | } 68 | return _chunk_equal(xv, yv) 69 | 70 | case Tags_Chunk: 71 | yv, ok := y.data.(Tags_Chunk) 72 | if !ok { 73 | return typeid_of(type_of(xv)), reflect.union_variant_typeid(y), typeid_of(Chunk_Data), false 74 | } 75 | return _chunk_equal(xv, yv) 76 | 77 | case Palette_Chunk: 78 | yv, ok := y.data.(Palette_Chunk) 79 | if !ok { 80 | return typeid_of(type_of(xv)), reflect.union_variant_typeid(y), typeid_of(Chunk_Data), false 81 | } 82 | return _chunk_equal(xv, yv) 83 | 84 | case Color_Profile_Chunk: 85 | yv, ok := y.data.(Color_Profile_Chunk) 86 | if !ok { 87 | return typeid_of(type_of(xv)), reflect.union_variant_typeid(y), typeid_of(Chunk_Data), false 88 | } 89 | return _chunk_equal(xv, yv) 90 | 91 | case User_Data_Chunk: 92 | yv, ok := y.data.(User_Data_Chunk) 93 | if !ok { 94 | return typeid_of(type_of(xv)), reflect.union_variant_typeid(y), typeid_of(Chunk_Data), false 95 | } 96 | return _chunk_equal(xv, yv) 97 | 98 | case Slice_Chunk: 99 | yv, ok := y.data.(Slice_Chunk) 100 | if !ok { 101 | return typeid_of(type_of(xv)), reflect.union_variant_typeid(y), typeid_of(Chunk_Data), false 102 | } 103 | return _chunk_equal(xv, yv) 104 | 105 | case Tileset_Chunk: 106 | yv, ok := y.data.(Tileset_Chunk) 107 | if !ok { 108 | return typeid_of(type_of(xv)), reflect.union_variant_typeid(y), typeid_of(Chunk_Data), false 109 | } 110 | return _chunk_equal(xv, yv) 111 | 112 | case nil: 113 | if y.data != nil { 114 | return x.data, y.data, typeid_of(Chunk_Data), false 115 | } 116 | case: 117 | return 118 | } 119 | 120 | eq = true 121 | return 122 | } 123 | 124 | frame_equal :: proc(x, y: Frame) -> (a: any, b: any, c: typeid, eq: bool) { 125 | if x.header != y.header { 126 | return x.header, y.header, typeid_of(ASE_Document), false 127 | } 128 | if len(x.chunks) != len(y.chunks) { 129 | return len(x.chunks), len(y.chunks), typeid_of(ASE_Document), false 130 | } 131 | for i in 0.. (a: any, b: any, c: typeid, eq: bool) { 141 | if x.header != y.header { 142 | return x.header, y.header, typeid_of(ASE_Document), false 143 | } 144 | if len(x.frames) != len(y.frames) { 145 | return len(x.frames), len(y.frames), typeid_of(ASE_Document), false 146 | } 147 | for i in 0.. (wrong: []Wrong, err: runtime.Allocator_Error) { 13 | buf := make([dynamic]Wrong, allocator) or_return 14 | defer delete(buf) 15 | 16 | 17 | 18 | if len(buf) > 0 { 19 | wrong = make([]Wrong, len(buf), allocator) or_return 20 | copy(wrong[:], buf[:]) 21 | } 22 | return 23 | } -------------------------------------------------------------------------------- /read.odin: -------------------------------------------------------------------------------- 1 | package aseprite_file_handler 2 | 3 | import "core:io" 4 | import "core:log" 5 | import "core:encoding/endian" 6 | 7 | read_bool :: proc(r: io.Reader, n: ^int) -> (data: bool, err: Read_Error) { 8 | return bool(read_byte(r, n) or_return), nil 9 | } 10 | 11 | read_i8 :: proc(r: io.Reader, n: ^int) -> (data: i8, err: Read_Error) { 12 | return i8(read_byte(r, n) or_return), nil 13 | } 14 | 15 | read_byte :: proc(r: io.Reader, n: ^int) -> (data: BYTE, err: Read_Error) { 16 | data, err = io.read_byte(r, n) 17 | if err != nil { 18 | log.error("Failed to read byte/i8/bool", n^) 19 | } 20 | return 21 | } 22 | 23 | read_word :: proc(r: io.Reader, n: ^int) -> (data: WORD, err: Read_Error) { 24 | buf: [2]byte 25 | s := io.read(r, buf[:], n) or_return 26 | if s != 2 { 27 | log.error("Failed to read word", s, n^) 28 | return 0, .Wrong_Read_Size 29 | } 30 | 31 | v, ok := endian.get_u16(buf[:], .Little) 32 | if !ok { 33 | err = .Unable_To_Decode_Data 34 | } 35 | return v, err 36 | } 37 | 38 | read_short :: proc(r: io.Reader, n: ^int) -> (data: SHORT, err: Read_Error) { 39 | buf: [2]byte 40 | s := io.read(r, buf[:], n) or_return 41 | if s != 2 { 42 | log.error("Failed to read short", s, n^) 43 | return 0, .Wrong_Read_Size 44 | } 45 | 46 | v, ok := endian.get_i16(buf[:], .Little) 47 | if !ok { 48 | err = .Unable_To_Decode_Data 49 | } 50 | return v, err 51 | } 52 | 53 | read_dword :: proc(r: io.Reader, n: ^int) -> (data: DWORD, err: Read_Error) { 54 | buf: [4]byte 55 | s := io.read(r, buf[:], n) or_return 56 | if s != 4 { 57 | log.error("Failed to read dword", s, n^) 58 | return 0, .Wrong_Read_Size 59 | } 60 | 61 | v, ok := endian.get_u32(buf[:], .Little) 62 | if !ok { 63 | err = .Unable_To_Decode_Data 64 | } return v, err 65 | } 66 | 67 | read_long :: proc(r: io.Reader, n: ^int) -> (data: LONG, err: Read_Error) { 68 | buf: [4]byte 69 | s := io.read(r, buf[:], n) or_return 70 | if s != 4 { 71 | log.error("Failed to read long", s, n^) 72 | return 0, .Wrong_Read_Size 73 | } 74 | 75 | v, ok := endian.get_i32(buf[:], .Little) 76 | if !ok { 77 | err = .Unable_To_Decode_Data 78 | } 79 | return v, err 80 | } 81 | 82 | read_fixed :: proc(r: io.Reader, n: ^int) -> (data: FIXED, err: Read_Error) { 83 | buf: [4]byte 84 | s := io.read(r, buf[:], n) or_return 85 | if s != 4 { 86 | log.error("Failed to read fixed", s, n^) 87 | return data, .Wrong_Read_Size 88 | } 89 | 90 | /*vi, ok_i := endian.get_i16(buf[:2], .Little) 91 | if !ok_i { 92 | err = .Unable_To_Decode_Data 93 | return 94 | } 95 | 96 | vf, ok_f := endian.get_i16(buf[2:], .Little) 97 | if !ok_f { 98 | err = .Unable_To_Decode_Data 99 | return 100 | } 101 | fixed.init_from_parts(&data, i32(vi), i32(vf))*/ 102 | 103 | v, ok := endian.get_i32(buf[:], .Little) 104 | if !ok { 105 | err = .Unable_To_Decode_Data 106 | return 107 | } 108 | data.i = v 109 | return 110 | } 111 | 112 | read_float :: proc(r: io.Reader, n: ^int) -> (data: FLOAT, err: Read_Error) { 113 | buf: [4]byte 114 | s := io.read(r, buf[:], n) or_return 115 | if s != 42 { 116 | log.error("Failed to read float", s, n^) 117 | return 0, .Wrong_Read_Size 118 | } 119 | 120 | v, ok := endian.get_f32(buf[:], .Little) 121 | if !ok { 122 | err = .Unable_To_Decode_Data 123 | } 124 | return v, err 125 | } 126 | 127 | read_double :: proc(r: io.Reader, n: ^int) -> (data: DOUBLE, err: Read_Error) { 128 | buf: [8]byte 129 | s := io.read(r, buf[:], n) or_return 130 | if s != 8 { 131 | log.error("Failed to read double", s, n^) 132 | return 0, .Wrong_Read_Size 133 | } 134 | 135 | v, ok := endian.get_f64(buf[:], .Little) 136 | if !ok { 137 | err = .Unable_To_Decode_Data 138 | } 139 | return v, err 140 | } 141 | 142 | read_qword :: proc(r: io.Reader, n: ^int) -> (data: QWORD, err: Read_Error) { 143 | buf: [8]byte 144 | s := io.read(r, buf[:], n) or_return 145 | if s != 8 { 146 | log.error("Failed to read qword", s, n^) 147 | return 0, .Wrong_Read_Size 148 | } 149 | 150 | v, ok := endian.get_u64(buf[:], .Little) 151 | if !ok { 152 | err = .Unable_To_Decode_Data 153 | } 154 | return v, err 155 | } 156 | 157 | read_long64 :: proc(r: io.Reader, n: ^int) -> (data: LONG64, err: Read_Error) { 158 | buf: [8]byte 159 | s := io.read(r, buf[:], n) or_return 160 | if s != 8 { 161 | log.error("Failed to read long64", s, n^) 162 | return 0, .Wrong_Read_Size 163 | } 164 | 165 | v, ok := endian.get_i64(buf[:], .Little) 166 | if !ok { 167 | err = .Unable_To_Decode_Data 168 | } 169 | return v, err 170 | } 171 | 172 | read_string :: proc(r: io.Reader, n: ^int, alloc := context.allocator, loc := #caller_location) -> (data: STRING, err: Read_Error) { 173 | size := int(read_word(r, n) or_return) 174 | if size == 0 { 175 | return 176 | } 177 | 178 | buf := make([]byte, size, alloc) or_return 179 | s: int 180 | s, err = io.read(r, buf, n) 181 | if err != nil { 182 | log.error("Failed to read string", size, err, n^, loc) 183 | return 184 | } 185 | if s != size { 186 | log.error("Unexpected string size", size, s, n^, loc) 187 | err = .Wrong_Read_Size 188 | return 189 | } 190 | 191 | data = string(buf) 192 | return 193 | } 194 | 195 | read_point :: proc(r: io.Reader, n: ^int) -> (data: POINT, err: Read_Error) { 196 | data.x = read_long(r, n) or_return 197 | data.y = read_long(r, n) or_return 198 | return 199 | } 200 | 201 | read_size :: proc(r: io.Reader, n: ^int) -> (data: SIZE, err: Read_Error) { 202 | data.w = read_long(r, n) or_return 203 | data.h = read_long(r, n) or_return 204 | return 205 | } 206 | 207 | read_rect :: proc(r: io.Reader, n: ^int) -> (data: RECT, err: Read_Error) { 208 | data.origin = read_point(r, n) or_return 209 | data.size = read_size(r, n) or_return 210 | return 211 | } 212 | 213 | read_uuid:: proc(r: io.Reader, n: ^int) -> (data: UUID, err: Read_Error) { 214 | s := io.read(r, data[:], n) or_return 215 | if s != 16 { 216 | log.error("Failed to read UUID", s, data, n^) 217 | err = .Wrong_Read_Size 218 | } 219 | return 220 | } 221 | 222 | read_pixel :: proc(r: io.Reader, n: ^int) -> (data: PIXEL, err: Read_Error) { 223 | return read_byte(r, n) 224 | } 225 | 226 | read_pixels :: proc(r: io.Reader, data: []PIXEL, n: ^int) -> (err: Read_Error) { 227 | return read_bytes(r, data[:], n) 228 | } 229 | 230 | read_tile :: proc(r: io.Reader, type: Tile_ID, n: ^int) -> (data: TILE, err: Read_Error) { 231 | switch type { 232 | case .byte: 233 | data = read_byte(r, n) or_return 234 | case .word: 235 | data = read_word(r, n) or_return 236 | case .dword: 237 | data = read_dword(r, n) or_return 238 | } 239 | return 240 | } 241 | 242 | read_tiles :: proc(r: io.Reader, data: []TILE, type: Tile_ID, n: ^int) -> (err: Read_Error) { 243 | size := len(data) 244 | if len(data) == 0 { 245 | return 246 | } 247 | for i in 0.. (err: Read_Error) { 254 | s := io.read(r, data, n) or_return 255 | if s != len(data) { 256 | log.error("Could read all the bytes asked.", s, len(data)) 257 | err = .Wrong_Read_Size 258 | } 259 | return 260 | } 261 | 262 | read_skip :: proc(r: io.Reader, to_skip: int, n: ^int) -> (err: Read_Error) { 263 | // i := io.seek(r, i64(n^+to_skip), .Current) or_return 264 | // n^ += int(i) 265 | 266 | for _ in 0.. (val: Property_Value, err: Unmarshal_Error) { 273 | context.allocator = alloc 274 | 275 | switch type { 276 | case .Null: return nil, nil 277 | case .Bool: return read_bool(r, n) 278 | case .I8: return read_i8(r, n) 279 | case .U8: return read_byte(r, n) 280 | case .I16: return read_short(r, n) 281 | case .U16: return read_word(r, n) 282 | case .I32: return read_long(r, n) 283 | case .U32: return read_dword(r, n) 284 | case .I64: return read_long64(r, n) 285 | case .U64: return read_qword(r, n) 286 | case .Fixed: return read_fixed(r, n) 287 | case .F32: return read_float(r, n) 288 | case .F64: return read_double(r, n) 289 | case .String: return read_string(r, n) 290 | case .Point: return read_point(r, n) 291 | case .Size: return read_size(r, n) 292 | case .Rect: return read_rect(r, n) 293 | case .UUID: return read_uuid(r, n) 294 | 295 | case .Vector: 296 | num := int(read_dword(r, n) or_return) 297 | val = make(UD_Vec, num) or_return 298 | vec_type := Property_Type(read_word(r, n) or_return) 299 | 300 | if vec_type == .Null { 301 | for i in 0..