1use std::sync::Arc;
6
7use image::{DynamicImage, RgbaImage};
8use resvg::tiny_skia::{Pixmap, Transform};
9use resvg::usvg;
10use serde_json::{Map, Value};
11
12use crate::fontmeta;
13
14pub const SIZE: u32 = 600;
16const INK: &str = "#333333";
18const LINE_HEIGHT: f32 = 1.35;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum Specimen {
25 Latin,
26 SimplifiedChinese,
27 TraditionalChinese,
28 Japanese,
29 Korean,
30}
31
32impl Specimen {
33 pub fn for_metas(metas: &Map<String, Value>) -> Specimen {
35 let support = |k: &str| metas.get("support").and_then(|s| s.get(k)).and_then(Value::as_bool) == Some(true);
36 let ps_lang = |k: &str| metas.get("postScriptName").and_then(|p| p.get(k)).is_some();
37 let (cn, tw, jp, kr) = (support("zh_CN"), support("zh_TW"), support("jp"), support("kr"));
38 if let Some(lang) = metas.get("preferLng").and_then(Value::as_str) {
39 return match lang {
40 "zh_CN" => Specimen::SimplifiedChinese,
41 "zh_TW" => Specimen::TraditionalChinese,
42 "jp" => Specimen::Japanese,
43 "kr" => Specimen::Korean,
44 _ => Specimen::Latin,
45 };
46 }
47 if cn && tw {
48 Specimen::SimplifiedChinese
49 } else if ps_lang("ja") {
50 Specimen::Japanese
51 } else if cn && ps_lang("zh") {
52 Specimen::SimplifiedChinese
53 } else if tw && ps_lang("zh_TW") {
54 Specimen::TraditionalChinese
55 } else if cn {
56 Specimen::SimplifiedChinese
57 } else if kr {
58 Specimen::Korean
59 } else if tw {
60 Specimen::TraditionalChinese
61 } else if jp {
62 Specimen::Japanese
63 } else {
64 Specimen::Latin
65 }
66 }
67
68 fn lines(self) -> (Vec<(&'static str, f32)>, f32) {
72 let cjk = |a: &'static str, b: &'static str| (vec![(a, 100.0), (b, 100.0), ("AaBbCcDd", 82.0), ("01234567", 82.0)], 380.0);
73 match self {
74 Specimen::Latin => (vec![("ABCDEFG", 90.0), ("abcdefg", 90.0), ("0123456", 90.0)], 400.0),
75 Specimen::SimplifiedChinese => cjk("字体样张", "汉字之美"),
76 Specimen::TraditionalChinese => cjk("字體樣張", "漢字之美"),
77 Specimen::Japanese => cjk("あいうえ", "日本語文"),
78 Specimen::Korean => (vec![("가나다라마", 90.0), ("AaBbCcDd", 82.0), ("01234567", 82.0)], 380.0),
79 }
80 }
81}
82
83fn escape(s: &str) -> String {
84 s.replace('&', "&").replace('<', "<").replace('"', """)
85}
86
87pub fn render(data: &[u8], metas: &Map<String, Value>) -> Option<DynamicImage> {
90 let sfnt = fontmeta::sfnt(data)?.into_owned();
91 let face = fontmeta::parse(&sfnt)?;
92 let t = face.tables();
95 if t.glyf.is_none() && t.cff.is_none() && t.cff2.is_none() {
96 return None;
97 }
98 let upm = f32::from(face.units_per_em());
99 let (asc, desc) = (f32::from(face.ascender()) / upm, -f32::from(face.descender()) / upm);
100 let mut db = usvg::fontdb::Database::new();
101 db.load_font_data(sfnt.clone());
102 let family = db.faces().next()?.families.first()?.0.clone();
103
104 let (lines, block_width) = Specimen::for_metas(metas).lines();
105 let block_height: f32 = lines.iter().map(|(_, s)| s * LINE_HEIGHT).sum();
106 let left = (SIZE as f32 - block_width) / 2.0;
107 let mut y = (SIZE as f32 - block_height) / 2.0;
108 let mut spans = String::new();
109 for (text, size) in &lines {
110 let line = size * LINE_HEIGHT;
111 let baseline = y + (line - (asc + desc) * size) / 2.0 + asc * size;
113 spans.push_str(&format!(r#"<text x="{left}" y="{baseline}" font-size="{size}">{}</text>"#, escape(text)));
114 y += line;
115 }
116 let svg = format!(
117 r##"<svg xmlns="http://www.w3.org/2000/svg" width="{SIZE}" height="{SIZE}"><rect width="100%" height="100%" fill="#ffffff"/><g font-family=""{}"" fill="{INK}">{spans}</g></svg>"##,
118 escape(&family)
119 );
120 let opts = usvg::Options { fontdb: Arc::new(db), ..Default::default() };
121 let tree = usvg::Tree::from_str(&svg, &opts).ok()?;
122 let mut pixmap = Pixmap::new(SIZE, SIZE)?;
123 resvg::render(&tree, Transform::identity(), &mut pixmap.as_mut());
124 let raw: Vec<u8> = pixmap.pixels().iter().flat_map(|p| [p.red(), p.green(), p.blue(), 255]).collect();
125 RgbaImage::from_raw(SIZE, SIZE, raw).map(|i| DynamicImage::ImageRgb8(DynamicImage::ImageRgba8(i).to_rgb8()))
126}
127
128impl crate::Library {
129 pub fn set_font_language(&mut self, id: &str, lang: &str) -> crate::Result<()> {
134 if !fontmeta::SAMPLE_LANGUAGES.iter().any(|(code, _)| *code == lang) {
135 return Err(self.invalid(format!("unknown sample language {lang}")));
136 }
137 let mut item = self.item(id).cloned().ok_or_else(|| self.invalid(format!("no item {id}")))?;
138 let Some(Value::Object(metas)) = item.extra.get_mut("fontMetas") else {
139 return Err(self.invalid(format!("{}.{} has no font metadata", item.name, item.ext)));
140 };
141 metas.insert("preferLng".into(), Value::String(lang.into()));
142 let metas = metas.clone();
143 let raw = item.original_path(self.root());
144 let data = std::fs::read(&raw).map_err(|e| crate::Error::io(&raw, e))?;
145 if let Some(img) = render(&data, &metas) {
146 let thumb = self.info_dir(&item).join(item.thumbnail_name());
147 item.palettes = crate::palette::extract(&img);
148 crate::thumbnail::write_thumbnail(img, crate::thumbnail::max_side(&item.ext), &thumb).map_err(|e| self.invalid(e.0))?;
149 }
150 self.update_item(item)
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use serde_json::json;
158
159 #[test]
160 fn picks_specimens() {
161 let m = |v: Value| v.as_object().unwrap().clone();
162 assert_eq!(Specimen::for_metas(&m(json!({}))), Specimen::Latin);
163 assert_eq!(Specimen::for_metas(&m(json!({"support": {"zh_CN": true, "zh_TW": true}}))), Specimen::SimplifiedChinese);
164 assert_eq!(Specimen::for_metas(&m(json!({"support": {"zh_TW": true}}))), Specimen::TraditionalChinese);
165 assert_eq!(Specimen::for_metas(&m(json!({"postScriptName": {"ja": "x"}}))), Specimen::Japanese);
166 assert_eq!(Specimen::for_metas(&m(json!({"support": {"kr": true, "jp": true}}))), Specimen::Korean);
167 }
168
169 #[test]
170 fn changing_the_language_redraws_the_specimen() {
171 let Ok(data) = std::fs::read("/System/Library/Fonts/Supplemental/Arial.ttf") else { return };
172 let tmp = tempfile::tempdir().unwrap();
173 let mut lib = crate::Library::create(tmp.path().join("T.library")).unwrap();
174 let src = tmp.path().join("Arial.ttf");
175 std::fs::write(&src, &data).unwrap();
176 let id = lib.import_file(&src, &crate::import::ImportOptions::default()).unwrap();
177 let thumb = lib.item(&id).unwrap().thumbnail_path(lib.root()).unwrap();
178 let before = std::fs::read(&thumb).unwrap();
179 assert!(lib.set_font_language(&id, "xx").is_err());
180 lib.set_font_language(&id, "zh_CN").unwrap();
181 let item = lib.item(&id).unwrap();
182 assert_eq!(item.extra["fontMetas"]["preferLng"], "zh_CN");
183 assert_ne!(std::fs::read(&thumb).unwrap(), before, "the specimen is drawn again");
184 lib.set_font_language(&id, "en").unwrap();
185 assert_eq!(lib.item(&id).unwrap().extra["fontMetas"]["preferLng"], "en");
186 }
187
188 #[test]
189 fn renders_a_system_font() {
190 let Ok(data) = std::fs::read("/System/Library/Fonts/Supplemental/Arial.ttf") else { return };
191 let metas = fontmeta::font_metas(&data, "X").unwrap();
192 let img = render(&data, &metas).unwrap().to_rgb8();
193 assert_eq!(img.dimensions(), (SIZE, SIZE));
194 let dark = img.pixels().filter(|p| p.0[0] < 128).count();
195 assert!(dark > 5_000, "text was drawn: {dark} dark pixels");
196 assert_eq!(img.get_pixel(5, 5).0, [255, 255, 255]);
198 assert_eq!(img.get_pixel(594, 594).0, [255, 255, 255]);
199 let dir = tempfile::tempdir().unwrap();
200 let out = dir.path().join("specimen_thumbnail.png");
201 crate::thumbnail::write_thumbnail(DynamicImage::ImageRgb8(img), crate::thumbnail::DEFAULT_MAX_SIDE, &out).unwrap();
202 assert_eq!(crate::thumbnail::decode(&out).unwrap().width(), 320);
203 }
204}