media: Format description struct, stubs for more formats
This commit is contained in:
298
packages/media/src/png.zig
Normal file
298
packages/media/src/png.zig
Normal file
@@ -0,0 +1,298 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Format = @import("Format.zig");
|
||||
|
||||
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";
|
||||
|
||||
const BitDepth = enum(u8) {
|
||||
@"1" = 1,
|
||||
@"2" = 2,
|
||||
@"4" = 4,
|
||||
@"8" = 8,
|
||||
@"16" = 16,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const CompressionMethod = enum(u8) {
|
||||
flate = 0,
|
||||
};
|
||||
|
||||
const FilterMethod = enum(u8) {
|
||||
adaptive = 0,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
const ChunkType = enum(u32) {
|
||||
IHDR = @bitCast("IHDR".*),
|
||||
PLTE = @bitCast("PLTE".*),
|
||||
IDAT = @bitCast("IDAT".*),
|
||||
IEND = @bitCast("IEND".*),
|
||||
tRNS = @bitCast("tRNS".*),
|
||||
gAMA = @bitCast("gAMA".*),
|
||||
cHRM = @bitCast("cHRM".*),
|
||||
sRGB = @bitCast("sRGB".*),
|
||||
iCCP = @bitCast("iCCP".*),
|
||||
tEXt = @bitCast("tEXt".*),
|
||||
zTXt = @bitCast("zTXt".*),
|
||||
iTXt = @bitCast("iTXt".*),
|
||||
bKGD = @bitCast("bKGD".*),
|
||||
pHYs = @bitCast("pHYs".*),
|
||||
sBIT = @bitCast("sBIT".*),
|
||||
sPLT = @bitCast("sPLT".*),
|
||||
hIST = @bitCast("hIST".*),
|
||||
tIME = @bitCast("tIME".*),
|
||||
_,
|
||||
|
||||
pub fn ancillary(self: ChunkType) bool {
|
||||
return @as([4]u8, @bitCast(self))[0] & 0b0010_0000 != 0;
|
||||
}
|
||||
|
||||
pub fn private(self: ChunkType) bool {
|
||||
return @as([4]u8, @bitCast(self))[1] & 0b0010_0000 != 0;
|
||||
}
|
||||
|
||||
pub fn safeToCopy(self: ChunkType) bool {
|
||||
return @as([4]u8, @bitCast(self))[3] & 0b0010_0000 != 0;
|
||||
}
|
||||
};
|
||||
|
||||
const Chunk = struct {
|
||||
chunk_type: ChunkType,
|
||||
data: []const u8,
|
||||
};
|
||||
|
||||
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 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);
|
||||
}
|
||||
};
|
||||
|
||||
/// 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 = 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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = chunk[8..21];
|
||||
const crc = std.mem.readInt(u32, chunk[21..25], .big);
|
||||
|
||||
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 .{
|
||||
.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 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (chunks) |ck| {
|
||||
if (index < ck.len) {
|
||||
ck[index] = .{
|
||||
.chunk_type = chunk_type,
|
||||
.data = data,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user