Merge pull request #297 from linebender/vello-fello

Replace font backend
diff --git a/Cargo.toml b/Cargo.toml
index ad6043f..397ece2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -44,7 +44,7 @@
 parking_lot = "0.12"
 bytemuck = { version = "1.12.1", features = ["derive"] }
 smallvec = "1.8.0"
-moscato = { git = "https://github.com/dfrg/pinot", rev = "59db153" }
+fello = { git = "https://github.com/dfrg/fount", rev = "a8c0686ca9da236420c04d1f476c41cf7fe6d2ba" }
 peniko = { git = "https://github.com/linebender/peniko", rev = "cafdac9a211a0fb2fec5656bd663d1ac770bcc81" }
 guillotiere = "0.6.2"
 
diff --git a/examples/assets/inconsolata/Inconsolata.ttf b/examples/assets/inconsolata/Inconsolata.ttf
new file mode 100644
index 0000000..34848ca
--- /dev/null
+++ b/examples/assets/inconsolata/Inconsolata.ttf
Binary files differ
diff --git a/examples/assets/inconsolata/LICENSE.txt b/examples/assets/inconsolata/LICENSE.txt
new file mode 100644
index 0000000..77b1731
--- /dev/null
+++ b/examples/assets/inconsolata/LICENSE.txt
@@ -0,0 +1,91 @@
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded, 
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/examples/scenes/src/simple_text.rs b/examples/scenes/src/simple_text.rs
index 1d3dc47..c6371af 100644
--- a/examples/scenes/src/simple_text.rs
+++ b/examples/scenes/src/simple_text.rs
@@ -18,11 +18,9 @@
 
 use vello::{
     encoding::Glyph,
-    glyph::{
-        pinot,
-        pinot::{FontRef, TableProvider},
-        GlyphContext,
-    },
+    fello::meta::MetadataProvider,
+    fello::raw::FontRef,
+    glyph::GlyphContext,
     kurbo::Affine,
     peniko::{Blob, Brush, BrushRef, Font, StyleRef},
     SceneBuilder,
@@ -30,24 +28,28 @@
 
 // This is very much a hack to get things working.
 // On Windows, can set this to "c:\\Windows\\Fonts\\seguiemj.ttf" to get color emoji
-const FONT_DATA: &[u8] = include_bytes!("../../assets/roboto/Roboto-Regular.ttf");
+const ROBOTO_FONT: &[u8] = include_bytes!("../../assets/roboto/Roboto-Regular.ttf");
+const INCONSOLATA_FONT: &[u8] = include_bytes!("../../assets/inconsolata/Inconsolata.ttf");
 
 pub struct SimpleText {
     gcx: GlyphContext,
-    font: Font,
+    roboto: Font,
+    inconsolata: Font,
 }
 
 impl SimpleText {
     pub fn new() -> Self {
         Self {
             gcx: GlyphContext::new(),
-            font: Font::new(Blob::new(Arc::new(FONT_DATA)), 0),
+            roboto: Font::new(Blob::new(Arc::new(ROBOTO_FONT)), 0),
+            inconsolata: Font::new(Blob::new(Arc::new(INCONSOLATA_FONT)), 0),
         }
     }
 
     pub fn add_run<'a>(
         &mut self,
         builder: &mut SceneBuilder,
+        font: Option<&Font>,
         size: f32,
         brush: impl Into<BrushRef<'a>>,
         transform: Affine,
@@ -55,92 +57,126 @@
         style: impl Into<StyleRef<'a>>,
         text: &str,
     ) {
-        let font = FontRef {
-            data: FONT_DATA,
-            offset: 0,
+        self.add_var_run(
+            builder,
+            font,
+            size,
+            &[],
+            brush,
+            transform,
+            glyph_transform,
+            style,
+            text,
+        );
+    }
+
+    pub fn add_var_run<'a>(
+        &mut self,
+        builder: &mut SceneBuilder,
+        font: Option<&Font>,
+        size: f32,
+        variations: &[(&str, f32)],
+        brush: impl Into<BrushRef<'a>>,
+        transform: Affine,
+        glyph_transform: Option<Affine>,
+        style: impl Into<StyleRef<'a>>,
+        text: &str,
+    ) {
+        let default_font = if variations.is_empty() {
+            &self.roboto
+        } else {
+            &self.inconsolata
         };
+        let font = font.unwrap_or(default_font);
+        let font_ref = to_font_ref(font).unwrap();
         let brush = brush.into();
         let style = style.into();
-        if let Some(cmap) = font.cmap() {
-            if let Some(hmtx) = font.hmtx() {
-                let upem = font.head().map(|head| head.units_per_em()).unwrap_or(1000) as f64;
-                let scale = size as f64 / upem;
-                let hmetrics = hmtx.hmetrics();
-                let default_advance = hmetrics
-                    .get(hmetrics.len().saturating_sub(1))
-                    .map(|h| h.advance_width)
-                    .unwrap_or(0);
-                let mut pen_x = 0f64;
-                builder
-                    .draw_glyphs(&self.font)
-                    .font_size(size)
-                    .transform(transform)
-                    .glyph_transform(glyph_transform)
-                    .brush(brush)
-                    .draw(
-                        style,
-                        text.chars().map(|ch| {
-                            let gid = cmap.map(ch as u32).unwrap_or(0);
-                            let advance = hmetrics
-                                .get(gid as usize)
-                                .map(|h| h.advance_width)
-                                .unwrap_or(default_advance)
-                                as f64
-                                * scale;
-                            let x = pen_x as f32;
-                            pen_x += advance;
-                            Glyph {
-                                id: gid as u32,
-                                x,
-                                y: 0.0,
-                            }
-                        }),
-                    )
-            }
-        }
+        let axes = font_ref.axes();
+        let fello_size = vello::fello::Size::new(size);
+        let coords = axes
+            .normalize(variations.iter().copied())
+            .collect::<Vec<_>>();
+        let charmap = font_ref.charmap();
+        let metrics = font_ref.metrics(fello_size, coords.as_slice().into());
+        let line_height = metrics.ascent - metrics.descent + metrics.leading;
+        let glyph_metrics = font_ref.glyph_metrics(fello_size, coords.as_slice().into());
+        let mut pen_x = 0f32;
+        let mut pen_y = 0f32;
+        builder
+            .draw_glyphs(font)
+            .font_size(size)
+            .transform(transform)
+            .glyph_transform(glyph_transform)
+            .normalized_coords(&coords)
+            .brush(brush)
+            .draw(
+                style,
+                text.chars().filter_map(|ch| {
+                    if ch == '\n' {
+                        pen_y += line_height;
+                        pen_x = 0.0;
+                        return None;
+                    }
+                    let gid = charmap.map(ch).unwrap_or_default();
+                    let advance = glyph_metrics.advance_width(gid).unwrap_or_default();
+                    let x = pen_x as f32;
+                    pen_x += advance;
+                    Some(Glyph {
+                        id: gid.to_u16() as u32,
+                        x,
+                        y: pen_y,
+                    })
+                }),
+            );
     }
 
     pub fn add(
         &mut self,
         builder: &mut SceneBuilder,
-        font: Option<&FontRef>,
+        font: Option<&Font>,
         size: f32,
         brush: Option<&Brush>,
         transform: Affine,
         text: &str,
     ) {
-        let font = font.unwrap_or(&FontRef {
-            data: FONT_DATA,
-            offset: 0,
-        });
-        if let Some(cmap) = font.cmap() {
-            if let Some(hmtx) = font.hmtx() {
-                let upem = font.head().map(|head| head.units_per_em()).unwrap_or(1000) as f64;
-                let scale = size as f64 / upem;
-                let vars: [(pinot::types::Tag, f32); 0] = [];
-                let mut provider = self.gcx.new_provider(font, None, size, false, vars);
-                let hmetrics = hmtx.hmetrics();
-                let default_advance = hmetrics
-                    .get(hmetrics.len().saturating_sub(1))
-                    .map(|h| h.advance_width)
-                    .unwrap_or(0);
-                let mut pen_x = 0f64;
-                for ch in text.chars() {
-                    let gid = cmap.map(ch as u32).unwrap_or(0);
-                    let advance = hmetrics
-                        .get(gid as usize)
-                        .map(|h| h.advance_width)
-                        .unwrap_or(default_advance) as f64
-                        * scale;
-                    if let Some(glyph) = provider.get(gid, brush) {
-                        let xform = transform
-                            * Affine::translate((pen_x, 0.0))
-                            * Affine::scale_non_uniform(1.0, -1.0);
-                        builder.append(&glyph, Some(xform));
-                    }
-                    pen_x += advance;
-                }
+        let default_font = FontRef::new(ROBOTO_FONT).unwrap();
+        let font = font
+            .map(|font| to_font_ref(font))
+            .flatten()
+            .unwrap_or(default_font);
+        let fello_size = vello::fello::Size::new(size);
+        let charmap = font.charmap();
+        let metrics = font.metrics(fello_size, Default::default());
+        let line_height = metrics.ascent - metrics.descent + metrics.leading;
+        let glyph_metrics = font.glyph_metrics(fello_size, Default::default());
+        let mut pen_x = 0f64;
+        let mut pen_y = 0f64;
+        let vars: [(&str, f32); 0] = [];
+        let mut provider = self.gcx.new_provider(&font, None, size, false, vars);
+        for ch in text.chars() {
+            if ch == '\n' {
+                pen_y += line_height as f64;
+                pen_x = 0.0;
+                continue;
             }
+            let gid = charmap.map(ch).unwrap_or_default();
+            let advance = glyph_metrics.advance_width(gid).unwrap_or_default() as f64;
+            if let Some(glyph) = provider.get(gid.to_u16(), brush) {
+                let xform = transform
+                    * Affine::translate((pen_x, pen_y))
+                    * Affine::scale_non_uniform(1.0, -1.0);
+                builder.append(&glyph, Some(xform));
+            }
+            pen_x += advance;
         }
     }
 }
+
+fn to_font_ref<'a>(font: &'a Font) -> Option<FontRef<'a>> {
+    use vello::fello::raw::FileRef;
+    let file_ref = FileRef::new(font.data.as_ref()).ok()?;
+    match file_ref {
+        FileRef::Font(font) => Some(font),
+        FileRef::Collection(collection) => collection.get(font.index).ok(),
+    }
+}
diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs
index ff3ff60..23ea373 100644
--- a/examples/scenes/src/test_scenes.rs
+++ b/examples/scenes/src/test_scenes.rs
@@ -135,6 +135,7 @@
     );
     params.text.add_run(
         sb,
+        None,
         text_size,
         Color::WHITE,
         Affine::translate((110.0, 700.0)),
@@ -143,6 +144,21 @@
         &Stroke::new(1.0),
         s,
     );
+    let t = ((params.time).sin() * 0.5 + 0.5) as f32;
+    let weight = t * 700.0 + 200.0;
+    let width = t * 150.0 + 50.0;
+    params.text.add_var_run(
+        sb,
+        None,
+        72.0,
+        &[("wght", weight), ("wdth", width)],
+        Color::WHITE,
+        Affine::translate((110.0, 800.0)),
+        // Add a skew to simulate an oblique font.
+        None,
+        Fill::NonZero,
+        "And some vello\ntext with a newline",
+    );
     let th = params.time as f64;
     let center = Point::new(500.0, 500.0);
     let mut p1 = center;
diff --git a/src/encoding/encoding.rs b/src/encoding/encoding.rs
index d212a89..03ca730 100644
--- a/src/encoding/encoding.rs
+++ b/src/encoding/encoding.rs
@@ -21,6 +21,7 @@
     PathEncoder, PathTag, Transform,
 };
 
+use fello::NormalizedCoord;
 use peniko::{kurbo::Shape, BlendMode, BrushRef, ColorStop, Extend, GradientKind, Image};
 
 /// Encoded data streams for a scene.
@@ -47,7 +48,7 @@
     /// Sequences of glyphs.
     pub glyph_runs: Vec<GlyphRun>,
     /// Normalized coordinate buffer for variable fonts.
-    pub normalized_coords: Vec<i16>,
+    pub normalized_coords: Vec<NormalizedCoord>,
     /// Number of encoded paths.
     pub n_paths: u32,
     /// Number of encoded path segments.
diff --git a/src/encoding/glyph_cache.rs b/src/encoding/glyph_cache.rs
index cbfea17..f919afb 100644
--- a/src/encoding/glyph_cache.rs
+++ b/src/encoding/glyph_cache.rs
@@ -17,8 +17,9 @@
 use std::collections::HashMap;
 
 use super::{Encoding, StreamOffsets};
-use crate::glyph::GlyphProvider;
 
+use fello::scale::Scaler;
+use fello::GlyphId;
 use peniko::{Fill, Style};
 
 #[derive(Copy, Clone, PartialEq, Eq, Hash, Default, Debug)]
@@ -46,18 +47,31 @@
         &mut self,
         key: GlyphKey,
         style: &Style,
-        scaler: &mut GlyphProvider,
+        scaler: &mut Scaler,
     ) -> Option<CachedRange> {
+        let is_fill = matches!(style, Style::Fill(_));
+        let is_var = !scaler.normalized_coords().is_empty();
         let encoding_cache = &mut self.encoding;
         let mut encode_glyph = || {
             let start = encoding_cache.stream_offsets();
-            scaler.encode_glyph(key.glyph_id as u16, style, encoding_cache)?;
+            match style {
+                Style::Fill(Fill::NonZero) => encoding_cache.encode_linewidth(-1.0),
+                Style::Fill(Fill::EvenOdd) => encoding_cache.encode_linewidth(-2.0),
+                Style::Stroke(stroke) => encoding_cache.encode_linewidth(stroke.width),
+            }
+            let mut path = crate::glyph::PathEncoderPen(encoding_cache.encode_path(is_fill));
+            scaler
+                .outline(GlyphId::new(key.glyph_id as u16), &mut path)
+                .ok()?;
+            if path.0.finish(false) == 0 {
+                return None;
+            }
             let end = encoding_cache.stream_offsets();
             Some(CachedRange { start, end })
         };
-        // For now, only cache non-zero filled glyphs so we don't need to keep style
+        // For now, only cache non-zero filled, non-variable glyphs so we don't need to keep style
         // as part of the key.
-        let range = if matches!(style, Style::Fill(Fill::NonZero)) {
+        let range = if matches!(style, Style::Fill(Fill::NonZero)) && !is_var {
             use std::collections::hash_map::Entry;
             match self.glyphs.entry(key) {
                 Entry::Occupied(entry) => *entry.get(),
diff --git a/src/encoding/resolve.rs b/src/encoding/resolve.rs
index 58b942f..0077b5c 100644
--- a/src/encoding/resolve.rs
+++ b/src/encoding/resolve.rs
@@ -17,7 +17,6 @@
 use std::ops::Range;
 
 use bytemuck::{Pod, Zeroable};
-use moscato::pinot::FontRef;
 use peniko::Image;
 
 use super::{
@@ -26,7 +25,6 @@
     ramp_cache::{RampCache, Ramps},
     DrawTag, Encoding, PathTag, StreamOffsets, Transform,
 };
-use crate::glyph::GlyphContext;
 use crate::shaders;
 
 /// Layout of a packed encoding.
@@ -144,7 +142,7 @@
 pub struct Resolver {
     glyph_cache: GlyphCache,
     glyph_ranges: Vec<CachedRange>,
-    glyph_cx: GlyphContext,
+    glyph_cx: fello::scale::Context,
     ramp_cache: RampCache,
     image_cache: ImageCache,
     pending_images: Vec<PendingImage>,
@@ -389,14 +387,19 @@
                     let run = &encoding.glyph_runs[*index];
                     let font_id = run.font.data.id();
                     let font_size_u32 = run.font_size.to_bits();
-                    let Some(font) = FontRef::from_index(run.font.data.as_ref(), run.font.index) else { continue };
+                    let Ok(font_file) = fello::raw::FileRef::new(run.font.data.as_ref()) else { continue };
+                    let font = match font_file {
+                        fello::raw::FileRef::Font(font) => Some(font),
+                        fello::raw::FileRef::Collection(collection) => {
+                            collection.get(run.font.index).ok()
+                        }
+                    };
+                    let Some(font) = font else { continue };
                     let glyphs = &encoding.glyphs[run.glyphs.clone()];
-                    let _coords = &encoding.normalized_coords[run.normalized_coords.clone()];
-                    let vars: [(moscato::pinot::types::Tag, f32); 0] = [];
-                    let hint_id = if run.font.index < 0xFF {
-                        Some(font_id << 8 | run.font.index as u64)
-                    } else {
-                        None
+                    let coords = &encoding.normalized_coords[run.normalized_coords.clone()];
+                    let key = fello::FontKey {
+                        data_id: font_id,
+                        index: run.font.index,
                     };
                     let mut hint = run.hint;
                     let mut font_size = run.font_size;
@@ -417,7 +420,12 @@
                     }
                     let mut scaler = self
                         .glyph_cx
-                        .new_provider(&font, hint_id, font_size, hint, vars);
+                        .new_scaler()
+                        .key(Some(key))
+                        .hint(hint.then_some(fello::scale::Hinting::VerticalSubpixel))
+                        .coords(coords)
+                        .size(fello::Size::new(font_size))
+                        .build(&font);
                     let glyph_start = self.glyph_ranges.len();
                     for glyph in glyphs {
                         let key = GlyphKey {
diff --git a/src/glyph.rs b/src/glyph.rs
index 7f825f2..b398e6f 100644
--- a/src/glyph.rs
+++ b/src/glyph.rs
@@ -16,17 +16,19 @@
 
 //! Support for glyph rendering.
 
-pub use moscato::pinot;
+use fello::scale::Pen;
 
-use crate::encoding::Encoding;
+use crate::encoding::{Encoding, PathEncoder};
 use crate::scene::{SceneBuilder, SceneFragment};
-use peniko::kurbo::{Affine, Rect};
-use peniko::{Brush, Color, Fill, Mix, Style};
+use peniko::kurbo::Affine;
+use peniko::{Brush, Color, Fill, Style};
 
-use moscato::{Context, Scaler};
-use pinot::{types::Tag, FontRef};
-
-use smallvec::SmallVec;
+use fello::{
+    raw::types::GlyphId,
+    raw::FontRef,
+    scale::{Context, Scaler},
+    FontKey, Setting, Size,
+};
 
 /// General context for creating scene fragments for glyph outlines.
 pub struct GlyphContext {
@@ -52,30 +54,23 @@
     pub fn new_provider<'a, V>(
         &'a mut self,
         font: &FontRef<'a>,
-        font_id: Option<u64>,
+        font_id: Option<FontKey>,
         ppem: f32,
         hint: bool,
         variations: V,
     ) -> GlyphProvider<'a>
     where
         V: IntoIterator,
-        V::Item: Into<(Tag, f32)>,
+        V::Item: Into<Setting<f32>>,
     {
-        let scaler = if let Some(font_id) = font_id {
-            self.ctx
-                .new_scaler_with_id(font, font_id)
-                .size(ppem)
-                .hint(hint)
-                .variations(variations)
-                .build()
-        } else {
-            self.ctx
-                .new_scaler(font)
-                .size(ppem)
-                .hint(hint)
-                .variations(variations)
-                .build()
-        };
+        let scaler = self
+            .ctx
+            .new_scaler()
+            .size(Size::new(ppem))
+            .hint(hint.then_some(fello::scale::Hinting::VerticalSubpixel))
+            .key(font_id)
+            .variations(variations)
+            .build(font);
         GlyphProvider { scaler }
     }
 }
@@ -90,276 +85,86 @@
     /// Returns a scene fragment containing the commands to render the
     /// specified glyph.
     pub fn get(&mut self, gid: u16, brush: Option<&Brush>) -> Option<SceneFragment> {
-        let glyph = self.scaler.glyph(gid)?;
-        let path = glyph.path(0)?;
         let mut fragment = SceneFragment::default();
         let mut builder = SceneBuilder::for_fragment(&mut fragment);
+        let mut path = BezPathPen::default();
+        self.scaler.outline(GlyphId::new(gid), &mut path).ok()?;
         builder.fill(
             Fill::NonZero,
             Affine::IDENTITY,
             brush.unwrap_or(&Brush::Solid(Color::rgb8(255, 255, 255))),
             None,
-            &convert_path(path.elements()),
+            &path.0,
         );
         Some(fragment)
     }
 
     pub fn encode_glyph(&mut self, gid: u16, style: &Style, encoding: &mut Encoding) -> Option<()> {
-        let glyph = self.scaler.glyph(gid)?;
-        let path = glyph.path(0)?;
         match style {
             Style::Fill(Fill::NonZero) => encoding.encode_linewidth(-1.0),
             Style::Fill(Fill::EvenOdd) => encoding.encode_linewidth(-2.0),
             Style::Stroke(stroke) => encoding.encode_linewidth(stroke.width),
         }
-        let mut path_encoder = encoding.encode_path(matches!(style, Style::Fill(_)));
-        for el in path.elements() {
-            use moscato::Element::*;
-            match el {
-                MoveTo(p) => path_encoder.move_to(p.x, p.y),
-                LineTo(p) => path_encoder.line_to(p.x, p.y),
-                QuadTo(c, p) => path_encoder.quad_to(c.x, c.y, p.x, p.y),
-                CurveTo(c0, c1, p) => path_encoder.cubic_to(c0.x, c0.y, c1.x, c1.y, p.x, p.y),
-                Close => path_encoder.close(),
-            }
-        }
-        if path_encoder.finish(false) != 0 {
+        let mut path = PathEncoderPen(encoding.encode_path(matches!(style, Style::Fill(_))));
+        self.scaler.outline(GlyphId::new(gid), &mut path).ok()?;
+        if path.0.finish(false) != 0 {
             Some(())
         } else {
             None
         }
     }
+}
 
-    /// Returns a scene fragment containing the commands and resources to
-    /// render the specified color glyph.
-    pub fn get_color(&mut self, palette_index: u16, gid: u16) -> Option<SceneFragment> {
-        use moscato::Command;
-        let glyph = self.scaler.color_glyph(palette_index, gid)?;
-        let mut fragment = SceneFragment::default();
-        let mut builder = SceneBuilder::for_fragment(&mut fragment);
-        let mut xform_stack: SmallVec<[Affine; 8]> = SmallVec::new();
-        for command in glyph.commands() {
-            match command {
-                Command::PushTransform(xform) => {
-                    let xform = if let Some(parent) = xform_stack.last() {
-                        convert_transform(xform) * *parent
-                    } else {
-                        convert_transform(xform)
-                    };
-                    xform_stack.push(xform);
-                }
-                Command::PopTransform => {
-                    xform_stack.pop();
-                }
-                Command::PushClip(path_index) => {
-                    let path = glyph.path(*path_index)?;
-                    if let Some(xform) = xform_stack.last() {
-                        builder.push_layer(
-                            Mix::Clip,
-                            1.0,
-                            Affine::IDENTITY,
-                            &convert_transformed_path(path.elements(), xform),
-                        );
-                    } else {
-                        builder.push_layer(
-                            Mix::Clip,
-                            1.0,
-                            Affine::IDENTITY,
-                            &convert_path(path.elements()),
-                        );
-                    }
-                }
-                Command::PopClip => builder.pop_layer(),
-                Command::PushLayer(bounds) => {
-                    let mut min = convert_point(bounds.min);
-                    let mut max = convert_point(bounds.max);
-                    if let Some(xform) = xform_stack.last() {
-                        min = *xform * min;
-                        max = *xform * max;
-                    }
-                    let rect = Rect::from_points(min, max);
-                    builder.push_layer(Mix::Normal, 1.0, Affine::IDENTITY, &rect);
-                }
-                Command::PopLayer => builder.pop_layer(),
-                Command::BeginBlend(bounds, mode) => {
-                    let mut min = convert_point(bounds.min);
-                    let mut max = convert_point(bounds.max);
-                    if let Some(xform) = xform_stack.last() {
-                        min = *xform * min;
-                        max = *xform * max;
-                    }
-                    let rect = Rect::from_points(min, max);
-                    builder.push_layer(convert_blend(*mode), 1.0, Affine::IDENTITY, &rect);
-                }
-                Command::EndBlend => builder.pop_layer(),
-                Command::SimpleFill(path_index, brush, brush_xform) => {
-                    let path = glyph.path(*path_index)?;
-                    let brush = convert_brush(brush);
-                    let brush_xform = brush_xform.map(|xform| convert_transform(&xform));
-                    if let Some(xform) = xform_stack.last() {
-                        builder.fill(
-                            Fill::NonZero,
-                            Affine::IDENTITY,
-                            &brush,
-                            brush_xform.map(|x| x * *xform),
-                            &convert_transformed_path(path.elements(), xform),
-                        );
-                    } else {
-                        builder.fill(
-                            Fill::NonZero,
-                            Affine::IDENTITY,
-                            &brush,
-                            brush_xform,
-                            &convert_path(path.elements()),
-                        );
-                    }
-                }
-                Command::Fill(_brush, _brush_xform) => {
-                    // TODO: this needs to compute a bounding box for
-                    // the parent clips
-                }
-            }
-        }
-        Some(fragment)
+#[derive(Default)]
+struct BezPathPen(peniko::kurbo::BezPath);
+
+impl Pen for BezPathPen {
+    fn move_to(&mut self, x: f32, y: f32) {
+        self.0.move_to((x as f64, y as f64))
+    }
+
+    fn line_to(&mut self, x: f32, y: f32) {
+        self.0.line_to((x as f64, y as f64))
+    }
+
+    fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
+        self.0
+            .quad_to((cx0 as f64, cy0 as f64), (x as f64, y as f64))
+    }
+
+    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
+        self.0.curve_to(
+            (cx0 as f64, cy0 as f64),
+            (cx1 as f64, cy1 as f64),
+            (x as f64, y as f64),
+        )
+    }
+
+    fn close(&mut self) {
+        self.0.close_path()
     }
 }
 
-fn convert_path(path: impl Iterator<Item = moscato::Element> + Clone) -> peniko::kurbo::BezPath {
-    let mut result = peniko::kurbo::BezPath::new();
-    for el in path {
-        result.push(convert_path_el(&el));
+pub(crate) struct PathEncoderPen<'a>(pub PathEncoder<'a>);
+
+impl Pen for PathEncoderPen<'_> {
+    fn move_to(&mut self, x: f32, y: f32) {
+        self.0.move_to(x, y)
     }
-    result
-}
 
-fn convert_transformed_path(
-    path: impl Iterator<Item = moscato::Element> + Clone,
-    xform: &Affine,
-) -> peniko::kurbo::BezPath {
-    let mut result = peniko::kurbo::BezPath::new();
-    for el in path {
-        result.push(*xform * convert_path_el(&el));
+    fn line_to(&mut self, x: f32, y: f32) {
+        self.0.line_to(x, y)
     }
-    result
-}
 
-fn convert_blend(mode: moscato::CompositeMode) -> peniko::BlendMode {
-    use moscato::CompositeMode;
-    use peniko::{BlendMode, Compose};
-    let mut mix = Mix::Normal;
-    let mut compose = Compose::SrcOver;
-    match mode {
-        CompositeMode::Clear => compose = Compose::Clear,
-        CompositeMode::Src => compose = Compose::Copy,
-        CompositeMode::Dest => compose = Compose::Dest,
-        CompositeMode::SrcOver => {}
-        CompositeMode::DestOver => compose = Compose::DestOver,
-        CompositeMode::SrcIn => compose = Compose::SrcIn,
-        CompositeMode::DestIn => compose = Compose::DestIn,
-        CompositeMode::SrcOut => compose = Compose::SrcOut,
-        CompositeMode::DestOut => compose = Compose::DestOut,
-        CompositeMode::SrcAtop => compose = Compose::SrcAtop,
-        CompositeMode::DestAtop => compose = Compose::DestAtop,
-        CompositeMode::Xor => compose = Compose::Xor,
-        CompositeMode::Plus => compose = Compose::Plus,
-        CompositeMode::Screen => mix = Mix::Screen,
-        CompositeMode::Overlay => mix = Mix::Overlay,
-        CompositeMode::Darken => mix = Mix::Darken,
-        CompositeMode::Lighten => mix = Mix::Lighten,
-        CompositeMode::ColorDodge => mix = Mix::ColorDodge,
-        CompositeMode::ColorBurn => mix = Mix::ColorBurn,
-        CompositeMode::HardLight => mix = Mix::HardLight,
-        CompositeMode::SoftLight => mix = Mix::SoftLight,
-        CompositeMode::Difference => mix = Mix::Difference,
-        CompositeMode::Exclusion => mix = Mix::Exclusion,
-        CompositeMode::Multiply => mix = Mix::Multiply,
-        CompositeMode::HslHue => mix = Mix::Hue,
-        CompositeMode::HslSaturation => mix = Mix::Saturation,
-        CompositeMode::HslColor => mix = Mix::Color,
-        CompositeMode::HslLuminosity => mix = Mix::Luminosity,
+    fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
+        self.0.quad_to(cx0, cy0, x, y)
     }
-    BlendMode { mix, compose }
-}
 
-fn convert_transform(xform: &moscato::Transform) -> peniko::kurbo::Affine {
-    peniko::kurbo::Affine::new([
-        xform.xx as f64,
-        xform.yx as f64,
-        xform.xy as f64,
-        xform.yy as f64,
-        xform.dx as f64,
-        xform.dy as f64,
-    ])
-}
-
-fn convert_point(point: moscato::Point) -> peniko::kurbo::Point {
-    peniko::kurbo::Point::new(point.x as f64, point.y as f64)
-}
-
-fn convert_brush(brush: &moscato::Brush) -> peniko::Brush {
-    use peniko::Gradient;
-    match brush {
-        moscato::Brush::Solid(color) => Brush::Solid(Color {
-            r: color.r,
-            g: color.g,
-            b: color.b,
-            a: color.a,
-        }),
-        moscato::Brush::LinearGradient(grad) => Brush::Gradient(
-            Gradient::new_linear(convert_point(grad.start), convert_point(grad.end))
-                .with_stops(convert_stops(&grad.stops).as_slice())
-                .with_extend(convert_extend(grad.extend)),
-        ),
-
-        moscato::Brush::RadialGradient(grad) => Brush::Gradient(
-            Gradient::new_two_point_radial(
-                convert_point(grad.center0),
-                grad.radius0,
-                convert_point(grad.center1),
-                grad.radius1,
-            )
-            .with_stops(convert_stops(&grad.stops).as_slice())
-            .with_extend(convert_extend(grad.extend)),
-        ),
+    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
+        self.0.cubic_to(cx0, cy0, cx1, cy1, x, y)
     }
-}
 
-fn convert_stops(stops: &[moscato::ColorStop]) -> peniko::ColorStops {
-    stops
-        .iter()
-        .map(|stop| {
-            (
-                stop.offset,
-                Color {
-                    r: stop.color.r,
-                    g: stop.color.g,
-                    b: stop.color.b,
-                    a: stop.color.a,
-                },
-            )
-                .into()
-        })
-        .collect()
-}
-
-fn convert_extend(extend: moscato::ExtendMode) -> peniko::Extend {
-    use peniko::Extend::*;
-    match extend {
-        moscato::ExtendMode::Pad => Pad,
-        moscato::ExtendMode::Repeat => Repeat,
-        moscato::ExtendMode::Reflect => Reflect,
-    }
-}
-
-fn convert_path_el(el: &moscato::Element) -> peniko::kurbo::PathEl {
-    use peniko::kurbo::PathEl::*;
-    match el {
-        moscato::Element::MoveTo(p0) => MoveTo(convert_point(*p0)),
-        moscato::Element::LineTo(p0) => LineTo(convert_point(*p0)),
-        moscato::Element::QuadTo(p0, p1) => QuadTo(convert_point(*p0), convert_point(*p1)),
-        moscato::Element::CurveTo(p0, p1, p2) => {
-            CurveTo(convert_point(*p0), convert_point(*p1), convert_point(*p2))
-        }
-        moscato::Element::Close => ClosePath,
+    fn close(&mut self) {
+        self.0.close()
     }
 }
diff --git a/src/lib.rs b/src/lib.rs
index 91a4d44..d61e176 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -24,6 +24,9 @@
 /// 2D geometry, with a focus on curves.
 pub use peniko::kurbo;
 
+#[doc(hidden)]
+pub use fello;
+
 pub mod encoding;
 
 pub mod glyph;
diff --git a/src/scene.rs b/src/scene.rs
index 1d2b345..ce777b8 100644
--- a/src/scene.rs
+++ b/src/scene.rs
@@ -14,6 +14,7 @@
 //
 // Also licensed under MIT license, at your choice.
 
+use fello::NormalizedCoord;
 use peniko::kurbo::{Affine, Rect, Shape};
 use peniko::{BlendMode, BrushRef, Color, Fill, Font, Image, Stroke, StyleRef};
 
@@ -262,7 +263,7 @@
     }
 
     /// Sets the normalized design space coordinates for a variable font instance.
-    pub fn normalized_coords(mut self, coords: &[i16]) -> Self {
+    pub fn normalized_coords(mut self, coords: &[NormalizedCoord]) -> Self {
         self.encoding
             .normalized_coords
             .truncate(self.run.normalized_coords.start);