Merge pull request #2964 from dotCooCoo/fix/pdfinfo-thumbnail-page

MCP: document.pdfInfo refuses a thumbnail that is not a page number
This commit is contained in:
BFlat
2026-10-19 23:11:22 +07:00
committed by GitHub
2 changed files with 32 additions and 7 deletions
+11 -7
View File
@@ -183,15 +183,22 @@ pub(super) fn pdf_info(_: &mut Session, p: &Value) -> Result<Value> {
})
.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<Option<usize>> {
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<Value> {
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)?);
}
@@ -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 {