diffenator3_lib/render/
renderer.rs1use 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 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 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 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 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 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 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 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 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}