1#![forbid(unsafe_code)]
15
16use crate::native_route::{
17 FallbackFace, NativeShapingError, NativeShapingRequest, NativeShapingRoute, PlatformRunGlyph,
18 PlatformShapedOutput, ShapingRouteCapabilities, ShapingRouteKind, assemble_platform_run,
19};
20use crate::shaping::Direction;
21use crate::text_run::{FontId, FontOrigin, OwnedTextRun, TextRunContext, utf16_to_byte};
22use std::fmt;
23
24pub const DEFAULT_MAC_CONTEXT_BUDGET: usize = 65_536;
26
27pub const MAX_MAC_RASTER_DIMENSION: u32 = 1024;
29
30pub const MAX_MAC_RASTER_BYTES: usize = 4 * 1024 * 1024;
32
33#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct MacFontAdapterConfig {
36 pub max_paragraph_bytes: usize,
38 pub allow_fallback_fonts: bool,
40 pub supports_color_emoji: bool,
42 pub raster_dimension_cap: u32,
44 pub raster_bytes_cap: usize,
46}
47
48impl Default for MacFontAdapterConfig {
49 fn default() -> Self {
50 Self {
51 max_paragraph_bytes: DEFAULT_MAC_CONTEXT_BUDGET,
52 allow_fallback_fonts: true,
53 supports_color_emoji: true,
54 raster_dimension_cap: MAX_MAC_RASTER_DIMENSION,
55 raster_bytes_cap: MAX_MAC_RASTER_BYTES,
56 }
57 }
58}
59
60#[derive(Clone, Debug, PartialEq)]
62pub struct RawCoreTextGlyph {
63 pub glyph_id: u16,
65 pub string_index_utf16: usize,
67 pub string_length_utf16: usize,
69 pub font_postscript_name: String,
71 pub font_family_name: String,
73 pub x_advance: f32,
75 pub y_advance: f32,
77 pub x_offset: f32,
79 pub y_offset: f32,
81 pub is_color_emoji: bool,
83}
84
85#[derive(Clone, Debug, PartialEq)]
87pub struct RawCoreTextLine {
88 pub glyphs: Vec<RawCoreTextGlyph>,
90 pub is_rtl: bool,
92 pub total_advance: f32,
94}
95
96#[derive(Clone, Debug, PartialEq)]
98pub struct RawCoreGraphicsRaster {
99 pub width: u32,
101 pub height: u32,
103 pub pixels: Vec<u8>,
105 pub is_bgra: bool,
107 pub bearing_x: f32,
109 pub bearing_y: f32,
111 pub advance_width: f32,
113}
114
115#[derive(Clone, Debug, PartialEq, Eq)]
117pub enum MacFontAdapterError {
118 ContextBudgetExceeded { length: usize, max_allowed: usize },
120 EmptyText,
122 InvalidUtf16Offset { utf16_offset: usize },
124 MidScalarBoundary { byte_offset: usize },
126 BridgeUnavailable(String),
128 ForeignCallFailed(String),
130 RasterDimensionTooLarge { dimension: u32, max_allowed: u32 },
132 RasterBytesTooLarge { bytes: usize, max_allowed: usize },
134 RasterBufferMismatch { expected: usize, actual: usize },
136 FallbackDisabled { unshaped_byte_offset: usize },
138}
139
140impl fmt::Display for MacFontAdapterError {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 match self {
143 Self::ContextBudgetExceeded {
144 length,
145 max_allowed,
146 } => {
147 write!(
148 f,
149 "CoreText context budget exceeded: {length} bytes > {max_allowed} max"
150 )
151 }
152 Self::EmptyText => write!(f, "cannot shape empty text with CoreText"),
153 Self::InvalidUtf16Offset { utf16_offset } => {
154 write!(f, "invalid UTF-16 code unit offset: {utf16_offset}")
155 }
156 Self::MidScalarBoundary { byte_offset } => {
157 write!(
158 f,
159 "mapped byte offset {byte_offset} is inside a multi-byte UTF-8 scalar"
160 )
161 }
162 Self::BridgeUnavailable(msg) => write!(f, "Mac bridge unavailable: {msg}"),
163 Self::ForeignCallFailed(msg) => write!(f, "foreign CoreText call failed: {msg}"),
164 Self::RasterDimensionTooLarge {
165 dimension,
166 max_allowed,
167 } => {
168 write!(
169 f,
170 "raster dimension {dimension} exceeds maximum {max_allowed}"
171 )
172 }
173 Self::RasterBytesTooLarge { bytes, max_allowed } => {
174 write!(f, "raster bytes {bytes} exceeds maximum {max_allowed}")
175 }
176 Self::RasterBufferMismatch { expected, actual } => {
177 write!(
178 f,
179 "raster buffer mismatch: expected {expected} bytes, got {actual}"
180 )
181 }
182 Self::FallbackDisabled {
183 unshaped_byte_offset,
184 } => {
185 write!(
186 f,
187 "system fallback required at byte offset {unshaped_byte_offset} but disabled"
188 )
189 }
190 }
191 }
192}
193
194impl std::error::Error for MacFontAdapterError {}
195
196impl From<MacFontAdapterError> for NativeShapingError {
197 fn from(err: MacFontAdapterError) -> Self {
198 match err {
199 MacFontAdapterError::ContextBudgetExceeded {
200 length,
201 max_allowed,
202 } => Self::ContextBudgetExceeded {
203 length,
204 max_allowed,
205 },
206 MacFontAdapterError::EmptyText => Self::EmptyText,
207 MacFontAdapterError::InvalidUtf16Offset { utf16_offset } => Self::InvalidByteRange {
208 offset: utf16_offset,
209 text_len: 0,
210 },
211 MacFontAdapterError::MidScalarBoundary { byte_offset } => Self::MidScalarBoundary {
212 offset: byte_offset,
213 },
214 MacFontAdapterError::FallbackDisabled {
215 unshaped_byte_offset,
216 } => Self::FallbackRequired {
217 unshaped_byte_offset,
218 },
219 MacFontAdapterError::BridgeUnavailable(msg) => Self::PlatformUnavailable(msg),
220 MacFontAdapterError::ForeignCallFailed(msg) => Self::AdapterError(msg),
221 MacFontAdapterError::RasterDimensionTooLarge { dimension, .. } => {
222 Self::AdapterError(format!("raster dimension {dimension} too large"))
223 }
224 MacFontAdapterError::RasterBytesTooLarge { bytes, .. } => {
225 Self::AdapterError(format!("raster bytes {bytes} too large"))
226 }
227 MacFontAdapterError::RasterBufferMismatch { expected, actual } => Self::AdapterError(
228 format!("raster buffer mismatch: expected {expected}, got {actual}"),
229 ),
230 }
231 }
232}
233
234pub trait MacBridgeDriver: Send + Sync {
239 fn shape_line(
241 &self,
242 text: &str,
243 font_family: &str,
244 font_size: f32,
245 is_rtl: bool,
246 ) -> Result<RawCoreTextLine, MacFontAdapterError>;
247
248 fn rasterize_glyph(
250 &self,
251 font_postscript_name: &str,
252 glyph_id: u16,
253 font_size_px: f32,
254 ) -> Result<Option<RawCoreGraphicsRaster>, MacFontAdapterError>;
255}
256
257pub struct MacFontAdapter<D: MacBridgeDriver> {
259 driver: D,
260 config: MacFontAdapterConfig,
261}
262
263impl<D: MacBridgeDriver> MacFontAdapter<D> {
264 #[must_use]
266 pub fn new(driver: D) -> Self {
267 Self {
268 driver,
269 config: MacFontAdapterConfig::default(),
270 }
271 }
272
273 #[must_use]
275 pub fn with_config(driver: D, config: MacFontAdapterConfig) -> Self {
276 Self { driver, config }
277 }
278
279 #[must_use]
281 pub fn config(&self) -> &MacFontAdapterConfig {
282 &self.config
283 }
284
285 #[must_use]
287 pub fn driver(&self) -> &D {
288 &self.driver
289 }
290
291 pub fn get_glyph_rgba_raster(
293 &self,
294 font_postscript_name: &str,
295 glyph_id: u16,
296 font_size_px: f32,
297 ) -> Result<Option<RawCoreGraphicsRaster>, MacFontAdapterError> {
298 let raw_opt = self
299 .driver
300 .rasterize_glyph(font_postscript_name, glyph_id, font_size_px)?;
301 let mut raw = match raw_opt {
302 Some(r) => r,
303 None => return Ok(None),
304 };
305
306 if raw.width > self.config.raster_dimension_cap {
307 return Err(MacFontAdapterError::RasterDimensionTooLarge {
308 dimension: raw.width,
309 max_allowed: self.config.raster_dimension_cap,
310 });
311 }
312 if raw.height > self.config.raster_dimension_cap {
313 return Err(MacFontAdapterError::RasterDimensionTooLarge {
314 dimension: raw.height,
315 max_allowed: self.config.raster_dimension_cap,
316 });
317 }
318 if raw.pixels.len() > self.config.raster_bytes_cap {
319 return Err(MacFontAdapterError::RasterBytesTooLarge {
320 bytes: raw.pixels.len(),
321 max_allowed: self.config.raster_bytes_cap,
322 });
323 }
324
325 let expected_bytes = match (raw.width as usize)
326 .checked_mul(raw.height as usize)
327 .and_then(|px| px.checked_mul(4))
328 {
329 Some(exp) => exp,
330 None => {
331 return Err(MacFontAdapterError::RasterBytesTooLarge {
332 bytes: usize::MAX,
333 max_allowed: self.config.raster_bytes_cap,
334 });
335 }
336 };
337
338 if raw.pixels.len() != expected_bytes {
339 return Err(MacFontAdapterError::RasterBufferMismatch {
340 expected: expected_bytes,
341 actual: raw.pixels.len(),
342 });
343 }
344
345 if raw.is_bgra {
347 for chunk in raw.pixels.chunks_exact_mut(4) {
348 chunk.swap(0, 2); }
350 raw.is_bgra = false;
351 }
352
353 Ok(Some(raw))
354 }
355}
356
357impl<D: MacBridgeDriver> NativeShapingRoute for MacFontAdapter<D> {
358 fn route_kind(&self) -> ShapingRouteKind {
359 ShapingRouteKind::SystemPlatform
360 }
361
362 fn capabilities(&self) -> ShapingRouteCapabilities {
363 ShapingRouteCapabilities {
364 route_kind: ShapingRouteKind::SystemPlatform,
365 is_pixel_deterministic: false,
366 supports_fallback_fonts: self.config.allow_fallback_fonts,
367 supports_color_emoji: self.config.supports_color_emoji,
368 supports_bidi: true,
369 max_paragraph_bytes: self.config.max_paragraph_bytes,
370 }
371 }
372
373 fn shape_run(
374 &self,
375 req: &NativeShapingRequest<'_>,
376 ) -> Result<OwnedTextRun, NativeShapingError> {
377 if req.text.is_empty() {
378 return Err(NativeShapingError::EmptyText);
379 }
380
381 if req.text.len() > self.config.max_paragraph_bytes {
383 return Err(NativeShapingError::ContextBudgetExceeded {
384 length: req.text.len(),
385 max_allowed: self.config.max_paragraph_bytes,
386 });
387 }
388
389 let is_rtl = req.direction == Direction::RightToLeft;
390 let line = self
391 .driver
392 .shape_line(req.text, "SystemFont", req.font_size, is_rtl)?;
393
394 let mut platform_glyphs: Vec<PlatformRunGlyph> = Vec::with_capacity(line.glyphs.len());
395 let mut used_fallbacks: Vec<FallbackFace> = Vec::new();
396
397 for g in &line.glyphs {
398 let byte_start = match utf16_to_byte(req.text, g.string_index_utf16) {
400 Some(b) => b,
401 None => {
402 return Err(NativeShapingError::InvalidByteRange {
403 offset: g.string_index_utf16,
404 text_len: req.text.len(),
405 });
406 }
407 };
408
409 let utf16_end = g.string_index_utf16.saturating_add(g.string_length_utf16);
410 let byte_end = match utf16_to_byte(req.text, utf16_end) {
411 Some(b) => b,
412 None => {
413 return Err(NativeShapingError::InvalidByteRange {
414 offset: utf16_end,
415 text_len: req.text.len(),
416 });
417 }
418 };
419
420 if byte_end < byte_start {
421 return Err(NativeShapingError::InvalidByteRange {
422 offset: byte_end,
423 text_len: req.text.len(),
424 });
425 }
426
427 let byte_len = byte_end - byte_start;
428
429 let is_fallback =
431 !g.font_postscript_name.is_empty() && g.font_postscript_name != "SystemFont";
432
433 let font_id = if is_fallback {
434 if !req.allow_system_fallback {
435 return Err(NativeShapingError::FallbackRequired {
436 unshaped_byte_offset: byte_start,
437 });
438 }
439
440 let mut hash = 0xcbf29ce484222325u64;
442 for &b in g.font_postscript_name.as_bytes() {
443 hash = (hash ^ u64::from(b)).wrapping_mul(0x100000001b3);
444 }
445 let fid = FontId::new(hash);
446
447 if !used_fallbacks.iter().any(|f| f.font_id == fid) {
448 used_fallbacks.push(FallbackFace {
449 font_id: fid,
450 family_name: g.font_family_name.clone(),
451 postscript_name: g.font_postscript_name.clone(),
452 units_per_em: 1000,
453 is_color_emoji: g.is_color_emoji,
454 });
455 }
456 fid
457 } else {
458 req.primary_font_id
459 };
460
461 platform_glyphs.push(PlatformRunGlyph {
462 glyph_id: g.glyph_id,
463 font_id,
464 cluster_byte_offset: byte_start,
465 cluster_byte_len: byte_len,
466 x_advance: g.x_advance,
467 y_advance: g.y_advance,
468 x_offset: g.x_offset,
469 y_offset: g.y_offset,
470 });
471 }
472
473 let output = PlatformShapedOutput {
474 logical_text: req.text.to_string(),
475 direction: req.direction,
476 font_size: req.font_size,
477 glyphs: platform_glyphs,
478 fallback_faces: used_fallbacks,
479 route_kind: ShapingRouteKind::SystemPlatform,
480 };
481
482 let primary_context = TextRunContext {
483 font_id: req.primary_font_id,
484 font_size: req.font_size,
485 script: req.script,
486 language: req.language,
487 direction: req.direction,
488 font_origin: FontOrigin::BundledFace,
489 };
490
491 assemble_platform_run(primary_context, output)
492 }
493}
494
495#[derive(Clone, Debug, Default)]
497pub struct SimulatedMacBridge {
498 fail_bridge: bool,
499}
500
501impl SimulatedMacBridge {
502 #[must_use]
504 pub fn new() -> Self {
505 Self { fail_bridge: false }
506 }
507
508 #[must_use]
510 pub fn new_failing() -> Self {
511 Self { fail_bridge: true }
512 }
513}
514
515impl MacBridgeDriver for SimulatedMacBridge {
516 fn shape_line(
517 &self,
518 text: &str,
519 _font_family: &str,
520 font_size: f32,
521 is_rtl: bool,
522 ) -> Result<RawCoreTextLine, MacFontAdapterError> {
523 if self.fail_bridge {
524 return Err(MacFontAdapterError::ForeignCallFailed(
525 "simulated CoreText foreign call abort".to_string(),
526 ));
527 }
528
529 let mut glyphs = Vec::new();
530 let mut cur_utf16 = 0usize;
531 let mut total_advance = 0.0f32;
532
533 for ch in text.chars() {
534 let cp = ch as u32;
535 let utf16_len = ch.len_utf16();
536
537 let (ps_name, fam_name, is_color, adv_scale) =
539 if (0x2E80..=0x9FFF).contains(&cp) || (0xAC00..=0xD7AF).contains(&cp) {
540 ("PingFangSC-Regular", "PingFang SC", false, 1.0)
541 } else if (0x0590..=0x08FF).contains(&cp) || (0xFB1D..=0xFEFF).contains(&cp) {
542 ("GeezaPro", "Geeza Pro", false, 0.6)
543 } else if (0x1F300..=0x1FAFF).contains(&cp) || (0x2600..=0x27BF).contains(&cp) {
544 ("AppleColorEmoji", "Apple Color Emoji", true, 1.0)
545 } else {
546 ("SystemFont", "SystemFont", false, 0.5)
547 };
548
549 let x_adv = font_size * adv_scale;
550 let glyph_id = (cp % 5000) as u16;
551
552 glyphs.push(RawCoreTextGlyph {
553 glyph_id,
554 string_index_utf16: cur_utf16,
555 string_length_utf16: utf16_len,
556 font_postscript_name: ps_name.to_string(),
557 font_family_name: fam_name.to_string(),
558 x_advance: x_adv,
559 y_advance: 0.0,
560 x_offset: 0.0,
561 y_offset: 0.0,
562 is_color_emoji: is_color,
563 });
564
565 cur_utf16 += utf16_len;
566 total_advance += x_adv;
567 }
568
569 Ok(RawCoreTextLine {
570 glyphs,
571 is_rtl,
572 total_advance,
573 })
574 }
575
576 fn rasterize_glyph(
577 &self,
578 font_postscript_name: &str,
579 glyph_id: u16,
580 font_size_px: f32,
581 ) -> Result<Option<RawCoreGraphicsRaster>, MacFontAdapterError> {
582 if self.fail_bridge {
583 return Err(MacFontAdapterError::ForeignCallFailed(
584 "simulated CoreGraphics foreign call abort".to_string(),
585 ));
586 }
587
588 if font_postscript_name == "AppleColorEmoji" || glyph_id == 777 {
589 let size = (font_size_px as u32).clamp(8, 64);
590 let len = (size as usize) * (size as usize) * 4;
591 let mut pixels = vec![0u8; len];
592 for chunk in pixels.chunks_exact_mut(4) {
594 chunk[0] = 255; chunk[1] = 200; chunk[2] = 50; chunk[3] = 255; }
599 Ok(Some(RawCoreGraphicsRaster {
600 width: size,
601 height: size,
602 pixels,
603 is_bgra: true,
604 bearing_x: 0.0,
605 bearing_y: font_size_px,
606 advance_width: font_size_px,
607 }))
608 } else {
609 Ok(None)
610 }
611 }
612}