- Import brings in videos (MP4, MOV, MKV, WebM, AVI, WMV, MPEG, MTS/M2TS, FLV) on Windows, read with FFmpeg: the file as it was in the original, a frame a tenth of the way in (at most 3 s), turned the right way up, as the rendered image (what the grid, Capture window, OCR and labels see), and import.json with Duration, FrameRate, VideoCodec, Rotation, PosterTime, Audio, and the camera, date and location from the container. Elsewhere videos are skipped as "video (Windows only)". - .mcap version 4: the original's size is a u64, so a video can be any size. Versions 1-3 still read. mcap_read_without_original and mcap_write_streamed read and rewrite a file with its original left on disk; mcap_store, mcap_erase, the import index and the import itself never hold a video in memory. - Animated GIFs: Frames and Duration in import.json (gif_info). - Grid: a play mark and duration on video thumbnails, GIF on animations. Capture window: what the video is, and Play in the default player (written out to %TEMP%\mcap). - modules/ffmpeg: FFmpeg 6.0 bindings and the five DLLs used (avformat, avcodec, avutil, swscale, swresample), copied beside the executable by the build. swscale converts into a buffer of its own allocator with padded rows: an exact-sized one let its SIMD write past the end, crashing on frames of odd widths (1916, 1106 ...). - image-probe shows videos and GIF frame counts; ocr-bench reads a video's frame. - Tests: new video formats, display-matrix rotation, ISO 6709, video dates, GIF frames, and v4 serialize/parse, v3 compatibility, streamed writes, store/erase and extraction. SnagitConverter's copy of Capture_File needs syncing to read version 4 files.
579 lines
26 KiB
Plaintext
579 lines
26 KiB
Plaintext
// .mcap: one capture in one file. A fixed magic, a file version stamp, the capture's Capture Text,
|
|
// its OCR text (version 2), its front chunks (version 3), a small preview, the original (an image or a
|
|
// video; its size 64-bit from version 4) and the rendered image, then its back chunks (version 3). See
|
|
// README.md for the layout.
|
|
//
|
|
// Chunks hold what models add (labels, embeddings, later masks), each named by a tag and a key (the
|
|
// model's name): small ones in front, where a library browser reads them without the images, large
|
|
// ones at the back. Chunks this build does not know are kept as they are when a file is rewritten.
|
|
//
|
|
// Reading gives views into the bytes it was given; nothing is copied. Images stay PNG.
|
|
//
|
|
// An original can be larger than memory (a video), so a file is also read and written with its
|
|
// original left on disk: mcap_read_without_original and mcap_write_streamed. Storing what a scan adds
|
|
// (mcap_store and the others) works that way, copying the original across a few MB at a time.
|
|
//
|
|
// The main copy is mcap's (D:/jai_projects/active/mcap/modules/Capture_File); SnagitConverter has
|
|
// the same files.
|
|
|
|
MCAP_MAGIC :: u8.[0x89, #char "M", #char "C", #char "A", #char "P", 0x0D, 0x0A, 0x1A];
|
|
MCAP_VERSION :: 4; // what is written; every version from 1 up is read
|
|
MCAP_EXTENSION :: "mcap";
|
|
MCAP_PREVIEW_MAX_SIDE :: 320; // the preview's longest side, in pixels
|
|
|
|
Mcap_Rendered_Kind :: enum u8 {
|
|
NONE :: 0;
|
|
SAME_AS_ORIGINAL :: 1; // the rendered image is byte for byte the original, so it is stored once
|
|
OWN :: 2;
|
|
}
|
|
|
|
Mcap_Image :: struct {
|
|
width: u16;
|
|
height: u16;
|
|
png: [] u8; // empty when there is none
|
|
}
|
|
|
|
// What a model added to a capture, in its own format. A file holds one chunk per tag and key.
|
|
Mcap_Chunk :: struct {
|
|
tag: [4] u8; // what it is, in four ASCII letters, such as "labl" or "embd"
|
|
key: string; // whose: the model's name
|
|
data: [] u8;
|
|
}
|
|
|
|
Mcap :: struct {
|
|
version: u32 = MCAP_VERSION; // as read; always MCAP_VERSION when written
|
|
text: string; // Capture Text
|
|
ocr: string; // OCR text (ocr_text.jai), one section per model; "" when none, and always in version 1
|
|
front_chunks: [] Mcap_Chunk; // small: read with the front. None before version 3
|
|
preview: Mcap_Image;
|
|
original_name: string; // the original's name: in the .snagx, such as {GUID}.png, or an imported file's
|
|
original: [] u8; // PNG from a .snagx; an imported image or video in its own format
|
|
rendered_kind: Mcap_Rendered_Kind;
|
|
rendered: [] u8; // PNG; OWN only, see mcap_rendered. A video's is a frame from it
|
|
back_chunks: [] Mcap_Chunk; // large: after the images. None before version 3
|
|
}
|
|
|
|
// "labl" as a tag.
|
|
mcap_tag :: (text: string) -> [4] u8 {
|
|
assert(text.count == 4, "a chunk tag is four letters");
|
|
tag: [4] u8;
|
|
for 0..3 tag[it] = text[it];
|
|
return tag;
|
|
}
|
|
|
|
mcap_tag_text :: (tag: [4] u8) -> string {
|
|
return tprint("%", to_string(tag.data, 4));
|
|
}
|
|
|
|
// The chunk with tag and key in chunks, or null.
|
|
mcap_find_chunk :: (chunks: [] Mcap_Chunk, tag: [4] u8, key: string) -> *Mcap_Chunk {
|
|
for * chunks if mcap_same_tag(it.tag, tag) && it.key == key return it;
|
|
return null;
|
|
}
|
|
|
|
mcap_same_tag :: (a: [4] u8, b: [4] u8) -> bool {
|
|
for a if it != b[it_index] return false;
|
|
return true;
|
|
}
|
|
|
|
// The rendered image (Snagit's thumbnail.png) whichever way it is stored; empty when there is none.
|
|
mcap_rendered :: (m: Mcap) -> [] u8 {
|
|
if m.rendered_kind == {
|
|
case .SAME_AS_ORIGINAL; return m.original;
|
|
case .OWN; return m.rendered;
|
|
}
|
|
return .[];
|
|
}
|
|
|
|
// ---------------------------------------------------------------- writing
|
|
|
|
// The file's bytes. A part too large for its size field fails the whole file rather than writing one
|
|
// that cannot be read back.
|
|
mcap_serialize :: (m: Mcap) -> Serializer, ok: bool, error: string {
|
|
s, ok, error := mcap_serialize_head(m, m.original.count, extra = m.original.count + mcap_tail_size(m));
|
|
if !ok return s, false, error;
|
|
AddArray_NoSize(*s, m.original);
|
|
ok, error = mcap_add_tail(*s, m);
|
|
return s, ok, error;
|
|
}
|
|
|
|
// Writes beside path and then swaps it in, so a failure leaves any earlier file as it was.
|
|
mcap_write :: (path: string, bytes: [] u8) -> ok: bool, error: string {
|
|
temp_path := tprint("%.tmp", path);
|
|
if !write_entire_file(temp_path, bytes.data, bytes.count) return false, tprint("cannot write %", temp_path);
|
|
replaced, replace_error := mcap_replace(temp_path, path);
|
|
return replaced, replace_error;
|
|
}
|
|
|
|
// An original left in a file: size bytes at offset in path. See mcap_read_without_original.
|
|
Mcap_Original_Ref :: struct {
|
|
path: string;
|
|
offset: s64;
|
|
size: s64;
|
|
}
|
|
|
|
// Writes m to path as mcap_write does, with the original copied from original, a file, a few MB at a
|
|
// time, in place of m.original: it is never all in memory. original may be in the file at path
|
|
// itself, which is only replaced once it has been read.
|
|
mcap_write_streamed :: (path: string, m: Mcap, original: Mcap_Original_Ref) -> ok: bool, error: string {
|
|
head, ok, error := mcap_serialize_head(m, original.size, extra = mcap_tail_size(m),, temp);
|
|
if !ok return false, error;
|
|
ok, error = mcap_add_tail(*head, m);
|
|
if !ok return false, error;
|
|
tail := array_view(head, head.count - mcap_tail_size(m));
|
|
head.count -= tail.count;
|
|
|
|
temp_path := tprint("%.tmp", path);
|
|
failed := "";
|
|
{
|
|
out, opened := file_open(temp_path, for_writing = true, log_errors = false);
|
|
if !opened return false, tprint("cannot write %", temp_path);
|
|
defer file_close(*out);
|
|
source, source_opened := file_open(original.path, log_errors = false);
|
|
if !source_opened failed = tprint("cannot read %", original.path);
|
|
defer if source_opened file_close(*source);
|
|
|
|
if !failed && !file_write(*out, head.data, head.count) failed = tprint("cannot write %", temp_path);
|
|
if !failed && original.size > 0 {
|
|
if !file_set_position(source, original.offset) failed = tprint("cannot read %", original.path);
|
|
buffer := NewArray(min(original.size, MCAP_COPY_SIZE), u8, initialized = false,, context.default_allocator);
|
|
defer array_free(buffer,, context.default_allocator);
|
|
left := original.size;
|
|
while !failed && left > 0 {
|
|
count := min(left, buffer.count);
|
|
read_ok, read := file_read(source, buffer.data, count);
|
|
if !read_ok || read != count { failed = tprint("% ends before its original does", original.path); break; }
|
|
if !file_write(*out, buffer.data, count) { failed = tprint("cannot write %", temp_path); break; }
|
|
left -= count;
|
|
}
|
|
}
|
|
if !failed && !file_write(*out, tail.data, tail.count) failed = tprint("cannot write %", temp_path);
|
|
}
|
|
if failed {
|
|
file_delete(temp_path);
|
|
return false, failed;
|
|
}
|
|
replaced, replace_error := mcap_replace(temp_path, path);
|
|
return replaced, replace_error;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- reading
|
|
|
|
// A whole .mcap. The parts are views into data. Exactly the file's bytes must be consumed.
|
|
mcap_parse :: (data: [] u8) -> Mcap, ok: bool, error: string {
|
|
m: Mcap;
|
|
d := to_deserializer(data);
|
|
ok, error := mcap_read_front_part(*d, *m);
|
|
if !ok return m, false, error;
|
|
|
|
m.original_name = ReadSizedString(*d, u16);
|
|
size := mcap_read_original_size(*d, m.version);
|
|
m.original = ReadArrayView(*d, size, u8);
|
|
if !d.ok return m, false, "the file ends early (it is truncated or damaged)";
|
|
ok, error = mcap_read_after_original(*d, *m);
|
|
return m, ok, error;
|
|
}
|
|
|
|
// Only the front of the file, as a library browser needs it: the header, text, OCR text, front chunks
|
|
// and, with preview, the preview. One read of the start of the file, and a few more only when a part runs past
|
|
// it. The images are left unread and empty. The parts are views into a buffer from the context allocator.
|
|
mcap_read_front :: (path: string, preview := true) -> Mcap, ok: bool, error: string {
|
|
m: Mcap;
|
|
file, opened := file_open(path, log_errors = false);
|
|
if !opened return m, false, "cannot be opened";
|
|
defer file_close(*file);
|
|
length, length_ok := file_length(file);
|
|
if !length_ok return m, false, "cannot be read";
|
|
ok, error := mcap_read_front_file(file, length, *m, preview);
|
|
return m, ok, error;
|
|
}
|
|
|
|
// The whole file but the original, which stays on disk: the ref says where it is. When the rendered
|
|
// image is the original (SAME_AS_ORIGINAL), rendered stays empty and the ref is it too. The parts are
|
|
// views into buffers from the context allocator.
|
|
mcap_read_without_original :: (path: string) -> Mcap, Mcap_Original_Ref, ok: bool, error: string {
|
|
m: Mcap;
|
|
ref: Mcap_Original_Ref;
|
|
file, opened := file_open(path, log_errors = false);
|
|
if !opened return m, ref, false, "cannot be opened";
|
|
defer file_close(*file);
|
|
length, length_ok := file_length(file);
|
|
if !length_ok return m, ref, false, "cannot be read";
|
|
|
|
front_end: s64;
|
|
ok, error := mcap_read_front_file(file, length, *m, true, *front_end);
|
|
if !ok return m, ref, false, error;
|
|
|
|
// The original's name and size: at most 2 + 65535 + 8 bytes.
|
|
middle := NewArray(min(length - front_end, 2 + 0xFFFF + 8), u8, initialized = false);
|
|
if !middle.count || !file_set_position(file, front_end) || !file_read(file, middle.data, middle.count) return m, ref, false, "the file ends early (it is truncated or damaged)";
|
|
d := to_deserializer(middle);
|
|
m.original_name = ReadSizedString(*d, u16);
|
|
size := mcap_read_original_size(*d, m.version);
|
|
ref = .{path = path, offset = front_end + d.cursor, size = size};
|
|
if !d.ok || size > length - ref.offset return m, ref, false, "the file ends early (it is truncated or damaged)";
|
|
|
|
// What follows the original: the rendered image and the back chunks.
|
|
rest := NewArray(length - ref.offset - size, u8, initialized = false);
|
|
if rest.count && (!file_set_position(file, ref.offset + size) || !file_read(file, rest.data, rest.count)) return m, ref, false, "cannot be read";
|
|
rest_d := to_deserializer(rest);
|
|
ok, error = mcap_read_after_original(*rest_d, *m);
|
|
return m, ref, ok, error;
|
|
}
|
|
|
|
// The bytes ref points at, from the context allocator.
|
|
mcap_read_original :: (ref: Mcap_Original_Ref) -> [] u8, ok: bool {
|
|
bytes := NewArray(ref.size, u8, initialized = false);
|
|
if !ref.size return bytes, true;
|
|
file, opened := file_open(ref.path, log_errors = false);
|
|
if !opened { array_free(bytes); return .[], false; }
|
|
defer file_close(*file);
|
|
if !file_set_position(file, ref.offset) || !file_read(file, bytes.data, ref.size) { array_free(bytes); return .[], false; }
|
|
return bytes, true;
|
|
}
|
|
|
|
// The bytes ref points at, written to a file of their own at path (beside it, then swapped in), a few
|
|
// MB at a time: an original as it was imported, to open in another program.
|
|
mcap_extract_original :: (ref: Mcap_Original_Ref, path: string) -> ok: bool, error: string {
|
|
temp_path := tprint("%.tmp", path);
|
|
failed := "";
|
|
{
|
|
source, opened := file_open(ref.path, log_errors = false);
|
|
if !opened return false, tprint("cannot read %", ref.path);
|
|
defer file_close(*source);
|
|
out, out_opened := file_open(temp_path, for_writing = true, log_errors = false);
|
|
if !out_opened return false, tprint("cannot write %", temp_path);
|
|
defer file_close(*out);
|
|
if !file_set_position(source, ref.offset) failed = tprint("cannot read %", ref.path);
|
|
buffer := NewArray(max(min(ref.size, MCAP_COPY_SIZE), 1), u8, initialized = false,, context.default_allocator);
|
|
defer array_free(buffer,, context.default_allocator);
|
|
left := ref.size;
|
|
while !failed && left > 0 {
|
|
count := min(left, buffer.count);
|
|
read_ok, read := file_read(source, buffer.data, count);
|
|
if !read_ok || read != count { failed = tprint("% ends before its original does", ref.path); break; }
|
|
if !file_write(*out, buffer.data, count) { failed = tprint("cannot write %", temp_path); break; }
|
|
left -= count;
|
|
}
|
|
}
|
|
if failed {
|
|
file_delete(temp_path);
|
|
return false, failed;
|
|
}
|
|
replaced, replace_error := mcap_replace(temp_path, path);
|
|
return replaced, replace_error;
|
|
}
|
|
|
|
// The image the capture is shown as, as the Capture window shows it: the rendered one (annotations
|
|
// and all, or a video's frame), else the original; which says which, "rendered" or "original". The
|
|
// rest of the file comes with it in m, and m.original only when it is the image. A video's original
|
|
// is never read: it always has a frame.
|
|
mcap_read_shown_image :: (path: string) -> Mcap, image: [] u8, which: string, ok: bool, error: string {
|
|
m, ref, ok, error := mcap_read_without_original(path);
|
|
if !ok return m, .[], "", false, error;
|
|
if m.rendered_kind == .OWN return m, m.rendered, "rendered", true, "";
|
|
bytes, read_ok := mcap_read_original(ref);
|
|
if !read_ok return m, .[], "", false, "cannot be read";
|
|
m.original = bytes;
|
|
return m, bytes, ifx m.rendered_kind == .SAME_AS_ORIGINAL then "rendered" else "original", true, "";
|
|
}
|
|
|
|
#scope_module
|
|
|
|
MCAP_HEADER_SIZE :: 12; // magic and version
|
|
MCAP_FRONT_READ_SIZE :: 64 * 1024; // holds the header, text and preview of nearly every capture
|
|
MCAP_FRONT_READS :: 16; // reads of the front at most: each reaches past at least one more size field
|
|
MCAP_COPY_SIZE :: 4 * 1024 * 1024; // an original is copied this much at a time
|
|
|
|
// The front of file (length bytes) into m, as mcap_read_front; end, when given, is where it ends.
|
|
mcap_read_front_file :: (file: File, length: s64, m: *Mcap, preview: bool, end: *s64 = null) -> ok: bool, error: string {
|
|
wanted := min(length, MCAP_FRONT_READ_SIZE);
|
|
for attempt: 1..MCAP_FRONT_READS {
|
|
if wanted <= 0 return false, "is not an .mcap file (it is empty)";
|
|
buffer := NewArray(wanted, u8, initialized = false);
|
|
if !file_set_position(file, 0) || !file_read(file, buffer.data, wanted) { array_free(buffer); return false, "cannot be read"; }
|
|
|
|
d := to_deserializer(buffer);
|
|
ok, error := mcap_read_front_part(*d, m, preview);
|
|
if ok {
|
|
if end end.* = d.cursor;
|
|
return true, "";
|
|
}
|
|
if d.ok || wanted == length { array_free(buffer); return false, error; } // not a matter of too few bytes
|
|
|
|
// Short: the sizes read so far say how far the front reaches at least. Read that and as much
|
|
// again as the first read, so the size fields after it (the OCR, the chunk count, each chunk's
|
|
// size) come in the same read rather than one read each.
|
|
needed := mcap_front_size(buffer, preview);
|
|
array_free(buffer);
|
|
if needed <= wanted || needed > length return false, "the file ends early (it is truncated or damaged)";
|
|
wanted = min(needed + MCAP_FRONT_READ_SIZE, length);
|
|
}
|
|
return false, tprint("its front was not all read in % reads", MCAP_FRONT_READS);
|
|
}
|
|
|
|
// The original's size: a u64 from version 4, a u32 before.
|
|
mcap_read_original_size :: (d: *Deserializer, version: u32) -> s64 {
|
|
if version < 4 return cast(s64) Read(d, u32);
|
|
size := Read(d, u64);
|
|
if size > 0x7FFF_FFFF_FFFF_FFFF { d.ok = false; return 0; }
|
|
return cast,no_check(s64) size;
|
|
}
|
|
|
|
// The parts after the original: the rendered kind and image, then the back chunks, which end d.
|
|
mcap_read_after_original :: (d: *Deserializer, m: *Mcap) -> ok: bool, error: string {
|
|
m.rendered_kind = Read(d, Mcap_Rendered_Kind);
|
|
if d.ok && m.rendered_kind == .OWN m.rendered = ReadSizedArray(d, u8, u32);
|
|
if d.ok && m.version >= 3 {
|
|
ok: bool;
|
|
error: string;
|
|
m.back_chunks, ok, error = mcap_read_chunks(d);
|
|
if !ok return false, error;
|
|
}
|
|
if !d.ok return false, "the file ends early (it is truncated or damaged)";
|
|
if cast(u8) m.rendered_kind > cast(u8) Mcap_Rendered_Kind.OWN return false, tprint("the rendered image is of unknown kind %", cast(u8) m.rendered_kind);
|
|
if d.cursor != d.count return false, tprint("% bytes follow the last part", d.count - d.cursor);
|
|
return true, "";
|
|
}
|
|
|
|
// Everything up to the original's bytes, for an original of original_size bytes, in a serializer
|
|
// with room for extra bytes more.
|
|
mcap_serialize_head :: (m: Mcap, original_size: s64, extra: s64 = 0) -> Serializer, ok: bool, error: string {
|
|
size := MCAP_HEADER_SIZE + 4 + m.text.count + 4 + m.ocr.count + mcap_chunks_size(m.front_chunks) + 8 + m.preview.png.count + 2 + m.original_name.count + 8;
|
|
s := new_serializer(size + extra);
|
|
|
|
magic := MCAP_MAGIC;
|
|
AddArray_NoSize(*s, magic);
|
|
Add(*s, cast(u32) MCAP_VERSION);
|
|
|
|
if !AddString(*s, m.text, u32) return s, false, "the text is too large for its size field";
|
|
if !AddString(*s, m.ocr, u32) return s, false, "the OCR text is too large for its size field";
|
|
ok, error := mcap_add_chunks(*s, m.front_chunks);
|
|
if !ok return s, false, error;
|
|
Add(*s, m.preview.width);
|
|
Add(*s, m.preview.height);
|
|
if !AddSizedArray(*s, m.preview.png, u32) return s, false, "the preview is too large for its size field";
|
|
if !AddString(*s, m.original_name, u16) return s, false, "the original's name is too long";
|
|
Add(*s, cast(u64) original_size);
|
|
assert(s.count == size);
|
|
return s, true, "";
|
|
}
|
|
|
|
// The bytes after the original: the rendered kind and image, and the back chunks.
|
|
mcap_tail_size :: (m: Mcap) -> s64 {
|
|
size := 1 + mcap_chunks_size(m.back_chunks);
|
|
if m.rendered_kind == .OWN size += 4 + m.rendered.count;
|
|
return size;
|
|
}
|
|
|
|
mcap_add_tail :: (s: *Serializer, m: Mcap) -> ok: bool, error: string {
|
|
start := s.count;
|
|
Add(s, m.rendered_kind);
|
|
if m.rendered_kind == .OWN && !AddSizedArray(s, m.rendered, u32) return false, "the rendered image is too large for its size field";
|
|
ok, error := mcap_add_chunks(s, m.back_chunks);
|
|
if !ok return false, error;
|
|
assert(s.count - start == mcap_tail_size(m));
|
|
return true, "";
|
|
}
|
|
|
|
mcap_replace :: (temp_path: string, path: string) -> ok: bool, error: string {
|
|
if !atomic_file_replace_or_just_rename_if_destination_file_does_not_exist(temp_path, path) {
|
|
file_delete(temp_path);
|
|
return false, tprint("cannot move % to %", temp_path, path);
|
|
}
|
|
return true, "";
|
|
}
|
|
|
|
// Header, text, OCR text, front chunks and, with preview, the preview.
|
|
mcap_read_front_part :: (d: *Deserializer, m: *Mcap, preview := true) -> ok: bool, error: string {
|
|
magic := ReadArrayView(d, MCAP_MAGIC.count, u8);
|
|
if !d.ok return false, "is not an .mcap file (it is shorter than the magic)";
|
|
for MCAP_MAGIC if magic[it_index] != it return false, "is not an .mcap file (its magic is wrong)";
|
|
m.version = Read(d, u32);
|
|
if !d.ok return false, "the file ends early (it is truncated or damaged)";
|
|
if m.version == 0 return false, "has file version 0, which does not exist";
|
|
if m.version > MCAP_VERSION return false, tprint("is file version %, but this build reads up to %; it was written by a later version", m.version, MCAP_VERSION);
|
|
|
|
m.text = ReadSizedString(d, u32);
|
|
if m.version >= 2 m.ocr = ReadSizedString(d, u32); // version 1 has no OCR block
|
|
if d.ok && m.version >= 3 { // nor versions before 3 chunks
|
|
ok: bool;
|
|
error: string;
|
|
m.front_chunks, ok, error = mcap_read_chunks(d);
|
|
if !ok return false, error;
|
|
}
|
|
if !preview {
|
|
if !d.ok return false, "the file ends early (it is truncated or damaged)";
|
|
return true, "";
|
|
}
|
|
m.preview.width = Read(d, u16);
|
|
m.preview.height = Read(d, u16);
|
|
m.preview.png = ReadSizedArray(d, u8, u32);
|
|
if !d.ok return false, "the file ends early (it is truncated or damaged)";
|
|
return true, "";
|
|
}
|
|
|
|
// How many bytes the front takes, as far as the size fields in prefix tell: the whole front when
|
|
// they are all in it, otherwise how far to read to reach the next size field.
|
|
mcap_front_size :: (prefix: [] u8, preview := true) -> s64 {
|
|
d := to_deserializer(prefix);
|
|
d.cursor = MCAP_MAGIC.count;
|
|
version := Read(*d, u32);
|
|
if !d.ok return MCAP_HEADER_SIZE;
|
|
position: s64 = MCAP_HEADER_SIZE;
|
|
skip_sized :: (prefix: [] u8, position: *s64) -> reached: bool { // a u32 size and what it counts
|
|
if position.* + 4 > prefix.count { position.* += 4; return false; }
|
|
d := to_deserializer(prefix);
|
|
d.cursor = position.*;
|
|
position.* += 4 + cast(s64) Read(*d, u32);
|
|
return true;
|
|
}
|
|
if !skip_sized(prefix, *position) return position; // the text
|
|
if version >= 2 && !skip_sized(prefix, *position) return position; // the OCR text
|
|
if version >= 3 { // the front chunks
|
|
if position + 4 > prefix.count return position + 4;
|
|
d.cursor = position;
|
|
count := Read(*d, u32);
|
|
position += 4;
|
|
for 1..count {
|
|
position += 4; // the tag
|
|
if !skip_sized(prefix, *position) return position; // its size and payload
|
|
}
|
|
}
|
|
if !preview return position;
|
|
position += 4; // the preview's width and height
|
|
skip_sized(prefix, *position); // the preview
|
|
return position;
|
|
}
|
|
|
|
// Chunks: a u32 count, then each as its tag, a u32 size, and that many bytes: a u16 key size, the
|
|
// key, and the data.
|
|
mcap_chunks_size :: (chunks: [] Mcap_Chunk) -> s64 {
|
|
size: s64 = 4;
|
|
for chunks size += 4 + 4 + 2 + it.key.count + it.data.count;
|
|
return size;
|
|
}
|
|
|
|
mcap_add_chunks :: (s: *Serializer, chunks: [] Mcap_Chunk) -> ok: bool, error: string {
|
|
Add(s, cast(u32) chunks.count);
|
|
for chunks {
|
|
payload := 2 + it.key.count + it.data.count;
|
|
if it.key.count > 0xFFFF || payload > 0xFFFF_FFFF return false, tprint("the % chunk of % is too large for its size field", mcap_tag_text(it.tag), it.key);
|
|
tag := it.tag;
|
|
AddArray_NoSize(s, tag);
|
|
Add(s, cast(u32) payload);
|
|
AddString(s, it.key, u16);
|
|
AddArray_NoSize(s, it.data);
|
|
}
|
|
return true, "";
|
|
}
|
|
|
|
// Views into d's bytes; the list itself from the context allocator.
|
|
mcap_read_chunks :: (d: *Deserializer) -> [] Mcap_Chunk, ok: bool, error: string {
|
|
count := Read(d, u32);
|
|
if !d.ok return .[], false, "the file ends early (it is truncated or damaged)";
|
|
// Running out of bytes clears d.ok, as Read does: a front read then reads further, a whole parse fails.
|
|
if cast(s64) count * 10 > d.count - d.cursor { d.ok = false; return .[], false, tprint("it says it has % chunks, more than its bytes can hold", count); }
|
|
chunks := NewArray(count, Mcap_Chunk);
|
|
for * chunk: chunks {
|
|
tag := ReadArrayView(d, 4, u8);
|
|
payload := Read(d, u32);
|
|
if !d.ok || payload < 2 return chunks, false, "a chunk runs past the end of the file (it is truncated or damaged)";
|
|
if cast(s64) payload > d.count - d.cursor { d.ok = false; return chunks, false, "a chunk runs past the end of the file (it is truncated or damaged)"; }
|
|
for 0..3 chunk.tag[it] = tag[it];
|
|
chunk.key = ReadSizedString(d, u16);
|
|
if !d.ok || 2 + chunk.key.count > cast(s64) payload return chunks, false, "a chunk's key runs past the chunk";
|
|
chunk.data = ReadArrayView(d, payload - 2 - chunk.key.count, u8);
|
|
}
|
|
if !d.ok return chunks, false, "the file ends early (it is truncated or damaged)";
|
|
return chunks, true, "";
|
|
}
|
|
|
|
#scope_export
|
|
|
|
// Stores what a scan adds to the .mcap at path in one rewrite: with ocr, that OCR section in place of
|
|
// any by the same model (the others kept); and each of front, in place of any front chunk with the
|
|
// same tag and key (every other chunk kept, known to this build or not). Upgrades the file to the
|
|
// current version. Rewritten beside itself and swapped in (mcap_write_streamed, which copies the
|
|
// original across without holding it), so a failure leaves it as it was.
|
|
mcap_store :: (path: string, ocr: *Mcap_Ocr_Section = null, front: [] Mcap_Chunk = .[]) -> ok: bool, error: string {
|
|
push_allocator(temp);
|
|
m, original, parsed, parse_error := mcap_read_without_original(path);
|
|
if !parsed return false, parse_error;
|
|
|
|
if ocr {
|
|
sections, ok, error := mcap_ocr_parse(m.ocr);
|
|
if !ok return false, error;
|
|
replaced := false;
|
|
for * sections if it.model == ocr.model { it.* = ocr.*; replaced = true; break; }
|
|
if !replaced array_add(*sections, ocr.*);
|
|
m.ocr = mcap_ocr_text(sections);
|
|
}
|
|
m.front_chunks = mcap_chunks_with(m.front_chunks, front);
|
|
written, write_error := mcap_write_streamed(path, m, original);
|
|
return written, write_error;
|
|
}
|
|
|
|
// Takes out of the .mcap at path: with ocr, every OCR section; and every chunk, front or back, whose
|
|
// tag is one of tags (whatever its key). Everything else stays as it is. The file is rewritten (and
|
|
// upgraded to the current version) only when there was something to take out; changed says whether.
|
|
mcap_erase :: (path: string, ocr := false, tags: [] string = .[]) -> ok: bool, error: string, changed: bool {
|
|
push_allocator(temp);
|
|
m, original, parsed, parse_error := mcap_read_without_original(path);
|
|
if !parsed return false, parse_error, false;
|
|
|
|
changed := false;
|
|
if ocr && m.ocr.count { m.ocr = ""; changed = true; }
|
|
without :: (chunks: [] Mcap_Chunk, tags: [] string, changed: *bool) -> [] Mcap_Chunk {
|
|
kept: [..] Mcap_Chunk;
|
|
for chunks {
|
|
erase := false;
|
|
for tag: tags if mcap_same_tag(it.tag, mcap_tag(tag)) erase = true;
|
|
if erase changed.* = true;
|
|
else array_add(*kept, it);
|
|
}
|
|
return kept;
|
|
}
|
|
m.front_chunks = without(m.front_chunks, tags, *changed);
|
|
m.back_chunks = without(m.back_chunks, tags, *changed);
|
|
if !changed return true, "", false;
|
|
|
|
written, write_error := mcap_write_streamed(path, m, original);
|
|
return written, write_error, written;
|
|
}
|
|
|
|
// Stores chunk alone: in front, or with back after the images. See mcap_store.
|
|
mcap_store_chunk :: (path: string, chunk: Mcap_Chunk, back := false) -> ok: bool, error: string {
|
|
if !back {
|
|
ok, error := mcap_store(path, front = .[chunk]);
|
|
return ok, error;
|
|
}
|
|
push_allocator(temp);
|
|
m, original, parsed, parse_error := mcap_read_without_original(path);
|
|
if !parsed return false, parse_error;
|
|
m.back_chunks = mcap_chunks_with(m.back_chunks, .[chunk]);
|
|
written, write_error := mcap_write_streamed(path, m, original);
|
|
return written, write_error;
|
|
}
|
|
|
|
// chunks with each of added in place of the one with its tag and key, or after them. Temp.
|
|
mcap_chunks_with :: (chunks: [] Mcap_Chunk, added: [] Mcap_Chunk) -> [] Mcap_Chunk {
|
|
list: [..] Mcap_Chunk;
|
|
list.allocator = temp;
|
|
array_add(*list, ..chunks);
|
|
for added {
|
|
existing := mcap_find_chunk(list, it.tag, it.key);
|
|
if existing existing.* = it;
|
|
else array_add(*list, it);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
#load "ocr_text.jai";
|
|
#load "label_chunks.jai";
|
|
|
|
#import "Basic";
|
|
#import "File";
|
|
#import "File_Utilities";
|
|
#import "Serialization";
|