media: png chunk structs and decoders
This commit is contained in:
@@ -2,7 +2,7 @@ const std = @import("std");
|
||||
|
||||
const Format = @import("Format.zig");
|
||||
|
||||
const format: Format = .{
|
||||
pub const format: Format = .{
|
||||
.magic_length = magic.len,
|
||||
// 4B - chunk length
|
||||
// 4B - chunk type
|
||||
@@ -16,15 +16,84 @@ const format: Format = .{
|
||||
|
||||
const magic = "\x89PNG\r\n\x1A\n";
|
||||
|
||||
const BitDepth = enum(u8) {
|
||||
// --- IHDR --------------------------------------------------------------------
|
||||
|
||||
pub const Header = struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
bit_depth: BitDepth,
|
||||
color_type: ColorType,
|
||||
compression_method: CompressionMethod,
|
||||
filter_method: FilterMethod,
|
||||
interlace_method: InterlaceMethod,
|
||||
|
||||
pub fn decode(chunk: Chunk) Header {
|
||||
std.debug.assert(chunk.chunk_type == .IHDR);
|
||||
|
||||
if (chunk.data.len != 13) return error.InvalidPng;
|
||||
const width = std.mem.readInt(u32, chunk.data[0..4], .big);
|
||||
const height = std.mem.readInt(u32, chunk.data[4..8], .big);
|
||||
const bit_depth = chunk.data[8];
|
||||
const color_type = chunk.data[9];
|
||||
const compression_method = chunk.data[10];
|
||||
const filter_method = chunk.data[11];
|
||||
const interlace_method = chunk.data[12];
|
||||
|
||||
if (width == 0 or width > 0x7FFF_FFFF or
|
||||
height == 0 or height > 0x7FFF_FFFF or
|
||||
bit_depth == 0 or bit_depth > 16 or !std.math.isPowerOfTwo(bit_depth) or
|
||||
color_type == 1 or color_type == 5 or color_type > 6 or
|
||||
compression_method != 0 or
|
||||
filter_method != 0 or
|
||||
interlace_method > 1)
|
||||
{
|
||||
return error.InvalidPng;
|
||||
}
|
||||
|
||||
switch (color_type) {
|
||||
0 => {
|
||||
// all bit depths allowed
|
||||
},
|
||||
2 => {
|
||||
if (bit_depth < 8) return error.InvalidPng;
|
||||
},
|
||||
3 => {
|
||||
if (bit_depth > 8) return error.InvalidPng;
|
||||
},
|
||||
4 => {
|
||||
if (bit_depth < 8) return error.InvalidPng;
|
||||
},
|
||||
6 => {
|
||||
if (bit_depth < 8) return error.InvalidPng;
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
return .{
|
||||
.width = width,
|
||||
.height = height,
|
||||
.bit_depth = @enumFromInt(bit_depth),
|
||||
.color_type = @enumFromInt(color_type),
|
||||
.compression_method = @enumFromInt(compression_method),
|
||||
.filter_method = @enumFromInt(filter_method),
|
||||
.interlace_method = @enumFromInt(interlace_method),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const BitDepth = enum(u8) {
|
||||
@"1" = 1,
|
||||
@"2" = 2,
|
||||
@"4" = 4,
|
||||
@"8" = 8,
|
||||
@"16" = 16,
|
||||
|
||||
pub fn range(self: BitDepth) usize {
|
||||
return @as(usize, 2) << @intFromEnum(@intFromEnum(self));
|
||||
}
|
||||
};
|
||||
|
||||
const ColorType = enum(u8) {
|
||||
pub const ColorType = enum(u8) {
|
||||
grayscale = 0,
|
||||
rgb = 2,
|
||||
palette = 3,
|
||||
@@ -44,30 +113,279 @@ const ColorType = enum(u8) {
|
||||
}
|
||||
};
|
||||
|
||||
const CompressionMethod = enum(u8) {
|
||||
pub const CompressionMethod = enum(u8) {
|
||||
flate = 0,
|
||||
};
|
||||
|
||||
const FilterMethod = enum(u8) {
|
||||
pub const FilterMethod = enum(u8) {
|
||||
adaptive = 0,
|
||||
};
|
||||
|
||||
const InterlaceMethod = enum(u8) {
|
||||
pub const InterlaceMethod = enum(u8) {
|
||||
none = 0,
|
||||
adam7 = 1,
|
||||
};
|
||||
|
||||
const Header = struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
bit_depth: BitDepth,
|
||||
color_type: ColorType,
|
||||
compression_method: CompressionMethod,
|
||||
filter_method: FilterMethod,
|
||||
interlace_method: InterlaceMethod,
|
||||
// --- PLTE --------------------------------------------------------------------
|
||||
|
||||
pub const Palette = struct {
|
||||
len: u16,
|
||||
entries: [256][3]u8,
|
||||
|
||||
pub fn asSlice(self: *const Palette) []const [3]u8 {
|
||||
return self.entries[0..self.len];
|
||||
}
|
||||
|
||||
pub fn decode(chunk: Chunk, ctx: Header) !Palette {
|
||||
std.debug.assert(chunk.chunk_type == .PLTE);
|
||||
|
||||
const len = std.math.divExact(usize, chunk.data.len, 3) catch return error.InvalidPng;
|
||||
|
||||
switch (ctx.color_type) {
|
||||
.grayscale => return error.InvalidPng,
|
||||
.rgb => if (len < 1 or len > 256) return error.InvalidPng,
|
||||
.palette => if (len < 1 or len > ctx.bit_depth.range()) return error.InvalidPng,
|
||||
.grayscale_alpha => return error.InvalidPng,
|
||||
.rgba => if (len < 1 or len > 256) return error.InvalidPng,
|
||||
}
|
||||
|
||||
var entries: [256][3]u8 = undefined;
|
||||
@memcpy(entries[0..len], @as([]const [3]u8, @ptrCast(chunk.data)));
|
||||
return .{
|
||||
.len = @intCast(len),
|
||||
.entries = entries,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const ChunkType = enum(u32) {
|
||||
// --- tRNS --------------------------------------------------------------------
|
||||
|
||||
pub const Transparency = union(enum) {
|
||||
grayscale: u16,
|
||||
rgb: [3]u16,
|
||||
palette: TransparencyPalette,
|
||||
|
||||
pub fn initGrayscale(grayscale: u16) Transparency {
|
||||
return .{ .grayscale = grayscale };
|
||||
}
|
||||
|
||||
pub fn initRgb(rgb: [3]u16) Transparency {
|
||||
return .{ .rgb = rgb };
|
||||
}
|
||||
|
||||
pub fn initPalette(palette: TransparencyPalette) Transparency {
|
||||
return .{ .palette = palette };
|
||||
}
|
||||
|
||||
pub fn decode(chunk: Chunk, header: Header, maybe_palette: ?Palette) !Transparency {
|
||||
std.debug.assert(chunk.chunk_type == .tRNS);
|
||||
|
||||
return switch (header.color_type) {
|
||||
.grayscale => {
|
||||
if (chunk.data.len != 2) return error.InvalidPng;
|
||||
const grayscale = std.mem.readInt(u16, chunk.data[0..2], .big);
|
||||
return .initGrayscale(grayscale);
|
||||
},
|
||||
.rgb => {
|
||||
if (chunk.data.len != 6) return error.InvalidPng;
|
||||
const r = std.mem.readInt(u16, chunk.data[0..2], .big);
|
||||
const g = std.mem.readInt(u16, chunk.data[2..4], .big);
|
||||
const b = std.mem.readInt(u16, chunk.data[4..6], .big);
|
||||
return .initRgb(.{ r, g, b });
|
||||
},
|
||||
.palette => if (maybe_palette) |palette| {
|
||||
const len = chunk.data.len;
|
||||
if (len < 1 or len > palette.len) return error.InvalidPng;
|
||||
var entries: [256]u8 = undefined;
|
||||
@memcpy(entries[0..len], chunk.data[0..len]);
|
||||
return .initPalette(.{
|
||||
.len = @intCast(len),
|
||||
.entries = entries,
|
||||
});
|
||||
} else {
|
||||
return error.InvalidPng;
|
||||
},
|
||||
.grayscale_alpha => error.InvalidPng,
|
||||
.rgba => error.InvalidPng,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const TransparencyPalette = struct {
|
||||
len: u16,
|
||||
entries: [256]u8,
|
||||
|
||||
pub fn asSlice(self: *const TransparencyPalette) []const u8 {
|
||||
return self.entries[0..self.len];
|
||||
}
|
||||
};
|
||||
|
||||
// --- gAMA --------------------------------------------------------------------
|
||||
|
||||
const Gamma = struct {
|
||||
/// Gamma times `scale`, i.e. 100000, For example, a gamma of 1/2.2 would be
|
||||
/// stored as 45455.
|
||||
gamma: u32,
|
||||
|
||||
const scale = 100000;
|
||||
|
||||
pub fn initExponent(exponent: f32) Gamma {
|
||||
const gamma: u32 = @intFromFloat(@round(exponent * scale));
|
||||
return .{ .gamma = gamma };
|
||||
}
|
||||
|
||||
pub fn asExponent(self: Gamma) f32 {
|
||||
return @as(f32, @floatFromInt(self.gamma)) / scale;
|
||||
}
|
||||
|
||||
pub fn decode(chunk: Chunk) Gamma {
|
||||
std.debug.assert(chunk.chunk_type == .gAMA);
|
||||
|
||||
if (chunk.data.len != 4) return error.InvalidPng;
|
||||
const gamma = std.mem.readInt(u32, chunk.data[0..4], .big);
|
||||
return .{ .gamma = gamma };
|
||||
}
|
||||
};
|
||||
|
||||
// --- cHRM --------------------------------------------------------------------
|
||||
|
||||
const Chromaticities = struct {
|
||||
white_point_x: u32,
|
||||
white_point_y: u32,
|
||||
red_x: u32,
|
||||
red_y: u32,
|
||||
green_x: u32,
|
||||
green_y: u32,
|
||||
blue_x: u32,
|
||||
blue_y: u32,
|
||||
|
||||
pub fn decode(chunk: Chunk) !Chunk {
|
||||
std.debug.assert(chunk.chunk_type == .cHRM);
|
||||
|
||||
if (chunk.data.len != 32) return error.InvalidPng;
|
||||
return .{
|
||||
.white_point_x = std.mem.readInt(u32, chunk.data[0..4], .big),
|
||||
.white_point_y = std.mem.readInt(u32, chunk.data[4..8], .big),
|
||||
.red_x = std.mem.readInt(u32, chunk.data[8..12], .big),
|
||||
.red_y = std.mem.readInt(u32, chunk.data[12..16], .big),
|
||||
.green_x = std.mem.readInt(u32, chunk.data[16..20], .big),
|
||||
.green_y = std.mem.readInt(u32, chunk.data[20..24], .big),
|
||||
.blue_x = std.mem.readInt(u32, chunk.data[24..28], .big),
|
||||
.blue_y = std.mem.readInt(u32, chunk.data[28..32], .big),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// --- sRGB --------------------------------------------------------------------
|
||||
|
||||
const RenderingIntent = enum(u8) {
|
||||
/// Perceptual intent is for images preferring good adaptation to the output
|
||||
/// device gamut at the expense of colorimetric accuracy, like photographs.
|
||||
perceptual = 0,
|
||||
/// Relative colorimetric intent is for images requiring color appearance
|
||||
/// matching (relative to the output device white point), like logos.
|
||||
relative_colorimetric = 1,
|
||||
/// Saturation intent is for images preferring preservation of saturation at
|
||||
/// the expense of hue and lightness, like charts and graphs.
|
||||
saturation = 2,
|
||||
/// Absolute colorimetric intent is for images requiring preservation of
|
||||
/// absolute colorimetry, like proofs (previews of images destined for a
|
||||
/// different output device).
|
||||
absolute_colorimetric = 3,
|
||||
|
||||
pub fn decode(chunk: Chunk) RenderingIntent {
|
||||
std.debug.assert(chunk.chunk_type == .sRGB);
|
||||
|
||||
if (chunk.data.len != 1) return error.InvalidPng;
|
||||
const rendering_intent = chunk.data[0];
|
||||
if (rendering_intent > 3) return error.InvalidPng;
|
||||
return @enumFromInt(rendering_intent);
|
||||
}
|
||||
};
|
||||
|
||||
// --- pHYs --------------------------------------------------------------------
|
||||
|
||||
pub const PhysicalPixelDimensions = struct {
|
||||
pixels_per_unit_x: u32,
|
||||
pixels_per_unit_y: u32,
|
||||
unit: PhysicalUnit,
|
||||
|
||||
pub fn decode(chunk: Chunk) PhysicalPixelDimensions {
|
||||
std.debug.assert(chunk.chunk_type == .pHYs);
|
||||
|
||||
if (chunk.data.len != 9) return error.InvalidPng;
|
||||
const pixels_per_unit_x = std.mem.readInt(u32, chunk.data[0..4], .big);
|
||||
const pixels_per_unit_y = std.mem.readInt(u32, chunk.data[4..8], .big);
|
||||
const unit = chunk.data[8];
|
||||
|
||||
if (pixels_per_unit_x == 0 or pixels_per_unit_y == 0 or unit > 1) {
|
||||
return error.InvalidPng;
|
||||
}
|
||||
|
||||
return .{
|
||||
.pixels_per_unit_x = pixels_per_unit_x,
|
||||
.pixels_per_unit_y = pixels_per_unit_y,
|
||||
.unit = @enumFromInt(unit),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const PhysicalUnit = enum(u8) {
|
||||
unknown = 0,
|
||||
meter = 1,
|
||||
};
|
||||
|
||||
// --- tIME --------------------------------------------------------------------
|
||||
|
||||
pub const LastModificationTime = struct {
|
||||
year: u16,
|
||||
/// 1 - 12
|
||||
month: u8,
|
||||
/// 1 - 31
|
||||
day: u8,
|
||||
/// 0 - 23
|
||||
hour: u8,
|
||||
/// 0 - 59
|
||||
minute: u8,
|
||||
/// 0 - 60 (may include leap seconds)
|
||||
second: u8,
|
||||
|
||||
pub fn decode(chunk: Chunk) !LastModificationTime {
|
||||
std.debug.assert(chunk.chunk_type == .tIME);
|
||||
|
||||
if (chunk.data.len == 7) return error.InvalidPng;
|
||||
const year = std.mem.readInt(u16, chunk.data[0..2], .big);
|
||||
const month = chunk.data[2];
|
||||
const day = chunk.data[3];
|
||||
const hour = chunk.data[4];
|
||||
const minute = chunk.data[5];
|
||||
const second = chunk.data[6];
|
||||
|
||||
// NOTE This implementation is intentionally trying not to be too clever
|
||||
// with validation.
|
||||
if (month < 1 or month > 12 or
|
||||
day < 1 or day > 31 or
|
||||
hour > 23 or
|
||||
minute > 59 or
|
||||
second > 60)
|
||||
{
|
||||
return error.InvalidPng;
|
||||
}
|
||||
|
||||
return .{
|
||||
.year = year,
|
||||
.month = month,
|
||||
.day = day,
|
||||
.hour = hour,
|
||||
.minute = minute,
|
||||
.second = second,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
pub const ChunkType = enum(u32) {
|
||||
IHDR = @bitCast("IHDR".*),
|
||||
PLTE = @bitCast("PLTE".*),
|
||||
IDAT = @bitCast("IDAT".*),
|
||||
@@ -101,12 +419,12 @@ const ChunkType = enum(u32) {
|
||||
}
|
||||
};
|
||||
|
||||
const Chunk = struct {
|
||||
pub const Chunk = struct {
|
||||
chunk_type: ChunkType,
|
||||
data: []const u8,
|
||||
};
|
||||
|
||||
const StandardKeyword = enum {
|
||||
pub const StandardKeyword = enum {
|
||||
/// Short (one line) title or caption for image
|
||||
Title,
|
||||
/// Name of image’s creator
|
||||
@@ -159,68 +477,54 @@ pub fn info(buffer: []const u8) ?Header {
|
||||
return null;
|
||||
}
|
||||
|
||||
const chunk = buffer[format.magic_length..format.info_length];
|
||||
|
||||
const length = std.mem.readInt(u32, chunk[0..4], .big);
|
||||
const chunk_type: ChunkType = @bitCast(chunk[4..8].*);
|
||||
|
||||
if (length != 13 or chunk_type != .IHDR) {
|
||||
const chunk, _ = decodeChunk(buffer[format.magic_length..]) catch return null orelse return null;
|
||||
if (chunk.chunk_type != .IHDR) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = chunk[8..21];
|
||||
const crc = std.mem.readInt(u32, chunk[21..25], .big);
|
||||
const header = Header.decode(chunk) catch return null;
|
||||
return header;
|
||||
}
|
||||
|
||||
pub fn decodeChunk(buffer: []const u8) !?struct { Chunk, []const u8 } {
|
||||
var rest: []const u8 = buffer;
|
||||
|
||||
if (rest.len < 8) {
|
||||
// Not enough data
|
||||
return null;
|
||||
}
|
||||
|
||||
const length = std.mem.readInt(u32, rest[0..4], .big);
|
||||
const chunk_type: ChunkType = @bitCast(rest[4..8].*);
|
||||
rest = rest[8..];
|
||||
|
||||
if (length > 0x7FFF_FFFF) {
|
||||
return error.InvalidPng;
|
||||
}
|
||||
|
||||
if (rest.len < length) {
|
||||
// Not enough data
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = rest[0..length];
|
||||
rest = rest[length..];
|
||||
|
||||
if (rest.len < 4) {
|
||||
// Not enough data
|
||||
return null;
|
||||
}
|
||||
|
||||
const crc = std.mem.readInt(u32, rest[0..4], .big);
|
||||
rest = rest[4..];
|
||||
|
||||
if (std.hash.crc.Crc32IsoHdlc.hash(data) != crc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const width = std.mem.readInt(u32, data[0..4], .big);
|
||||
const height = std.mem.readInt(u32, data[4..8], .big);
|
||||
const bit_depth = data[8];
|
||||
const color_type = data[9];
|
||||
const compression_method = data[10];
|
||||
const filter_method = data[11];
|
||||
const interlace_method = data[12];
|
||||
|
||||
if (width == 0 or width > 0x7FFF_FFFF or
|
||||
height == 0 or height > 0x7FFF_FFFF or
|
||||
bit_depth == 0 or bit_depth > 16 or !std.math.isPowerOfTwo(bit_depth) or
|
||||
color_type == 1 or color_type == 5 or color_type > 6 or
|
||||
compression_method != 0 or
|
||||
filter_method != 0 or
|
||||
interlace_method > 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (color_type) {
|
||||
0 => {
|
||||
// all bit depths allowed
|
||||
},
|
||||
2 => {
|
||||
if (bit_depth < 8) return null;
|
||||
},
|
||||
3 => {
|
||||
if (bit_depth > 8) return null;
|
||||
},
|
||||
4 => {
|
||||
if (bit_depth < 8) return null;
|
||||
},
|
||||
6 => {
|
||||
if (bit_depth < 8) return null;
|
||||
},
|
||||
else => unreachable,
|
||||
return error.InvalidPng;
|
||||
}
|
||||
|
||||
return .{
|
||||
.width = width,
|
||||
.height = height,
|
||||
.bit_depth = @enumFromInt(bit_depth),
|
||||
.color_type = @enumFromInt(color_type),
|
||||
.compression_method = @enumFromInt(compression_method),
|
||||
.filter_method = @enumFromInt(filter_method),
|
||||
.interlace_method = @enumFromInt(interlace_method),
|
||||
.{ .chunk_type = chunk_type, .data = data },
|
||||
rest,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -233,41 +537,16 @@ pub fn decodeChunks(buffer: []const u8, chunks: ?[]Chunk) !usize {
|
||||
var rest: []const u8 = buffer[format.magic_length..];
|
||||
|
||||
while (rest.len > 0) : (index += 1) {
|
||||
if (rest.len < 8) {
|
||||
return error.InvalidPng;
|
||||
}
|
||||
|
||||
const length = std.mem.readInt(u32, rest[0..4], .big);
|
||||
const chunk_type: ChunkType = @bitCast(rest[4..8].*);
|
||||
rest = rest[8..];
|
||||
|
||||
if (rest.len < length or length > 0x7FFF_FFFF) {
|
||||
return error.InvalidPng;
|
||||
}
|
||||
|
||||
const data = rest[0..length];
|
||||
rest = rest[length..];
|
||||
|
||||
if (rest.len < 4) {
|
||||
return error.InvalidPng;
|
||||
}
|
||||
|
||||
const crc = std.mem.readInt(u32, rest[0..4], .big);
|
||||
rest = rest[4..];
|
||||
|
||||
if (std.hash.crc.Crc32IsoHdlc.hash(data) != crc) {
|
||||
return error.InvalidPng;
|
||||
}
|
||||
const chunk, rest = try decodeChunk(rest) orelse return error.InvalidPng;
|
||||
|
||||
if (chunks) |ck| {
|
||||
if (index < ck.len) {
|
||||
ck[index] = .{
|
||||
.chunk_type = chunk_type,
|
||||
.data = data,
|
||||
};
|
||||
ck[index] = chunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
pub fn decodeChunksAlloc(buffer: []const u8, allocator: std.mem.Allocator) ![]Chunk {
|
||||
|
||||
Reference in New Issue
Block a user