646 lines
18 KiB
Zig
646 lines
18 KiB
Zig
const std = @import("std");
|
||
|
||
const Format = @import("Format.zig");
|
||
|
||
pub const format: Format = .{
|
||
.magic_length = magic.len,
|
||
// 4B - chunk length
|
||
// 4B - chunk type
|
||
// 13B - IHDR data
|
||
// 4B - checksum
|
||
.info_length = magic.len + 25,
|
||
.extension = "png",
|
||
.media_type = "image/png",
|
||
.isFormat = isPng,
|
||
};
|
||
|
||
const magic = "\x89PNG\r\n\x1A\n";
|
||
|
||
// --- 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),
|
||
};
|
||
}
|
||
|
||
test "refAllDecls" {
|
||
std.testing.refAllDecls(@This());
|
||
}
|
||
};
|
||
|
||
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) << @intCast(@intFromEnum(self));
|
||
}
|
||
|
||
test "refAllDecls" {
|
||
std.testing.refAllDecls(@This());
|
||
}
|
||
};
|
||
|
||
pub const ColorType = enum(u8) {
|
||
grayscale = 0,
|
||
rgb = 2,
|
||
palette = 3,
|
||
grayscale_alpha = 4,
|
||
rgba = 6,
|
||
|
||
pub fn paletteUsed(self: ColorType) bool {
|
||
return @intFromEnum(self) & 0b0000_0001 != 0;
|
||
}
|
||
|
||
pub fn colorUsed(self: ColorType) bool {
|
||
return @intFromEnum(self) & 0b0000_0010 != 0;
|
||
}
|
||
|
||
pub fn alphaChannelUsed(self: ColorType) bool {
|
||
return @intFromEnum(self) & 0b0000_0100 != 0;
|
||
}
|
||
|
||
test "refAllDecls" {
|
||
std.testing.refAllDecls(@This());
|
||
}
|
||
};
|
||
|
||
pub const CompressionMethod = enum(u8) {
|
||
flate = 0,
|
||
};
|
||
|
||
pub const FilterMethod = enum(u8) {
|
||
adaptive = 0,
|
||
};
|
||
|
||
pub const InterlaceMethod = enum(u8) {
|
||
none = 0,
|
||
adam7 = 1,
|
||
};
|
||
|
||
// --- 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,
|
||
};
|
||
}
|
||
|
||
test "refAllDecls" {
|
||
std.testing.refAllDecls(@This());
|
||
}
|
||
};
|
||
|
||
// --- 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,
|
||
};
|
||
}
|
||
|
||
test "refAllDecls" {
|
||
std.testing.refAllDecls(@This());
|
||
}
|
||
};
|
||
|
||
const TransparencyPalette = struct {
|
||
len: u16,
|
||
entries: [256]u8,
|
||
|
||
pub fn asSlice(self: *const TransparencyPalette) []const u8 {
|
||
return self.entries[0..self.len];
|
||
}
|
||
|
||
test "refAllDecls" {
|
||
std.testing.refAllDecls(@This());
|
||
}
|
||
};
|
||
|
||
// --- 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 };
|
||
}
|
||
|
||
test "refAllDecls" {
|
||
std.testing.refAllDecls(@This());
|
||
}
|
||
};
|
||
|
||
// --- 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),
|
||
};
|
||
}
|
||
|
||
test "refAllDecls" {
|
||
std.testing.refAllDecls(@This());
|
||
}
|
||
};
|
||
|
||
// --- 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);
|
||
}
|
||
|
||
test "refAllDecls" {
|
||
std.testing.refAllDecls(@This());
|
||
}
|
||
};
|
||
|
||
// --- 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),
|
||
};
|
||
}
|
||
|
||
test "refAllDecls" {
|
||
std.testing.refAllDecls(@This());
|
||
}
|
||
};
|
||
|
||
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,
|
||
};
|
||
}
|
||
|
||
test "refAllDecls" {
|
||
std.testing.refAllDecls(@This());
|
||
}
|
||
};
|
||
|
||
// -----------------------------------------------------------------------------
|
||
|
||
pub const ChunkType = enum(u32) {
|
||
IHDR = fromName("IHDR"),
|
||
PLTE = fromName("PLTE"),
|
||
IDAT = fromName("IDAT"),
|
||
IEND = fromName("IEND"),
|
||
tRNS = fromName("tRNS"),
|
||
gAMA = fromName("gAMA"),
|
||
cHRM = fromName("cHRM"),
|
||
sRGB = fromName("sRGB"),
|
||
iCCP = fromName("iCCP"),
|
||
tEXt = fromName("tEXt"),
|
||
zTXt = fromName("zTXt"),
|
||
iTXt = fromName("iTXt"),
|
||
bKGD = fromName("bKGD"),
|
||
pHYs = fromName("pHYs"),
|
||
sBIT = fromName("sBIT"),
|
||
sPLT = fromName("sPLT"),
|
||
hIST = fromName("hIST"),
|
||
tIME = fromName("tIME"),
|
||
_,
|
||
|
||
fn fromName(name: *const [4]u8) u32 {
|
||
return std.mem.bytesToValue(u32, name);
|
||
}
|
||
|
||
pub fn fromBytes(bytes: *const [4]u8) ChunkType {
|
||
return @enumFromInt(fromName(bytes));
|
||
}
|
||
|
||
pub fn toBytes(self: ChunkType) [4]u8 {
|
||
return std.mem.toBytes(self);
|
||
}
|
||
|
||
pub fn isAncillary(self: ChunkType) bool {
|
||
return self.toBytes()[0] & 0b0010_0000 != 0;
|
||
}
|
||
|
||
pub fn isPrivate(self: ChunkType) bool {
|
||
return self.toBytes()[1] & 0b0010_0000 != 0;
|
||
}
|
||
|
||
pub fn isSafeToCopy(self: ChunkType) bool {
|
||
return self.toBytes()[3] & 0b0010_0000 != 0;
|
||
}
|
||
|
||
test "refAllDecls" {
|
||
std.testing.refAllDecls(@This());
|
||
}
|
||
};
|
||
|
||
pub const Chunk = struct {
|
||
chunk_type: ChunkType,
|
||
data: []const u8,
|
||
};
|
||
|
||
pub const StandardKeyword = enum {
|
||
/// Short (one line) title or caption for image
|
||
Title,
|
||
/// Name of image’s creator
|
||
Author,
|
||
/// Description of image (possibly long)
|
||
Description,
|
||
/// Copyright notice
|
||
Copyright,
|
||
/// Time of original image creation
|
||
Creation,
|
||
/// Software used to create the image
|
||
Software,
|
||
/// Legal disclaimer
|
||
Disclaimer,
|
||
/// Warning of nature of content
|
||
Warning,
|
||
/// Device used to create the image
|
||
Source,
|
||
/// Miscellaneous comment; conversion from GIF comment
|
||
Comment,
|
||
|
||
pub const map: std.StaticStringMap(StandardKeyword) = blk: {
|
||
const fields = @typeInfo(StandardKeyword).@"enum".fields;
|
||
|
||
var kvs_list: [fields.len]struct { []const u8, StandardKeyword } = undefined;
|
||
for (fields, 0..) |field, i| {
|
||
kvs_list[i] = .{ field.name, @field(StandardKeyword, field.name) };
|
||
}
|
||
|
||
break :blk .initComptime(kvs_list);
|
||
};
|
||
|
||
pub fn isStandardKeyword(keyword: []const u8) ?StandardKeyword {
|
||
return map.get(keyword);
|
||
}
|
||
|
||
test "refAllDecls" {
|
||
std.testing.refAllDecls(@This());
|
||
}
|
||
};
|
||
|
||
/// The caller asserts that the buffer is at least `format.magic_length` bytes
|
||
/// long.
|
||
pub fn isPng(buffer: []const u8) bool {
|
||
return std.mem.eql(u8, buffer[0..format.magic_length], magic);
|
||
}
|
||
|
||
/// The caller asserts that the buffer is at least `format.info_length` bytes
|
||
/// long.
|
||
pub fn info(buffer: []const u8) ?Header {
|
||
std.debug.assert(buffer.len >= format.info_length);
|
||
|
||
if (!isPng(buffer)) {
|
||
return null;
|
||
}
|
||
|
||
const chunk, _ = (decodeChunk(buffer[format.magic_length..]) catch return null) orelse return null;
|
||
if (chunk.chunk_type != .IHDR) {
|
||
return null;
|
||
}
|
||
|
||
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 = .fromBytes(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 error.InvalidPng;
|
||
}
|
||
|
||
return .{
|
||
.{ .chunk_type = chunk_type, .data = data },
|
||
rest,
|
||
};
|
||
}
|
||
|
||
pub fn decodeChunks(buffer: []const u8, chunks: ?[]Chunk) !usize {
|
||
if (buffer.len < format.magic_length or !isPng(buffer)) {
|
||
return error.InvalidPng;
|
||
}
|
||
|
||
var index: usize = 0;
|
||
var rest: []const u8 = buffer[format.magic_length..];
|
||
|
||
while (rest.len > 0) : (index += 1) {
|
||
const chunk, rest = try decodeChunk(rest) orelse return error.InvalidPng;
|
||
|
||
if (chunks) |ck| {
|
||
if (index < ck.len) {
|
||
ck[index] = chunk;
|
||
}
|
||
}
|
||
}
|
||
|
||
return index;
|
||
}
|
||
|
||
pub fn decodeChunksAlloc(buffer: []const u8, allocator: std.mem.Allocator) ![]Chunk {
|
||
const n = try decodeChunks(buffer, null);
|
||
|
||
const chunks = try allocator.alloc(Chunk, n);
|
||
errdefer allocator.free(chunks);
|
||
|
||
_ = try decodeChunks(buffer, chunks);
|
||
return chunks;
|
||
}
|
||
|
||
pub fn encodeChunks(chunks: []const Chunk, writer: *std.Io.Writer) !void {
|
||
try writer.writeAll(magic);
|
||
|
||
for (chunks) |chunk| {
|
||
var crc: std.hash.crc.Crc32IsoHdlc = .init();
|
||
var data: []const u8 = chunk.data;
|
||
|
||
while (data.len > 0) {
|
||
const bytes_written = try writer.write(data);
|
||
crc.update(data[0..bytes_written]);
|
||
data = data[bytes_written..];
|
||
}
|
||
|
||
try writer.writeInt(u32, crc.final(), .big);
|
||
}
|
||
}
|
||
|
||
test "refAllDecls" {
|
||
std.testing.refAllDecls(@This());
|
||
}
|