📜 [Script] Amiga IFF/ILBM Export

Itch Cover

Aseprite and Amiga game developers rejoice!

Presenting a new script for Aseprite pixel artists that have always wished to be able to export their sprites to Amiga IFF/ILBM image format directly from within Aseprite.

Please check it out!

Link to Product Page on Itch

2 Likes

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

Hey, thanks for all the great feedback! Wow, that’s quite a bit! Much appreciated!

I’ll review and consider making some of those changes. I’ll already started using some of your coding advice. :slight_smile:

This Amiga IFF/ILBM Export Script has been updated today and includes the following new enhancements:

  • Dialog button shortcuts have been added.
  • Added a new checkbox option to export just the sprite’s palette (CMAP). This is useful for Deluxe Paint users and Amiga game developers that work in Blitz Basic 2 and any other tools that allow you to load in an IFF palette file. The file essentially consists of just the IFF CMAP chunk.
  • Miscellaneous code cleanup.

Hi Jeremy.

Blockquote
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.

I see the artifacts but I’m not sure how this relates to my script? Can you please explain?

Blockquote
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.

Firstly, how did you import that IFF into Aseprite? Anyway, I tried with a different image and ensured that the layer was bigger than the sprite bounds. I then exported to IFF with my script and opened on a real Amiga. Only the visible portion of the layer within the sprite was found in the IFF file as it should. The image was not garbled as it is in your example. I noticed that you are using an unusual version of Aseprite, according to those screenshots. Could that be the reason? Any chance you can try whatever steps you followed with the latest version of Aseprite (1.32)?

Hey @Mousey! You are very welcome! This is precisely why I created this script. I started looking into Blitz2 myself recently and noticed that there was a gap between modern sprite editors and importing sprites onto the Amiga. So I wanted to fill that gap for myself and others that may be in the same situation.

Also, be sure to check out my other Amiga related script (the Amiga OCS/ECS Color Palette Mixer) . It should be of great help to you as well to ensure that you use the limited colour set that the Amiga uses.

Enjoy!

Hi Nelson,

I tested in Aseprite version 1.3.2 and got the same results. Here are 2 other file examples:

Example: https://amiga.lychesis.net/assets/Ra/Ra_Crawl.iff (from Dec. 31, 2020).

Imported with a script:

Opened in Irfanview:

Below are the first three rows from the file in hex:

Row 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
00000000 46 4F 52 4D 00 00 72 C0 50 42 4D 20 42 4D 48 44 F O R M . . r P B M . B M H D
00000010 00 00 00 14 01 70 01 22 00 00 00 00 08 00 01 00 . . . . . p . " . . . . . . . .
00000020 00 00 05 06 01 40 00 C8 43 4D 41 50 00 00 03 00 . . . . . @ . C M A P . . . .

width: 368
height: 290
planes: 8
masking: 0
compressed: 1
alphaIndex: 0
xAspect: 5
yAspect: 6
wPage: 320
hPage: 200

The exported version:

It looks the same in Irfanview.

Row 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
00000000 46 4F 52 4D 00 01 A4 1C 49 4C 42 4D 42 4D 48 44 F O R M . . . I L B M B M H D
00000010 00 00 00 14 01 40 00 C8 00 00 00 00 08 02 00 00 . . . . . @ . . . . . . . . .
00000020 00 00 2C 2C 01 40 01 00 43 4D 41 50 00 00 03 00 . . , , . @ . . C M A P . . . .

width: 320
height: 200
planes: 8
masking: 2
compressed: 0
alphaIndex: 0
xAspect: 44
yAspect: 44
wPage: 320
hPage: 256

Here’s a case where the image is narrower than the sprite, taller than the export page. The exported version can’t be re-imported either by script or by Irvanview: https://amiga.lychesis.net/assets/Fairfax/Fairfax_Orthlund.iff

Original:

Row 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
00000000 46 4F 52 4D 00 01 B0 9C 49 4C 42 4D 42 4D 48 44 F O R M . . I L B M B M H D
00000010 00 00 00 14 01 20 02 00 00 00 00 00 06 00 00 00 . . . . . . . . . . . . . . . .
00000020 00 00 14 0B 01 40 02 00 43 41 4D 47 00 00 00 04 . . . . . @ . . C A M G . . . .

width: 288
height: 512
planes: 6
masking: 0
compressed: 0
alphaIndex: 0
xAspect: 20
yAspect: 11
wPage: 320
hPage: 512

Exported:

Row 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
00000000 46 4F 52 4D 00 01 B0 FC 49 4C 42 4D 42 4D 48 44 F O R M . . I L B M B M H D
00000010 00 00 00 14 01 40 02 00 00 00 00 00 06 02 00 00 . . . . . @ . . . . . . . . . .
00000020 00 00 2C 2C 01 40 01 00 43 4D 41 50 00 00 00 C0 . . , , . @ . . C M A P . . .

width: 320
height: 512
planes: 6
masking: 2
compressed: 0
alphaIndex: 0
xAspect: 44
yAspect: 44
wPage: 320
hPage: 256