Format Overview
JAZ files reduce texture size by:- Color: Stored as lossy JPEG (small, good compression)
- Alpha: Stored as RLE-compressed bytes (lossless, efficient for large transparent areas)
- Wrapper: Zlib compression of the entire payload
Binary Layout
Compression Method
Field:method (1 byte at offset 0x00)Value:
0x01 = zlib compressionNote: Only method
1 is used in Crimsonland assets. Other values are unsupported.
Payload Sizes
comp_size (u32 at offset 0x01):
- Length of the zlib-compressed stream
- Used to read the exact number of bytes for decompression
u32 at offset 0x05):
- Expected size after zlib decompression
- Validation:
len(decompressed) == raw_size
JPEG Color Data
The decompressed payload starts with a 4-byte length field, followed by a standard JPEG stream: jpeg_len: Number of bytes in the JPEG imagejpeg_data: Complete JPEG file (with SOI marker
0xffd8 and EOI marker 0xffd9)
The JPEG contains RGB color only (no alpha). Dimensions (width × height) are embedded in the JPEG metadata.
Alpha RLE Format
After the JPEG data, the remainder of the payload is alpha channel data encoded as run-length pairs:Decoding Algorithm
width × height (from JPEG dimensions)
Padding
Most assets expand to exactlywidth × height bytes. One known exception: One file is short by 1 pixel.
Solution: Pad with 0x00 (transparent) if RLE produces fewer bytes than expected.
Python Decoder
The reference decoder is insrc/grim/jaz.py:
Usage
Decode to PIL Image:Example File
Hex dump ofsmall_sprite.jaz:
- Method:
0x01(zlib) ✓ - Decompress 4003 bytes → 4040 bytes
- JPEG length: 3796 bytes
- Alpha RLE:
4040 - 4 - 3796 = 240bytes - Decode JPEG → 64×64 RGB image
- Decode RLE → 4096 alpha bytes (64×64)
- Combine → 64×64 RGBA image
Known JAZ Files
Commonly found incrimson.paq:
Creature Sprites
Creature Sprites
game/bodies0.jazthroughgame/bodies7.jaz- Each atlas contains multiple creature sprites
- Dimensions: 512×512 or 1024×512
Blood and Gore
Blood and Gore
game/blood0.jaz,game/blood1.jaz- Splatter and corpse decals
- Dimensions: 512×512
Terrain
Terrain
game/terrain.jaz- Background tiles
- Dimensions: 256×256
UI Elements
UI Elements
ui/panel_*.jaz- Menu backgrounds with transparency
- Dimensions: Vary (256×256 to 512×512)
Why JAZ?
Advantages
- Small color data: JPEG is compact for photos/textures
- Lossless alpha: RLE preserves sharp transparency edges
- Fast decode: JPEG decoding is hardware-accelerated
- Good for sprites: Large transparent areas compress well with RLE
Disadvantages
- JPEG artifacts: Color data has compression artifacts
- No PNG features: No indexed color, no metadata
- Custom format: No tool support (requires custom decoder)
Comparison with Modern Formats
| Feature | JAZ | PNG | WebP |
|---|---|---|---|
| Color compression | JPEG (lossy) | DEFLATE (lossless) | VP8 (lossy/lossless) |
| Alpha compression | RLE | DEFLATE | VP8L |
| File size | Small | Medium | Very small |
| Quality | Lossy color | Lossless | Configurable |
| Tool support | None | Universal | Growing |
| Decode speed | Fast | Medium | Fast |
Edge Cases
Short Alpha Data
One asset (game/blood1.jaz entry 5) produces 4095 alpha bytes instead of 4096:
Method Field
Only method1 (zlib) is used. Other values should raise an error:
Related Formats
PAQ Archives
Container format for JAZ and other assets
Sprite Atlas
How sprites are cut from decoded JAZ textures
Implementation Notes
- Decoder uses PIL (Pillow) for JPEG parsing
- Alpha RLE is custom (not a standard RLE variant)
- The extractor automatically converts JAZ → PNG with alpha
- Original engine uses embedded libjpeg and libpng
References
- Source:
src/grim/jaz.py - Tests:
tests/test_jaz.py - CLI:
src/crimson/cli.py(extractcommand)