mcap/modules/jaison/generic.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

406 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Generic JSON parsing/writing functions. Result is always a JSON_Value,
// which is awful to read and even more awful to create for complex structures.
// But it’s useful for some cases where re-creating the whole JSON structure as
// custom Jai struct types is inconvenient or not possible.
// This generic interface was the very first part I wrote in Jai and hasn’t been thorougly tested.
// Tread with care. There may be dragons.
JSON_Type :: enum u8 {
NULL :: 0;
BOOLEAN :: 1;
NUMBER :: 3;
STRING :: 2;
ARRAY :: 5;
OBJECT :: 4;
}
JSON_Value :: struct {
type: JSON_Type;
union {
boolean: bool;
number: float64;
str: string;
array: [] JSON_Value;
object: *JSON_Object;
};
}
JSON_Object :: Table(string, JSON_Value);
json_free :: (using val: JSON_Value) {
if #complete type == {
case .NULL;
case .BOOLEAN;
case .NUMBER;
case .STRING;
free(str);
case .ARRAY;
for array {
json_free(it);
}
array_free(array);
case .OBJECT;
for object {
free(it_index);
json_free(it);
}
table_deinit(object);
free(object);
}
}
json_parse_file :: (filename: string) -> success: bool, JSON_Value {
file_data, success := read_entire_file(filename);
result: JSON_Value;
if !success {
log_error("Could not read file");
return false, result;
}
defer free(file_data);
if context.log_level >= .VERBOSE log("Read file: %", success);
success, result = json_parse_string(file_data);
return success, result;
}
json_parse_string :: (content: string) -> success: bool, JSON_Value {
if !content then return false, .{};
result, remainder, success := parse_value(content);
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;
}
// For debug purposes
print_val :: (using val: JSON_Value) {
if #complete type == {
case .NULL;
print("null");
case .BOOLEAN;
print("%", boolean);
case .NUMBER;
print("%", number);
case .STRING;
print("\"%\"", str);
case .ARRAY;
print("[");
for array print_val(it);
print("]");
case .OBJECT;
print("%", object.*);
}
}
json_value :: (str: string) -> JSON_Value {
val: JSON_Value;
val.type =.STRING;
val.str = str;
return val;
}
json_value :: (obj: *JSON_Object) -> JSON_Value {
val: JSON_Value;
val.type =.OBJECT;
val.object = obj;
return val;
}
json_set_null :: (val: *JSON_Value) {
val.type = .NULL;
}
json_set :: (val: *JSON_Value, value: bool) {
val.type = .BOOLEAN;
val.boolean = value;
}
json_set :: (val: *JSON_Value, value: int) {
val.type = .NUMBER;
val.number = cast(float64) value;
}
json_set :: (val: *JSON_Value, value: float64) {
val.type = .NUMBER;
val.number = value;
}
json_set :: (val: *JSON_Value, value: string) {
val.type = .STRING;
val.str = value;
}
json_set :: (val: *JSON_Value, value: [] JSON_Value) {
val.type = .ARRAY;
val.array = value;
}
json_set :: (val: *JSON_Value, value: *JSON_Object) {
val.type = .OBJECT;
val.object = value;
}
json_write_json_value :: (builder: *String_Builder, using val: JSON_Value, indent_char := "\t", level := 0, float_handling := Special_Float_Handling.SUPPORT_NULL) {
if #complete type == {
case JSON_Type.NULL;
append(builder, "null");
case JSON_Type.BOOLEAN;
append(builder, ifx boolean "true" else "false");
case JSON_Type.NUMBER;
is_nan, is_infinity, is_negative := get_nan_or_infinity((cast(*u64) *number).*);
if is_nan || is_infinity {
append_special_float(builder, float_handling, is_nan, is_negative);
} else {
print_item_to_builder(builder, number);
}
case JSON_Type.STRING;
json_append_escaped(builder, str);
case JSON_Type.ARRAY;
append(builder, "[");
for array {
if indent_char.count {
append(builder, "\n");
for 0..level append(builder, indent_char);
}
json_write_json_value(builder, it, indent_char, level + 1, float_handling);
if it_index != array.count - 1 append(builder, ",");
}
if indent_char.count {
append(builder, "\n");
for 0..level-1 append(builder, indent_char);
}
append(builder, "]");
case JSON_Type.OBJECT;
append(builder, "{");
obj := object;
keys: [..] string;
defer array_free(keys);
array_reserve(*keys, obj.count);
for v, k: obj.* {
array_add(*keys, k);
}
intro_sort(keys, compare);
for keys {
if indent_char.count {
append(builder, "\n");
for 0..level append(builder, indent_char);
}
json_append_escaped(builder, it);
append(builder, ": ");
found, v := table_find(obj, it);
assert(found, "Missing table value %", it);
json_write_json_value(builder, v, indent_char, level + 1, float_handling);
if it_index != obj.count - 1 append(builder, ",");
}
if indent_char.count {
append(builder, "\n");
for 0..level-1 append(builder, indent_char);
}
append(builder, "}");
}
}
json_set :: (obj: *JSON_Object, path: string, val: JSON_Value) -> bool {
dotpos := find_index_from_left(path, #char ".");
if dotpos == -1 {
table_set(obj, path, val);
return true;
}
next := slice(path, 0, dotpos);
remainder := advance(path, dotpos + 1);
if !next.count return false;
if !remainder.count return false;
success, next_value := table_find(obj, next);
next_obj: *JSON_Object;
if success {
if next_value.type != JSON_Type.OBJECT return false;
next_obj = xx next_value.object;
} else {
next_obj = cast(*JSON_Object) alloc(size_of(JSON_Object));
memset(next_obj, 0, size_of(JSON_Object));
next_value = json_value(next_obj);
table_add(obj, next, next_value);
}
return json_set(next_obj, remainder, val);
}
get_as :: (val: JSON_Value, $T: Type) -> T {
#insert #run () -> string {
if T == bool {
return #string END
assert(val.type == .BOOLEAN, "Expected a % but got %", T, val.type);
return val.boolean;
END;
} else if T == float || T == float64 {
return #string END
assert(val.type == .NUMBER, "Expected a % but got %", T, val.type);
return cast(T) val.number;
END;
} else if T == string {
return #string END
assert(val.type == .STRING, "Expected a % but got %", T, val.type);
return val.str;
END;
} else if T == [] JSON_Value {
return #string END
assert(val.type == .ARRAY, "Expected a % but got %", T, val.type);
return val.array;
END;
} else if T == JSON_Object {
return #string END
assert(val.type == .OBJECT, "Expected a % but got %", T, val.type);
return val.object.*;
END;
} else {
assert(false, "Unsupported type");
return "";
}
}();
}
#scope_module
// Deeper input fails rather than overflowing the stack. A debug build uses about 4 KB of stack per
// level and overflows a 1 MB thread stack near 200; web manifests nest about four deep.
MAX_NESTING :: 64;
parse_value :: (to_parse: string, depth := 0) -> JSON_Value, remainder: string, success: bool {
result: JSON_Value;
remainder := trim_left(to_parse, WHITESPACE_CHARS);
success := false;
if !remainder || depth > MAX_NESTING return result, remainder, false;
if remainder[0] == {
case #char "n";
remainder, success = expect_and_slice(remainder, "null");
if !success return result, remainder, false;
json_set_null(*result);
result.type = JSON_Type.NULL;
return result, remainder, true;
case #char "t";
remainder, success = expect_and_slice(remainder, "true");
if !success return result, remainder, false;
json_set(*result, true);
return result, remainder, true;
case #char "f";
remainder, success = expect_and_slice(remainder, "false");
if !success return result, remainder, false;
json_set(*result, false);
return result, remainder, true;
case #char "\"";
str: string;
str, remainder, success = parse_string(remainder);
json_set(*result, str);
case #char "[";
result.type = JSON_Type.ARRAY;
result.array, remainder, success = parse_array(remainder, depth + 1);
case #char "{";
obj := cast(*JSON_Object) alloc(size_of(JSON_Object));
obj.*, remainder, success = parse_object(remainder, depth + 1);
result = json_value(obj);
case;
result.type = JSON_Type.NUMBER;
result.number, success, remainder = string_to_float64(remainder);
}
return result, remainder, success;
}
parse_array:: (str: string, depth := 0) -> result: [] JSON_Value, remainder: string, success: bool {
assert(str[0] == #char "[", "Invalid object start %", str);
remainder := advance(str);
result: [..] JSON_Value;
remainder = trim_left(remainder, WHITESPACE_CHARS);
if !remainder return result, remainder, false;
if remainder[0] == #char "]" {
remainder = advance(remainder);
return result, remainder, true;
}
while true {
value: JSON_Value;
success: bool;
value, remainder, success = parse_value(remainder, depth);
if !success return result, remainder, false;
array_add(*result, value);
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 result, remainder, false;
remainder = advance(remainder);
return result, remainder, true;
}
parse_object :: (str: string, depth := 0) -> result: JSON_Object, remainder: string, success: bool {
assert(str[0] == #char "{", "Invalid object start %", str);
remainder := advance(str);
result: JSON_Object;
remainder = trim_left(remainder, WHITESPACE_CHARS);
if !remainder return result, remainder, false;
if remainder[0] == #char "}" {
remainder = advance(remainder);
return result, remainder, true;
}
table_resize(*result, 32);
while true {
if !remainder || remainder[0] != #char "\"" return result, remainder, false;
key: string;
value: JSON_Value;
success: bool;
key, remainder, success = parse_string(remainder);
if !success return result, remainder, false;
existing := table_find_pointer(*result, key);
if existing {
free(key);
return result, remainder, false;
}
remainder = trim_left(remainder, WHITESPACE_CHARS);
if !remainder || remainder[0] != #char ":" {
free(key);
return result, remainder, false;
}
remainder = advance(remainder);
remainder = trim_left(remainder, WHITESPACE_CHARS);
value, remainder, success = parse_value(remainder, depth);
if !success {
free(key);
return result, remainder, false;
}
table_add(*result, key, value);
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 result, remainder, false;
remainder = advance(remainder);
return result, remainder, true;
}