summaryrefslogtreecommitdiff
path: root/libpanto/src/image.zig
diff options
context:
space:
mode:
Diffstat (limited to 'libpanto/src/image.zig')
-rw-r--r--libpanto/src/image.zig50
1 files changed, 24 insertions, 26 deletions
diff --git a/libpanto/src/image.zig b/libpanto/src/image.zig
index 0367373..26474d1 100644
--- a/libpanto/src/image.zig
+++ b/libpanto/src/image.zig
@@ -2,8 +2,8 @@
//!
//! Two responsibilities:
//!
-//! 1. `detectMediaType` — identify an attachment's MIME type from its
-//! leading bytes (magic numbers), not its file extension.
+//! 1. `detectCodec` — identify an attachment's codec from its leading
+//! bytes (magic numbers), not its file extension.
//! 2. `maybeResize` — bound raster images to `max_dim` on each side so a
//! single screenshot can't blow out the model's context. PDFs and
//! already-small images pass through untouched.
@@ -29,10 +29,9 @@ const jpeg_quality: c_int = 80;
pub const Codec = enum { png, jpeg, gif, bmp, webp, pdf };
-/// Detect a supported media type from leading bytes. Returns null when the
-/// bytes don't match any known signature.
-pub fn detectMediaType(bytes: []const u8) ?[]const u8 {
- return switch (detectCodec(bytes) orelse return null) {
+/// The MIME type string for a codec (static; do not free).
+pub fn mediaTypeForCodec(codec: Codec) []const u8 {
+ return switch (codec) {
.png => "image/png",
.jpeg => "image/jpeg",
.gif => "image/gif",
@@ -88,28 +87,27 @@ pub const Processed = struct {
/// detection recognizes the bytes — the caller decides how to surface
/// that (e.g. drop the attachment, or fall back to text).
pub fn process(allocator: Allocator, bytes: []const u8, hint: ?[]const u8) !Processed {
- const media_type = blk: {
+ const codec = blk: {
if (hint) |h| {
- if (codecForMediaType(h) != null) break :blk h;
+ if (codecForMediaType(h)) |hinted| break :blk hinted;
}
- break :blk detectMediaType(bytes) orelse return error.UnknownMediaType;
+ break :blk detectCodec(bytes) orelse return error.UnknownMediaType;
};
- return maybeResize(allocator, bytes, media_type);
+ return maybeResize(allocator, bytes, codec);
}
/// Resize `bytes` so neither dimension exceeds `max_dim`, preserving the
/// input codec where possible. Returns an owned copy of the (possibly
/// unchanged) bytes plus the resulting media type.
///
-/// - PDF or unknown: returned verbatim (a copy), media type unchanged.
+/// - PDF: returned verbatim (a copy), media type unchanged.
/// - raster <= max_dim on both sides: returned verbatim (a copy) — we
/// skip the decode/encode round-trip to avoid quality loss + CPU.
/// - stb-supported raster larger than max_dim: decode -> resize -> same
/// codec.
/// - WEBP larger than max_dim: jebp decode -> resize -> JPEG.
-pub fn maybeResize(allocator: Allocator, bytes: []const u8, media_type: []const u8) !Processed {
- const codec = codecForMediaType(media_type) orelse
- return .{ .media_type = media_type, .data = try allocator.dupe(u8, bytes) };
+pub fn maybeResize(allocator: Allocator, bytes: []const u8, codec: Codec) !Processed {
+ const media_type = mediaTypeForCodec(codec);
if (codec == .pdf)
return .{ .media_type = media_type, .data = try allocator.dupe(u8, bytes) };
@@ -268,16 +266,16 @@ fn resizeWebp(allocator: Allocator, bytes: []const u8, media_type: []const u8) !
const testing = std.testing;
-test "detectMediaType - magic bytes" {
- try testing.expectEqualStrings("image/png", detectMediaType(&.{ 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A }).?);
- try testing.expectEqualStrings("image/jpeg", detectMediaType(&.{ 0xFF, 0xD8, 0xFF, 0xE0 }).?);
- try testing.expectEqualStrings("image/gif", detectMediaType("GIF89a....").?);
- try testing.expectEqualStrings("image/bmp", detectMediaType("BM....").?);
- try testing.expectEqualStrings("application/pdf", detectMediaType("%PDF-1.7").?);
+test "detectCodec + mediaTypeForCodec - magic bytes" {
+ try testing.expectEqualStrings("image/png", mediaTypeForCodec(detectCodec(&.{ 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A }).?));
+ try testing.expectEqualStrings("image/jpeg", mediaTypeForCodec(detectCodec(&.{ 0xFF, 0xD8, 0xFF, 0xE0 }).?));
+ try testing.expectEqualStrings("image/gif", mediaTypeForCodec(detectCodec("GIF89a....").?));
+ try testing.expectEqualStrings("image/bmp", mediaTypeForCodec(detectCodec("BM....").?));
+ try testing.expectEqualStrings("application/pdf", mediaTypeForCodec(detectCodec("%PDF-1.7").?));
const webp = "RIFF" ++ &[_]u8{ 0, 0, 0, 0 } ++ "WEBP";
- try testing.expectEqualStrings("image/webp", detectMediaType(webp).?);
- try testing.expect(detectMediaType("not an image") == null);
- try testing.expect(detectMediaType(&.{0x89}) == null);
+ try testing.expectEqualStrings("image/webp", mediaTypeForCodec(detectCodec(webp).?));
+ try testing.expect(detectCodec("not an image") == null);
+ try testing.expect(detectCodec(&.{0x89}) == null);
}
test "targetDims - skip when small, scale when large preserving aspect" {
@@ -315,7 +313,7 @@ test "process - detects type from raw bytes when hint absent" {
test "maybeResize - PDF passes through unchanged" {
const a = testing.allocator;
const pdf = "%PDF-1.7\nfake pdf body";
- const out = try maybeResize(a, pdf, "application/pdf");
+ const out = try maybeResize(a, pdf, .pdf);
defer a.free(out.data);
try testing.expectEqualStrings("application/pdf", out.media_type);
try testing.expectEqualStrings(pdf, out.data);
@@ -334,7 +332,7 @@ test "maybeResize - small PNG round-trips, large PNG shrinks and stays PNG" {
var sctx = StbWriteCtx{ .list = &small_png, .allocator = a };
try testing.expect(c.stbi_write_png_to_func(stbWriteCb, &sctx, small_w, small_h, 4, &small_px, small_w * 4) != 0);
- const small_out = try maybeResize(a, small_png.items, "image/png");
+ const small_out = try maybeResize(a, small_png.items, .png);
defer a.free(small_out.data);
try testing.expectEqualStrings("image/png", small_out.media_type);
// Small image is returned verbatim (byte-identical copy).
@@ -351,7 +349,7 @@ test "maybeResize - small PNG round-trips, large PNG shrinks and stays PNG" {
var bctx = StbWriteCtx{ .list = &big_png, .allocator = a };
try testing.expect(c.stbi_write_png_to_func(stbWriteCb, &bctx, big_w, big_h, 4, big_px.ptr, big_w * 4) != 0);
- const big_out = try maybeResize(a, big_png.items, "image/png");
+ const big_out = try maybeResize(a, big_png.items, .png);
defer a.free(big_out.data);
try testing.expectEqualStrings("image/png", big_out.media_type);
const dims = probeDims(big_out.data).?;