mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 20:01:43 +00:00
Native files: open reads the PDF preference in the same pass as the document
Opening a native file read its JSON twice: once for the document and once more for the saved pdfCompatible preference. The format crate now takes the preference from craft-vectorcraft-format's Contents::read_with_pdf_preference (craft-libs#562), so a file is read in one pass. That method refuses a pdfCompatible that is not a boolean, which main opens by skipping the value and falling back to whether the file carries a PDF. Such a file, or one with any other data error, is read again with Contents::read and scanned for the preference as before, so the files that open today still open with the same result. The craft-vectorcraft-format pin moves to 1b34eba41e, the craft-libs revision craft-update already uses.
This commit is contained in:
@@ -510,20 +510,32 @@ fn load_file_within(bytes: &[u8], max_nodes: usize, max_values: usize) -> Result
|
||||
// at `MAX_READ_ANCHORS` anchors together, the document at `MAX_READ_NODES` objects, and the
|
||||
// fields read before an object's type at `MAX_BUFFERED_VALUES` values. The preview and the
|
||||
// PDF are skipped without being kept.
|
||||
let read = || {
|
||||
vectorcraft_doc::node::with_node_budget(max_nodes, || {
|
||||
vectorcraft_doc::node::with_buffered_value_budget(max_values, || Contents::<Document>::read(file.json()))
|
||||
fn within_budgets<T>(max_nodes: usize, max_values: usize, read: impl FnOnce() -> T) -> T {
|
||||
vectorcraft_doc::geom::path::with_anchor_budget(vectorcraft_doc::geom::path::MAX_READ_ANCHORS, || {
|
||||
vectorcraft_doc::node::with_node_budget(max_nodes, || vectorcraft_doc::node::with_buffered_value_budget(max_values, read))
|
||||
})
|
||||
};
|
||||
let mut f = vectorcraft_doc::geom::path::with_anchor_budget(vectorcraft_doc::geom::path::MAX_READ_ANCHORS, read).map_err(|e| match e {
|
||||
}
|
||||
let read_error = |e| match e {
|
||||
ReadError::Json(e) => {
|
||||
let why = if e.to_string().contains("recursion limit") { format!("its objects are nested too deep ({e})") } else { e.to_string() };
|
||||
FormatError::NotVectorcraft(why)
|
||||
}
|
||||
ReadError::Envelope(e) => e.into(),
|
||||
e => FormatError::NotVectorcraft(e.to_string()),
|
||||
})?;
|
||||
let pdf_compatible = pdf_member(file.json()).map_err(|e| FormatError::NotVectorcraft(e.to_string()))?;
|
||||
};
|
||||
// The document and the saved PDF preference come from one pass over the JSON.
|
||||
let (mut f, pdf_compatible) = match within_budgets(max_nodes, max_values, || Contents::<Document>::read_with_pdf_preference(file.json())) {
|
||||
Ok(read) => read,
|
||||
// That pass refuses a `pdfCompatible` that is not a boolean, and any other data error.
|
||||
// The file is read again without the preference, and only then is its JSON scanned for
|
||||
// the preference a second time, so such a file opens as before.
|
||||
Err(ReadError::Json(e)) if e.is_data() => {
|
||||
let f = within_budgets(max_nodes, max_values, || Contents::<Document>::read(file.json())).map_err(read_error)?;
|
||||
let pdf_compatible = pdf_member(file.json()).map_err(|e| FormatError::NotVectorcraft(e.to_string()))?;
|
||||
(f, pdf_compatible)
|
||||
}
|
||||
Err(e) => return Err(read_error(e)),
|
||||
};
|
||||
let info = FileInfo { version: f.version, legacy: f.format == LEGACY_EXTENSION, pdf_compatible };
|
||||
// A version 1 document saved before VectorCraft 0.21.0, or for an app before it (an older file
|
||||
// version): its Twist angles turned the other way. The app that wrote it is read from the JSON
|
||||
@@ -615,9 +627,17 @@ fn load_file_within(bytes: &[u8], max_nodes: usize, max_values: usize) -> Result
|
||||
Ok(NativeFile { doc, depth_notes, info, profiles })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// Tests: how many times a file's JSON was scanned again for its PDF preference on this thread.
|
||||
static PDF_MEMBER_SCANS: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// The saved PDF preference, falling back to the presence of its payload without retaining or
|
||||
/// decoding it. The array form has neither member. Repeated members use their last value.
|
||||
fn pdf_member(json: &[u8]) -> Result<bool, serde_json::Error> {
|
||||
#[cfg(test)]
|
||||
PDF_MEMBER_SCANS.with(|n| n.set(n.get() + 1));
|
||||
struct PdfMember;
|
||||
impl<'de> Visitor<'de> for PdfMember {
|
||||
type Value = bool;
|
||||
@@ -801,6 +821,32 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_a_preference_that_is_not_a_boolean_scans_the_json_again() {
|
||||
let scans_for = |bytes: &[u8]| {
|
||||
PDF_MEMBER_SCANS.with(|n| n.set(0));
|
||||
let compatible = load_info(bytes).unwrap().1.pdf_compatible;
|
||||
(compatible, PDF_MEMBER_SCANS.with(|n| n.get()))
|
||||
};
|
||||
let d = Document::new(10.0, 10.0);
|
||||
assert_eq!(scans_for(&save(&d, false)), (false, 0));
|
||||
assert_eq!(scans_for(&save(&d, true)), (false, 0));
|
||||
for compatible in [false, true] {
|
||||
let bytes = save_with(&d, &SaveOptions { pdf_compatible: Some(compatible), ..SaveOptions::default() }).unwrap();
|
||||
assert_eq!(scans_for(&bytes), (compatible, 0));
|
||||
}
|
||||
let with_pdf = save_with(&d, &SaveOptions { pdf: Some(vec![7; 1024]), ..SaveOptions::default() }).unwrap();
|
||||
assert_eq!(scans_for(&with_pdf), (true, 0));
|
||||
let mut value: serde_json::Value = serde_json::from_slice(&save(&d, false)).unwrap();
|
||||
for odd in [serde_json::json!("yes"), serde_json::json!(1), serde_json::json!([true])] {
|
||||
value["pdfCompatible"] = odd.clone();
|
||||
assert_eq!(scans_for(&serde_json::to_vec(&value).unwrap()), (false, 1), "{odd}");
|
||||
}
|
||||
// A document that can't be read still fails, after the second read.
|
||||
value["document"] = serde_json::json!("not a document");
|
||||
assert!(load_info(&serde_json::to_vec(&value).unwrap()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_info_keeps_the_pdf_preference_without_artboards() {
|
||||
let mut d = Document::new(10.0, 10.0);
|
||||
|
||||
Reference in New Issue
Block a user