utils/
graphics.rs

1use std::io::Cursor;
2
3use anyhow::{Context, Result};
4use image::{DynamicImage, ImageFormat, Rgba};
5use rusttype::{Font, PositionedGlyph, Scale, point};
6
7pub struct FontRenderer {
8    font: Font<'static>,
9}
10
11impl FontRenderer {
12    pub fn new(ttf_bin: Vec<u8>) -> Result<Self> {
13        // ロードエラー時は None が返るので anyhow::Error に変換する
14        let font = Font::try_from_vec(ttf_bin).context("Invalid font data")?;
15
16        Ok(Self { font })
17    }
18
19    pub fn draw_multiline_text(
20        &self,
21        fgcolor: (u8, u8, u8),
22        bgcolor: (u8, u8, u8),
23        text: &str,
24        scale: u32,
25        width: u32,
26    ) -> Vec<u8> {
27        let scale = Scale::uniform(scale as f32);
28        let vmet = self.font.v_metrics(scale);
29        let glyphs_h = (vmet.ascent - vmet.descent).ceil() as u32;
30
31        let mut lines = Vec::new();
32        // 行で分解
33        for orig_line in text.split('\n') {
34            // この行をさらに分解する
35            // 残り: orig_line[start..len]
36            let mut start = 0_usize;
37            // 空行
38            if orig_line.is_empty() {
39                lines.push(Vec::new());
40            }
41            while start < orig_line.len() {
42                // orig_line[start..len] の先頭部分文字列で
43                // 横幅が width を超えない最大のインデックスを求める
44                let rest = &orig_line[start..orig_line.len()];
45                // unicode 文字の開始位置
46                let idxlist: Vec<_> = rest.char_indices().map(|(i, _c)| i).collect();
47
48                let mut le = 0_usize;
49                let mut ri = idxlist.len() + 1;
50                while le < ri {
51                    let m = le + (ri - le) / 2;
52                    let m_idx = *idxlist.get(m).unwrap_or(&rest.len());
53                    let (glyphs_w, _) = self.calc_line_w(
54                        &rest[0..m_idx],
55                        scale,
56                        (glyphs_h * lines.len() as u32) as f32,
57                        vmet.ascent,
58                    );
59                    if glyphs_w <= width {
60                        le = m + 1;
61                    } else {
62                        ri = m;
63                    }
64                }
65                // le = ri = 初めて横幅がwidthを超えるidxlistのインデックス
66                let le_idx = *idxlist.get(le.saturating_sub(1)).unwrap_or(&rest.len());
67                let (_w, glyphs) = self.calc_line_w(
68                    &rest[0..le_idx],
69                    scale,
70                    (glyphs_h * lines.len() as u32) as f32,
71                    vmet.ascent,
72                );
73                lines.push(glyphs);
74
75                start += le_idx;
76            }
77        }
78
79        let height = glyphs_h * lines.len() as u32;
80        let mut image = DynamicImage::new_rgba8(width, height).to_rgba8();
81        for (_x, _y, pixel) in image.enumerate_pixels_mut() {
82            *pixel = Rgba([bgcolor.0, bgcolor.1, bgcolor.2, 255_u8]);
83        }
84
85        for glyphs in lines {
86            for glyph in glyphs {
87                if let Some(bounding_box) = glyph.pixel_bounding_box() {
88                    // Draw the glyph into the image per-pixel by using the draw closure
89                    glyph.draw(|x, y, v| {
90                        let x = x + bounding_box.min.x as u32;
91                        let y = y + bounding_box.min.y as u32;
92                        if x >= width || y >= height {
93                            return;
94                        }
95                        let [r1, g1, b1, _a1] = image.get_pixel(x, y).0;
96                        let [r2, g2, b2] = [fgcolor.0, fgcolor.1, fgcolor.2];
97                        let r3 = (r1 as f32 * (1.0 - v) + r2 as f32 * v) as u8;
98                        let g3 = (g1 as f32 * (1.0 - v) + g2 as f32 * v) as u8;
99                        let b3 = (b1 as f32 * (1.0 - v) + b2 as f32 * v) as u8;
100                        image.put_pixel(
101                            // Offset the position by the glyph bounding box
102                            x,
103                            y,
104                            Rgba([r3, g3, b3, 255]),
105                        )
106                    });
107                }
108            }
109        }
110
111        // Save the image to a png file
112        let mut filebuf = Vec::new();
113        image
114            .write_to(&mut Cursor::new(&mut filebuf), ImageFormat::Png)
115            .unwrap();
116
117        filebuf
118    }
119
120    fn calc_line_w(
121        &self,
122        text: &str,
123        scale: Scale,
124        y: f32,
125        height: f32,
126    ) -> (u32, Vec<PositionedGlyph<'_>>) {
127        if text.is_empty() {
128            return (0, Vec::new());
129        }
130
131        let glyphs: Vec<_> = self
132            .font
133            .layout(text, scale, point(0.0, y + height))
134            .collect();
135
136        let mut min_x = i32::MAX;
137        let mut max_x = i32::MIN;
138        for g in glyphs.iter() {
139            let (le, ri) = match g.pixel_bounding_box() {
140                Some(bb) => (bb.min.x, bb.max.x),
141                None => (g.position().x as i32, g.position().x as i32),
142            };
143            min_x = min_x.min(le);
144            max_x = max_x.max(ri);
145        }
146
147        ((max_x - min_x) as u32, glyphs)
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use std::fs;
155
156    #[test]
157    #[ignore]
158    // sudo apt install fonts-ipafont
159    // cargo test font -- --ignored
160    fn font() -> Result<()> {
161        let fname = "/usr/share/fonts/truetype/fonts-japanese-gothic.ttf";
162        println!("Load font file from: {fname}");
163        let ttf_bin = fs::read(fname)?;
164
165        let r = FontRenderer::new(ttf_bin)?;
166        let text =
167"役割論理(Role logic)は、形式言語の一種であり、論理学やコンピュータ科学の分野で用いられる言語です。役割論理では、対象の役割に着目し、役割に関する命題を表現します。役割論理には、役割論理の基本的な概念である「役割」、「役割変数」、「役割制約」、「役割コンストラクタ」などがあります。
168役割論理では、対象の役割を表現するために、役割変数を用います。役割変数は、役割を表す変数であり、通常はRやSなどのアルファベットで表されます。役割制約は、役割変数が取りうる値に制限を加える制約のことです。たとえば、「人間は動物である」という役割制約は、「人間」の役割変数に対して、「動物」の値を許容することを意味します。
169役割コンストラクタは、複数の役割を組み合わせて新しい役割を構成するための方法です。たとえば、「親子関係」という役割は、「親」と「子」の役割を組み合わせて構成されます。役割コンストラクタには、合成(composition)や逆(inverse)などがあります。
170役割論理は、主に知識表現や推論に用いられます。例えば、役割論理を用いて、複数のエージェントの間で共有される知識を表現することができます。また、役割論理は、オントロジー言語の一種であるOWL(Web Ontology Language)の基盤としても用いられています。";
171
172        let png = r.draw_multiline_text((0xff, 0xff, 0xff), (0x80, 0x00, 0x80), text, 16, 640);
173
174        let fname = "font_test.png";
175        println!("Write image to: {fname}");
176        fs::write(fname, png)?;
177
178        Ok(())
179    }
180}