diff --git a/crates/vectorcraft-format/README.md b/crates/vectorcraft-format/README.md index e69a3ac9..4d1ea34e 100644 --- a/crates/vectorcraft-format/README.md +++ b/crates/vectorcraft-format/README.md @@ -34,6 +34,12 @@ reads one, and `Contents::read(json)` reads `format`, `version`, `document`, `im `profiles` and skips `generator`, `preview` and `pdf` as the JSON is read, so their base64 is not buffered or kept. Both read the files VectorCraft opens: +`Contents::read_with_pdf_preference(json)` returns `(contents, pdf_compatible)` in that same +envelope pass. It reads an optional boolean `pdfCompatible`, falling back to a non-null `pdf` +member when the preference is absent or null. It neither retains nor decodes the PDF payload. +Repeated PDF members use their last value, and the array form returns false. A non-boolean +preference is an error in this opt-in method; `Contents::read` keeps ignoring that field. + - They return `ReadError::Envelope(Error::Format)` for another format name and `ReadError::Envelope(Error::TooNew)` for a version newer than `VERSION`, after the JSON reads; `ReadError::Json` otherwise. diff --git a/crates/vectorcraft-format/src/lib.rs b/crates/vectorcraft-format/src/lib.rs index 8b4892f2..3c9ff4e1 100644 --- a/crates/vectorcraft-format/src/lib.rs +++ b/crates/vectorcraft-format/src/lib.rs @@ -188,18 +188,30 @@ impl File { where D: Deserialize<'de>, { - read(json, true) + read(json, true, false).map(|(file, _)| file) } } /// [`File::read`] and [`Contents::read`]; `keep` is false for [`Contents`], which skips /// `generator`, `preview` and `pdf` without keeping them. -fn read<'de, D: Deserialize<'de>>(json: &'de [u8], keep: bool) -> Result, ReadError> { +fn read<'de, D: Deserialize<'de>>(json: &'de [u8], keep: bool, read_pdf: bool) -> Result<(File, bool), ReadError> { let mut de = serde_json::Deserializer::from_slice(json); - let file = serde::Deserializer::deserialize_struct(&mut de, "File", MEMBERS, FileVisitor { keep, document: PhantomData })?; + let mut pdf = PdfPreference::default(); + let file = serde::Deserializer::deserialize_struct( + &mut de, + "File", + MEMBERS, + FileVisitor { keep, pdf: read_pdf.then_some(&mut pdf), document: PhantomData }, + )?; de.end()?; file.check()?; - Ok(file) + Ok((file, pdf.preference.unwrap_or(pdf.present))) +} + +#[derive(Default)] +struct PdfPreference { + preference: Option, + present: bool, } /// The members of a file, in the order of the array form. @@ -219,12 +231,13 @@ struct Members { } /// Reads a [`File`] from a JSON object or array. -struct FileVisitor { +struct FileVisitor<'a, D> { keep: bool, + pdf: Option<&'a mut PdfPreference>, document: PhantomData, } -impl<'de, D: Deserialize<'de>> Visitor<'de> for FileVisitor { +impl<'de, D: Deserialize<'de>> Visitor<'de> for FileVisitor<'_, D> { type Value = File; fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -236,7 +249,7 @@ impl<'de, D: Deserialize<'de>> Visitor<'de> for FileVisitor { Ok(File { format, version, generator: None, preview: None, profiles, pdf: None, document, images }) } - fn visit_map>(self, mut map: A) -> Result, A::Error> { + fn visit_map>(mut self, mut map: A) -> Result, A::Error> { let (mut format, mut version, mut document, mut images, mut profiles) = (None, None, None, None, None); let (mut generator, mut preview, mut pdf) = (None, None, None); while let Some(key) = map.next_key::()? { @@ -249,6 +262,16 @@ impl<'de, D: Deserialize<'de>> Visitor<'de> for FileVisitor { "generator" if self.keep => generator = map.next_value::>()?.0, "preview" if self.keep => preview = map.next_value::>()?.0, "pdf" if self.keep => pdf = map.next_value::>()?.0, + "pdfCompatible" if self.pdf.is_some() => { + if let Some(state) = self.pdf.as_mut() { + state.preference = map.next_value::>()?; + } + } + "pdf" if self.pdf.is_some() => { + if let Some(state) = self.pdf.as_mut() { + state.present = map.next_value::>()?.is_some(); + } + } _ => { map.next_value::()?; } @@ -339,10 +362,25 @@ impl Contents { where D: Deserialize<'de>, { - let File { format, version, document, images, profiles, .. } = read(json, false)?; + let (file, _) = read(json, false, false)?; + let File { format, version, document, images, profiles, .. } = file; Ok(Self { format, version, document, images, profiles }) } + /// Reads the document and its saved PDF preference in one envelope pass, without + /// keeping or decoding the PDF payload. A non-null `pdf` supplies the fallback when + /// `pdfCompatible` is absent or null. Repeated members use their last value; arrays + /// have no PDF preference and return false. A non-boolean preference is an error. + /// The existing [`Self::read`] continues to ignore that optional preference. + pub fn read_with_pdf_preference<'de>(json: &'de [u8]) -> Result<(Self, bool), ReadError> + where + D: Deserialize<'de>, + { + let (file, compatible) = read(json, false, true)?; + let File { format, version, document, images, profiles, .. } = file; + Ok((Self { format, version, document, images, profiles }, compatible)) + } + /// [`File::is_legacy`]. pub fn is_legacy(&self) -> bool { self.format == LEGACY_EXTENSION diff --git a/crates/vectorcraft-format/tests/envelope.rs b/crates/vectorcraft-format/tests/envelope.rs index 1712a99b..80591e14 100644 --- a/crates/vectorcraft-format/tests/envelope.rs +++ b/crates/vectorcraft-format/tests/envelope.rs @@ -252,6 +252,49 @@ fn contents_read_the_document_images_and_profiles_without_the_preview_and_pdf() assert!(Contents::::read(no_document).is_err()); } +#[test] +fn contents_read_pdf_preference_while_skipping_the_payload() { + for (members, expected) in [ + ("", false), + (r#", "pdf":null"#, false), + (r#", "pdf":{"data":"not base64"}"#, true), + (r#", "pdf":[1,2,3]"#, true), + (r#", "pdfCompatible":true"#, true), + (r#", "pdf":{}, "pdfCompatible":false"#, false), + (r#", "pdfCompatible":false, "pdf":{}"#, false), + (r#", "pdfCompatible":true, "pdfCompatible":null, "pdf":{}"#, true), + (r#", "pdf":{}, "pdf":null"#, false), + (r#", "pdfCompatible":false, "pdfCompatible":true"#, true), + (r#", "unknown":{"pdf":{}}"#, false), + ] { + let json = format!(r#"{{"format":"vectorcraft","version":4,"document":{{"layers":[]}}{members}}}"#); + let (contents, compatible) = Contents::::read_with_pdf_preference(json.as_bytes()).unwrap(); + assert_eq!(compatible, expected, "{members}"); + assert_eq!(contents, Contents::::read(json.as_bytes()).unwrap()); + } + let array = br#"["drawcraft",1,{"layers":[]}]"#; + assert!(!Contents::::read_with_pdf_preference(array).unwrap().1); + let mut file = File::new(doc()); + file.pdf = Some(Image::inline("application/pdf", &vec![7; 128 << 10])); + assert!(Contents::::read_with_pdf_preference(&serde_json::to_vec(&file).unwrap()).unwrap().1); +} + +#[test] +fn pdf_preference_read_is_opt_in_and_preserves_envelope_validation() { + let invalid = br#"{"format":"vectorcraft","version":4,"document":{"layers":[]},"pdfCompatible":"invalid"}"#; + assert!(Contents::::read(invalid).is_ok()); + assert!(File::::read(invalid).is_ok()); + assert!(Contents::::read_with_pdf_preference(invalid).is_err()); + for json in [ + br#"{"format":"other","version":4,"document":{"layers":[]}}"#.as_slice(), + br#"{"format":"vectorcraft","version":999,"document":{"layers":[]}}"#.as_slice(), + br#"{"format":"vectorcraft","version":4}"#.as_slice(), + br#"{"format":"vectorcraft","version":4,"version":4,"document":{"layers":[]}}"#.as_slice(), + ] { + assert_eq!(Contents::::read(json).unwrap_err().to_string(), Contents::::read_with_pdf_preference(json).unwrap_err().to_string()); + } +} + #[test] fn reading_refuses_another_format_and_a_newer_version() { let newer = br#"{"format":"vectorcraft","version":5,"document":0}"#;