use typst::diag::Warned;
use typst::layout::PagedDocument;
use crate::typst_world::{InkhavenWorld, WorldSettings};
pub struct RenderedParagraph {
pub width: u32,
pub height: u32,
pub png_bytes: Vec<u8>,
pub image: image::DynamicImage,
}
pub fn render_all(
source: &str,
settings: WorldSettings,
pixel_per_pt: f32,
) -> Result<Vec<RenderedParagraph>, String> {
let world = InkhavenWorld::in_memory(
std::env::temp_dir(),
source.to_owned(),
settings,
);
let Warned { output, warnings: _ } =
typst::compile::<PagedDocument>(&world);
let document = output.map_err(|errors| format_errors(&errors))?;
if document.pages.is_empty() {
return Err("compile produced zero pages".to_owned());
}
let mut out = Vec::with_capacity(document.pages.len());
for page in &document.pages {
let pixmap = typst_render::render(page, pixel_per_pt);
let width = pixmap.width();
let height = pixmap.height();
let png_bytes = pixmap
.encode_png()
.map_err(|e| format!("encode PNG: {e}"))?;
let raw = pixmap.data().to_vec();
let rgba = image::RgbaImage::from_raw(width, height, raw)
.ok_or_else(|| "image dimensions did not match buffer".to_owned())?;
out.push(RenderedParagraph {
width,
height,
png_bytes,
image: image::DynamicImage::ImageRgba8(rgba),
});
}
Ok(out)
}
pub fn render_page(
source: &str,
settings: WorldSettings,
pixel_per_pt: f32,
page_idx: usize,
) -> Result<RenderedParagraph, String> {
let world = InkhavenWorld::in_memory(
std::env::temp_dir(),
source.to_owned(),
settings,
);
let Warned { output, warnings: _ } =
typst::compile::<PagedDocument>(&world);
let document = output.map_err(|errors| format_errors(&errors))?;
let total = document.pages.len();
let page = document
.pages
.get(page_idx)
.ok_or_else(|| format!("page index {page_idx} out of range (have {total})"))?;
let pixmap = typst_render::render(page, pixel_per_pt);
let width = pixmap.width();
let height = pixmap.height();
let png_bytes = pixmap
.encode_png()
.map_err(|e| format!("encode PNG: {e}"))?;
let raw = pixmap.data().to_vec();
let rgba = image::RgbaImage::from_raw(width, height, raw)
.ok_or_else(|| "image dimensions did not match buffer".to_owned())?;
Ok(RenderedParagraph {
width,
height,
png_bytes,
image: image::DynamicImage::ImageRgba8(rgba),
})
}
fn format_errors(errors: &[typst::diag::SourceDiagnostic]) -> String {
let mut out = String::new();
for e in errors {
out.push_str(&e.message);
out.push('\n');
}
if out.is_empty() {
out.push_str("compile failed with no diagnostics");
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn renders_a_simple_paragraph() {
let source =
"#set page(width: 10cm, height: 5cm, margin: 1cm)\n\
= Hello\nProse line.\n";
let pages = render_all(
source,
WorldSettings {
bundle_fonts: true,
use_system_fonts: true,
packages_enabled: false,
},
2.0,
)
.expect("render");
assert!(!pages.is_empty(), "expected at least one page");
let first = &pages[0];
assert!(first.width > 0, "width was 0");
assert!(first.height > 0, "height was 0");
assert!(!first.png_bytes.is_empty(), "no PNG bytes");
assert_eq!(
&first.png_bytes[..8],
&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A],
);
assert_eq!(pages.len(), 1, "single-page paragraph");
}
#[test]
fn renders_each_page_consistently() {
let source =
"#set page(width: 10cm, height: 5cm, margin: 1cm)\n\
= Page 1\n#pagebreak()\n= Page 2\n#pagebreak()\n= Page 3\n";
let settings = WorldSettings {
bundle_fonts: true,
use_system_fonts: true,
packages_enabled: false,
};
let pages = render_all(source, settings.clone(), 2.0).expect("render_all");
assert_eq!(pages.len(), 3, "expected 3 pages, got {}", pages.len());
let mid = render_page(source, settings, 1.0, 1).expect("render_page");
assert!(mid.width > 0 && mid.height > 0);
}
}