diff --git a/crates/engine/src/cmd/fileio/pdfimport.rs b/crates/engine/src/cmd/fileio/pdfimport.rs index 200cc5c44..e736e04b0 100644 --- a/crates/engine/src/cmd/fileio/pdfimport.rs +++ b/crates/engine/src/cmd/fileio/pdfimport.rs @@ -183,15 +183,22 @@ pub(super) fn pdf_info(_: &mut Session, p: &Value) -> Result { }) .collect(); let mut out = json!({"pages": pages.len(), "needsPassword": false, "pageInfo": pages}); - if let Some(n) = p.get("thumbnail").and_then(Value::as_u64) { - let page = usize::try_from(n).ok().and_then(|n| n.checked_sub(1)).filter(|i| *i < info.pages.len()); - let page = page.ok_or_else(|| bad(INFO, format!("thumbnail: no page {n} (the PDF has {})", info.pages.len())))?; + if let Some(page) = thumbnail_page(p, info.pages.len(), "PDF")? { let (doc, _) = page_document(&bytes, page, &o)?; out["thumbnail"] = json!(thumbnail(&doc, p)?); } Ok(out) } +/// The page (0-based) `document.pdfInfo`'s `thumbnail` names, from 1 to `count`; `None` when it is +/// absent or null. Any other value (0, a negative or fractional number, a string) is an error that +/// names the pages the `what` has. +fn thumbnail_page(p: &Value, count: usize, what: &str) -> Result> { + let Some(v) = p.get("thumbnail").filter(|v| !v.is_null()) else { return Ok(None) }; + let page = v.as_u64().and_then(|n| usize::try_from(n).ok()).and_then(|n| n.checked_sub(1)).filter(|i| *i < count); + page.map(Some).ok_or_else(|| bad(INFO, format!("thumbnail: no page {v} (the {what} has {count})"))) +} + /// `document.pdfInfo` of a PostScript file (an EPS, a `.ps` file, a `.ai` in PostScript form): one /// page, the page [`vectorcraft_eps::page`] reads, with no PDF boxes. Its thumbnail is the file as /// the EPS reader reads it without any editing data it carries: what the page prints, or the @@ -200,10 +207,7 @@ fn postscript_info(bytes: &[u8], p: &Value) -> Result { let page = vectorcraft_eps::page(bytes).map_err(err)?; let info = json!({"width": page.width(), "height": page.height(), "rotation": 0, "boxes": {}}); let mut out = json!({"pages": 1, "needsPassword": false, "pageInfo": [info]}); - if let Some(n) = p.get("thumbnail").and_then(Value::as_u64) { - if n != 1 { - return Err(bad(INFO, format!("thumbnail: no page {n} (the file has 1)"))); - } + if thumbnail_page(p, 1, "file")?.is_some() { let doc = vectorcraft_eps::import_with(bytes, false).map_err(err)?.document; out["thumbnail"] = json!(thumbnail(&doc, p)?); } diff --git a/crates/engine/src/cmd/fileio/tests_pdfimport.rs b/crates/engine/src/cmd/fileio/tests_pdfimport.rs index ac4e67673..f270c87b8 100644 --- a/crates/engine/src/cmd/fileio/tests_pdfimport.rs +++ b/crates/engine/src/cmd/fileio/tests_pdfimport.rs @@ -228,6 +228,27 @@ fn pdf_info_reads_postscript_as_one_page() { assert!(e.contains("no page 2"), "{e}"); } +/// `document.pdfInfo`'s `thumbnail` is a page number from 1, for a PDF and for PostScript alike: +/// any other value is an error that names the pages, and an absent or null one gives no thumbnail. +#[test] +fn pdf_info_refuses_a_thumbnail_that_is_not_a_page_number() { + let ps = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 50\n%%EndComments\n0 0 1 setrgbcolor 0 0 50 20 rectfill\n"; + let mut s = Session::new(); + for (bytes, pages) in [(&three_pages()[..], "(the PDF has 3)"), (ps, "(the file has 1)")] { + for n in [json!(-1), json!(0), json!(1.5), json!("1"), json!(true)] { + let e = s.execute("document.pdfInfo", &json!({"dataBase64": b64(bytes), "thumbnail": n})).unwrap_err().to_string(); + assert!(e.contains(&format!("thumbnail: no page {n} {pages}")), "{n}: {e}"); + } + for none in [json!({"dataBase64": b64(bytes)}), json!({"dataBase64": b64(bytes), "thumbnail": null})] { + assert!(s.execute("document.pdfInfo", &none).unwrap().get("thumbnail").is_none(), "{pages}"); + } + let v = s.execute("document.pdfInfo", &json!({"dataBase64": b64(bytes), "thumbnail": 1, "thumbnailSize": 64})).unwrap(); + let png = vectorcraft_format::base64_decode(v["thumbnail"].as_str().unwrap()).unwrap(); + let img = image::load_from_memory(&png).unwrap(); + assert_eq!(img.width().max(img.height()), 64, "{pages}"); + } +} + /// A page showing only text, with an editor's private data when `private`. fn text_page(private: bool, also: &str) -> PdfPage { PdfPage {