Last active
June 27, 2026 10:04
-
-
Save fundon/2fbf7f6b5d685bb8ed87174fcdafe867 to your computer and use it in GitHub Desktop.
glifo + parley + vello_cpu example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [package] | |
| name = "glifo-example" | |
| version = "0.1.0" | |
| edition = "2024" | |
| [dependencies] | |
| skrifa = { version = "0.42.0", default-features = false, features = ["autohint_shaping"] } | |
| parley = { version = "0.10.0", default-features = false, features = ["system", "accesskit"] } | |
| glifo = { version = "0.1.1", default-features = false, git = "https://github.com/fundon/vello.git", branch = "vello_cpu_supports_custom_atlas_size" } | |
| vello_cpu = { version = "0.0.9", default-features = false, features = ["png", "text", "f32_pipeline", "multithreading"], git = "https://github.com/fundon/vello.git", branch = "vello_cpu_supports_custom_atlas_size" } | |
| smallvec = { version = "1.15.1", features = ["const_new"] } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use std::sync::Arc; | |
| use glifo::atlas::key::quantize_subpixel; | |
| use glifo::{GlyphCacheKey, GlyphRenderer}; | |
| use parley::{ | |
| Alignment, AlignmentOptions, FontFamily, GenericFamily, Layout, LineHeight, | |
| PositionedLayoutItem, StyleProperty, | |
| }; | |
| use parley::{FontContext, FontFamilyName, LayoutContext}; | |
| use skrifa::metrics::GlyphMetrics; | |
| use skrifa::prelude::{LocationRef, NormalizedCoord, Size}; | |
| use skrifa::raw::TableProvider; | |
| use skrifa::{FontRef, MetadataProvider}; | |
| use smallvec::SmallVec; | |
| use vello_cpu::color::palette::css::WHITE; | |
| use vello_cpu::kurbo::{Affine, Rect, Vec2}; | |
| use vello_cpu::peniko::{Color, Extend, ImageSampler}; | |
| use vello_cpu::{ | |
| Glyph, Image, ImageSource, Level, Pixmap, RasterizerSettings, RenderContext, RenderMode, | |
| RenderSettings, Resources, | |
| }; | |
| const TEXT: &str = "Hello 😂🤣😭😜🤪😇👁👄🫦👅🧔🕴💃 world\nHello 😂🤣😭😜🤪😇👁👄🫦👅🧔🕴💃 world"; | |
| const FONT_SIZE: f32 = 32.0; | |
| const OUTPUT_PATH: &str = "glifo_colr_position.png"; | |
| #[derive(Clone, Copy, Debug, PartialEq)] | |
| pub struct ColorBrush { | |
| pub color: Color, | |
| } | |
| impl Default for ColorBrush { | |
| fn default() -> Self { | |
| Self { | |
| color: Color::BLACK, | |
| } | |
| } | |
| } | |
| fn main() { | |
| let width = 800; | |
| let height = 400; | |
| let mut ctx = RenderContext::new_with( | |
| width, | |
| height, | |
| RenderSettings { | |
| level: Level::new(), | |
| num_threads: 0, | |
| }, | |
| ); | |
| let mut resources = Resources::new(); | |
| ctx.set_paint(WHITE); | |
| ctx.fill_rect(&Rect::new(0.0, 0.0, f64::from(width), f64::from(height))); | |
| let mut atlas_ctx = RenderContext::new_with( | |
| 2048, | |
| 2048, | |
| RenderSettings { | |
| level: Level::new(), | |
| num_threads: 0, | |
| }, | |
| ); | |
| let mut layout_ctx = LayoutContext::<ColorBrush>::new(); | |
| let mut font_ctx = FontContext::new(); | |
| font_ctx.collection.load_system_fonts(); | |
| draw_text( | |
| &mut ctx, | |
| &mut atlas_ctx, | |
| &mut resources, | |
| &mut layout_ctx, | |
| &mut font_ctx, | |
| TEXT, | |
| ); | |
| ctx.flush(); | |
| let mut pixmap = Pixmap::new(width, height); | |
| ctx.render_with( | |
| &mut pixmap, | |
| &mut Resources::default(), | |
| RasterizerSettings { | |
| render_mode: RenderMode::OptimizeSpeed, | |
| ..Default::default() | |
| }, | |
| ); | |
| std::fs::write(OUTPUT_PATH, pixmap.into_png().unwrap()).expect("write output PNG"); | |
| println!("wrote {OUTPUT_PATH}"); | |
| println!("text: {TEXT}"); | |
| let Some(pixmaps) = resources.pixmaps() else { | |
| return; | |
| }; | |
| for (index, pixmap) in pixmaps.iter().enumerate() { | |
| { | |
| let dir = std::env::current_dir().unwrap(); | |
| let dir = dir.to_string_lossy(); | |
| let atlas_pixmap = | |
| Pixmap::from_parts(pixmap.data().to_vec(), pixmap.width(), pixmap.height()); | |
| let data = atlas_pixmap.into_png().unwrap(); | |
| std::fs::write(format!("{dir}/target/atlas-{}.png", index), data).unwrap(); | |
| } | |
| } | |
| } | |
| fn draw_text( | |
| ctx: &mut RenderContext, | |
| atlas_ctx: &mut RenderContext, | |
| resources: &mut Resources, | |
| layout_ctx: &mut LayoutContext<ColorBrush>, | |
| font_ctx: &mut FontContext, | |
| text: &str, | |
| ) { | |
| let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, true); | |
| builder.push_default(FontFamily::from( | |
| [ | |
| GenericFamily::SystemUi.into(), | |
| FontFamilyName::named("Fluent Emoji Color"), | |
| // FontFamilyName::named("Noto Color Emoji"), | |
| ] | |
| .as_slice(), | |
| )); | |
| builder.push_default(LineHeight::FontSizeRelative(1.2)); | |
| builder.push_default(StyleProperty::FontSize(FONT_SIZE)); | |
| let mut layout: Layout<ColorBrush> = builder.build(text); | |
| layout.break_all_lines(Some(ctx.width() as f32)); | |
| layout.align(Alignment::Start, AlignmentOptions::default()); | |
| for line in layout.lines() { | |
| for item in line.items() { | |
| match item { | |
| PositionedLayoutItem::GlyphRun(glyph_run) => { | |
| let run = glyph_run.run(); | |
| let font = run.font(); | |
| let font_data = &font.data; | |
| let font_id = font_data.id(); | |
| let font_index = font.index; | |
| let font_ref = FontRef::from_index(font_data.data(), font_index).unwrap(); | |
| let is_color = font_ref.colr().is_ok(); | |
| let scale = FONT_SIZE / font_ref.head().unwrap().units_per_em() as f32; | |
| let normalized_coords = run | |
| .normalized_coords() | |
| .iter() | |
| .copied() | |
| .map(NormalizedCoord::from_bits) | |
| .collect::<Vec<_>>(); | |
| let location_ref = LocationRef::new(&normalized_coords); | |
| let positioned_glyphs = glyph_run.positioned_glyphs(); | |
| let mut temp_glyph_cache_key = GlyphCacheKey { | |
| font_id, | |
| font_index, | |
| hinted: !is_color, | |
| size_bits: FONT_SIZE.to_bits(), | |
| var_coords: SmallVec::from_slice(&normalized_coords), | |
| ..GlyphCacheKey::DEFAULT | |
| }; | |
| if is_color { | |
| temp_glyph_cache_key = temp_glyph_cache_key.to_colr(); | |
| } | |
| let glifo_run = atlas_ctx | |
| .glyph_run(resources, font) | |
| .font_size(FONT_SIZE) | |
| .hint(!is_color) | |
| .atlas_cache(true); | |
| glifo_run.fill_glyphs(positioned_glyphs.clone().map(|glyph| Glyph { | |
| id: glyph.id, | |
| x: glyph.x, | |
| y: glyph.y, | |
| })); | |
| // Upload glyphs into the atlas cache. | |
| resources.prepare_glyph_cache(RenderMode::OptimizeSpeed); | |
| resources.maintain_glyph_cache(); | |
| // Clear the tick so the old glyphs are not replaced with the new ones. | |
| // Keep the old glyphs in the cache. | |
| if let Some(glyph_atlas) = resources.glyph_atlas_mut() { | |
| glyph_atlas.clear_tick(); | |
| } | |
| for glyph in positioned_glyphs { | |
| let mut glyph_cache_key = GlyphCacheKey { | |
| glyph_id: glyph.id, | |
| ..temp_glyph_cache_key.clone() | |
| }; | |
| if !is_color { | |
| glyph_cache_key.subpixel_x = quantize_subpixel(glyph.x.fract()); | |
| } | |
| let Some(pixmaps) = resources.pixmaps() else { | |
| return; | |
| }; | |
| let Some(glyph_atlas) = resources.glyph_atlas_mut() else { | |
| continue; | |
| }; | |
| let Some(atlas_slot) = glyph_atlas.get(&glyph_cache_key) else { | |
| continue; | |
| }; | |
| let Some(pixmap) = pixmaps.get(atlas_slot.page_index as usize) else { | |
| continue; | |
| }; | |
| let mut glyph_pixmap = Pixmap::new(atlas_slot.width, atlas_slot.height); | |
| copy_pixmap_from_atlas( | |
| &pixmap, | |
| &mut glyph_pixmap, | |
| atlas_slot.x, | |
| atlas_slot.y, | |
| ); | |
| #[cfg(debug_assertions)] | |
| { | |
| let dir = std::env::current_dir().unwrap(); | |
| let dir = dir.to_string_lossy(); | |
| if let Ok(png) = glyph_pixmap.clone().into_png() { | |
| std::fs::write(format!("{dir}/target/glyph-{}.png", glyph.id), png) | |
| .unwrap(); | |
| } | |
| } | |
| let image = Image { | |
| image: ImageSource::Pixmap(Arc::new(glyph_pixmap)), | |
| sampler: ImageSampler { | |
| x_extend: Extend::Pad, | |
| y_extend: Extend::Pad, | |
| quality: vello_cpu::peniko::ImageQuality::Medium, | |
| alpha: 1.0, | |
| }, | |
| }; | |
| let pos = Vec2::new(glyph.x as f64, glyph.y as f64); | |
| let bearing = | |
| Vec2::new(atlas_slot.bearing_x as f64, atlas_slot.bearing_y as f64); | |
| let transform = Affine::translate(pos + bearing); | |
| let state = ctx.save_state(); | |
| if is_color { | |
| let color_glyphs = font_ref.color_glyphs(); | |
| let bounds = color_glyphs | |
| .get(glyph.id.into()) | |
| .unwrap() | |
| // For COLRv1 glyphs | |
| .bounding_box(location_ref, Size::unscaled()) | |
| .or_else(|| { | |
| // For COLRv0 glyphs | |
| let metrics = GlyphMetrics::new( | |
| &font_ref, | |
| Size::unscaled(), | |
| location_ref, | |
| ); | |
| metrics.bounds(glyph.id.into()) | |
| }) | |
| .unwrap() | |
| .scale(scale); | |
| let tx = bounds.x_min as f64; | |
| let ty = bounds.y_min as f64; | |
| ctx.set_transform( | |
| transform | |
| .then_translate(Vec2::new(tx, -ty)) | |
| .pre_scale_non_uniform(1.0, -1.0), | |
| ); | |
| } else { | |
| ctx.set_transform(transform); | |
| } | |
| ctx.set_paint_image(image); | |
| ctx.fill_rect(&Rect { | |
| x0: 0.0, | |
| y0: 0.0, | |
| x1: atlas_slot.width as f64, | |
| y1: atlas_slot.height as f64, | |
| }); | |
| ctx.restore_state(state); | |
| } | |
| } | |
| _ => {} | |
| } | |
| } | |
| } | |
| } | |
| fn copy_pixmap_from_atlas(src: &Pixmap, dst: &mut Pixmap, src_x: u16, src_y: u16) { | |
| let src_stride = src.width() as usize; | |
| let copy_width = dst.width() as usize; | |
| let copy_height = dst.height() as usize; | |
| let dst_stride = copy_width; | |
| let src_data = src.data_as_u8_slice(); | |
| let dst_data = dst.data_as_u8_slice_mut(); | |
| for y in 0..copy_height { | |
| let dst_row_start = y * dst_stride * 4; | |
| let dst_row_end = dst_row_start + copy_width * 4; | |
| let src_row_start = ((src_y as usize + y) * src_stride + src_x as usize) * 4; | |
| let src_row_end = src_row_start + copy_width * 4; | |
| dst_data[dst_row_start..dst_row_end].copy_from_slice(&src_data[src_row_start..src_row_end]); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use std::sync::Arc; | |
| use glifo::atlas::key::quantize_subpixel; | |
| use glifo::{GlyphCacheKey, GlyphRenderer}; | |
| use parley::{ | |
| Alignment, AlignmentOptions, FontFamily, GenericFamily, Layout, LineHeight, | |
| PositionedLayoutItem, StyleProperty, | |
| }; | |
| use parley::{FontContext, FontFamilyName, LayoutContext}; | |
| use skrifa::FontRef; | |
| use skrifa::prelude::NormalizedCoord; | |
| use skrifa::raw::TableProvider; | |
| use smallvec::SmallVec; | |
| use vello_cpu::color::palette::css::WHITE; | |
| use vello_cpu::kurbo::{Affine, Rect, Vec2}; | |
| use vello_cpu::peniko::{Color, Extend, ImageSampler}; | |
| use vello_cpu::{ | |
| Glyph, Image, ImageSource, Level, Pixmap, RasterizerSettings, RenderContext, RenderMode, | |
| RenderSettings, Resources, | |
| }; | |
| const TEXT: &str = "Hello 😂🤣😭😜🤪😇👁👄🫦👅🧔🕴💃 world\nHello 😂🤣😭😜🤪😇👁👄🫦👅🧔🕴💃 world"; | |
| const FONT_SIZE: f32 = 32.0; | |
| const OUTPUT_PATH: &str = "glifo_colr_position.png"; | |
| #[derive(Clone, Copy, Debug, PartialEq)] | |
| pub struct ColorBrush { | |
| pub color: Color, | |
| } | |
| impl Default for ColorBrush { | |
| fn default() -> Self { | |
| Self { | |
| color: Color::BLACK, | |
| } | |
| } | |
| } | |
| fn main() { | |
| let width = 800; | |
| let height = 400; | |
| let mut ctx = RenderContext::new_with( | |
| width, | |
| height, | |
| RenderSettings { | |
| level: Level::new(), | |
| num_threads: 0, | |
| }, | |
| ); | |
| let mut resources = Resources::new(); | |
| ctx.set_paint(WHITE); | |
| ctx.fill_rect(&Rect::new(0.0, 0.0, f64::from(width), f64::from(height))); | |
| let mut atlas_ctx = RenderContext::new_with( | |
| 2048, | |
| 2048, | |
| RenderSettings { | |
| level: Level::new(), | |
| num_threads: 0, | |
| }, | |
| ); | |
| let mut layout_ctx = LayoutContext::<ColorBrush>::new(); | |
| let mut font_ctx = FontContext::new(); | |
| font_ctx.collection.load_system_fonts(); | |
| draw_text( | |
| &mut ctx, | |
| &mut atlas_ctx, | |
| &mut resources, | |
| &mut layout_ctx, | |
| &mut font_ctx, | |
| TEXT, | |
| ); | |
| ctx.flush(); | |
| let mut pixmap = Pixmap::new(width, height); | |
| ctx.render_with( | |
| &mut pixmap, | |
| &mut Resources::default(), | |
| RasterizerSettings { | |
| render_mode: RenderMode::OptimizeSpeed, | |
| ..Default::default() | |
| }, | |
| ); | |
| std::fs::write(OUTPUT_PATH, pixmap.into_png().unwrap()).expect("write output PNG"); | |
| println!("wrote {OUTPUT_PATH}"); | |
| println!("text: {TEXT}"); | |
| let Some(pixmaps) = resources.pixmaps() else { | |
| return; | |
| }; | |
| for (index, pixmap) in pixmaps.iter().enumerate() { | |
| { | |
| let dir = std::env::current_dir().unwrap(); | |
| let dir = dir.to_string_lossy(); | |
| let atlas_pixmap = | |
| Pixmap::from_parts(pixmap.data().to_vec(), pixmap.width(), pixmap.height()); | |
| let data = atlas_pixmap.into_png().unwrap(); | |
| std::fs::write(format!("{dir}/target/atlas-{}.png", index), data).unwrap(); | |
| } | |
| } | |
| } | |
| fn draw_text( | |
| ctx: &mut RenderContext, | |
| atlas_ctx: &mut RenderContext, | |
| resources: &mut Resources, | |
| layout_ctx: &mut LayoutContext<ColorBrush>, | |
| font_ctx: &mut FontContext, | |
| text: &str, | |
| ) { | |
| let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, true); | |
| builder.push_default(FontFamily::from( | |
| [ | |
| GenericFamily::SystemUi.into(), | |
| // FontFamilyName::named("Fluent Emoji Color"), | |
| FontFamilyName::named("Noto Color Emoji"), | |
| ] | |
| .as_slice(), | |
| )); | |
| builder.push_default(LineHeight::FontSizeRelative(1.2)); | |
| builder.push_default(StyleProperty::FontSize(FONT_SIZE)); | |
| let mut layout: Layout<ColorBrush> = builder.build(text); | |
| layout.break_all_lines(Some(ctx.width() as f32)); | |
| layout.align(Alignment::Start, AlignmentOptions::default()); | |
| for line in layout.lines() { | |
| // let line_height = line.metrics().line_height as f64; | |
| for item in line.items() { | |
| match item { | |
| PositionedLayoutItem::GlyphRun(glyph_run) => { | |
| let run = glyph_run.run(); | |
| let font = run.font(); | |
| let font_data = &font.data; | |
| let font_id = font_data.id(); | |
| let font_index = font.index; | |
| let font_ref = FontRef::from_index(font_data.data(), font_index).unwrap(); | |
| let is_color = font_ref.colr().is_ok(); | |
| let normalized_coords = run | |
| .normalized_coords() | |
| .iter() | |
| .copied() | |
| .map(NormalizedCoord::from_bits) | |
| .collect::<Vec<_>>(); | |
| let positioned_glyphs = glyph_run.positioned_glyphs(); | |
| let mut temp_glyph_cache_key = GlyphCacheKey { | |
| font_id, | |
| font_index, | |
| hinted: !is_color, | |
| size_bits: FONT_SIZE.to_bits(), | |
| var_coords: SmallVec::from_slice(&normalized_coords), | |
| ..GlyphCacheKey::DEFAULT | |
| }; | |
| if is_color { | |
| temp_glyph_cache_key = temp_glyph_cache_key.to_colr(); | |
| } | |
| atlas_ctx | |
| .glyph_run(resources, font) | |
| .font_size(FONT_SIZE) | |
| .hint(!is_color) | |
| .atlas_cache(true) | |
| .fill_glyphs(positioned_glyphs.clone().map(|glyph| Glyph { | |
| id: glyph.id, | |
| x: glyph.x, | |
| y: glyph.y, | |
| })); | |
| // Upload glyphs into the atlas cache. | |
| resources.prepare_glyph_cache(RenderMode::OptimizeSpeed); | |
| resources.maintain_glyph_cache(); | |
| // Clear the tick so the old glyphs are not replaced with the new ones. | |
| // Keep the old glyphs in the cache. | |
| if let Some(glyph_atlas) = resources.glyph_atlas_mut() { | |
| glyph_atlas.clear_tick(); | |
| } | |
| for glyph in positioned_glyphs { | |
| let mut glyph_cache_key = GlyphCacheKey { | |
| glyph_id: glyph.id, | |
| ..temp_glyph_cache_key.clone() | |
| }; | |
| if !is_color { | |
| glyph_cache_key.subpixel_x = quantize_subpixel(glyph.x.fract()); | |
| } | |
| let Some(pixmaps) = resources.pixmaps() else { | |
| return; | |
| }; | |
| let Some(glyph_atlas) = resources.glyph_atlas_mut() else { | |
| continue; | |
| }; | |
| let Some(atlas_slot) = glyph_atlas.get(&glyph_cache_key) else { | |
| continue; | |
| }; | |
| let Some(pixmap) = pixmaps.get(atlas_slot.page_index as usize) else { | |
| continue; | |
| }; | |
| let mut glyph_pixmap = Pixmap::new(atlas_slot.width, atlas_slot.height); | |
| copy_pixmap_from_atlas( | |
| &pixmap, | |
| &mut glyph_pixmap, | |
| atlas_slot.x, | |
| atlas_slot.y, | |
| ); | |
| #[cfg(debug_assertions)] | |
| { | |
| let dir = std::env::current_dir().unwrap(); | |
| let dir = dir.to_string_lossy(); | |
| if let Ok(png) = glyph_pixmap.clone().into_png() { | |
| std::fs::write(format!("{dir}/target/glyph-{}.png", glyph.id), png) | |
| .unwrap(); | |
| } | |
| } | |
| let image = Image { | |
| image: ImageSource::Pixmap(Arc::new(glyph_pixmap)), | |
| sampler: ImageSampler { | |
| x_extend: Extend::Pad, | |
| y_extend: Extend::Pad, | |
| quality: vello_cpu::peniko::ImageQuality::Medium, | |
| alpha: 1.0, | |
| }, | |
| }; | |
| let pos = Vec2::new(glyph.x as f64, glyph.y as f64); | |
| let bearing = | |
| Vec2::new(atlas_slot.bearing_x as f64, atlas_slot.bearing_y as f64); | |
| let transform = Affine::translate(pos + bearing); | |
| let state = ctx.save_state(); | |
| if is_color { | |
| ctx.set_transform( | |
| transform | |
| * Affine::scale_non_uniform(1.0, -1.0) | |
| * Affine::translate(Vec2::new( | |
| 0.0, | |
| atlas_slot.height as f64 * -1.0, | |
| )), | |
| ); | |
| } else { | |
| ctx.set_transform(transform); | |
| } | |
| ctx.set_paint_image(image); | |
| ctx.fill_rect(&Rect { | |
| x0: 0.0, | |
| y0: 0.0, | |
| x1: atlas_slot.width as f64, | |
| y1: atlas_slot.height as f64, | |
| }); | |
| ctx.restore_state(state); | |
| } | |
| } | |
| _ => {} | |
| } | |
| } | |
| } | |
| } | |
| fn copy_pixmap_from_atlas(src: &Pixmap, dst: &mut Pixmap, src_x: u16, src_y: u16) { | |
| let src_stride = src.width() as usize; | |
| let copy_width = dst.width() as usize; | |
| let copy_height = dst.height() as usize; | |
| let dst_stride = copy_width; | |
| let src_data = src.data_as_u8_slice(); | |
| let dst_data = dst.data_as_u8_slice_mut(); | |
| for y in 0..copy_height { | |
| let dst_row_start = y * dst_stride * 4; | |
| let dst_row_end = dst_row_start + copy_width * 4; | |
| let src_row_start = ((src_y as usize + y) * src_stride + src_x as usize) * 4; | |
| let src_row_end = src_row_start + copy_width * 4; | |
| dst_data[dst_row_start..dst_row_end].copy_from_slice(&src_data[src_row_start..src_row_end]); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment