Task 691: .STI File Format

Task 691: .STI File Format

.STI File Format Specifications

The .STI file format is the STCI (Sir-Tech's Crazy Image) format used in the game Jagged Alliance 2 for storing graphical objects. It supports single or multiple images in either 16-bit RGB565 or 8-bit indexed color formats. 16-bit files contain one uncompressed image, while 8-bit files can contain multiple images, optionally compressed with ETRLE (a run-length encoding variant) or ZLIB (though ETRLE is common and ZLIB is not observed in practice). The format includes a header, optional palette, subimage headers for multi-image files, image data, and optional application data for animated files.

List of all the properties of this file format intrinsic to its file system:

  • Format Identifier: 4-byte string "STCI"
  • Original Size: 4-byte unsigned integer (initial image size in bytes; often arbitrary for multi-image files)
  • Compressed Size: 4-byte unsigned integer (size of compressed image data in bytes)
  • Transparent Index: 4-byte unsigned integer (index of transparent color in palette; always 0 for 8-bit files)
  • Flags: 4-byte unsigned integer (bit flags: bit 3 for RGB/16-bit, bit 4 for indexed/8-bit, bit 5 for ZLIB compression, bit 6 for ETRLE compression; typically 4 for 16-bit, 40 for 8-bit non-animated, 41 for 8-bit animated)
  • Height: 2-byte unsigned integer (image height in pixels; used in 16-bit files, or per subimage in 8-bit)
  • Width: 2-byte unsigned integer (image width in pixels; used in 16-bit files, or per subimage in 8-bit)
  • Color Format Details:
  • For 16-bit: Red mask (4 bytes), Green mask (4 bytes), Blue mask (4 bytes), Alpha mask (4 bytes), Red depth (1 byte), Green depth (1 byte), Blue depth (1 byte), Alpha depth (1 byte)
  • For 8-bit: Number of colors (4 bytes, typically 256), Number of subimages (2 bytes), Red depth (1 byte, typically 8), Green depth (1 byte, typically 8), Blue depth (1 byte, typically 8)
  • Color Depth: 1-byte unsigned integer (bits per pixel: 8 or 16)
  • App Data Size: 4-byte unsigned integer (size of application data; typically number of subimages * 16 for animated files)
  • Reserved: 16 bytes (unused)
  • Palette: 768 bytes (256 * 3 bytes RGB; present only in 8-bit files)
  • Subimage Headers: Variable (number of subimages * 16 bytes each; present in 8-bit files):
  • Data Offset: 4-byte unsigned integer (offset to image data from start of all image data)
  • Data Size: 4-byte unsigned integer (size of image data in bytes)
  • X Offset: 2-byte unsigned integer (horizontal shift in pixels)
  • Y Offset: 2-byte unsigned integer (vertical shift in pixels)
  • Subimage Height: 2-byte unsigned integer
  • Subimage Width: 2-byte unsigned integer
  • Image Data: Variable (raw pixels for 16-bit; indexed and compressed for 8-bit using ETRLE or ZLIB)
  • Application Data: Variable (for animated 8-bit files; number of subimages * 16 bytes; contains frame counts and markers for animation directions)

Two direct download links for files of format .STI:

Ghost blog embedded HTML JavaScript for drag and drop .STI file dump:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>STI File Dumper</title>
  <style>
    #dropzone {
      width: 400px;
      height: 200px;
      border: 2px dashed #ccc;
      text-align: center;
      line-height: 200px;
    }
    #output {
      margin-top: 20px;
      white-space: pre-wrap;
    }
  </style>
</head>
<body>
  <div id="dropzone">Drag and drop .STI file here</div>
  <div id="output"></div>
  <script>
    const dropzone = document.getElementById('dropzone');
    const output = document.getElementById('output');

    dropzone.addEventListener('dragover', e => {
      e.preventDefault();
      dropzone.style.borderColor = '#000';
    });

    dropzone.addEventListener('dragleave', e => {
      dropzone.style.borderColor = '#ccc';
    });

    dropzone.addEventListener('drop', e => {
      e.preventDefault();
      dropzone.style.borderColor = '#ccc';
      const file = e.dataTransfer.files[0];
      if (file.name.endsWith('.sti')) {
        const reader = new FileReader();
        reader.onload = () => {
          const arrayBuffer = reader.result;
          const view = new DataView(arrayBuffer);
          let props = 'STI File Properties:\n';

          // Header
          props += `Format Identifier: ${String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3))}\n`;
          props += `Original Size: ${view.getUint32(4, true)}\n`;
          props += `Compressed Size: ${view.getUint32(8, true)}\n`;
          props += `Transparent Index: ${view.getUint32(12, true)}\n`;
          const flags = view.getUint32(16, true);
          props += `Flags: ${flags} (0x${flags.toString(16)})\n`;
          props += `Height: ${view.getUint16(20, true)}\n`;
          props += `Width: ${view.getUint16(22, true)}\n`;
          const is16bit = (flags & 0x08) !== 0;
          const is8bit = (flags & 0x10) !== 0;
          if (is16bit) {
            props += `Red Mask: ${view.getUint32(24, true)}\n`;
            props += `Green Mask: ${view.getUint32(28, true)}\n`;
            props += `Blue Mask: ${view.getUint32(32, true)}\n`;
            props += `Alpha Mask: ${view.getUint32(36, true)}\n`;
            props += `Red Depth: ${view.getUint8(40)}\n`;
            props += `Green Depth: ${view.getUint8(41)}\n`;
            props += `Blue Depth: ${view.getUint8(42)}\n`;
            props += `Alpha Depth: ${view.getUint8(43)}\n`;
          } else if (is8bit) {
            props += `Number of Colors: ${view.getUint32(24, true)}\n`;
            props += `Number of Subimages: ${view.getUint16(28, true)}\n`;
            props += `Red Depth: ${view.getUint8(30)}\n`;
            props += `Green Depth: ${view.getUint8(31)}\n`;
            props += `Blue Depth: ${view.getUint8(32)}\n`;
          }
          props += `Color Depth: ${view.getUint8(44)}\n`;
          props += `App Data Size: ${view.getUint32(45, true)}\n`;

          // Palette for 8-bit
          if (is8bit) {
            props += 'Palette: (first 5 entries for brevity)\n';
            for (let i = 0; i < 5; i++) {
              const offset = 64 + i * 3;
              props += `Color ${i}: R=${view.getUint8(offset)} G=${view.getUint8(offset+1)} B=${view.getUint8(offset+2)}\n`;
            }
          }

          // Subimage Headers for 8-bit
          if (is8bit) {
            const numSubimages = view.getUint16(28, true);
            const subHeaderStart = 64 + (is8bit ? 768 : 0);
            props += `Subimage Headers (${numSubimages}):\n`;
            for (let i = 0; i < numSubimages; i++) {
              const offset = subHeaderStart + i * 16;
              props += `Subimage ${i}:\n`;
              props += `  Data Offset: ${view.getUint32(offset, true)}\n`;
              props += `  Data Size: ${view.getUint32(offset+4, true)}\n`;
              props += `  X Offset: ${view.getUint16(offset+8, true)}\n`;
              props += `  Y Offset: ${view.getUint16(offset+10, true)}\n`;
              props += `  Height: ${view.getUint16(offset+12, true)}\n`;
              props += `  Width: ${view.getUint16(offset+14, true)}\n`;
            }
          }

          // App Data for animated
          if (view.getUint32(45, true) > 0) {
            props += 'Application Data present (size ' + view.getUint32(45, true) + ' bytes)\n';
          }

          output.textContent = props;
        };
        reader.readAsArrayBuffer(file);
      } else {
        output.textContent = 'Please drop a .STI file.';
      }
    });
  </script>
</body>
</html>
  1. Python class for .STI file:
import struct
import os

class STIFile:
    def __init__(self, filename=None):
        self.filename = filename
        self.format_id = None
        self.original_size = None
        self.compressed_size = None
        self.transparent_index = None
        self.flags = None
        self.height = None
        self.width = None
        self.color_details = {}
        self.color_depth = None
        self.app_data_size = None
        self.palette = None
        self.subimages = []
        self.image_data = None
        self.app_data = None
        if filename:
            self.read(filename)

    def read(self, filename):
        with open(filename, 'rb') as f:
            data = f.read()
        self.filename = filename
        (self.format_id, self.original_size, self.compressed_size, self.transparent_index, 
         self.flags, self.height, self.width) = struct.unpack('<4sIIIHH', data[0:24])
        self.format_id = self.format_id.decode('ascii')
        is16bit = (self.flags & 0x08) != 0
        is8bit = (self.flags & 0x10) != 0
        if is16bit:
            (self.color_details['red_mask'], self.color_details['green_mask'], 
             self.color_details['blue_mask'], self.color_details['alpha_mask'],
             self.color_details['red_depth'], self.color_details['green_depth'], 
             self.color_details['blue_depth'], self.color_details['alpha_depth']) = struct.unpack('<IIIIBBBB', data[24:44])
        elif is8bit:
            (self.color_details['num_colors'], self.color_details['num_subimages'], 
             self.color_details['red_depth'], self.color_details['green_depth'], 
             self.color_details['blue_depth']) = struct.unpack('<IHBBB', data[24:33])
            self.palette = data[64:64+768]
            subheader_start = 64 + 768
            for i in range(self.color_details['num_subimages']):
                offset = subheader_start + i * 16
                sub = struct.unpack('<IIHHHH', data[offset:offset+16])
                self.subimages.append({'data_offset': sub[0], 'data_size': sub[1], 'x_offset': sub[2], 
                                       'y_offset': sub[3], 'height': sub[4], 'width': sub[5]})
            image_start = subheader_start + self.color_details['num_subimages'] * 16
            self.image_data = data[image_start:image_start + self.compressed_size]
        self.color_depth = data[44]
        self.app_data_size = struct.unpack('<I', data[45:49])[0]
        if self.app_data_size > 0:
            app_start = 64 + (768 if is8bit else 0) + (len(self.subimages) * 16 if is8bit else 0) + len(self.image_data)
            self.app_data = data[app_start:app_start + self.app_data_size]

    def print_properties(self):
        print(f"Format Identifier: {self.format_id}")
        print(f"Original Size: {self.original_size}")
        print(f"Compressed Size: {self.compressed_size}")
        print(f"Transparent Index: {self.transparent_index}")
        print(f"Flags: {self.flags} (0x{self.flags:x})")
        print(f"Height: {self.height}")
        print(f"Width: {self.width}")
        print(f"Color Details: {self.color_details}")
        print(f"Color Depth: {self.color_depth}")
        print(f"App Data Size: {self.app_data_size}")
        if self.palette:
            print("Palette: (first 5 colors)")
            for i in range(5):
                offset = i * 3
                print(f"Color {i}: R={self.palette[offset]} G={self.palette[offset+1]} B={self.palette[offset+2]}")
        if self.subimages:
            print("Subimages:")
            for i, sub in enumerate(self.subimages):
                print(f"Subimage {i}: {sub}")
        if self.app_data_size > 0:
            print(f"Application Data present (size {self.app_data_size} bytes)")

    def write(self, filename=None):
        if not filename:
            filename = self.filename or "output.sti"
        with open(filename, 'wb') as f:
            f.write(struct.pack('<4sIIIHH', self.format_id.encode('ascii'), self.original_size, self.compressed_size, 
                                self.transparent_index, self.flags, self.height, self.width))
            is16bit = (self.flags & 0x08) != 0
            is8bit = (self.flags & 0x10) != 0
            if is16bit:
                f.write(struct.pack('<IIIIBBBB', self.color_details.get('red_mask', 0), self.color_details.get('green_mask', 0), 
                                    self.color_details.get('blue_mask', 0), self.color_details.get('alpha_mask', 0), 
                                    self.color_details.get('red_depth', 0), self.color_details.get('green_depth', 0), 
                                    self.color_details.get('blue_depth', 0), self.color_details.get('alpha_depth', 0)))
            elif is8bit:
                f.write(struct.pack('<IHBBB', self.color_details.get('num_colors', 0), self.color_details.get('num_subimages', 0), 
                                    self.color_details.get('red_depth', 0), self.color_details.get('green_depth', 0), 
                                    self.color_details.get('blue_depth', 0)))
                f.write(b'\x00' * 11)  # Padding to 44
            f.write(struct.pack('<B', self.color_depth))
            f.write(struct.pack('<I', self.app_data_size))
            f.write(b'\x00' * 16)  # Reserved
            if is8bit:
                f.write(self.palette or b'\x00' * 768)
                for sub in self.subimages:
                    f.write(struct.pack('<IIHHHH', sub['data_offset'], sub['data_size'], sub['x_offset'], 
                                        sub['y_offset'], sub['height'], sub['width']))
            if self.image_data:
                f.write(self.image_data)
            if self.app_data:
                f.write(self.app_data)

# Example usage:
# sti = STIFile('example.sti')
# sti.print_properties()
# sti.write('output.sti')
  1. Java class for .STI file:
import java.io.*;
import java.nio.*;
import java.nio.file.*;

public class STIFile {
    private String filename;
    private String formatId;
    private int originalSize;
    private int compressedSize;
    private int transparentIndex;
    private int flags;
    private short height;
    private short width;
    private ByteBuffer colorDetails = ByteBuffer.allocate(20); // For masks/depths
    private byte colorDepth;
    private int appDataSize;
    private byte[] palette;
    private SubImage[] subimages;
    private byte[] imageData;
    private byte[] appData;

    public STIFile(String filename) throws IOException {
        this.filename = filename;
        read(filename);
    }

    private void read(String filename) throws IOException {
        byte[] data = Files.readAllBytes(Paths.get(filename));
        ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
        formatId = new String(new byte[] {bb.get(), bb.get(), bb.get(), bb.get()});
        originalSize = bb.getInt();
        compressedSize = bb.getInt();
        transparentIndex = bb.getInt();
        flags = bb.getInt();
        height = bb.getShort();
        width = bb.getShort();
        boolean is16bit = (flags & 0x08) != 0;
        boolean is8bit = (flags & 0x10) != 0;
        bb.position(24);
        colorDetails.put(bb.slice().limit(20));
        colorDetails.rewind();
        if (is16bit) {
            colorDetails.getInt(); // red_mask
            colorDetails.getInt(); // green_mask
            colorDetails.getInt(); // blue_mask
            colorDetails.getInt(); // alpha_mask
            colorDetails.get(); // red_depth
            colorDetails.get(); // green_depth
            colorDetails.get(); // blue_depth
            colorDetails.get(); // alpha_depth
        } else if (is8bit) {
            colorDetails.rewind();
            int numColors = colorDetails.getInt();
            short numSubimages = colorDetails.getShort();
            byte redDepth = colorDetails.get();
            byte greenDepth = colorDetails.get();
            byte blueDepth = colorDetails.get();
            palette = new byte[768];
            bb.position(64);
            bb.get(palette);
            subimages = new SubImage[numSubimages];
            int subHeaderStart = 64 + 768;
            for (int i = 0; i < numSubimages; i++) {
                bb.position(subHeaderStart + i * 16);
                subimages[i] = new SubImage(bb.getInt(), bb.getInt(), bb.getShort(), bb.getShort(), bb.getShort(), bb.getShort());
            }
            int imageStart = subHeaderStart + numSubimages * 16;
            imageData = new byte[compressedSize];
            bb.position(imageStart);
            bb.get(imageData);
        }
        bb.position(44);
        colorDepth = bb.get();
        appDataSize = bb.getInt();
        if (appDataSize > 0) {
            int appStart = 64 + (is8bit ? 768 + subimages.length * 16 + imageData.length : imageData.length);
            appData = new byte[appDataSize];
            bb.position(appStart);
            bb.get(appData);
        }
    }

    public void printProperties() {
        System.out.println("Format Identifier: " + formatId);
        System.out.println("Original Size: " + originalSize);
        System.out.println("Compressed Size: " + compressedSize);
        System.out.println("Transparent Index: " + transparentIndex);
        System.out.println("Flags: " + flags + " (0x" + Integer.toHexString(flags) + ")");
        System.out.println("Height: " + height);
        System.out.println("Width: " + width);
        // Print color details as hex dump for brevity
        colorDetails.rewind();
        System.out.print("Color Details (hex): ");
        for (int i = 0; i < 20; i++) {
            System.out.print(String.format("%02X ", colorDetails.get()));
        }
        System.out.println();
        System.out.println("Color Depth: " + (colorDepth & 0xFF));
        System.out.println("App Data Size: " + appDataSize);
        if (palette != null) {
            System.out.println("Palette (first 5 colors):");
            for (int i = 0; i < 5; i++) {
                int offset = i * 3;
                System.out.println("Color " + i + ": R=" + (palette[offset] & 0xFF) + " G=" + (palette[offset+1] & 0xFF) + " B=" + (palette[offset+2] & 0xFF));
            }
        }
        if (subimages != null) {
            System.out.println("Subimages:");
            for (int i = 0; i < subimages.length; i++) {
                System.out.println("Subimage " + i + ": " + subimages[i]);
            }
        }
        if (appDataSize > 0) {
            System.out.println("Application Data present (size " + appDataSize + " bytes)");
        }
    }

    public void write(String filename) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(filename)) {
            ByteBuffer bb = ByteBuffer.allocate(64 + (palette != null ? 768 : 0) + (subimages != null ? subimages.length * 16 : 0) + (imageData != null ? imageData.length : 0) + appDataSize);
            bb.order(ByteOrder.LITTLE_ENDIAN);
            bb.put(formatId.getBytes());
            bb.putInt(originalSize);
            bb.putInt(compressedSize);
            bb.putInt(transparentIndex);
            bb.putInt(flags);
            bb.putShort(height);
            bb.putShort(width);
            colorDetails.rewind();
            bb.put(colorDetails);
            bb.put(colorDepth);
            bb.putInt(appDataSize);
            bb.put(new byte[16]); // Reserved
            if (palette != null) {
                bb.put(palette);
                for (SubImage sub : subimages) {
                    bb.putInt(sub.dataOffset);
                    bb.putInt(sub.dataSize);
                    bb.putShort(sub.xOffset);
                    bb.putShort(sub.yOffset);
                    bb.putShort(sub.height);
                    bb.putShort(sub.width);
                }
            }
            if (imageData != null) {
                bb.put(imageData);
            }
            if (appData != null) {
                bb.put(appData);
            }
            fos.write(bb.array());
        }
    }

    static class SubImage {
        int dataOffset, dataSize;
        short xOffset, yOffset, height, width;

        SubImage(int doff, int dsize, short xoff, short yoff, short h, short w) {
            dataOffset = doff;
            dataSize = dsize;
            xOffset = xoff;
            yOffset = yoff;
            height = h;
            width = w;
        }

        @Override
        public String toString() {
            return "Data Offset: " + dataOffset + ", Data Size: " + dataSize + ", X Offset: " + xOffset + ", Y Offset: " + yOffset + ", Height: " + height + ", Width: " + width;
        }
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     STIFile sti = new STIFile("example.sti");
    //     sti.printProperties();
    //     sti.write("output.sti");
    // }
}
  1. JavaScript class for .STI file (for Node.js):
const fs = require('fs');

class STIFile {
  constructor(filename = null) {
    this.filename = filename;
    this.formatId = null;
    this.originalSize = null;
    this.compressedSize = null;
    this.transparentIndex = null;
    this.flags = null;
    this.height = null;
    this.width = null;
    this.colorDetails = {};
    this.colorDepth = null;
    this.appDataSize = null;
    this.palette = null;
    this.subimages = [];
    this.imageData = null;
    this.appData = null;
    if (filename) {
      this.read(filename);
    }
  }

  read(filename) {
    const data = fs.readFileSync(filename);
    this.filename = filename;
    const view = new DataView(data.buffer);
    this.formatId = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3));
    this.originalSize = view.getUint32(4, true);
    this.compressedSize = view.getUint32(8, true);
    this.transparentIndex = view.getUint32(12, true);
    this.flags = view.getUint32(16, true);
    this.height = view.getUint16(20, true);
    this.width = view.getUint16(22, true);
    const is16bit = (this.flags & 0x08) !== 0;
    const is8bit = (this.flags & 0x10) !== 0;
    if (is16bit) {
      this.colorDetails.redMask = view.getUint32(24, true);
      this.colorDetails.greenMask = view.getUint32(28, true);
      this.colorDetails.blueMask = view.getUint32(32, true);
      this.colorDetails.alphaMask = view.getUint32(36, true);
      this.colorDetails.redDepth = view.getUint8(40);
      this.colorDetails.greenDepth = view.getUint8(41);
      this.colorDetails.blueDepth = view.getUint8(42);
      this.colorDetails.alphaDepth = view.getUint8(43);
    } else if (is8bit) {
      this.colorDetails.numColors = view.getUint32(24, true);
      this.colorDetails.numSubimages = view.getUint16(28, true);
      this.colorDetails.redDepth = view.getUint8(30);
      this.colorDetails.greenDepth = view.getUint8(31);
      this.colorDetails.blueDepth = view.getUint8(32);
      this.palette = new Uint8Array(data.slice(64, 64 + 768));
      const subHeaderStart = 64 + 768;
      for (let i = 0; i < this.colorDetails.numSubimages; i++) {
        const offset = subHeaderStart + i * 16;
        this.subimages.push({
          dataOffset: view.getUint32(offset, true),
          dataSize: view.getUint32(offset + 4, true),
          xOffset: view.getUint16(offset + 8, true),
          yOffset: view.getUint16(offset + 10, true),
          height: view.getUint16(offset + 12, true),
          width: view.getUint16(offset + 14, true)
        });
      }
      const imageStart = subHeaderStart + this.colorDetails.numSubimages * 16;
      this.imageData = new Uint8Array(data.slice(imageStart, imageStart + this.compressedSize));
    }
    this.colorDepth = view.getUint8(44);
    this.appDataSize = view.getUint32(45, true);
    if (this.appDataSize > 0) {
      const appStart = 64 + (is8bit ? 768 + this.subimages.length * 16 + this.imageData.length : this.imageData.length);
      this.appData = new Uint8Array(data.slice(appStart, appStart + this.appDataSize));
    }
  }

  printProperties() {
    console.log(`Format Identifier: ${this.formatId}`);
    console.log(`Original Size: ${this.originalSize}`);
    console.log(`Compressed Size: ${this.compressedSize}`);
    console.log(`Transparent Index: ${this.transparentIndex}`);
    console.log(`Flags: ${this.flags} (0x${this.flags.toString(16)})`);
    console.log(`Height: ${this.height}`);
    console.log(`Width: ${this.width}`);
    console.log(`Color Details: ${JSON.stringify(this.colorDetails)}`);
    console.log(`Color Depth: ${this.colorDepth}`);
    console.log(`App Data Size: ${this.appDataSize}`);
    if (this.palette) {
      console.log('Palette (first 5 colors):');
      for (let i = 0; i < 5; i++) {
        const offset = i * 3;
        console.log(`Color ${i}: R=${this.palette[offset]} G=${this.palette[offset+1]} B=${this.palette[offset+2]}`);
      }
    }
    if (this.subimages.length > 0) {
      console.log('Subimages:');
      this.subimages.forEach((sub, i) => {
        console.log(`Subimage ${i}: ${JSON.stringify(sub)}`);
      });
    }
    if (this.appDataSize > 0) {
      console.log(`Application Data present (size ${this.appDataSize} bytes)`);
    }
  }

  write(filename = null) {
    if (!filename) filename = this.filename || 'output.sti';
    const is16bit = (this.flags & 0x08) !== 0;
    const is8bit = (this.flags & 0x10) !== 0;
    const bufferSize = 64 + (this.palette ? 768 : 0) + (this.subimages.length * 16) + (this.imageData ? this.imageData.length : 0) + this.appDataSize;
    const buffer = new ArrayBuffer(bufferSize);
    const view = new DataView(buffer);
    let pos = 0;
    for (let char of this.formatId) {
      view.setUint8(pos++, char.charCodeAt(0));
    }
    view.setUint32(pos, this.originalSize, true); pos += 4;
    view.setUint32(pos, this.compressedSize, true); pos += 4;
    view.setUint32(pos, this.transparentIndex, true); pos += 4;
    view.setUint32(pos, this.flags, true); pos += 4;
    view.setUint16(pos, this.height, true); pos += 2;
    view.setUint16(pos, this.width, true); pos += 2;
    if (is16bit) {
      view.setUint32(pos, this.colorDetails.redMask, true); pos += 4;
      view.setUint32(pos, this.colorDetails.greenMask, true); pos += 4;
      view.setUint32(pos, this.colorDetails.blueMask, true); pos += 4;
      view.setUint32(pos, this.colorDetails.alphaMask, true); pos += 4;
      view.setUint8(pos, this.colorDetails.redDepth); pos++;
      view.setUint8(pos, this.colorDetails.greenDepth); pos++;
      view.setUint8(pos, this.colorDetails.blueDepth); pos++;
      view.setUint8(pos, this.colorDetails.alphaDepth); pos++;
    } else if (is8bit) {
      view.setUint32(pos, this.colorDetails.numColors, true); pos += 4;
      view.setUint16(pos, this.colorDetails.numSubimages, true); pos += 2;
      view.setUint8(pos, this.colorDetails.redDepth); pos++;
      view.setUint8(pos, this.colorDetails.greenDepth); pos++;
      view.setUint8(pos, this.colorDetails.blueDepth); pos++;
      pos += 11; // Padding
    }
    view.setUint8(pos, this.colorDepth); pos++;
    view.setUint32(pos, this.appDataSize, true); pos += 4;
    pos += 16; // Reserved
    if (this.palette) {
      new Uint8Array(buffer, pos, 768).set(this.palette);
      pos += 768;
      for (let sub of this.subimages) {
        view.setUint32(pos, sub.dataOffset, true); pos += 4;
        view.setUint32(pos, sub.dataSize, true); pos += 4;
        view.setUint16(pos, sub.xOffset, true); pos += 2;
        view.setUint16(pos, sub.yOffset, true); pos += 2;
        view.setUint16(pos, sub.height, true); pos += 2;
        view.setUint16(pos, sub.width, true); pos += 2;
      }
    }
    if (this.imageData) {
      new Uint8Array(buffer, pos, this.imageData.length).set(this.imageData);
      pos += this.imageData.length;
    }
    if (this.appData) {
      new Uint8Array(buffer, pos, this.appData.length).set(this.appData);
    }
    fs.writeFileSync(filename, new Uint8Array(buffer));
  }
}

// Example usage:
// const sti = new STIFile('example.sti');
// sti.printProperties();
// sti.write('output.sti');
  1. C class for .STI file:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

typedef struct {
    uint32_t data_offset;
    uint32_t data_size;
    uint16_t x_offset;
    uint16_t y_offset;
    uint16_t height;
    uint16_t width;
} SubImage;

typedef struct {
    char *filename;
    char format_id[5];
    uint32_t original_size;
    uint32_t compressed_size;
    uint32_t transparent_index;
    uint32_t flags;
    uint16_t height;
    uint16_t width;
    union {
        struct {
            uint32_t red_mask;
            uint32_t green_mask;
            uint32_t blue_mask;
            uint32_t alpha_mask;
            uint8_t red_depth;
            uint8_t green_depth;
            uint8_t blue_depth;
            uint8_t alpha_depth;
        } bit16;
        struct {
            uint32_t num_colors;
            uint16_t num_subimages;
            uint8_t red_depth;
            uint8_t green_depth;
            uint8_t blue_depth;
        } bit8;
    } color_details;
    uint8_t color_depth;
    uint32_t app_data_size;
    uint8_t *palette;
    SubImage *subimages;
    uint8_t *image_data;
    uint8_t *app_data;
} STIFile;

STIFile *sti_create(const char *filename) {
    STIFile *sti = malloc(sizeof(STIFile));
    memset(sti, 0, sizeof(STIFile));
    sti->filename = strdup(filename);
    return sti;
}

void sti_read(STIFile *sti, const char *filename) {
    FILE *f = fopen(filename, "rb");
    if (!f) return;
    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    fseek(f, 0, SEEK_SET);
    uint8_t *data = malloc(size);
    fread(data, 1, size, f);
    fclose(f);

    memcpy(sti->format_id, data, 4);
    sti->format_id[4] = '\0';
    memcpy(&sti->original_size, data + 4, 4);
    memcpy(&sti->compressed_size, data + 8, 4);
    memcpy(&sti->transparent_index, data + 12, 4);
    memcpy(&sti->flags, data + 16, 4);
    memcpy(&sti->height, data + 20, 2);
    memcpy(&sti->width, data + 22, 2);
    uint32_t is16bit = sti->flags & 0x08;
    uint32_t is8bit = sti->flags & 0x10;
    if (is16bit) {
        memcpy(&sti->color_details.bit16.red_mask, data + 24, 4);
        memcpy(&sti->color_details.bit16.green_mask, data + 28, 4);
        memcpy(&sti->color_details.bit16.blue_mask, data + 32, 4);
        memcpy(&sti->color_details.bit16.alpha_mask, data + 36, 4);
        memcpy(&sti->color_details.bit16.red_depth, data + 40, 1);
        memcpy(&sti->color_details.bit16.green_depth, data + 41, 1);
        memcpy(&sti->color_details.bit16.blue_depth, data + 42, 1);
        memcpy(&sti->color_details.bit16.alpha_depth, data + 43, 1);
    } else if (is8bit) {
        memcpy(&sti->color_details.bit8.num_colors, data + 24, 4);
        memcpy(&sti->color_details.bit8.num_subimages, data + 28, 2);
        memcpy(&sti->color_details.bit8.red_depth, data + 30, 1);
        memcpy(&sti->color_details.bit8.green_depth, data + 31, 1);
        memcpy(&sti->color_details.bit8.blue_depth, data + 32, 1);
        sti->palette = malloc(768);
        memcpy(sti->palette, data + 64, 768);
        int num_sub = sti->color_details.bit8.num_subimages;
        sti->subimages = malloc(num_sub * sizeof(SubImage));
        int sub_start = 64 + 768;
        for (int i = 0; i < num_sub; i++) {
            int off = sub_start + i * 16;
            memcpy(&sti->subimages[i].data_offset, data + off, 4);
            memcpy(&sti->subimages[i].data_size, data + off + 4, 4);
            memcpy(&sti->subimages[i].x_offset, data + off + 8, 2);
            memcpy(&sti->subimages[i].y_offset, data + off + 10, 2);
            memcpy(&sti->subimages[i].height, data + off + 12, 2);
            memcpy(&sti->subimages[i].width, data + off + 14, 2);
        }
        int image_start = sub_start + num_sub * 16;
        sti->image_data = malloc(sti->compressed_size);
        memcpy(sti->image_data, data + image_start, sti->compressed_size);
    }
    sti->color_depth = data[44];
    memcpy(&sti->app_data_size, data + 45, 4);
    if (sti->app_data_size > 0) {
        int app_start = 64 + (is8bit ? 768 + sti->color_details.bit8.num_subimages * 16 + sti->compressed_size : sti->compressed_size);
        sti->app_data = malloc(sti->app_data_size);
        memcpy(sti->app_data, data + app_start, sti->app_data_size);
    }
    free(data);
}

void sti_print_properties(const STIFile *sti) {
    printf("Format Identifier: %s\n", sti->format_id);
    printf("Original Size: %u\n", sti->original_size);
    printf("Compressed Size: %u\n", sti->compressed_size);
    printf("Transparent Index: %u\n", sti->transparent_index);
    printf("Flags: %u (0x%x)\n", sti->flags, sti->flags);
    printf("Height: %hu\n", sti->height);
    printf("Width: %hu\n", sti->width);
    uint32_t is16bit = sti->flags & 0x08;
    uint32_t is8bit = sti->flags & 0x10;
    if (is16bit) {
        printf("Red Mask: %u\n", sti->color_details.bit16.red_mask);
        printf("Green Mask: %u\n", sti->color_details.bit16.green_mask);
        printf("Blue Mask: %u\n", sti->color_details.bit16.blue_mask);
        printf("Alpha Mask: %u\n", sti->color_details.bit16.alpha_mask);
        printf("Red Depth: %u\n", sti->color_details.bit16.red_depth);
        printf("Green Depth: %u\n", sti->color_details.bit16.green_depth);
        printf("Blue Depth: %u\n", sti->color_details.bit16.blue_depth);
        printf("Alpha Depth: %u\n", sti->color_details.bit16.alpha_depth);
    } else if (is8bit) {
        printf("Number of Colors: %u\n", sti->color_details.bit8.num_colors);
        printf("Number of Subimages: %hu\n", sti->color_details.bit8.num_subimages);
        printf("Red Depth: %u\n", sti->color_details.bit8.red_depth);
        printf("Green Depth: %u\n", sti->color_details.bit8.green_depth);
        printf("Blue Depth: %u\n", sti->color_details.bit8.blue_depth);
    }
    printf("Color Depth: %u\n", sti->color_depth);
    printf("App Data Size: %u\n", sti->app_data_size);
    if (sti->palette) {
        printf("Palette (first 5 colors):\n");
        for (int i = 0; i < 5; i++) {
            int offset = i * 3;
            printf("Color %d: R=%u G=%u B=%u\n", i, sti->palette[offset], sti->palette[offset+1], sti->palette[offset+2]);
        }
    }
    if (sti->subimages) {
        printf("Subimages:\n");
        for (int i = 0; i < sti->color_details.bit8.num_subimages; i++) {
            SubImage sub = sti->subimages[i];
            printf("Subimage %d: Data Offset %u, Data Size %u, X Offset %hu, Y Offset %hu, Height %hu, Width %hu\n",
                   i, sub.data_offset, sub.data_size, sub.x_offset, sub.y_offset, sub.height, sub.width);
        }
    }
    if (sti->app_data_size > 0) {
        printf("Application Data present (size %u bytes)\n", sti->app_data_size);
    }
}

void sti_write(const STIFile *sti, const char *filename) {
    FILE *f = fopen(filename ? filename : sti->filename, "wb");
    if (!f) return;
    fwrite(sti->format_id, 1, 4, f);
    fwrite(&sti->original_size, 4, 1, f);
    fwrite(&sti->compressed_size, 4, 1, f);
    fwrite(&sti->transparent_index, 4, 1, f);
    fwrite(&sti->flags, 4, 1, f);
    fwrite(&sti->height, 2, 1, f);
    fwrite(&sti->width, 2, 1, f);
    uint32_t is16bit = sti->flags & 0x08;
    uint32_t is8bit = sti->flags & 0x10;
    if (is16bit) {
        fwrite(&sti->color_details.bit16, sizeof(sti->color_details.bit16), 1, f);
    } else if (is8bit) {
        fwrite(&sti->color_details.bit8, sizeof(sti->color_details.bit8), 1, f);
        char pad[11] = {0};
        fwrite(pad, 1, 11, f);
    }
    fwrite(&sti->color_depth, 1, 1, f);
    fwrite(&sti->app_data_size, 4, 1, f);
    char reserved[16] = {0};
    fwrite(reserved, 1, 16, f);
    if (sti->palette) {
        fwrite(sti->palette, 1, 768, f);
        for (int i = 0; i < sti->color_details.bit8.num_subimages; i++) {
            fwrite(&sti->subimages[i], sizeof(SubImage), 1, f);
        }
    }
    if (sti->image_data) {
        fwrite(sti->image_data, 1, sti->compressed_size, f);
    }
    if (sti->app_data) {
        fwrite(sti->app_data, 1, sti->app_data_size, f);
    }
    fclose(f);
}

void sti_free(STIFile *sti) {
    free(sti->filename);
    free(sti->palette);
    free(sti->subimages);
    free(sti->image_data);
    free(sti->app_data);
    free(sti);
}

// Example usage:
// int main() {
//     STIFile *sti = sti_create(NULL);
//     sti_read(sti, "example.sti");
//     sti_print_properties(sti);
//     sti_write(sti, "output.sti");
//     sti_free(sti);
//     return 0;
// }