Skip to content

Commit

Permalink
feat(exporter::svg): layout and shape text in browser (#416)
Browse files Browse the repository at this point in the history
* feat: properly visualize text selection

* dev(exporter::svg): apply additional scale to text

* feat(exporter::svg): impl text shaping v1

* dev(exporter::svg): set content hint

* dev(exporter::svg): update svg utils

* build: sanitize content-hint changes

* dev(core): more robust way to calculating rev_cmap

* dev(core): fix both typst-preview and ci

* build: lock content-hint features

* dev: option to disable browser side text layout
  • Loading branch information
Myriad-Dreamin authored Nov 20, 2023
1 parent e9f6295 commit acfaa92
Show file tree
Hide file tree
Showing 35 changed files with 1,252 additions and 525 deletions.
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 10 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,18 @@ typst-ts-raster-exporter = { path = "exporter/raster" }
typst-ts-serde-exporter = { path = "exporter/serde" }
typst-ts-svg-exporter = { path = "exporter/svg" }

typst = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "typst.ts-v0.9.0" }
typst-library = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "typst.ts-v0.9.0" }
typst-syntax = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "typst.ts-v0.9.0" }
typst-ide = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "typst.ts-v0.9.0" }
# typst = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "typst.ts-v0.9.0" }
# typst-library = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "typst.ts-v0.9.0" }
# typst-syntax = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "typst.ts-v0.9.0" }
# typst-ide = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "typst.ts-v0.9.0" }
hayagriva = { git = "https://github.com/Myriad-Dreamin/hayagriva.git", branch = "typst.ts-v0.9.0" }

typst = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "typst.ts-v0.9.0-content-hint" }
typst-library = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "typst.ts-v0.9.0-content-hint" }
typst-syntax = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "typst.ts-v0.9.0-content-hint" }
typst-ide = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "typst.ts-v0.9.0-content-hint" }
# hayagriva = { git = "https://github.com/Myriad-Dreamin/hayagriva.git", branch = "typst.ts-v0.9.0" }

# typst = { path = "../typst/crates/typst" }
# typst-library = { path = "../typst/crates/typst-library" }
# typst-syntax = { path = "../typst/crates/typst-syntax" }
Expand Down
8 changes: 6 additions & 2 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ tracing-subscriber.workspace = true
codespan-reporting.workspace = true
human-panic.workspace = true

typst-ts-core.workspace = true
typst-ts-core = { workspace = true }
typst-ts-compiler = { workspace = true, default-features = false, features = [
"system",
# "lazy-fontdb",
Expand All @@ -65,7 +65,11 @@ pdf = ["typst-ts-pdf-exporter"]
raster = ["typst-ts-raster-exporter"]
serde-json = ["typst-ts-serde-exporter", "typst-ts-serde-exporter/json"]
serde-rmp = ["typst-ts-serde-exporter", "typst-ts-serde-exporter/rmp"]
svg = ["typst-ts-svg-exporter"]
svg = [
#
"typst-ts-svg-exporter",
"typst-ts-svg-exporter/experimental-ligature",
]
default = [
"pdf",
"raster",
Expand Down
30 changes: 30 additions & 0 deletions compiler/src/eval.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use comemo::{Track, TrackedMut};
use typst::{
diag::SourceResult,
eval::{Module, Route, Tracer},
World,
};

/// Compile a source file into a module.
///
/// - Returns `Ok(document)` if there were no fatal errors.
/// - Returns `Err(errors)` if there were fatal errors.
///
/// Requires a mutable reference to a tracer. Such a tracer can be created with
/// `Tracer::new()`. Independently of whether compilation succeeded, calling
/// `tracer.warnings()` after compilation will return all compiler warnings.
pub fn evaluate(world: &dyn World, tracer: &mut Tracer) -> SourceResult<Module> {
let route = Route::default();

// Call `track` just once to keep comemo's ID stable.
let world = world.track();
let mut tracer = tracer.track_mut();

// Try to evaluate the source file into a module.
typst::eval::eval(
world,
route.track(),
TrackedMut::reborrow_mut(&mut tracer),
&world.main(),
)
}
1 change: 1 addition & 0 deletions compiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ pub mod vfs;
/// A common implementation of [`typst::World`]
pub mod world;

pub mod eval;
/// Diff and parse the source code.
pub mod parser;
mod utils;
Expand Down
1 change: 1 addition & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ flat-vector = ["rkyv", "rkyv-validation"]
vector-bbox = []
debug-gc = []
experimental-ligature = []
no-content-hint = []

__web = ["dep:wasm-bindgen", "dep:js-sys", "dep:web-sys"]
web = ["__web"]
Expand Down
10 changes: 8 additions & 2 deletions core/src/font/ligature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,12 @@ impl LigatureResolver {
let c: ImmutStr = ligature
.components
.into_iter()
.map(|g| self.rev_cmap.get(&g).unwrap())
.map(|g| {
self.rev_cmap.get(&g).unwrap_or_else(|| {
println!("ligature component not found: {:?} {:?}", g, face);
&' '
})
})
.collect::<String>()
.into();

Expand Down Expand Up @@ -110,6 +115,7 @@ fn get_ligature_resolver(font: &Font) -> Rc<LigatureResolver> {

pub(super) fn resolve_ligature(font: &Font, id: GlyphId) -> Option<ImmutStr> {
let resolver = get_ligature_resolver(font);
// println!("resolve_ligature {:?} {:?}", font, id);
// let res = resolver.resolve(font.ttf(), id);
// println!("resolve_ligature {:?} {:?} -> {:?}", font, id, res);
resolver.resolve(font.ttf(), id)
}
1 change: 1 addition & 0 deletions core/src/vector/flat_ir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ pub enum FlatSvgItem {
Item(TransformedRef),
Group(GroupRef, Option<Size>),
Gradient(GradientItem),
ContentHint(char),
}

/// Flatten text item.
Expand Down
1 change: 1 addition & 0 deletions core/src/vector/flat_ir/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ impl<const ENABLE_REF_CNT: bool> ModuleBuilderImpl<ENABLE_REF_CNT> {

FlatSvgItem::Group(GroupRef(items.into()), size)
}
SvgItem::ContentHint(c) => FlatSvgItem::ContentHint(c),
};

let fingerprint = self.fingerprint_builder.resolve(&resolved_item);
Expand Down
9 changes: 9 additions & 0 deletions core/src/vector/flat_vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ pub trait FlatRenderVm<'m>: Sized + FontIndice<'m> {
g.render_image(self, image);
g.into()
}
ir::FlatSvgItem::ContentHint(c) => {
let mut g = self.start_flat_group(abs_ref);
g.render_content_hint(self, *c);
g.into()
}
ir::FlatSvgItem::Gradient(..) | ir::FlatSvgItem::None => {
panic!("FlatRenderVm.RenderFrame.UnknownItem {:?}", item)
}
Expand Down Expand Up @@ -252,6 +257,10 @@ where
group_ctx.render_image(self, image);
group_ctx
}
ir::FlatSvgItem::ContentHint(c) => {
group_ctx.render_content_hint(self, *c);
group_ctx
}
ir::FlatSvgItem::Gradient(..) | ir::FlatSvgItem::None => {
panic!("FlatRenderVm.RenderFrame.UnknownItem {:?}", next_item)
}
Expand Down
1 change: 1 addition & 0 deletions core/src/vector/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ pub enum SvgItem {
Group(GroupItem, Option<Size>),
// todo: big size 64
Gradient(GradientItem),
ContentHint(char),
}

/// Data of an `<image/>` element.
Expand Down
21 changes: 17 additions & 4 deletions core/src/vector/lowering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ pub struct LowerBuilder {
pub extra_items: HashMap<Fingerprint, ir::SvgItem>,
}

static LINE_HINT_ELEMENTS: once_cell::sync::Lazy<std::collections::HashSet<&'static str>> =
once_cell::sync::Lazy::new(|| {
let mut set = std::collections::HashSet::new();
set.insert("heading");
set
});

impl LowerBuilder {
pub fn new(output: &Document) -> Self {
Self {
Expand Down Expand Up @@ -99,11 +106,17 @@ impl LowerBuilder {
Self::lower_position(dest, *size)
}
},
Meta::Elem(elem) => {
if !LINE_HINT_ELEMENTS.contains(elem.func().name()) {
continue;
}

SvgItem::ContentHint('\n')
}
#[cfg(not(feature = "no-content-hint"))]
Meta::ContentHint(c) => SvgItem::ContentHint(*c),
// todo: support page label
Meta::PdfPageLabel(..)
| Meta::Elem(..)
| Meta::PageNumbering(..)
| Meta::Hide => continue,
Meta::PdfPageLabel(..) | Meta::PageNumbering(..) | Meta::Hide => continue,
},
};

Expand Down
8 changes: 8 additions & 0 deletions core/src/vector/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ pub trait GroupContext<C>: Sized {
fn render_image(&mut self, _ctx: &mut C, _image_item: &ir::ImageItem) {}

fn attach_debug_info(&mut self, _ctx: &mut C, _span_id: u64) {}

/// Render a semantic link into underlying context.
fn render_content_hint(&mut self, _ctx: &mut C, _ch: char) {}
}

/// Contextual information for rendering.
Expand Down Expand Up @@ -195,6 +198,11 @@ pub trait RenderVm: Sized {
g.render_image(self, image);
g.into()
}
ir::SvgItem::ContentHint(c) => {
let mut g = self.start_group();
g.render_content_hint(self, *c);
g.into()
}
ir::SvgItem::Gradient(..) => {
panic!("RenderVm.RenderFrame.UnknownItem {:?}", item)
}
Expand Down
1 change: 1 addition & 0 deletions exporter/raster/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ fn render_frame(canvas: &mut sk::PixmapMut, state: State, frame: &Frame) {
Meta::PageNumbering(_) => {}
Meta::PdfPageLabel(_) => {}
Meta::Hide => {}
Meta::ContentHint(_) => {}
},
}
}
Expand Down
1 change: 1 addition & 0 deletions exporter/svg/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ log.workspace = true
siphasher.workspace = true

[features]
experimental-ligature = ["typst-ts-core/experimental-ligature"]
rkyv = ["typst-ts-core/rkyv"]
flat-vector = ["rkyv", "typst-ts-core/flat-vector"]
default = ["flat-vector"]
40 changes: 35 additions & 5 deletions exporter/svg/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,15 @@ impl SvgGlyphBuilder {
Self::render_glyph_inner(gp, glyph_id, glyph_item)
}

fn render_ligature_attr(ll: u8) -> String {
// println!("ligature len: {}", ll);
if ll > 0 {
format!(r#" data-liga-len="{}""#, ll)
} else {
"".to_owned()
}
}

pub fn is_image_glyph(&mut self, glyph_item: &ir::GlyphItem) -> Option<bool> {
let gp: &GlyphProvider = &self.glyph_provider;
Self::is_image_glyph_inner(gp, glyph_item)
Expand Down Expand Up @@ -198,8 +207,11 @@ impl SvgGlyphBuilder {

let img = render_image(&ig.image.image, ig.image.size, false, &transform_style);

// Ligature information
let li = Self::render_ligature_attr(ig.ligature_len);

let symbol_def = format!(
r#"<symbol overflow="visible" id="{}" class="image_glyph">{}</symbol>"#,
r#"<symbol overflow="visible" id="{}" class="image_glyph"{li}>{}</symbol>"#,
glyph_id, img
);
Some(symbol_def)
Expand All @@ -210,8 +222,11 @@ impl SvgGlyphBuilder {
glyph_id: &str,
outline_glyph: &ir::OutlineGlyphItem,
) -> Option<String> {
// Ligature information
let li = Self::render_ligature_attr(outline_glyph.ligature_len);

let symbol_def = format!(
r#"<path id="{}" class="outline_glyph" d="{}"/>"#,
r#"<path id="{}" class="outline_glyph" d="{}"{li}/>"#,
glyph_id, outline_glyph.d
);
Some(symbol_def)
Expand Down Expand Up @@ -288,17 +303,25 @@ impl SvgTextBuilder {
};

// todo: investigate &nbsp;

// we also apply some additional scaling.
// so that the font-size doesn't hit the limit of the browser.
// See <https://stackoverflow.com/questions/13416989/computed-font-size-is-bigger-than-defined-in-css-on-the-asus-nexus-7>
self.content.push(SvgText::Plain(format!(
concat!(
// apply a negative scaleY to flip the text, since a glyph in font is
// rendered upside down.
r#"<g transform="scale(1,-1)">"#,
r#"<foreignObject x="0" y="-{}" width="{}" height="{}">"#,
r#"<g transform="scale(16,-16)">"#,
r#"<foreignObject x="0" y="-{:.2}" width="{:.2}" height="{:.2}">"#,
r#"<h5:div class="tsel" style="font-size: {}px">"#,
"{}",
r#"</h5:div></foreignObject></g>"#,
),
ascender, width, upem, upem, text_content
ascender / 16.,
width / 16.,
upem / 16.,
((upem + 1e-3) / 16.) as u32,
text_content
)))
}
}
Expand Down Expand Up @@ -494,6 +517,13 @@ impl<
.push(render_image_item(image_item, ctx.enable_inlined_svg()))
}

fn render_content_hint(&mut self, _ctx: &mut C, ch: char) {
self.attributes
.push(("class", "typst-content-hint".to_owned()));
self.attributes
.push(("data-hint", format!("{:x}", ch as u32)));
}

#[inline]
fn attach_debug_info(&mut self, ctx: &mut C, span_id: u64) {
if ctx.should_attach_debug_info() {
Expand Down
2 changes: 1 addition & 1 deletion exporter/svg/src/frontend/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ fn has_gradient<'m, 't, Feat: ExportFeature>(
use FlatSvgItem::*;
match item {
Gradient(..) => true,
Image(..) | Link(..) | None => false,
Image(..) | Link(..) | ContentHint(..) | None => false,
Item(t) => has_gradient(ctx, &t.1),
Group(g, ..) => g.0.iter().any(|(_, x)| has_gradient(ctx, x)),
Path(p) => p.styles.iter().any(|s| match s {
Expand Down
1 change: 1 addition & 0 deletions exporter/svg/src/frontend/flat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ impl<Feat: ExportFeature> SvgTask<Feat> {
let state = RenderState::new_size(page.size);
svg_body.push(SvgText::Content(Arc::new(SvgTextNode {
attributes: vec![
("class", "typst-page".into()),
("transform", format!("translate(0, {})", acc_height)),
("data-tid", entry.as_svg_id("p")),
("data-page-width", size.x.to_string()),
Expand Down
1 change: 1 addition & 0 deletions exporter/svg/src/frontend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,7 @@ impl<Feat: ExportFeature> SvgTask<Feat> {
let size = Self::page_size(size_f32);

let attributes = vec![
("class", "typst-page".into()),
("transform", format!("translate(0, {})", acc_height)),
("data-page-width", size.x.to_string()),
("data-page-height", size.y.to_string()),
Expand Down
Loading

0 comments on commit acfaa92

Please sign in to comment.