Layout_frame() not sizing properly

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.

Math

This behavior is on purpose. You use the default EquationElem, which is an inline equation (block: false). Inline equations have a fake bounds to prevent uneven line spacing. You should use display equations (block: true) which have a real bounding box.
In pure Typst:

#set page(height: auto, width: auto, margin: 0pt)
#math.equation($sqrt(pi / 2)$) // or no spaces $sqrt(pi / 2)$

test

VS

#set page(height: auto, width: auto, margin: 0pt)
#math.equation(block: true, $sqrt(pi / 2)$) // or with spaces $ sqrt(pi / 2) $

Text

For text, you have to set the correct top-edge and bottom-edge, which is bounds for both. This is again to get even line spacing, if you have a descender, e.g. g the line spacing to the next line shouldn’t suddenly be bigger.
In Typst code again:

#set page(height: auto, width: auto, margin: 0pt)
Foo, bar, etc. good

test

VS

#set page(height: auto, width: auto, margin: 0pt)
#set text(top-edge: "bounds", bottom-edge: "bounds")
Foo, bar, etc. good

test

Both

Depending on your goals, an alternative for both issues would also be to simply use some page margin.