media: Format description struct, stubs for more formats
This commit is contained in:
BIN
packages/media/jpeg-specification.pdf
(Stored with Git LFS)
Normal file
BIN
packages/media/jpeg-specification.pdf
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
packages/media/png-specification.pdf
(Stored with Git LFS)
Normal file
BIN
packages/media/png-specification.pdf
(Stored with Git LFS)
Normal file
Binary file not shown.
21
packages/media/src/Format.zig
Normal file
21
packages/media/src/Format.zig
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
/// The number of bytes necessary to confirm that a buffer contains this media
|
||||||
|
/// format.
|
||||||
|
magic_length: usize,
|
||||||
|
/// The number of bytes necessary to confirm that a buffer contains this media
|
||||||
|
/// format and to extract metadata stored at the beggining, if any. Must be at
|
||||||
|
/// least as big as `magic_length`.
|
||||||
|
info_length: usize,
|
||||||
|
/// The file extension usually associated with this media format; all in
|
||||||
|
/// lowercase and without the dot character.
|
||||||
|
extension: []const u8,
|
||||||
|
/// The media type (aka MIME type or Content-Type) usually associated with this
|
||||||
|
/// media format; not necessarily officially registered.
|
||||||
|
media_type: []const u8,
|
||||||
|
|
||||||
|
/// Confirm whether a buffer contains this media format. The buffer doesn't have
|
||||||
|
/// to contain the entire file, only its beginning. The caller asserts that
|
||||||
|
/// `buffer` is at least `magic_length` bytes long.
|
||||||
|
isFormat: *const fn (buffer: []const u8) bool,
|
||||||
@@ -17,6 +17,6 @@ pub const Sample = extern struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const Stream = struct {
|
pub const Stream = struct {
|
||||||
source: std.io.Reader,
|
source: *std.Io.Reader,
|
||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
};
|
};
|
||||||
|
|||||||
139
packages/media/src/jpeg.zig
Normal file
139
packages/media/src/jpeg.zig
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Format = @import("Format.zig");
|
||||||
|
|
||||||
|
const format: Format = .{
|
||||||
|
.magic_length = magic.len,
|
||||||
|
// NOTE The information (like width and height) is not in a fixed position
|
||||||
|
.info_length = magic.len,
|
||||||
|
.extension = "jpeg",
|
||||||
|
.media_type = "image/jpeg",
|
||||||
|
.isFormat = isJpeg,
|
||||||
|
};
|
||||||
|
|
||||||
|
const magic = "\xFF\xD8\xFF";
|
||||||
|
|
||||||
|
const Info = union(enum) {
|
||||||
|
partial: void,
|
||||||
|
full: Header,
|
||||||
|
|
||||||
|
pub fn makeFull(full: Header) Info {
|
||||||
|
return .{ .full = full };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Header = struct {
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Marker = enum(u8) {
|
||||||
|
/// Start of frame, Huffman coding, Baseline DCT
|
||||||
|
SOF_0 = 0xC0,
|
||||||
|
/// Start of frame, Huffman coding, Extended sequential DCT
|
||||||
|
SOF_1 = 0xC1,
|
||||||
|
/// Start of frame, Huffman coding, Progressive DCT
|
||||||
|
SOF_2 = 0xC2,
|
||||||
|
/// Start of frame, Huffman coding, Lossless (sequential)
|
||||||
|
SOF_3 = 0xC3,
|
||||||
|
/// Start of frame, Huffman coding, Differential sequential DCT
|
||||||
|
SOF_5 = 0xC5,
|
||||||
|
/// Start of frame, Huffman coding, Differential progressive DCT
|
||||||
|
SOF_6 = 0xC6,
|
||||||
|
/// Start of frame, Huffman coding, Differential lossless (sequential)
|
||||||
|
SOF_7 = 0xC7,
|
||||||
|
/// Start of frame, arithmetic coding, Extended sequential DCT
|
||||||
|
SOF_9 = 0xC9,
|
||||||
|
/// Start of frame, arithmetic coding, Progressive DCT
|
||||||
|
SOF_10 = 0xCA,
|
||||||
|
/// Start of frame, arithmetic coding, Lossless (sequential)
|
||||||
|
SOF_11 = 0xCB,
|
||||||
|
/// Start of frame, arithmetic coding, Differential sequential DCT
|
||||||
|
SOF_13 = 0xCD,
|
||||||
|
/// Start of frame, arithmetic coding, Differential progressive DCT
|
||||||
|
SOF_14 = 0xCE,
|
||||||
|
/// Start of frame, arithmetic coding, Differential lossless (sequential)
|
||||||
|
SOF_15 = 0xCF,
|
||||||
|
/// Define Huffman table(s)
|
||||||
|
DHT = 0xC4,
|
||||||
|
/// Define arithmetic coding conditioning(s)
|
||||||
|
DAC = 0xCC,
|
||||||
|
/// Start of image
|
||||||
|
SOI = 0xD8,
|
||||||
|
/// End of image
|
||||||
|
EOI = 0xD9,
|
||||||
|
/// Start of scan
|
||||||
|
SOS = 0xDA,
|
||||||
|
/// Define quantization table(s)
|
||||||
|
DQT = 0xDB,
|
||||||
|
/// Define number of lines
|
||||||
|
DNL = 0xDC,
|
||||||
|
/// Define restart interval
|
||||||
|
DRI = 0xDD,
|
||||||
|
/// Define hierarchical progression
|
||||||
|
DHP = 0xDE,
|
||||||
|
/// Expand reference component(s)
|
||||||
|
EXP = 0xDF,
|
||||||
|
/// Comment
|
||||||
|
COM = 0xFE,
|
||||||
|
/// For temporary private use in arithmetic coding
|
||||||
|
TEM = 0x01,
|
||||||
|
|
||||||
|
_,
|
||||||
|
|
||||||
|
/// Restart with modulo 8 count `m`
|
||||||
|
pub fn RST(m: u3) Marker {
|
||||||
|
return @enumFromInt(0xD0 | m);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isRST(self: Marker) ?u3 {
|
||||||
|
return if (@intFromEnum(self) & 0b1111_1000 == 0xD0) @intCast(@intFromEnum(self) & 0b0000_0111) else null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reserved for application segments
|
||||||
|
pub fn APP(n: u4) Marker {
|
||||||
|
return @enumFromInt(0xE0 | n);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isAPP(self: Marker) ?u3 {
|
||||||
|
return if (@intFromEnum(self) & 0b1111_0000 == 0xE0) @intCast(@intFromEnum(self) & 0b0000_1111) else null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A standalone marker has no content and are not followed by segment
|
||||||
|
/// length parameter.
|
||||||
|
pub fn isStandalone(self: Marker) bool {
|
||||||
|
return self.isRST() != null or
|
||||||
|
self == .SOI or
|
||||||
|
self == .EOI or
|
||||||
|
self == .TEM;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The caller asserts that the buffer is at least `format.magic_length` bytes
|
||||||
|
/// long.
|
||||||
|
pub fn isJpeg(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. The information is not in a fixed position, so you need to provide a
|
||||||
|
/// substantial amount of data, in the order of kilobytes. Depending on the
|
||||||
|
/// amount of metadata, the information might be in the first kilobyte or dozens
|
||||||
|
/// of kilobytes in.
|
||||||
|
///
|
||||||
|
/// This function returns:
|
||||||
|
///
|
||||||
|
/// - `null` when the buffer is not a JPEG or it's a malformed JPEG
|
||||||
|
/// - `.partial` when the buffer appears to be a part of a JPEG, but the
|
||||||
|
/// information could not be found within the buffer provided
|
||||||
|
/// - `.full` when the buffer appears to be a part of a JPEG and the information
|
||||||
|
/// was fully contained within the buffer provided
|
||||||
|
pub fn info(buffer: []const u8) ?Info {
|
||||||
|
std.debug.assert(buffer.len >= format.info_length);
|
||||||
|
|
||||||
|
if (!isJpeg(buffer)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@panic("TODO");
|
||||||
|
}
|
||||||
37
packages/media/src/jxl.zig
Normal file
37
packages/media/src/jxl.zig
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Format = @import("Format.zig");
|
||||||
|
|
||||||
|
const format: Format = .{
|
||||||
|
.magic_length = @max(magic_naked.len, magic_container.len),
|
||||||
|
.info_length = @max(magic_naked.len, magic_container.len),
|
||||||
|
.extension = "jxl",
|
||||||
|
.media_type = "image/jxl",
|
||||||
|
.isFormat = isJxl,
|
||||||
|
};
|
||||||
|
|
||||||
|
const magic_naked = "\xFF\x0A";
|
||||||
|
const magic_container = "\x00\x00\x00\x0CJXL \r\n\x87\n";
|
||||||
|
|
||||||
|
const Header = struct {};
|
||||||
|
|
||||||
|
/// The caller asserts that the buffer is at least `format.magic_length` bytes
|
||||||
|
/// long.
|
||||||
|
pub fn isJxl(buffer: []const u8) bool {
|
||||||
|
std.debug.assert(buffer.len >= format.magic_length);
|
||||||
|
|
||||||
|
return std.mem.startsWith(u8, buffer, magic_naked) or
|
||||||
|
std.mem.startsWith(u8, buffer, magic_container);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 (!isJxl(buffer)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@panic("TODO");
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,27 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Format = @import("Format.zig");
|
||||||
|
|
||||||
|
const format: Format = .{
|
||||||
|
.magic_length = magic.len,
|
||||||
|
// 4B - sample count
|
||||||
|
// 1B - channels
|
||||||
|
// 3B - sample rate
|
||||||
|
.info_length = magic.len + 8,
|
||||||
|
.extension = "qoa",
|
||||||
|
.media_type = "audio/qoa",
|
||||||
|
.isFormat = isQoa,
|
||||||
|
};
|
||||||
|
|
||||||
|
const magic = "qoaf";
|
||||||
|
|
||||||
const Header = union(enum) {
|
const Header = union(enum) {
|
||||||
streaming: HeaderStreaming,
|
streaming: HeaderStreaming,
|
||||||
static: HeaderStatic,
|
static: HeaderStatic,
|
||||||
|
|
||||||
|
pub fn initStatic(static: HeaderStatic) Header {
|
||||||
|
return .{ .static = static };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const HeaderStreaming = void;
|
const HeaderStreaming = void;
|
||||||
@@ -19,30 +38,38 @@ const HeaderStatic = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The caller asserts that the buffer is at least 12 bytes long, which can
|
/// The caller asserts that the buffer is at least `format.magic_length` bytes
|
||||||
/// contain the entirety of a QOA file header and the relevant information in
|
/// long.
|
||||||
/// the first frame header.
|
pub fn isQoa(buffer: []const u8) bool {
|
||||||
pub fn info(buffer: []const u8) ?Header {
|
return std.mem.eql(u8, buffer[0..format.magic_length], magic);
|
||||||
std.debug.assert(buffer.len >= 12);
|
}
|
||||||
|
|
||||||
|
/// 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 (!isQoa(buffer)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const magic = buffer[0..4];
|
|
||||||
const samples = std.mem.readInt(u32, buffer[4..8], .big);
|
const samples = std.mem.readInt(u32, buffer[4..8], .big);
|
||||||
const channels = buffer[8];
|
const channels = buffer[8];
|
||||||
const sample_rate = std.mem.readInt(u24, buffer[9..12], .big);
|
const sample_rate = std.mem.readInt(u24, buffer[9..12], .big);
|
||||||
|
|
||||||
if (!std.mem.eql(u8, magic, "qoaf") or channels == 0 or channels > 8 or sample_rate == 0) {
|
if (channels == 0 or channels > 8 or
|
||||||
|
sample_rate == 0)
|
||||||
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (samples == 0) {
|
if (samples == 0) {
|
||||||
return .streaming;
|
return .streaming;
|
||||||
} else {
|
} else {
|
||||||
return .{
|
return .initStatic(.{
|
||||||
.static = .{
|
.samples = samples,
|
||||||
.samples = samples,
|
.channels = channels,
|
||||||
.channels = channels,
|
.sample_rate = sample_rate,
|
||||||
.sample_rate = sample_rate,
|
});
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Format = @import("Format.zig");
|
||||||
|
|
||||||
|
const format: Format = .{
|
||||||
|
.magic_length = magic.len,
|
||||||
|
// 4B - width
|
||||||
|
// 4B - height
|
||||||
|
// 1B - channels
|
||||||
|
// 1B - color space
|
||||||
|
.info_length = magic.len + 10,
|
||||||
|
.extension = "qoi",
|
||||||
|
.media_type = "image/qoi",
|
||||||
|
.isFormat = isQoi,
|
||||||
|
};
|
||||||
|
|
||||||
|
const magic = "qoif";
|
||||||
|
|
||||||
const Channels = enum(u8) {
|
const Channels = enum(u8) {
|
||||||
rgb = 3,
|
rgb = 3,
|
||||||
rgba = 4,
|
rgba = 4,
|
||||||
@@ -17,18 +33,31 @@ const Header = struct {
|
|||||||
color_space: ColorSpace,
|
color_space: ColorSpace,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The caller asserts that the buffer is at least 14 bytes long, which can
|
/// The caller asserts that the buffer is at least `format.magic_length` bytes
|
||||||
/// contain the entirety of a QOI header.
|
/// long.
|
||||||
pub fn info(buffer: []const u8) ?Header {
|
pub fn isQoi(buffer: []const u8) bool {
|
||||||
std.debug.assert(buffer.len >= 14);
|
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 (!isQoi(buffer)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const magic = buffer[0..4];
|
|
||||||
const width = std.mem.readInt(u32, buffer[4..8], .big);
|
const width = std.mem.readInt(u32, buffer[4..8], .big);
|
||||||
const height = std.mem.readInt(u32, buffer[8..12], .big);
|
const height = std.mem.readInt(u32, buffer[8..12], .big);
|
||||||
const channels = buffer[12];
|
const channels = buffer[12];
|
||||||
const color_space = buffer[13];
|
const color_space = buffer[13];
|
||||||
|
|
||||||
if (!std.mem.eql(u8, magic, "qoif") or width == 0 or height == 0 or channels < 3 or channels > 4 or color_space > 1) {
|
if (width == 0 or
|
||||||
|
height == 0 or
|
||||||
|
channels < 3 or channels > 4 or
|
||||||
|
color_space > 1)
|
||||||
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
pub const audio = @import("audio.zig");
|
pub const audio = @import("audio.zig");
|
||||||
|
pub const Format = @import("Format.zig");
|
||||||
pub const image = @import("image.zig");
|
pub const image = @import("image.zig");
|
||||||
|
pub const jpeg = @import("jpeg.zig");
|
||||||
|
pub const jxl = @import("jxl.zig");
|
||||||
|
pub const png = @import("png.zig");
|
||||||
pub const qoa = @import("qoa.zig");
|
pub const qoa = @import("qoa.zig");
|
||||||
pub const qoi = @import("qoi.zig");
|
pub const qoi = @import("qoi.zig");
|
||||||
pub const stbi = @import("stbi.zig");
|
pub const stbi = @import("stbi.zig");
|
||||||
|
|||||||
Reference in New Issue
Block a user