# codec.py import struct import zlib import time import json MAGIC = b"SIMIMG" # 6-byte file signature. VERSION = b'\x01' # 1-byte version. MODE_RGB = b'\x01' # For 24-bit RGB (3 bytes per pixel). MODE_RGBA = b'\x02' # For 32-bit RGBA (4 bytes per pixel). # Constants for the compression flag. NO_COMPRESSION = b'\x00' COMPRESSION_ZLIB = b'\x01' # Indicates that zlib compression is used. def encode(image_pixels, use_zlib=True): """ Encodes an image (2D list of (R, G, B) tuples OR (R, G, B, A) tuples) into our custom SIMIMG format. It first applies an RLE-based compression to the pixel data and then optionally compresses the RLE data using zlib. The file format now includes a variable-length metadata block stored in JSON. Fixed header layout (17 bytes total): MAGIC (6 bytes) VERSION (1 byte) Width (4 bytes, little-endian) Height (4 bytes, little-endian) Mode (1 byte) -> MODE_RGB (0x01) for 24-bit RGB OR MODE_RGBA (0x02) for 32-bit RGBA. Compression (1 byte) -> 0x01 indicates zlib compression, 0x00 indicates none Followed by: Metadata length (4 bytes, little-endian) Metadata JSON (variable length) Then: Body: The RLE-compressed (and optionally zlib-compressed) pixel data. The metadata stored includes: - encoder: Identifier string. - timestamp: Unix epoch time when the image was encoded. - original_rle_size: Size of the RLE data (before zlib compression). - compressed_body_size: Size of the final body after compression. - compression_ratio: Ratio = compressed_body_size / original_rle_size. - width, height, mode, and compression type. Returns: A bytes object containing the full encoded image with metadata. """ height = len(image_pixels) width = len(image_pixels[0]) if height > 0 else 0 # Determine the pixel format by checking the first pixel. if height > 0 and width > 0: first_pixel = image_pixels[0][0] if len(first_pixel) == 4: mode = MODE_RGBA mode_str = "32-bit RGBA" else: mode = MODE_RGB mode_str = "24-bit RGB" else: mode = MODE_RGB mode_str = "24-bit RGB" # Determine the compression flag. compression_flag = COMPRESSION_ZLIB if use_zlib else NO_COMPRESSION # Build the fixed header (17 bytes total). fixed_header = ( MAGIC + VERSION + struct.pack("= 2: if literal_buffer: control_byte = len(literal_buffer) # Literal count (range 1-127). rle_data.append(control_byte) for pix in literal_buffer: rle_data.extend(bytes(pix)) literal_buffer = [] # For a run block, control = 128 + (run_length - 1). control = 128 + (run_length - 1) rle_data.append(control) rle_data.extend(bytes(flat_pixels[i])) i += run_length else: literal_buffer.append(flat_pixels[i]) i += 1 if len(literal_buffer) == 128: control_byte = len(literal_buffer) rle_data.append(control_byte) for pix in literal_buffer: rle_data.extend(bytes(pix)) literal_buffer = [] # Flush any remaining literal pixels. if literal_buffer: control_byte = len(literal_buffer) rle_data.append(control_byte) for pix in literal_buffer: rle_data.extend(bytes(pix)) original_rle_size = len(rle_data) # Optionally compress the RLE data with zlib. if use_zlib: compressed_body = zlib.compress(bytes(rle_data)) else: compressed_body = bytes(rle_data) compressed_body_size = len(compressed_body) compression_ratio = (compressed_body_size / original_rle_size if original_rle_size > 0 else 1.0) # Build metadata dictionary. metadata = { "encoder": "SimImg Codec v1.0", "timestamp": time.time(), "original_rle_size": original_rle_size, "compressed_body_size": compressed_body_size, "compression_ratio": compression_ratio, "width": width, "height": height, "mode": mode_str, "compression": "ZLIB" if use_zlib else "None" } metadata_json = json.dumps(metadata) metadata_bytes = metadata_json.encode("utf-8") metadata_length = len(metadata_bytes) # Pack metadata length as 4 bytes little-endian. metadata_header = struct.pack("= 128: run_length = control - 128 + 1 pixel = tuple(rle_data[rle_offset:rle_offset+pixel_size]) rle_offset += pixel_size flat_pixels.extend([pixel] * run_length) else: literal_count = control for _ in range(literal_count): pixel = tuple(rle_data[rle_offset:rle_offset+pixel_size]) rle_offset += pixel_size flat_pixels.append(pixel) if width * height != len(flat_pixels): raise ValueError("Pixel data does not match width/height in header.") # Reconstruct a 2D list (row-major). image = [] for y in range(height): row = flat_pixels[y * width : (y + 1) * width] image.append(row) return image # Aliases for clarity. compress = encode decompress = decode if __name__ == '__main__': # Test: Create and round-trip a small image. # Here we test with a simple RGB image. test_image_rgb = [ [(255, 0, 0)] * 10, [(0, 255, 0)] * 10, [(0, 0, 255)] * 10, ] encoded_rgb = encode(test_image_rgb, use_zlib=True) decoded_rgb = decode(encoded_rgb) assert decoded_rgb == test_image_rgb, "Round-trip encoding/decoding failed for RGB." # Test with a simple RGBA image (with transparency). test_image_rgba = [ [(255, 0, 0, 128)] * 10, [(0, 255, 0, 128)] * 10, [(0, 0, 255, 128)] * 10, ] encoded_rgba = encode(test_image_rgba, use_zlib=True) decoded_rgba = decode(encoded_rgba) assert decoded_rgba == test_image_rgba, "Round-trip encoding/decoding failed for RGBA." print("Codec test passed.")