Skip to content

Commit 8db446e

Browse files
committed
Implement font rendering
1 parent 4bcdf14 commit 8db446e

File tree

6 files changed

+278
-41
lines changed

6 files changed

+278
-41
lines changed

Cargo.lock

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,19 @@ strip = true
3030

3131
[dependencies]
3232
ahash = { version = "0.8", default-features = false }
33+
argon2 = { version = "0.5.3", features = ["std"] }
3334
asdf-pixel-sort = { version = "0.2.1", git = "https://github.com/giraffekey/asdf-pixel-sort" }
3435
base64 = { version = "0.22", default-features = false }
3536
clap = { version = "4.5", features = ["derive"] }
3637
factorial = "0.4"
38+
fontdue = "0.9"
3739
hashbrown = "0.15"
3840
image = "0.25"
3941
itertools = { version = "0.14", default-features = false }
4042
noise = "0.9"
4143
nom = { version = "8.0", default-features = false }
4244
num = "0.4"
45+
num-bigint = "0.4.6"
4346
palette = { version = "0.7", default-features = false }
4447
png = "0.17"
4548
rand = { version = "0.9", default-features = false }
@@ -53,6 +56,7 @@ default = ["std", "simd"]
5356
std = [
5457
"ahash/std",
5558
"base64/std",
59+
"fontdue/std",
5660
"itertools/use_std",
5761
"nom/std",
5862
"palette/std",

src/functions/image.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,24 @@ builtin_function!(import_image => {
1919
}
2020
});
2121

22+
builtin_function!(import_font => {
23+
[Value::String(path)] => {
24+
todo!()
25+
}
26+
});
27+
28+
builtin_function!(text => {
29+
[Value::String(text), Value::String(font), size] => {
30+
let size = match size {
31+
Value::Integer(size) => *size as f32,
32+
Value::Float(size) => *size,
33+
_ => return Err(Error::InvalidArgument("text".into())),
34+
};
35+
36+
Value::Shape(Rc::new(RefCell::new(Shape::text(font.clone(), text.clone(), size))))
37+
}
38+
});
39+
2240
builtin_function!(image_quality => {
2341
[Value::FilterQuality(quality), Value::Shape(image)] => {
2442
let image = dedup_shape(image);

src/functions/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,8 @@ define_builtins! {
303303
"cubic_to" => {path::cubic_to, 6},
304304
"close" => {path::close, 0},
305305
"import_image" => {image::import_image, 1},
306+
"import_font" => {image::import_font, 1},
307+
"text" => {image::text, 3},
306308
"image_quality" => {image::image_quality, 2},
307309
"brighten" => {image::brighten, 2},
308310
"contrast" => {image::contrast, 2},

src/renderer.rs

Lines changed: 117 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ use crate::shape::{
1212

1313
use asdf_pixel_sort::{sort_with_options, Options};
1414
use core::{cell::RefCell, ops::Add};
15-
use image::{imageops, ImageFormat, ImageReader, Pixel};
15+
use fontdue::{Font, FontSettings};
16+
use image::{imageops, DynamicImage, ImageBuffer, ImageFormat, ImageReader, Pixel};
1617
use palette::{rgb::Rgba, FromColor};
1718
use tiny_skia::{
1819
BlendMode, FillRule, GradientStop, IntSize, LinearGradient, Mask, MaskType, Paint, Path,
@@ -45,6 +46,16 @@ enum ShapeData<'a> {
4546
zindex: f32,
4647
mask: Option<Vec<ShapeData<'a>>>,
4748
},
49+
Text {
50+
font: String,
51+
text: String,
52+
size: f32,
53+
ops: Vec<ImageOp>,
54+
transform: Transform,
55+
paint: PixmapPaint,
56+
zindex: f32,
57+
mask: Option<Vec<ShapeData<'a>>>,
58+
},
4859
Fill {
4960
color: tiny_skia::Color,
5061
zindex: f32,
@@ -61,6 +72,7 @@ impl ShapeData<'_> {
6172
ShapeData::FillPath { zindex, .. }
6273
| ShapeData::StrokePath { zindex, .. }
6374
| ShapeData::Image { zindex, .. }
75+
| ShapeData::Text { zindex, .. }
6476
| ShapeData::Fill { zindex, .. }
6577
| ShapeData::FillPaint { zindex, .. } => *zindex,
6678
}
@@ -485,6 +497,45 @@ fn convert_shape_rec(
485497
mask,
486498
});
487499
}
500+
Shape::Text {
501+
font,
502+
text,
503+
size,
504+
ops,
505+
transform,
506+
zindex,
507+
opacity,
508+
blend_mode,
509+
quality,
510+
mask,
511+
} => {
512+
let transform = transform.post_concat(parent_transform);
513+
let zindex = overwrite_zindex(*zindex, zindex_overwrite, zindex_shift);
514+
let blend_mode = overwrite_blend_mode(*blend_mode, blend_mode_overwrite);
515+
let paint = PixmapPaint {
516+
opacity: *opacity,
517+
blend_mode,
518+
quality: *quality,
519+
};
520+
521+
let mask = overwrite_mask(mask.clone(), mask_overwrite);
522+
let mask = mask.map(|shape| {
523+
let mut mask_data = Vec::new();
524+
convert_shape(&mut mask_data, shape, parent_transform).unwrap();
525+
mask_data
526+
});
527+
528+
data.push(ShapeData::Text {
529+
font: font.clone(),
530+
text: text.clone(),
531+
size: *size,
532+
ops: ops.clone(),
533+
transform,
534+
paint,
535+
zindex,
536+
mask,
537+
});
538+
}
488539
Shape::Basic(BasicShape::Fill { zindex, color }, _) => {
489540
let zindex = zindex.unwrap_or(0.0);
490541
let color = overwrite_color(color.clone(), color_overwrite, color_shift);
@@ -703,7 +754,7 @@ fn convert_shape(
703754
}
704755

705756
fn render_to_pixmap(shape_data: ShapeData, pixmap: &mut Pixmap, width: u32, height: u32) {
706-
match shape_data {
757+
match shape_data.clone() {
707758
ShapeData::FillPath {
708759
path,
709760
transform,
@@ -747,7 +798,13 @@ fn render_to_pixmap(shape_data: ShapeData, pixmap: &mut Pixmap, width: u32, heig
747798
pixmap.stroke_path(&path, &paint, &stroke, transform, mask.as_ref());
748799
}
749800
ShapeData::Image {
750-
path,
801+
ops,
802+
transform,
803+
paint,
804+
mask,
805+
..
806+
}
807+
| ShapeData::Text {
751808
ops,
752809
transform,
753810
paint,
@@ -756,21 +813,64 @@ fn render_to_pixmap(shape_data: ShapeData, pixmap: &mut Pixmap, width: u32, heig
756813
} => {
757814
#[cfg(feature = "std")]
758815
{
759-
let mut image = match path {
760-
ImagePath::File(path) =>
761-
{
762-
#[cfg(feature = "std")]
763-
ImageReader::open(path).unwrap().decode().unwrap()
764-
}
765-
ImagePath::Shape(shape) => {
766-
let pixmap = render(shape.clone(), width, height).unwrap();
767-
ImageReader::with_format(
768-
Cursor::new(pixmap.encode_png().unwrap()),
769-
ImageFormat::Png,
770-
)
771-
.decode()
772-
.unwrap()
816+
let mut image = match shape_data {
817+
ShapeData::Image { path, .. } => match path {
818+
ImagePath::File(path) =>
819+
{
820+
#[cfg(feature = "std")]
821+
ImageReader::open(path).unwrap().decode().unwrap()
822+
}
823+
ImagePath::Shape(shape) => {
824+
let pixmap = render(shape.clone(), width, height).unwrap();
825+
ImageReader::with_format(
826+
Cursor::new(pixmap.encode_png().unwrap()),
827+
ImageFormat::Png,
828+
)
829+
.decode()
830+
.unwrap()
831+
}
832+
},
833+
ShapeData::Text {
834+
font, text, size, ..
835+
} => {
836+
use std::fs;
837+
838+
let font = fs::read(font).unwrap();
839+
let font = Font::from_bytes(font, FontSettings::default()).unwrap();
840+
841+
let mut bitmaps = Vec::new();
842+
let mut width = 0;
843+
let mut height = 0;
844+
845+
for c in text.chars() {
846+
let (metrics, bitmap) = font.rasterize(c, size);
847+
width += (metrics.width as u32).max(metrics.advance_width as u32);
848+
height = height.max(metrics.height as u32);
849+
bitmaps.push((metrics, bitmap));
850+
}
851+
852+
let mut image = ImageBuffer::new(width, height);
853+
854+
let mut x_offset = 0;
855+
for (metrics, bitmap) in bitmaps {
856+
for (i, &value) in bitmap.iter().enumerate() {
857+
let x = (x_offset as i32
858+
+ metrics.xmin
859+
+ i as i32 % metrics.width as i32)
860+
.max(0) as u32;
861+
let y = height
862+
- (metrics.ymin + i as i32 / metrics.width as i32).max(0)
863+
as u32
864+
- 1;
865+
image.put_pixel(x, y, image::Rgba([value as u8; 4]));
866+
}
867+
868+
x_offset += metrics.advance_width as u32;
869+
}
870+
871+
image.into()
773872
}
873+
_ => unreachable!(),
774874
};
775875

776876
for op in ops {

0 commit comments

Comments
 (0)