Skip to main content

diffenator3_lib/render/
renderer.rs

1/// Turn some words into images
2use harfrust::{
3    Direction, Script, ShapePlan, ShaperData, ShaperInstance, UnicodeBuffer, Variation,
4};
5use image::{DynamicImage, GrayImage, Luma};
6use skrifa::{instance::Size, raw::TableProvider, GlyphId, MetadataProvider};
7use zeno::Command;
8
9use super::{
10    cachedoutlines::CachedOutlineGlyphCollection,
11    utils::{terrible_bounding_box, RecordingPen},
12};
13use crate::dfont::DFont;
14
15pub struct Renderer<'a> {
16    shaper_data: ShaperData,
17    scale: f32,
18    font: skrifa::FontRef<'a>,
19    plan: Option<ShapePlan>,
20    instance: ShaperInstance,
21    outlines: CachedOutlineGlyphCollection<'a>,
22}
23
24impl<'a> Renderer<'a> {
25    /// Create a new renderer for a font
26    ///
27    /// Direction and script are needed for correct shaping; no automatic detection is done.
28    pub fn new(
29        dfont: &'a DFont,
30        font_size: f32,
31        direction: Option<Direction>,
32        script: Option<Script>,
33    ) -> Self {
34        let font = harfrust::FontRef::new(&dfont.backing).unwrap_or_else(|_| {
35            panic!(
36                "error constructing a Font from data for {:}",
37                dfont.family_name()
38            );
39        });
40        let shaper_data = ShaperData::new(&font);
41
42        // Convert our location into a structure that rustybuzz/harfruzz can use
43        let instance = ShaperInstance::from_variations(
44            &font,
45            dfont.location.iter().map(|setting| {
46                let tag = setting.selector;
47                let value = setting.value;
48                Variation { tag, value }
49            }),
50        );
51        let shaper = shaper_data.shaper(&font).instance(Some(&instance)).build();
52
53        let plan = if let Some(direction) = direction {
54            if script.is_some() {
55                Some(ShapePlan::new(&shaper, direction, script, None, &[]))
56            } else {
57                None
58            }
59        } else {
60            None
61        };
62        let location = (&dfont.normalized_location).into();
63        let outlines = CachedOutlineGlyphCollection::new(
64            font.outline_glyphs(),
65            Size::new(font_size),
66            location,
67        );
68
69        Self {
70            shaper_data,
71            font,
72            plan,
73            instance,
74            scale: font_size,
75            outlines,
76        }
77    }
78
79    /// Render a string to a series of commands
80    ///
81    /// The commands can be used to render the string to an image. This routine also returns a
82    /// serialized buffer that can be used both for debugging purposes and also to detect
83    /// glyph sequences which have been rendered already (which helps to speed up the comparison).
84    pub fn string_to_positioned_glyphs(&mut self, string: &str) -> Option<(String, Vec<Command>)> {
85        let mut pen = RecordingPen::default();
86
87        let mut buffer = UnicodeBuffer::new();
88        buffer.push_str(string);
89        let shaper = self
90            .shaper_data
91            .shaper(&self.font)
92            .instance(Some(&self.instance))
93            .build();
94
95        let output = if let Some(plan) = &self.plan {
96            // If we have a shaping plan, we can use it to shape the string
97            if let Some(script) = plan.script() {
98                buffer.set_script(script);
99            }
100            buffer.set_direction(plan.direction());
101            if let Some(lang) = plan.language() {
102                buffer.set_language(lang.clone());
103            }
104            shaper.shape_with_plan(plan, buffer, &[])
105        } else {
106            // Otherwise, we guess segment properties
107            buffer.guess_segment_properties();
108            shaper.shape(buffer, &[])
109        };
110        let upem = self.font.head().unwrap().units_per_em();
111        let factor = self.scale / upem as f32;
112
113        let mut cursor = 0.0;
114
115        // The results of the shaping operation are stored in the `output` buffer.
116        let positions = output.glyph_positions();
117        let infos = output.glyph_infos();
118
119        let mut serialized_buffer = String::new();
120
121        for (position, info) in positions.iter().zip(infos) {
122            pen.offset_x = cursor + (position.x_offset as f32 * factor);
123            pen.offset_y = position.y_offset as f32 * factor;
124            self.outlines.draw(GlyphId::new(info.glyph_id), &mut pen);
125            serialized_buffer.push_str(&format!("{}", info.glyph_id,));
126            if position.x_offset != 0 || position.y_offset != 0 {
127                serialized_buffer
128                    .push_str(&format!("@{},{}", position.x_offset, position.y_offset));
129            }
130            serialized_buffer.push('|');
131            cursor += position.x_advance as f32 * factor;
132        }
133        if serialized_buffer.is_empty() {
134            return None;
135        }
136        Some((serialized_buffer, pen.buffer))
137    }
138
139    /// Render a series of commands to an image
140    ///
141    /// This routine takes a series of commands returned from [string_to_positioned_glyphs]
142    /// and renders them to an image.
143    pub fn render_positioned_glyphs(&mut self, pen_buffer: &[Command]) -> GrayImage {
144        let (min_x, min_y, max_x, max_y) = terrible_bounding_box(pen_buffer);
145        let x_origin = min_x.min(0.0);
146        let y_origin = min_y.min(0.0);
147        let x_size = (max_x - x_origin).ceil() as usize;
148        let y_size = (max_y - y_origin).ceil() as usize;
149
150        let mut rasterizer = ab_glyph_rasterizer::Rasterizer::new(x_size, y_size);
151
152        let mut cursor = ab_glyph::Point { x: 0.0, y: 0.0 };
153        let v2p = |v: &zeno::Vector| ab_glyph::Point {
154            x: v.x - x_origin.ceil(),
155            y: v.y - y_origin.ceil(),
156        };
157        let mut home = v2p(&zeno::Vector::new(0.0, 0.0));
158        for command in pen_buffer {
159            match command {
160                Command::MoveTo(to) => {
161                    cursor = v2p(to);
162                    home = cursor;
163                }
164                Command::LineTo(to) => {
165                    let newpt = v2p(to);
166                    rasterizer.draw_line(cursor, newpt);
167                    cursor = newpt;
168                }
169                Command::QuadTo(ctrl, to) => {
170                    let ctrlpt = v2p(ctrl);
171                    let newpt = v2p(to);
172                    rasterizer.draw_quad(cursor, ctrlpt, newpt);
173                    cursor = newpt;
174                }
175                Command::CurveTo(ctrl0, ctrl1, to) => {
176                    let ctrl0pt = v2p(ctrl0);
177                    let ctrl1pt = v2p(ctrl1);
178                    let newpt = v2p(to);
179                    rasterizer.draw_cubic(cursor, ctrl0pt, ctrl1pt, newpt);
180                    cursor = newpt;
181                }
182                Command::Close => {
183                    if cursor != home {
184                        rasterizer.draw_line(cursor, home);
185                    }
186                }
187            };
188        }
189        let mut image = DynamicImage::new_luma8(x_size as u32, y_size as u32).into_luma8();
190        rasterizer.for_each_pixel_2d(|x, y, alpha| {
191            image.put_pixel(x, y, Luma([(alpha * 255.0) as u8]));
192        });
193        image
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    // use harfruzz::script;
201    use harfrust::script;
202
203    #[test]
204    fn test_zeno_path() {
205        let path = "NotoSansArabic-NewRegular.ttf";
206        let data = std::fs::read(path).unwrap();
207        let font = DFont::new(&data);
208        let mut renderer = Renderer::new(
209            &font,
210            40.0,
211            Some(Direction::RightToLeft),
212            Some(script::ARABIC),
213        );
214        let (_serialized_buffer, commands) =
215            renderer.string_to_positioned_glyphs("السلام عليكم").unwrap();
216        let image = renderer.render_positioned_glyphs(&commands);
217        image.save("test.png").unwrap();
218    }
219}