1use std::sync::Arc;
10
11pub mod curves;
12pub mod edge;
13pub mod fixed;
14pub mod glyph_cache;
15pub mod grayscale;
16pub mod rasterizer;
17pub mod scan_converter;
18
19#[derive(Debug, Copy, Clone, PartialEq, Eq)]
21pub enum FillRule {
22 NonZeroWinding,
24 EvenOdd,
26}
27
28#[derive(Debug, Copy, Clone, PartialEq, Eq)]
30pub enum DropoutMode {
31 None,
33 Simple,
35 Smart,
37}
38
39use typf_core::{
40 error::{RenderError, Result},
41 traits::{FontRef, Renderer},
42 types::{BitmapData, BitmapFormat, RenderOutput, ShapingResult},
43 Color, GlyphSource, RenderParams,
44};
45
46#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
47mod simd;
48
49#[cfg(feature = "parallel")]
50pub mod parallel;
51
52pub struct OpixaRenderer {
58 max_width: u32,
59 max_height: u32,
60 max_pixels: u64,
61 cache: Option<Arc<glyph_cache::GlyphCache>>,
62}
63
64impl OpixaRenderer {
65 pub fn new() -> Self {
66 Self {
67 max_width: typf_core::get_max_bitmap_width(),
68 max_height: typf_core::get_max_bitmap_height(),
69 max_pixels: typf_core::get_max_bitmap_pixels(),
70 cache: None,
71 }
72 }
73
74 pub fn with_cache() -> Self {
75 Self::with_cache_capacity(1000)
76 }
77
78 pub fn with_cache_capacity(capacity: usize) -> Self {
79 Self {
80 max_width: typf_core::get_max_bitmap_width(),
81 max_height: typf_core::get_max_bitmap_height(),
82 max_pixels: typf_core::get_max_bitmap_pixels(),
83 cache: Some(Arc::new(glyph_cache::GlyphCache::new(capacity))),
84 }
85 }
86
87 pub fn cache_stats(&self) -> Option<glyph_cache::GlyphCacheStats> {
88 self.cache.as_ref().map(|c| c.stats())
89 }
90
91 pub fn cache_hit_rate(&self) -> Option<f64> {
92 self.cache.as_ref().map(|c| c.hit_rate())
93 }
94
95 pub fn clear_cache(&self) {
96 if let Some(ref cache) = self.cache {
97 cache.clear();
98 }
99 }
100
101 #[cfg(feature = "parallel")]
103 pub fn with_parallel_rendering(&self) -> parallel::ParallelRenderer {
104 parallel::ParallelRenderer::new()
105 }
106
107 fn composite_glyph(
109 &self,
110 canvas: &mut [u8],
111 canvas_width: u32,
112 glyph: &rasterizer::GlyphBitmap,
113 x: i32,
114 y: i32,
115 color: Color,
116 ) {
117 if glyph.width == 0 || glyph.height == 0 {
118 return;
119 }
120
121 let glyph_bitmap = &glyph.data;
122 let glyph_width = glyph.width;
123 let glyph_height = glyph.height;
124
125 let x = x + glyph.left;
126 let y = y - glyph.top;
127 let canvas_height = canvas.len() as u32 / (canvas_width * 4);
128
129 let mut colored_glyph = Vec::with_capacity((glyph_width * glyph_height * 4) as usize);
130
131 for coverage in glyph_bitmap.iter() {
132 let alpha = (*coverage as u16 * color.a as u16 / 255) as u8;
133 colored_glyph.push(color.r);
134 colored_glyph.push(color.g);
135 colored_glyph.push(color.b);
136 colored_glyph.push(alpha);
137 }
138
139 #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
140 {
141 for gy in 0..glyph_height {
142 let py = y + gy as i32;
143 if py < 0 || py >= canvas_height as i32 {
144 continue;
145 }
146
147 let px_start = x.max(0);
148 let px_end = (x + glyph_width as i32).min(canvas_width as i32);
149 if px_start >= px_end {
150 continue;
151 }
152
153 let glyph_x_start = (px_start - x) as u32;
154 let glyph_x_end = (px_end - x) as u32;
155 let row_width = (glyph_x_end - glyph_x_start) as usize * 4;
156
157 let canvas_row_start = ((py as u32 * canvas_width + px_start as u32) * 4) as usize;
158 let glyph_row_start = ((gy * glyph_width + glyph_x_start) * 4) as usize;
159
160 simd::blend_over(
161 &mut canvas[canvas_row_start..canvas_row_start + row_width],
162 &colored_glyph[glyph_row_start..glyph_row_start + row_width],
163 );
164 }
165 }
166
167 #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
168 {
169 for gy in 0..glyph_height {
170 for gx in 0..glyph_width {
171 let px = x + gx as i32;
172 let py = y + gy as i32;
173
174 if px < 0 || py < 0 || px >= canvas_width as i32 || py >= canvas_height as i32 {
175 continue;
176 }
177
178 let coverage = glyph_bitmap[(gy * glyph_width + gx) as usize];
179 if coverage == 0 {
180 continue;
181 }
182
183 let canvas_idx = ((py as u32 * canvas_width + px as u32) * 4) as usize;
184
185 let alpha = (coverage as f32 / 255.0) * (color.a as f32 / 255.0);
186 let inv_alpha = 1.0 - alpha;
187
188 canvas[canvas_idx] =
189 (canvas[canvas_idx] as f32 * inv_alpha + color.r as f32 * alpha) as u8;
190 canvas[canvas_idx + 1] =
191 (canvas[canvas_idx + 1] as f32 * inv_alpha + color.g as f32 * alpha) as u8;
192 canvas[canvas_idx + 2] =
193 (canvas[canvas_idx + 2] as f32 * inv_alpha + color.b as f32 * alpha) as u8;
194 canvas[canvas_idx + 3] = ((canvas[canvas_idx + 3] as f32 * inv_alpha
195 + 255.0 * alpha)
196 .min(255.0)) as u8;
197 }
198 }
199 }
200 }
201}
202
203impl Default for OpixaRenderer {
204 fn default() -> Self {
205 Self::new()
206 }
207}
208
209impl Renderer for OpixaRenderer {
210 fn name(&self) -> &'static str {
211 "opixa"
212 }
213
214 fn render(
215 &self,
216 shaped: &ShapingResult,
217 font: Arc<dyn FontRef>,
218 params: &RenderParams,
219 ) -> Result<RenderOutput> {
220 log::debug!("OpixaRenderer: Rendering {} glyphs", shaped.glyphs.len());
221
222 let allows_outline = params
223 .glyph_sources
224 .effective_order()
225 .iter()
226 .any(|s| matches!(s, GlyphSource::Glyf | GlyphSource::Cff | GlyphSource::Cff2));
227 if !allows_outline {
228 return Err(RenderError::BackendError(
229 "opixa renderer requires outline glyph sources".to_string(),
230 )
231 .into());
232 }
233
234 let font_data = font.data();
235 let padding = params.padding as f32;
236 let glyph_size = shaped.advance_height;
237
238 let mut rendered_glyphs: Vec<RenderedGlyph> = Vec::new();
239 let mut min_y: f32 = 0.0;
240 let mut max_y: f32 = 0.0;
241
242 let mut rasterizer = if !shaped.glyphs.is_empty() {
243 match rasterizer::GlyphRasterizer::new(font_data, glyph_size) {
244 Ok(mut r) => {
245 if !params.variations.is_empty() {
246 if let Err(e) = r.set_variations(¶ms.variations) {
247 log::warn!("Variable font setup failed: {}", e);
248 }
249 }
250 Some(r)
251 },
252 Err(e) => {
253 log::warn!("Failed to create rasterizer: {}", e);
254 None
255 },
256 }
257 } else {
258 None
259 };
260
261 for glyph in &shaped.glyphs {
262 let glyph_bitmap = if let Some(ref cache) = self.cache {
263 let cache_key = glyph_cache::GlyphCacheKey::new(
264 font_data,
265 glyph.id,
266 glyph_size,
267 ¶ms.variations,
268 );
269
270 if let Some(cached) = cache.get(&cache_key) {
271 cached
272 } else {
273 let Some(ref mut rast) = rasterizer else {
274 log::warn!("Skipping glyph {} (no rasterizer available)", glyph.id);
275 continue;
276 };
277
278 let bitmap = match rast.render_glyph(
279 glyph.id,
280 FillRule::NonZeroWinding,
281 DropoutMode::None,
282 ) {
283 Ok(b) => b,
284 Err(e) => {
285 log::warn!("Glyph {} rasterization failed: {}", glyph.id, e);
286 continue;
287 },
288 };
289
290 cache.insert(cache_key, bitmap.clone());
291 bitmap
292 }
293 } else {
294 let Some(ref mut rast) = rasterizer else {
295 log::warn!("Skipping glyph {} (no rasterizer available)", glyph.id);
296 continue;
297 };
298
299 match rast.render_glyph(glyph.id, FillRule::NonZeroWinding, DropoutMode::None) {
300 Ok(bitmap) => bitmap,
301 Err(e) => {
302 log::warn!("Glyph {} rasterization failed: {}", glyph.id, e);
303 continue;
304 },
305 }
306 };
307
308 if glyph_bitmap.width == 0 || glyph_bitmap.height == 0 {
309 continue;
310 }
311
312 let glyph_top = glyph.y + glyph_bitmap.top as f32;
313 let glyph_bottom = glyph.y + glyph_bitmap.top as f32 - glyph_bitmap.height as f32;
314
315 max_y = max_y.max(glyph_top);
316 min_y = min_y.min(glyph_bottom);
317
318 rendered_glyphs.push(RenderedGlyph {
319 bitmap: glyph_bitmap,
320 glyph_x: glyph.x,
321 glyph_y: glyph.y,
322 });
323 }
324
325 let min_width = if shaped.glyphs.is_empty() && shaped.advance_width == 0.0 {
326 1
327 } else {
328 (shaped.advance_width + padding * 2.0).ceil() as u32
329 };
330 let width = min_width.max(1);
331
332 let (metrics_ascent, metrics_descent) = font
333 .metrics()
334 .filter(|m| m.units_per_em > 0 && (m.ascent != 0 || m.descent != 0))
335 .map(|m| {
336 let scale = glyph_size / (m.units_per_em as f32);
337 let ascent = (m.ascent as f32).max(0.0) * scale;
338 let descent = (m.descent as f32).abs() * scale;
339 (ascent, descent)
340 })
341 .unwrap_or((0.0, 0.0));
342
343 let glyph_top = max_y.max(0.0);
344 let glyph_bottom = (-min_y).max(0.0);
345 let top = glyph_top.max(metrics_ascent);
346 let bottom = glyph_bottom.max(metrics_descent);
347
348 let content_height = if rendered_glyphs.is_empty() {
349 16.0
350 } else {
351 top + bottom
352 };
353 let height = (content_height + padding * 2.0).ceil() as u32;
354
355 if width == 0 || height == 0 {
356 return Err(RenderError::ZeroDimensions { width, height }.into());
357 }
358
359 if width > self.max_width || height > self.max_height {
360 return Err(RenderError::DimensionsTooLarge {
361 width,
362 height,
363 max_width: self.max_width,
364 max_height: self.max_height,
365 }
366 .into());
367 }
368
369 let total_pixels = width as u64 * height as u64;
370 if total_pixels > self.max_pixels {
371 return Err(RenderError::TotalPixelsTooLarge {
372 width,
373 height,
374 total: total_pixels,
375 max: self.max_pixels,
376 }
377 .into());
378 }
379
380 let mut canvas = vec![0u8; (width * height * 4) as usize];
381
382 if let Some(bg) = params.background {
383 for pixel in canvas.chunks_exact_mut(4) {
384 pixel[0] = bg.r;
385 pixel[1] = bg.g;
386 pixel[2] = bg.b;
387 pixel[3] = bg.a;
388 }
389 }
390
391 let baseline_y = if rendered_glyphs.is_empty() {
392 padding
393 } else {
394 padding + top
395 };
396
397 for rg in rendered_glyphs {
398 let x = (rg.glyph_x + padding) as i32;
399 let y = (baseline_y + rg.glyph_y) as i32;
400
401 self.composite_glyph(&mut canvas, width, &rg.bitmap, x, y, params.foreground);
402 }
403
404 Ok(RenderOutput::Bitmap(BitmapData {
405 width,
406 height,
407 format: BitmapFormat::Rgba8,
408 data: canvas,
409 }))
410 }
411
412 fn supports_format(&self, format: &str) -> bool {
413 matches!(format, "bitmap" | "rgba" | "rgb" | "gray")
414 }
415}
416
417struct RenderedGlyph {
418 bitmap: rasterizer::GlyphBitmap,
419 glyph_x: f32,
420 glyph_y: f32,
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426 use typf_core::{
427 types::{Direction, PositionedGlyph},
428 GlyphSource, GlyphSourcePreference,
429 };
430
431 #[test]
432 fn test_basic_rendering() {
433 let renderer = OpixaRenderer::new();
434
435 let shaped = ShapingResult {
436 glyphs: vec![
437 PositionedGlyph {
438 id: 72, x: 0.0,
440 y: 0.0,
441 advance: 10.0,
442 cluster: 0,
443 },
444 PositionedGlyph {
445 id: 105, x: 10.0,
447 y: 0.0,
448 advance: 5.0,
449 cluster: 1,
450 },
451 ],
452 advance_width: 15.0,
453 advance_height: 16.0,
454 direction: Direction::LeftToRight,
455 };
456
457 struct MockFont;
458 impl FontRef for MockFont {
459 fn data(&self) -> &[u8] {
460 &[]
461 }
462 fn units_per_em(&self) -> u16 {
463 1000
464 }
465 fn glyph_id(&self, _ch: char) -> Option<u32> {
466 Some(0)
467 }
468 fn advance_width(&self, _glyph_id: u32) -> f32 {
469 500.0
470 }
471 }
472
473 let font = Arc::new(MockFont);
474 let params = RenderParams::default();
475
476 let result = renderer.render(&shaped, font, ¶ms).unwrap();
477
478 match result {
479 RenderOutput::Bitmap(bitmap) => {
480 assert_eq!(bitmap.format, BitmapFormat::Rgba8);
481 assert!(bitmap.width > 0);
482 assert!(bitmap.height > 0);
483 assert_eq!(
484 bitmap.data.len(),
485 (bitmap.width * bitmap.height * 4) as usize
486 );
487 },
488 _ => panic!("Expected bitmap output"),
489 }
490 }
491
492 #[test]
493 fn errors_when_outlines_denied() {
494 let renderer = OpixaRenderer::new();
495
496 let shaped = ShapingResult {
497 glyphs: vec![PositionedGlyph {
498 id: 1,
499 x: 0.0,
500 y: 0.0,
501 advance: 10.0,
502 cluster: 0,
503 }],
504 advance_width: 10.0,
505 advance_height: 16.0,
506 direction: Direction::LeftToRight,
507 };
508
509 struct MockFont;
510 impl FontRef for MockFont {
511 fn data(&self) -> &[u8] {
512 &[]
513 }
514 fn units_per_em(&self) -> u16 {
515 1000
516 }
517 fn glyph_id(&self, _ch: char) -> Option<u32> {
518 Some(1)
519 }
520 fn advance_width(&self, _glyph_id: u32) -> f32 {
521 500.0
522 }
523 }
524
525 let font = Arc::new(MockFont);
526 let params = RenderParams {
527 glyph_sources: GlyphSourcePreference::from_parts(vec![GlyphSource::Colr1], []),
528 ..RenderParams::default()
529 };
530
531 let result = renderer.render(&shaped, font, ¶ms);
532 assert!(result.is_err(), "outline denial should be an error");
533 }
534
535 #[test]
536 fn test_with_background() {
537 let renderer = OpixaRenderer::new();
538
539 let shaped = ShapingResult {
540 glyphs: vec![],
541 advance_width: 100.0,
542 advance_height: 20.0,
543 direction: Direction::LeftToRight,
544 };
545
546 struct MockFont;
547 impl FontRef for MockFont {
548 fn data(&self) -> &[u8] {
549 &[]
550 }
551 fn units_per_em(&self) -> u16 {
552 1000
553 }
554 fn glyph_id(&self, _ch: char) -> Option<u32> {
555 Some(0)
556 }
557 fn advance_width(&self, _glyph_id: u32) -> f32 {
558 500.0
559 }
560 }
561
562 let font = Arc::new(MockFont);
563 let params = RenderParams {
564 background: Some(Color::rgba(255, 0, 0, 255)),
565 ..Default::default()
566 };
567
568 let result = renderer.render(&shaped, font, ¶ms).unwrap();
569
570 match result {
571 RenderOutput::Bitmap(bitmap) => {
572 assert_eq!(bitmap.data[0], 255); assert_eq!(bitmap.data[1], 0); assert_eq!(bitmap.data[2], 0); assert_eq!(bitmap.data[3], 255); },
578 _ => panic!("Expected bitmap output"),
579 }
580 }
581}