317 lines
14 KiB
Plaintext
317 lines
14 KiB
Plaintext
// Image_Decode: any image file's bytes to RGBA pixels, the right way up, and the PNG previews .mcap
|
|
// files hold. stb_image reads PNG, JPEG, GIF, BMP, PSD and TGA; everything else (HEIC, AVIF, WebP,
|
|
// TIFF, camera RAW) goes to the system's own codecs: on Windows WIC, which reads what the installed
|
|
// Store extensions (HEIF, WebP, Raw Image) can, and on macOS ImageIO (imageio.jai). Linux reads only
|
|
// what stb_image does. A photo's EXIF orientation is applied, so a phone's portrait
|
|
// photo comes out standing up.
|
|
//
|
|
// What the bytes are comes from their first bytes (image_kind), never from a file name, except for
|
|
// the camera RAW formats that are TIFF inside: image_kind_from_name tells those apart.
|
|
//
|
|
// Videos (video.jai) are told apart here too, by their first bytes or their extension; video_poster
|
|
// reads a frame from one with FFmpeg, on Windows, and gif_info counts an animated GIF's frames.
|
|
//
|
|
// Thread-safe: every call stands alone, and WIC is set up and released on the calling thread; ImageIO
|
|
// needs no setting up.
|
|
|
|
Image_Kind :: enum u8 {
|
|
UNKNOWN;
|
|
PNG; JPEG; GIF; BMP; PSD; TGA; // stb_image
|
|
WEBP; HEIF; AVIF; TIFF; RAW; // WIC on Windows, ImageIO on macOS
|
|
VIDEO; // MP4, QuickTime, Matroska and WebM, AVI, WMV, MPEG: see video_poster
|
|
}
|
|
|
|
// Pixels from image_decode: RGBA, 4 bytes a pixel, rows packed. Free with image_free.
|
|
Image :: struct {
|
|
pixels: *u8;
|
|
width, height: s32;
|
|
from_stb: bool; // pixels are stb_image's, freed with stbi_image_free; otherwise the default allocator's
|
|
}
|
|
|
|
image_free :: (image: *Image) {
|
|
if image.pixels {
|
|
if image.from_stb stbi_image_free(image.pixels);
|
|
else free(image.pixels,, context.default_allocator);
|
|
}
|
|
image.* = .{};
|
|
}
|
|
|
|
// What the bytes hold, from their first bytes. A TIFF may be a camera's RAW file; see image_kind_from_name.
|
|
image_kind :: (bytes: [] u8) -> Image_Kind {
|
|
starts :: (bytes: [] u8, at: s64, magic: string) -> bool {
|
|
if bytes.count < at + magic.count return false;
|
|
return memcmp(bytes.data + at, magic.data, magic.count) == 0;
|
|
}
|
|
if starts(bytes, 0, "\x89PNG\r\n\x1A\n") return .PNG;
|
|
if starts(bytes, 0, "\xFF\xD8\xFF") return .JPEG;
|
|
if starts(bytes, 0, "GIF87a") || starts(bytes, 0, "GIF89a") return .GIF;
|
|
if starts(bytes, 0, "BM") && bytes.count > 26 return .BMP;
|
|
if starts(bytes, 0, "8BPS") return .PSD;
|
|
if starts(bytes, 0, "RIFF") && starts(bytes, 8, "WEBP") return .WEBP;
|
|
if starts(bytes, 0, "II*\0") || starts(bytes, 0, "MM\0*") return .TIFF;
|
|
if starts(bytes, 0, "IIRO") || starts(bytes, 0, "IIU\0") || starts(bytes, 0, "FUJIFILMCCD-RAW") return .RAW; // Olympus, Panasonic, Fuji
|
|
if starts(bytes, 4, "ftyp") {
|
|
// ISO base media: HEIF and AVIF images, Canon's CR3, or a video, by the major brand and the
|
|
// compatible ones. AVIF first: an AVIF often names the general mif1 brand too.
|
|
size := be32(bytes, 0);
|
|
end := min(cast(s64) size, bytes.count);
|
|
if end < 16 return .UNKNOWN;
|
|
found: [#run enum_highest_value(Image_Kind) + 1] bool;
|
|
found[ftyp_brand_kind(slice_bytes(bytes, 8, 4))] = true;
|
|
at := 16;
|
|
while at + 4 <= end {
|
|
found[ftyp_brand_kind(slice_bytes(bytes, at, 4))] = true;
|
|
at += 4;
|
|
}
|
|
if found[Image_Kind.AVIF] return .AVIF;
|
|
if found[Image_Kind.HEIF] return .HEIF;
|
|
if found[Image_Kind.RAW] return .RAW;
|
|
return .VIDEO; // an ftyp with no image brand: an MP4, MOV, 3GP and the like
|
|
}
|
|
if starts(bytes, 4, "moov") || starts(bytes, 4, "mdat") || starts(bytes, 4, "wide") || starts(bytes, 4, "free") return .VIDEO; // old QuickTime
|
|
if starts(bytes, 0, "\x1A\x45\xDF\xA3") return .VIDEO; // Matroska and WebM (EBML)
|
|
if starts(bytes, 0, "RIFF") && starts(bytes, 8, "AVI ") return .VIDEO;
|
|
if starts(bytes, 0, "\x30\x26\xB2\x75\x8E\x66\xCF\x11") return .VIDEO; // ASF: WMV
|
|
if starts(bytes, 0, "\x00\x00\x01\xBA") || starts(bytes, 0, "\x00\x00\x01\xB3") return .VIDEO; // MPEG program stream, or bare MPEG video
|
|
if starts(bytes, 0, "FLV\x01") return .VIDEO;
|
|
if bytes.count >= 189 && bytes[0] == 0x47 && bytes[188] == 0x47 return .VIDEO; // MPEG transport stream: a sync byte every 188
|
|
if bytes.count >= 197 && bytes[4] == 0x47 && bytes[196] == 0x47 return .VIDEO; // ... with a 4-byte time code before each (.m2ts, .mts)
|
|
return .UNKNOWN;
|
|
}
|
|
|
|
// The container a video is in, for people to read: "MP4", "QuickTime", "WebM" ... from its first
|
|
// bytes and, where those are shared (MP4 and QuickTime, Matroska and WebM), its extension.
|
|
video_format_name :: (head: [] u8, name: string) -> string {
|
|
extension := to_lower_copy(path_extension(name),, temp);
|
|
starts :: (bytes: [] u8, at: s64, magic: string) -> bool {
|
|
return bytes.count >= at + magic.count && memcmp(bytes.data + at, magic.data, magic.count) == 0;
|
|
}
|
|
if starts(head, 4, "ftyp") {
|
|
if starts(head, 8, "qt ") return "QuickTime";
|
|
if starts(head, 8, "3g") return "3GP";
|
|
return "MP4";
|
|
}
|
|
if starts(head, 4, "moov") || starts(head, 4, "mdat") || starts(head, 4, "wide") || starts(head, 4, "free") return "QuickTime";
|
|
if starts(head, 0, "\x1A\x45\xDF\xA3") return ifx extension == "webm" then "WebM" else "Matroska";
|
|
if starts(head, 0, "RIFF") return "AVI";
|
|
if starts(head, 0, "\x30\x26\xB2\x75") return "WMV";
|
|
if starts(head, 0, "FLV") return "FLV";
|
|
if starts(head, 0, "\x00\x00\x01") return "MPEG";
|
|
if head.count && (head[0] == 0x47 || (head.count > 4 && head[4] == 0x47)) return "MPEG-TS";
|
|
return "video";
|
|
}
|
|
|
|
// The kind for bytes that image_kind cannot place, or places as TIFF, from the file's name: camera RAW
|
|
// formats built on TIFF, and TGA, which has no magic of its own. kind is image_kind's answer.
|
|
image_kind_from_name :: (kind: Image_Kind, name: string) -> Image_Kind {
|
|
extension := to_lower_copy(path_extension(name),, temp);
|
|
if kind == .TIFF {
|
|
for RAW_TIFF_EXTENSIONS if extension == it return .RAW;
|
|
return .TIFF;
|
|
}
|
|
if kind == .UNKNOWN && extension == "tga" return .TGA;
|
|
return kind;
|
|
}
|
|
|
|
RAW_TIFF_EXTENSIONS :: string.["dng", "cr2", "nef", "nrw", "arw", "srf", "sr2", "pef", "erf", "kdc", "dcr", "mos", "3fr", "iiq", "srw", "x3f", "mrw"];
|
|
|
|
image_kind_name :: (kind: Image_Kind) -> string {
|
|
if kind == {
|
|
case .PNG; return "PNG";
|
|
case .JPEG; return "JPEG";
|
|
case .GIF; return "GIF";
|
|
case .BMP; return "BMP";
|
|
case .PSD; return "PSD";
|
|
case .TGA; return "TGA";
|
|
case .WEBP; return "WebP";
|
|
case .HEIF; return "HEIF";
|
|
case .AVIF; return "AVIF";
|
|
case .TIFF; return "TIFF";
|
|
case .RAW; return "RAW";
|
|
case .VIDEO; return "video";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
// The image in bytes as RGBA, turned and flipped as its EXIF orientation (or, for HEIF and AVIF, its
|
|
// container) says, unless orient is false. kind may be passed when known; image_kind is used otherwise.
|
|
// On failure, why.
|
|
image_decode :: (bytes: [] u8, kind := Image_Kind.UNKNOWN, orient := true) -> Image, ok: bool, error: string {
|
|
image: Image;
|
|
if !bytes.count return image, false, "it is empty";
|
|
if kind == .UNKNOWN kind = image_kind(bytes);
|
|
if kind == .VIDEO return image, false, "it is a video";
|
|
|
|
stb_kinds :: Image_Kind.[.PNG, .JPEG, .GIF, .BMP, .PSD, .TGA, .UNKNOWN];
|
|
decoded := false;
|
|
error: string;
|
|
if array_find(stb_kinds, kind) && bytes.count < S32_MAX {
|
|
w, h, channels: s32;
|
|
pixels := stbi_load_from_memory(bytes.data, cast(s32) bytes.count, *w, *h, *channels, 4);
|
|
if pixels && w > 0 && h > 0 {
|
|
image = .{pixels = pixels, width = w, height = h, from_stb = true};
|
|
decoded = true;
|
|
} else {
|
|
if pixels stbi_image_free(pixels);
|
|
error = to_string(stbi_failure_reason());
|
|
}
|
|
}
|
|
container_orientation := 0; // ImageIO's, for HEIF and AVIF on macOS; 0 when not known
|
|
if !decoded {
|
|
#if OS == .WINDOWS {
|
|
wic_ok: bool;
|
|
wic_error: string;
|
|
image, wic_ok, wic_error = wic_decode(bytes);
|
|
if !wic_ok {
|
|
if error return image, false, tprint("% (stb_image: %)", wic_error, error);
|
|
return image, false, wic_error;
|
|
}
|
|
} else #if OS == .MACOS {
|
|
imageio_ok: bool;
|
|
imageio_error: string;
|
|
image, imageio_ok, imageio_error, container_orientation = imageio_decode(bytes);
|
|
if !imageio_ok {
|
|
if error return image, false, tprint("% (stb_image: %)", imageio_error, error);
|
|
return image, false, imageio_error;
|
|
}
|
|
} else {
|
|
return image, false, ifx error then tprint("stb_image: %", error) else tprint("% images are only read on Windows and macOS", image_kind_name(kind));
|
|
}
|
|
}
|
|
|
|
if orient {
|
|
orientation := 1;
|
|
if (kind == .HEIF || kind == .AVIF) && container_orientation {
|
|
// ImageIO gives the image as stored, and the container's irot and imir as an orientation.
|
|
orientation = container_orientation;
|
|
} else if kind == .HEIF || kind == .AVIF {
|
|
// The container's irot and imir are the orientation; the EXIF tag only repeats it. WIC applies
|
|
// them (HEIF requires a reader to); a quarter turn it left out shows as the sides not swapped.
|
|
heif := heif_info(bytes);
|
|
if heif.rotation == 90 || heif.rotation == 270 {
|
|
if heif.width && image.width == heif.width && image.height == heif.height {
|
|
orientation = ifx heif.rotation == 90 then 8 else 6; // irot turns anticlockwise
|
|
}
|
|
}
|
|
} else {
|
|
exif := exif_read(bytes, kind);
|
|
orientation = exif.orientation;
|
|
}
|
|
if orientation > 1 && orientation <= 8 image_orient(*image, orientation);
|
|
}
|
|
return image, true, "";
|
|
}
|
|
|
|
// Turns and flips the pixels as EXIF orientation 1-8 says, into new pixels from the default allocator.
|
|
image_orient :: (image: *Image, orientation: s64) {
|
|
if orientation <= 1 || orientation > 8 || !image.pixels return;
|
|
w, h := image.width, image.height;
|
|
swaps := orientation >= 5;
|
|
out_w := ifx swaps then h else w;
|
|
out_h := ifx swaps then w else h;
|
|
out := cast(*u32) alloc(cast(s64) w * h * 4,, context.default_allocator);
|
|
source := cast(*u32) image.pixels;
|
|
for dy: 0..out_h - 1 {
|
|
row := out + cast(s64) dy * out_w;
|
|
for dx: 0..out_w - 1 {
|
|
sx, sy: s32;
|
|
if orientation == {
|
|
case 2; sx, sy = w - 1 - dx, dy; // mirrored left to right
|
|
case 3; sx, sy = w - 1 - dx, h - 1 - dy; // half a turn
|
|
case 4; sx, sy = dx, h - 1 - dy; // mirrored top to bottom
|
|
case 5; sx, sy = dy, dx; // transposed
|
|
case 6; sx, sy = dy, h - 1 - dx; // stored a quarter turn anticlockwise: turned clockwise
|
|
case 7; sx, sy = w - 1 - dy, h - 1 - dx; // transversed
|
|
case 8; sx, sy = w - 1 - dy, dx; // stored a quarter turn clockwise: turned anticlockwise
|
|
}
|
|
row[dx] = source[cast(s64) sy * w + sx];
|
|
}
|
|
}
|
|
image_free(image);
|
|
image.* = .{pixels = cast(*u8) out, width = out_w, height = out_h};
|
|
}
|
|
|
|
// The preview an .mcap holds: the pixels scaled so the longest side is at most max_side, as a PNG
|
|
// (temp, or stb's when reused is set: see below). The width and height are the preview's.
|
|
image_preview_png :: (image: Image, max_side: s64) -> png: [] u8, width: s64, height: s64, ok: bool, error: string {
|
|
w, h := image.width, image.height;
|
|
pixels := image.pixels;
|
|
longest := max(w, h);
|
|
if longest > max_side {
|
|
scale := cast(float64) max_side / longest;
|
|
out_w := max(cast(s32) 1, cast(s32) (w * scale + 0.5));
|
|
out_h := max(cast(s32) 1, cast(s32) (h * scale + 0.5));
|
|
scaled := NewArray(cast(s64) out_w * out_h * 4, u8, initialized = false,, temp);
|
|
if !stbir_resize_uint8_srgb(pixels, w, h, 0, scaled.data, out_w, out_h, 0, 4, 3, 0) return .[], 0, 0, false, "its image cannot be scaled";
|
|
pixels = scaled.data;
|
|
w, h = out_w, out_h;
|
|
}
|
|
length: s32;
|
|
encoded := stbi_write_png_to_mem(pixels, w * 4, w, h, 4, *length);
|
|
if !encoded return .[], 0, 0, false, "the preview cannot be encoded";
|
|
// stb_image_write allocates with the C runtime's malloc, and stb_image frees with its free: both
|
|
// libraries are linked against the same runtime.
|
|
defer stbi_image_free(encoded);
|
|
return cast([] u8) copy_string(string.{length, encoded},, temp), w, h, true, "";
|
|
}
|
|
|
|
// The width and height a PNG says it has, from its header, without decoding it.
|
|
png_size :: (png: [] u8) -> width: s64, height: s64, ok: bool {
|
|
if png.count < 24 return 0, 0, false;
|
|
for u8.[0x89, #char "P", #char "N", #char "G", 0x0D, 0x0A, 0x1A, 0x0A] if png[it_index] != it return 0, 0, false;
|
|
return be32(png, 16), be32(png, 20), true;
|
|
}
|
|
|
|
#load "exif.jai";
|
|
#load "video.jai";
|
|
#if OS == .WINDOWS #load "wic.jai";
|
|
#if OS == .MACOS #load "imageio.jai";
|
|
#if OS == .WINDOWS #load "video_ffmpeg.jai";
|
|
|
|
#scope_module
|
|
|
|
be16 :: (b: [] u8, at: s64) -> u16 {
|
|
if at < 0 || at + 2 > b.count return 0;
|
|
return ((cast(u16) b[at]) << 8) | (cast(u16) b[at + 1]);
|
|
}
|
|
|
|
be32 :: (b: [] u8, at: s64) -> u32 {
|
|
if at < 0 || at + 4 > b.count return 0;
|
|
return ((cast(u32) b[at]) << 24) | ((cast(u32) b[at + 1]) << 16) | ((cast(u32) b[at + 2]) << 8) | (cast(u32) b[at + 3]);
|
|
}
|
|
|
|
be64 :: (b: [] u8, at: s64) -> u64 {
|
|
return ((cast(u64) be32(b, at)) << 32) | (cast(u64) be32(b, at + 4));
|
|
}
|
|
|
|
le32 :: (b: [] u8, at: s64) -> u32 {
|
|
if at < 0 || at + 4 > b.count return 0;
|
|
return (cast(u32) b[at]) | ((cast(u32) b[at + 1]) << 8) | ((cast(u32) b[at + 2]) << 16) | ((cast(u32) b[at + 3]) << 24);
|
|
}
|
|
|
|
// count bytes at at, cut short at the end; empty when at is past it.
|
|
slice_bytes :: (b: [] u8, at: s64, count: s64) -> [] u8 {
|
|
if at < 0 || at >= b.count || count <= 0 return .[];
|
|
result: [] u8;
|
|
result.data = b.data + at;
|
|
result.count = min(count, b.count - at);
|
|
return result;
|
|
}
|
|
|
|
ftyp_brand_kind :: (brand: [] u8) -> Image_Kind {
|
|
if brand.count != 4 return .UNKNOWN;
|
|
text := cast(string) brand;
|
|
if text == "avif" || text == "avis" return .AVIF;
|
|
if text == "heic" || text == "heix" || text == "hevc" || text == "hevx" || text == "heim" || text == "heis" || text == "mif1" || text == "msf1" return .HEIF;
|
|
if text == "crx " return .RAW; // Canon CR3
|
|
return .UNKNOWN;
|
|
}
|
|
|
|
#import "Basic";
|
|
#import "String";
|
|
#import "Math";
|
|
#import "stb_image";
|
|
#import "stb_image_write";
|
|
#import "stb_image_resize";
|
|
#if OS == .WINDOWS #import "Windows";
|