1 | // font-kit/src/sources/fontconfig.rs |
2 | // |
3 | // Copyright © 2018 The Pathfinder Project Developers. |
4 | // |
5 | // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or |
6 | // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license |
7 | // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your |
8 | // option. This file may not be copied, modified, or distributed |
9 | // except according to those terms. |
10 | |
11 | //! A source that contains the fonts installed on the system, as reported by the Fontconfig |
12 | //! library. |
13 | //! |
14 | //! On macOS and Windows, the Cargo feature `source-fontconfig` can be used to opt into fontconfig |
15 | //! support. To prefer it over the native font source (only if you know what you're doing), use the |
16 | //! `source-fontconfig-default` feature. |
17 | |
18 | use crate::error::SelectionError; |
19 | use crate::family_handle::FamilyHandle; |
20 | use crate::family_name::FamilyName; |
21 | use crate::handle::Handle; |
22 | use crate::properties::Properties; |
23 | use crate::source::Source; |
24 | use std::any::Any; |
25 | |
26 | /// A source that contains the fonts installed on the system, as reported by the Fontconfig |
27 | /// library. |
28 | /// |
29 | /// On macOS and Windows, the Cargo feature `source-fontconfig` can be used to opt into fontconfig |
30 | /// support. To prefer it over the native font source (only if you know what you're doing), use the |
31 | /// `source-fontconfig-default` feature. |
32 | #[allow (missing_debug_implementations)] |
33 | pub struct FontconfigSource { |
34 | config: fc::Config, |
35 | } |
36 | |
37 | impl FontconfigSource { |
38 | /// Initializes Fontconfig and prepares it for queries. |
39 | pub fn new() -> FontconfigSource { |
40 | FontconfigSource { |
41 | config: fc::Config::new(), |
42 | } |
43 | } |
44 | |
45 | /// Returns paths of all fonts installed on the system. |
46 | pub fn all_fonts(&self) -> Result<Vec<Handle>, SelectionError> { |
47 | let pattern = fc::Pattern::new(); |
48 | |
49 | // We want the family name. |
50 | let mut object_set = fc::ObjectSet::new(); |
51 | object_set.push_string(fc::Object::File); |
52 | object_set.push_string(fc::Object::Index); |
53 | |
54 | let patterns = pattern |
55 | .list(&self.config, object_set) |
56 | .map_err(|_| SelectionError::NotFound)?; |
57 | |
58 | let mut handles = vec![]; |
59 | for patt in patterns { |
60 | let path = match patt.get_string(fc::Object::File) { |
61 | Some(v) => v, |
62 | None => continue, |
63 | }; |
64 | |
65 | let index = match patt.get_integer(fc::Object::Index) { |
66 | Some(v) => v, |
67 | None => continue, |
68 | }; |
69 | |
70 | handles.push(Handle::Path { |
71 | path: path.into(), |
72 | font_index: index as u32, |
73 | }); |
74 | } |
75 | |
76 | if !handles.is_empty() { |
77 | Ok(handles) |
78 | } else { |
79 | Err(SelectionError::NotFound) |
80 | } |
81 | } |
82 | |
83 | /// Returns the names of all families installed on the system. |
84 | pub fn all_families(&self) -> Result<Vec<String>, SelectionError> { |
85 | let pattern = fc::Pattern::new(); |
86 | |
87 | // We want the family name. |
88 | let mut object_set = fc::ObjectSet::new(); |
89 | object_set.push_string(fc::Object::Family); |
90 | |
91 | let patterns = pattern |
92 | .list(&self.config, object_set) |
93 | .map_err(|_| SelectionError::NotFound)?; |
94 | |
95 | let mut result_families = vec![]; |
96 | for patt in patterns { |
97 | if let Some(family) = patt.get_string(fc::Object::Family) { |
98 | result_families.push(family); |
99 | } |
100 | } |
101 | |
102 | result_families.sort(); |
103 | result_families.dedup(); |
104 | |
105 | if !result_families.is_empty() { |
106 | Ok(result_families) |
107 | } else { |
108 | Err(SelectionError::NotFound) |
109 | } |
110 | } |
111 | |
112 | /// Looks up a font family by name and returns the handles of all the fonts in that family. |
113 | pub fn select_family_by_name(&self, family_name: &str) -> Result<FamilyHandle, SelectionError> { |
114 | use std::borrow::Cow; |
115 | |
116 | let family_name = match family_name { |
117 | "serif" | "sans-serif" | "monospace" | "cursive" | "fantasy" => { |
118 | Cow::from(self.select_generic_font(family_name)?) |
119 | } |
120 | _ => Cow::from(family_name), |
121 | }; |
122 | |
123 | let pattern = fc::Pattern::from_name(family_name.as_ref()); |
124 | |
125 | let mut object_set = fc::ObjectSet::new(); |
126 | object_set.push_string(fc::Object::File); |
127 | object_set.push_string(fc::Object::Index); |
128 | |
129 | let patterns = pattern |
130 | .list(&self.config, object_set) |
131 | .map_err(|_| SelectionError::NotFound)?; |
132 | |
133 | let mut handles = vec![]; |
134 | for patt in patterns { |
135 | let font_path = patt.get_string(fc::Object::File).unwrap(); |
136 | let font_index = patt.get_integer(fc::Object::Index).unwrap() as u32; |
137 | let handle = Handle::from_path(std::path::PathBuf::from(font_path), font_index); |
138 | handles.push(handle); |
139 | } |
140 | |
141 | if !handles.is_empty() { |
142 | Ok(FamilyHandle::from_font_handles(handles.into_iter())) |
143 | } else { |
144 | Err(SelectionError::NotFound) |
145 | } |
146 | } |
147 | |
148 | /// Selects a font by a generic name. |
149 | /// |
150 | /// Accepts: serif, sans-serif, monospace, cursive and fantasy. |
151 | fn select_generic_font(&self, name: &str) -> Result<String, SelectionError> { |
152 | let mut pattern = fc::Pattern::from_name(name); |
153 | pattern.config_substitute(fc::MatchKind::Pattern); |
154 | pattern.default_substitute(); |
155 | |
156 | let patterns = pattern |
157 | .sorted(&self.config) |
158 | .map_err(|_| SelectionError::NotFound)?; |
159 | |
160 | if let Some(patt) = patterns.into_iter().next() { |
161 | if let Some(family) = patt.get_string(fc::Object::Family) { |
162 | return Ok(family); |
163 | } |
164 | } |
165 | |
166 | Err(SelectionError::NotFound) |
167 | } |
168 | |
169 | /// Selects a font by PostScript name, which should be a unique identifier. |
170 | /// |
171 | /// The default implementation, which is used by the DirectWrite and the filesystem backends, |
172 | /// does a brute-force search of installed fonts to find the one that matches. |
173 | pub fn select_by_postscript_name( |
174 | &self, |
175 | postscript_name: &str, |
176 | ) -> Result<Handle, SelectionError> { |
177 | let mut pattern = fc::Pattern::new(); |
178 | pattern.push_string(fc::Object::PostScriptName, postscript_name.to_owned()); |
179 | |
180 | // We want the file path and the font index. |
181 | let mut object_set = fc::ObjectSet::new(); |
182 | object_set.push_string(fc::Object::File); |
183 | object_set.push_string(fc::Object::Index); |
184 | |
185 | let patterns = pattern |
186 | .list(&self.config, object_set) |
187 | .map_err(|_| SelectionError::NotFound)?; |
188 | |
189 | if let Some(patt) = patterns.into_iter().next() { |
190 | let font_path = patt.get_string(fc::Object::File).unwrap(); |
191 | let font_index = patt.get_integer(fc::Object::Index).unwrap() as u32; |
192 | let handle = Handle::from_path(std::path::PathBuf::from(font_path), font_index); |
193 | Ok(handle) |
194 | } else { |
195 | Err(SelectionError::NotFound) |
196 | } |
197 | } |
198 | |
199 | /// Performs font matching according to the CSS Fonts Level 3 specification and returns the |
200 | /// handle. |
201 | #[inline ] |
202 | pub fn select_best_match( |
203 | &self, |
204 | family_names: &[FamilyName], |
205 | properties: &Properties, |
206 | ) -> Result<Handle, SelectionError> { |
207 | <Self as Source>::select_best_match(self, family_names, properties) |
208 | } |
209 | } |
210 | |
211 | impl Source for FontconfigSource { |
212 | #[inline ] |
213 | fn all_fonts(&self) -> Result<Vec<Handle>, SelectionError> { |
214 | self.all_fonts() |
215 | } |
216 | |
217 | #[inline ] |
218 | fn all_families(&self) -> Result<Vec<String>, SelectionError> { |
219 | self.all_families() |
220 | } |
221 | |
222 | #[inline ] |
223 | fn select_family_by_name(&self, family_name: &str) -> Result<FamilyHandle, SelectionError> { |
224 | self.select_family_by_name(family_name) |
225 | } |
226 | |
227 | #[inline ] |
228 | fn select_by_postscript_name(&self, postscript_name: &str) -> Result<Handle, SelectionError> { |
229 | self.select_by_postscript_name(postscript_name) |
230 | } |
231 | |
232 | #[inline ] |
233 | fn as_any(&self) -> &dyn Any { |
234 | self |
235 | } |
236 | |
237 | #[inline ] |
238 | fn as_mut_any(&mut self) -> &mut dyn Any { |
239 | self |
240 | } |
241 | } |
242 | |
243 | // A minimal fontconfig wrapper. |
244 | mod fc { |
245 | #![allow (dead_code)] |
246 | |
247 | use fontconfig_sys as ffi; |
248 | use fontconfig_sys::ffi_dispatch; |
249 | |
250 | #[cfg (feature = "source-fontconfig-dlopen" )] |
251 | use ffi::statics::LIB; |
252 | #[cfg (not(feature = "source-fontconfig-dlopen" ))] |
253 | use ffi::*; |
254 | |
255 | use std::ffi::{CStr, CString}; |
256 | use std::os::raw::{c_char, c_uchar}; |
257 | use std::ptr; |
258 | |
259 | #[derive (Clone, Copy)] |
260 | pub enum Error { |
261 | NoMatch, |
262 | TypeMismatch, |
263 | NoId, |
264 | OutOfMemory, |
265 | } |
266 | |
267 | #[derive (Clone, Copy)] |
268 | pub enum MatchKind { |
269 | Pattern, |
270 | Font, |
271 | Scan, |
272 | } |
273 | |
274 | impl MatchKind { |
275 | fn to_u32(&self) -> u32 { |
276 | match self { |
277 | MatchKind::Pattern => ffi::FcMatchPattern, |
278 | MatchKind::Font => ffi::FcMatchFont, |
279 | MatchKind::Scan => ffi::FcMatchScan, |
280 | } |
281 | } |
282 | } |
283 | |
284 | // https://www.freedesktop.org/software/fontconfig/fontconfig-devel/x19.html |
285 | #[derive (Clone, Copy)] |
286 | pub enum Object { |
287 | Family, |
288 | File, |
289 | Index, |
290 | PostScriptName, |
291 | } |
292 | |
293 | impl Object { |
294 | fn as_bytes(&self) -> &[u8] { |
295 | match self { |
296 | Object::Family => b"family \0" , |
297 | Object::File => b"file \0" , |
298 | Object::Index => b"index \0" , |
299 | Object::PostScriptName => b"postscriptname \0" , |
300 | } |
301 | } |
302 | |
303 | fn as_ptr(&self) -> *const libc::c_char { |
304 | self.as_bytes().as_ptr() as *const libc::c_char |
305 | } |
306 | } |
307 | |
308 | pub struct Config { |
309 | d: *mut ffi::FcConfig, |
310 | } |
311 | |
312 | impl Config { |
313 | // FcInitLoadConfigAndFonts |
314 | pub fn new() -> Self { |
315 | unsafe { |
316 | Config { |
317 | d: ffi_dispatch!( |
318 | feature = "source-fontconfig-dlopen" , |
319 | LIB, |
320 | FcInitLoadConfigAndFonts, |
321 | ), |
322 | } |
323 | } |
324 | } |
325 | } |
326 | |
327 | impl Drop for Config { |
328 | fn drop(&mut self) { |
329 | unsafe { |
330 | ffi_dispatch!( |
331 | feature = "source-fontconfig-dlopen" , |
332 | LIB, |
333 | FcConfigDestroy, |
334 | self.d |
335 | ); |
336 | } |
337 | } |
338 | } |
339 | |
340 | pub struct Pattern { |
341 | d: *mut ffi::FcPattern, |
342 | c_strings: Vec<CString>, |
343 | } |
344 | |
345 | impl Pattern { |
346 | fn from_ptr(d: *mut ffi::FcPattern) -> Self { |
347 | Pattern { |
348 | d, |
349 | c_strings: vec![], |
350 | } |
351 | } |
352 | |
353 | // FcPatternCreate |
354 | pub fn new() -> Self { |
355 | unsafe { |
356 | Pattern::from_ptr(ffi_dispatch!( |
357 | feature = "source-fontconfig-dlopen" , |
358 | LIB, |
359 | FcPatternCreate, |
360 | )) |
361 | } |
362 | } |
363 | |
364 | // FcNameParse |
365 | pub fn from_name(name: &str) -> Self { |
366 | let c_name = CString::new(name).unwrap(); |
367 | unsafe { |
368 | Pattern::from_ptr(ffi_dispatch!( |
369 | feature = "source-fontconfig-dlopen" , |
370 | LIB, |
371 | FcNameParse, |
372 | c_name.as_ptr() as *mut c_uchar |
373 | )) |
374 | } |
375 | } |
376 | |
377 | // FcPatternAddString |
378 | pub fn push_string(&mut self, object: Object, value: String) { |
379 | unsafe { |
380 | let c_string = CString::new(value).unwrap(); |
381 | ffi_dispatch!( |
382 | feature = "source-fontconfig-dlopen" , |
383 | LIB, |
384 | FcPatternAddString, |
385 | self.d, |
386 | object.as_ptr(), |
387 | c_string.as_ptr() as *const c_uchar |
388 | ); |
389 | |
390 | // We have to keep this string, because `FcPattern` has a pointer to it now. |
391 | self.c_strings.push(c_string) |
392 | } |
393 | } |
394 | |
395 | // FcConfigSubstitute |
396 | pub fn config_substitute(&mut self, match_kind: MatchKind) { |
397 | unsafe { |
398 | ffi_dispatch!( |
399 | feature = "source-fontconfig-dlopen" , |
400 | LIB, |
401 | FcConfigSubstitute, |
402 | ptr::null_mut(), |
403 | self.d, |
404 | match_kind.to_u32() |
405 | ); |
406 | } |
407 | } |
408 | |
409 | // FcDefaultSubstitute |
410 | pub fn default_substitute(&mut self) { |
411 | unsafe { |
412 | ffi_dispatch!( |
413 | feature = "source-fontconfig-dlopen" , |
414 | LIB, |
415 | FcDefaultSubstitute, |
416 | self.d |
417 | ); |
418 | } |
419 | } |
420 | |
421 | // FcFontSort |
422 | pub fn sorted(&self, config: &Config) -> Result<FontSet, Error> { |
423 | let mut res = ffi::FcResultMatch; |
424 | let d = unsafe { |
425 | ffi_dispatch!( |
426 | feature = "source-fontconfig-dlopen" , |
427 | LIB, |
428 | FcFontSort, |
429 | config.d, |
430 | self.d, |
431 | 1, |
432 | ptr::null_mut(), |
433 | &mut res |
434 | ) |
435 | }; |
436 | |
437 | match res { |
438 | ffi::FcResultMatch => Ok(FontSet { d, idx: 0 }), |
439 | ffi::FcResultTypeMismatch => Err(Error::TypeMismatch), |
440 | ffi::FcResultNoId => Err(Error::NoId), |
441 | ffi::FcResultOutOfMemory => Err(Error::OutOfMemory), |
442 | _ => Err(Error::NoMatch), |
443 | } |
444 | } |
445 | |
446 | // FcFontList |
447 | pub fn list(&self, config: &Config, set: ObjectSet) -> Result<FontSet, Error> { |
448 | let d = unsafe { |
449 | ffi_dispatch!( |
450 | feature = "source-fontconfig-dlopen" , |
451 | LIB, |
452 | FcFontList, |
453 | config.d, |
454 | self.d, |
455 | set.d |
456 | ) |
457 | }; |
458 | if !d.is_null() { |
459 | Ok(FontSet { d, idx: 0 }) |
460 | } else { |
461 | Err(Error::NoMatch) |
462 | } |
463 | } |
464 | } |
465 | |
466 | impl Drop for Pattern { |
467 | #[inline ] |
468 | fn drop(&mut self) { |
469 | unsafe { |
470 | ffi_dispatch!( |
471 | feature = "source-fontconfig-dlopen" , |
472 | LIB, |
473 | FcPatternDestroy, |
474 | self.d |
475 | ) |
476 | } |
477 | } |
478 | } |
479 | |
480 | // A read-only `FcPattern` without a destructor. |
481 | pub struct PatternRef { |
482 | d: *mut ffi::FcPattern, |
483 | } |
484 | |
485 | impl PatternRef { |
486 | // FcPatternGetString |
487 | pub fn get_string(&self, object: Object) -> Option<String> { |
488 | unsafe { |
489 | let mut string = ptr::null_mut(); |
490 | let res = ffi_dispatch!( |
491 | feature = "source-fontconfig-dlopen" , |
492 | LIB, |
493 | FcPatternGetString, |
494 | self.d, |
495 | object.as_ptr(), |
496 | 0, |
497 | &mut string |
498 | ); |
499 | if res != ffi::FcResultMatch { |
500 | return None; |
501 | } |
502 | |
503 | if string.is_null() { |
504 | return None; |
505 | } |
506 | |
507 | CStr::from_ptr(string as *const c_char) |
508 | .to_str() |
509 | .ok() |
510 | .map(|string| string.to_owned()) |
511 | } |
512 | } |
513 | |
514 | // FcPatternGetInteger |
515 | pub fn get_integer(&self, object: Object) -> Option<i32> { |
516 | unsafe { |
517 | let mut integer = 0; |
518 | let res = ffi_dispatch!( |
519 | feature = "source-fontconfig-dlopen" , |
520 | LIB, |
521 | FcPatternGetInteger, |
522 | self.d, |
523 | object.as_ptr(), |
524 | 0, |
525 | &mut integer |
526 | ); |
527 | if res != ffi::FcResultMatch { |
528 | return None; |
529 | } |
530 | |
531 | Some(integer) |
532 | } |
533 | } |
534 | } |
535 | |
536 | pub struct FontSet { |
537 | d: *mut ffi::FcFontSet, |
538 | idx: usize, |
539 | } |
540 | |
541 | impl FontSet { |
542 | pub fn is_empty(&self) -> bool { |
543 | self.len() == 0 |
544 | } |
545 | |
546 | pub fn len(&self) -> usize { |
547 | unsafe { (*self.d).nfont as usize } |
548 | } |
549 | } |
550 | |
551 | impl Iterator for FontSet { |
552 | type Item = PatternRef; |
553 | |
554 | fn next(&mut self) -> Option<Self::Item> { |
555 | if self.idx == self.len() { |
556 | return None; |
557 | } |
558 | |
559 | let idx = self.idx; |
560 | self.idx += 1; |
561 | |
562 | let d = unsafe { *(*self.d).fonts.offset(idx as isize) }; |
563 | Some(PatternRef { d }) |
564 | } |
565 | |
566 | fn size_hint(&self) -> (usize, Option<usize>) { |
567 | (0, Some(self.len())) |
568 | } |
569 | } |
570 | |
571 | impl Drop for FontSet { |
572 | fn drop(&mut self) { |
573 | unsafe { |
574 | ffi_dispatch!( |
575 | feature = "source-fontconfig-dlopen" , |
576 | LIB, |
577 | FcFontSetDestroy, |
578 | self.d |
579 | ) |
580 | } |
581 | } |
582 | } |
583 | |
584 | pub struct ObjectSet { |
585 | d: *mut ffi::FcObjectSet, |
586 | } |
587 | |
588 | impl ObjectSet { |
589 | // FcObjectSetCreate |
590 | pub fn new() -> Self { |
591 | unsafe { |
592 | ObjectSet { |
593 | d: ffi_dispatch!(feature = "source-fontconfig-dlopen" , LIB, FcObjectSetCreate,), |
594 | } |
595 | } |
596 | } |
597 | |
598 | // FcObjectSetAdd |
599 | pub fn push_string(&mut self, object: Object) { |
600 | unsafe { |
601 | // Returns `false` if the property name cannot be inserted |
602 | // into the set (due to allocation failure). |
603 | assert_eq!( |
604 | ffi_dispatch!( |
605 | feature = "source-fontconfig-dlopen" , |
606 | LIB, |
607 | FcObjectSetAdd, |
608 | self.d, |
609 | object.as_ptr() |
610 | ), |
611 | 1 |
612 | ); |
613 | } |
614 | } |
615 | } |
616 | |
617 | impl Drop for ObjectSet { |
618 | fn drop(&mut self) { |
619 | unsafe { |
620 | ffi_dispatch!( |
621 | feature = "source-fontconfig-dlopen" , |
622 | LIB, |
623 | FcObjectSetDestroy, |
624 | self.d |
625 | ) |
626 | } |
627 | } |
628 | } |
629 | } |
630 | |