mod common;
use std::io::Cursor;
use boko::Format;
const JPEG_MAGIC: [u8; 2] = [0xFF, 0xD8];
fn photographic_image() -> image::RgbImage {
image::RgbImage::from_fn(256, 256, |x, y| {
let (fx, fy) = (x as f32 / 256.0, y as f32 / 256.0);
image::Rgb([
(128.0 + 127.0 * (fx * 7.1).sin() * (fy * 5.3).cos()) as u8,
(128.0 + 127.0 * (fx * 3.7 + fy * 2.9).sin()) as u8,
(128.0 + 127.0 * ((fx + fy) * 4.3).cos()) as u8,
])
})
}
fn photographic_png() -> Vec<u8> {
let mut buf = Cursor::new(Vec::new());
photographic_image()
.write_to(&mut buf, image::ImageFormat::Png)
.expect("encode png");
buf.into_inner()
}
fn high_quality_jpeg() -> Vec<u8> {
let mut buf = Cursor::new(Vec::new());
let img = photographic_image();
image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, 100)
.encode(
img.as_raw(),
img.width(),
img.height(),
image::ExtendedColorType::Rgb8,
)
.expect("encode jpeg");
buf.into_inner()
}
#[test]
fn optimize_transcodes_large_png_and_rewrites_references() {
use common::{Doc, EpubBuilder, Nav};
let png = photographic_png();
assert!(
png.len() > 10 * 1024,
"fixture must exceed the pass minimum"
);
let epub = EpubBuilder::new("Optimize Book")
.image("images/photo.png", png.clone())
.doc(Doc::new(
"text/ch1.xhtml",
"One",
"<p>before</p><img src=\"../images/photo.png\" alt=\"photo\"/><p>after</p>",
))
.nav(vec![Nav::new("One", "text/ch1.xhtml")])
.build();
let mut book = boko::Book::from_bytes(&epub, Format::Epub).expect("import epub");
let report = book.optimize();
assert_eq!(report.passes.len(), 1);
assert_eq!(report.passes[0].pass, "images");
assert_eq!(report.assets_changed(), 1);
assert!(report.bytes_saved() > 0);
let assets = book.list_assets().to_vec();
let jpg_path = assets
.iter()
.find(|p| p.ends_with("photo.jpg"))
.expect("renamed asset listed")
.clone();
assert!(!assets.iter().any(|p| p.ends_with("photo.png")));
let new_data = book.load_asset(&jpg_path).expect("load renamed");
assert_eq!(new_data[..2], JPEG_MAGIC);
assert!(new_data.len() < png.len());
let old_path = jpg_path.replace(".jpg", ".png");
assert_eq!(book.load_asset(&old_path).expect("old path"), new_data);
let ids: Vec<_> = book.spine().iter().map(|e| e.id).collect();
let mut saw_jpg_src = false;
for id in ids {
let ch = book.load_chapter(id).expect("load chapter");
for node in ch.iter_dfs() {
if let Some(src) = ch.semantics.src(node) {
assert!(!src.ends_with(".png"), "src not rewritten: {src}");
saw_jpg_src |= src.ends_with("photo.jpg");
}
}
}
assert!(saw_jpg_src, "rewritten image src not found");
for format in [Format::Epub, Format::Kfx, Format::Azw3] {
let out = common::export_to_bytes(&mut book, format);
assert!(!out.is_empty(), "{format:?} export produced no output");
}
let epub_out = common::export_to_bytes(&mut book, Format::Epub);
let mut zip = zip::ZipArchive::new(Cursor::new(epub_out)).expect("open exported epub");
let names: Vec<String> = (0..zip.len())
.map(|i| zip.by_index(i).unwrap().name().to_string())
.collect();
assert!(
names.iter().any(|n| n.ends_with("photo.jpg")),
"exported EPUB missing renamed image: {names:?}"
);
assert!(!names.iter().any(|n| n.ends_with("photo.png")));
}
#[test]
fn optimize_recompresses_large_jpeg_in_place() {
use common::{Doc, EpubBuilder, Nav};
let jpeg = high_quality_jpeg();
assert!(
jpeg.len() > 10 * 1024,
"fixture must exceed the pass minimum"
);
let epub = EpubBuilder::new("JPEG Book")
.image("images/photo.jpg", jpeg.clone())
.doc(Doc::new(
"text/ch1.xhtml",
"One",
"<p>x</p><img src=\"../images/photo.jpg\" alt=\"photo\"/>",
))
.nav(vec![Nav::new("One", "text/ch1.xhtml")])
.build();
let mut book = boko::Book::from_bytes(&epub, Format::Epub).expect("import epub");
let report = book.optimize();
assert_eq!(report.assets_changed(), 1);
let path = book
.list_assets()
.iter()
.find(|p| p.ends_with("photo.jpg"))
.expect("jpeg still listed")
.clone();
let data = book.load_asset(&path).expect("load");
assert_eq!(data[..2], JPEG_MAGIC);
assert!(data.len() < jpeg.len());
}
#[test]
fn optimize_downscales_images_beyond_paperwhite_resolution() {
use common::{Doc, EpubBuilder, Nav};
let big = image::DynamicImage::ImageRgb8(image::RgbImage::from_fn(3000, 2000, |x, y| {
let (fx, fy) = (x as f32 / 3000.0, y as f32 / 2000.0);
image::Rgb([
(128.0 + 127.0 * (fx * 6.3).sin() * (fy * 4.1).cos()) as u8,
(128.0 + 127.0 * (fx * 2.2 + fy * 3.8).sin()) as u8,
(fx * 255.0) as u8,
])
}));
let mut buf = Cursor::new(Vec::new());
image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, 95)
.encode(
big.as_bytes(),
big.width(),
big.height(),
image::ExtendedColorType::Rgb8,
)
.expect("encode jpeg");
let jpeg = buf.into_inner();
let epub = EpubBuilder::new("Huge Image Book")
.image("images/plate.jpg", jpeg.clone())
.doc(Doc::new(
"text/ch1.xhtml",
"One",
"<p>x</p><img src=\"../images/plate.jpg\" alt=\"plate\"/>",
))
.nav(vec![Nav::new("One", "text/ch1.xhtml")])
.build();
let mut book = boko::Book::from_bytes(&epub, Format::Epub).expect("import epub");
let report = book.optimize();
assert_eq!(report.assets_changed(), 1);
let path = book
.list_assets()
.iter()
.find(|p| p.ends_with("plate.jpg"))
.expect("jpeg listed")
.clone();
let data = book.load_asset(&path).expect("load");
assert!(data.len() < jpeg.len());
let img = image::load_from_memory(&data).expect("decode optimized");
assert_eq!(
img.width().max(img.height()),
1236,
"long edge must be capped at the Paperwhite content width ({}x{})",
img.width(),
img.height()
);
assert_eq!(img.width(), 1236);
assert!(
(823..=824).contains(&img.height()),
"height {} lost the 3:2 aspect",
img.height()
);
}
#[test]
fn optimize_keeps_small_flat_and_css_referenced_images() {
use common::{Doc, EpubBuilder, Nav};
let big_png = photographic_png();
let small_png = common::tiny_png();
let epub = EpubBuilder::new("Untouched Book")
.css(".hero { background-image: url(../images/wallpaper.png); }")
.image("images/wallpaper.png", big_png.clone())
.image("images/icon.png", small_png.clone())
.doc(Doc::new(
"text/ch1.xhtml",
"One",
"<p class=\"hero\">styled</p><img src=\"../images/icon.png\" alt=\"icon\"/>",
))
.nav(vec![Nav::new("One", "text/ch1.xhtml")])
.build();
let mut book = boko::Book::from_bytes(&epub, Format::Epub).expect("import epub");
let report = book.optimize();
assert_eq!(report.assets_changed(), 0, "report: {report:?}");
let assets = book.list_assets().to_vec();
assert!(assets.iter().any(|p| p.ends_with("wallpaper.png")));
assert!(assets.iter().any(|p| p.ends_with("icon.png")));
let wallpaper = assets
.iter()
.find(|p| p.ends_with("wallpaper.png"))
.unwrap();
assert_eq!(book.load_asset(wallpaper).expect("load"), big_png);
}