bmp.zig (12131B) - Raw
1 //! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader 2 //! https://learn.microsoft.com/en-us/previous-versions//dd183376(v=vs.85) 3 //! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfo 4 //! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapcoreheader 5 //! https://archive.org/details/mac_Graphics_File_Formats_Second_Edition_1996/page/n607/mode/2up 6 //! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header 7 //! 8 //! Notes: 9 //! - The Microsoft documentation is incredibly unclear about the color table when the 10 //! bit depth is >= 16. 11 //! + For bit depth 24 it says "the bmiColors member of BITMAPINFO is NULL" but also 12 //! says "the bmiColors color table is used for optimizing colors used on palette-based 13 //! devices, and must contain the number of entries specified by the bV5ClrUsed member" 14 //! + For bit depth 16 and 32, it seems to imply that if the compression is BI_BITFIELDS 15 //! or BI_ALPHABITFIELDS, then the color table *only* consists of the bit masks, but 16 //! doesn't really say this outright and the Wikipedia article seems to disagree 17 //! For the purposes of this implementation, color tables can always be present for any 18 //! bit depth and compression, and the color table follows the header + any optional 19 //! bit mask fields dictated by the specified compression. 20 21 const std = @import("std"); 22 const BitmapHeader = @import("ico.zig").BitmapHeader; 23 const builtin = @import("builtin"); 24 const native_endian = builtin.cpu.arch.endian(); 25 26 pub const windows_format_id = std.mem.readInt(u16, "BM", native_endian); 27 pub const file_header_len = 14; 28 29 pub const ReadError = error{ 30 UnexpectedEOF, 31 InvalidFileHeader, 32 ImpossiblePixelDataOffset, 33 UnknownBitmapVersion, 34 InvalidBitsPerPixel, 35 TooManyColorsInPalette, 36 MissingBitfieldMasks, 37 }; 38 39 pub const BitmapInfo = struct { 40 dib_header_size: u32, 41 /// Contains the interpreted number of colors in the palette (e.g. 42 /// if the field's value is zero and the bit depth is <= 8, this 43 /// will contain the maximum number of colors for the bit depth 44 /// rather than the field's value directly). 45 colors_in_palette: u32, 46 bytes_per_color_palette_element: u8, 47 pixel_data_offset: u32, 48 compression: Compression, 49 50 pub fn getExpectedPaletteByteLen(self: *const BitmapInfo) u64 { 51 return @as(u64, self.colors_in_palette) * self.bytes_per_color_palette_element; 52 } 53 54 pub fn getActualPaletteByteLen(self: *const BitmapInfo) u64 { 55 return self.getByteLenBetweenHeadersAndPixels() - self.getBitmasksByteLen(); 56 } 57 58 pub fn getByteLenBetweenHeadersAndPixels(self: *const BitmapInfo) u64 { 59 return @as(u64, self.pixel_data_offset) - self.dib_header_size - file_header_len; 60 } 61 62 pub fn getBitmasksByteLen(self: *const BitmapInfo) u8 { 63 // Only BITMAPINFOHEADER (3.1) has trailing bytes for the BITFIELDS 64 // The 2.0 format doesn't have a compression field and 4.0+ has dedicated 65 // fields for the masks in the header. 66 const dib_version = BitmapHeader.Version.get(self.dib_header_size); 67 return switch (dib_version) { 68 .@"nt3.1" => switch (self.compression) { 69 .BI_BITFIELDS => 12, 70 .BI_ALPHABITFIELDS => 16, 71 else => 0, 72 }, 73 else => 0, 74 }; 75 } 76 77 pub fn getMissingPaletteByteLen(self: *const BitmapInfo) u64 { 78 if (self.getActualPaletteByteLen() >= self.getExpectedPaletteByteLen()) return 0; 79 return self.getExpectedPaletteByteLen() - self.getActualPaletteByteLen(); 80 } 81 82 /// Returns the full byte len of the DIB header + optional bitmasks + color palette 83 pub fn getExpectedByteLenBeforePixelData(self: *const BitmapInfo) u64 { 84 return @as(u64, self.dib_header_size) + self.getBitmasksByteLen() + self.getExpectedPaletteByteLen(); 85 } 86 87 /// Returns the full expected byte len 88 pub fn getExpectedByteLen(self: *const BitmapInfo, file_size: u64) u64 { 89 return self.getExpectedByteLenBeforePixelData() + self.getPixelDataLen(file_size); 90 } 91 92 pub fn getPixelDataLen(self: *const BitmapInfo, file_size: u64) u64 { 93 return file_size - self.pixel_data_offset; 94 } 95 }; 96 97 pub fn read(reader: anytype, max_size: u64) ReadError!BitmapInfo { 98 var bitmap_info: BitmapInfo = undefined; 99 const file_header = reader.readBytesNoEof(file_header_len) catch return error.UnexpectedEOF; 100 101 const id = std.mem.readInt(u16, file_header[0..2], native_endian); 102 if (id != windows_format_id) return error.InvalidFileHeader; 103 104 bitmap_info.pixel_data_offset = std.mem.readInt(u32, file_header[10..14], .little); 105 if (bitmap_info.pixel_data_offset > max_size) return error.ImpossiblePixelDataOffset; 106 107 bitmap_info.dib_header_size = reader.readInt(u32, .little) catch return error.UnexpectedEOF; 108 if (bitmap_info.pixel_data_offset < file_header_len + bitmap_info.dib_header_size) return error.ImpossiblePixelDataOffset; 109 const dib_version = BitmapHeader.Version.get(bitmap_info.dib_header_size); 110 switch (dib_version) { 111 .@"nt3.1", .@"nt4.0", .@"nt5.0" => { 112 var dib_header_buf: [@sizeOf(BITMAPINFOHEADER)]u8 align(@alignOf(BITMAPINFOHEADER)) = undefined; 113 std.mem.writeInt(u32, dib_header_buf[0..4], bitmap_info.dib_header_size, .little); 114 reader.readNoEof(dib_header_buf[4..]) catch return error.UnexpectedEOF; 115 var dib_header: *BITMAPINFOHEADER = @ptrCast(&dib_header_buf); 116 structFieldsLittleToNative(BITMAPINFOHEADER, dib_header); 117 118 bitmap_info.colors_in_palette = try dib_header.numColorsInTable(); 119 bitmap_info.bytes_per_color_palette_element = 4; 120 bitmap_info.compression = @enumFromInt(dib_header.biCompression); 121 122 if (bitmap_info.getByteLenBetweenHeadersAndPixels() < bitmap_info.getBitmasksByteLen()) { 123 return error.MissingBitfieldMasks; 124 } 125 }, 126 .@"win2.0" => { 127 var dib_header_buf: [@sizeOf(BITMAPCOREHEADER)]u8 align(@alignOf(BITMAPCOREHEADER)) = undefined; 128 std.mem.writeInt(u32, dib_header_buf[0..4], bitmap_info.dib_header_size, .little); 129 reader.readNoEof(dib_header_buf[4..]) catch return error.UnexpectedEOF; 130 const dib_header: *BITMAPCOREHEADER = @ptrCast(&dib_header_buf); 131 structFieldsLittleToNative(BITMAPCOREHEADER, dib_header); 132 133 // > The size of the color palette is calculated from the BitsPerPixel value. 134 // > The color palette has 2, 16, 256, or 0 entries for a BitsPerPixel of 135 // > 1, 4, 8, and 24, respectively. 136 bitmap_info.colors_in_palette = switch (dib_header.bcBitCount) { 137 inline 1, 4, 8 => |bit_count| 1 << bit_count, 138 24 => 0, 139 else => return error.InvalidBitsPerPixel, 140 }; 141 bitmap_info.bytes_per_color_palette_element = 3; 142 143 bitmap_info.compression = .BI_RGB; 144 }, 145 .unknown => return error.UnknownBitmapVersion, 146 } 147 148 return bitmap_info; 149 } 150 151 /// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapcoreheader 152 pub const BITMAPCOREHEADER = extern struct { 153 bcSize: u32, 154 bcWidth: u16, 155 bcHeight: u16, 156 bcPlanes: u16, 157 bcBitCount: u16, 158 }; 159 160 /// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader 161 pub const BITMAPINFOHEADER = extern struct { 162 bcSize: u32, 163 biWidth: i32, 164 biHeight: i32, 165 biPlanes: u16, 166 biBitCount: u16, 167 biCompression: u32, 168 biSizeImage: u32, 169 biXPelsPerMeter: i32, 170 biYPelsPerMeter: i32, 171 biClrUsed: u32, 172 biClrImportant: u32, 173 174 /// Returns error.TooManyColorsInPalette if the number of colors specified 175 /// exceeds the number of possible colors referenced in the pixel data (i.e. 176 /// if 1 bit is used per pixel, then the color table can't have more than 2 colors 177 /// since any more couldn't possibly be indexed in the pixel data) 178 /// 179 /// Returns error.InvalidBitsPerPixel if the bit depth is not 1, 4, 8, 16, 24, or 32. 180 pub fn numColorsInTable(self: BITMAPINFOHEADER) !u32 { 181 switch (self.biBitCount) { 182 inline 1, 4, 8 => |bit_count| switch (self.biClrUsed) { 183 // > If biClrUsed is zero, the array contains the maximum number of 184 // > colors for the given bitdepth; that is, 2^biBitCount colors 185 0 => return 1 << bit_count, 186 // > If biClrUsed is nonzero and the biBitCount member is less than 16, 187 // > the biClrUsed member specifies the actual number of colors the 188 // > graphics engine or device driver accesses. 189 else => { 190 const max_colors = 1 << bit_count; 191 if (self.biClrUsed > max_colors) { 192 return error.TooManyColorsInPalette; 193 } 194 return self.biClrUsed; 195 }, 196 }, 197 // > If biBitCount is 16 or greater, the biClrUsed member specifies 198 // > the size of the color table used to optimize performance of the 199 // > system color palettes. 200 // 201 // Note: Bit depths >= 16 only use the color table 'for optimizing colors 202 // used on palette-based devices', but it still makes sense to limit their 203 // colors since the pixel data is still limited to this number of colors 204 // (i.e. even though the color table is not indexed by the pixel data, 205 // the color table having more colors than the pixel data can represent 206 // would never make sense and indicates a malformed bitmap). 207 inline 16, 24, 32 => |bit_count| { 208 const max_colors = 1 << bit_count; 209 if (self.biClrUsed > max_colors) { 210 return error.TooManyColorsInPalette; 211 } 212 return self.biClrUsed; 213 }, 214 else => return error.InvalidBitsPerPixel, 215 } 216 } 217 }; 218 219 pub const Compression = enum(u32) { 220 BI_RGB = 0, 221 BI_RLE8 = 1, 222 BI_RLE4 = 2, 223 BI_BITFIELDS = 3, 224 BI_JPEG = 4, 225 BI_PNG = 5, 226 BI_ALPHABITFIELDS = 6, 227 BI_CMYK = 11, 228 BI_CMYKRLE8 = 12, 229 BI_CMYKRLE4 = 13, 230 _, 231 }; 232 233 fn structFieldsLittleToNative(comptime T: type, x: *T) void { 234 inline for (@typeInfo(T).@"struct".fields) |field| { 235 @field(x, field.name) = std.mem.littleToNative(field.type, @field(x, field.name)); 236 } 237 } 238 239 test "read" { 240 var bmp_data = "BM<\x00\x00\x00\x00\x00\x00\x006\x00\x00\x00(\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x10\x00\x00\x00\x00\x00\x06\x00\x00\x00\x12\x0b\x00\x00\x12\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x7f\x00\x00\x00\x00".*; 241 var fbs = std.io.fixedBufferStream(&bmp_data); 242 243 { 244 const bitmap = try read(fbs.reader(), bmp_data.len); 245 try std.testing.expectEqual(@as(u32, BitmapHeader.Version.@"nt3.1".len()), bitmap.dib_header_size); 246 } 247 248 { 249 fbs.reset(); 250 bmp_data[file_header_len] = 11; 251 try std.testing.expectError(error.UnknownBitmapVersion, read(fbs.reader(), bmp_data.len)); 252 253 // restore 254 bmp_data[file_header_len] = BitmapHeader.Version.@"nt3.1".len(); 255 } 256 257 { 258 fbs.reset(); 259 bmp_data[0] = 'b'; 260 try std.testing.expectError(error.InvalidFileHeader, read(fbs.reader(), bmp_data.len)); 261 262 // restore 263 bmp_data[0] = 'B'; 264 } 265 266 { 267 const cutoff_len = file_header_len + BitmapHeader.Version.@"nt3.1".len() - 1; 268 var dib_cutoff_fbs = std.io.fixedBufferStream(bmp_data[0..cutoff_len]); 269 try std.testing.expectError(error.UnexpectedEOF, read(dib_cutoff_fbs.reader(), bmp_data.len)); 270 } 271 272 { 273 const cutoff_len = file_header_len - 1; 274 var bmp_cutoff_fbs = std.io.fixedBufferStream(bmp_data[0..cutoff_len]); 275 try std.testing.expectError(error.UnexpectedEOF, read(bmp_cutoff_fbs.reader(), bmp_data.len)); 276 } 277 }