diff --git a/README.md b/README.md index c4f28f0..a550eed 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | FFX | | [`patterns/ffx/*`](https://gitlab.com/EvelynTSMG/imhex-ffx-pats) | Various Final Fantasy X files | | File System | `application/x-ima` | [`patterns/fs.hexpat`](patterns/fs.hexpat) | Drive File System | | FLAC | `audio/flac` | [`patterns/flac.hexpat`](patterns/flac.hexpat) | Free Lossless Audio Codec, FLAC Audio Format | +| FLC/FLIC | | [`patterns/flc.hexpat`](patterns/flc.hexpat) | FLC/FLIC animation file | | Flipper Zero Settings | | [`patterns/flipper_settings.hexpat`](patterns/flipper_settings.hexpat) | Flipper Zero Settings Files | | GB | `application/x-gameboy-rom` | [`patterns/gb.hexpat`](patterns/gb.hexpat) | Game Boy ROM | | GBA | `application/x-gameboy-advance-rom` | [`patterns/gba.hexpat`](patterns/gba.hexpat) | Game Boy Advance ROM header | diff --git a/patterns/flc.hexpat b/patterns/flc.hexpat new file mode 100644 index 0000000..20c6f77 --- /dev/null +++ b/patterns/flc.hexpat @@ -0,0 +1,268 @@ +#pragma description FLC/FLIC animation file +#pragma endian little +#pragma magic [12 AF] @ 0x4 + +// A flic file could contain many DELTA_FLC chunks, which contain many RLE packets +//#define DECODE_DELTA_FLC + +import std.mem; +import std.core; +import std.io; +import type.color; + +using Color = type::RGB8 [[hex::inline_visualize("color", r, g, b, 0xff)]]; +bitfield Color64 { + r : 6; + padding : 2; + g : 6; + padding : 2; + b : 6; + padding : 2; + + u8 r8 = r << 2 | r >> 4; + u8 g8 = g << 2 | g >> 4; + u8 b8 = b << 2 | b >> 4; +} [[hex::inline_visualize("color", r8, g8, b8, 0xff)]]; + +enum FlicType : u16 { + FLI = 0xAF11, + FLC_8bit = 0xAF12, + FLIC_other = 0xAF44, + FLIC_Huffmann = 0xAF30, + FLIC_frame_shift = 0xAF31, +}; + +enum ChunkType : u16 { + CEL_DATA = 3, // Registration and transparency + COLOR_256 = 4, // 256-level colour palette + DELTA_FLC = 7, // Delta image, word oriented RLE (FLI_SS2) + COLOR_64 = 11, // 64-level colour palette + DELTA_FLI = 12, // Delta image, byte oriented RLE (FLI_LC) + BLACK = 13, // Full black frame (rare) + BYTE_RUN = 15, // Full image, byte oriented RLE (FLI_BRUN) + FLI_COPY = 16, // Uncompressed image (rare) + PSTAMP = 18, // Postage stamp (icon of the first frame) + DTA_BRUN = 25, // Full image, pixel oriented RLE + DTA_COPY = 26, // Uncompressed image + DTA_LC = 27, // Delta image, pixel oriented RLE + LABEL = 31, // Frame label + BMP_MASK = 32, // Bitmap mask + MLEV_MASK = 33, // Multilevel mask + SEGMENT = 34, // Segment information + KEY_IMAGE = 35, // Key image, similar to BYTE_RUN / DTA_BRUN + KEY_PAL = 36, // Key palette, similar to COLOR_256 + REGION = 37, // Region of frame differences + WAVE = 38, // Digitized audio + USERSTRING = 39, // General purpose user data + RGN_MASK = 40, // Region mask + LABELEX = 41, // Extended frame label (includes symbolic name) + SHIFT = 42, // Scanline delta shifts (compression) + PATHMAP = 43, // Path map (segment transitions) + + PREFIX_TYPE = 0xF100, // Prefix chunk + SCRIPT_CHUNK = 0xF1E0, // Embedded "Small" script + FRAME_TYPE = 0xF1FA, // Frame chunk + SEGMENT_TABLE = 0xF1FB, // Segment table chunk + HUFFMAN_TABLE = 0xF1FC // Huffman compression table chunk +}; + +bitfield ExtFlags { + segment_table_present : 1; + regular_key_imagees : 1; + identical_palette : 1; + huffmann_compressed : 1; + bwt_huffmann_compressed : 1; + bitmap_masks_present : 1; + multilevel_masks_present : 1; + region_data_present : 1; + password_protected : 1; + digitized_audio : 1; + contains_script : 1; + region_masks_present : 1; + contains_overlays : 1; + frame_shift_compressed : 1; + padding : 2; +}; + +u16 pixels_per_line = 0; +u16 lines = 0; + +struct Header { + u32 size [[comment("Size of FLIC including this header")]]; + FlicType type [[comment("File type 0xAF11, 0xAF12, 0xAF30, 0xAF44, ...")]]; + u16 frames [[comment("Number of frames in first segment")]]; + u16 width [[comment("FLIC width in pixels")]]; + u16 height [[comment("FLIC height in pixels")]]; + u16 depth [[comment("Bits per pixel (usually 8)")]]; + u16 flags [[comment("Set to zero or to three")]]; + u32 speed [[comment("Delay between frames")]]; + u16 reserved1 [[comment("Set to zero")]]; + u32 created [[comment("Date of FLIC creation (FLC only)")]]; + u32 creator [[comment("Serial number or compiler id (FLC only)")]]; + u32 updated [[comment("Date of FLIC update (FLC only)")]]; + u32 updater [[comment("Serial number (FLC only), see creator")]]; + u16 aspect_dx [[comment("Width of square rectangle (FLC only)")]]; + u16 aspect_dy [[comment("Height of square rectangle (FLC only)")]]; + ExtFlags ext_flags [[comment("EGI: flags for specific EGI extensions")]]; + u16 keyframes [[comment("EGI: key-image frequency")]]; + u16 totalframes [[comment("EGI: total number of frames (segments)")]]; + u32 req_memory [[comment("EGI: maximum chunk size (uncompressed)")]]; + u16 max_regions [[comment("EGI: max. number of regions in a CHK_REGION chunk")]]; + u16 transp_num [[comment("EGI: number of transparent levels")]]; + u8 reserved2[24] [[comment("Set to zero")]]; + u32 oframe1 [[comment("Offset to frame 1 (FLC only)")]]; + u32 oframe2 [[comment("Offset to frame 2 (FLC only)")]]; + u8 reserved3[40] [[comment("Set to zero")]]; + + pixels_per_line = width; + lines = height; +}; + +namespace DeltaFLCChunk { + enum OpcodeType : u8 { + Count = 0b00, + Undefined = 0b01, + LowByte = 0b10, + LineSkipCount = 0b11, + }; + + struct Packet { + u8 skip_count; + s8 count; + if (count < 0) { + u8 replicate_bytes[2]; + } else { + u8 literal_bytes[count * 2]; + } + }; + + u16 packets_in_line; + + fn format_opcode(auto opcode) { + match (opcode.type) { + (OpcodeType::Count): return std::format("Packet(s): {0}", opcode.value); + (OpcodeType::Undefined): return "Undefined"; + (OpcodeType::LowByte): return std::format("Last byte: {0:02x}", opcode.value); + (OpcodeType::LineSkipCount): { + s16 total = 0xC000 | opcode.value; + return std::format("Lines skipped: {0}", total); + } + } + }; + + bitfield Opcode { + value: 14; + OpcodeType type: 2; + + if (type == OpcodeType::Count) { + packets_in_line = value; + break; + } + } [[format("DeltaFLCChunk::format_opcode")]]; + + struct Line { + packets_in_line = 0; + Opcode opcodes[while(true)]; + Packet packets[packets_in_line]; + }; +} + +namespace ByteRunChunk { + u16 pixels_in_line_counter = 0; + + struct Packet { + s8 count; + if (count < 0) { + pixels_in_line_counter = -count + pixels_in_line_counter; + u8 literal_bytes[-count]; + } else { + pixels_in_line_counter = count + pixels_in_line_counter; + u8 replicate_byte; + } + } [[format("format_byte_run_packet")]]; + + struct Line { + pixels_in_line_counter = 0; + u8 packet_count; + Packet packets[while(pixels_in_line_counter < pixels_per_line)]; + }; +} + +struct ColorPalettePacket { + u8 skip; + u8 copy; + if (copy == 0) { + C color[256]; + } else { + C color[copy]; + } +}; + +fn format_chunk(auto chunk) { + return std::format("{}", chunk.type); +}; + +fn format_byte_run_packet(auto packet) { + if (packet.count == -1) { + return std::format("One {}", packet.literal_bytes[0]); + } else if (packet.count < 0) { + return std::format("{} bytes", -packet.count); + } else { + return std::format("{}x color {}", packet.count, packet.replicate_byte); + } +}; + +struct Chunk { + u32 size; + ChunkType type; + + if (type == ChunkType::FRAME_TYPE) { + u16 chunks; + u16 delay; + u16 reserved; + u16 width; + u16 height; + // Not sure if this is the intended operation here? + if (width > 0) { + pixels_per_line = width; + } + if (height > 0) { + lines = height; + } + Chunk subchunks[chunks]; + } else if (type == ChunkType::COLOR_256) { + u16 num_packets; + ColorPalettePacket packets[num_packets]; + } else if (type == ChunkType::COLOR_64) { + u16 num_packets; + ColorPalettePacket packets[num_packets]; + } else if (type == ChunkType::BYTE_RUN) { + ByteRunChunk::Line lines[lines]; + } else if (type == ChunkType::DELTA_FLC) { + u16 line_count; + #ifdef DECODE_DELTA_FLC + DeltaFLCChunk::Line lines[line_count]; + #endif + #ifndef DECODE_DELTA_FLC + u8 data[size - ($ - addressof(this))]; + #endif + } else if (type == ChunkType::PSTAMP) { + u16 height; + u16 width; + u16 xlate; + u8 data[size - ($ - addressof(this))]; + } else if (type == ChunkType::FLI_COPY) { + u8 pixels[lines * pixels_per_line]; + } + s128 remainingBytes = size - ($ - addressof(this)); + // When the chunk size is rounded up to the next even number, it might need a padding byte. + // Unrecognized chunk types will also fill out the padding + if (remainingBytes > 0) { + padding[size - ($ - addressof(this))]; + } else if (remainingBytes < 0) { + std::warning("Chunk size wasn't large enough"); + } +} [[format("format_chunk")]]; + +Header header @ $; +Chunk chunks[while(!std::mem::eof())] @ $; \ No newline at end of file diff --git a/tests/patterns/test_data/flc.hexpat.flc b/tests/patterns/test_data/flc.hexpat.flc new file mode 100644 index 0000000..6174d02 Binary files /dev/null and b/tests/patterns/test_data/flc.hexpat.flc differ