Skip to content

feat: add bitmap (BMP) encoder for 24-bit RGB images#35

Merged
Tsukina-7mochi merged 10 commits intodevelopfrom
claude/bitmap-018Xb3qDxXRKwYkBkECEW2Ge
Nov 14, 2025
Merged

feat: add bitmap (BMP) encoder for 24-bit RGB images#35
Tsukina-7mochi merged 10 commits intodevelopfrom
claude/bitmap-018Xb3qDxXRKwYkBkECEW2Ge

Conversation

@Tsukina-7mochi
Copy link
Owner

Summary

Adds a library to export Aseprite RGB images as 24-bit BMP files, enabling exports in the widely-supported bitmap format.

Features

  • Encodes Aseprite Image objects to BMP binary string
  • Supports 24-bit RGB color mode only
  • Proper BGR byte order conversion
  • Bottom-to-top row ordering per BMP specification
  • Automatic row padding to 4-byte boundaries
  • Standard BMP file and info headers (14 + 40 bytes)

Implementation

  • src/pkg/bitmap/init.lua: Main BMP encoder with create() API
  • src/pkg/bitmap/bitmap_test.lua: Comprehensive test suite with 11 test cases
  • src/pkg/asepriteUtil/mock.lua: Reusable Aseprite API mocks for testing

Usage Example

local bitmap = require("pkg.bitmap")

-- In Aseprite script
local image = app.activeSprite.cels[1].image
local bmpData = bitmap.create(image)

-- Save to file
local file = io.open("output.bmp", "wb")
file:write(bmpData)
file:close()

Dependencies

Builds on PR #34 which added little-endian pack functions needed for BMP format.

Test Coverage

  • File header generation
  • Info header structure
  • BGR byte order conversion
  • Row padding for various widths
  • Bottom-to-top row ordering
  • Complete BMP file generation
  • Color mode validation

claude and others added 10 commits November 14, 2025 05:56
Add a library to export Aseprite RGB images as 24-bit BMP files. This
enables exporting images in the widely-supported bitmap format.

Features:
- Encodes Aseprite Image objects to BMP binary string
- Supports 24-bit RGB color mode only
- Proper BGR byte order conversion
- Bottom-to-top row ordering per BMP specification
- Automatic row padding to 4-byte boundaries
- Standard BMP file and info headers (14 + 40 bytes)

Implementation:
- src/pkg/bitmap/init.lua: Main BMP encoder with create() API
- src/pkg/bitmap/bitmap_test.lua: Comprehensive test suite
- src/pkg/asepriteUtil/mock.lua: Reusable Aseprite API mocks for testing

The bitmap encoder uses the little-endian pack functions added in PR #34
to generate proper BMP file structures.
Change bitmap.create() to return a table with metatable instead of
a plain string. This allows the bitmap object to be converted to a
string via tostring() or the __tostring metamethod.

Changes:
- Replace create() function with bitmap class using metatable
- Add tostring() method that generates BMP binary data
- Set __tostring metamethod for implicit string conversion
- Update all tests to use new API: bitmap(image)
- Add test for both explicit and implicit tostring conversion

Usage:
```lua
local bitmap = require("pkg.bitmap").bitmap
local bmp = bitmap(image)

-- Explicit conversion
local data = bmp:tostring()

-- Implicit conversion
local data = tostring(bmp)

-- Or use in string concatenation
local data = "" .. bmp
```

This pattern matches the RIFF chunk implementation and provides
more flexibility for future extensions.
Move bitmap class definition from init.lua to bitmap.lua for better
code organization and separation of concerns.

Changes:
- Create src/pkg/bitmap/bitmap.lua with bitmap class and helper functions
- Update init.lua to just require and export the bitmap class
- No API changes - tests continue to work without modification

This makes the codebase more modular and easier to maintain.
Major refactoring of bitmap module for cleaner architecture:

- mock.lua: Use bit operations instead of string.pack for rgba
- bitmap.lua: Use table.concat instead of string concatenation
- bitmap.lua: Rename class to BitmapFile and store components separately
  - fileHeader: BMP file header (14 bytes)
  - bitmapInfoHeader: Bitmap info header (40 bytes)
  - pixelData: Binary pixel data
- bitmap.lua: Export create function instead of __call metamethod
- init.lua: Simplify to directly return bitmap module
- bitmap_test.lua: Use hard-coded binary string literals instead of helpers
- bitmap_test.lua: Add test for BitmapFile structure

API change: bitmap(image) -> bitmap.create(image)
@Tsukina-7mochi Tsukina-7mochi merged commit 5d8b59a into develop Nov 14, 2025
6 checks passed
@Tsukina-7mochi Tsukina-7mochi deleted the claude/bitmap-018Xb3qDxXRKwYkBkECEW2Ge branch November 14, 2025 16:52
Tsukina-7mochi added a commit that referenced this pull request Feb 19, 2026
* chore: add style

* style: apply stylua

* chore: add lib to styluaignore

* feat: update type definition

* feat: add pre-init and post-run code to cleanup modules

* chore: update api definition

* chore: add claude configuration

* feat: add little-endian pack functions to string.pack module

Add u32LE, i32LE, and u16LE functions for little-endian binary encoding.
These functions complement the existing big-endian functions and are
needed for bitmap file format support.

- Add u32LE: 32-bit unsigned integer, little-endian
- Add i32LE: 32-bit signed integer, little-endian
- Add u16LE: 16-bit unsigned integer, little-endian
- Add comprehensive tests for all three functions

* chore: update submomule origin to HTTP

* feat: add bitmap (BMP) encoder for 24-bit RGB images (#35)

* feat: add bitmap (BMP) encoder for 24-bit RGB images

Add a library to export Aseprite RGB images as 24-bit BMP files. This
enables exporting images in the widely-supported bitmap format.

Features:
- Encodes Aseprite Image objects to BMP binary string
- Supports 24-bit RGB color mode only
- Proper BGR byte order conversion
- Bottom-to-top row ordering per BMP specification
- Automatic row padding to 4-byte boundaries
- Standard BMP file and info headers (14 + 40 bytes)

Implementation:
- src/pkg/bitmap/init.lua: Main BMP encoder with create() API
- src/pkg/bitmap/bitmap_test.lua: Comprehensive test suite
- src/pkg/asepriteUtil/mock.lua: Reusable Aseprite API mocks for testing

The bitmap encoder uses the little-endian pack functions added in PR #34
to generate proper BMP file structures.

* refactor: use metatable pattern for bitmap objects

Change bitmap.create() to return a table with metatable instead of
a plain string. This allows the bitmap object to be converted to a
string via tostring() or the __tostring metamethod.

Changes:
- Replace create() function with bitmap class using metatable
- Add tostring() method that generates BMP binary data
- Set __tostring metamethod for implicit string conversion
- Update all tests to use new API: bitmap(image)
- Add test for both explicit and implicit tostring conversion

Usage:
```lua
local bitmap = require("pkg.bitmap").bitmap
local bmp = bitmap(image)

-- Explicit conversion
local data = bmp:tostring()

-- Implicit conversion
local data = tostring(bmp)

-- Or use in string concatenation
local data = "" .. bmp
```

This pattern matches the RIFF chunk implementation and provides
more flexibility for future extensions.

* refactor: separate bitmap class into dedicated file

Move bitmap class definition from init.lua to bitmap.lua for better
code organization and separation of concerns.

Changes:
- Create src/pkg/bitmap/bitmap.lua with bitmap class and helper functions
- Update init.lua to just require and export the bitmap class
- No API changes - tests continue to work without modification

This makes the codebase more modular and easier to maintain.

* chore: remove unnecessary @module annotations

* chore: remove @module annotation from mock.lua

* refactor: use string.pack in mock pixelColor.rgba

* chore: set pixels per meter to 0 in BMP header

* refactor: restructure bitmap implementation

Major refactoring of bitmap module for cleaner architecture:

- mock.lua: Use bit operations instead of string.pack for rgba
- bitmap.lua: Use table.concat instead of string concatenation
- bitmap.lua: Rename class to BitmapFile and store components separately
  - fileHeader: BMP file header (14 bytes)
  - bitmapInfoHeader: Bitmap info header (40 bytes)
  - pixelData: Binary pixel data
- bitmap.lua: Export create function instead of __call metamethod
- init.lua: Simplify to directly return bitmap module
- bitmap_test.lua: Use hard-coded binary string literals instead of helpers
- bitmap_test.lua: Add test for BitmapFile structure

API change: bitmap(image) -> bitmap.create(image)

* refactor: remove empty init.lua

* refactor: change expression

---------

Co-authored-by: Claude <noreply@anthropic.com>

* build: refactor bootstrap code

* refactor: refactor bitmap pkg

* feat(bitmap-pkg): add factory to create image with alpha mask

* feat: add pre-init and post-run code to cleanup modules

* feat(icon-and-cursor): add parameter definition

* feat(icon-and-cursor): implement dialog

* feat: implement layer&frame extraction

* feat(icon-cursor): implement ico/cur export

* feat: update RIFF library

* feat: reimplement icon-cursor script

* feat(icon-cursor): implement cli

* feat(icon-cursor): persist dialog state and improve UX

Refactor export dialog to save/restore user settings via sprite
properties, allowing preferences to persist between exports. Replace
hardcoded option strings with structured option tables using
optionLabels/getOptionValue helpers. Wrap dialog in a validation loop
so users can fix errors without re-entering all fields.

* fix(icon-cursor): use descriptive labels for filetype dropdown options

Replace abbreviation-style labels (ICO, CUR, ANI) with human-readable
names (Icon, Cursor, Animated Cursor) in the export dialog filetype
selector for better clarity.

* fix(icon-cursor): refactor option lookup to return full entry and fix UI updates

Replace getOptionValue with getOptionEntry to return the full option object
instead of just the value string. Add isCursor and isAnimated flags to
FILETYPE_OPTIONS to derive visibility state from data rather than hardcoded
string comparisons. Fix filename update to properly modify the dialog widget
via dialog:modify. Add default values for hotspot and framerate params.

* fix(asepriteUtil): access frameNumber property from tag frame objects

The tag.fromFrame and tag.toFrame properties return Frame objects, not
numeric indices. Use .frameNumber to get the integer value needed for
iteration and array indexing.

* feat(icon-cursor): support single frame selection in export dialog

Add ability to select individual frames (not just tags) for export.
The tag parameter now accepts a frame number (integer) in addition to
a tag name (string) or nil (all frames). Frame options are listed in
the dialog alongside tag options. Also removes leftover debug print
statements and replaces util.alert with error() for consistency.

* fix(icon-cursor): zero out hotspot fields for ICO format

ICO files should always have hotSpotX and hotSpotY set to zero,
as hotspot coordinates are only meaningful for CUR (cursor) files.
Previously, user-specified hotspot values were written regardless
of file type.

* fix(bitmap): fix bitmap info header image size calculation

Replace manual row-size calculations with sizes derived from the
actual encoded byte strings. Parameterize createInfoHeader to accept
bitsPerPixel and imageSize, removing hardcoded values. This eliminates
redundant size arithmetic and ensures headers always match the real
payload, making the module easier to extend for other bit depths.

* feat(icon-cursor): add CLI argument parsing with proper type coercion for non-UI mode

Extract getParamsFromArgs to properly map CLI arguments (app.params) to
internal parameter format, applying type coercion for numeric fields
(hot-spot-x, hot-spot-y, framerate, tag) and initializing from sprite
defaults before overriding with provided arguments. This replaces the
previous approach of passing raw app.params directly, which skipped
default initialization and lacked type safety.

* feat(icon-cursor): bump version to v0.2.0

* chore: build

* feat(icon-cursor): add configurable completion dialog with showCompleted parameter

Add a "Show completion dialog" checkbox to the export dialog, allowing users
to suppress the post-export confirmation. Include field documentation, default
value, and validation for the new boolean parameter.

* fix(icon-cursor): fix operator precedence in dialog default value fallbacks

`tostring()` was being called before the `or` fallback, causing `or` to never trigger since `tostring()` always returns a non-nil string. Move the `or` inside `tostring()` so the fallback applies to the raw value before conversion.

* chore: build

* docs(icon-cursor): update readme

Document new version highlights including dialog validator fix,
reduced icon file size, and CLI support. Move license notice out
of readme.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants