use crate::cache::{subpixel_offset, subpixel_slot, CachedGlyph, GlyphCache, GlyphKey};
use crate::face::Face;
use crate::face_chain::{shear_for, FaceChain};
use crate::rasterizer::Rasterizer;
use crate::shaper::PositionedGlyph;
use crate::stroke::{dilate_alpha, dilate_offset};
use crate::style::{synthetic_bold_radius, Style};
use crate::{Error, Rgba};
#[derive(Debug, Clone, Default)]
pub struct RgbaBitmap {
pub width: u32,
pub height: u32,
pub data: Vec<u8>,
}
impl RgbaBitmap {
pub fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
data: vec![0; (width as usize) * (height as usize) * 4],
}
}
pub fn stride(&self) -> usize {
(self.width as usize) * 4
}
pub fn is_empty(&self) -> bool {
self.width == 0 || self.height == 0
}
pub fn get(&self, x: u32, y: u32) -> [u8; 4] {
if x >= self.width || y >= self.height {
return [0; 4];
}
let off = ((y as usize) * (self.width as usize) + (x as usize)) * 4;
[
self.data[off],
self.data[off + 1],
self.data[off + 2],
self.data[off + 3],
]
}
pub fn nonzero_alpha_count(&self) -> usize {
self.data.chunks_exact(4).filter(|p| p[3] != 0).count()
}
pub fn resample_bilinear(&self, dst_width: u32, dst_height: u32) -> RgbaBitmap {
if self.is_empty() || dst_width == 0 || dst_height == 0 {
return RgbaBitmap::default();
}
if dst_width == self.width && dst_height == self.height {
return self.clone();
}
let src_w = self.width as usize;
let src_h = self.height as usize;
let dw = dst_width as usize;
let dh = dst_height as usize;
let mut out = RgbaBitmap::new(dst_width, dst_height);
let sx = self.width as f32 / dst_width as f32;
let sy = self.height as f32 / dst_height as f32;
for dy in 0..dh {
let src_y = (dy as f32 + 0.5) * sy - 0.5;
let y0_f = src_y.floor();
let fy = src_y - y0_f;
let y0 = (y0_f as i32).clamp(0, src_h as i32 - 1) as usize;
let y1 = (y0_f as i32 + 1).clamp(0, src_h as i32 - 1) as usize;
for dx in 0..dw {
let src_x = (dx as f32 + 0.5) * sx - 0.5;
let x0_f = src_x.floor();
let fx = src_x - x0_f;
let x0 = (x0_f as i32).clamp(0, src_w as i32 - 1) as usize;
let x1 = (x0_f as i32 + 1).clamp(0, src_w as i32 - 1) as usize;
let off00 = (y0 * src_w + x0) * 4;
let off10 = (y0 * src_w + x1) * 4;
let off01 = (y1 * src_w + x0) * 4;
let off11 = (y1 * src_w + x1) * 4;
let dst_off = (dy * dw + dx) * 4;
let w00 = (1.0 - fx) * (1.0 - fy);
let w10 = fx * (1.0 - fy);
let w01 = (1.0 - fx) * fy;
let w11 = fx * fy;
for c in 0..4 {
let s00 = self.data[off00 + c] as f32;
let s10 = self.data[off10 + c] as f32;
let s01 = self.data[off01 + c] as f32;
let s11 = self.data[off11 + c] as f32;
let mixed = s00 * w00 + s10 * w10 + s01 * w01 + s11 * w11;
out.data[dst_off + c] = mixed.round().clamp(0.0, 255.0) as u8;
}
}
}
out
}
}
#[derive(Debug, Default)]
pub struct Composer {
cache: GlyphCache,
}
impl Composer {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(cap: usize) -> Self {
Self {
cache: GlyphCache::new(cap),
}
}
pub fn cache(&self) -> &GlyphCache {
&self.cache
}
pub fn cache_mut(&mut self) -> &mut GlyphCache {
&mut self.cache
}
#[allow(clippy::too_many_arguments)]
pub fn compose_run(
&mut self,
glyphs: &[PositionedGlyph],
face: &Face,
size_px: f32,
color: Rgba,
dst: &mut RgbaBitmap,
x: f32,
y: f32,
) -> Result<(), Error> {
let face_ref = SingleFace::Borrowed(face);
self.compose_run_inner(
glyphs,
ChainRef::Single(face_ref),
size_px,
Style::REGULAR,
color,
None,
dst,
x,
y,
)
}
#[allow(clippy::too_many_arguments)]
pub fn compose_run_styled(
&mut self,
glyphs: &[PositionedGlyph],
chain: &FaceChain,
size_px: f32,
style: Style,
color: Rgba,
dst: &mut RgbaBitmap,
x: f32,
y: f32,
) -> Result<(), Error> {
self.compose_run_inner(
glyphs,
ChainRef::Chain(chain),
size_px,
style,
color,
None,
dst,
x,
y,
)
}
#[allow(clippy::too_many_arguments)]
pub fn compose_run_with_stroke(
&mut self,
glyphs: &[PositionedGlyph],
chain: &FaceChain,
size_px: f32,
style: Style,
fill_color: Rgba,
stroke: Option<StrokeStyle>,
dst: &mut RgbaBitmap,
x: f32,
y: f32,
) -> Result<(), Error> {
if let Some(s) = stroke {
if s.width_px > 0.0 && s.color[3] > 0 {
self.compose_run_inner(
glyphs,
ChainRef::Chain(chain),
size_px,
style,
s.color,
Some(s.width_px),
dst,
x,
y,
)?;
}
}
self.compose_run_inner(
glyphs,
ChainRef::Chain(chain),
size_px,
style,
fill_color,
None,
dst,
x,
y,
)
}
#[allow(clippy::too_many_arguments)]
fn compose_run_inner(
&mut self,
glyphs: &[PositionedGlyph],
chain: ChainRef<'_>,
size_px: f32,
style: Style,
color: Rgba,
dilate_radius_px: Option<f32>,
dst: &mut RgbaBitmap,
x: f32,
y: f32,
) -> Result<(), Error> {
if dst.is_empty() || glyphs.is_empty() {
return Ok(());
}
let mut pen_x = x;
let pen_y = y;
for g in glyphs {
let face = chain.face(g.face_idx);
let shear = shear_for(face, style);
if dilate_radius_px.is_none()
&& matches!(face.kind(), crate::FaceKind::Ttf)
&& face.has_color_bitmaps()
{
if let Some(cgb) = face.raster_color_glyph_at(g.glyph_id, size_px)? {
if !cgb.bitmap.is_empty() {
let target_x = pen_x + g.x_offset;
let int_x = target_x.floor();
let glyph_x = int_x + cgb.bearing_x as f32;
let glyph_y = pen_y + g.y_offset - cgb.bearing_y as f32;
compose_color_bitmap_over(
dst,
glyph_x.round() as i32,
glyph_y.round() as i32,
&cgb.bitmap,
);
pen_x += g.x_advance;
continue;
}
}
}
let target_x = pen_x + g.x_offset;
let int_x = target_x.floor();
let frac_x = target_x - int_x;
let slot = subpixel_slot(frac_x);
let key = GlyphKey::new_subpixel(face.id(), g.glyph_id, size_px, shear, slot);
let cached = if let Some(c) = self.cache.get(&key) {
c
} else {
let sub_x = subpixel_offset(slot);
let bitmap =
Rasterizer::raster_glyph_subpixel(face, g.glyph_id, size_px, shear, sub_x)?;
let (off_x, off_y) =
Rasterizer::glyph_offset_subpixel(face, g.glyph_id, size_px, shear, sub_x)?;
let entry = CachedGlyph {
bitmap,
offset_x: off_x,
offset_y: off_y,
};
self.cache.insert(key, entry.clone());
entry
};
let bold_r = synthetic_bold_radius(style, face.weight_class(), size_px);
let total_r = dilate_radius_px.unwrap_or(0.0) + bold_r;
let (blit_bitmap, blit_dx, blit_dy) = if total_r > 0.0 {
let dil = dilate_alpha(&cached.bitmap, total_r);
let off = dilate_offset(total_r) as f32;
(dil, -off, -off)
} else {
(cached.bitmap.clone(), 0.0, 0.0)
};
let glyph_x = int_x + cached.offset_x + blit_dx;
let glyph_y = pen_y + g.y_offset + cached.offset_y + blit_dy;
if !blit_bitmap.is_empty() {
let dw = dst.width;
let dh = dst.height;
let ds = dst.stride();
oxideav_pixfmt::blit_alpha_mask(
&mut dst.data,
dw,
dh,
ds,
glyph_x.round() as i32,
glyph_y.round() as i32,
&blit_bitmap.data,
blit_bitmap.width,
blit_bitmap.height,
blit_bitmap.width as usize,
color,
);
}
pen_x += g.x_advance;
}
Ok(())
}
}
fn compose_color_bitmap_over(dst: &mut RgbaBitmap, dst_x: i32, dst_y: i32, src: &RgbaBitmap) {
if dst.is_empty() || src.is_empty() {
return;
}
let dw = dst.width as i32;
let dh = dst.height as i32;
let sw = src.width as i32;
let sh = src.height as i32;
let x0 = dst_x.max(0);
let y0 = dst_y.max(0);
let x1 = (dst_x + sw).min(dw);
let y1 = (dst_y + sh).min(dh);
if x1 <= x0 || y1 <= y0 {
return;
}
let dst_w_us = dst.width as usize;
let src_w_us = src.width as usize;
for y in y0..y1 {
let src_y = (y - dst_y) as usize;
let dst_row_off = (y as usize) * dst_w_us * 4;
let src_row_off = src_y * src_w_us * 4;
for x in x0..x1 {
let src_x = (x - dst_x) as usize;
let s_off = src_row_off + src_x * 4;
let d_off = dst_row_off + (x as usize) * 4;
let s = [
src.data[s_off],
src.data[s_off + 1],
src.data[s_off + 2],
src.data[s_off + 3],
];
if s[3] == 0 {
continue;
}
let d = [
dst.data[d_off],
dst.data[d_off + 1],
dst.data[d_off + 2],
dst.data[d_off + 3],
];
let out = oxideav_pixfmt::over_straight(s, d);
dst.data[d_off] = out[0];
dst.data[d_off + 1] = out[1];
dst.data[d_off + 2] = out[2];
dst.data[d_off + 3] = out[3];
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StrokeStyle {
pub width_px: f32,
pub color: Rgba,
}
impl StrokeStyle {
pub fn new(width_px: f32, color: Rgba) -> Self {
Self { width_px, color }
}
}
enum SingleFace<'a> {
Borrowed(&'a Face),
}
enum ChainRef<'a> {
Single(SingleFace<'a>),
Chain(&'a FaceChain),
}
impl<'a> ChainRef<'a> {
fn face(&self, idx: u16) -> &Face {
match self {
ChainRef::Single(SingleFace::Borrowed(f)) => f,
ChainRef::Chain(c) => c.face(idx),
}
}
}