├── .gitignore ├── LICENSE ├── README.md ├── assets ├── demo.png └── hero.png ├── examples ├── 6_by_9.png └── example.json ├── extension ├── font-custom.lua ├── json.lua ├── package.json └── plugin.lua └── font-custom.aseprite-extension /.gitignore: -------------------------------------------------------------------------------- 1 | # Other ignorables 2 | **/test_fonts/ 3 | **/.DS_STORE 4 | demo.xcf 5 | 6 | # Compiled Lua sources 7 | luac.out 8 | 9 | # luarocks build files 10 | *.src.rock 11 | *.zip 12 | *.tar.gz 13 | 14 | # Object files 15 | *.o 16 | *.os 17 | *.ko 18 | *.obj 19 | *.elf 20 | 21 | # Precompiled Headers 22 | *.gch 23 | *.pch 24 | 25 | # Libraries 26 | *.lib 27 | *.a 28 | *.la 29 | *.lo 30 | *.def 31 | *.exp 32 | 33 | # Shared objects (inc. Windows DLLs) 34 | *.dll 35 | *.so 36 | *.so.* 37 | *.dylib 38 | 39 | # Executables 40 | *.exe 41 | *.out 42 | *.app 43 | *.i*86 44 | *.x86_64 45 | *.hex 46 | 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 David Fletcher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Font Custom 2 | ![hero image](./assets/hero.png) 3 | 4 | Create new fonts with Aseprite and load them in to be used in your projects! 5 | 6 | All you'll need is a `font atlas` (i.e. a grid-based arrangement of the characters you'd like to use) and a simple `.json` file that describe the properties of the font. 7 | 8 | ![a demonstration](./assets/demo.png) 9 | 10 | ## FAQ 11 | 12 | * Does this extension create fonts from my pixel art? (i.e., export to .ttf, .otf, etc) 13 | * No, unfortunately it does not create font files. It does, however, give you the option to use your custom font within Aseprite as if you were using the `Edit > Insert Text` menu option. If you'd like to create font files based on a pixel map, check out [this itch.io project](https://yellowafterlife.itch.io/pixelfont)! 14 | * Why aren't special characters like `Ð`, `Þ`, `Æ`, or others working? 15 | * Unfortunately, for now, this extension can only handle the ASCII characters at the moment. I am investigating how we can modify the extension in the future to allow for (full) UTF-8, UTF-16 and UTF-32 compliant characters! 16 | * Are there other Aseprite extensions / scripts related to this? 17 | * Of course! Here's a list of projects I know about thus far: 18 | * [PixelFont](https://yellowafterlife.itch.io/pixelfont)! 19 | * [Write Tool For Aseprite](https://bowgrape.itch.io/write-tool) 20 | 21 | ## How to Use Font Custom 22 | 23 | 1. Download this extension by visiting the releases page! 24 | 2. Create a pixel art font based on a grid with standard height and width. Don't worry about kerning, character widths, and etc; the next step will handle that. **Save this file as `.png`, `.ase`, or `.aseprite`!** 25 | 26 | ![gif for font creation](https://media.giphy.com/media/rPHEAnZYVDzcJ5ibR1/giphy-downsized.gif?cid=790b76117b2dc73e0ecf46e24ddc77c3a619f338dc40a823&rid=giphy-downsized.gif&ct=g) 27 | 28 | 3. Create a properties file with the extension `.json`. The data has to be formatted in a special way, so I recommend using the [example.json](./examples/example.json) file you can find here as a stepping stool. 29 | 30 | ![gif for property creation](https://media.giphy.com/media/YZRIUu7uKkSc0U7uyo/giphy-downsized.gif?cid=790b76113bc80f8bd1362c832c3086f14db0b405536b2262&rid=giphy-downsized.gif&ct=g) 31 | 32 | 4. In Aseprite, simply install this extension (only have to do this once) and navigate to `Edit > FX > Use Custom Font` and follow the dialog prompts to include text with your custom font in your Aseprite project! 33 | 34 | ![gif for using extension](https://media.giphy.com/media/BKyfLNEwpxNiiGD5O9/giphy-downsized.gif?cid=790b76118fd0d366939eb128c3f721f794a3b8445b297d8f&rid=giphy-downsized.gif&ct=g) 35 | 36 | ## Things to Know 37 | 1. In your properties file, not every "object" is mandatory. All possible objects are listed below; mandatory objects have a `*` next to them. DO NOT include the asterix in the key-names in your properties file (use [example.json](./examples/example.json) as a template). 38 | * `*alphabet` - a list of all of the characters found in the font file. Due to technical reasons, the list of characters must _match exactly_ the ordering found in the font file (left to right, top to bottom). 39 | * `*sprite_path` - a pointer to the sprite file that contains the font. Valid file extensions are `.png`, `.ase`, or `.aseprite`. If a _relative_ file name is given, the extension will look for a font file in the same directory as the property file (recommended). However, you can also give it an _absolute_ file name anywhere on your computer to locate the font file, if desired. 40 | * `*atlas` - an object containing information relevant to the construction of each character in the alphabet 41 | * `*rows` - how many rows of letters exist in the font file 42 | * `*cols` - how many columns of letters exist in the font file 43 | * `*grid_width` - in pixels, how wide is _exactly one letter_ 44 | * `*grid_height` - in pixels, how tall is _exactly one letter_ 45 | * `common_width` - in pixels, how wide should most characters _be rendered_ (will default to `grid_width` if not specified) 46 | * `character_widths` - an object containing width information for particular letters (will default to `grid_width` if not specified) 47 | * every `key` in this object will be _a single letter_ from the `alphabet`; every `value` in this object will be the number of pixels wide that this `key` should be rendered with 48 | * `default_spacing` - in pixels, the default amount of space to be left in between each letter when rendering (will default to `1` if not specified) 49 | * `kerning` - an object that specifies spacing between relationships of characters (will default to not use any kerning if not specified) 50 | * every `key` in this object will be _a single letter_ from the `alphabet`; every `value` will be another object with the form: 51 | * `*paired_with` - a list of all of the characters, that when appearing _after_ `key`, will have their spacing adjusted by the amount `spacing` 52 | * `*spacing` - in pixels, how much should the spacing be changed to accomodate the `key` : `paired_with` relationship (can be a negative value) when rendering 53 | 54 | 2. If you encounter a bug, please report it as an Issue here on this repository! If you are code-saavy, you can also fork this repository and then submit a pull-request. 55 | 56 | ## Credits 57 | 58 | This extension was commissioned by `dani boye` on the [Aseprite Discord server](https://discord.gg/Rt5S6NZFkK). He also provided the [6_by_9.png](./examples/6_by_9.png) to use as an example font! 59 | 60 | The json-parsing library, `json.lua`, was provided by `rxi` under the MIT License: https://github.com/rxi/json.lua 61 | 62 | As an advocate of open-source software, feel free to suggest edits, or just fork this repository and make your own! The license on this software is open for commercial and private use. This extension will remain free forever; however, if you'd like to buy me a coffee, you can do so here: 63 | 64 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/L3L766S5F) 65 | -------------------------------------------------------------------------------- /assets/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fletchmakes/font-custom/5f46cc22e8671cc835543e1b2adfdc9ee25e4d0e/assets/demo.png -------------------------------------------------------------------------------- /assets/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fletchmakes/font-custom/5f46cc22e8671cc835543e1b2adfdc9ee25e4d0e/assets/hero.png -------------------------------------------------------------------------------- /examples/6_by_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fletchmakes/font-custom/5f46cc22e8671cc835543e1b2adfdc9ee25e4d0e/examples/6_by_9.png -------------------------------------------------------------------------------- /examples/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "alphabet": "abcdefghijklmnopqrstuvwxyz!?.,", 3 | "sprite_path": "6_by_9.png", 4 | "atlas": { 5 | "rows": 6, 6 | "cols": 5, 7 | "grid_width": 6, 8 | "grid_height": 9, 9 | "common_width": 6, 10 | "character_widths": { 11 | "i": 2, 12 | "j": 3, 13 | "t": 4 14 | } 15 | }, 16 | "default_spacing": 1, 17 | "kerning": { 18 | "t": { 19 | "paired_with": "abc", 20 | "spacing": -1 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /extension/font-custom.lua: -------------------------------------------------------------------------------- 1 | -- MIT License 2 | 3 | -- Copyright (c) 2021 David Fletcher 4 | 5 | -- Permission is hereby granted, free of charge, to any person obtaining a copy 6 | -- of this software and associated documentation files (the "Software"), to deal 7 | -- in the Software without restriction, including without limitation the rights 8 | -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | -- copies of the Software, and to permit persons to whom the Software is 10 | -- furnished to do so, subject to the following conditions: 11 | 12 | -- The above copyright notice and this permission notice shall be included in all 13 | -- copies or substantial portions of the Software. 14 | 15 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | -- SOFTWARE. 22 | 23 | -- import libraries 24 | local json_lib_path = app.fs.joinPath(app.fs.userConfigPath, "extensions", "font-custom", "json.lua") 25 | json = dofile(json_lib_path) 26 | 27 | -- helper methods 28 | -- get everything from a file, returns an empty string if the file does not exist 29 | local function file_to_str(filename) 30 | if not app.fs.isFile(filename) then return "" end 31 | local file = io.open(filename) 32 | local str = file:read("*a") 33 | file:close() 34 | return str 35 | end 36 | 37 | -- return the index for a character (or nil if no index found) 38 | -- unfortunately we have to brute-force this one since string.find doesn't work 39 | -- as desired if we pass it a special pattern char like . % {} etc 40 | local function find_char_idx(str, search_char) 41 | for i = 1, #str do 42 | local char = str:sub(i, i) 43 | if (search_char == char) then 44 | return i 45 | end 46 | end 47 | 48 | return nil 49 | end 50 | 51 | -- create an error alert and exit the dialog 52 | local function create_error(str, dialog, exit) 53 | app.alert(str) 54 | if (exit == 1) then dialog:close() end 55 | end 56 | 57 | -- create a confirmation dialog and wait for the user to confirm 58 | local function create_confirm(str) 59 | local confirm = Dialog("Confirm?") 60 | 61 | confirm:label { 62 | id="text", 63 | text=str 64 | } 65 | 66 | confirm:button { 67 | id="cancel", 68 | text="Cancel", 69 | onclick=function() 70 | confirm:close() 71 | end 72 | } 73 | 74 | confirm:button { 75 | id="confirm", 76 | text="Confirm", 77 | onclick=function() 78 | confirm:close() 79 | end 80 | } 81 | 82 | -- always give the user a way to exit 83 | local function cancelWizard(confirm) 84 | confirm:close() 85 | end 86 | 87 | -- show to grab centered coordinates 88 | confirm:show{ wait=true } 89 | 90 | return confirm.data.confirm 91 | end 92 | 93 | -- get the calculated pixel width of the text string 94 | local function get_text_pixel_width(str) 95 | local last_char = "" 96 | local len = 0 97 | -- it might seem strange that we iterate until 1 past the string length 98 | -- however, we need to ensure that the accumulator includes with width of last_char 99 | -- this is a slightly hacky way to do so 100 | for i = 1, (#str+1) do 101 | -- get the pixels to paint onto the new image 102 | local char = "" 103 | if (i <= #str) then char = str:sub(i, i) end 104 | 105 | -- shift our 'cursor' over to get ready for the next letter 106 | if (last_char ~= "") then 107 | -- if the character has a special width value, use that instead 108 | if (props.atlas.character_widths[last_char] ~= nil) then 109 | len = len + props.atlas.character_widths[last_char] + props.default_spacing 110 | else 111 | len = len + props.atlas.common_width + props.default_spacing 112 | end 113 | -- add the kerning value if the current character has a special pairing with the previous 114 | if (props.kerning[last_char] ~= nil) then 115 | if (string.find(props.kerning[last_char].paired_with, char)) then 116 | len = len + props.kerning[last_char].spacing 117 | end 118 | end 119 | end 120 | 121 | -- remember the last character we scanned 122 | last_char = char 123 | end 124 | 125 | -- get the last_char's width and add that to the accumulator 126 | return len 127 | end 128 | 129 | -- grab pixel info from the image for the given character 130 | local function get_pixel_data(image, char) 131 | -- check if the character is a space 132 | if (char == " ") then return {} end 133 | 134 | local idx = find_char_idx(props.alphabet, char) - 1 135 | 136 | if (idx == nil) then create_error("Oh no! Could not find character in alphabet.", dlg, 1) end 137 | 138 | -- identify the "letter cell" in the font atlas that we need to scan 139 | local row = math.floor(idx / props.atlas.cols) 140 | local col = idx % props.atlas.cols 141 | 142 | local px_x = col * props.atlas.grid_width 143 | local px_y = row * props.atlas.grid_height 144 | 145 | -- now, scan every pixel into a handy table that we can read from later 146 | local pixels = {} 147 | for i = 0, (props.atlas.grid_height - 1) do -- rows 148 | pixels[i] = {} 149 | for j = 0, (props.atlas.grid_width - 1) do -- columns 150 | local newx = px_x + j 151 | local newy = px_y + i 152 | if (newx >= image.width) or (newy >= image.height) then 153 | break 154 | end 155 | pixels[i][j] = image:getPixel(newx, newy) 156 | end 157 | end 158 | 159 | return pixels 160 | end 161 | 162 | -- write pixel info to an image 163 | local function write_pixel_data(image, x, y, pixels) 164 | for i = 0, #pixels do -- rows 165 | if (not pixels[i]) then break end 166 | for j = 0, #pixels[i] do -- columns 167 | if (not pixels[i][j]) then break end 168 | image:drawPixel(j + x, i + y, pixels[i][j]) 169 | end 170 | end 171 | end 172 | 173 | -- data validation 174 | local function validate_char_width_prop(prop) 175 | local err = nil 176 | for key, val in pairs(prop) do 177 | -- each key should be a single character long 178 | if (#key > 1) then 179 | err = "Key \""..key.."\" is too long. It must specify one character only." 180 | break 181 | end 182 | -- each value should be an integer 183 | if (type(val) ~= "number") then 184 | err = "Value "..val.." (key \""..key.."\") must be an integer (with no decimal places)." 185 | break 186 | else 187 | -- modf returns a tuple; integer, fractional (parts of the number) 188 | local i, f = math.modf(val) 189 | if (f ~= 0) then 190 | err = "Value "..val.." (key \""..key.."\") must be an integer (with no decimal places)." 191 | break 192 | end 193 | end 194 | end 195 | 196 | return err 197 | end 198 | 199 | local function validate_kerning_prop(prop) 200 | local err = nil 201 | for key, val in pairs(prop) do 202 | -- each key should be a single character long 203 | if (#key > 1) then 204 | err = "Key \""..key.."\" is too long. It must specify one character only." 205 | break 206 | end 207 | -- each value should be an object with keys "paired_with" and "spacing" 208 | if (type(val) ~= "table") then 209 | err = "Value with key \""..key.."\" is not an object {}." 210 | break 211 | else 212 | -- validate that "paired_with" and "spacing" exist and are properly formatted 213 | if (val.paired_with == nil) then 214 | err = "Object with key \""..key.."\" is missing the \"paired_with\" property." 215 | break 216 | elseif (type(val.paired_with) ~= "string") then 217 | err = "Object with key \""..key.."\"'s \"paired_with\" property is formatted incorrectly." 218 | break 219 | end 220 | 221 | if (val.spacing == nil) then 222 | err = "Object with key \""..key.."\" is missing the \"spacing\" property." 223 | break 224 | elseif (type(val.spacing) ~= "number") then 225 | err = "Object with key \""..key.."\"'s \"spacing\" property must be an integer (with no decimal places)." 226 | break 227 | else 228 | -- modf returns a tuple; integer, fractional (parts of the number) 229 | local i, f = math.modf(val.spacing) 230 | if (f ~= 0) then 231 | err = "Object with key \""..key.."\"'s \"spacing\" property must be an integer (with no decimal places)." 232 | break 233 | end 234 | end 235 | end 236 | end 237 | 238 | return err 239 | end 240 | 241 | -- now that we've read in properties, update the dialog to allow the user to continue 242 | local function update_dialog_with_props(dialog, success) 243 | dialog:modify { 244 | id="properties_label", 245 | text="Properties read in successfully!" 246 | } 247 | 248 | dialog:modify { 249 | id="text", 250 | enabled=true 251 | } 252 | 253 | dialog:modify { 254 | id="ok", 255 | enabled=true 256 | } 257 | end 258 | 259 | -- properties object to be used later 260 | props = { 261 | alphabet={}, 262 | sprite="", 263 | rows=0, 264 | cols=0, 265 | width=0, 266 | height=0 267 | } 268 | 269 | -- declare the dialog object 270 | dlg = Dialog("Use Custom Pixel Font") 271 | 272 | -- populate dialog object 273 | -- properties file 274 | dlg:file { 275 | id="props", 276 | label="Open Properties File", 277 | title="Properties File", 278 | open=true, 279 | filetypes={ "json" } 280 | } 281 | 282 | -- read properties in and display to user 283 | dlg:button { 284 | id="read", 285 | text="Read Properties File", 286 | focus=false, 287 | onclick=function() 288 | local props_filename = dlg.data.props 289 | 290 | -- attempt to load the properties 291 | if (not app.fs.isFile(props_filename)) then 292 | create_error("Oh no! Error loading the properties file.", dlg, 0) 293 | return 294 | end 295 | 296 | -- read and parse the file 297 | local json_str = file_to_str(props_filename) 298 | if (json_str == "") then 299 | create_error("Oh no! Error reading the properties file.", dlg, 0) 300 | return 301 | end 302 | 303 | local status, val = pcall(json.decode, json_str) 304 | if (not status) then create_error(val, dlg, 0) return 305 | else props = val end 306 | 307 | -- validate all essential properties exist 308 | if (not props.alphabet) or (props.alphabet == "") then 309 | create_error("Missing an essential property: alphabet", dlg, 0) 310 | return 311 | end 312 | 313 | if (not props.sprite_path) or (props.sprite_path == "") then 314 | create_error("Missing an essential property: sprite_path", dlg, 0) 315 | return 316 | end 317 | 318 | local checkExt = app.fs.fileExtension(props.sprite_path) 319 | if (checkExt ~= "png") and (checkExt ~= "aseprite") and (checkExt ~= "ase") then 320 | create_error("Sprite in .json must have one of the following extensions: .png, .ase, .aseprite") 321 | return 322 | end 323 | 324 | if (not props.atlas) then 325 | create_error("Missing an essential object: atlas", dlg, 0) 326 | return 327 | end 328 | 329 | if (not props.atlas.rows) or (props.atlas.rows < 0) then 330 | create_error("Missing an essential property: atlas.rows", dlg, 0) 331 | return 332 | end 333 | 334 | if (not props.atlas.cols) or (props.atlas.cols < 0) then 335 | create_error("Missing an essential property: atlas.cols", dlg, 0) 336 | return 337 | end 338 | 339 | if (not props.atlas.grid_width) or (props.atlas.grid_width < 0) then 340 | create_error("Missing an essential property: atlas.grid_width", dlg, 0) 341 | return 342 | end 343 | 344 | if (not props.atlas.grid_height) or (props.atlas.grid_height < 0) then 345 | create_error("Missing an essential property: atlas.grid_height", dlg, 0) 346 | return 347 | end 348 | 349 | -- default certain values so that we don't hit undefined errors 350 | if (props.atlas.common_width == nil) then 351 | props.atlas.common_width = props.atlas.grid_width 352 | end 353 | 354 | if (props.atlas.character_widths == nil) then 355 | props.atlas.character_widths = {} 356 | else 357 | local err = validate_char_width_prop(props.atlas.character_widths) 358 | if (err) then 359 | create_error(err, dlg, 0) 360 | return 361 | end 362 | end 363 | 364 | if (props.default_spacing == nil) then 365 | props.default_spacing = 1 366 | end 367 | 368 | if (props.kerning == nil) then 369 | props.kerning = {} 370 | else 371 | local err = validate_kerning_prop(props.kerning) 372 | if (err) then 373 | create_error(err, dlg, 0) 374 | return 375 | end 376 | end 377 | 378 | -- if the filename is relative, try to find it in the same directory as the properties file 379 | if (not string.find(props.sprite_path, app.fs.pathSeparator)) then 380 | local full_path = app.fs.joinPath(app.fs.filePath(props_filename), props.sprite_path) 381 | props.sprite_path = full_path 382 | end 383 | 384 | -- check for existence 385 | if (not app.fs.isFile(props.sprite_path)) then 386 | create_error("No sprite file found at location: "..props.sprite_path, dlg, 0) 387 | return 388 | end 389 | 390 | -- update dialog with prop values 391 | update_dialog_with_props(dlg) 392 | end 393 | } 394 | 395 | -- separator 396 | dlg:separator { id="props_separator" } 397 | 398 | dlg:label { 399 | id="properties_label", 400 | label="Properties:", 401 | visible=true, 402 | text="No properties found. Read in file first." 403 | } 404 | 405 | -- separator 406 | dlg:separator { id="text_separator" } 407 | 408 | -- text to draw to screen 409 | dlg:entry { 410 | id="text", 411 | label="Text:", 412 | text="", 413 | focus=false, 414 | enabled=false 415 | } 416 | 417 | -- OK button to execute logic 418 | dlg:button { 419 | id="ok", 420 | text="OK", 421 | focus=false, 422 | enabled=false, 423 | onclick=function() 424 | app.transaction( function() 425 | local text = dlg.data.text 426 | 427 | -- validate the text field is not blank 428 | if (text == "") then 429 | create_error("Oh no! The text field was left blank.", dlg, 0) 430 | return 431 | end 432 | 433 | -- validate the text does not use any characters not defined in the alphabet 434 | for char in text:gmatch"." do 435 | if (not find_char_idx(props.alphabet, char)) and (char ~= " ") then 436 | create_error("Oh no! Text has characters not defined by the alphabet.", dlg, 0) 437 | return 438 | end 439 | end 440 | 441 | -- do math to center the text within the destination image 442 | local text_width = get_text_pixel_width(text) 443 | local text_height = props.atlas.grid_height 444 | local img_sprite = app.activeSprite 445 | 446 | local continue = true 447 | if (text_width > img_sprite.width) or (text_height > img_sprite.height) then 448 | -- if the text wouldn't fit, ask if they would like to continue anyways 449 | continue = create_confirm("The text won't fit on the canvas, and will be clipped. Would you like to continue anyways?") 450 | end 451 | 452 | if (continue) then 453 | -- save the current sprite into a variable so we can manipulate it later 454 | local layer = img_sprite:newLayer() 455 | layer.name = "CUSTOM TEXT" 456 | local img_cel = img_sprite:newCel(layer, app.activeFrame) 457 | local img_image = img_cel.image 458 | 459 | -- open the sprite 460 | local font_sprite = app.open(props.sprite_path) 461 | -- flatten the sprite so it only has 1 layer 462 | font_sprite:flatten() 463 | -- however, flattening also "trims" the image to the smallest available size 464 | -- we need to resize the image again back to the full height / width of the canvas 465 | local font_image = font_sprite.layers[1]:cel(1).image 466 | 467 | local x = math.floor( (img_image.width - text_width) / 2 ) 468 | local y = math.floor( (img_image.height - text_height) / 2 ) 469 | local last_char = "" 470 | -- paint every character in the text string to the destination image 471 | for i = 1, #text do 472 | -- get the pixels to paint onto the new image 473 | local char = text:sub(i, i) 474 | local pixels = get_pixel_data(font_image, char) 475 | 476 | -- shift our 'cursor' over to get ready for the next letter 477 | if (last_char ~= "") then 478 | -- if the character has a special width value, use that instead 479 | if (props.atlas.character_widths[last_char] ~= nil) then 480 | x = x + props.atlas.character_widths[last_char] + props.default_spacing 481 | else 482 | x = x + props.atlas.common_width + props.default_spacing 483 | end 484 | -- add the kerning value if the current character has a special pairing with the previous 485 | if (props.kerning[last_char] ~= nil) then 486 | if (string.find(props.kerning[last_char].paired_with, char)) then 487 | x = x + props.kerning[last_char].spacing 488 | end 489 | end 490 | end 491 | 492 | -- paint them on the new image 493 | write_pixel_data(img_image, x, y, pixels) 494 | 495 | -- remember the last character we printed 496 | last_char = char 497 | end 498 | 499 | font_sprite:close() -- THIS DOES NOT SAVE THE SPRITE (which is what we want) 500 | end 501 | 502 | -- wrap up the dialog 503 | app.activeSprite = img_sprite 504 | dlg:close() 505 | end ) -- end transaction 506 | end 507 | } 508 | 509 | 510 | -- always give the user a way to exit 511 | local function cancelWizard(dlg) 512 | dlg:close() 513 | end 514 | 515 | -- show to grab centered coordinates 516 | dlg:show{ wait=false } 517 | -------------------------------------------------------------------------------- /extension/json.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- json.lua 3 | -- 4 | -- Copyright (c) 2020 rxi 5 | -- 6 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | -- this software and associated documentation files (the "Software"), to deal in 8 | -- the Software without restriction, including without limitation the rights to 9 | -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | -- of the Software, and to permit persons to whom the Software is furnished to do 11 | -- so, subject to the following conditions: 12 | -- 13 | -- The above copyright notice and this permission notice shall be included in all 14 | -- copies or substantial portions of the Software. 15 | -- 16 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | -- SOFTWARE. 23 | -- 24 | 25 | local json = { _version = "0.1.2" } 26 | 27 | ------------------------------------------------------------------------------- 28 | -- Encode 29 | ------------------------------------------------------------------------------- 30 | 31 | local encode 32 | 33 | local escape_char_map = { 34 | [ "\\" ] = "\\", 35 | [ "\"" ] = "\"", 36 | [ "\b" ] = "b", 37 | [ "\f" ] = "f", 38 | [ "\n" ] = "n", 39 | [ "\r" ] = "r", 40 | [ "\t" ] = "t", 41 | } 42 | 43 | local escape_char_map_inv = { [ "/" ] = "/" } 44 | for k, v in pairs(escape_char_map) do 45 | escape_char_map_inv[v] = k 46 | end 47 | 48 | 49 | local function escape_char(c) 50 | return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) 51 | end 52 | 53 | 54 | local function encode_nil(val) 55 | return "null" 56 | end 57 | 58 | 59 | local function encode_table(val, stack) 60 | local res = {} 61 | stack = stack or {} 62 | 63 | -- Circular reference? 64 | if stack[val] then error("circular reference") end 65 | 66 | stack[val] = true 67 | 68 | if rawget(val, 1) ~= nil or next(val) == nil then 69 | -- Treat as array -- check keys are valid and it is not sparse 70 | local n = 0 71 | for k in pairs(val) do 72 | if type(k) ~= "number" then 73 | error("invalid table: mixed or invalid key types") 74 | end 75 | n = n + 1 76 | end 77 | if n ~= #val then 78 | error("invalid table: sparse array") 79 | end 80 | -- Encode 81 | for i, v in ipairs(val) do 82 | table.insert(res, encode(v, stack)) 83 | end 84 | stack[val] = nil 85 | return "[" .. table.concat(res, ",") .. "]" 86 | 87 | else 88 | -- Treat as an object 89 | for k, v in pairs(val) do 90 | if type(k) ~= "string" then 91 | error("invalid table: mixed or invalid key types") 92 | end 93 | table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) 94 | end 95 | stack[val] = nil 96 | return "{" .. table.concat(res, ",") .. "}" 97 | end 98 | end 99 | 100 | 101 | local function encode_string(val) 102 | return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' 103 | end 104 | 105 | 106 | local function encode_number(val) 107 | -- Check for NaN, -inf and inf 108 | if val ~= val or val <= -math.huge or val >= math.huge then 109 | error("unexpected number value '" .. tostring(val) .. "'") 110 | end 111 | return string.format("%.14g", val) 112 | end 113 | 114 | 115 | local type_func_map = { 116 | [ "nil" ] = encode_nil, 117 | [ "table" ] = encode_table, 118 | [ "string" ] = encode_string, 119 | [ "number" ] = encode_number, 120 | [ "boolean" ] = tostring, 121 | } 122 | 123 | 124 | encode = function(val, stack) 125 | local t = type(val) 126 | local f = type_func_map[t] 127 | if f then 128 | return f(val, stack) 129 | end 130 | error("unexpected type '" .. t .. "'") 131 | end 132 | 133 | 134 | function json.encode(val) 135 | return ( encode(val) ) 136 | end 137 | 138 | 139 | ------------------------------------------------------------------------------- 140 | -- Decode 141 | ------------------------------------------------------------------------------- 142 | 143 | local parse 144 | 145 | local function create_set(...) 146 | local res = {} 147 | for i = 1, select("#", ...) do 148 | res[ select(i, ...) ] = true 149 | end 150 | return res 151 | end 152 | 153 | local space_chars = create_set(" ", "\t", "\r", "\n") 154 | local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") 155 | local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") 156 | local literals = create_set("true", "false", "null") 157 | 158 | local literal_map = { 159 | [ "true" ] = true, 160 | [ "false" ] = false, 161 | [ "null" ] = nil, 162 | } 163 | 164 | 165 | local function next_char(str, idx, set, negate) 166 | for i = idx, #str do 167 | if set[str:sub(i, i)] ~= negate then 168 | return i 169 | end 170 | end 171 | return #str + 1 172 | end 173 | 174 | 175 | local function decode_error(str, idx, msg) 176 | local line_count = 1 177 | local col_count = 1 178 | for i = 1, idx - 1 do 179 | col_count = col_count + 1 180 | if str:sub(i, i) == "\n" then 181 | line_count = line_count + 1 182 | col_count = 1 183 | end 184 | end 185 | error( string.format("%s at line %d col %d", msg, line_count, col_count) ) 186 | end 187 | 188 | 189 | local function codepoint_to_utf8(n) 190 | -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa 191 | local f = math.floor 192 | if n <= 0x7f then 193 | return string.char(n) 194 | elseif n <= 0x7ff then 195 | return string.char(f(n / 64) + 192, n % 64 + 128) 196 | elseif n <= 0xffff then 197 | return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) 198 | elseif n <= 0x10ffff then 199 | return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, 200 | f(n % 4096 / 64) + 128, n % 64 + 128) 201 | end 202 | error( string.format("invalid unicode codepoint '%x'", n) ) 203 | end 204 | 205 | 206 | local function parse_unicode_escape(s) 207 | local n1 = tonumber( s:sub(1, 4), 16 ) 208 | local n2 = tonumber( s:sub(7, 10), 16 ) 209 | -- Surrogate pair? 210 | if n2 then 211 | return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) 212 | else 213 | return codepoint_to_utf8(n1) 214 | end 215 | end 216 | 217 | 218 | local function parse_string(str, i) 219 | local res = "" 220 | local j = i + 1 221 | local k = j 222 | 223 | while j <= #str do 224 | local x = str:byte(j) 225 | 226 | if x < 32 then 227 | decode_error(str, j, "control character in string") 228 | 229 | elseif x == 92 then -- `\`: Escape 230 | res = res .. str:sub(k, j - 1) 231 | j = j + 1 232 | local c = str:sub(j, j) 233 | if c == "u" then 234 | local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) 235 | or str:match("^%x%x%x%x", j + 1) 236 | or decode_error(str, j - 1, "invalid unicode escape in string") 237 | res = res .. parse_unicode_escape(hex) 238 | j = j + #hex 239 | else 240 | if not escape_chars[c] then 241 | decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") 242 | end 243 | res = res .. escape_char_map_inv[c] 244 | end 245 | k = j + 1 246 | 247 | elseif x == 34 then -- `"`: End of string 248 | res = res .. str:sub(k, j - 1) 249 | return res, j + 1 250 | end 251 | 252 | j = j + 1 253 | end 254 | 255 | decode_error(str, i, "expected closing quote for string") 256 | end 257 | 258 | 259 | local function parse_number(str, i) 260 | local x = next_char(str, i, delim_chars) 261 | local s = str:sub(i, x - 1) 262 | local n = tonumber(s) 263 | if not n then 264 | decode_error(str, i, "invalid number '" .. s .. "'") 265 | end 266 | return n, x 267 | end 268 | 269 | 270 | local function parse_literal(str, i) 271 | local x = next_char(str, i, delim_chars) 272 | local word = str:sub(i, x - 1) 273 | if not literals[word] then 274 | decode_error(str, i, "invalid literal '" .. word .. "'") 275 | end 276 | return literal_map[word], x 277 | end 278 | 279 | 280 | local function parse_array(str, i) 281 | local res = {} 282 | local n = 1 283 | i = i + 1 284 | while 1 do 285 | local x 286 | i = next_char(str, i, space_chars, true) 287 | -- Empty / end of array? 288 | if str:sub(i, i) == "]" then 289 | i = i + 1 290 | break 291 | end 292 | -- Read token 293 | x, i = parse(str, i) 294 | res[n] = x 295 | n = n + 1 296 | -- Next token 297 | i = next_char(str, i, space_chars, true) 298 | local chr = str:sub(i, i) 299 | i = i + 1 300 | if chr == "]" then break end 301 | if chr ~= "," then decode_error(str, i, "expected ']' or ','") end 302 | end 303 | return res, i 304 | end 305 | 306 | 307 | local function parse_object(str, i) 308 | local res = {} 309 | i = i + 1 310 | while 1 do 311 | local key, val 312 | i = next_char(str, i, space_chars, true) 313 | -- Empty / end of object? 314 | if str:sub(i, i) == "}" then 315 | i = i + 1 316 | break 317 | end 318 | -- Read key 319 | if str:sub(i, i) ~= '"' then 320 | decode_error(str, i, "expected string for key") 321 | end 322 | key, i = parse(str, i) 323 | -- Read ':' delimiter 324 | i = next_char(str, i, space_chars, true) 325 | if str:sub(i, i) ~= ":" then 326 | decode_error(str, i, "expected ':' after key") 327 | end 328 | i = next_char(str, i + 1, space_chars, true) 329 | -- Read value 330 | val, i = parse(str, i) 331 | -- Set 332 | res[key] = val 333 | -- Next token 334 | i = next_char(str, i, space_chars, true) 335 | local chr = str:sub(i, i) 336 | i = i + 1 337 | if chr == "}" then break end 338 | if chr ~= "," then decode_error(str, i, "expected '}' or ','") end 339 | end 340 | return res, i 341 | end 342 | 343 | 344 | local char_func_map = { 345 | [ '"' ] = parse_string, 346 | [ "0" ] = parse_number, 347 | [ "1" ] = parse_number, 348 | [ "2" ] = parse_number, 349 | [ "3" ] = parse_number, 350 | [ "4" ] = parse_number, 351 | [ "5" ] = parse_number, 352 | [ "6" ] = parse_number, 353 | [ "7" ] = parse_number, 354 | [ "8" ] = parse_number, 355 | [ "9" ] = parse_number, 356 | [ "-" ] = parse_number, 357 | [ "t" ] = parse_literal, 358 | [ "f" ] = parse_literal, 359 | [ "n" ] = parse_literal, 360 | [ "[" ] = parse_array, 361 | [ "{" ] = parse_object, 362 | } 363 | 364 | 365 | parse = function(str, idx) 366 | local chr = str:sub(idx, idx) 367 | local f = char_func_map[chr] 368 | if f then 369 | return f(str, idx) 370 | end 371 | decode_error(str, idx, "unexpected character '" .. chr .. "'") 372 | end 373 | 374 | 375 | function json.decode(str) 376 | if type(str) ~= "string" then 377 | error("expected argument of type string, got " .. type(str)) 378 | end 379 | local res, idx = parse(str, next_char(str, 1, space_chars, true)) 380 | idx = next_char(str, idx, space_chars, true) 381 | if idx <= #str then 382 | decode_error(str, idx, "trailing garbage") 383 | end 384 | return res 385 | end 386 | 387 | 388 | return json 389 | -------------------------------------------------------------------------------- /extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "font-custom", 3 | "displayName": "Font Custom", 4 | "description": "Create and use your custom pixel fonts!", 5 | "version": "1.1", 6 | "author": { "name": "David Fletcher", 7 | "email": "david.login@aol.com", 8 | "url": "https://github.com/david-fletcher/font-custom" }, 9 | "contributors": [ ], 10 | "publisher": "David Fletcher", 11 | "license": "MIT", 12 | "categories": [ "Scripts" ], 13 | "contributes": { 14 | "scripts": [ 15 | { "path": "./plugin.lua" } 16 | ] 17 | } 18 | } -------------------------------------------------------------------------------- /extension/plugin.lua: -------------------------------------------------------------------------------- 1 | -- MIT License 2 | 3 | -- Copyright (c) 2021 David Fletcher 4 | 5 | -- Permission is hereby granted, free of charge, to any person obtaining a copy 6 | -- of this software and associated documentation files (the "Software"), to deal 7 | -- in the Software without restriction, including without limitation the rights 8 | -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | -- copies of the Software, and to permit persons to whom the Software is 10 | -- furnished to do so, subject to the following conditions: 11 | 12 | -- The above copyright notice and this permission notice shall be included in all 13 | -- copies or substantial portions of the Software. 14 | 15 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | -- SOFTWARE. 22 | 23 | function init(plugin) 24 | print("Aseprite is initializing Font Custom") 25 | 26 | plugin:newCommand { 27 | id="Custom Font", 28 | title="Use Custom Font", 29 | group="edit_fx", 30 | onclick=function() 31 | local executable = app.fs.joinPath(app.fs.userConfigPath, "extensions", "font-custom", "font-custom.lua") 32 | dofile(executable) 33 | end 34 | } 35 | end 36 | 37 | function exit(plugin) 38 | print("Aseprite is closing Font Custom") 39 | end -------------------------------------------------------------------------------- /font-custom.aseprite-extension: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fletchmakes/font-custom/5f46cc22e8671cc835543e1b2adfdc9ee25e4d0e/font-custom.aseprite-extension --------------------------------------------------------------------------------