/** * 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; }