From 343918b5069aea62f278a36bb612529bb77f5d47 Mon Sep 17 00:00:00 2001 From: "Chris (ChrisJr404)" <11917633+ChrisJr404@users.noreply.github.com> Date: Tue, 25 Aug 2026 16:13:49 -0400 Subject: [PATCH] bmp: Add decoder --- format/all/all.go | 1 + format/bmp/bmp.go | 110 +++++++++++++++++++++++++++++ format/bmp/testdata/4x4.bmp | Bin 0 -> 102 bytes format/bmp/testdata/4x4.fqtest | 27 +++++++ format/bmp/testdata/palette.bmp | Bin 0 -> 66 bytes format/bmp/testdata/palette.fqtest | 37 ++++++++++ format/bmp/testdata/v4.bmp | Bin 0 -> 138 bytes format/bmp/testdata/v4.fqtest | 29 ++++++++ format/format.go | 1 + 9 files changed, 205 insertions(+) create mode 100644 format/bmp/bmp.go create mode 100644 format/bmp/testdata/4x4.bmp create mode 100644 format/bmp/testdata/4x4.fqtest create mode 100644 format/bmp/testdata/palette.bmp create mode 100644 format/bmp/testdata/palette.fqtest create mode 100644 format/bmp/testdata/v4.bmp create mode 100644 format/bmp/testdata/v4.fqtest diff --git a/format/all/all.go b/format/all/all.go index 0b92931e3..a13418e70 100644 --- a/format/all/all.go +++ b/format/all/all.go @@ -13,6 +13,7 @@ import ( _ "github.com/wader/fq/format/bencode" _ "github.com/wader/fq/format/bitcoin" _ "github.com/wader/fq/format/bits" + _ "github.com/wader/fq/format/bmp" _ "github.com/wader/fq/format/bson" _ "github.com/wader/fq/format/bzip2" _ "github.com/wader/fq/format/caff" diff --git a/format/bmp/bmp.go b/format/bmp/bmp.go new file mode 100644 index 000000000..bef379ac3 --- /dev/null +++ b/format/bmp/bmp.go @@ -0,0 +1,110 @@ +package bmp + +// https://en.wikipedia.org/wiki/BMP_file_format +// https://learn.microsoft.com/en-us/windows/win32/gdi/bitmap-header-types + +// Decodes the file header and the BITMAPINFOHEADER family of DIB headers +// (size >= 40), which covers essentially all BMPs seen in the wild. The +// 12 byte BITMAPCOREHEADER is not handled specially. + +import ( + "github.com/wader/fq/format" + "github.com/wader/fq/pkg/decode" + "github.com/wader/fq/pkg/interp" + "github.com/wader/fq/pkg/scalar" +) + +func init() { + interp.RegisterFormat( + format.BMP, + &decode.Format{ + Description: "Bitmap image", + Groups: []*decode.Group{format.Probe, format.Image}, + DecodeFn: bmpDecode, + }) +} + +const ( + compressionRGB = 0 + compressionRLE8 = 1 + compressionRLE4 = 2 + compressionBitfields = 3 + compressionJPEG = 4 + compressionPNG = 5 + compressionAlphaBitfields = 6 + compressionCMYK = 11 + compressionCMYKRLE8 = 12 + compressionCMYKRLE4 = 13 +) + +var compressionNames = scalar.UintMapSymStr{ + compressionRGB: "rgb", + compressionRLE8: "rle8", + compressionRLE4: "rle4", + compressionBitfields: "bitfields", + compressionJPEG: "jpeg", + compressionPNG: "png", + compressionAlphaBitfields: "alpha_bitfields", + compressionCMYK: "cmyk", + compressionCMYKRLE8: "cmyk_rle8", + compressionCMYKRLE4: "cmyk_rle4", +} + +func bmpDecode(d *decode.D) any { + d.Endian = decode.LittleEndian + + var bitmapOffset uint64 + var bitsPerPixel uint64 + var colorsUsed uint64 + + d.FieldStruct("file_header", func(d *decode.D) { + d.FieldUTF8("type", 2, d.StrAssert("BM")) + d.FieldU32("size") + d.FieldU16("reserved1") + d.FieldU16("reserved2") + bitmapOffset = d.FieldU32("bitmap_offset") + }) + + d.FieldStruct("dib_header", func(d *decode.D) { + headerSize := d.FieldU32("size") + d.FieldS32("width") + d.FieldS32("height") + d.FieldU16("planes") + bitsPerPixel = d.FieldU16("bits_per_pixel") + d.FieldU32("compression", compressionNames) + d.FieldU32("image_size") + d.FieldS32("x_pixels_per_meter") + d.FieldS32("y_pixels_per_meter") + colorsUsed = d.FieldU32("colors_used") + d.FieldU32("colors_important") + // v4/v5 headers add bit masks, color space and gamma after the common part + if headerSize > 40 { + d.FieldRawLen("rest", int64(headerSize-40)*8) + } + }) + + if bitsPerPixel <= 8 { + n := colorsUsed + if n == 0 { + n = 1 << bitsPerPixel + } + d.FieldArray("color_table", func(d *decode.D) { + for range int(n) { + d.FieldStruct("color", func(d *decode.D) { + d.FieldU8("blue") + d.FieldU8("green") + d.FieldU8("red") + d.FieldU8("reserved") + }) + } + }) + } + + if gap := int64(bitmapOffset)*8 - d.Pos(); gap > 0 { + d.FieldRawLen("gap", gap) + } + + d.FieldRawLen("pixels", d.BitsLeft()) + + return nil +} diff --git a/format/bmp/testdata/4x4.bmp b/format/bmp/testdata/4x4.bmp new file mode 100644 index 0000000000000000000000000000000000000000..434fb6882364167729634439c73e304fa8b64bcf GIT binary patch literal 102 tcmZ?rO=ExnGa#h_#4J$E$RGihFaVRN#Q*>QfdUW`&H!>DA`lW;901~R8I1q{ literal 0 HcmV?d00001 diff --git a/format/bmp/testdata/4x4.fqtest b/format/bmp/testdata/4x4.fqtest new file mode 100644 index 000000000..0337a5116 --- /dev/null +++ b/format/bmp/testdata/4x4.fqtest @@ -0,0 +1,27 @@ +# convert -size 4x4 pattern:gray50 -define bmp:format=bmp3 -type truecolor BMP3:4x4.bmp +$ fq -d bmp dv 4x4.bmp + |00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f|0123456789abcdef|.{}: 4x4.bmp (bmp) 0x0-0x66 (102) + | | | file_header{}: 0x0-0xe (14) +0x00|42 4d |BM | type: "BM" (valid) 0x0-0x2 (2) +0x00| 66 00 00 00 | f... | size: 102 0x2-0x6 (4) +0x00| 00 00 | .. | reserved1: 0 0x6-0x8 (2) +0x00| 00 00 | .. | reserved2: 0 0x8-0xa (2) +0x00| 36 00 00 00 | 6... | bitmap_offset: 54 0xa-0xe (4) + | | | dib_header{}: 0xe-0x36 (40) +0x00| 28 00| (.| size: 40 0xe-0x12 (4) +0x10|00 00 |.. | +0x10| 04 00 00 00 | .... | width: 4 0x12-0x16 (4) +0x10| 04 00 00 00 | .... | height: 4 0x16-0x1a (4) +0x10| 01 00 | .. | planes: 1 0x1a-0x1c (2) +0x10| 18 00 | .. | bits_per_pixel: 24 0x1c-0x1e (2) +0x10| 00 00| ..| compression: "rgb" (0) 0x1e-0x22 (4) +0x20|00 00 |.. | +0x20| 30 00 00 00 | 0... | image_size: 48 0x22-0x26 (4) +0x20| 00 00 00 00 | .... | x_pixels_per_meter: 0 0x26-0x2a (4) +0x20| 00 00 00 00 | .... | y_pixels_per_meter: 0 0x2a-0x2e (4) +0x20| 00 00| ..| colors_used: 0 0x2e-0x32 (4) +0x30|00 00 |.. | +0x30| 00 00 00 00 | .... | colors_important: 0 0x32-0x36 (4) +0x30| ff ff ff 00 00 00 ff ff ff 00| ..........| pixels: raw bits 0x36-0x66 (48) +0x40|00 00 00 00 00 ff ff ff 00 00 00 ff ff ff ff ff|................| +* |until 0x65.7 (end) (48) | | diff --git a/format/bmp/testdata/palette.bmp b/format/bmp/testdata/palette.bmp new file mode 100644 index 0000000000000000000000000000000000000000..bb41938b1ea749dabd317fec643909de738d0090 GIT binary patch literal 66 rcmZ?rbz*=3J0PV2#2i4(2*x0ifq?}~LI@@x1;QZk|Nno6P#_HeWCR4A literal 0 HcmV?d00001 diff --git a/format/bmp/testdata/palette.fqtest b/format/bmp/testdata/palette.fqtest new file mode 100644 index 000000000..d3334356a --- /dev/null +++ b/format/bmp/testdata/palette.fqtest @@ -0,0 +1,37 @@ +# convert -size 8x1 pattern:gray50 -monochrome -define bmp:format=bmp3 BMP3:palette.bmp +$ fq -d bmp dv palette.bmp + |00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f|0123456789abcdef|.{}: palette.bmp (bmp) 0x0-0x42 (66) + | | | file_header{}: 0x0-0xe (14) +0x00|42 4d |BM | type: "BM" (valid) 0x0-0x2 (2) +0x00| 42 00 00 00 | B... | size: 66 0x2-0x6 (4) +0x00| 00 00 | .. | reserved1: 0 0x6-0x8 (2) +0x00| 00 00 | .. | reserved2: 0 0x8-0xa (2) +0x00| 3e 00 00 00 | >... | bitmap_offset: 62 0xa-0xe (4) + | | | dib_header{}: 0xe-0x36 (40) +0x00| 28 00| (.| size: 40 0xe-0x12 (4) +0x10|00 00 |.. | +0x10| 08 00 00 00 | .... | width: 8 0x12-0x16 (4) +0x10| 01 00 00 00 | .... | height: 1 0x16-0x1a (4) +0x10| 01 00 | .. | planes: 1 0x1a-0x1c (2) +0x10| 01 00 | .. | bits_per_pixel: 1 0x1c-0x1e (2) +0x10| 00 00| ..| compression: "rgb" (0) 0x1e-0x22 (4) +0x20|00 00 |.. | +0x20| 04 00 00 00 | .... | image_size: 4 0x22-0x26 (4) +0x20| 00 00 00 00 | .... | x_pixels_per_meter: 0 0x26-0x2a (4) +0x20| 00 00 00 00 | .... | y_pixels_per_meter: 0 0x2a-0x2e (4) +0x20| 02 00| ..| colors_used: 2 0x2e-0x32 (4) +0x30|00 00 |.. | +0x30| 02 00 00 00 | .... | colors_important: 2 0x32-0x36 (4) + | | | color_table[0:2]: 0x36-0x3e (8) + | | | [0]{}: color 0x36-0x3a (4) +0x30| 00 | . | blue: 0 0x36-0x37 (1) +0x30| 00 | . | green: 0 0x37-0x38 (1) +0x30| 00 | . | red: 0 0x38-0x39 (1) +0x30| 00 | . | reserved: 0 0x39-0x3a (1) + | | | [1]{}: color 0x3a-0x3e (4) +0x30| ff | . | blue: 255 0x3a-0x3b (1) +0x30| ff | . | green: 255 0x3b-0x3c (1) +0x30| ff | . | red: 255 0x3c-0x3d (1) +0x30| 00 | . | reserved: 0 0x3d-0x3e (1) +0x30| 55 00| U.| pixels: raw bits 0x3e-0x42 (4) +0x40|00 00| |..| | diff --git a/format/bmp/testdata/v4.bmp b/format/bmp/testdata/v4.bmp new file mode 100644 index 0000000000000000000000000000000000000000..1e6b886e71ad31c2286d21ca61f63f1ae5035cd7 GIT binary patch literal 138 ycmZ?r?P7p{Dj<~u#7t1k$e_T$3}g!cF(&xWzyLuYDIkj+@c%zhCy)SQAOHZCWe_R= literal 0 HcmV?d00001 diff --git a/format/bmp/testdata/v4.fqtest b/format/bmp/testdata/v4.fqtest new file mode 100644 index 000000000..8348806af --- /dev/null +++ b/format/bmp/testdata/v4.fqtest @@ -0,0 +1,29 @@ +# BITMAPV4HEADER (108 byte) 2x2 32-bit BGRA image, generated by hand +$ fq -d bmp dv v4.bmp + |00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f|0123456789abcdef|.{}: v4.bmp (bmp) 0x0-0x8a (138) + | | | file_header{}: 0x0-0xe (14) +0x00|42 4d |BM | type: "BM" (valid) 0x0-0x2 (2) +0x00| 8a 00 00 00 | .... | size: 138 0x2-0x6 (4) +0x00| 00 00 | .. | reserved1: 0 0x6-0x8 (2) +0x00| 00 00 | .. | reserved2: 0 0x8-0xa (2) +0x00| 7a 00 00 00 | z... | bitmap_offset: 122 0xa-0xe (4) + | | | dib_header{}: 0xe-0x7a (108) +0x00| 6c 00| l.| size: 108 0xe-0x12 (4) +0x10|00 00 |.. | +0x10| 02 00 00 00 | .... | width: 2 0x12-0x16 (4) +0x10| 02 00 00 00 | .... | height: 2 0x16-0x1a (4) +0x10| 01 00 | .. | planes: 1 0x1a-0x1c (2) +0x10| 20 00 | . | bits_per_pixel: 32 0x1c-0x1e (2) +0x10| 03 00| ..| compression: "bitfields" (3) 0x1e-0x22 (4) +0x20|00 00 |.. | +0x20| 10 00 00 00 | .... | image_size: 16 0x22-0x26 (4) +0x20| 00 00 00 00 | .... | x_pixels_per_meter: 0 0x26-0x2a (4) +0x20| 00 00 00 00 | .... | y_pixels_per_meter: 0 0x2a-0x2e (4) +0x20| 00 00| ..| colors_used: 0 0x2e-0x32 (4) +0x30|00 00 |.. | +0x30| 00 00 00 00 | .... | colors_important: 0 0x32-0x36 (4) +0x30| 00 00 ff 00 00 ff 00 00 ff 00| ..........| rest: raw bits 0x36-0x7a (68) +0x40|00 00 00 00 00 ff 00 00 00 00 00 00 00 00 00 00|................| +* |until 0x79.7 (68) | | +0x70| 00 00 ff ff 00 ff| ......| pixels: raw bits 0x7a-0x8a (16) +0x80|00 ff ff 00 00 ff ff ff ff ff| |..........| | diff --git a/format/format.go b/format/format.go index 8a3166e95..53e22dd67 100644 --- a/format/format.go +++ b/format/format.go @@ -81,6 +81,7 @@ var ( Bitcoin_Block = &decode.Group{Name: "bitcoin_block"} Bitcoin_Script = &decode.Group{Name: "bitcoin_script"} Bitcoin_Transaction = &decode.Group{Name: "bitcoin_transaction"} + BMP = &decode.Group{Name: "bmp"} Bplist = &decode.Group{Name: "bplist"} BSD_Loopback_Frame = &decode.Group{Name: "bsd_loopback_frame"} BSON = &decode.Group{Name: "bson"}