// 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";