mcap/modules/Capture_File/ocr_text.jai

193 lines
7.9 KiB
Plaintext

// OCR text: the OCR block of an .mcap (version 2), in the style of the Ex4 text formats. One section
// per model, so a capture read by several models keeps every reading; reading it again with a model
// replaces only that model's section.
//
// @version 2
// [ocr ppocr-v5-mobile]
// engine: "onnxruntime 1.30.0, CUDA"
// image: rendered 1920 1080
// date: 2026-10-08T21:04:11Z
// line: 412 96 640 18 412 96 1052 96 1052 114 412 114 0.993 "Assign Footprints - KiCad"
//
// line: x y width height, the line's four corners as x y pairs, confidence, "text". All in the pixels
// of the image named by image: (rendered or original, at that size). x y width height bound the
// corners; the corners go clockwise from the text's top left, so a turned line says which way it
// reads. Text is quoted, with "" for a quote; a control character is written as a space. A line that
// cannot be read is skipped; keys this version does not know are skipped too.
//
// Version 1 had no corners (line: x y width height confidence "text"); it is still read, its corners
// taken from its box.
MCAP_OCR_TEXT_VERSION :: 2;
Mcap_Ocr_Line :: struct {
x, y, width, height: s32;
confidence: float;
text: string;
corners: [8] s32; // x0 y0 x1 y1 x2 y2 x3 y3: top left, top right, bottom right, bottom left of the text; all 0 means the box's
}
// The line's corners: its own, or its box's when it has none.
mcap_ocr_corners :: (line: Mcap_Ocr_Line) -> [8] s32 {
for line.corners if it return line.corners;
x0, y0, x1, y1 := line.x, line.y, line.x + line.width, line.y + line.height;
return .[x0, y0, x1, y0, x1, y1, x0, y1];
}
Mcap_Ocr_Section :: struct {
model: string; // the OCR model's name, as in its models.config
engine: string; // what ran it, such as "onnxruntime 1.30.0, CUDA"
image: string; // "rendered" or "original"
image_width, image_height: s32;
date: string; // when it was read, UTC: 2026-10-08T21:04:11Z
lines: [..] Mcap_Ocr_Line;
}
// The sections of an OCR block; strings and arrays from the context allocator. "" gives none.
mcap_ocr_parse :: (text: string) -> [..] Mcap_Ocr_Section, ok: bool, error: string {
sections: [..] Mcap_Ocr_Section;
if !text.count return sections, true, "";
section: *Mcap_Ocr_Section;
version_seen := false;
version: s64;
for raw: split(text, "\n",, temp) {
line := trim(raw);
if !line.count || line[0] == #char "#" continue;
if begins_with(line, "@version") {
rest := trim(advance(line, 8));
number, ok := string_to_int(rest);
if !ok || number < 1 || number > MCAP_OCR_TEXT_VERSION return sections, false, tprint("its OCR text is version %, but this build reads up to %", rest, MCAP_OCR_TEXT_VERSION);
version = number;
version_seen = true;
continue;
}
if !version_seen return sections, false, "its OCR text does not start with @version";
if begins_with(line, "[ocr ") && ends_with(line, "]") {
section = array_add(*sections);
section.* = .{};
section.model = copy_string(trim(slice(line, 5, line.count - 6)));
continue;
}
colon := find_index_from_left(line, #char ":");
if colon < 0 || !section continue;
key := trim(slice(line, 0, colon));
value := trim(advance(line, colon + 1));
if key == {
case "engine";
quoted, _, ok := mcap_read_quoted(value);
if ok section.engine = copy_string(quoted);
case "image";
parts := split(value, " ",, temp);
if parts.count == 3 {
section.image = copy_string(parts[0]);
section.image_width = cast(s32) string_to_int(parts[1]);
section.image_height = cast(s32) string_to_int(parts[2]);
}
case "date";
section.date = copy_string(value);
case "line";
ocr_line, ok := ocr_parse_line(value, version);
if ok array_add(*section.lines, ocr_line);
}
}
return sections, true, "";
}
// The OCR block for sections. Temp.
mcap_ocr_text :: (sections: [] Mcap_Ocr_Section) -> string {
if !sections.count return "";
builder: String_Builder;
builder.allocator = temp;
print_to_builder(*builder, "@version %\n", MCAP_OCR_TEXT_VERSION);
for sections {
print_to_builder(*builder, "\n[ocr %]\n", it.model);
print_to_builder(*builder, "engine: %\n", mcap_quote(it.engine));
print_to_builder(*builder, "image: % % %\n", it.image, it.image_width, it.image_height);
print_to_builder(*builder, "date: %\n", it.date);
for line: it.lines {
c := mcap_ocr_corners(line);
print_to_builder(*builder, "line: % % % % % % % % % % % % % %\n", line.x, line.y, line.width, line.height,
c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], formatFloat(line.confidence, trailing_width = 3, zero_removal = .NO), mcap_quote(line.text));
}
}
return builder_to_string(*builder,, temp);
}
// Stores section in the .mcap at path, in place of any section by the same model, keeping the
// others, and upgrades the file to the current version. The file is rewritten beside itself and
// swapped in (mcap_write), so a failure leaves it as it was.
mcap_store_ocr :: (path: string, section: Mcap_Ocr_Section) -> ok: bool, error: string {
ok, error := mcap_store(path, *section);
return ok, error;
}
// The time now as the date: key writes it, UTC. Temp.
mcap_ocr_now :: () -> string {
t := to_calendar(current_time_consensus(), .UTC);
two :: (n: s64) -> string { return tprint("%", formatInt(n, minimum_digits = 2)); }
return tprint("%-%-%T%:%:%Z", t.year, two(t.month_starting_at_0 + 1), two(t.day_of_month_starting_at_0 + 1), two(t.hour), two(t.minute), two(t.second));
}
#scope_module // the quoting is shared with label_chunks.jai
// Quoted, a quote written twice, a control character as a space. Temp.
mcap_quote :: (text: string) -> string {
builder: String_Builder;
builder.allocator = temp;
append(*builder, #char "\"");
for 0..text.count - 1 {
c := text[it];
if c == #char "\"" append(*builder, "\"\"");
else if c < 0x20 || c == 0x7F append(*builder, #char " ");
else append(*builder, c);
}
append(*builder, #char "\"");
return builder_to_string(*builder,, temp);
}
// One quoted string ("" for a quote) at the start of s: its text (context allocator) and what follows.
mcap_read_quoted :: (s: string) -> text: string, rest: string, ok: bool {
if !s.count || s[0] != #char "\"" return "", s, false;
builder: String_Builder;
i := 1;
while i < s.count {
if s[i] == #char "\"" {
if i + 1 < s.count && s[i + 1] == #char "\"" { append(*builder, #char "\""); i += 2; continue; }
return builder_to_string(*builder), advance(s, i + 1), true;
}
append(*builder, s[i]);
i += 1;
}
free_buffers(*builder);
return "", s, false;
}
// Version 2: x y width height x0 y0 x1 y1 x2 y2 x3 y3 confidence "text". Version 1: x y width height confidence "text".
ocr_parse_line :: (value: string, version: s64) -> Mcap_Ocr_Line, bool {
result: Mcap_Ocr_Line;
rest := value;
all_numbers: [13] float64;
numbers := array_view(all_numbers, 0, ifx version >= 2 then 13 else 5);
for * numbers {
rest = trim_left(rest);
space := find_index_from_left(rest, #char " ");
if space < 0 return result, false;
number, ok := string_to_float64(slice(rest, 0, space));
if !ok return result, false;
it.* = number;
rest = advance(rest, space);
}
text, after, ok := mcap_read_quoted(trim_left(rest));
if !ok || trim(after).count return result, false;
result.x, result.y = cast(s32) numbers[0], cast(s32) numbers[1];
result.width, result.height = cast(s32) numbers[2], cast(s32) numbers[3];
result.confidence = cast(float) numbers[numbers.count - 1];
result.text = text;
if numbers.count == 13 for 0..7 result.corners[it] = cast(s32) numbers[4 + it];
result.corners = mcap_ocr_corners(result);
return result, true;
}
#import "String";