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}