Fonts and text
Text needs the text module: init({ text: true }) or loadText() in TypeScript, and the default text feature in Rust.
Two kinds of font
Section titled “Two kinds of font”- The standard 14 (
Font.standard("Helvetica"),Standard14::Helveticain Rust) need no bytes and aren’t embedded. They cover Latin text only, without shaping. Text they can’t encode fails withmissing-glyph. More than 37 distinct letters outside WinAnsi fails withencoding-full. - Your own fonts (
Font.fromBytes(bytes): TrueType, OpenType or a collection) are shaped with HarfRust and subset to the glyphs you draw. They’re embedded as Type 0 fonts with/Wwidths and a ToUnicode CMap, so the text can be extracted again. Equal fonts are embedded once per document, however manyFontobjects name them.
fsType: a font whose licence bits forbid embedding (restricted, or bitmap only) fails with font-not-embeddable. In Rust, EmbedPolicy::Override embeds it anyway, if you hold the rights. A font marked “no subsetting” is embedded whole.
Scripts, direction and line breaking
Section titled “Scripts, direction and line breaking”surface.text(str, { font, size, at }) draws one shaped line, with at[1] its baseline. surface.textBox(str, { font, size, box, direction, align }) breaks a paragraph at UAX #14 opportunities to fit its box, lays out each line in visual order (UAX #9 bidi), and reports overflow.
This example draws five scripts (Latin with ligatures, Arabic, Devanagari, Hebrew and Chinese), each in its own embedded font:
// Five scripts, each with its own embedded font: a shaped line and a paragraph broken into a// narrow box (right-to-left for Arabic and Hebrew). The TS twin of// crates/lamina/examples/multilingual.rs.import { Builder, type Direction, Font } from "@lamina/pdf";
/** Each script's line, its paragraph and the paragraph's direction (fonts in the same order). */export const SCRIPTS: readonly (readonly [line: string, paragraph: string, direction: Direction])[] = [ [ "Latin: the office staff fill the affine waffle flask", "A paragraph breaks at spaces and fits the width of its box, line by line.", "ltr", ], ["مرحبا بالعالم السلام عليكم", "مرحبا بالعالم السلام عليكم مرحبا بالعالم السلام عليكم", "rtl"], ["नमस्ते दुनिया। हिन्दी पाठ का परीक्षण", "नमस्ते दुनिया। हिन्दी पाठ का परीक्षण नमस्ते दुनिया।", "ltr"], ["שלום עולם וברוכים הבאים", "שלום עולם וברוכים הבאים שלום עולם וברוכים הבאים", "rtl"], ["你好世界,这是中文字体子集测试。", "你好世界,这是中文字体子集测试。你好世界,这是中文。", "ltr"],];
/** * The document's bytes. Needs `init({ text: true })`. * * @param fonts - Noto Sans, Noto Sans Arabic, Devanagari, Hebrew and SC (`fixtures/fonts/`), in * the order of {@link SCRIPTS}. */export function multilingual(fonts: readonly Uint8Array[]): Uint8Array { const b = new Builder({ id: new TextEncoder().encode("lamina-ex-multil"), producer: "lamina examples", creationDate: "D:20261030120000Z", }); const loaded = fonts.map((bytes) => Font.fromBytes(bytes)); for (const font of loaded) b.font(font); const page = b.page("A4"); const s = page.surface(); SCRIPTS.forEach(([line, paragraph, direction], row) => { const font = loaded[row]; if (font === undefined) throw new Error(`no font for script ${row}`); const y = 80 + 140 * row; s.text(line, { font, size: 16, at: [56, y] }); // A narrow box breaks the paragraph into lines; right-to-left paragraphs start at the right. s.textBox(paragraph, { font, size: 12, box: [56, y + 14, 220, 100], direction, align: "start" }); }); page.finish(); return b.finish().save();}//! Five scripts on one page: Latin, Arabic and Hebrew (right to left), Devanagari and//! simplified Chinese, each shaped with HarfRust and bidi-resolved, as single lines and as//! wrapped paragraphs in text boxes. Fonts are Noto subsets from `fixtures/fonts/` (OFL),//! passed as bytes.//!//! ```sh//! cargo run -p lamina --example multilingual -- [out-dir] # writes <out-dir>/multilingual.pdf//! ```
use std::error::Error;use std::path::PathBuf;
use lamina::{ Alignment, BuildOptions, Builder, CancelToken, Direction, PageSize, PointPt, Pt, RectPt, SaveOptions, TextBoxOptions,};
/// Each script's font, its line, its paragraph and the paragraph's direction.const SCRIPTS: [(&[u8], &str, &str, Direction); 5] = [ ( include_bytes!("../../../fixtures/fonts/NotoSans-Regular.ttf"), "Latin: the office staff fill the affine waffle flask", "A paragraph breaks at spaces and fits the width of its box, line by line.", Direction::LeftToRight, ), ( include_bytes!("../../../fixtures/fonts/NotoSansArabic-Regular.ttf"), "مرحبا بالعالم السلام عليكم", "مرحبا بالعالم السلام عليكم مرحبا بالعالم السلام عليكم", Direction::RightToLeft, ), ( include_bytes!("../../../fixtures/fonts/NotoSansDevanagari-Regular.ttf"), "नमस्ते दुनिया। हिन्दी पाठ का परीक्षण", "नमस्ते दुनिया। हिन्दी पाठ का परीक्षण नमस्ते दुनिया।", Direction::LeftToRight, ), ( include_bytes!("../../../fixtures/fonts/NotoSansHebrew-Regular.ttf"), "שלום עולם וברוכים הבאים", "שלום עולם וברוכים הבאים שלום עולם וברוכים הבאים", Direction::RightToLeft, ), ( include_bytes!("../../../fixtures/fonts/NotoSansSC-Regular.otf"), "你好世界,这是中文字体子集测试。", "你好世界,这是中文字体子集测试。你好世界,这是中文。", Direction::LeftToRight, ),];
fn main() -> Result<(), Box<dyn Error>> { let out = PathBuf::from(std::env::args().nth(1).unwrap_or_else(|| "target/examples-out".into())); std::fs::create_dir_all(&out)?; std::fs::write(out.join("multilingual.pdf"), build()?)?; Ok(())}
/// The document's bytes.pub(crate) fn build() -> Result<Vec<u8>, Box<dyn Error>> { let options = BuildOptions::default() .with_id(*b"lamina-ex-multil") .with_producer("lamina examples") .with_creation_date("D:20261030120000Z".parse()?); let mut b = Builder::new(options); let fonts = SCRIPTS .iter() .map(|(bytes, ..)| b.font_bytes(bytes.to_vec())) .collect::<Result<Vec<_>, _>>()?; let mut page = b.page(PageSize::A4); let s = page.surface(); for (row, ((_, line, paragraph, direction), font)) in (0_u32..).zip(SCRIPTS.iter().zip(&fonts)) { let y = 80.0 + 140.0 * f64::from(row); let _ = s.text(line, font, Pt::new(16.0), PointPt::from_points(56.0, y))?; // A narrow box breaks the paragraph into lines; right-to-left paragraphs start at the right. let options = TextBoxOptions::default() .with_direction(*direction) .with_align(Alignment::Start); let area = RectPt::from_xywh(Pt::new(56.0), Pt::new(y + 14.0), Pt::new(220.0), Pt::new(100.0)); s.text_box(paragraph, font, Pt::new(12.0), area, &options)?; } page.finish()?; let mut doc = b.finish()?; Ok(doc.save(&SaveOptions::new(), &CancelToken::new())?.bytes)}See the output. Text extracted from it (with pdf.js, pypdf or pdftotext) equals the input strings, ligatures included.