use crate::error::TextError;
use crate::types::{GlyphBitmap, GlyphBounds};
use objc2_core_foundation::{CGFloat, CGPoint, CGRect, CGSize};
use objc2_core_graphics::{CGBitmapContextCreate, CGContext, CGImageAlphaInfo};
use objc2_core_text::{CTFont, CTFontOrientation};
use std::ptr::NonNull;
const MAX_RENDER_PX: usize = 512;
pub fn rasterize(
ct_font: &CTFont,
glyph_id: u16,
size_lpx: f32,
scale: f32,
variant: u8,
) -> Result<GlyphBitmap, TextError> {
if variant > 3 {
return Err(TextError::RasterizationFailed(
"variant out of range (must be 0-3)".into(),
));
}
let uncapped = (size_lpx * scale * 2.0).ceil() as usize;
if uncapped == 0 {
return Err(TextError::RasterizationFailed("render size is zero".into()));
}
let render_size = if uncapped > MAX_RENDER_PX {
log::warn!(
"rasterize: render size {} exceeds MAX_RENDER_PX {}; clamping (font size {:.1}px, scale {:.2})",
uncapped,
MAX_RENDER_PX,
size_lpx,
scale,
);
MAX_RENDER_PX
} else {
uncapped
};
let render_w = render_size;
let render_h = render_size;
let mut buffer: Vec<u8> = vec![0; render_w * render_h];
let ctx = unsafe {
CGBitmapContextCreate(
buffer.as_mut_ptr().cast(),
render_w,
render_h,
8, render_w, None, CGImageAlphaInfo::Only.0,
)
};
let Some(ctx) = ctx else {
return Err(TextError::RasterizationFailed(
"CGBitmapContextCreate returned null".into(),
));
};
CGContext::set_should_antialias(Some(&*ctx), true);
CGContext::set_should_smooth_fonts(Some(&*ctx), false);
CGContext::set_allows_font_subpixel_positioning(Some(&*ctx), true);
CGContext::set_should_subpixel_position_fonts(Some(&*ctx), true);
CGContext::set_allows_font_subpixel_quantization(Some(&*ctx), false);
CGContext::set_should_subpixel_quantize_fonts(Some(&*ctx), false);
let sub_pixel_offset_device: CGFloat = (variant as CGFloat) * 0.25;
let sub_pixel_offset_user: CGFloat = sub_pixel_offset_device / scale as CGFloat;
let baseline_x: CGFloat = (render_w as CGFloat) * 0.25;
let baseline_y: CGFloat = (render_h as CGFloat) * 0.25;
CGContext::translate_ctm(Some(&*ctx), baseline_x, baseline_y);
CGContext::scale_ctm(Some(&*ctx), scale as CGFloat, scale as CGFloat);
let glyph = glyph_id;
let position = CGPoint {
x: sub_pixel_offset_user,
y: 0.0,
};
unsafe {
ct_font.draw_glyphs(NonNull::from(&glyph), NonNull::from(&position), 1, &ctx);
}
let mut advance = CGSize {
width: 0.0,
height: 0.0,
};
unsafe {
ct_font.advances_for_glyphs(
CTFontOrientation::Default,
NonNull::from(&glyph),
&mut advance as *mut CGSize,
1,
);
}
let advance_x_lpx = advance.width as f32;
let (min_x, min_y, max_x, max_y) = find_tight_bounds(&buffer, render_w, render_h);
if min_x > max_x || min_y > max_y {
return Ok(GlyphBitmap {
width: 0,
height: 0,
bearing_x_lpx: 0.0,
bearing_y_lpx: 0.0,
advance_x_lpx,
alpha: vec![],
});
}
let tight_w = max_x - min_x + 1;
let tight_h = max_y - min_y + 1;
let mut cropped = Vec::with_capacity(tight_w * tight_h);
for y in min_y..=max_y {
let row_start = y * render_w + min_x;
cropped.extend_from_slice(&buffer[row_start..row_start + tight_w]);
}
let bearing_x_px = min_x as f32 - (baseline_x as f32);
let bearing_y_px = (render_h as f32) - baseline_y as f32 - (min_y as f32);
let bearing_x_lpx = bearing_x_px / scale;
let bearing_y_lpx = bearing_y_px / scale;
Ok(GlyphBitmap {
width: tight_w as u32,
height: tight_h as u32,
bearing_x_lpx,
bearing_y_lpx,
advance_x_lpx,
alpha: cropped,
})
}
pub fn get_glyph_bounds(
ct_font: &CTFont,
glyph_id: u16,
scale: f32,
) -> Result<GlyphBounds, TextError> {
let mut bounds = CGRect {
origin: CGPoint { x: 0.0, y: 0.0 },
size: CGSize {
width: 0.0,
height: 0.0,
},
};
unsafe {
ct_font.bounding_rects_for_glyphs(
CTFontOrientation::Default,
NonNull::from(&glyph_id),
&mut bounds as *mut CGRect,
1,
);
}
if bounds.size.width <= 0.0 || bounds.size.height <= 0.0 {
return Ok(GlyphBounds::ZERO);
}
let width = (bounds.size.width as f32 * scale).ceil() as u32;
let height = (bounds.size.height as f32 * scale).ceil() as u32;
Ok(GlyphBounds { width, height })
}
fn find_tight_bounds(buffer: &[u8], width: usize, height: usize) -> (usize, usize, usize, usize) {
let mut min_y = height; 'top: for y in 0..height {
for x in 0..width {
if buffer[y * width + x] >= 1 {
min_y = y;
break 'top;
}
}
}
if min_y == height {
return (width, height, 0, 0);
}
let mut max_y = min_y;
'bottom: for y in (min_y..height).rev() {
for x in 0..width {
if buffer[y * width + x] >= 1 {
max_y = y;
break 'bottom;
}
}
}
let mut min_x = width; let mut max_x = 0usize;
for y in min_y..=max_y {
let row = &buffer[y * width..(y + 1) * width];
#[allow(clippy::needless_range_loop)] for x in 0..min_x {
if row[x] >= 1 {
min_x = x;
break;
}
}
for x in (max_x..width).rev() {
if row[x] >= 1 {
if x > max_x {
max_x = x;
}
break;
}
}
}
(min_x, min_y, max_x, max_y)
}