📜 [Script] Amiga IFF/ILBM Export

Hi again,

I’m hailing from the other thread. Your script is cool and well done, even more so for being new to Aseprite and to scripting in its API. Thanks for being open to feedback.

I hope some of these ideas will help you make it better. I realize how sucky it is when you’re leaned on for this and that feature, however, after you’ve put your work on the Internet for free. The .iff extension seems so expansive that you could spend a lot of time and effort and still not explore all the possibilities or quash every bug.

As mentioned, I don’t have an Amiga, am not part of that community. Excuse incorrect assumptions, terminology, etc.

Suggestions
  1. Putting an ampersand before a character in the text field allows the user to hold down Alt and press the letter to activate a button. For example, Alt+E for dlg:button { id = "export", text = "&Export", focus = true }. However, beware of conflicts with the menus in the menu bar. Alt+E is also the Edit menu, so maybe an alternative like "E&xport".

  2. A cel image may be smaller or larger than the sprite canvas, or it may fall partially outside the sprite canvas due to an offset position. The BMHD chunk seems to support both of these possibilities (maybe SPRT is also needed, idk?) . I don’t understand why page width and height are signed words, though, unless my analogy between a page and a sprite canvas is wrong. You can simplify the matter by creating a flattened copy at a given frame with Image:drawSprite. The sprite’s spec can be passed to the image constructor. If you prefer to handle the complexity, try a rectangle intersection between a sprite’s and cel’s bounds.

  3. If you want to handle cel images, beware of tile map color mode images from tile map layers in Aseprite 1.3 onward. How to handle these varies with which version of 1.3 you use, and may be simplified in the future / from the last time I focused on them.

  4. Aseprite supports pixel aspect ratios. The ratios supported explicitly by Aseprite’s UI are 2:1 and 1:2. Internally, the scripting API allows whatever. By ‘whatever’, I mean including invalid ratios like -5:0. Aspect ratios like 11:13 will have visual glitches, but may be worth it. To support this would require reducing aspect ratios – e.g., from 6:3 to 2:1 – to keep the sprite from being so big that Aseprite can’t zoom out enough.

  5. For palettes, it’d be smoother UX imo to find the nearest larger power of 2 from the palette’s length, then write the palette (or up to 256 in case the palette is longer than that), then fill any remainder with a default color. Maybe black, or maybe loop through the palette repeatedly using the floor modulo operator, % . This is arguable, I suppose, since you may prefer to fail loudly if users do not discipline themselves re: the medium’s constraints. But you know how users are.

  6. Same with color mode, although you could say this is even more hand-holdy. For RGB color mode, you could access Aseprite’s nearest palette match functionality – without converting the sprite – via a Color object’s index field. A dictionary using a 32-bit integer ABGR color as a key is an option if you’re concerned about performance costs. For grayscale, you can’t assume that the sprite palette is 256 entries from black to white, but you can create such a palette. After the alpha is isolated from the 16-bit pixel, the gray value can be demoted to an index. Or you can promote a gray image to 32-bit color by repeating a pixel gray three times as BGR. Or you could try the sprite’s copy constructor to create a duplicate, convert to indexed (the command allows for dithering options), then close the duplicate after writing the file.

  7. For .bmp files, Aseprite uses whether or not a sprite has a background as a surrogate for determining 24-bit RGB color vs. 32-bit. I guess for .iff this is referred to as ‘deep’ or ‘true color’. Alpha with background layers in indexed color mode is messy because you can, for example, set an opaque swatch at index 6 to be your transparent color and paint a background with a palette swatch that has zero alpha at index 7.

  8. It could be nice to support specific extensions, say, .ilbm in addition to .iff. However, that can introduce extra problems, for example, if the user types .ilbm but then the sprite has something critical that’s unsupported by that format. It also may change when you can report a problem to the user.

  9. I like to not close the dialog on export, but to just announce success and return to the ready state. I usually use app.alert when the UI is available, but I also like how you’ve set it up.

  10. I tried and failed to write a compression algorithm from scratch. I gather that it falls under run-length encoding. One of the tricks being that it prefixes both runs of same and different bytes with a length signal, if I understand the spec correctly.

  11. Precede function definitions with local unless there’s a reason for them to be global, e.g., local function showError(err). Same for variables local spr = app.sprite.

  12. In newer versions of Lua, the <const> keyword allows for immutability, e.g., local dlg <const> = Dialog("Export Complete"). I find it makes debugging easier, and is worth the extra syntax. The const is less of a safeguard to reference types than value types, though.

  13. The line title=string, in the dialog file widget is probably included by mistake.

  14. If you plan on more Lua coding, look for extensions for your editor of choice. If the ext. supports custom type definitions, look for one. I use one for Aseprite version 1.3 (forked from the original) with the Lua LLS extension in VS Code. When all else fails, check the source code that handles inputs from Lua. It helps to reference the return type of certain methods.

  15. I do not specialize in optimization or performance. And I realize the first rule of optimization is don’t. But a general Lua guide on performance I know of is “Lua Performance Tips” by Roberto Ierusalimschy. I use Lua’s os.clock to measure elapsed time.

Images to illustrate some things from above:

Above, the King Tut display aspect is 11:13 on the left vs. 1:1 on the right. The left is at 8.3% zoom. Notice the visual artifacts from moving the dialog window around.

Above is Seven Seas by Fairfax in Aseprite. The blue rectangle is from View > Show > Layer Edges (the UI terminology differs from that of the scripting API). The image is larger than the sprite canvas. An importing bug on my end is a likelihood. For images created in Aseprite from the start, arguably not a big deal, given how regularly cels are trimmed.

Above, IrfanView seems to ignore any difference between image width and sprite width, or maybe takes the larger of the two?

Above is IrfanView’s display of the re-exported image from the script.

Cheers,
Jeremy