--- /dev/null
+LBX Images:
+
+This document describes the LBX image format used in Moo2. The goal is that
+the code to decode images in liblbx can be written against this specification,
+thus guiding the implementation.
+
+All multi-byte integer types are stored in the image file from
+least-significant to most-significant byte. This document uses the names
+"uint8", "uint16" and "uint32" to refer to 8-bit, 16-bit and 32-bit unsigned
+integers, respectively.
+
+LBX Images begin with a 12-byte header which has the following layout:
+
+ OFFSET TYPE DESCRIPTION
+ -----------------------------------------------------
+ 0 uint16 Image width in pixels
+ 2 uint16 Image height in pixels
+ 4 uint16 Unknown (always 0?)
+ 6 uint8 Number of frames
+ 7 uint8 Unknown (always 0?)
+ 8 uint8 Lead-in (less than the frame count)
+ 9 uint8 Chunk size
+ 10 uint16 Flags
+
+Immediately following the header is an sequence of uint32 offsets (one for
+each frame) indicating the start of each frame, followed by a final uint32
+offset which marks the end of the file.
+
+The frame count, lead-in and chunk size describe how the image is animated,
+which is described later. The flags value is the bitwise-OR of zero or more
+of the following values:
+
+ VALUE NAME DESCRIPTION
+ ----------------------------------------------------------------------------
+ 0x0100 raw If set, this flag enables a simpler method of storing
+ frame data in the image.
+ 0x0400 overwrite If set, behave as if the chunk size was 1 instead of its
+ actual value.
+ 0x0800 building Unknown. This flag is set on the building images
+ (perhaps to enable a special blending mode for shadows).
+ 0x1000 palette If set, the image contains embedded palette data.
+ 0x2000 loop If set, behave as if the lead-in was 0 instead of its
+ actual value.
+
+If the palette flag is set, the palette data follows the frame offsets. The
+palette has a 4-byte header:
+
+ OFFSET TYPE DESCRIPTION
+ -----------------------------------------------------
+ 0 uint16 Index of the first embedded palette entry
+ 2 uint16 Number of embedded palette entries.
+
+The sum of these two values must not exceed 256. Immediately following the
+palette header are the palette entries. Each palette entry is 4 bytes with
+the following layout:
+
+ OFFSET TYPE DESCRIPTION
+ -----------------------------------------------------
+ 0 uint8 Always 1.
+ 1 uint8 Red component (0-63)
+ 2 uint8 Green component (0-63)
+ 3 uint8 Blue component (0-63)
+
+Note that component values are stored as an 8-bit integer but only range
+from 0-63 (6 bits per channel), with (0, 0, 0) being the darkest black and
+(63, 63, 63) the brightest white. The values in the image's embedded palette
+supersede the values in the "main" palette.
+
+Image data:
+
+Each LBX image consists of one or more frames. The start of each frame
+can be found by seeking to the appropriate offset in the file, as described
+above.
+
+There are two main methods of encoding frame data. The simplest is the raw
+encoding, which is used when the "raw" flag (0x0100) is set in the header.
+In raw encoding, the pixel data for the entire frame is stored verbatim in
+row-major order (one uint8 value per pixel). There are no headers to parse
+within the frame, and this format does not support transparency as each frame
+specifies a palette index for every pixel in the image.
+
+For example, a 3x2 raw image has exactly 6 bytes per frame, with the pixels
+laid out as in the following format. The numbers represent byte offsets
+within the file:
+
+ +---+---+---+
+ | 0 | 1 | 2 |
+ +---+---+---+
+ | 3 | 4 | 5 |
+ +---+---+---+
+
+The more common method is a line-based encoding, which is more flexible.
+In this format, a cursor is maintained to track where pixel data is to be
+written. The X value of the cursor specifies the number of pixels right
+from the left edge of the image, and the Y value of the cursor specifies
+the number of pixels down from the top edge of the image.
+
+Each frame in this encoding begins with a header.
+
+ OFFSET TYPE DESCRIPTION
+ -----------------------------------------------------
+ 0 uint16 Always 1.
+ 2 uint16 Initial Y position of cursor.
+
+The cursor has an initial X value of 0. Immediately following the header
+is a sequence of one or more drawing commands. Each command consists of a
+2-byte header.
+
+ OFFSET TYPE DESCRIPTION
+ -----------------------------------------------------
+ 0 uint16 Length: number of pixels to follow
+ 2 uint16 Cursor offset
+
+If length is non-zero, first the offset is added to the current X value of the
+cursor, then the pixel data (1 byte per pixel) follows and is drawn starting
+at the current cursor position, with successive pixels increasing in X value.
+The cursor is updated as pixels are drawn. In general, new pixel data
+supersedes any previous value for a particular pixel. Pixels that have no
+data specified are transparent. Animated images may re-use pixel values from
+the previous frame, see below for details.
+
+If length is odd, there is a padding byte after the last pixel which must be
+skipped.
+
+If length is 0, then the offset is added to the Y value of the cursor, the X
+value of the cursor is reset to 0, and no pixel data follows.
+
+Notwithstanding the above, if both length is 0 and offset is exactly 1000,
+then there are no further drawing commands in this frame.
+
+Animation:
+
+There are 3 parameters relevant to animations found in the image header: the
+frame count, the lead-in, and the chunk size. Frame count specifies the total
+number of frames in the image, chunk size affects how frames are decoded and
+the lead-in affects how animations are displayed.
+
+Normally, each decoded frame is drawn on top of the previous frame, with the
+first frame drawn on a fully transparent slate. So if the first frame has any
+unspecified pixel values, those pixels are transparent, while if the second
+frame has any unspecified pixel values, those pixels retain the value from the
+first frame (transparent or otherwise).
+
+However, if the chunk size is set to a non-zero value, then the slate is reset
+to a fully transparent state when decoding a frame number which is divisible
+by the chunk size. So if this is set to 1, every frame must specify all
+non-transparent pixels as they will each be drawn on top of a transparent
+slate (just like the first frame). This parameter is the only way for pixels
+which are opaque in one frame to become transparent in the next.
+
+The lead-in specifies the frame to be displayed after the last frame in an
+animation. If this is different from the last frame, then the result is a
+looping animation. If this is the same as the last frame, then the result is
+an animation that stops at the end.
return lbx_img_open(p, &lbx_pipe_fops, pipe_close);
}
-static int _lbx_drawrow(int first, struct lbx_image_priv *img)
+static int _lbx_drawrow(struct lbx_image_priv *img)
{
- unsigned short type, count, yval, xval;
+ unsigned short length, offset;
unsigned char buf[4];
unsigned char *pos;
size_t rc;
if (img->fops->read(buf, sizeof buf, img->f) != sizeof buf)
goto readerr;
- type = unpack_16_le(buf+0);
- if (first) {
- img->currentx = 0;
- img->currenty = 0;
- type = 0;
- }
-
- if (type == 0) {
- yval = unpack_16_le(buf+2);
- if (yval == 1000)
- return 1;
-
- if (img->fops->read(buf, sizeof buf, img->f) != sizeof buf)
- goto readerr;
- count = unpack_16_le(buf+0);
-
- xval = unpack_16_le(buf+2);
- if (xval == 1000)
- return 1;
+ length = unpack_16_le(buf+0);
+ offset = unpack_16_le(buf+2);
+ if (length == 0 && offset == 1000)
+ return 1;
- /* Ensure that the row fits in the image. */
- if (img->pub.height - img->currenty <= yval
- || xval >= img->pub.width) {
+ /* Length of 0 increments Y position */
+ if (!length) {
+ if (offset > img->pub.height - img->currenty) {
lbx_error_raise(LBX_EFORMAT);
return -1;
}
- img->currenty += yval;
- img->currentx = xval;
- } else {
- xval = unpack_16_le(buf+2);
-
- if (img->pub.width - img->currentx <= xval) {
- lbx_error_raise(LBX_EFORMAT);
- return -1;
- }
- img->currentx += xval;
+ img->currenty += offset;
+ img->currentx = 0;
+ return 0;
+ }
- count = type;
+ /* Otherwise we read pixel data */
+ if (offset > img->pub.width - img->currentx) {
+ lbx_error_raise(LBX_EFORMAT);
+ return -1;
}
+ img->currentx += offset;
- if (count > img->pub.width - img->currentx) {
+ if (length > img->pub.width - img->currentx) {
lbx_error_raise(LBX_EFORMAT);
return -1;
}
- memset(&img->mask[img->currenty][img->currentx], 1, count);
+ memset(&img->mask[img->currenty][img->currentx], 1, length);
pos = &img->framedata[img->currenty][img->currentx];
- rc = img->fops->read(pos, count, img->f);
+ rc = img->fops->read(pos, length, img->f);
img->currentx += rc;
- if (rc < count)
+ if (rc < length)
goto readerr;
- if (count % 2) {
+ /* Skip padding byte, if any */
+ if (length % 2) {
if (img->fops->read(buf, 1, img->f) != 1)
goto readerr;
}
unsigned char **lbx_img_getframe(struct lbx_image *pub, int frame)
{
struct lbx_image_priv *img = (struct lbx_image_priv *)pub;
+ unsigned char buf[4];
if (frame >= pub->frames || frame < 0) {
lbx_error_raise(LBX_ENOENT);
}
if (img->currentframe != frame) {
- int rc, first = 1;
+ int rc;
if (img->fops->seek(img->f, img->offsets[frame], SEEK_SET)) {
return NULL;
}
+ /* Read frame header */
+ if (img->fops->read(buf, 4, img->f) != 4) {
+ if (img->fops->eof(img->f))
+ lbx_error_raise(LBX_EEOF);
+ return NULL;
+ }
+
+ if (unpack_16_le(buf) != 1) {
+ lbx_error_raise(LBX_EFORMAT);
+ return NULL;
+ }
+
+ img->currentx = 0;
+ img->currenty = unpack_16_le(buf+2);
+ if (img->currenty > img->pub.height) {
+ lbx_error_raise(LBX_EFORMAT);
+ return NULL;
+ }
+
do {
- rc = _lbx_drawrow(first, img);
+ rc = _lbx_drawrow(img);
if (rc == -1)
return NULL;
- first = 0;
if (img->fops->tell(img->f) > img->offsets[frame+1]) {
lbx_error_raise(LBX_EFORMAT);