mcap/modules/jaison/typed.jai
Musa Mahmood 811c096dd0 [0.0.8] Import: any folder of screenshots and photos into the library, a folder for each source
- Import window (and -import SOURCE DEST): every image under a folder (PNG, JPEG, HEIC, AVIF, WebP,
  GIF, BMP, TIFF, camera RAW) becomes an .mcap holding the file as it was, a preview turned by its
  EXIF orientation, and metadata.json and import.json Capture Text (date from EXIF, the file name or
  its time; file name as title; camera as app). .snagx files become the same .mcap SnagitConverter
  makes. Duplicates across the whole library are skipped by a hash index (.import-index); videos
  and sidecars are skipped; source files are only read.
- modules/Image_Decode: stb_image, then WIC for everything else; EXIF; orientation; previews.
  tools/image_probe.jai shows what it reads.
- modules/Snagx (and modules/jaison), copied from SnagitConverter.
- The library is a folder of folders: recursive scan, a folder filter, and a folder: search prefix.
- OCR, the viewer and ocr-bench decode any image format, turned the right way up.
- Rename D:\SnagitIndex to D:\McapIndex.
2026-10-09 08:15:12 +01:00

699 lines
25 KiB
Plaintext

// Parse a JSON string into the given Type.
// All members of Type that are not present in the JSON are kept at their default values.
// All fields in the JSON that have no corresponding member in Type are ignored by default
// but you can pass ignore_unknown = false to fail instead.
json_parse_string :: (content: string, $T: Type, ignore_unknown := true, rename := rename_by_note, float_handling := Special_Float_Handling.SUPPORT_NULL) -> success: bool, T {
result: T;
if !content then return false, result;
info := type_info(T);
remainder, success := parse_value(content, cast(*u8)*result, info, ignore_unknown, "", rename=rename, float_handling=float_handling);
if !success return false, result;
remainder = trim_left(remainder, WHITESPACE_CHARS);
if remainder.count {
log_error("Unexpected trailing characters: %", remainder);
return false, result;
}
return true, result;
}
json_parse_file :: (filename: string, $T: Type, ignore_unknown := true, rename := rename_by_note, float_handling := Special_Float_Handling.SUPPORT_NULL) -> success: bool, T {
file_data, success := read_entire_file(filename);
result: T;
if !success {
log_error("Could not read file: %", filename);
return false, result;
}
defer free(file_data);
if (context.log_level >= .VERBOSE) {
log("Read file: %", success);
}
success, result = json_parse_string(file_data, T, ignore_unknown, rename=rename, float_handling=float_handling);
return success, result;
}
json_write_native :: (builder: *String_Builder, data: *void, info: *Type_Info, indent_char := "\t", ignore := ignore_by_note, rename := rename_by_note, float_handling := Special_Float_Handling.SUPPORT_NULL, level := 0) {
if info.type == {
case .BOOL;
append(builder, ifx (.*)(cast(*bool) data) "true" else "false");
case .INTEGER;
any_val: Any;
any_val.type = info;
any_val.value_pointer = data;
print_item_to_builder(builder, any_val);
case .FLOAT;
is_nan, is_infinity, is_negative: bool;
if info.runtime_size == 4 {
is_nan, is_infinity, is_negative = get_nan_or_infinity((cast(*u32) data).*);
} else if info.runtime_size == 8 {
is_nan, is_infinity, is_negative = get_nan_or_infinity((cast(*u64) data).*);
}
if is_nan || is_infinity {
append_special_float(builder, float_handling, is_nan, is_negative);
} else {
any_val: Any;
any_val.type = info;
any_val.value_pointer = data;
print_item_to_builder(builder, any_val);
}
case .ENUM;
any_val: Any;
any_val.type = info;
any_val.value_pointer = data;
append(builder, #char "\"");
print_item_to_builder(builder, any_val);
append(builder, #char "\"");
case .STRING;
json_append_escaped(builder, (.*)(cast(*string) data));
case .ARRAY;
info_array := cast(*Type_Info_Array) info;
element_size := info_array.element_type.runtime_size;
assert(element_size != -1);
stride := element_size;
array_data := data;
array_count := info_array.array_count;
if info_array.array_count == -1 {
array_count = (.*) cast(*s64) data;
array_dest: **void = data + 8;
array_data = array_dest.*;
}
append(builder, "[");
if array_data {
for 0..array_count-1 {
if indent_char.count {
append(builder, "\n");
for 0..level append(builder, indent_char);
}
json_write_native(builder, array_data, info_array.element_type, indent_char, ignore, rename, float_handling=float_handling, level + 1);
if it != array_count - 1 append(builder, ",");
array_data += stride;
}
}
if indent_char.count {
append(builder, "\n");
for 0..level-1 append(builder, indent_char);
}
append(builder, "]");
case .STRUCT;
struct_info := cast(*Type_Info_Struct) info;
if is_generic_json_value(info) {
value := cast(*JSON_Value) data;
json_write_json_value(builder, value.*, indent_char, level);
} else {
append(builder, #char "{");
first := true;
json_write_native_members(builder, data, struct_info.members, indent_char, ignore, rename, float_handling, level, *first);
if indent_char.count {
append(builder, "\n");
for 0..level-1 append(builder, indent_char);
}
append(builder, "}");
}
case .POINTER;
ptr_info := cast(*Type_Info_Pointer) info;
ptr := (.*) cast(**void) data;
if ptr {
json_write_native(builder, ptr, ptr_info.pointer_to, indent_char, ignore, rename, float_handling, level);
} else {
append(builder, "null");
}
case;
assert(false, "Unsupported type: %", info.type);
}
}
#scope_file
json_write_native_members :: (builder: *String_Builder, data: *void, members: [] Type_Info_Struct_Member, indent_char := "\t", ignore := ignore_by_note, rename: Rename_Proc, float_handling: Special_Float_Handling, level := 0, first: *bool) {
for * member: members {
if member.flags & .CONSTANT continue;
if ignore(member) continue;
if (member.type.type == .STRUCT && member.flags & .USING) {
info := cast(*Type_Info_Struct) member.type;
json_write_native_members(builder, data + member.offset_in_bytes, info.members, indent_char, ignore, rename, float_handling, level, first);
} else {
if !first.* append(builder, ",");
first.* = false;
if indent_char.count {
append(builder, "\n");
for 0..level append(builder, indent_char);
}
renamed_name := rename(member);
name := ifx renamed_name.count > 0 renamed_name else member.name;
json_append_escaped(builder, name);
append(builder, ": ");
json_write_native(builder, data + member.offset_in_bytes, member.type, indent_char, ignore, rename, float_handling, level + 1);
}
}
}
is_generic_json_value_or_pointer_to_it :: (info: *Type_Info) -> bool {
value_info := info;
if info.type == .POINTER {
pointer_info := cast(*Type_Info_Pointer) info;
value_info = pointer_info.pointer_to;
}
return is_generic_json_value(info);
}
is_generic_json_value :: (info: *Type_Info) -> bool {
return info == type_info(JSON_Value);
}
parse_value :: (to_parse: string, slot: *u8, info: *Type_Info, ignore_unknown: bool, field_name: string, rename: Rename_Proc, float_handling: Special_Float_Handling) -> remainder: string, success: bool {
remainder := trim_left(to_parse, WHITESPACE_CHARS);
if !remainder return "", false;
success := true;
prepare_slot :: (expected_type: Type_Info_Tag, info: *Type_Info, slot: *u8, to_parse: string) -> *u8, success: bool, is_generic: bool, info: *Type_Info {
value_info := info;
if info.type == .POINTER {
pointer_info := cast(*Type_Info_Pointer) info;
value_info = pointer_info.pointer_to;
}
if info.type == .ENUM {
info_enum := cast(*Type_Info_Enum)info;
value_info = info_enum.internal_type;
}
is_generic := is_generic_json_value(value_info);
if !is_generic && value_info.type != expected_type {
teaser := to_parse;
if teaser.count > 50 teaser.count = 50;
builder: String_Builder;
print_type_to_builder(*builder, info);
type_name := builder_to_string(*builder,, temp);
log_error("Cannot parse % value into type \"%\". Remaining input is: %…", expected_type, type_name, teaser);
return null, false, false, value_info;
}
if info.type == .POINTER {
value_slot := alloc(value_info.runtime_size);
initializer: (*void) #no_context;
if value_info.type == .STRUCT {
struct_info := cast(*Type_Info_Struct) value_info;
initializer = struct_info.initializer;
}
if initializer {
initializer(value_slot);
} else {
memset(value_slot, 0, value_info.runtime_size);
}
(.*)cast(**u8)slot = value_slot;
return value_slot, true, is_generic, value_info;
} else {
return slot, true, is_generic, value_info;
}
}
store_float_value :: (info: *Type_Info, slot: *u8, to_parse: string, value: float64) -> bool {
value_slot, success, is_generic, value_info := prepare_slot(.FLOAT, info, slot, to_parse);
if success {
if is_generic {
json_set(cast(*JSON_Value)value_slot, value);
} else {
if value_info.runtime_size == 4 {
((.*) cast(*float) slot) = cast(float) value;
} else {
assert(value_info.runtime_size == 8);
((.*) cast(*float64) slot) = value;
}
}
}
return success;
}
is_generic: bool;
if remainder[0] == {
case #char "n";
remainder, success = expect_and_slice(remainder, "null");
if !success return remainder, false;
if slot {
if info.type == .POINTER {
(.*)cast(**void) slot = null;
} else if info.type == .FLOAT && float_handling & .SUPPORT_NULL {
success = store_float_value(info, slot, to_parse, NAN);
} else {
builder: String_Builder;
print_type_to_builder(*builder, info);
type_name := builder_to_string(*builder,, temp);
log_error("Found NULL value for non-pointer type \"%\" of field \"%\". Keeping default value instead.", type_name, field_name);
}
}
return remainder, true;
case #char "t";
remainder, success = expect_and_slice(remainder, "true");
if !success return remainder, false;
if slot {
value_slot: *u8;
value_slot, success, is_generic = prepare_slot(.BOOL, info, slot, to_parse);
if success {
if is_generic {
json_set(cast(*JSON_Value)value_slot, true);
} else {
(.*)cast(*bool)value_slot = true;
}
}
}
case #char "f";
remainder, success = expect_and_slice(remainder, "false");
if !success return remainder, false;
if slot {
value_slot: *u8;
value_slot, success, is_generic = prepare_slot(.BOOL, info, slot, to_parse);
if success {
if is_generic {
json_set(cast(*JSON_Value)value_slot, false);
} else {
(.*)cast(*bool)value_slot = false;
}
}
}
case #char "\"";
if slot && info && info.type == .ENUM {
info_enum := cast(*Type_Info_Enum)info;
value_slot: *u8;
value_slot, success, is_generic = prepare_slot(.INTEGER, info_enum.internal_type, slot, to_parse);
remainder, success = parse_enum_string(remainder, value_slot, info_enum);
} else {
value: string;
value, remainder, success = parse_string(remainder);
stored := false;
defer if !stored free(value);
if success && slot {
if info && info.type == .FLOAT && (float_handling & .SUPPORT_STRINGS) {
if value == {
case "NaN"; success = store_float_value(info, slot, to_parse, NAN);
case "Infinity"; success = store_float_value(info, slot, to_parse, INFINITY);
case "-Infinity"; success = store_float_value(info, slot, to_parse, NEGATIVE_INFINITY);
case;
builder: String_Builder;
print_type_to_builder(*builder, info);
type_name := builder_to_string(*builder,, temp);
log_error("Found unsupported string value \"%\" for field \"%\" of type %.", value, field_name, type_name);
}
} else {
value_slot: *u8;
value_slot, success, is_generic = prepare_slot(.STRING, info, slot, to_parse);
if success {
if is_generic {
json_set(cast(*JSON_Value)value_slot, value);
} else {
(.*)cast(*string)value_slot = value;
}
stored = true;
}
}
}
}
case #char "[";
value_slot: *u8;
value_info: *Type_Info;
if slot {
value_slot, success, is_generic, value_info = prepare_slot(.ARRAY, info, slot, to_parse);
}
if success {
if is_generic {
value: [] JSON_Value;
value, remainder, success = parse_array(remainder);
json_set(cast(*JSON_Value)value_slot, value);
} else {
remainder, success = parse_array(remainder, value_slot, cast(*Type_Info_Array) value_info, ignore_unknown, rename=rename, float_handling=float_handling);
}
}
case #char "{";
value_slot: *u8;
value_info: *Type_Info;
if slot {
value_slot, success, is_generic, value_info = prepare_slot(.STRUCT, info, slot, to_parse);
}
if success {
if is_generic {
value := New(JSON_Object);
value.*, remainder, success = parse_object(remainder);
json_set(cast(*JSON_Value)value_slot, value);
} else {
remainder, success = parse_object(remainder, value_slot, cast(*Type_Info_Struct) value_info, ignore_unknown, rename=rename, float_handling=float_handling);
}
}
case;
if slot == null || info.type == .FLOAT || is_generic_json_value_or_pointer_to_it(info) {
float_value: float64;
float_value, success, remainder = string_to_float64(remainder);
if success && slot {
success = store_float_value(info, slot, to_parse, float_value);
}
} else {
if slot {
value_slot: *u8;
value_info: *Type_Info;
value_slot, success, is_generic, value_info = prepare_slot(.INTEGER, info, slot, to_parse);
if success {
if is_generic {
int_value: s64;
int_value, success, remainder = string_to_int(remainder, T = s64);
if success {
json_set(cast(*JSON_Value)value_slot, int_value);
} else {
log_error("Could not parse \"%\" as an integer.", to_parse);
}
} else {
info_int := cast(*Type_Info_Integer) value_info;
success, remainder = parse_and_write_integer(info_int, value_slot, to_parse);
}
}
} else {
int_value: s64;
int_value, success, remainder = string_to_int(remainder, T = s64);
if !success {
log_error("Could not parse \"%\" as an integer.", to_parse);
}
}
}
}
return remainder, success;
}
parse_enum_string :: (str: string, slot: *u8, info_enum: *Type_Info_Enum) -> remainder: string, success: bool {
value, remainder, success := parse_string(str);
defer free(value);
if !success return remainder, false;
// Parse by members' names
normalize_enum_value :: inline (name: string) -> string #expand {
normalized := trim(name);
if normalized.count > info_enum.name.count && starts_with(normalized, info_enum.name) && normalized[info_enum.name.count] == #char "." {
normalized = slice(normalized, info_enum.name.count+1, normalized.count-info_enum.name.count-1);
} else if starts_with(normalized, ".") {
normalized = slice(normalized, 1, normalized.count-1);
}
return normalized;
}
int_info := info_enum.internal_type;
int_value: s64;
if info_enum.enum_type_flags & .FLAGS {
values := split(value, "|",, temp);
for v: values {
name := normalize_enum_value(v);
found_name := false;
for info_enum.names {
if name == it {
found_name = true;
int_value |= info_enum.values[it_index];
break;
}
}
if !found_name {
log_error("Enum \"%\" does not contain a member named \"%\".", info_enum.name, name);
success = false;
}
}
} else {
success = false;
name := normalize_enum_value(value);
for info_enum.names {
if name == it {
int_value = info_enum.values[it_index];
success = true;
break;
}
}
if !success {
log_error("Enum \"%\" does not contain a member named \"%\".", info_enum.name, name);
}
}
if success {
if int_info.signed {
valid, low, high := Reflection.range_check_and_store(int_value, int_info, slot);
if !valid {
log_error("The value '%' is out of range. (It must be between % and %.)", int_value, low, high);
return remainder, false;
}
} else {
valid, low, high := Reflection.range_check_and_store(cast(u64) int_value, int_info, slot);
if !valid {
log_error("The value '%' is out of range. (It must be between % and %.)", int_value, low, high);
return remainder, false;
}
}
}
return remainder, success;
}
parse_array :: (str: string, slot: *u8, info: *Type_Info_Array, ignore_unknown: bool, rename: Rename_Proc, float_handling: Special_Float_Handling) -> remainder: string, success: bool {
element_size: int;
if slot {
element_size = info.element_type.runtime_size;
assert(element_size != -1, "Unknown element size");
}
assert(str[0] == #char "[", "Invalid object start %", str);
remainder := advance(str);
remainder = trim_left(remainder, WHITESPACE_CHARS);
if !remainder return "", false;
success := true;
if slot {
array: Resizable_Array;
initializer: (*void) #no_context;
if info.element_type.type == .STRUCT {
struct_info := cast(*Type_Info_Struct) info.element_type;
initializer = struct_info.initializer;
}
while true {
if remainder[0] == #char "]" break;
if array.count {
if remainder[0] != #char "," {
success = false;
break;
}
remainder = advance(remainder);
remainder = trim_left(remainder, WHITESPACE_CHARS);
if !remainder {
success = false;
break;
}
if remainder[0] == #char "]" {
// Special case for trailing comma. This would also fail in parse_value below, but with a weird error.
success = false;
break;
}
}
if info.array_count != -1 && array.count >= info.array_count {
log_error("Expected array of size %, but found array of with at least % elements\n", info.array_count, info.array_count + 1);
success = false;
break;
}
maybe_grow(*array, element_size);
element_data := array.data + array.count * element_size;
array.count += 1;
if initializer {
initializer(element_data);
} else {
memset(element_data, 0, element_size);
}
remainder, success = parse_value(remainder, element_data, info.element_type, ignore_unknown, "", rename=rename, float_handling=float_handling);
if !success break;
remainder = trim_left(remainder, WHITESPACE_CHARS);
if !remainder {
success = false;
break;
}
}
if info.array_type == .VIEW {
view := (cast(*Array_View_64) slot);
view.count = array.count;
view.data = array.data;
} else if info.array_count == -1 {
// Resizable array
(.*)(cast(*Resizable_Array) slot) = array;
} else {
// Fixed-size array
if info.array_count != array.count {
log_error("Expected array of size %, but found array of size %.\n", info.array_count, array.count);
success = false;
// Fill up the rest of the array so that we can safely copy the data. Maybe this is unnecessary, but it allows to print the successfully parsed values even in case of an error.
while array.count < info.array_count {
maybe_grow(*array, element_size);
array.count += 1;
element_data := array.data + array.count * element_size;
if initializer {
initializer(element_data);
} else {
memset(element_data, 0, element_size);
}
}
}
assert(info.array_count == array.count);
memcpy(slot, array.data, array.count * element_size);
free(array.data);
}
} else {
first := true;
while true {
if remainder[0] == #char "]" break;
if !first {
if remainder[0] != #char "," return remainder, false;
remainder = advance(remainder);
}
first = false;
remainder, success = parse_value(remainder, null, null, ignore_unknown, "", rename=rename, float_handling=float_handling);
if !success return remainder, false;
remainder = trim_left(remainder, WHITESPACE_CHARS);
if !remainder return "", false;
}
}
if !remainder || remainder[0] != #char "]" return remainder, false;
remainder = advance(remainder);
return remainder, success;
}
Member_Offset :: struct {
member: *Type_Info_Struct_Member;
offset_in_bytes: s64;
}
// This procedure is somewhat copied from Basic.get_field.
fill_member_table :: (table: *Table(string, Member_Offset), info: *Type_Info_Struct, rename: Rename_Proc, base_offset := 0) {
for * member: info.members {
offset := base_offset + member.offset_in_bytes;
name := rename(member);
assert(!table_find_pointer(table, name), "Redeclaration of member \"%\": % vs. %", name, member.*, (.*)table_find_pointer(table, name));
table_set(table, name, .{member, offset});
if (member.flags & .USING) && (member.type.type == .STRUCT) {
fill_member_table(table, cast(*Type_Info_Struct)member.type, rename, offset);
}
}
}
parse_object :: (str: string, slot: *u8, info: *Type_Info_Struct, ignore_unknown: bool, rename: Rename_Proc, float_handling: Special_Float_Handling) -> remainder: string, success: bool {
assert(str[0] == #char "{", "Invalid object start %", str);
remainder := advance(str);
remainder = trim_left(remainder, WHITESPACE_CHARS);
if !remainder return "", false;
if remainder[0] == #char "}" {
remainder = advance(remainder);
return remainder, true;
}
// @Speed: Building this table every time is pretty silly.
// We should probably either not build it at all or cache it somewhere.
member_table: Table(string, Member_Offset);
defer table_deinit(*member_table);
if info fill_member_table(*member_table, info, rename);
while true {
if !remainder || remainder[0] != #char "\"" return remainder, false;
key: string;
success: bool;
key, remainder, success = parse_string(remainder);
if !success return remainder, false;
defer free(key);
member_found, member_offset := table_find(*member_table, key);
member_slot: *u8;
member_info: *Type_Info;
if member_found {
member_slot = slot + member_offset.offset_in_bytes;
member_info = member_offset.member.type;
} else if !ignore_unknown {
log_error("Missing member % in %", key, info.*);
return remainder, false;
}
remainder = trim_left(remainder, WHITESPACE_CHARS);
if !remainder || remainder[0] != #char ":" return remainder, false;
remainder = advance(remainder);
remainder, success = parse_value(remainder, member_slot, member_info, ignore_unknown, key, rename, float_handling=float_handling);
if !success return remainder, false;
remainder = trim_left(remainder, WHITESPACE_CHARS);
if !remainder || remainder[0] != #char "," break;
remainder = advance(remainder);
remainder = trim_left(remainder, WHITESPACE_CHARS);
}
if !remainder || remainder[0] != #char "}" return remainder, false;
remainder = advance(remainder);
return remainder, true;
}
parse_and_write_integer :: (info: *Type_Info_Integer, pointer: *void, string_value: string) -> bool, remainder: string {
if info.signed {
success, remainder := parse_and_write_integer(info, pointer, string_value, signed = true);
return success, remainder;
} else {
success, remainder := parse_and_write_integer(info, pointer, string_value, signed = false);
return success, remainder;
}
}
parse_and_write_integer :: (info: *Type_Info_Integer, pointer: *void, string_value: string, $signed: bool) -> bool, remainder: string {
#if signed {
Int_Type :: s64;
} else {
Int_Type :: u64;
}
int_value, int_success, remainder := string_to_int(string_value, T = Int_Type);
if !int_success {
#if signed {
log_error("Could not parse \"%\" as an integer.", string_value);
} else {
log_error("Could not parse \"%\" as an unsigned integer.", string_value);
}
return false, remainder;
}
valid, low, high := Reflection.range_check_and_store(int_value, info, pointer);
if !valid {
log_error("The value '%' is out of range. (It must be between % and %.)", int_value, low, high);
return false, remainder;
}
return true, remainder;
}
Reflection :: #import "Reflection";