1

I've managed to extract some graphics files from an archive of an old game. Haven't found anything about it online, so I'm trying my luck here, prehaps someone knows a similar format that can help me. It's a somehow encrypted/compressed graphics format that uses an external palette. I've managed to display a different graphics format from the game, .raw files, that were very similar to NetPBM files. This format however isn't as straight forward.

Here's what I know:

File extension: .cgf
File magic: CGFF
file: data
binwalk -E: 0.5 or 0.75, depends on "compression type", pretty much uniform across file
binwalk -X: DEFLATE streams, sometimes only 1 bit long (?)

Header structure is as follows. Names are an educated guess on what the field could be. Every field is 4 bytes long, numbers are signed ints stored in little endian.
Each line is 4 bytes in the file:

String CGFF (file magic)
Compression type, either "1" or "9" across all files
Number of layers
Number of layers, multiplied by 24
File size. If my guess is right, this is probably an unsigned int
"0" across all files but one, where it is "250"
"0" across all files
X Position                                      |
Y Position                                      |
Width                                           |
Height                                          |
"38" across all files                           |
Offset into the file starting after the headers |

The last 6 entries seem to make up a unit that repeats once for every layer.

Here's how I got to my guesses:

  • Compression type: 1-files have a different entropy and structure than 9-files.
  • Number of layers: Observarions like this: A file called "Ballammo" is loaded in a game where you have 10 snowballs to throw at targets. The header contains a "10" in this field. This works for other files too.
  • Filesize: I had to guess this since the archive I extracted the files from stored incorrect file lengths, but in most cases the numbers are similar enough
  • Position/Size: Files called "Background" always have the values 0 0 640 480. In some cases they were 640 480 -640 -480, hence my guess with the position. The size makes sense for a game of that time running in 4:3
  • Layer offset: Guess, since the first entry is always 0 and the subsequent are bigger numbers. Also it lines up nicely like this.

Regarding the file structure, 1-files have higher entropy and don't really show any pattern. In 9-files however, most of the time a byte is followed by 0xff, rarely by something else. It's not RLE, I've tried that.

As requested, here's two hexdumps. One 9-compressed, one 1-compressed.
https://pastebin.com/NGY79UgU

Oh right, I should say how these are supposed to look like. "Cursor.cgf" should contain a two hand-like cursors, one pointing and one grabbing. Regarding "Kid.cgf", the data says it contains 8 layers. Given the minigame the file loads in, this should contain 4 grinning mouths and 4 pairs of eyes.

I also forgot to mention that both images have transparency, this could however be implemented using the palette the game loads.

NOTE: I can't guarantee that these files are complete. They may contain garbage at the end or be incomplete. As I said, chances are good that the archive's stored file lengths are wrong, as I found a palette that contained the .wav header of the next file after extraction.

Here's how some of the layers of the files given should look like when correctly loaded. Three "Kid.cgf"-layers are lined in red, while one of the "Cursor.cgf"-layers is lined in green. Boxes are not exact.
enter image description here

UPDATE

I managed to point the game to an extracted file instead of an archived file and it loads just fine. After also writing a program that fully parses the header, extracting the layer data into seperate files and some minor experiments, here's my results on a type 1 image:

  • The game doesn't mind files being too long.
  • The data isn't just a bitmap. Assuming I correctly found the fields for width, height and layer offset, the lengths of the fields (= diff between two layer offsets) is always smaller than width*height. Also, randomly changing some bytes in the image section crashes that game on load.
  • The x and y pos isn't relative to the screen but to something else. This was revealed by swapping two images; both were still drawn in the correct location.
  • @pythonpython found the value 0x26 to be interesting. This value also is stored in the layer entries (I wrote '"38" in all files', see above).
  • The image data contains info on where a "pixel line" ends. Example: Let a layer be 32x64. Swapping the header values for the image to be 64x32 results in an image that is cut off in the middle but gets displayed correctly.

I'll keep experimenting and update accordingly.

2
2

The images are packed, data is little-endian where it matters.

General format:

struct CGFHeader {
  uint32_t magic;
  uint32_t flags;
  uint32_t frame_count;
  uint32_t frame_metadata_size;
  uint32_t frame_payload_size;
  uint32_t unk1;
  uint32_t unk2; 
};

Then repeated frame_count times, starting at +0x1c:

struct FrameMeta {
  uint32_t unk1;
  uint32_t unk2;
  uint32_t width;
  uint32_t height;
  uint32_t unk3;
  uint32_t payload_offset;
};

For frame N, the payload data starts at sizeof(CGFHeader) + cgf_header.frame_count*sizeof(FrameMeta) + frame_meta[N].payload_offset (i.e. at the corresponding payload_offset, based immediately after the metadata structs).

Each line/row of the packed image data is packed independently. Each line is prefixed by a uint32_t of that line's length (including the length field). Process the packed data as follows:

Read a method uint8_t and a length uint8_t (referred to n below), and process per the below table.

Method How to process
0x00 Append n transparent pixels. If n is 0, pad the line with transparent pixels to expected width.
0x01 Read n pairs of (palette_index, alpha) values from the packed data, appending to the line.
0x02 Read a single (palette_index, alpha) pair from the packed data. Append it to the line n times.
0x03 Read n palette_index values from the packed data, appending to the line.
0x04 Read a single palette_index from the packed data. Append it to the line n times.

Keep processing method uint8_t and a length uint8_t bytes from the packed data until you have the full line width.

In order to interpret the actual color values, you'll need the corresponding palette. The two formats you've mentioned:

  • CPAL254X3STD just has (in this case) 254 RGB triplets appended after that header value.
  • CPAL254X3ALPHA is the same, but with a 0x100000 byte structure appended that gets referred to as an "alpha map". I have not bothered to look at it at all.

Rough python3 example of processing, dumping frames to 0.png, 1.png, etc in subdirectory:

import errno
import os
import struct
import sys

from PIL import Image

class HugoPalette(object):
  def __init__(self, rawdat):
    assert(len(rawdat) >= 12)
    assert(rawdat[0:4] == b'CPAL') # palette
    self.alpha_map = None
    self.entries = None
    num_entries = int(rawdat[4:7])
    assert(rawdat[7:9] == b'X3') # rgb triples
    if rawdat[9:12] == b'STD': # palette
      assert(len(rawdat) >= 12 + num_entries*3)
      self.entries = []
      offset = 12
      for n in range(num_entries):
        self.entries.append(struct.unpack_from("BBB" ,rawdat, offset))
        offset = offset + 3
    elif rawdat[9:14] == b'ALPHA': # palette and alphamap
      assert(len(rawdat) >= 14 + num_entries*3 + 0x100000)
      self.entries = []
      offset = 14
      for n in range(num_entries):
        self.entries.append(struct.unpack_from("BBB" ,rawdat, offset))
        offset = offset + 3
      self.alphamap = rawdat[14 + num_entries*3:14 + num_entries*3 + 0x100000] # not sure how to interpret
    else:
      raise NotImplementedError("unknown palette type")


class HugoImage(object):
  def __init__(self, width, height, rawdat, offset):
    self.width = width
    self.height = height

    rows = []
    for n in range(height):
      (packed_line_length,) =  struct.unpack_from("<L", rawdat, offset)
      assert(len(rawdat) >= offset + packed_line_length)
      packed_line = rawdat[offset+4:offset+packed_line_length]
      line = []
      index = 0
      # unpacking:
      # 00 nn                   = skip nn pixels [nn=00: skip to end of line]
      # 01 nn pp aa [pp aa ...] = insert nn entries from trailing pp, replacing alpha with aa
      # 02 nn pp aa             = repeat pp for nn pixels, replacing alpha with aa
      # 03 nn pp [pp ...]       = insert nn entries from trailing pp
      # 04 nn pp                = repeat pp for nn pixels
      while True:
        method = packed_line[index]
        pixel_count = packed_line[index+1]
        index = index + 2
        if method == 0:
          if pixel_count == 0:
            while(len(line) < self.width):
              line.append((0, 0))
            break
          line.extend([(0,0)]*pixel_count)
        elif method == 1:
          for p in range(pixel_count):
            line.append((packed_line[index], packed_line[index+1]))
            index = index + 2
        elif method == 2:
          line.extend([(packed_line[index], packed_line[index+1])]*pixel_count)
          index = index + 2
        elif method == 3:
          for p in range(pixel_count):
            line.append((packed_line[index],0xff))
            index = index + 1
        elif packed_line[index] == 4:
          line.extend([(packed_line[index], 0xff)]*pixel_count)
          index = index + 1
      assert(len(line) == self.width)
      rows.append(line)
      offset = offset + 4 + index
    self.rows = rows

def load_images(rawdat):
  HEADER_STRUCT_SIZE=0x1c
  METADATA_STRUCT_SIZE=0x18
  offset = 0
  assert(len(rawdat) >= offset + HEADER_STRUCT_SIZE)
  (magic, _, count, metadata_size, payload_size, _, _) = struct.unpack_from("<LLLLLLL", rawdat, offset)
  offset = offset + HEADER_STRUCT_SIZE
  assert(magic == 0x46464743)
  assert(metadata_size == count * METADATA_STRUCT_SIZE)
  metadata = []
  for n in range(count):
    assert(len(rawdat) >= offset + METADATA_STRUCT_SIZE)
    metadata.append(struct.unpack_from("<LLLLLL", rawdat, offset))
    offset = offset + METADATA_STRUCT_SIZE
  images = []
  for im in metadata:
    images.append(HugoImage(im[2], im[3], rawdat, offset + im[5]))
  return images


def main(args):
  if len(args) != 3:
    print(f"usage: python3 {args[0]} palette.pal image.cgf")
    sys.exit(1)
  with open(args[1], "rb") as infile:
    dat = infile.read()
  pal = HugoPalette(dat)

  with open(args[2], "rb") as infile:
    dat = infile.read()
  images = load_images(dat)

  output_dir = args[2] + ".extracted"
  output_index = 0

  try:
    os.makedirs(output_dir)
  except OSError as e:
    if e.errno != errno.EEXIST:
      raise

  for i in images:
    img = Image.new('RGBA', (i.width, i.height))
    for y in range(i.height):
      for x in range(i.width):
        (index, alpha) = i.rows[y][x]
        pal_entry = pal.entries[index]
        col = (pal_entry[0], pal_entry[1],pal_entry[2], alpha)
        img.putpixel((x,y), col)
    img.save(os.path.join(output_dir, str(output_index) + ".png"), "PNG")
    output_index = output_index + 1

if __name__ == '__main__':
  main(sys.argv)
2
  • How did you figure this out? Incredible work, thanks a lot! – mindoverflow Jan 5 at 7:41
  • Finally found the time to implement it and test it with more files. Works great for most files, the one that fail are probably incomplete which is a fault on my part. Interestingly enough, the CPAL254X3ALPHA palettes work the same way as the ...X3STD palettes, I should be able to figure the details out myself. Thanks again! – mindoverflow Jan 8 at 21:30
0

Some interesting structure if you break the files by value 0x26.

How did you derive the number of layers?

I don't think these are compressed. There's lots of redundancy in the data which compression would destroy. You also wouldn't see the same values in the compressed result of two different inputs.

What is the game system?

What are the pixel sizes of the displayed kid.cgf and cursor.cgf on the device?

Analysis of hex.

6
  • I appreciate you taking the time to tend to the question, but your "answer" contains almost more questions than the real question here. However, I understand that it is impossible to put a screenshot into a comment and there is a limit to what can be done in comments. So I am converting your non-answer into a community wiki. Use that - for now - to figure out the details about the subject matter and write up an alternative answer once you are ready to answer. – 0xC0000022L Dec 21 '20 at 9:36
  • Thanks for the effort! I don't know what you mean by "game system", also getting the pixel size of these particular images is difficult, but I can try. I've found "Background.cgf" files with the values 640 and 480 stored as singed little endian ints (4 bytes) at offsets 32/0x20 and 36/0x24 respectively, so I assume these fields contain the image's size. – mindoverflow Dec 21 '20 at 12:58
  • About the layers: Observation from ingame sprites. I've written about it in the question, but here's another example: The same minigame loads "Present.cgf", containing a value of 8. There are 8 different present sprites in that minigame. All "Cursor.cgf" files have a value of 2: One for the default image and one used when hovering above something clickable. "Keycursor.cgf" has a value of 3. There are 3 unlockable keys. – mindoverflow Dec 21 '20 at 13:14
  • The cursor's about 32x23 px, matching the fields in cursor.cgf. No luck with "Kid.cgf" though – mindoverflow Dec 21 '20 at 13:19
  • Do these sprites have transparency? the 0x26000000 values made me think it's the Alpha set to 15% part of an ARGB32 pixel. – pythonpython Dec 21 '20 at 15:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.