210 lines
8.5 KiB
Plaintext
210 lines
8.5 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 := ocr_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", ocr_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), ocr_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 {
|
|
data, read_ok := read_entire_file(path, log_errors = false);
|
|
if !read_ok return false, "cannot be read";
|
|
defer free(data);
|
|
m, parsed, parse_error := mcap_parse(cast([] u8) data);
|
|
if !parsed return false, parse_error;
|
|
|
|
push_allocator(temp);
|
|
sections, ok, error := mcap_ocr_parse(m.ocr);
|
|
if !ok return false, error;
|
|
replaced := false;
|
|
for * sections if it.model == section.model { it.* = section; replaced = true; break; }
|
|
if !replaced array_add(*sections, section);
|
|
m.ocr = mcap_ocr_text(sections);
|
|
m.version = MCAP_VERSION;
|
|
|
|
serializer, serialized, serialize_error := mcap_serialize(m);
|
|
if !serialized return false, serialize_error;
|
|
written, write_error := mcap_write(path, serializer);
|
|
return written, write_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_file
|
|
|
|
// Quoted, a quote written twice, a control character as a space. Temp.
|
|
ocr_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.
|
|
ocr_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 := ocr_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";
|