]> git.draconx.ca Git - liblbx.git/commitdiff
lbximg: Fix wart in frame scanline header format
authorNick Bowler <nbowler@draconx.ca>
Tue, 17 Dec 2013 04:49:30 +0000 (23:49 -0500)
committerNick Bowler <nbowler@draconx.ca>
Tue, 17 Dec 2013 04:57:19 +0000 (23:57 -0500)
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 [new file with mode: 0644]
src/image.c
tests/testdata/image-1x1
tests/testdata/image-1x1-mt

diff --git a/doc/txt/image-format.txt b/doc/txt/image-format.txt
new file mode 100644 (file)
index 0000000..0a558dd
--- /dev/null
@@ -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.
index f68f2da9f9ac4b1573e9d16ed53279ac9a236043..764f2bedfd1720ea3ac4496232547968112b9370 100644 (file)
@@ -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);
index 0615d1fa72b9a25b07e1f056ede221b19af2b2b4..80ccbb789afc1a17adafd58f043728807bb39d6d 100644 (file)
Binary files a/tests/testdata/image-1x1 and b/tests/testdata/image-1x1 differ
index fdf87cae374f25e0152e124e65fa30e5902b3ede..c61d7789cff251a5d2ae50cd05907279b7954311 100644 (file)
Binary files a/tests/testdata/image-1x1-mt and b/tests/testdata/image-1x1-mt differ