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