Merge pull request #562 from dotCooCoo/feat/vectorcraft-pdf-preference-read

vectorcraft-format: read PDF preference with document contents
This commit is contained in:
FredApps authored and GitHub committed 2026-11-07 15:37:07 +01:00
commit a1d91da34d
3 files changed
+95 -8

No files matched your search

+6
View File
@@ -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.
+46 -8
View File
@@ -188,18 +188,30 @@ impl<D> File<D> {
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<File<D>, ReadError> {
fn read<'de, D: Deserialize<'de>>(json: &'de [u8], keep: bool, read_pdf: bool) -> Result<(File<D>, 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<bool>,
present: bool,
}
/// The members of a file, in the order of the array form.
@@ -219,12 +231,13 @@ struct Members<D> {
}
/// Reads a [`File`] from a JSON object or array.
struct FileVisitor<D> {
struct FileVisitor<'a, D> {
keep: bool,
pdf: Option<&'a mut PdfPreference>,
document: PhantomData<D>,
}
impl<'de, D: Deserialize<'de>> Visitor<'de> for FileVisitor<D> {
impl<'de, D: Deserialize<'de>> Visitor<'de> for FileVisitor<'_, D> {
type Value = File<D>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
@@ -236,7 +249,7 @@ impl<'de, D: Deserialize<'de>> Visitor<'de> for FileVisitor<D> {
Ok(File { format, version, generator: None, preview: None, profiles, pdf: None, document, images })
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<File<D>, A::Error> {
fn visit_map<A: MapAccess<'de>>(mut self, mut map: A) -> Result<File<D>, 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::<String>()? {
@@ -249,6 +262,16 @@ impl<'de, D: Deserialize<'de>> Visitor<'de> for FileVisitor<D> {
"generator" if self.keep => generator = map.next_value::<Lenient<String>>()?.0,
"preview" if self.keep => preview = map.next_value::<Lenient<Image>>()?.0,
"pdf" if self.keep => pdf = map.next_value::<Lenient<Image>>()?.0,
"pdfCompatible" if self.pdf.is_some() => {
if let Some(state) = self.pdf.as_mut() {
state.preference = map.next_value::<Option<bool>>()?;
}
}
"pdf" if self.pdf.is_some() => {
if let Some(state) = self.pdf.as_mut() {
state.present = map.next_value::<Option<IgnoredAny>>()?.is_some();
}
}
_ => {
map.next_value::<IgnoredAny>()?;
}
@@ -339,10 +362,25 @@ impl<D> Contents<D> {
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
@@ -252,6 +252,49 @@ fn contents_read_the_document_images_and_profiles_without_the_preview_and_pdf()
assert!(Contents::<Doc>::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::<Doc>::read_with_pdf_preference(json.as_bytes()).unwrap();
assert_eq!(compatible, expected, "{members}");
assert_eq!(contents, Contents::<Doc>::read(json.as_bytes()).unwrap());
}
let array = br#"["drawcraft",1,{"layers":[]}]"#;
assert!(!Contents::<Doc>::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::<Doc>::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::<Doc>::read(invalid).is_ok());
assert!(File::<Doc>::read(invalid).is_ok());
assert!(Contents::<Doc>::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::<Doc>::read(json).unwrap_err().to_string(), Contents::<Doc>::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}"#;