#![deny(clippy::pedantic)]
#![allow(clippy::doc_markdown)] #![allow(clippy::missing_errors_doc)]
use std::ffi::{CStr, CString, c_char, c_long};
use std::fmt;
use std::marker::PhantomData;
use std::sync::Once;
pub use fcft_sys as ffi;
static INIT: Once = Once::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogColorize {
Never,
Always,
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogClass {
None,
Error,
Warning,
Info,
Debug,
}
#[allow(clippy::missing_panics_doc)] pub fn init(colourize: LogColorize, syslog: bool, log_class: LogClass) {
let colourize = match colourize {
LogColorize::Never => ffi::fcft_log_colorize_FCFT_LOG_COLORIZE_NEVER,
LogColorize::Always => ffi::fcft_log_colorize_FCFT_LOG_COLORIZE_ALWAYS,
LogColorize::Auto => ffi::fcft_log_colorize_FCFT_LOG_COLORIZE_AUTO,
};
let log_class = match log_class {
LogClass::None => ffi::fcft_log_class_FCFT_LOG_CLASS_NONE,
LogClass::Error => ffi::fcft_log_class_FCFT_LOG_CLASS_ERROR,
LogClass::Warning => ffi::fcft_log_class_FCFT_LOG_CLASS_WARNING,
LogClass::Info => ffi::fcft_log_class_FCFT_LOG_CLASS_INFO,
LogClass::Debug => ffi::fcft_log_class_FCFT_LOG_CLASS_DEBUG,
};
INIT.call_once(|| {
let ok = unsafe { ffi::fcft_init(colourize, syslog, log_class) };
assert!(ok, "fcft_init failed");
});
}
fn ensure_init() {
init(LogColorize::Never, false, LogClass::Warning);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Capabilities {
bits: ffi::fcft_capabilities,
}
impl Capabilities {
#[must_use]
pub fn grapheme_shaping(self) -> bool {
self.bits & ffi::fcft_capabilities_FCFT_CAPABILITY_GRAPHEME_SHAPING != 0
}
#[must_use]
pub fn text_run_shaping(self) -> bool {
self.bits & ffi::fcft_capabilities_FCFT_CAPABILITY_TEXT_RUN_SHAPING != 0
}
#[must_use]
pub fn svg(self) -> bool {
self.bits & ffi::fcft_capabilities_FCFT_CAPABILITY_SVG != 0
}
}
#[must_use]
pub fn capabilities() -> Capabilities {
Capabilities {
bits: unsafe { ffi::fcft_capabilities() },
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Subpixel {
#[default]
Default,
None,
HorizontalRgb,
HorizontalBgr,
VerticalRgb,
VerticalBgr,
}
impl Subpixel {
fn to_ffi(self) -> ffi::fcft_subpixel {
match self {
Subpixel::Default => ffi::fcft_subpixel_FCFT_SUBPIXEL_DEFAULT,
Subpixel::None => ffi::fcft_subpixel_FCFT_SUBPIXEL_NONE,
Subpixel::HorizontalRgb => ffi::fcft_subpixel_FCFT_SUBPIXEL_HORIZONTAL_RGB,
Subpixel::HorizontalBgr => ffi::fcft_subpixel_FCFT_SUBPIXEL_HORIZONTAL_BGR,
Subpixel::VerticalRgb => ffi::fcft_subpixel_FCFT_SUBPIXEL_VERTICAL_RGB,
Subpixel::VerticalBgr => ffi::fcft_subpixel_FCFT_SUBPIXEL_VERTICAL_BGR,
}
}
}
#[derive(Debug)]
pub struct Error(());
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("failed to instantiate font")
}
}
impl std::error::Error for Error {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Decoration {
pub position: i32,
pub thickness: i32,
}
pub struct Font {
ptr: *mut ffi::fcft_font,
}
unsafe impl Send for Font {}
impl Font {
pub fn new(names: &[&str], attributes: Option<&str>) -> Result<Font, Error> {
ensure_init();
if names.is_empty() {
return Err(Error(()));
}
let names: Vec<CString> = names
.iter()
.map(|n| CString::new(*n).map_err(|_| Error(())))
.collect::<Result<_, _>>()?;
let mut ptrs: Vec<*const c_char> = names.iter().map(|n| n.as_ptr()).collect();
let attributes = match attributes {
Some(a) => Some(CString::new(a).map_err(|_| Error(()))?),
None => None,
};
let ptr = unsafe {
ffi::fcft_from_name(
ptrs.len(),
ptrs.as_mut_ptr(),
attributes.as_ref().map_or(std::ptr::null(), |a| a.as_ptr()),
)
};
if ptr.is_null() {
Err(Error(()))
} else {
Ok(Font { ptr })
}
}
fn raw(&self) -> &ffi::fcft_font {
unsafe { &*self.ptr }
}
#[must_use]
pub fn name(&self) -> Option<&str> {
let name = self.raw().name;
if name.is_null() {
return None;
}
unsafe { CStr::from_ptr(name) }.to_str().ok()
}
#[must_use]
pub fn height(&self) -> i32 {
self.raw().height
}
#[must_use]
pub fn ascent(&self) -> i32 {
self.raw().ascent
}
#[must_use]
pub fn descent(&self) -> i32 {
self.raw().descent
}
#[must_use]
pub fn max_advance(&self) -> (i32, i32) {
let a = self.raw().max_advance;
(a.x, a.y)
}
#[must_use]
pub fn underline(&self) -> Decoration {
let u = self.raw().underline;
Decoration {
position: u.position,
thickness: u.thickness,
}
}
#[must_use]
pub fn strikeout(&self) -> Decoration {
let s = self.raw().strikeout;
Decoration {
position: s.position,
thickness: s.thickness,
}
}
#[must_use]
pub fn antialias(&self) -> bool {
self.raw().antialias
}
#[must_use]
pub fn subpixel(&self) -> Subpixel {
match self.raw().subpixel {
ffi::fcft_subpixel_FCFT_SUBPIXEL_NONE => Subpixel::None,
ffi::fcft_subpixel_FCFT_SUBPIXEL_HORIZONTAL_RGB => Subpixel::HorizontalRgb,
ffi::fcft_subpixel_FCFT_SUBPIXEL_HORIZONTAL_BGR => Subpixel::HorizontalBgr,
ffi::fcft_subpixel_FCFT_SUBPIXEL_VERTICAL_RGB => Subpixel::VerticalRgb,
ffi::fcft_subpixel_FCFT_SUBPIXEL_VERTICAL_BGR => Subpixel::VerticalBgr,
_ => Subpixel::Default,
}
}
#[must_use]
pub fn glyph(&self, c: char, subpixel: Subpixel) -> Option<Glyph<'_>> {
let g =
unsafe { ffi::fcft_rasterize_char_utf32(self.ptr, u32::from(c), subpixel.to_ffi()) };
if g.is_null() {
None
} else {
Some(Glyph {
raw: unsafe { &*g },
})
}
}
#[must_use]
pub fn kerning(&self, left: char, right: char) -> Option<(i64, i64)> {
let (mut x, mut y): (c_long, c_long) = (0, 0);
let ok = unsafe {
ffi::fcft_kerning(
self.ptr,
u32::from(left),
u32::from(right),
&raw mut x,
&raw mut y,
)
};
#[allow(clippy::useless_conversion)] ok.then_some((x.into(), y.into()))
}
#[must_use]
pub fn shape(&self, text: &str, subpixel: Subpixel) -> TextRun<'_> {
if capabilities().text_run_shaping()
&& let Some(run) = self.shape_run(text, subpixel)
{
return run;
}
self.shape_chars(text, subpixel)
}
#[must_use]
pub fn shape_run(&self, text: &str, subpixel: Subpixel) -> Option<TextRun<'_>> {
let cps: Vec<u32> = text.chars().map(u32::from).collect();
if cps.is_empty() {
return None;
}
let run = unsafe {
ffi::fcft_rasterize_text_run_utf32(self.ptr, cps.len(), cps.as_ptr(), subpixel.to_ffi())
};
if run.is_null() {
return None;
}
Some(TextRun {
inner: RunInner::Shaped(run),
_font: PhantomData,
})
}
#[must_use]
pub fn shape_chars(&self, text: &str, subpixel: Subpixel) -> TextRun<'_> {
let mut glyphs = Vec::new();
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
let Some(glyph) = self.glyph(c, subpixel) else {
continue;
};
let mut advance = glyph.advance();
#[allow(clippy::cast_possible_truncation)]
if let Some(&next) = chars.peek()
&& let Some((kx, ky)) = self.kerning(c, next)
{
advance.0 += kx as i32;
advance.1 += ky as i32;
}
glyphs.push(CharGlyph {
glyph: glyph.raw,
advance,
});
}
TextRun {
inner: RunInner::Chars(glyphs),
_font: PhantomData,
}
}
#[must_use]
pub fn shape_truncated(&self, text: &str, subpixel: Subpixel, max_width: i32) -> TextRun<'_> {
let full = self.shape(text, subpixel);
if full.width() <= max_width {
return full;
}
let chars: Vec<char> = text.chars().collect();
let fits = |n: usize| {
let mut s: String = chars[..n].iter().collect();
s.push('…');
let run = self.shape(&s, subpixel);
(run.width() <= max_width).then_some(run)
};
let Some(mut best) = fits(0) else {
return self.shape_chars("", subpixel);
};
let (mut lo, mut hi) = (0, chars.len());
while hi - lo > 1 {
let mid = usize::midpoint(lo, hi);
match fits(mid) {
Some(run) => {
best = run;
lo = mid;
}
None => hi = mid,
}
}
best
}
}
impl Drop for Font {
fn drop(&mut self) {
unsafe { ffi::fcft_destroy(self.ptr) };
}
}
#[derive(Clone, Copy)]
pub struct Glyph<'a> {
raw: &'a ffi::fcft_glyph,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GlyphFormat {
A1,
A8,
X8R8G8B8,
A8R8G8B8,
Other(u32),
}
impl Glyph<'_> {
#[must_use]
pub fn codepoint(&self) -> u32 {
self.raw.cp
}
#[must_use]
pub fn x(&self) -> i32 {
self.raw.x
}
#[must_use]
pub fn y(&self) -> i32 {
self.raw.y
}
#[must_use]
pub fn width(&self) -> i32 {
self.raw.width
}
#[must_use]
pub fn height(&self) -> i32 {
self.raw.height
}
#[must_use]
pub fn advance(&self) -> (i32, i32) {
(self.raw.advance.x, self.raw.advance.y)
}
#[must_use]
pub fn is_color_glyph(&self) -> bool {
self.raw.is_color_glyph
}
#[must_use]
pub fn format(&self) -> GlyphFormat {
match unsafe { ffi::pixman_image_get_format(self.raw.pix) } {
ffi::pixman_format_code_t_PIXMAN_a1 => GlyphFormat::A1,
ffi::pixman_format_code_t_PIXMAN_a8 => GlyphFormat::A8,
ffi::pixman_format_code_t_PIXMAN_x8r8g8b8 => GlyphFormat::X8R8G8B8,
ffi::pixman_format_code_t_PIXMAN_a8r8g8b8 => GlyphFormat::A8R8G8B8,
other => GlyphFormat::Other(other),
}
}
#[must_use]
pub fn pixman_image(&mut self) -> *mut ffi::pixman_image_t {
self.raw.pix
}
#[must_use]
pub fn as_raw(&self) -> &ffi::fcft_glyph {
self.raw
}
}
pub struct ShapedGlyph<'a> {
pub glyph: Glyph<'a>,
pub advance: (i32, i32),
}
enum RunInner {
Shaped(*mut ffi::fcft_text_run),
Chars(Vec<CharGlyph>),
}
struct CharGlyph {
glyph: *const ffi::fcft_glyph,
advance: (i32, i32),
}
pub struct TextRun<'f> {
inner: RunInner,
_font: PhantomData<&'f Font>,
}
impl TextRun<'_> {
#[must_use]
pub fn len(&self) -> usize {
match &self.inner {
RunInner::Shaped(run) => unsafe { &**run }.count,
RunInner::Chars(glyphs) => glyphs.len(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn glyphs(&self) -> impl Iterator<Item = ShapedGlyph<'_>> {
let (run, chars) = match &self.inner {
RunInner::Shaped(run) => (Some(unsafe { &**run }), [].iter()),
RunInner::Chars(glyphs) => (None, glyphs.iter()),
};
let shaped = run.into_iter().flat_map(|run| {
unsafe { std::slice::from_raw_parts(run.glyphs, run.count) }
.iter()
.map(|&g| {
let glyph = Glyph {
raw: unsafe { &*g },
};
ShapedGlyph {
advance: glyph.advance(),
glyph,
}
})
});
let chars = chars.map(|cg| ShapedGlyph {
glyph: Glyph {
raw: unsafe { &*cg.glyph },
},
advance: cg.advance,
});
shaped.chain(chars)
}
#[must_use]
pub fn width(&self) -> i32 {
self.glyphs().map(|g| g.advance.0).sum()
}
}
impl Drop for TextRun<'_> {
fn drop(&mut self) {
if let RunInner::Shaped(run) = self.inner {
unsafe { ffi::fcft_text_run_destroy(run) };
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Color {
pub r: u16,
pub g: u16,
pub b: u16,
pub a: u16,
}
impl Color {
#[must_use]
pub const fn from_argb8888(argb: u32) -> Color {
Color {
a: ((argb >> 24) & 0xff) as u16 * 0x101,
r: ((argb >> 16) & 0xff) as u16 * 0x101,
g: ((argb >> 8) & 0xff) as u16 * 0x101,
b: (argb & 0xff) as u16 * 0x101,
}
}
fn to_pixman(self) -> ffi::pixman_color {
#[allow(clippy::cast_possible_truncation)]
let premul = |c: u16| ((u32::from(c) * u32::from(self.a)) / 0xffff) as u16;
ffi::pixman_color {
red: premul(self.r),
green: premul(self.g),
blue: premul(self.b),
alpha: self.a,
}
}
}
pub struct Canvas<'buf> {
image: *mut ffi::pixman_image_t,
_buf: PhantomData<&'buf mut [u8]>,
}
impl<'buf> Canvas<'buf> {
pub fn from_xrgb8888(
buf: &'buf mut [u8],
width: u32,
height: u32,
stride: u32,
) -> Canvas<'buf> {
assert!(stride.is_multiple_of(4), "stride must be a multiple of 4");
assert!(stride >= width * 4, "stride too small for width");
assert!(
buf.len() >= stride as usize * height as usize,
"buffer too small"
);
assert!(
buf.as_ptr().addr().is_multiple_of(4),
"buffer must be 4-byte aligned"
);
let image = unsafe {
ffi::pixman_image_create_bits_no_clear(
ffi::pixman_format_code_t_PIXMAN_x8r8g8b8,
i32::try_from(width).expect("width should fit in i32"),
i32::try_from(height).expect("height should fit in i32"),
buf.as_mut_ptr().cast(),
i32::try_from(stride).expect("stride should fit in i32"),
)
};
assert!(!image.is_null(), "pixman_image_create_bits_no_clear failed");
Canvas {
image,
_buf: PhantomData,
}
}
pub fn draw(
&mut self,
run: &TextRun,
pen_x: i32,
baseline_y: i32,
colour: Color,
) -> (i32, i32) {
let solid = unsafe { ffi::pixman_image_create_solid_fill(&colour.to_pixman()) };
assert!(!solid.is_null(), "pixman_image_create_solid_fill failed");
let (mut x, mut y) = (pen_x, baseline_y);
for ShapedGlyph { glyph, advance } in run.glyphs() {
self.composite(&glyph, solid, x, y);
x += advance.0;
y += advance.1;
}
unsafe { ffi::pixman_image_unref(solid) };
(x, y)
}
pub fn draw_glyph(&mut self, glyph: &Glyph, pen_x: i32, baseline_y: i32, colour: Color) {
let solid = unsafe { ffi::pixman_image_create_solid_fill(&colour.to_pixman()) };
assert!(!solid.is_null(), "pixman_image_create_solid_fill failed");
self.composite(glyph, solid, pen_x, baseline_y);
unsafe { ffi::pixman_image_unref(solid) };
}
fn composite(
&mut self,
glyph: &Glyph,
solid: *mut ffi::pixman_image_t,
pen_x: i32,
baseline_y: i32,
) {
let g = glyph.as_raw();
unsafe {
if g.is_color_glyph {
ffi::pixman_image_composite32(
ffi::pixman_op_t_PIXMAN_OP_OVER,
g.pix,
std::ptr::null_mut(),
self.image,
0,
0,
0,
0,
pen_x + g.x,
baseline_y - g.y,
g.width,
g.height,
);
} else {
ffi::pixman_image_composite32(
ffi::pixman_op_t_PIXMAN_OP_OVER,
solid,
g.pix,
self.image,
0,
0,
0,
0,
pen_x + g.x,
baseline_y - g.y,
g.width,
g.height,
);
}
}
}
pub fn pixman_image(&mut self) -> *mut ffi::pixman_image_t {
self.image
}
}
impl Drop for Canvas<'_> {
fn drop(&mut self) {
unsafe { ffi::pixman_image_unref(self.image) };
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn char_fallback_matches_shaped_glyph_count() {
let font = Font::new(&["Open Sans:pixelsize=24"], None).unwrap();
let run = font.shape_chars("Hello 123", Subpixel::None);
assert_eq!(run.len(), 9);
assert!(run.width() > 0);
for g in run.glyphs() {
assert!(g.advance.0 > 0);
}
let shaped = font.shape("Hello 123", Subpixel::None);
let diff = (run.width() - shaped.width()).abs();
assert!(
diff <= i32::try_from(run.len()).unwrap(),
"fallback width {} vs shaped {}",
run.width(),
shaped.width()
);
}
#[test]
fn empty_text_yields_empty_run() {
let font = Font::new(&["Open Sans:pixelsize=24"], None).unwrap();
let run = font.shape("", Subpixel::None);
assert!(run.is_empty());
assert_eq!(run.width(), 0);
}
}