zig/lib/compiler/resinator/bmp.zig

287 lines
12 KiB
Zig
Vendored

//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
//! https://learn.microsoft.com/en-us/previous-versions//dd183376(v=vs.85)
//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfo
//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapcoreheader
//! https://archive.org/details/mac_Graphics_File_Formats_Second_Edition_1996/page/n607/mode/2up
//! https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header
//!
//! Notes:
//! - The Microsoft documentation is incredibly unclear about the color table when the
//! bit depth is >= 16.
//! + For bit depth 24 it says "the bmiColors member of BITMAPINFO is NULL" but also
//! says "the bmiColors color table is used for optimizing colors used on palette-based
//! devices, and must contain the number of entries specified by the bV5ClrUsed member"
//! + For bit depth 16 and 32, it seems to imply that if the compression is BI_BITFIELDS
//! or BI_ALPHABITFIELDS, then the color table *only* consists of the bit masks, but
//! doesn't really say this outright and the Wikipedia article seems to disagree
//! For the purposes of this implementation, color tables can always be present for any
//! bit depth and compression, and the color table follows the header + any optional
//! bit mask fields dictated by the specified compression.
const std = @import("std");
const BitmapHeader = @import("ico.zig").BitmapHeader;
const builtin = @import("builtin");
const native_endian = builtin.cpu.arch.endian();
pub const windows_format_id = std.mem.readInt(u16, "BM", native_endian);
pub const file_header_len = 14;
pub const ReadError = error{
ReadFailed,
UnexpectedEOF,
InvalidFileHeader,
ImpossiblePixelDataOffset,
UnknownBitmapVersion,
InvalidBitsPerPixel,
TooManyColorsInPalette,
MissingBitfieldMasks,
};
pub const BitmapInfo = struct {
dib_header_size: u32,
/// Contains the interpreted number of colors in the palette (e.g.
/// if the field's value is zero and the bit depth is <= 8, this
/// will contain the maximum number of colors for the bit depth
/// rather than the field's value directly).
colors_in_palette: u32,
bytes_per_color_palette_element: u8,
pixel_data_offset: u32,
compression: Compression,
pub fn getExpectedPaletteByteLen(self: *const BitmapInfo) u64 {
return @as(u64, self.colors_in_palette) * self.bytes_per_color_palette_element;
}
pub fn getActualPaletteByteLen(self: *const BitmapInfo) u64 {
return self.getByteLenBetweenHeadersAndPixels() - self.getBitmasksByteLen();
}
pub fn getByteLenBetweenHeadersAndPixels(self: *const BitmapInfo) u64 {
return @as(u64, self.pixel_data_offset) - self.dib_header_size - file_header_len;
}
pub fn getBitmasksByteLen(self: *const BitmapInfo) u8 {
// Only BITMAPINFOHEADER (3.1) has trailing bytes for the BITFIELDS
// The 2.0 format doesn't have a compression field and 4.0+ has dedicated
// fields for the masks in the header.
const dib_version = BitmapHeader.Version.get(self.dib_header_size);
return switch (dib_version) {
.@"nt3.1" => switch (self.compression) {
.BI_BITFIELDS => 12,
.BI_ALPHABITFIELDS => 16,
else => 0,
},
else => 0,
};
}
pub fn getMissingPaletteByteLen(self: *const BitmapInfo) u64 {
if (self.getActualPaletteByteLen() >= self.getExpectedPaletteByteLen()) return 0;
return self.getExpectedPaletteByteLen() - self.getActualPaletteByteLen();
}
/// Returns the full byte len of the DIB header + optional bitmasks + color palette
pub fn getExpectedByteLenBeforePixelData(self: *const BitmapInfo) u64 {
return @as(u64, self.dib_header_size) + self.getBitmasksByteLen() + self.getExpectedPaletteByteLen();
}
/// Returns the full expected byte len
pub fn getExpectedByteLen(self: *const BitmapInfo, file_size: u64) u64 {
return self.getExpectedByteLenBeforePixelData() + self.getPixelDataLen(file_size);
}
pub fn getPixelDataLen(self: *const BitmapInfo, file_size: u64) u64 {
return file_size - self.pixel_data_offset;
}
};
pub fn read(reader: *std.Io.Reader, max_size: u64) ReadError!BitmapInfo {
var bitmap_info: BitmapInfo = undefined;
const file_header = reader.takeArray(file_header_len) catch |err| switch (err) {
error.EndOfStream => return error.UnexpectedEOF,
else => |e| return e,
};
const id = std.mem.readInt(u16, file_header[0..2], native_endian);
if (id != windows_format_id) return error.InvalidFileHeader;
bitmap_info.pixel_data_offset = std.mem.readInt(u32, file_header[10..14], .little);
if (bitmap_info.pixel_data_offset > max_size) return error.ImpossiblePixelDataOffset;
bitmap_info.dib_header_size = reader.takeInt(u32, .little) catch return error.UnexpectedEOF;
if (bitmap_info.pixel_data_offset < file_header_len + bitmap_info.dib_header_size) return error.ImpossiblePixelDataOffset;
const dib_version = BitmapHeader.Version.get(bitmap_info.dib_header_size);
switch (dib_version) {
.@"nt3.1", .@"nt4.0", .@"nt5.0" => {
var dib_header_buf: [@sizeOf(BITMAPINFOHEADER)]u8 align(@alignOf(BITMAPINFOHEADER)) = undefined;
std.mem.writeInt(u32, dib_header_buf[0..4], bitmap_info.dib_header_size, .little);
reader.readSliceAll(dib_header_buf[4..]) catch |err| switch (err) {
error.EndOfStream => return error.UnexpectedEOF,
error.ReadFailed => |e| return e,
};
var dib_header: *BITMAPINFOHEADER = @ptrCast(&dib_header_buf);
structFieldsLittleToNative(BITMAPINFOHEADER, dib_header);
bitmap_info.colors_in_palette = try dib_header.numColorsInTable();
bitmap_info.bytes_per_color_palette_element = 4;
bitmap_info.compression = @enumFromInt(dib_header.biCompression);
if (bitmap_info.getByteLenBetweenHeadersAndPixels() < bitmap_info.getBitmasksByteLen()) {
return error.MissingBitfieldMasks;
}
},
.@"win2.0" => {
var dib_header_buf: [@sizeOf(BITMAPCOREHEADER)]u8 align(@alignOf(BITMAPCOREHEADER)) = undefined;
std.mem.writeInt(u32, dib_header_buf[0..4], bitmap_info.dib_header_size, .little);
reader.readSliceAll(dib_header_buf[4..]) catch |err| switch (err) {
error.EndOfStream => return error.UnexpectedEOF,
error.ReadFailed => |e| return e,
};
const dib_header: *BITMAPCOREHEADER = @ptrCast(&dib_header_buf);
structFieldsLittleToNative(BITMAPCOREHEADER, dib_header);
// > The size of the color palette is calculated from the BitsPerPixel value.
// > The color palette has 2, 16, 256, or 0 entries for a BitsPerPixel of
// > 1, 4, 8, and 24, respectively.
bitmap_info.colors_in_palette = switch (dib_header.bcBitCount) {
inline 1, 4, 8 => |bit_count| 1 << bit_count,
24 => 0,
else => return error.InvalidBitsPerPixel,
};
bitmap_info.bytes_per_color_palette_element = 3;
bitmap_info.compression = .BI_RGB;
},
.unknown => return error.UnknownBitmapVersion,
}
return bitmap_info;
}
/// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapcoreheader
pub const BITMAPCOREHEADER = extern struct {
bcSize: u32,
bcWidth: u16,
bcHeight: u16,
bcPlanes: u16,
bcBitCount: u16,
};
/// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
pub const BITMAPINFOHEADER = extern struct {
bcSize: u32,
biWidth: i32,
biHeight: i32,
biPlanes: u16,
biBitCount: u16,
biCompression: u32,
biSizeImage: u32,
biXPelsPerMeter: i32,
biYPelsPerMeter: i32,
biClrUsed: u32,
biClrImportant: u32,
/// Returns error.TooManyColorsInPalette if the number of colors specified
/// exceeds the number of possible colors referenced in the pixel data (i.e.
/// if 1 bit is used per pixel, then the color table can't have more than 2 colors
/// since any more couldn't possibly be indexed in the pixel data)
///
/// Returns error.InvalidBitsPerPixel if the bit depth is not 1, 4, 8, 16, 24, or 32.
pub fn numColorsInTable(self: BITMAPINFOHEADER) !u32 {
switch (self.biBitCount) {
inline 1, 4, 8 => |bit_count| switch (self.biClrUsed) {
// > If biClrUsed is zero, the array contains the maximum number of
// > colors for the given bitdepth; that is, 2^biBitCount colors
0 => return 1 << bit_count,
// > If biClrUsed is nonzero and the biBitCount member is less than 16,
// > the biClrUsed member specifies the actual number of colors the
// > graphics engine or device driver accesses.
else => {
const max_colors = 1 << bit_count;
if (self.biClrUsed > max_colors) {
return error.TooManyColorsInPalette;
}
return self.biClrUsed;
},
},
// > If biBitCount is 16 or greater, the biClrUsed member specifies
// > the size of the color table used to optimize performance of the
// > system color palettes.
//
// Note: Bit depths >= 16 only use the color table 'for optimizing colors
// used on palette-based devices', but it still makes sense to limit their
// colors since the pixel data is still limited to this number of colors
// (i.e. even though the color table is not indexed by the pixel data,
// the color table having more colors than the pixel data can represent
// would never make sense and indicates a malformed bitmap).
inline 16, 24, 32 => |bit_count| {
const max_colors = 1 << bit_count;
if (self.biClrUsed > max_colors) {
return error.TooManyColorsInPalette;
}
return self.biClrUsed;
},
else => return error.InvalidBitsPerPixel,
}
}
};
pub const Compression = enum(u32) {
BI_RGB = 0,
BI_RLE8 = 1,
BI_RLE4 = 2,
BI_BITFIELDS = 3,
BI_JPEG = 4,
BI_PNG = 5,
BI_ALPHABITFIELDS = 6,
BI_CMYK = 11,
BI_CMYKRLE8 = 12,
BI_CMYKRLE4 = 13,
_,
};
fn structFieldsLittleToNative(comptime T: type, x: *T) void {
inline for (@typeInfo(T).@"struct".fields) |field| {
@field(x, field.name) = std.mem.littleToNative(field.type, @field(x, field.name));
}
}
test "read" {
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".*;
var fbs: std.Io.Reader = .fixed(&bmp_data);
{
const bitmap = try read(&fbs, bmp_data.len);
try std.testing.expectEqual(@as(u32, BitmapHeader.Version.@"nt3.1".len()), bitmap.dib_header_size);
}
{
fbs.seek = 0;
bmp_data[file_header_len] = 11;
try std.testing.expectError(error.UnknownBitmapVersion, read(&fbs, bmp_data.len));
// restore
bmp_data[file_header_len] = BitmapHeader.Version.@"nt3.1".len();
}
{
fbs.seek = 0;
bmp_data[0] = 'b';
try std.testing.expectError(error.InvalidFileHeader, read(&fbs, bmp_data.len));
// restore
bmp_data[0] = 'B';
}
{
const cutoff_len = file_header_len + BitmapHeader.Version.@"nt3.1".len() - 1;
var dib_cutoff_fbs: std.Io.Reader = .fixed(bmp_data[0..cutoff_len]);
try std.testing.expectError(error.UnexpectedEOF, read(&dib_cutoff_fbs, bmp_data.len));
}
{
const cutoff_len = file_header_len - 1;
var bmp_cutoff_fbs: std.Io.Reader = .fixed(bmp_data[0..cutoff_len]);
try std.testing.expectError(error.UnexpectedEOF, read(&bmp_cutoff_fbs, bmp_data.len));
}
}