zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

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 }