#![cfg(feature = "html")]
#![allow(non_snake_case)]
use printpdf::{
ops::PdfFontHandle,
units::{Mm, Pt},
wasm::api::{
Pdf_BytesToDocumentSync, Pdf_DocumentToBytesSync, Pdf_HtmlToDocumentSync,
Pdf_PageToSvgSync, Pdf_ResourcesForPageSync,
},
FontId, Op, ParsedFont, PdfDocument, PdfPage, TextItem,
};
use serde_json::{json, Value};
const ROBOTO_TTF: &[u8] = include_bytes!("../examples/assets/fonts/RobotoMedium.ttf");
fn call(f: fn(String) -> String, input: &Value) -> (u64, Value) {
let out = f(input.to_string());
let v: Value = serde_json::from_str(&out).unwrap_or_else(|e| {
panic!("API returned invalid JSON ({e}): {:?}", &out[..out.len().min(200)])
});
let status = v["status"].as_u64().expect("response must have a status");
(status, v["data"].clone())
}
fn doc_with_roboto(text: &str) -> Value {
let font = ParsedFont::from_bytes(ROBOTO_TTF, 0, &mut Vec::new()).expect("Roboto must parse");
let mut doc = PdfDocument::new("wasm-api-roundtrip");
let font_id = doc.add_font(&font);
let doc = doc.with_pages(vec![PdfPage::new(
Mm(210.0),
Mm(297.0),
show_text_ops(&font_id, text),
)]);
serde_json::to_value(&doc).expect("PdfDocument must serialize to JSON")
}
fn show_text_ops(font_id: &FontId, text: &str) -> Vec<Op> {
vec![
Op::StartTextSection,
Op::SetFont {
font: PdfFontHandle::External(font_id.clone()),
size: Pt(24.0),
},
Op::ShowText {
items: vec![TextItem::Text(text.to_string())],
},
Op::EndTextSection,
]
}
fn font_map<'a>(doc: &'a Value) -> &'a serde_json::Map<String, Value> {
doc["resources"]["fonts"]
.as_object()
.expect("resources.fonts must be a JSON object")
}
#[test]
fn empty_font_payload_fails_save_like_the_reported_demo_error() {
let mut doc = doc_with_roboto("Roboto");
let keys: Vec<String> = font_map(&doc).keys().cloned().collect();
assert!(!keys.is_empty(), "test document must contain a font");
for k in &keys {
doc["resources"]["fonts"][k]["parsed_font"] = json!("data:font/ttf;base64,");
}
let save_input = json!({ "doc": doc, "options": {} });
let (status, data) = call(Pdf_DocumentToBytesSync, &save_input);
assert_eq!(status, 1, "empty font payload must be an input-deserialization error");
let msg = data.as_str().expect("error data must be a string");
assert!(
msg.contains("failed to deserialize input"),
"error must identify the failing stage, got: {msg}"
);
assert!(
msg.contains("Failed to read font data: end of data reached unexpectedly"),
"error must surface the font parser's diagnosis, got: {msg}"
);
assert!(
msg.contains("line 1 column"),
"single-line JSON.stringify input must report a line-1 position, got: {msg}"
);
eprintln!("native repro of the demo save error: {msg}");
}
#[test]
fn document_with_font_roundtrips_through_the_demo_save_json() {
let doc = doc_with_roboto("Roboto");
assert!(!font_map(&doc).is_empty(), "document must carry an external font");
for (id, font) in font_map(&doc) {
let uri = font["parsed_font"].as_str().unwrap_or_else(|| {
panic!("font {id} must serialize as a data-URI string")
});
assert!(
uri.len() > "data:font/ttf;base64,".len() + 1000,
"font {id} serialized with a (near-)empty payload: {} chars",
uri.len()
);
}
let (status, data) = call(
Pdf_DocumentToBytesSync,
&json!({ "doc": doc, "options": { "subset_fonts": false } }),
);
assert_eq!(status, 0, "save must succeed, got error: {data}");
let b64 = data["bytes"].as_str().expect("bytes must be base64 string");
let pdf_bytes = {
use base64::Engine;
base64::prelude::BASE64_STANDARD.decode(b64).expect("valid base64")
};
assert!(pdf_bytes.starts_with(b"%PDF"), "output must be a PDF");
let (status, data) = call(Pdf_BytesToDocumentSync, &json!({ "bytes": b64, "options": {} }));
assert_eq!(status, 0, "re-parse must succeed, got error: {data}");
let reparsed = data["doc"].clone();
assert!(
!font_map(&reparsed).is_empty(),
"re-parsed document lost its fonts"
);
for (id, font) in font_map(&reparsed) {
let uri = font["parsed_font"]
.as_str()
.unwrap_or_else(|| panic!("re-parsed font {id} must be a data-URI string"));
assert!(
uri.len() > "data:font/ttf;base64,".len() + 1000,
"re-parsed font {id} has a (near-)empty program: {} chars",
uri.len()
);
}
let (status, data) = call(
Pdf_DocumentToBytesSync,
&json!({ "doc": reparsed, "options": {} }),
);
assert_eq!(status, 0, "save of re-parsed doc must succeed, got error: {data}");
}
#[test]
fn upload_of_subset_font_pdf_roundtrips() {
let doc = doc_with_roboto("Roboto");
let (status, data) = call(Pdf_DocumentToBytesSync, &json!({ "doc": doc, "options": {} }));
assert_eq!(status, 0, "save must succeed, got error: {data}");
let b64 = data["bytes"].as_str().expect("bytes must be base64 string");
let (status, data) = call(Pdf_BytesToDocumentSync, &json!({ "bytes": b64, "options": {} }));
assert_eq!(
status, 0,
"re-parse of a printpdf-subset PDF must succeed, got error: {data}"
);
let reparsed = data["doc"].clone();
assert!(!font_map(&reparsed).is_empty(), "re-parsed doc lost its fonts");
let (status, data) = call(
Pdf_DocumentToBytesSync,
&json!({ "doc": reparsed, "options": {} }),
);
assert_eq!(status, 0, "re-save must succeed, got error: {data}");
}
#[test]
fn html_to_document_response_is_always_a_json_envelope_and_roundtrips_on_success() {
use base64::Engine;
let input = json!({
"html": "<html><body><p style=\"font-family: sans-serif;\">Hello printpdf</p></body></html>",
"images": {},
"fonts": {
"RobotoMedium.ttf": base64::prelude::BASE64_STANDARD.encode(ROBOTO_TTF),
},
"options": {}
});
let (status, data) = call(Pdf_HtmlToDocumentSync, &input);
if status != 0 {
let msg = data.as_str().expect("non-zero status must carry an error string");
assert!(
!msg.is_empty(),
"error envelope must explain what failed"
);
eprintln!(
"note: html-to-document reported status {status} on this machine \
(font-dependent): {msg}"
);
return;
}
let doc = data["doc"].clone();
let pages = doc["pages"].as_array().expect("doc must have pages");
assert!(!pages.is_empty(), "html must produce at least one page");
let (status, data) = call(Pdf_DocumentToBytesSync, &json!({ "doc": doc, "options": {} }));
assert_eq!(
status, 0,
"the doc JSON produced by Pdf_HtmlToDocument must round-trip into \
Pdf_DocumentToBytes (the demo save button), got error: {data}"
);
let pdf_bytes = base64::prelude::BASE64_STANDARD
.decode(data["bytes"].as_str().expect("bytes must be base64"))
.expect("valid base64");
assert!(pdf_bytes.starts_with(b"%PDF"));
let page = &pages[0];
let (status, res_ids) = call(Pdf_ResourcesForPageSync, &json!({ "page": page }));
assert_eq!(status, 0, "resources-for-page must succeed, got error: {res_ids}");
let mut fonts = serde_json::Map::new();
for id in res_ids["fonts"].as_array().expect("fonts id list") {
let id = id.as_str().expect("font id is a string");
fonts.insert(id.to_string(), doc["resources"]["fonts"][id].clone());
}
let mut xobjects = serde_json::Map::new();
for id in res_ids["xobjects"].as_array().expect("xobject id list") {
let id = id.as_str().expect("xobject id is a string");
xobjects.insert(id.to_string(), doc["resources"]["xobjects"][id].clone());
}
let svg_input = json!({
"page": page,
"resources": { "fonts": fonts, "xobjects": xobjects, "layers": {}, "extgstates": doc["resources"]["extgstates"] },
"options": { "imageFormats": ["png", "jpeg"] }
});
let (status, data) = call(Pdf_PageToSvgSync, &svg_input);
assert_eq!(status, 0, "page-to-svg must succeed, got error: {data}");
let svg = data["svg"].as_str().expect("svg must be a string");
assert!(svg.contains("<svg"), "output must contain an <svg> root");
}
#[test]
fn viewer_flow_resources_for_page_and_page_to_svg() {
let doc = doc_with_roboto("Roboto");
let page = &doc["pages"][0];
let (status, res_ids) = call(Pdf_ResourcesForPageSync, &json!({ "page": page }));
assert_eq!(status, 0, "resources-for-page must succeed, got error: {res_ids}");
assert!(res_ids["fonts"].is_array());
assert!(res_ids["xobjects"].is_array());
assert!(res_ids["layers"].is_array());
let svg_input = json!({
"page": page,
"resources": { "fonts": {}, "xobjects": {}, "layers": {} },
"options": { "imageFormats": ["png", "jpeg"] }
});
let (status, data) = call(Pdf_PageToSvgSync, &svg_input);
assert_eq!(status, 0, "page-to-svg must succeed, got error: {data}");
assert!(data["svg"].as_str().expect("svg string").contains("<svg"));
}
#[test]
fn api_always_returns_parseable_json() {
let out = Pdf_DocumentToBytesSync("{not json".to_string());
let v: Value = serde_json::from_str(&out).expect("error path must produce valid JSON");
assert_eq!(v["status"], 1);
assert!(v["data"].as_str().is_some());
let out = Pdf_DocumentToBytesSync("{\"nope\": 1}".to_string());
let v: Value = serde_json::from_str(&out).expect("error path must produce valid JSON");
assert_eq!(v["status"], 1);
let msg = v["data"].as_str().expect("data must be the error string");
assert!(msg.contains("failed to deserialize input"));
}