mcap/modules/Capture_File/module.jai

191 lines
8.6 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), a small preview, then the images. See docs/mcap.md for the layout.
//
// Reading gives views into the bytes it was given; nothing is copied. Images stay PNG.
//
// 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 :: 2; // 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
}
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
preview: Mcap_Image;
original_name: string; // the original image's name in the .snagx, such as {GUID}.png
original: [] u8; // PNG
rendered_kind: Mcap_Rendered_Kind;
rendered: [] u8; // PNG; OWN only, see mcap_rendered
}
// 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 {
size := MCAP_HEADER_SIZE + 4 + m.text.count + 4 + m.ocr.count + 8 + m.preview.png.count + 2 + m.original_name.count + 4 + m.original.count + 1;
if m.rendered_kind == .OWN size += 4 + m.rendered.count;
s := new_serializer(size);
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";
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 image's name is too long";
if !AddSizedArray(*s, m.original, u32) return s, false, "the original image is too large for its size field";
Add(*s, m.rendered_kind);
if m.rendered_kind == .OWN && !AddSizedArray(*s, m.rendered, u32) return s, false, "the rendered image is too large for its size field";
assert(s.count == size);
return s, true, "";
}
// 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);
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, "";
}
// ---------------------------------------------------------------- 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);
m.original = ReadSizedArray(*d, u8, u32);
m.rendered_kind = Read(*d, Mcap_Rendered_Kind);
if d.ok && m.rendered_kind == .OWN m.rendered = ReadSizedArray(*d, u8, u32);
if !d.ok return m, false, "the file ends early (it is truncated or damaged)";
if cast(u8) m.rendered_kind > cast(u8) Mcap_Rendered_Kind.OWN return m, false, tprint("the rendered image is of unknown kind %", cast(u8) m.rendered_kind);
if d.cursor != d.count return m, false, tprint("% bytes follow the last part", d.count - d.cursor);
return m, true, "";
}
// Only the front of the file, as a library browser needs it: the header, text, OCR text 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";
wanted := min(length, MCAP_FRONT_READ_SIZE);
for attempt: 1..5 { // each further read reaches at least one more size field
buffer := NewArray(wanted, u8, initialized = false);
if !file_read(file, buffer.data, wanted) { array_free(buffer); return m, false, "cannot be read"; }
d := to_deserializer(buffer);
ok, error := mcap_read_front_part(*d, *m, preview);
if ok return m, true, "";
if d.ok || wanted == length { array_free(buffer); return m, false, error; } // not a matter of too few bytes
// Short: the sizes read so far say how much is needed.
needed := mcap_front_size(buffer, preview);
array_free(buffer);
if needed <= wanted || needed > length return m, false, "the file ends early (it is truncated or damaged)";
wanted = needed;
if !file_set_position(file, 0) return m, false, "cannot be read";
}
return m, false, "cannot be read";
}
#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
// Header, text, OCR text 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 !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 !preview return position;
position += 4; // the preview's width and height
skip_sized(prefix, *position); // the preview
return position;
}
#load "ocr_text.jai";
#import "Basic";
#import "File";
#import "File_Utilities";
#import "Serialization";