Hi! I’m working on a Rust project that formats and rasterizes an equation. I’ve worked my way through the various typst libraries to get what I believe to be a feasible system to put together an equation without having to parse it from a text string, but the output from layout_frame cuts off the top and bottom of the equation:
The frame reports a size of 17.589pt by 7.513000000000001pt which is consistent with this output at 20 px per point.
The relevant code is:
let eq = EquationElem::new(
RootElem::new(
FracElem::new(italic(TextElem::packed("π")), italic(TextElem::packed("2"))).pack(),
)
.pack(),
)
.pack();
let frame = layout_frame(
&mut engine,
&eq,
Locator::root(),
StyleChain::default(),
Region {
size: Size {
x: Abs::inf(),
y: Abs::inf(),
},
expand: Axes { x: false, y: false },
},
)
.unwrap();
The same happens, albiet to a lesser extent, with any input, not just equations:
let eq = TextElem::packed("Foo, bar, etc.");
What’s interesting is that the it seems to be scaled slightly to 56.144000000000005pt by 7.238pt with the horizontal dimension adjusting more aptly than the vertical although probably not perfectly based on the poorly centered output.
Full code
use std::path::PathBuf;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use typst::engine::{Engine, Route, Sink, Traced};
use typst::foundations::{Content, NativeElement, Smart, StyleChain};
use typst::introspection::{Introspector, Locator};
use typst::layout::{Abs, Axes, Page, Region, Size};
use typst::{LibraryExt, ROUTINES, World};
use typst::{math::EquationElem, text::TextElem};
use typst::Library;
use typst::comemo::Track;
use typst::diag::FileResult;
use typst::foundations::{Bytes, Datetime};
use typst::syntax::{FileId, Source};
use typst::text::{Font, FontBook};
use typst::utils::LazyHash;
use typst_kit::fonts::{FontSearcher, FontSlot};
use typst_layout::layout_frame;
use typst_render::render;
use typst::math::*;
pub struct EmptyWorld {
/// Root path to which files will be resolved.
root: PathBuf,
/// The content of a source.
source: Source,
/// The standard library.
library: LazyHash<Library>,
/// Metadata about all known fonts.
book: LazyHash<FontBook>,
/// Metadata about all known fonts.
fonts: Vec<FontSlot>,
/// Map of all known files.
files: Arc<Mutex<HashMap<FileId, Bytes>>>,
/// Cache directory (e.g. where packages are downloaded to).
cache_directory: PathBuf,
/// Datetime.
time: time::OffsetDateTime,
}
impl EmptyWorld {
pub fn new() -> Self {
let root = PathBuf::new();
let fonts = FontSearcher::new().include_system_fonts(false).search();
Self {
library: LazyHash::new(Library::default()),
book: LazyHash::new(fonts.book),
root,
fonts: fonts.fonts,
source: Source::detached(String::new()),
time: time::OffsetDateTime::now_utc(),
cache_directory: std::env::var_os("CACHE_DIRECTORY")
.map(|os_path| os_path.into())
.unwrap_or(std::env::temp_dir()),
files: Arc::new(Mutex::new(HashMap::new())),
}
}
fn upcast(&self) -> &dyn World {
return self;
}
}
#[comemo::track]
impl World for EmptyWorld {
/// Standard library.
fn library(&self) -> &LazyHash<Library> {
&self.library
}
/// Metadata about all known Books.
fn book(&self) -> &LazyHash<FontBook> {
&self.book
}
/// Accessing the main source file.
fn main(&self) -> FileId {
self.source.id()
}
/// Accessing a specified source file (based on `FileId`).
fn source(&self, id: FileId) -> FileResult<Source> {
Ok(self.source.clone()) // Butchered
}
/// Accessing a specified file (non-file).
fn file(&self, id: FileId) -> FileResult<Bytes> {
self.file(id).map(|file| file.clone()) // Butchered
}
/// Accessing a specified font per index of font book.
fn font(&self, id: usize) -> Option<Font> {
self.fonts[id].get()
}
/// Get the current date.
///
/// Optionally, an offset in hours is given.
fn today(&self, offset: Option<i64>) -> Option<Datetime> {
let offset = offset.unwrap_or(0);
let offset = time::UtcOffset::from_hms(offset.try_into().ok()?, 0, 0).ok()?;
let time = self.time.checked_to_offset(offset)?;
Some(Datetime::Date(time.date()))
}
}
fn main() {
let world = EmptyWorld::new();
let introspector = Introspector::default();
let traced = Traced::default();
let mut sink = Sink::new();
let mut engine = Engine {
routines: &ROUTINES,
world: world.upcast().track(),
introspector: introspector.track(),
traced: traced.track(),
sink: sink.track_mut(),
route: Route::default(),
};
let eq = EquationElem::new(
RootElem::new(
FracElem::new(italic(TextElem::packed("π")), italic(TextElem::packed("2"))).pack(),
)
.pack(),
)
.pack();
let frame = layout_frame(
&mut engine,
&eq,
Locator::root(),
StyleChain::default(),
Region::new(Size::splat(Abs::inf()), Axes::splat(false)),
)
.unwrap();
let page = Page {
frame: frame,
fill: Smart::Auto,
numbering: None,
supplement: Content::empty(),
number: 1,
};
render(&page, 20.).save_png("image.png").unwrap();
}
```
Thank you in advance for any help; I’m sure I’m just missing something here, but I’ve been on this for a few days now, and in the absense of any real documentation for these packages, I figured I’d ask here.



