|
34 | 34 | use crate::d3d::Gpu; |
35 | 35 | use anyhow::{anyhow, bail, Result}; |
36 | 36 | use std::ffi::c_void; |
| 37 | +use std::path::PathBuf; |
37 | 38 |
|
38 | 39 | /// Spécification d'un texte à rastériser. Mêmes champs que `text_windows::TextSpec` |
39 | 40 | /// — le moteur macOS les consomme via `cache_key` pour déterminer si une re-rastérisation |
@@ -141,6 +142,8 @@ const K_CT_PARAGRAPH_STYLE_SPECIFIER_ALIGNMENT: u32 = 0; |
141 | 142 | /// `CTFontSymbolicTraits` : italique / gras. |
142 | 143 | const K_CT_FONT_TRAIT_ITALIC: u32 = 1 << 0; |
143 | 144 | const K_CT_FONT_TRAIT_BOLD: u32 = 1 << 1; |
| 145 | +/// `kCTFontManagerScopeProcess` : la police n'existe que pour ce processus, rien n'est installé. |
| 146 | +const K_CT_FONT_MANAGER_SCOPE_PROCESS: u32 = 1; |
144 | 147 |
|
145 | 148 | #[repr(C)] |
146 | 149 | #[derive(Clone, Copy)] |
@@ -174,6 +177,12 @@ extern "C" { |
174 | 177 | str_: CFTypeRef, |
175 | 178 | attributes: CFTypeRef, |
176 | 179 | ) -> CFTypeRef; |
| 180 | + fn CFURLCreateFromFileSystemRepresentation( |
| 181 | + alloc: CFTypeRef, |
| 182 | + buffer: *const u8, |
| 183 | + buf_len: CFIndex, |
| 184 | + is_directory: u8, |
| 185 | + ) -> CFTypeRef; |
177 | 186 | static kCFTypeDictionaryKeyCallBacks: c_void; |
178 | 187 | static kCFTypeDictionaryValueCallBacks: c_void; |
179 | 188 | } |
@@ -231,6 +240,11 @@ extern "C" { |
231 | 240 | frame_attributes: CFTypeRef, |
232 | 241 | ) -> CFTypeRef; |
233 | 242 | fn CTFrameDraw(frame: CFTypeRef, context: CFTypeRef); |
| 243 | + fn CTFontManagerRegisterFontsForURL( |
| 244 | + font_url: CFTypeRef, |
| 245 | + scope: u32, |
| 246 | + error: *mut CFTypeRef, |
| 247 | + ) -> bool; |
234 | 248 |
|
235 | 249 | static kCTFontAttributeName: CFTypeRef; |
236 | 250 | static kCTForegroundColorAttributeName: CFTypeRef; |
@@ -406,13 +420,46 @@ unsafe fn cf_string(s: &str) -> Option<CFOwned> { |
406 | 420 | )) |
407 | 421 | } |
408 | 422 |
|
| 423 | +/// Rend `files` (`crate::text_fonts`) visibles de CoreText pour CE processus seulement : |
| 424 | +/// `CTFontCreateWithName` les trouve ensuite par leur nom de famille, sans que rien ne soit |
| 425 | +/// installé sur la machine. |
| 426 | +/// |
| 427 | +/// Un fichier refusé ne coûte que sa police : le texte retombe sur celles du système. Un |
| 428 | +/// fichier déjà enregistré est refusé aussi, et reste utilisable. |
| 429 | +unsafe fn register_fonts(files: &[PathBuf]) { |
| 430 | + use std::os::unix::ffi::OsStrExt; |
| 431 | + for path in files { |
| 432 | + let bytes = path.as_os_str().as_bytes(); |
| 433 | + let Some(url) = CFOwned::new(CFURLCreateFromFileSystemRepresentation( |
| 434 | + std::ptr::null(), |
| 435 | + bytes.as_ptr(), |
| 436 | + bytes.len() as CFIndex, |
| 437 | + 0, |
| 438 | + )) else { |
| 439 | + continue; |
| 440 | + }; |
| 441 | + let registered = CTFontManagerRegisterFontsForURL( |
| 442 | + url.get(), |
| 443 | + K_CT_FONT_MANAGER_SCOPE_PROCESS, |
| 444 | + std::ptr::null_mut(), |
| 445 | + ); |
| 446 | + if !registered { |
| 447 | + eprintln!("[text] police non enregistrée (ou déjà) : {}", path.display()); |
| 448 | + } |
| 449 | + } |
| 450 | +} |
| 451 | + |
409 | 452 | /// Rastériseur de texte macOS. Pas d'état persistant : CoreText et CoreGraphics sont |
410 | 453 | /// prêts dès le link des frameworks (côté Windows, `TextRasterizer::new` alloue les |
411 | 454 | /// factories DirectWrite/Direct2D — d'où le `Result` conservé pour la symétrie). |
412 | 455 | pub struct TextRasterizer; |
413 | 456 |
|
414 | 457 | impl TextRasterizer { |
415 | 458 | pub fn new() -> Result<TextRasterizer> { |
| 459 | + // Une fois par processus : l'enregistrement vaut pour tout CoreText, alors que chaque |
| 460 | + // compositeur (preview, export) crée son rastériseur. |
| 461 | + static REGISTER: std::sync::Once = std::sync::Once::new(); |
| 462 | + REGISTER.call_once(|| unsafe { register_fonts(&crate::text_fonts::embedded_font_files()) }); |
416 | 463 | Ok(TextRasterizer) |
417 | 464 | } |
418 | 465 |
|
@@ -739,6 +786,80 @@ mod tests { |
739 | 786 | (x0, y0, x1, y1) |
740 | 787 | } |
741 | 788 |
|
| 789 | + #[link(name = "CoreText", kind = "framework")] |
| 790 | + extern "C" { |
| 791 | + fn CTFontCopyFamilyName(font: CFTypeRef) -> CFTypeRef; |
| 792 | + fn CTFontGetSymbolicTraits(font: CFTypeRef) -> u32; |
| 793 | + } |
| 794 | + |
| 795 | + #[link(name = "CoreFoundation", kind = "framework")] |
| 796 | + extern "C" { |
| 797 | + fn CFStringGetCString( |
| 798 | + string: CFTypeRef, |
| 799 | + buffer: *mut std::ffi::c_char, |
| 800 | + buffer_size: CFIndex, |
| 801 | + encoding: u32, |
| 802 | + ) -> u8; |
| 803 | + } |
| 804 | + |
| 805 | + /// Le nom de famille que CoreText a réellement résolu pour `font`. |
| 806 | + unsafe fn family_of(font: CFTypeRef) -> String { |
| 807 | + let name = CFOwned::new(CTFontCopyFamilyName(font)).expect("CTFontCopyFamilyName"); |
| 808 | + let mut buf = [0 as std::ffi::c_char; 256]; |
| 809 | + let ok = CFStringGetCString( |
| 810 | + name.get(), |
| 811 | + buf.as_mut_ptr(), |
| 812 | + buf.len() as CFIndex, |
| 813 | + K_CF_STRING_ENCODING_UTF8, |
| 814 | + ); |
| 815 | + assert_ne!(ok, 0, "CFStringGetCString"); |
| 816 | + std::ffi::CStr::from_ptr(buf.as_ptr()).to_string_lossy().into_owned() |
| 817 | + } |
| 818 | + |
| 819 | + /// Chaque famille embarquée est celle que CoreText dessine. `CTFontCreateWithName` ne |
| 820 | + /// renvoie jamais NULL pour un nom inconnu : il retombe en silence sur une autre police, |
| 821 | + /// donc seul le nom de famille RÉSOLU dit si la bonne a été prise. |
| 822 | + #[test] |
| 823 | + fn every_shipped_family_draws_from_its_own_files() { |
| 824 | + unsafe { |
| 825 | + register_fonts(&crate::text_fonts::font_files_in(&crate::text_fonts::repo_fonts_dir())); |
| 826 | + for family in crate::text_fonts::SHIPPED_FAMILIES { |
| 827 | + let name = cf_string(family).unwrap(); |
| 828 | + let font = CFOwned::new(CTFontCreateWithName(name.get(), 40.0, std::ptr::null())) |
| 829 | + .unwrap(); |
| 830 | + assert_eq!(family_of(font.get()), family); |
| 831 | + let traits = CTFontGetSymbolicTraits(font.get()); |
| 832 | + assert_eq!(traits & K_CT_FONT_TRAIT_BOLD, 0, "{family} : le régulier sort gras"); |
| 833 | + // Le réglage Gras prend un vrai fichier gras de la MÊME famille. |
| 834 | + let bold = CFOwned::new(CTFontCreateCopyWithSymbolicTraits( |
| 835 | + font.get(), |
| 836 | + 0.0, |
| 837 | + std::ptr::null(), |
| 838 | + K_CT_FONT_TRAIT_BOLD, |
| 839 | + K_CT_FONT_TRAIT_BOLD | K_CT_FONT_TRAIT_ITALIC, |
| 840 | + )) |
| 841 | + .unwrap_or_else(|| panic!("{family} : pas de gras")); |
| 842 | + assert_eq!(family_of(bold.get()), family); |
| 843 | + assert_ne!(CTFontGetSymbolicTraits(bold.get()) & K_CT_FONT_TRAIT_BOLD, 0); |
| 844 | + } |
| 845 | + } |
| 846 | + |
| 847 | + // Et chaque famille se voit, à l'encre dessinée. |
| 848 | + let mut blocks = Vec::new(); |
| 849 | + for family in crate::text_fonts::SHIPPED_FAMILIES { |
| 850 | + let mut s = spec("Hamburgefonstiv"); |
| 851 | + s.font_family = family.into(); |
| 852 | + s.font_size_px = 100.0; |
| 853 | + s.box_px = [1600, 200]; |
| 854 | + let Some((px, w, h)) = raster_bgra(&s) else { |
| 855 | + return; |
| 856 | + }; |
| 857 | + let (x0, y0, x1, y1) = ink_bounds(&px, w, h); |
| 858 | + blocks.push((family, (x1 - x0 + 1) as f32, (y1 - y0 + 1) as f32)); |
| 859 | + } |
| 860 | + crate::text_fonts::assert_distinct_blocks(&blocks); |
| 861 | + } |
| 862 | + |
742 | 863 | /// Le texte n'est pas retourné. Le test regarde où est l'encre plutôt que de faire |
743 | 864 | /// confiance au sens du CTM : c'est la seule façon de distinguer « bien orienté » de |
744 | 865 | /// « retourné », et le retournement était un vrai bug de ce fichier. |
|
0 commit comments