master
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
21const std = @import("std");
22const BitmapHeader = @import("ico.zig").BitmapHeader;
23const builtin = @import("builtin");
24const native_endian = builtin.cpu.arch.endian();
25
26pub const windows_format_id = std.mem.readInt(u16, "BM", native_endian);
27pub const file_header_len = 14;
28
29pub const ReadError = error{
30 ReadFailed,
31 UnexpectedEOF,
32 InvalidFileHeader,
33 ImpossiblePixelDataOffset,
34 UnknownBitmapVersion,
35 InvalidBitsPerPixel,
36 TooManyColorsInPalette,
37 MissingBitfieldMasks,
38};
39
40pub 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
98pub 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 = @enumFromInt(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
162pub 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
171pub 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
229pub 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
243fn structFieldsLittleToNative(comptime T: type, x: *T) void {
244 inline for (@typeInfo(T).@"struct".fields) |field| {
245 @field(x, field.name) = std.mem.littleToNative(field.type, @field(x, field.name));
246 }
247}
248
249test "read" {
250 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".*;
251 var fbs: std.Io.Reader = .fixed(&bmp_data);
252
253 {
254 const bitmap = try read(&fbs, bmp_data.len);
255 try std.testing.expectEqual(@as(u32, BitmapHeader.Version.@"nt3.1".len()), bitmap.dib_header_size);
256 }
257
258 {
259 fbs.seek = 0;
260 bmp_data[file_header_len] = 11;
261 try std.testing.expectError(error.UnknownBitmapVersion, read(&fbs, bmp_data.len));
262
263 // restore
264 bmp_data[file_header_len] = BitmapHeader.Version.@"nt3.1".len();
265 }
266
267 {
268 fbs.seek = 0;
269 bmp_data[0] = 'b';
270 try std.testing.expectError(error.InvalidFileHeader, read(&fbs, bmp_data.len));
271
272 // restore
273 bmp_data[0] = 'B';
274 }
275
276 {
277 const cutoff_len = file_header_len + BitmapHeader.Version.@"nt3.1".len() - 1;
278 var dib_cutoff_fbs: std.Io.Reader = .fixed(bmp_data[0..cutoff_len]);
279 try std.testing.expectError(error.UnexpectedEOF, read(&dib_cutoff_fbs, bmp_data.len));
280 }
281
282 {
283 const cutoff_len = file_header_len - 1;
284 var bmp_cutoff_fbs: std.Io.Reader = .fixed(bmp_data[0..cutoff_len]);
285 try std.testing.expectError(error.UnexpectedEOF, read(&bmp_cutoff_fbs, bmp_data.len));
286 }
287}