diff --git a/ipf_encoder/Makefile b/ipf_encoder/Makefile new file mode 100644 index 0000000..efa7157 --- /dev/null +++ b/ipf_encoder/Makefile @@ -0,0 +1,81 @@ +# Makefile for iPF (TSVM Interchangeable Picture Format) Encoder +# Created by CuriousTorvald and Claude on 2025-12-19. + +CC = gcc +CFLAGS = -std=c99 -Wall -Wextra -O2 -D_GNU_SOURCE +DBGFLAGS = +PREFIX = /usr/local + +# Zstd flags (use pkg-config if available, fallback for cross-platform compatibility) +ZSTD_CFLAGS = $(shell pkg-config --cflags libzstd 2>/dev/null || echo "") +ZSTD_LIBS = $(shell pkg-config --libs libzstd 2>/dev/null || echo "-lzstd") +LIBS = -lm $(ZSTD_LIBS) + +# Targets +TARGETS = encoder_ipf decoder_ipf + +# Build all (default) +all: $(TARGETS) + +encoder_ipf: encoder_ipf.c + rm -f encoder_ipf + $(CC) $(CFLAGS) $(ZSTD_CFLAGS) -o encoder_ipf encoder_ipf.c $(LIBS) + @echo "iPF encoder built: encoder_ipf" + +decoder_ipf: decoder_ipf.c + rm -f decoder_ipf + $(CC) $(CFLAGS) $(ZSTD_CFLAGS) -o decoder_ipf decoder_ipf.c $(LIBS) + @echo "iPF decoder built: decoder_ipf" + +# Build with debug symbols +debug: CFLAGS += -g -DDEBUG -fsanitize=address -fno-omit-frame-pointer +debug: DBGFLAGS += -fsanitize=address -fno-omit-frame-pointer +debug: clean $(TARGETS) + +# Build with optimizations +release: CFLAGS = -std=c99 -Wall -Wextra -O3 -D_GNU_SOURCE -march=native +release: clean $(TARGETS) + +# Clean build artifacts +clean: + rm -f $(TARGETS) *.o + +# Install +install: $(TARGETS) + cp encoder_ipf $(PREFIX)/bin/ + cp decoder_ipf $(PREFIX)/bin/ + +# Check for required dependencies +check-deps: + @echo "Checking dependencies..." + @pkg-config --exists libzstd || (echo "Error: libzstd-dev not found. Install libzstd-dev or equivalent" && exit 1) + @which ffmpeg >/dev/null 2>&1 || (echo "Error: ffmpeg not found in PATH" && exit 1) + @which ffprobe >/dev/null 2>&1 || (echo "Error: ffprobe not found in PATH" && exit 1) + @echo "All dependencies found." + +# Help +help: + @echo "iPF (TSVM Interchangeable Picture Format) Tools" + @echo "" + @echo "Targets:" + @echo " all - Build encoder and decoder (default)" + @echo " encoder_ipf - Build encoder only" + @echo " decoder_ipf - Build decoder only" + @echo " debug - Build with debug symbols and AddressSanitizer" + @echo " release - Build with full optimizations" + @echo " clean - Remove build artifacts" + @echo " install - Install to /usr/local/bin" + @echo " check-deps - Check for required dependencies" + @echo " help - Show this help" + @echo "" + @echo "Requirements:" + @echo " - GCC with C99 support" + @echo " - libzstd-dev (Zstd compression library)" + @echo " - FFmpeg (for image encoding/decoding)" + @echo "" + @echo "Usage:" + @echo " make # Build all" + @echo " ./encoder_ipf -i input.png -o output.ipf # Encode" + @echo " ./decoder_ipf -i output.ipf -o decoded.png # Decode" + +.PHONY: all clean install check-deps help debug release diff --git a/ipf_encoder/decoder_ipf.c b/ipf_encoder/decoder_ipf.c new file mode 100644 index 0000000..3b2e3f4 --- /dev/null +++ b/ipf_encoder/decoder_ipf.c @@ -0,0 +1,592 @@ +/** + * iPF Decoder - TSVM Interchangeable Picture Format Decoder + * + * Decodes iPF format (Type 1 or Type 2) images to standard formats via FFmpeg. + * + * Created by CuriousTorvald and Claude on 2025-12-19. + */ + +#include +#include +#include +#include +#include +#include +#include + +// ============================================================================= +// Constants +// ============================================================================= + +#define IPF_MAGIC "\x1F\x54\x53\x56\x4D\x69\x50\x46" // "\x1FTSVMiPF" +#define IPF_HEADER_SIZE 28 // 8 magic + 2 width + 2 height + 1 flags + 1 type + 10 reserved + 4 uncompressed + +#define IPF_TYPE_1 0 // 4:2:0 chroma subsampling +#define IPF_TYPE_2 1 // 4:2:2 chroma subsampling + +#define IPF_FLAG_ALPHA 0x01 +#define IPF_FLAG_ZSTD 0x10 +#define IPF_FLAG_PROGRESSIVE 0x80 + +#define MAX_PATH 4096 + +// ============================================================================= +// Structures +// ============================================================================= + +typedef struct { + uint16_t width; + uint16_t height; + uint8_t flags; + uint8_t type; + uint32_t uncompressed_size; +} ipf_header_t; + +typedef struct { + char *input_file; + char *output_file; + int verbose; + int raw_output; // Output raw RGB instead of using FFmpeg +} decoder_config_t; + +// ============================================================================= +// Utility Functions +// ============================================================================= + +static void print_usage(const char *program) { + printf("iPF Decoder - TSVM Interchangeable Picture Format\n"); + printf("\nUsage: %s -i input.ipf -o output.png [options]\n\n", program); + printf("Required:\n"); + printf(" -i, --input FILE Input iPF file\n"); + printf(" -o, --output FILE Output image file (any format FFmpeg supports)\n"); + printf("\nOptions:\n"); + printf(" --raw Output raw RGB24/RGBA data instead of image file\n"); + printf(" -v, --verbose Verbose output\n"); + printf(" -h, --help Show this help\n"); + printf("\nExamples:\n"); + printf(" %s -i photo.ipf -o photo.png\n", program); + printf(" %s -i logo.ipf -o logo.jpg -v\n", program); +} + +static float clampf(float v, float lo, float hi) { + return v < lo ? lo : (v > hi ? hi : v); +} + +// ============================================================================= +// iPF File Reading +// ============================================================================= + +static int read_ipf_header(FILE *fp, ipf_header_t *header) { + uint8_t magic[8]; + + if (fread(magic, 1, 8, fp) != 8) { + fprintf(stderr, "Error: Failed to read magic\n"); + return -1; + } + + if (memcmp(magic, IPF_MAGIC, 8) != 0) { + fprintf(stderr, "Error: Invalid iPF magic\n"); + return -1; + } + + // Read width (uint16 LE) + if (fread(&header->width, 2, 1, fp) != 1) return -1; + + // Read height (uint16 LE) + if (fread(&header->height, 2, 1, fp) != 1) return -1; + + // Read flags + if (fread(&header->flags, 1, 1, fp) != 1) return -1; + + // Read type + if (fread(&header->type, 1, 1, fp) != 1) return -1; + + // Skip reserved (10 bytes) + fseek(fp, 10, SEEK_CUR); + + // Read uncompressed size (uint32 LE) + if (fread(&header->uncompressed_size, 4, 1, fp) != 1) return -1; + + return 0; +} + +// ============================================================================= +// YCoCg to RGB Conversion +// ============================================================================= + +/** + * Convert YCoCg to RGB for 4 pixels sharing the same chroma. + * y_values: 4 Y values packed as nibbles (Y0|Y1 in low byte, Y2|Y3 in high byte style) + * a_values: 4 alpha values packed similarly + * co, cg: 4-bit chroma values [0..15] + * + * Output: fills rgb array with R,G,B[,A] values for 4 pixels + */ +static void ycocg_to_rgb_quad(int co, int cg, int y0, int y1, int y2, int y3, + int a0, int a1, int a2, int a3, + int has_alpha, uint8_t *rgb) { + // Convert chroma from [0..15] to [-1..1] + float co_f = (co - 7) / 8.0f; + float cg_f = (cg - 7) / 8.0f; + + int ys[4] = {y0, y1, y2, y3}; + int as[4] = {a0, a1, a2, a3}; + + int stride = has_alpha ? 4 : 3; + + for (int i = 0; i < 4; i++) { + float y = ys[i] / 15.0f; + + // YCoCg to RGB conversion + float tmp = y - cg_f / 2.0f; + float g = clampf(cg_f + tmp, 0.0f, 1.0f); + float b = clampf(tmp - co_f / 2.0f, 0.0f, 1.0f); + float r = clampf(b + co_f, 0.0f, 1.0f); + + rgb[i * stride + 0] = (uint8_t)(r * 255.0f + 0.5f); + rgb[i * stride + 1] = (uint8_t)(g * 255.0f + 0.5f); + rgb[i * stride + 2] = (uint8_t)(b * 255.0f + 0.5f); + + if (has_alpha) { + rgb[i * stride + 3] = (uint8_t)(as[i] * 17); // Scale 0-15 to 0-255 + } + } +} + +/** + * Decode iPF1 block (4:2:0 chroma subsampling). + * Input: 12 bytes (or 20 with alpha) + * Output: 16 pixels in RGB24/RGBA format + */ +static void decode_ipf1_block(const uint8_t *block, int has_alpha, uint8_t *pixels, int stride) { + // Read chroma (4 values for 2x2 regions) + int co1 = block[0] & 0x0F; + int co2 = (block[0] >> 4) & 0x0F; + int co3 = block[1] & 0x0F; + int co4 = (block[1] >> 4) & 0x0F; + + int cg1 = block[2] & 0x0F; + int cg2 = (block[2] >> 4) & 0x0F; + int cg3 = block[3] & 0x0F; + int cg4 = (block[3] >> 4) & 0x0F; + + // Read Y values (16 values) + // Layout: [Y1|Y0|Y5|Y4], [Y3|Y2|Y7|Y6], [Y9|Y8|YD|YC], [YB|YA|YF|YE] + int Y[16]; + Y[0] = block[4] & 0x0F; + Y[1] = (block[4] >> 4) & 0x0F; + Y[4] = block[5] & 0x0F; + Y[5] = (block[5] >> 4) & 0x0F; + Y[2] = block[6] & 0x0F; + Y[3] = (block[6] >> 4) & 0x0F; + Y[6] = block[7] & 0x0F; + Y[7] = (block[7] >> 4) & 0x0F; + Y[8] = block[8] & 0x0F; + Y[9] = (block[8] >> 4) & 0x0F; + Y[12] = block[9] & 0x0F; + Y[13] = (block[9] >> 4) & 0x0F; + Y[10] = block[10] & 0x0F; + Y[11] = (block[10] >> 4) & 0x0F; + Y[14] = block[11] & 0x0F; + Y[15] = (block[11] >> 4) & 0x0F; + + // Read alpha values if present + int A[16]; + if (has_alpha) { + A[0] = block[12] & 0x0F; + A[1] = (block[12] >> 4) & 0x0F; + A[4] = block[13] & 0x0F; + A[5] = (block[13] >> 4) & 0x0F; + A[2] = block[14] & 0x0F; + A[3] = (block[14] >> 4) & 0x0F; + A[6] = block[15] & 0x0F; + A[7] = (block[15] >> 4) & 0x0F; + A[8] = block[16] & 0x0F; + A[9] = (block[16] >> 4) & 0x0F; + A[12] = block[17] & 0x0F; + A[13] = (block[17] >> 4) & 0x0F; + A[10] = block[18] & 0x0F; + A[11] = (block[18] >> 4) & 0x0F; + A[14] = block[19] & 0x0F; + A[15] = (block[19] >> 4) & 0x0F; + } else { + for (int i = 0; i < 16; i++) A[i] = 15; + } + + int channels = has_alpha ? 4 : 3; + uint8_t quad[16]; // 4 pixels max + + // Decode 4 quads (2x2 regions), each sharing one chroma pair + // Top-left quad (pixels 0,1,4,5) uses co1/cg1 + ycocg_to_rgb_quad(co1, cg1, Y[0], Y[1], Y[4], Y[5], A[0], A[1], A[4], A[5], has_alpha, quad); + memcpy(pixels + 0 * stride + 0 * channels, quad + 0 * channels, channels); + memcpy(pixels + 0 * stride + 1 * channels, quad + 1 * channels, channels); + memcpy(pixels + 1 * stride + 0 * channels, quad + 2 * channels, channels); + memcpy(pixels + 1 * stride + 1 * channels, quad + 3 * channels, channels); + + // Top-right quad (pixels 2,3,6,7) uses co2/cg2 + ycocg_to_rgb_quad(co2, cg2, Y[2], Y[3], Y[6], Y[7], A[2], A[3], A[6], A[7], has_alpha, quad); + memcpy(pixels + 0 * stride + 2 * channels, quad + 0 * channels, channels); + memcpy(pixels + 0 * stride + 3 * channels, quad + 1 * channels, channels); + memcpy(pixels + 1 * stride + 2 * channels, quad + 2 * channels, channels); + memcpy(pixels + 1 * stride + 3 * channels, quad + 3 * channels, channels); + + // Bottom-left quad (pixels 8,9,12,13) uses co3/cg3 + ycocg_to_rgb_quad(co3, cg3, Y[8], Y[9], Y[12], Y[13], A[8], A[9], A[12], A[13], has_alpha, quad); + memcpy(pixels + 2 * stride + 0 * channels, quad + 0 * channels, channels); + memcpy(pixels + 2 * stride + 1 * channels, quad + 1 * channels, channels); + memcpy(pixels + 3 * stride + 0 * channels, quad + 2 * channels, channels); + memcpy(pixels + 3 * stride + 1 * channels, quad + 3 * channels, channels); + + // Bottom-right quad (pixels 10,11,14,15) uses co4/cg4 + ycocg_to_rgb_quad(co4, cg4, Y[10], Y[11], Y[14], Y[15], A[10], A[11], A[14], A[15], has_alpha, quad); + memcpy(pixels + 2 * stride + 2 * channels, quad + 0 * channels, channels); + memcpy(pixels + 2 * stride + 3 * channels, quad + 1 * channels, channels); + memcpy(pixels + 3 * stride + 2 * channels, quad + 2 * channels, channels); + memcpy(pixels + 3 * stride + 3 * channels, quad + 3 * channels, channels); +} + +/** + * Decode iPF2 block (4:2:2 chroma subsampling). + * Input: 16 bytes (or 24 with alpha) + * Output: 16 pixels in RGB24/RGBA format + */ +static void decode_ipf2_block(const uint8_t *block, int has_alpha, uint8_t *pixels, int stride) { + // Read chroma (8 values for horizontal pairs) + int co[8], cg[8]; + co[0] = block[0] & 0x0F; + co[1] = (block[0] >> 4) & 0x0F; + co[2] = block[1] & 0x0F; + co[3] = (block[1] >> 4) & 0x0F; + co[4] = block[2] & 0x0F; + co[5] = (block[2] >> 4) & 0x0F; + co[6] = block[3] & 0x0F; + co[7] = (block[3] >> 4) & 0x0F; + + cg[0] = block[4] & 0x0F; + cg[1] = (block[4] >> 4) & 0x0F; + cg[2] = block[5] & 0x0F; + cg[3] = (block[5] >> 4) & 0x0F; + cg[4] = block[6] & 0x0F; + cg[5] = (block[6] >> 4) & 0x0F; + cg[6] = block[7] & 0x0F; + cg[7] = (block[7] >> 4) & 0x0F; + + // Read Y values (16 values) - same layout as iPF1 + int Y[16]; + Y[0] = block[8] & 0x0F; + Y[1] = (block[8] >> 4) & 0x0F; + Y[4] = block[9] & 0x0F; + Y[5] = (block[9] >> 4) & 0x0F; + Y[2] = block[10] & 0x0F; + Y[3] = (block[10] >> 4) & 0x0F; + Y[6] = block[11] & 0x0F; + Y[7] = (block[11] >> 4) & 0x0F; + Y[8] = block[12] & 0x0F; + Y[9] = (block[12] >> 4) & 0x0F; + Y[12] = block[13] & 0x0F; + Y[13] = (block[13] >> 4) & 0x0F; + Y[10] = block[14] & 0x0F; + Y[11] = (block[14] >> 4) & 0x0F; + Y[14] = block[15] & 0x0F; + Y[15] = (block[15] >> 4) & 0x0F; + + // Read alpha values if present + int A[16]; + if (has_alpha) { + A[0] = block[16] & 0x0F; + A[1] = (block[16] >> 4) & 0x0F; + A[4] = block[17] & 0x0F; + A[5] = (block[17] >> 4) & 0x0F; + A[2] = block[18] & 0x0F; + A[3] = (block[18] >> 4) & 0x0F; + A[6] = block[19] & 0x0F; + A[7] = (block[19] >> 4) & 0x0F; + A[8] = block[20] & 0x0F; + A[9] = (block[20] >> 4) & 0x0F; + A[12] = block[21] & 0x0F; + A[13] = (block[21] >> 4) & 0x0F; + A[10] = block[22] & 0x0F; + A[11] = (block[22] >> 4) & 0x0F; + A[14] = block[23] & 0x0F; + A[15] = (block[23] >> 4) & 0x0F; + } else { + for (int i = 0; i < 16; i++) A[i] = 15; + } + + int channels = has_alpha ? 4 : 3; + + // iPF2: 4:2:2 - each horizontal pair shares chroma + // Row 0: pixels 0,1 share co[0]/cg[0], pixels 2,3 share co[1]/cg[1] + // Row 1: pixels 4,5 share co[2]/cg[2], pixels 6,7 share co[3]/cg[3] + // Row 2: pixels 8,9 share co[4]/cg[4], pixels 10,11 share co[5]/cg[5] + // Row 3: pixels 12,13 share co[6]/cg[6], pixels 14,15 share co[7]/cg[7] + + int pixel_map[8][4] = { + {0, 1, 0, 1}, // co/cg index 0: pixels 0,1 + {2, 3, 2, 3}, // co/cg index 1: pixels 2,3 + {4, 5, 4, 5}, // co/cg index 2: pixels 4,5 + {6, 7, 6, 7}, // co/cg index 3: pixels 6,7 + {8, 9, 8, 9}, // co/cg index 4: pixels 8,9 + {10, 11, 10, 11}, // co/cg index 5: pixels 10,11 + {12, 13, 12, 13}, // co/cg index 6: pixels 12,13 + {14, 15, 14, 15} // co/cg index 7: pixels 14,15 + }; + + for (int ci = 0; ci < 8; ci++) { + int p0 = pixel_map[ci][0]; + int p1 = pixel_map[ci][1]; + + uint8_t quad[16]; // 4 pixels max (ycocg_to_rgb_quad writes 4 pixels) + ycocg_to_rgb_quad(co[ci], cg[ci], Y[p0], Y[p1], Y[p0], Y[p1], + A[p0], A[p1], A[p0], A[p1], has_alpha, quad); + + int row = p0 / 4; + int col0 = p0 % 4; + int col1 = p1 % 4; + + memcpy(pixels + row * stride + col0 * channels, quad + 0 * channels, channels); + memcpy(pixels + row * stride + col1 * channels, quad + 1 * channels, channels); + } +} + +// ============================================================================= +// Main Decoding +// ============================================================================= + +static int decode_ipf(const decoder_config_t *cfg) { + FILE *fp = fopen(cfg->input_file, "rb"); + if (!fp) { + fprintf(stderr, "Error: Failed to open input file: %s\n", cfg->input_file); + return -1; + } + + // Read header + ipf_header_t header; + if (read_ipf_header(fp, &header) < 0) { + fclose(fp); + return -1; + } + + int has_alpha = (header.flags & IPF_FLAG_ALPHA) != 0; + int use_zstd = (header.flags & IPF_FLAG_ZSTD) != 0; + int progressive = (header.flags & IPF_FLAG_PROGRESSIVE) != 0; + + if (cfg->verbose) { + printf("iPF Header:\n"); + printf(" Size: %dx%d\n", header.width, header.height); + printf(" Type: iPF%d (%s)\n", header.type + 1, + header.type == 0 ? "4:2:0" : "4:2:2"); + printf(" Flags: %s%s%s\n", + has_alpha ? "alpha " : "", + use_zstd ? "zstd " : "", + progressive ? "progressive " : ""); + printf(" Uncompressed size: %u bytes\n", header.uncompressed_size); + } + + if (progressive) { + fprintf(stderr, "Warning: Progressive mode not implemented, decoding as sequential\n"); + } + + // Read compressed/raw block data + fseek(fp, 0, SEEK_END); + long file_size = ftell(fp); + fseek(fp, IPF_HEADER_SIZE, SEEK_SET); + + size_t compressed_size = file_size - IPF_HEADER_SIZE; + uint8_t *compressed_data = malloc(compressed_size); + if (!compressed_data) { + fclose(fp); + fprintf(stderr, "Error: Failed to allocate memory\n"); + return -1; + } + + if (fread(compressed_data, 1, compressed_size, fp) != compressed_size) { + free(compressed_data); + fclose(fp); + fprintf(stderr, "Error: Failed to read block data\n"); + return -1; + } + fclose(fp); + + // Decompress if needed + uint8_t *block_data; + size_t block_data_size; + + if (use_zstd) { + block_data_size = header.uncompressed_size; + block_data = malloc(block_data_size); + if (!block_data) { + free(compressed_data); + fprintf(stderr, "Error: Failed to allocate decompression buffer\n"); + return -1; + } + + size_t result = ZSTD_decompress(block_data, block_data_size, + compressed_data, compressed_size); + if (ZSTD_isError(result)) { + fprintf(stderr, "Error: Zstd decompression failed: %s\n", + ZSTD_getErrorName(result)); + free(block_data); + free(compressed_data); + return -1; + } + + if (cfg->verbose) { + printf("Decompressed: %zu -> %zu bytes\n", compressed_size, block_data_size); + } + + free(compressed_data); + } else { + block_data = compressed_data; + block_data_size = compressed_size; + } + + // Allocate output image + int channels = has_alpha ? 4 : 3; + size_t image_size = (size_t)header.width * header.height * channels; + uint8_t *image = malloc(image_size); + if (!image) { + free(block_data); + fprintf(stderr, "Error: Failed to allocate image buffer\n"); + return -1; + } + + // Decode blocks + int blocks_x = (header.width + 3) / 4; + int blocks_y = (header.height + 3) / 4; + int block_size = (header.type == IPF_TYPE_1) ? (has_alpha ? 20 : 12) : (has_alpha ? 24 : 16); + int row_stride = header.width * channels; + int block_stride = 4 * channels; // 4 pixels per block row + + size_t block_offset = 0; + for (int by = 0; by < blocks_y; by++) { + for (int bx = 0; bx < blocks_x; bx++) { + // Calculate output position + uint8_t *block_pixels = image + by * 4 * row_stride + bx * block_stride; + + if (header.type == IPF_TYPE_1) { + decode_ipf1_block(block_data + block_offset, has_alpha, block_pixels, row_stride); + } else { + decode_ipf2_block(block_data + block_offset, has_alpha, block_pixels, row_stride); + } + + block_offset += block_size; + } + } + + free(block_data); + + if (cfg->verbose) { + printf("Decoded %d blocks (%dx%d)\n", blocks_x * blocks_y, blocks_x, blocks_y); + } + + // Output image + int result = 0; + + if (cfg->raw_output) { + // Write raw RGB/RGBA data + FILE *out = fopen(cfg->output_file, "wb"); + if (!out) { + fprintf(stderr, "Error: Failed to open output file: %s\n", cfg->output_file); + result = -1; + } else { + fwrite(image, 1, image_size, out); + fclose(out); + if (cfg->verbose) { + printf("Wrote %zu bytes raw %s data\n", image_size, has_alpha ? "RGBA" : "RGB24"); + } + } + } else { + // Use FFmpeg to write output image + char cmd[MAX_PATH * 2]; + const char *pix_fmt = has_alpha ? "rgba" : "rgb24"; + + snprintf(cmd, sizeof(cmd), + "ffmpeg -hide_banner -v quiet -y -f rawvideo -pix_fmt %s -s %dx%d " + "-i - \"%s\"", + pix_fmt, header.width, header.height, cfg->output_file); + + if (cfg->verbose) { + printf("FFmpeg command: %s\n", cmd); + } + + FILE *pipe = popen(cmd, "w"); + if (!pipe) { + fprintf(stderr, "Error: Failed to start FFmpeg\n"); + result = -1; + } else { + fwrite(image, 1, image_size, pipe); + int status = pclose(pipe); + if (status != 0) { + fprintf(stderr, "Error: FFmpeg failed with status %d\n", status); + result = -1; + } + } + } + + free(image); + + return result; +} + +// ============================================================================= +// Main Entry Point +// ============================================================================= + +int main(int argc, char *argv[]) { + decoder_config_t cfg = { + .input_file = NULL, + .output_file = NULL, + .verbose = 0, + .raw_output = 0 + }; + + static struct option long_options[] = { + {"input", required_argument, 0, 'i'}, + {"output", required_argument, 0, 'o'}, + {"raw", no_argument, 0, 'R'}, + {"verbose", no_argument, 0, 'v'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0} + }; + + int opt; + while ((opt = getopt_long(argc, argv, "i:o:vh", long_options, NULL)) != -1) { + switch (opt) { + case 'i': + cfg.input_file = optarg; + break; + case 'o': + cfg.output_file = optarg; + break; + case 'R': + cfg.raw_output = 1; + break; + case 'v': + cfg.verbose = 1; + break; + case 'h': + print_usage(argv[0]); + return 0; + default: + print_usage(argv[0]); + return 1; + } + } + + // Validate required arguments + if (!cfg.input_file || !cfg.output_file) { + fprintf(stderr, "Error: Input and output files are required\n\n"); + print_usage(argv[0]); + return 1; + } + + int result = decode_ipf(&cfg); + + if (result == 0) { + printf("Successfully decoded: %s\n", cfg.output_file); + } + + return result == 0 ? 0 : 1; +} diff --git a/ipf_encoder/encoder_ipf.c b/ipf_encoder/encoder_ipf.c new file mode 100644 index 0000000..7057023 --- /dev/null +++ b/ipf_encoder/encoder_ipf.c @@ -0,0 +1,787 @@ +/** + * iPF Encoder - TSVM Interchangeable Picture Format Encoder + * + * Encodes images to iPF format (Type 1 or Type 2) with: + * - YCoCg colour space with chroma subsampling + * - 4x4 block encoding + * - Optional Zstd compression + * - Optional alpha channel + * - Optional Adam7 progressive ordering + * + * Created by CuriousTorvald and Claude on 2025-12-19. + */ + +#include +#include +#include +#include +#include +#include +#include + +// ============================================================================= +// Constants +// ============================================================================= + +#define IPF_MAGIC "\x1F\x54\x53\x56\x4D\x69\x50\x46" // "\x1FTSVMiPF" +#define IPF_HEADER_SIZE 28 // 8 magic + 2 width + 2 height + 1 flags + 1 type + 10 reserved + 4 uncompressed size + +#define DEFAULT_WIDTH 560 +#define DEFAULT_HEIGHT 448 + +#define IPF_TYPE_1 0 // 4:2:0 chroma subsampling (12 bytes per block, +8 with alpha) +#define IPF_TYPE_2 1 // 4:2:2 chroma subsampling (16 bytes per block, +8 with alpha) + +#define IPF_FLAG_ALPHA 0x01 // Has alpha channel +#define IPF_FLAG_ZSTD 0x10 // Zstd compressed +#define IPF_FLAG_PROGRESSIVE 0x80 // Adam7 progressive ordering + +#define MAX_PATH 4096 + +// Bayer dithering kernel (4x4) +static const float BAYER_4X4[16] = { + 0.0f/16.0f, 8.0f/16.0f, 2.0f/16.0f, 10.0f/16.0f, + 12.0f/16.0f, 4.0f/16.0f, 14.0f/16.0f, 6.0f/16.0f, + 3.0f/16.0f, 11.0f/16.0f, 1.0f/16.0f, 9.0f/16.0f, + 15.0f/16.0f, 7.0f/16.0f, 13.0f/16.0f, 5.0f/16.0f +}; + +// Adam7 interlace pattern - pass number (1-7) for each pixel in 8x8 block +// 0 = not in this standard pattern, we'll adapt for 4x4 blocks +static const int ADAM7_PASS[8][8] = { + {1, 6, 4, 6, 2, 6, 4, 6}, + {7, 7, 7, 7, 7, 7, 7, 7}, + {5, 6, 5, 6, 5, 6, 5, 6}, + {7, 7, 7, 7, 7, 7, 7, 7}, + {3, 6, 4, 6, 3, 6, 4, 6}, + {7, 7, 7, 7, 7, 7, 7, 7}, + {5, 6, 5, 6, 5, 6, 5, 6}, + {7, 7, 7, 7, 7, 7, 7, 7} +}; + +// ============================================================================= +// Structures +// ============================================================================= + +typedef struct { + char *input_file; + char *output_file; + int width; + int height; + int ipf_type; // 0 = iPF1, 1 = iPF2 + int use_zstd; // 1 = compress with Zstd + int force_alpha; // 1 = force alpha channel in output + int no_alpha; // 1 = strip alpha even if present in input + int progressive; // 1 = Adam7 progressive ordering + int dither; // Bayer dither pattern index (-1 = no dithering) + int verbose; +} encoder_config_t; + +typedef struct { + uint8_t *data; // RGB or RGBA data + int width; + int height; + int channels; // 3 = RGB, 4 = RGBA + int has_alpha; // 1 if input image has meaningful alpha +} image_t; + +// ============================================================================= +// Utility Functions +// ============================================================================= + +static void print_usage(const char *program) { + printf("iPF Encoder - TSVM Interchangeable Picture Format\n"); + printf("\nUsage: %s -i input.png -o output.ipf [options]\n\n", program); + printf("Required:\n"); + printf(" -i, --input FILE Input image file (any format FFmpeg supports)\n"); + printf(" -o, --output FILE Output iPF file\n"); + printf("\nOptions:\n"); + printf(" -s, --size WxH Output size (default: %dx%d)\n", DEFAULT_WIDTH, DEFAULT_HEIGHT); + printf(" -t, --type N iPF type: 1 (4:2:0, default) or 2 (4:2:2)\n"); + printf(" --no-zstd Disable Zstd compression (default: enabled)\n"); + printf(" --alpha Force alpha channel in output\n"); + printf(" --no-alpha Strip alpha channel from input\n"); + printf(" -p, --progressive Use Adam7 progressive ordering\n"); + printf(" -d, --dither N Bayer dither pattern (0=4x4, -1=none, default: 0)\n"); + printf(" -v, --verbose Verbose output\n"); + printf(" -h, --help Show this help\n"); + printf("\nExamples:\n"); + printf(" %s -i photo.jpg -o photo.ipf\n", program); + printf(" %s -i logo.png -o logo.ipf --alpha\n", program); + printf(" %s -i image.png -o image.ipf -s 280x224 -t 2\n", program); +} + +static int clampi(int v, int lo, int hi) { + return v < lo ? lo : (v > hi ? hi : v); +} + +// Convert chroma value [-1..1] to 4-bit [0..15] +static int chroma_to_four_bits(float f) { + return clampi((int)roundf(f * 8.0f) + 7, 0, 15); +} + +// ============================================================================= +// Image Loading via FFmpeg +// ============================================================================= + +/** + * Probe input image dimensions using FFmpeg. + * Returns 0 on success, -1 on error. + */ +static int probe_image_dimensions(const char *input_file, int *width, int *height, int *has_alpha) { + char cmd[MAX_PATH * 2]; + + // Use ffprobe to get dimensions and pixel format + snprintf(cmd, sizeof(cmd), + "ffprobe -v quiet -select_streams v:0 -show_entries stream=width,height,pix_fmt " + "-of csv=p=0:s=x \"%s\" 2>/dev/null", + input_file); + + FILE *fp = popen(cmd, "r"); + if (!fp) { + fprintf(stderr, "Error: Failed to run ffprobe\n"); + return -1; + } + + char buffer[256]; + if (fgets(buffer, sizeof(buffer), fp) == NULL) { + pclose(fp); + fprintf(stderr, "Error: Failed to read image info\n"); + return -1; + } + pclose(fp); + + // Parse "width x height x pix_fmt" + char pix_fmt[64] = ""; + if (sscanf(buffer, "%dx%dx%63s", width, height, pix_fmt) < 2) { + // Try alternate format without pix_fmt + if (sscanf(buffer, "%dx%d", width, height) != 2) { + fprintf(stderr, "Error: Failed to parse image dimensions\n"); + return -1; + } + } + + // Check if pixel format indicates alpha + *has_alpha = (strstr(pix_fmt, "rgba") != NULL || + strstr(pix_fmt, "argb") != NULL || + strstr(pix_fmt, "bgra") != NULL || + strstr(pix_fmt, "abgr") != NULL || + strstr(pix_fmt, "ya") != NULL || + strstr(pix_fmt, "pal8") != NULL || // palette may have alpha + strstr(pix_fmt, "yuva") != NULL); + + return 0; +} + +/** + * Load and resize image using FFmpeg. + * Maintains aspect ratio and crops to target size. + * Returns image data or NULL on error. + */ +static image_t* load_image(const char *input_file, int target_width, int target_height, + int want_alpha, int verbose) { + int src_width, src_height, src_has_alpha; + + // Probe source dimensions + if (probe_image_dimensions(input_file, &src_width, &src_height, &src_has_alpha) < 0) { + return NULL; + } + + if (verbose) { + printf("Source image: %dx%d, alpha: %s\n", + src_width, src_height, src_has_alpha ? "yes" : "no"); + } + + // Determine if we need alpha channel + int use_alpha = want_alpha || src_has_alpha; + int channels = use_alpha ? 4 : 3; + const char *pix_fmt = use_alpha ? "rgba" : "rgb24"; + + // Build FFmpeg command with scale and crop filter + char cmd[MAX_PATH * 2]; + snprintf(cmd, sizeof(cmd), + "ffmpeg -hide_banner -v quiet -i \"%s\" -f rawvideo -pix_fmt %s -vf " + "\"scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d\" -frames:v 1 -", + input_file, pix_fmt, target_width, target_height, target_width, target_height); + + if (verbose) { + printf("FFmpeg command: %s\n", cmd); + } + + FILE *fp = popen(cmd, "r"); + if (!fp) { + fprintf(stderr, "Error: Failed to start FFmpeg\n"); + return NULL; + } + + // Allocate image + image_t *img = malloc(sizeof(image_t)); + if (!img) { + pclose(fp); + return NULL; + } + + size_t data_size = (size_t)target_width * target_height * channels; + img->data = malloc(data_size); + if (!img->data) { + free(img); + pclose(fp); + return NULL; + } + + img->width = target_width; + img->height = target_height; + img->channels = channels; + img->has_alpha = use_alpha; + + // Read image data + size_t bytes_read = fread(img->data, 1, data_size, fp); + pclose(fp); + + if (bytes_read != data_size) { + fprintf(stderr, "Error: Expected %zu bytes, got %zu\n", data_size, bytes_read); + free(img->data); + free(img); + return NULL; + } + + if (verbose) { + printf("Loaded %dx%d image, %d channels, %zu bytes\n", + img->width, img->height, img->channels, data_size); + } + + return img; +} + +static void free_image(image_t *img) { + if (img) { + free(img->data); + free(img); + } +} + +// ============================================================================= +// iPF Block Encoding +// ============================================================================= + +/** + * Encode a 4x4 block to YCoCg with dithering. + * Returns arrays of Y (16 values), A (16 values), Co (16 values), Cg (16 values). + */ +static void encode_block_to_ycocg(const image_t *img, int block_x, int block_y, + int dither_pattern, + int *Y_out, int *A_out, float *Co_out, float *Cg_out) { + for (int py = 0; py < 4; py++) { + for (int px = 0; px < 4; px++) { + int ox = block_x * 4 + px; + int oy = block_y * 4 + py; + + // Handle out-of-bounds (extend edge pixels) + ox = clampi(ox, 0, img->width - 1); + oy = clampi(oy, 0, img->height - 1); + + // Get dither threshold + float t = 0.0f; + if (dither_pattern >= 0) { + t = BAYER_4X4[(py % 4) * 4 + (px % 4)]; + } + + // Read pixel + int offset = (oy * img->width + ox) * img->channels; + float r0 = img->data[offset + 0] / 255.0f; + float g0 = (img->channels >= 3) ? img->data[offset + 1] / 255.0f : r0; + float b0 = (img->channels >= 3) ? img->data[offset + 2] / 255.0f : r0; + float a0 = (img->channels == 4) ? img->data[offset + 3] / 255.0f : 1.0f; + + // Apply dithering + float r = floorf((t / 15.0f + r0) * 15.0f) / 15.0f; + float g = floorf((t / 15.0f + g0) * 15.0f) / 15.0f; + float b = floorf((t / 15.0f + b0) * 15.0f) / 15.0f; + float a = floorf((t / 15.0f + a0) * 15.0f) / 15.0f; + + // Convert to YCoCg + float co = r - b; // [-1..1] + float tmp = b + co / 2.0f; + float cg = g - tmp; // [-1..1] + float y = tmp + cg / 2.0f; // [0..1] + + int index = py * 4 + px; + Y_out[index] = (int)roundf(y * 15.0f); + A_out[index] = (int)roundf(a * 15.0f); + Co_out[index] = co; + Cg_out[index] = cg; + } + } +} + +/** + * Encode iPF1 block (4:2:0 chroma subsampling). + * Returns 12 bytes (or 20 with alpha). + */ +static int encode_ipf1_block(const int *Ys, const int *As, const float *COs, const float *CGs, + int has_alpha, uint8_t *out) { + // Subsample Co/Cg by averaging 2x2 regions (4:2:0) + int cos1 = chroma_to_four_bits((COs[0] + COs[1] + COs[4] + COs[5]) / 4.0f); + int cos2 = chroma_to_four_bits((COs[2] + COs[3] + COs[6] + COs[7]) / 4.0f); + int cos3 = chroma_to_four_bits((COs[8] + COs[9] + COs[12] + COs[13]) / 4.0f); + int cos4 = chroma_to_four_bits((COs[10] + COs[11] + COs[14] + COs[15]) / 4.0f); + + int cgs1 = chroma_to_four_bits((CGs[0] + CGs[1] + CGs[4] + CGs[5]) / 4.0f); + int cgs2 = chroma_to_four_bits((CGs[2] + CGs[3] + CGs[6] + CGs[7]) / 4.0f); + int cgs3 = chroma_to_four_bits((CGs[8] + CGs[9] + CGs[12] + CGs[13]) / 4.0f); + int cgs4 = chroma_to_four_bits((CGs[10] + CGs[11] + CGs[14] + CGs[15]) / 4.0f); + + // Pack according to iPF1 format + // uint16 [Co4 | Co3 | Co2 | Co1] + out[0] = (cos2 << 4) | cos1; + out[1] = (cos4 << 4) | cos3; + // uint16 [Cg4 | Cg3 | Cg2 | Cg1] + out[2] = (cgs2 << 4) | cgs1; + out[3] = (cgs4 << 4) | cgs3; + // Y values: [Y1|Y0|Y5|Y4], [Y3|Y2|Y7|Y6], [Y9|Y8|YD|YC], [YB|YA|YF|YE] + out[4] = (Ys[1] << 4) | Ys[0]; + out[5] = (Ys[5] << 4) | Ys[4]; + out[6] = (Ys[3] << 4) | Ys[2]; + out[7] = (Ys[7] << 4) | Ys[6]; + out[8] = (Ys[9] << 4) | Ys[8]; + out[9] = (Ys[13] << 4) | Ys[12]; + out[10] = (Ys[11] << 4) | Ys[10]; + out[11] = (Ys[15] << 4) | Ys[14]; + + int block_size = 12; + + if (has_alpha) { + // Alpha values: same layout as Y + out[12] = (As[1] << 4) | As[0]; + out[13] = (As[5] << 4) | As[4]; + out[14] = (As[3] << 4) | As[2]; + out[15] = (As[7] << 4) | As[6]; + out[16] = (As[9] << 4) | As[8]; + out[17] = (As[13] << 4) | As[12]; + out[18] = (As[11] << 4) | As[10]; + out[19] = (As[15] << 4) | As[14]; + block_size = 20; + } + + return block_size; +} + +/** + * Encode iPF2 block (4:2:2 chroma subsampling). + * Returns 16 bytes (or 24 with alpha). + */ +static int encode_ipf2_block(const int *Ys, const int *As, const float *COs, const float *CGs, + int has_alpha, uint8_t *out) { + // Subsample Co/Cg horizontally only (4:2:2) - 8 values each + int cos1 = chroma_to_four_bits((COs[0] + COs[1]) / 2.0f); + int cos2 = chroma_to_four_bits((COs[2] + COs[3]) / 2.0f); + int cos3 = chroma_to_four_bits((COs[4] + COs[5]) / 2.0f); + int cos4 = chroma_to_four_bits((COs[6] + COs[7]) / 2.0f); + int cos5 = chroma_to_four_bits((COs[8] + COs[9]) / 2.0f); + int cos6 = chroma_to_four_bits((COs[10] + COs[11]) / 2.0f); + int cos7 = chroma_to_four_bits((COs[12] + COs[13]) / 2.0f); + int cos8 = chroma_to_four_bits((COs[14] + COs[15]) / 2.0f); + + int cgs1 = chroma_to_four_bits((CGs[0] + CGs[1]) / 2.0f); + int cgs2 = chroma_to_four_bits((CGs[2] + CGs[3]) / 2.0f); + int cgs3 = chroma_to_four_bits((CGs[4] + CGs[5]) / 2.0f); + int cgs4 = chroma_to_four_bits((CGs[6] + CGs[7]) / 2.0f); + int cgs5 = chroma_to_four_bits((CGs[8] + CGs[9]) / 2.0f); + int cgs6 = chroma_to_four_bits((CGs[10] + CGs[11]) / 2.0f); + int cgs7 = chroma_to_four_bits((CGs[12] + CGs[13]) / 2.0f); + int cgs8 = chroma_to_four_bits((CGs[14] + CGs[15]) / 2.0f); + + // Pack according to iPF2 format + // uint32 [Co8 | Co7 | Co6 | Co5 | Co4 | Co3 | Co2 | Co1] + out[0] = (cos2 << 4) | cos1; + out[1] = (cos4 << 4) | cos3; + out[2] = (cos6 << 4) | cos5; + out[3] = (cos8 << 4) | cos7; + // uint32 [Cg8 | Cg7 | Cg6 | Cg5 | Cg4 | Cg3 | Cg2 | Cg1] + out[4] = (cgs2 << 4) | cgs1; + out[5] = (cgs4 << 4) | cgs3; + out[6] = (cgs6 << 4) | cgs5; + out[7] = (cgs8 << 4) | cgs7; + // Y values: same as iPF1 + out[8] = (Ys[1] << 4) | Ys[0]; + out[9] = (Ys[5] << 4) | Ys[4]; + out[10] = (Ys[3] << 4) | Ys[2]; + out[11] = (Ys[7] << 4) | Ys[6]; + out[12] = (Ys[9] << 4) | Ys[8]; + out[13] = (Ys[13] << 4) | Ys[12]; + out[14] = (Ys[11] << 4) | Ys[10]; + out[15] = (Ys[15] << 4) | Ys[14]; + + int block_size = 16; + + if (has_alpha) { + // Alpha values: same layout as Y + out[16] = (As[1] << 4) | As[0]; + out[17] = (As[5] << 4) | As[4]; + out[18] = (As[3] << 4) | As[2]; + out[19] = (As[7] << 4) | As[6]; + out[20] = (As[9] << 4) | As[8]; + out[21] = (As[13] << 4) | As[12]; + out[22] = (As[11] << 4) | As[10]; + out[23] = (As[15] << 4) | As[14]; + block_size = 24; + } + + return block_size; +} + +// ============================================================================= +// Adam7 Progressive Ordering +// ============================================================================= + +/** + * Get Adam7 pass number for a block at (block_x, block_y). + * For blocks, we use a simplified version based on block position. + */ +static int get_adam7_pass(int block_x, int block_y) { + // Use Adam7 pattern for 8x8 blocks, but adapt for 4x4 block indices + int px = (block_x * 4) % 8; + int py = (block_y * 4) % 8; + return ADAM7_PASS[py][px]; +} + +/** + * Encode blocks in Adam7 progressive order. + * Returns the encoded block data in progressive order. + */ +static uint8_t* encode_progressive(const image_t *img, const encoder_config_t *cfg, + int has_alpha, size_t *out_size) { + int blocks_x = (img->width + 3) / 4; + int blocks_y = (img->height + 3) / 4; + int total_blocks = blocks_x * blocks_y; + + int block_size = (cfg->ipf_type == IPF_TYPE_1) ? (has_alpha ? 20 : 12) : (has_alpha ? 24 : 16); + size_t max_size = (size_t)total_blocks * block_size; + + uint8_t *output = malloc(max_size); + if (!output) return NULL; + + // Temporary storage for all encoded blocks + uint8_t *all_blocks = malloc(max_size); + if (!all_blocks) { + free(output); + return NULL; + } + + // Encode all blocks first + size_t offset = 0; + for (int by = 0; by < blocks_y; by++) { + for (int bx = 0; bx < blocks_x; bx++) { + int Ys[16], As[16]; + float COs[16], CGs[16]; + + encode_block_to_ycocg(img, bx, by, cfg->dither, Ys, As, COs, CGs); + + if (cfg->ipf_type == IPF_TYPE_1) { + encode_ipf1_block(Ys, As, COs, CGs, has_alpha, all_blocks + offset); + } else { + encode_ipf2_block(Ys, As, COs, CGs, has_alpha, all_blocks + offset); + } + offset += block_size; + } + } + + // Reorder blocks according to Adam7 progressive order (7 passes) + size_t out_offset = 0; + for (int pass = 1; pass <= 7; pass++) { + for (int by = 0; by < blocks_y; by++) { + for (int bx = 0; bx < blocks_x; bx++) { + if (get_adam7_pass(bx, by) == pass) { + int block_idx = by * blocks_x + bx; + memcpy(output + out_offset, all_blocks + block_idx * block_size, block_size); + out_offset += block_size; + } + } + } + } + + free(all_blocks); + *out_size = out_offset; + return output; +} + +/** + * Encode blocks in sequential (raster) order. + */ +static uint8_t* encode_sequential(const image_t *img, const encoder_config_t *cfg, + int has_alpha, size_t *out_size) { + int blocks_x = (img->width + 3) / 4; + int blocks_y = (img->height + 3) / 4; + int total_blocks = blocks_x * blocks_y; + + int block_size = (cfg->ipf_type == IPF_TYPE_1) ? (has_alpha ? 20 : 12) : (has_alpha ? 24 : 16); + size_t max_size = (size_t)total_blocks * block_size; + + uint8_t *output = malloc(max_size); + if (!output) return NULL; + + size_t offset = 0; + for (int by = 0; by < blocks_y; by++) { + for (int bx = 0; bx < blocks_x; bx++) { + int Ys[16], As[16]; + float COs[16], CGs[16]; + + encode_block_to_ycocg(img, bx, by, cfg->dither, Ys, As, COs, CGs); + + if (cfg->ipf_type == IPF_TYPE_1) { + offset += encode_ipf1_block(Ys, As, COs, CGs, has_alpha, output + offset); + } else { + offset += encode_ipf2_block(Ys, As, COs, CGs, has_alpha, output + offset); + } + } + } + + *out_size = offset; + return output; +} + +// ============================================================================= +// iPF File Writing +// ============================================================================= + +static int write_ipf_file(const char *output_file, const encoder_config_t *cfg, + const image_t *img, int verbose) { + // Determine if we use alpha + int has_alpha = 0; + if (cfg->force_alpha) { + has_alpha = 1; + } else if (!cfg->no_alpha && img->has_alpha) { + has_alpha = 1; + } + + // Encode blocks + size_t block_data_size; + uint8_t *block_data; + + if (cfg->progressive) { + block_data = encode_progressive(img, cfg, has_alpha, &block_data_size); + } else { + block_data = encode_sequential(img, cfg, has_alpha, &block_data_size); + } + + if (!block_data) { + fprintf(stderr, "Error: Failed to encode image blocks\n"); + return -1; + } + + if (verbose) { + printf("Encoded %zu bytes of block data\n", block_data_size); + } + + // Prepare output data (may be compressed) + uint8_t *output_data = block_data; + size_t output_size = block_data_size; + uint8_t *compressed_data = NULL; + + if (cfg->use_zstd) { + size_t max_compressed = ZSTD_compressBound(block_data_size); + compressed_data = malloc(max_compressed); + if (!compressed_data) { + free(block_data); + fprintf(stderr, "Error: Failed to allocate compression buffer\n"); + return -1; + } + + output_size = ZSTD_compress(compressed_data, max_compressed, + block_data, block_data_size, 7); + if (ZSTD_isError(output_size)) { + fprintf(stderr, "Error: Zstd compression failed: %s\n", + ZSTD_getErrorName(output_size)); + free(block_data); + free(compressed_data); + return -1; + } + + output_data = compressed_data; + + if (verbose) { + printf("Compressed: %zu -> %zu bytes (%.1f%%)\n", + block_data_size, output_size, + 100.0 * output_size / block_data_size); + } + } + + // Open output file + FILE *fp = fopen(output_file, "wb"); + if (!fp) { + fprintf(stderr, "Error: Failed to open output file: %s\n", output_file); + free(block_data); + if (compressed_data) free(compressed_data); + return -1; + } + + // Build flags byte + uint8_t flags = 0; + if (has_alpha) flags |= IPF_FLAG_ALPHA; + if (cfg->use_zstd) flags |= IPF_FLAG_ZSTD; + if (cfg->progressive) flags |= IPF_FLAG_PROGRESSIVE | IPF_FLAG_ZSTD; // Progressive always sets zstd flag + + // Write header + // Magic: "\x1FTSVMiPF" (8 bytes) + fwrite(IPF_MAGIC, 1, 8, fp); + + // Width (uint16 LE) + uint16_t width_le = (uint16_t)cfg->width; + fwrite(&width_le, 2, 1, fp); + + // Height (uint16 LE) + uint16_t height_le = (uint16_t)cfg->height; + fwrite(&height_le, 2, 1, fp); + + // Flags (uint8) + fwrite(&flags, 1, 1, fp); + + // Type (uint8) + uint8_t type_byte = (uint8_t)cfg->ipf_type; + fwrite(&type_byte, 1, 1, fp); + + // Reserved (10 bytes) + uint8_t reserved[10] = {0}; + fwrite(reserved, 1, 10, fp); + + // Uncompressed size (uint32 LE) + uint32_t uncompressed_size_le = (uint32_t)block_data_size; + fwrite(&uncompressed_size_le, 4, 1, fp); + + // Write block data + fwrite(output_data, 1, output_size, fp); + + fclose(fp); + + if (verbose) { + printf("Wrote %zu bytes to %s\n", IPF_HEADER_SIZE + output_size, output_file); + printf(" Format: iPF%d, %dx%d\n", cfg->ipf_type + 1, cfg->width, cfg->height); + printf(" Flags: %s%s%s\n", + has_alpha ? "alpha " : "", + cfg->use_zstd ? "zstd " : "", + cfg->progressive ? "progressive " : ""); + } + + free(block_data); + if (compressed_data) free(compressed_data); + + return 0; +} + +// ============================================================================= +// Main Entry Point +// ============================================================================= + +static int parse_size(const char *arg, int *width, int *height) { + return sscanf(arg, "%dx%d", width, height) == 2 ? 0 : -1; +} + +int main(int argc, char *argv[]) { + encoder_config_t cfg = { + .input_file = NULL, + .output_file = NULL, + .width = DEFAULT_WIDTH, + .height = DEFAULT_HEIGHT, + .ipf_type = IPF_TYPE_1, + .use_zstd = 1, + .force_alpha = 0, + .no_alpha = 0, + .progressive = 0, + .dither = 0, + .verbose = 0 + }; + + static struct option long_options[] = { + {"input", required_argument, 0, 'i'}, + {"output", required_argument, 0, 'o'}, + {"size", required_argument, 0, 's'}, + {"type", required_argument, 0, 't'}, + {"no-zstd", no_argument, 0, 'Z'}, + {"alpha", no_argument, 0, 'A'}, + {"no-alpha", no_argument, 0, 'N'}, + {"progressive", no_argument, 0, 'p'}, + {"dither", required_argument, 0, 'd'}, + {"verbose", no_argument, 0, 'v'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0} + }; + + int opt; + while ((opt = getopt_long(argc, argv, "i:o:s:t:pd:vh", long_options, NULL)) != -1) { + switch (opt) { + case 'i': + cfg.input_file = optarg; + break; + case 'o': + cfg.output_file = optarg; + break; + case 's': + if (parse_size(optarg, &cfg.width, &cfg.height) != 0) { + fprintf(stderr, "Error: Invalid size format (use WxH)\n"); + return 1; + } + break; + case 't': + cfg.ipf_type = atoi(optarg) - 1; // User specifies 1 or 2 + if (cfg.ipf_type < 0 || cfg.ipf_type > 1) { + fprintf(stderr, "Error: Invalid iPF type (use 1 or 2)\n"); + return 1; + } + break; + case 'Z': + cfg.use_zstd = 0; + break; + case 'A': + cfg.force_alpha = 1; + break; + case 'N': + cfg.no_alpha = 1; + break; + case 'p': + cfg.progressive = 1; + break; + case 'd': + cfg.dither = atoi(optarg); + break; + case 'v': + cfg.verbose = 1; + break; + case 'h': + print_usage(argv[0]); + return 0; + default: + print_usage(argv[0]); + return 1; + } + } + + // Validate required arguments + if (!cfg.input_file || !cfg.output_file) { + fprintf(stderr, "Error: Input and output files are required\n\n"); + print_usage(argv[0]); + return 1; + } + + // Load image + if (cfg.verbose) { + printf("Loading image: %s\n", cfg.input_file); + } + + image_t *img = load_image(cfg.input_file, cfg.width, cfg.height, + cfg.force_alpha, cfg.verbose); + if (!img) { + fprintf(stderr, "Error: Failed to load image\n"); + return 1; + } + + // Encode and write iPF file + int result = write_ipf_file(cfg.output_file, &cfg, img, cfg.verbose); + + free_image(img); + + if (result == 0) { + printf("Successfully encoded: %s\n", cfg.output_file); + } + + return result == 0 ? 0 : 1; +}