use serde_json::json;
use crate::Session;
fn unzip(b64: &str) -> zip::ZipArchive>> {
zip::ZipArchive::new(std::io::Cursor::new(super::super::file::base64_decode(b64))).unwrap()
}
fn read(z: &mut zip::ZipArchive>>, name: &str) -> String {
let mut s = String::new();
std::io::Read::read_to_string(&mut z.by_name(name).unwrap(), &mut s).unwrap();
s
}
/// Split Document is stored with the document as one undo step, follows a renamed style, and
/// an export can override it without storing its own.
#[test]
fn epub_split_document_setting() {
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
let r = s.execute("frame.create", &json!({"rect": [72, 72, 400, 300], "content": "text", "text": "One\nText\nTwo\nMore"})).unwrap();
let sid = r["story"].as_u64().unwrap();
s.execute("style.paragraph.create", &json!({"name": "Chapter"})).unwrap();
for at in [0, 9] {
s.execute("text.select", &json!({"story": sid, "anchor": at, "focus": at})).unwrap();
s.execute("style.paragraph.apply", &json!({"name": "Chapter"})).unwrap();
}
let files = |s: &mut Session, p: serde_json::Value| {
let mut z = unzip(s.execute("file.exportEpub", &p).unwrap()["base64"].as_str().unwrap());
(1..).take_while(|n| z.by_name(&if *n == 1 { "OEBPS/content.xhtml".into() } else { format!("OEBPS/content{n}.xhtml") }).is_ok()).count()
};
assert_eq!(s.execute("document.epubSplit", &json!({})).unwrap()["split"], "off");
assert_eq!(files(&mut s, json!({})), 1, "one content file by default");
assert!(s.execute("document.epubSplit", &json!({"split": {"style": "Nope"}})).is_err());
assert!(s.execute("document.epubSplit", &json!({"split": "h1"})).is_err());
assert!(s.execute("file.exportEpub", &json!({"split": {"style": "Nope"}})).is_err());
assert_eq!(s.execute("document.epubSplit", &json!({"split": {"style": "Chapter"}})).unwrap()["split"], json!({"style": "Chapter"}));
assert_eq!(files(&mut s, json!({})), 2);
assert_eq!(files(&mut s, json!({"split": "off"})), 1);
assert_eq!(s.doc().unwrap().doc.settings.epub_split, designcraft_doc::EpubSplit::Style("Chapter".into()), "the override isn't stored");
s.execute("style.paragraph.edit", &json!({"name": "Chapter", "rename": "Part"})).unwrap();
assert_eq!(s.execute("document.epubSplit", &json!({})).unwrap()["split"], json!({"style": "Part"}));
assert_eq!(files(&mut s, json!({})), 2);
s.execute("edit.undo", &json!({})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.execute("document.epubSplit", &json!({})).unwrap()["split"], "off");
}
#[test]
fn object_export_options() {
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
// A plain rectangle rasterised for EPUB, centred, after a page break.
let r = s.execute("frame.create", &json!({"rect": [72, 72, 200, 200], "shape": "ellipse"})).unwrap();
s.execute("object.fill", &json!({"swatch": "[Black]", "ids": [r["id"]]})).unwrap();
s.execute("object.exportOptions", &json!({"ids": [r["id"]], "rasterize": true, "align": "center", "pageBreakBefore": true, "altText": "A dot"}))
.unwrap();
let e = s.execute("file.exportEpub", &json!({})).unwrap();
let mut z = unzip(e["base64"].as_str().unwrap());
let x = read(&mut z, "OEBPS/content.xhtml");
assert!(x.contains("
"));
assert!(z.by_name("OEBPS/images/cover.png").is_ok());
let r = s.execute("file.exportFixedEpub", &json!({})).unwrap();
assert_eq!(r["pages"], 2);
let mut z = unzip(r["base64"].as_str().unwrap());
let opf = read(&mut z, "OEBPS/content.opf");
assert!(opf.contains("rendition:layout\">pre-paginated") && opf.contains("page2.xhtml"));
let p1 = read(&mut z, "OEBPS/page1.xhtml");
assert!(p1.contains("width=612, height=792") && p1.contains("Page one words"), "{p1}");
}
/// #775: the fallback follows the font's class, and embedFonts packages the stylesheet's fonts
/// (and reports those it can't).
#[test]
fn epub_font_fallbacks_and_embedding() {
let mut s = Session::new();
s.execute("file.new", &json!({"pages": 1})).unwrap();
s.execute("style.paragraph.create", &json!({"name": "Sans", "chars": {"fontFamily": "Source Sans 3", "fontStyle": "Regular"}})).unwrap();
s.execute("style.paragraph.create", &json!({"name": "Gone", "chars": {"fontFamily": "No Such Font Family", "fontStyle": "Regular"}})).unwrap();
s.execute("frame.create", &json!({"rect": [72, 72, 400, 200], "content": "text", "text": "Words", "caret": false})).unwrap();
let r = s.execute("file.exportEpub", &json!({})).unwrap();
let mut z = unzip(r["base64"].as_str().unwrap());
let css = read(&mut z, "OEBPS/style.css");
assert!(css.contains("font-family: \"Source Sans 3\", sans-serif;"), "{css}");
assert!(!css.contains("@font-face") && r["warnings"] == json!([]), "nothing embedded unless asked");
let r = s.execute("file.exportEpub", &json!({"embedFonts": true})).unwrap();
let mut z = unzip(r["base64"].as_str().unwrap());
let css = read(&mut z, "OEBPS/style.css");
assert!(css.contains("@font-face { font-family: \"Source Sans 3\"; src: url(\"fonts/"), "{css}");
let opf = read(&mut z, "OEBPS/content.opf");
let font = opf.split("href=\"fonts/").nth(1).and_then(|r| r.split('"').next()).map(|n| format!("OEBPS/fonts/{n}"));
assert!(font.as_ref().is_some_and(|f| z.by_name(f).is_ok_and(|e| e.size() > 1000)), "{opf}");
let warnings = r["warnings"].as_array().unwrap();
assert!(warnings.iter().any(|w| w.as_str().unwrap().contains("No Such Font Family")), "{warnings:?}");
let html = s.execute("file.exportHtml", &json!({})).unwrap()["text"].as_str().unwrap().to_string();
assert!(html.contains("\"Source Sans 3\", sans-serif;"));
}
/// A font whose licence allows embedding bitmaps only (fsType bit 9) is not packaged whole,
/// and a Semibold style asks for the Semibold face's weight, not Bold's.
#[test]
fn epub_embedding_follows_bitmap_only_licences_and_weights() {
use designcraft_fonts::testing::{font_with, with_fs_type};
const BITMAP: &str = "DocFont Epub Bitmap Only";
for fs_type in [0x0200, 0x0208] {
designcraft_fonts::FontDb::global().add_font(with_fs_type(font_with(&format!("{BITMAP} {fs_type:04x}"), &['W']).unwrap(), fs_type).unwrap());
}
let mut s = Session::new();
s.execute("file.new", &json!({"pages": 1})).unwrap();
for fs_type in [0x0200, 0x0208] {
let family = format!("{BITMAP} {fs_type:04x}");
s.execute("style.paragraph.create", &json!({"name": family, "chars": {"fontFamily": family, "fontStyle": "Regular"}})).unwrap();
}
s.execute("style.paragraph.create", &json!({"name": "Semi", "chars": {"fontFamily": "Source Sans 3", "fontStyle": "Semibold"}})).unwrap();
s.execute("frame.create", &json!({"rect": [72, 72, 400, 200], "content": "text", "text": "Words", "caret": false})).unwrap();
let r = s.execute("file.exportEpub", &json!({"embedFonts": true})).unwrap();
let mut z = unzip(r["base64"].as_str().unwrap());
let css = read(&mut z, "OEBPS/style.css");
assert!(!css.contains(&format!("@font-face {{ font-family: \"{BITMAP}")), "{css}");
let warnings = r["warnings"].to_string();
for fs_type in [0x0200, 0x0208] {
assert!(warnings.contains(&format!("{BITMAP} {fs_type:04x} Regular`: its licence doesn't allow embedding")), "{warnings}");
}
let semi = css.lines().find(|l| l.contains(".semi {")).unwrap_or_default();
assert!(semi.contains("font-weight: 600;"), "{css}");
}
/// #771, #773: a cover image file and an identifier through the command; the package's
/// modified time is the export's.
#[test]
fn epub_cover_image_file_and_identifier() {
let dir = std::env::temp_dir().join(format!("dc-epub-cover-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let jpg = dir.join("cover.jpg");
std::fs::write(&jpg, [0xFF, 0xD8, 0xFF, 0xE0, 0, 16]).unwrap();
let bad = dir.join("cover.txt");
std::fs::write(&bad, b"not an image").unwrap();
let mut s = Session::new();
s.execute("file.new", &json!({"pages": 1})).unwrap();
s.execute("frame.create", &json!({"rect": [72, 72, 400, 200], "content": "text", "text": "Words", "caret": false})).unwrap();
let r = s.execute("file.exportEpub", &json!({"coverImage": jpg.to_str().unwrap(), "identifier": "urn:isbn:9780000000002"})).unwrap();
let mut z = unzip(r["base64"].as_str().unwrap());
let opf = read(&mut z, "OEBPS/content.opf");
assert!(opf.contains("urn:isbn:9780000000002"), "{opf}");
assert!(opf.contains("href=\"images/cover.jpg\" media-type=\"image/jpeg\""), "{opf}");
assert!(!opf.contains("2026-01-01T00:00:00Z"), "the export time, not a fixed date: {opf}");
assert!(z.by_name("OEBPS/images/cover.jpg").is_ok());
assert!(s.execute("file.exportEpub", &json!({"coverImage": bad.to_str().unwrap()})).is_err());
std::fs::remove_dir_all(&dir).ok();
}
/// The color spaces of every image XObject in `pdf`, as the families and colorant names.
fn image_spaces(pdf: &[u8]) -> Vec {
use hayro_syntax::object::{Name, Object};
let file = hayro_syntax::Pdf::new(pdf.to_vec()).unwrap();
file.objects()
.into_iter()
.filter_map(|o| o.into_stream())
.filter(|s| s.dict().get::>(b"Subtype").is_some_and(|n| n.as_ref() == b"Image"))
.filter_map(|s| match s.dict().get::
"));
let tagged = s.execute("file.exportText", &json!({"frame": r["id"], "format": "tagged"})).unwrap();
assert!(tagged["text"].as_str().unwrap().contains("