From fc46251027e2c65bff081cc04939ff13015501aa Mon Sep 17 00:00:00 2001 From: Nick Bowler Date: Mon, 16 Dec 2013 23:49:30 -0500 Subject: [PATCH] lbximg: Fix wart in frame scanline header format The format of frame scanlines has been a little weird since day 1: We had 2 "types" of scanline headers disambiguated by some non-trivial logic, including whether or not it was the first decoded scanline in the frame. Here's a new, simpler approach, which has only one type of scanline header, consistent meanings to the header fields, and by interpreting the first 4 bytes as a "frame header" we get rid of the special case for the first scanline, too. Add a document in English which describes the LBX image file format, to be used as the baseline format specification going forward. Finally, update the contrived test cases for the new format. All the test cases using actual MOO2 data still pass. --- doc/txt/image-format.txt | 154 ++++++++++++++++++++++++++++++++++++ src/image.c | 89 +++++++++++---------- tests/testdata/image-1x1 | Bin 34 -> 34 bytes tests/testdata/image-1x1-mt | Bin 24 -> 28 bytes 4 files changed, 200 insertions(+), 43 deletions(-) create mode 100644 doc/txt/image-format.txt diff --git a/doc/txt/image-format.txt b/doc/txt/image-format.txt new file mode 100644 index 0000000..0a558dd --- /dev/null +++ b/doc/txt/image-format.txt @@ -0,0 +1,154 @@ +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. diff --git a/src/image.c b/src/image.c index f68f2da..764f2be 100644 --- a/src/image.c +++ b/src/image.c @@ -196,9 +196,9 @@ struct lbx_image *lbx_img_fopen(const char *file) 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; @@ -208,63 +208,47 @@ static int _lbx_drawrow(int first, struct lbx_image_priv *img) 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; } @@ -338,6 +322,7 @@ static unsigned char **read_raw_frame(struct lbx_image_priv *img, int frame) 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); @@ -380,17 +365,35 @@ unsigned char **lbx_img_getframe(struct lbx_image *pub, int frame) } 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); diff --git a/tests/testdata/image-1x1 b/tests/testdata/image-1x1 index 0615d1fa72b9a25b07e1f056ede221b19af2b2b4..80ccbb789afc1a17adafd58f043728807bb39d6d 100644 GIT binary patch delta 19 XcmY#Vnjpf*$iTn=!cI{P3@?}g71#oR delta 19 XcmY#Vnjpf*00fLc>=ebo@PZit70LpC diff --git a/tests/testdata/image-1x1-mt b/tests/testdata/image-1x1-mt index fdf87cae374f25e0152e124e65fa30e5902b3ede..c61d7789cff251a5d2ae50cd05907279b7954311 100644 GIT binary patch literal 28 XcmZQ%U}RtbV+IBhAWH_$d%+9<1k(W^ literal 24 ZcmZQ%U}RtbV+IBhAWH&>85mwL0{{ct0UH1S -- 2.43.2