- 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.
300 lines
9.5 KiB
Plaintext
300 lines
9.5 KiB
Plaintext
// This file contains just the JSON serialization functions. See generic.jai and typed.jai for parse fuctions.
|
|
|
|
// Generates a JSON string from either a JSON_Value or any custom type.
|
|
// "indent_char" does what it says on the tin.
|
|
// "ignore" is only used for custom types to determine which properties of your custom type should be serialized.
|
|
// The default ignore function ignores all struct members that have the note @JsonIgnore.
|
|
// "rename" is used for renaming certain members.
|
|
// It gets called with the Type_Info_Struct_Member and must return the new name of the field.
|
|
// The default procedure rename members by their @JsonName note. Eg: @JsonName(renamed_member).
|
|
json_write_string :: (value: $T, indent_char := "\t", ignore := ignore_by_note, rename := rename_by_note, float_handling := Special_Float_Handling.SUPPORT_NULL) -> string {
|
|
builder: String_Builder;
|
|
defer free_buffers(*builder);
|
|
json_append_value(*builder, value, indent_char, ignore, rename, float_handling);
|
|
return builder_to_string(*builder);
|
|
}
|
|
|
|
json_write_file :: (filename: string, value: $T, indent_char := "\t", ignore := ignore_by_note, rename := rename_by_note, float_handling := Special_Float_Handling.SUPPORT_NULL) -> bool {
|
|
builder: String_Builder;
|
|
defer free_buffers(*builder);
|
|
json_append_value(*builder, value, indent_char, ignore, rename, float_handling);
|
|
return write_entire_file(filename, *builder);
|
|
}
|
|
|
|
json_append_value :: (builder: *String_Builder, val: $T, indent_char := "\t", ignore := ignore_by_note, rename := rename_by_note, float_handling := Special_Float_Handling.SUPPORT_NULL) {
|
|
#if T == JSON_Value {
|
|
json_write_json_value(builder, val, indent_char, float_handling=float_handling);
|
|
} else {
|
|
info := type_info(T);
|
|
json_write_native(builder, *val, info, indent_char, ignore, rename, float_handling=float_handling);
|
|
}
|
|
}
|
|
|
|
// This function is useful if you have a JSON template string and just want to
|
|
// safely insert a value without having to replicate the full json structure in Jai.
|
|
// The return value does NOT include quotes around the string.
|
|
//
|
|
// Example:
|
|
// JSON_TEMPLATE :: #string END
|
|
// {
|
|
// "complicated": {
|
|
// "json": {
|
|
// "structure": {
|
|
// "for_a_stupid_api": {
|
|
// "that_needs": [
|
|
// {"a_deeply_nested_value": "%1"}
|
|
// ]
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// END
|
|
// escaped_value := json_escape_string(my_unsafe_value);
|
|
// defer free(escaped_value);
|
|
// json_str := print(JSON_TEMPLATE, escaped_value);
|
|
json_escape_string :: (str: string) -> string {
|
|
builder: String_Builder;
|
|
defer free_buffers(*builder);
|
|
json_append_escaped(*builder, str);
|
|
escaped := builder_to_string(*builder);
|
|
return escaped;
|
|
}
|
|
|
|
// How shall NaN and Infinity be serialized / deserialized?
|
|
Special_Float_Handling :: enum_flags {
|
|
SUPPORT_NULL :: 0x01;
|
|
SUPPORT_STRINGS :: 0x02;
|
|
SUPPORT_ALL :: SUPPORT_NULL | SUPPORT_STRINGS;
|
|
}
|
|
|
|
Ignore_Proc :: #type (member: *Type_Info_Struct_Member) -> bool;
|
|
Rename_Proc :: #type (member: *Type_Info_Struct_Member) -> string;
|
|
|
|
ignore_by_note :: (member: *Type_Info_Struct_Member) -> bool {
|
|
for note: member.notes {
|
|
if note == "JsonIgnore" return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
rename_by_note :: (member: *Type_Info_Struct_Member) -> string {
|
|
for note: member.notes {
|
|
if !begins_with(note, "JsonName(") continue;
|
|
if note.count <= 10 || note[note.count-1] != #char ")" {
|
|
log_error("Invalid JsonName note format. Expected a name in parenthesis, but the note was \"%\".", note);
|
|
continue;
|
|
}
|
|
|
|
return slice(note, 9, note.count-10);
|
|
}
|
|
|
|
return member.name;
|
|
}
|
|
|
|
#scope_module
|
|
|
|
WHITESPACE_CHARS :: " \t\n\r";
|
|
|
|
#load "generic.jai";
|
|
#load "typed.jai";
|
|
|
|
json_append_escaped :: (builder: *String_Builder, str: string) {
|
|
remaining := str;
|
|
next_pos := index_of_illegal_string_char(remaining);
|
|
append(builder, "\"");
|
|
while (next_pos >= 0) {
|
|
append(builder, slice(remaining, 0, next_pos));
|
|
if remaining[next_pos] == {
|
|
case #char "\\";
|
|
append(builder, "\\\\");
|
|
case #char "\"";
|
|
append(builder, "\\\"");
|
|
case #char "\n";
|
|
append(builder, "\\n");
|
|
case #char "\r";
|
|
append(builder, "\\r");
|
|
case #char "\t";
|
|
append(builder, "\\t");
|
|
case;
|
|
// ToDo: handle illegal multi-byte characters
|
|
// print("Escaping: %\n\n", slice(remaining, next_pos, remaining.count - next_pos));
|
|
print_to_builder(builder, "\\u%", formatInt(remaining[next_pos], base=16, minimum_digits=4));
|
|
}
|
|
remaining = advance(remaining, next_pos + 1);
|
|
next_pos = index_of_illegal_string_char(remaining);
|
|
}
|
|
append(builder, remaining);
|
|
append(builder, "\"");
|
|
}
|
|
|
|
index_of_illegal_string_char :: (str: string) -> s64 {
|
|
for 0..str.count - 1 {
|
|
if str[it] == #char "\\" || str[it] == #char "\"" || str[it] <= 0x1F {
|
|
return it;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
expect_and_slice :: (str: string, expected: string) -> remainder: string, success: bool {
|
|
if str.count < expected.count || !equal(slice(str, 0, expected.count), expected) {
|
|
log_error("Unexpected token. Expected \"%\" but got: %", expected, str);
|
|
return str, false;
|
|
}
|
|
remainder := advance(str, expected.count);
|
|
return remainder, true;
|
|
}
|
|
|
|
parse_string :: (str: string) -> result: string, remainder: string, success: bool {
|
|
assert(str[0] == #char "\"", "Invalid string start %", str);
|
|
inside := advance(str);
|
|
if !inside return "", "", false;
|
|
needsUnescape := false;
|
|
while true {
|
|
if !inside || (inside.count < 2 && inside[0] != #char "\"") return "", str, false;
|
|
if inside[0] == #char "\"" break;
|
|
if inside[0] == #char "\\" {
|
|
needsUnescape = true;
|
|
if inside.count < 2 return "", str, false;
|
|
advance(*inside);
|
|
}
|
|
advance(*inside);
|
|
}
|
|
|
|
length := inside.data - str.data - 1;
|
|
result := slice(str, 1, length);
|
|
if needsUnescape {
|
|
success: bool;
|
|
result, success = unescape(result);
|
|
if !success return "", str, false;
|
|
} else {
|
|
result = copy_string(result);
|
|
}
|
|
|
|
remainder := slice(str, length + 2, str.count - length - 2);
|
|
return result, remainder, true;
|
|
}
|
|
|
|
unescape :: (str: string) -> result: string, success: bool {
|
|
result := alloc_string(str.count);
|
|
rc := 0;
|
|
success := true;
|
|
for i: 0..str.count-1 {
|
|
if str[i] != #char "\\" {
|
|
// Check for invalid characters for JSON
|
|
if str[i] < 0x20 {
|
|
success = false;
|
|
break;
|
|
}
|
|
|
|
result[rc] = str[i];
|
|
rc += 1;
|
|
} else {
|
|
if i == str.count - 1 {
|
|
success = false;
|
|
break;
|
|
}
|
|
|
|
i += 1;
|
|
if str[i] == {
|
|
case #char "\""; #through;
|
|
case #char "/"; #through;
|
|
case #char "\\";
|
|
result[rc] = str[i];
|
|
rc += 1;
|
|
case #char "b";
|
|
result[rc] = 0x08;
|
|
rc += 1;
|
|
case #char "f";
|
|
result[rc] = 0x0c;
|
|
rc += 1;
|
|
case #char "n";
|
|
result[rc] = #char "\n";
|
|
rc += 1;
|
|
case #char "r";
|
|
result[rc] = #char "\r";
|
|
rc += 1;
|
|
case #char "t";
|
|
result[rc] = #char "\t";
|
|
rc += 1;
|
|
case #char "u";
|
|
if i + 4 >= str.count {
|
|
success = false;
|
|
break;
|
|
}
|
|
unicode_char, unicode_success := parse_unicode(slice(str, i + 1, 4));
|
|
if !unicode_success {
|
|
success = false;
|
|
break;
|
|
}
|
|
// A high surrogate followed by \u and a low one is a single character beyond
|
|
// U+FFFF, such as an emoji: four bytes of UTF-8, not two 3-byte halves (which are
|
|
// not valid UTF-8). A surrogate without its other half is encoded on its own, as before.
|
|
if unicode_char >= 0xD800 && unicode_char <= 0xDBFF && i + 10 < str.count && str[i + 5] == #char "\\" && str[i + 6] == #char "u" {
|
|
low, low_success := parse_unicode(slice(str, i + 7, 4));
|
|
if low_success && low >= 0xDC00 && low <= 0xDFFF {
|
|
code_point := 0x10000 + ((cast(u32) unicode_char - 0xD800) << 10) + (cast(u32) low - 0xDC00);
|
|
result[rc] = xx (0xF0 | (code_point >> 18));
|
|
result[rc + 1] = xx (0x80 | ((code_point >> 12) & 0x3F));
|
|
result[rc + 2] = xx (0x80 | ((code_point >> 6) & 0x3F));
|
|
result[rc + 3] = xx (0x80 | (code_point & 0x3F));
|
|
rc += 4;
|
|
i += 10;
|
|
continue;
|
|
}
|
|
}
|
|
utf8_len := encode_utf8(unicode_char, *(result.data[rc]));
|
|
rc += utf8_len;
|
|
i += 4;
|
|
case;
|
|
success = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if !success {
|
|
free(result);
|
|
return "", false;
|
|
}
|
|
result.count = rc;
|
|
return result, true;
|
|
}
|
|
|
|
append_special_float :: (builder: *String_Builder, float_handling: Special_Float_Handling, is_nan: bool, is_negative: bool) {
|
|
if !(float_handling & .SUPPORT_STRINGS) {
|
|
append(builder, "null");
|
|
} else if is_nan {
|
|
append(builder, "\"NaN\"");
|
|
} else if is_negative {
|
|
append(builder, "\"-Infinity\"");
|
|
} else {
|
|
append(builder, "\"Infinity\"");
|
|
}
|
|
}
|
|
|
|
get_nan_or_infinity :: (value: u32) -> is_nan: bool, is_infinity: bool, is_negative: bool {
|
|
is_special := (value & 0x7F80_0000) == 0x7F80_0000;
|
|
mant := (value & 0x007F_FFFF);
|
|
return (is_special && mant), is_special && !mant, !!(value & 0x8000_0000);
|
|
}
|
|
|
|
get_nan_or_infinity :: (value: u64) -> is_nan: bool, is_infinity: bool, is_negative: bool {
|
|
is_special := (value & 0x7FF0_0000_0000_0000) == 0x7FF0_0000_0000_0000;
|
|
mant := (value & 0xF_FFFF_FFFF_FFF);
|
|
return (is_special && mant), is_special && !mant, !!(value & 0x8000_0000_0000_0000);
|
|
}
|
|
|
|
NAN :: 0h7FFF_FFFF_FFFF_FFFF;
|
|
INFINITY :: 0h7FF0_0000_0000_0000;
|
|
NEGATIVE_INFINITY :: 0h8FF0_0000_0000_0000;
|
|
|
|
#import "Basic";
|
|
#import "File";
|
|
#import "String";
|
|
|
|
#import "Hash_Table";
|
|
#import,dir "./unicode_utils";
|
|
#import "IntroSort";
|
|
|