mcap/modules/Capture_File/module.jai
Musa Mahmood 8b6602119a [0.0.12] Segmentation phase 3: segs and mask chunks in .mcap files
- modules/Capture_File/segment_chunks.jai: a front "segs" chunk (text: engine, date, the image read
  and its size, the class list's hash, and each segment's id, score, area, box and class) and a back
  "mask" chunk (a segment id per pixel, run-length encoded with LEB128 lengths, longest side at most
  1024), keyed by model. No new file version: chunks came in version 3.
- mcap_store takes back chunks too, so a model's summary and mask are stored in one rewrite.
- Tests: the text round trip, the runs' edge cases (one segment over everything, none, runs across
  rows, multi-byte lengths, id 255), bad masks refused, and storing, replacing and erasing both
  chunks in a file with other chunks kept.
- segment-probe takes .mcap files (the image each is shown as; videos skipped) and with -store stores
  its segments in them. On 27 images a mask is 0.8 to 6.4 KB.
- The format README's "Segmentation chunks" section; the plan's phase 3 results.
2026-10-10 03:44:41 +01:00

582 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 and of back, in place of any chunk there
// with the same tag and key (every other chunk kept, known to this build or not), so a model's front
// summary and back data are stored together or not at all. 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 = .[], back: [] 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);
m.back_chunks = mcap_chunks_with(m.back_chunks, back);
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";
#load "segment_chunks.jai";
#import "Basic";
#import "File";
#import "File_Utilities";
#import "Serialization";