use crate::types::Fixed;
use mirx::image::Region;
#[cfg(any(feature = "sdl-gpu", feature = "wgpu", test))]
use super::GlyphSurface;
#[cfg(any(feature = "sdl-gpu", feature = "wgpu", test))]
pub(crate) const fn alpha_bits(layout: mirx::image::SampleLayout) -> Option<u8> {
match layout {
mirx::image::SampleLayout::A1 => Some(1),
mirx::image::SampleLayout::A2 => Some(2),
mirx::image::SampleLayout::A4 => Some(4),
mirx::image::SampleLayout::A8 => Some(8),
_ => None,
}
}
#[cfg(any(feature = "sdl-gpu", feature = "wgpu", test))]
pub(crate) fn unpack_surface(
surface: GlyphSurface<'_>,
output: &mut alloc::vec::Vec<u8>,
) -> Option<()> {
let bits = alpha_bits(surface.sample_layout())?;
let width = usize::try_from(surface.width()).ok()?;
let height = usize::try_from(surface.height()).ok()?;
let stride = usize::try_from(surface.stride()).ok()?;
let len = width.checked_mul(height)?;
output.clear();
output.resize(len, 0);
let max = (1u16 << bits) - 1;
for y in 0..height {
let row = surface
.samples()
.get(y.checked_mul(stride)?..)?
.get(..stride)?;
for x in 0..width {
let bit = x.checked_mul(usize::from(bits))?;
let byte = *row.get(bit / 8)?;
let shift = 8 - bits - (bit % 8) as u8;
let value = u16::from((byte >> shift) & max as u8);
output[y * width + x] = ((value * 255 + max / 2) / max) as u8;
}
}
Some(())
}
#[derive(Clone, Copy)]
pub(crate) struct ScalarField<'a> {
samples: &'a [u8],
stride: u32,
region: Region,
bits: u8,
max_value: u16,
}
impl<'a> ScalarField<'a> {
pub(crate) fn new(samples: &'a [u8], stride: u32, region: Region, bits: u8) -> Option<Self> {
if !matches!(bits, 1 | 2 | 4 | 8) || region.width() == 0 || region.height() == 0 {
return None;
}
let row_bits =
u64::from(region.x().checked_add(region.width())?).checked_mul(u64::from(bits))?;
if row_bits > u64::from(stride).checked_mul(8)? {
return None;
}
let rows = u64::from(region.y().checked_add(region.height())?);
let required = rows.checked_mul(u64::from(stride))?;
if required > samples.len() as u64 {
return None;
}
Some(Self {
samples,
stride,
region,
bits,
max_value: (1u16 << bits) - 1,
})
}
pub(crate) fn sample(&self, x: i32, y: i32) -> Fixed {
Fixed::from_ratio(i32::from(self.quantized(x, y)), i32::from(self.max_value))
}
pub(crate) fn alpha(&self, x: u32, y: u32) -> u8 {
debug_assert!(x < self.region.width());
debug_assert!(y < self.region.height());
let bit = (self.region.x() + x) * u32::from(self.bits);
let row = (self.region.y() + y) as usize * self.stride as usize;
let byte = self.samples[row + (bit >> 3) as usize];
let shift = 8 - self.bits - (bit & 7) as u8;
let value = u16::from((byte >> shift) & self.max_value as u8);
((value * 255 + self.max_value / 2) / self.max_value) as u8
}
pub(crate) fn width(&self) -> u32 {
self.region.width()
}
pub(crate) fn height(&self) -> u32 {
self.region.height()
}
pub(crate) fn sample_bilinear(&self, x: Fixed, y: Fixed) -> Fixed {
self.sample_bilinear_with_gradient(x, y).0
}
pub(crate) fn sample_bilinear_with_gradient(
&self,
x: Fixed,
y: Fixed,
) -> (Fixed, Fixed, Fixed) {
let max_x = i32::try_from(self.region.width()).unwrap_or(i32::MAX) - 1;
let max_y = i32::try_from(self.region.height()).unwrap_or(i32::MAX) - 1;
let x = x.max(Fixed::ZERO).min(Fixed::from_int(max_x));
let y = y.max(Fixed::ZERO).min(Fixed::from_int(max_y));
let x0 = x.to_int();
let y0 = y.to_int();
let x1 = (x0 + 1).min(max_x);
let y1 = (y0 + 1).min(max_y);
let fx = x - Fixed::from_int(x0);
let fy = y - Fixed::from_int(y0);
let q00 = self.sample(x0, y0);
let q10 = self.sample(x1, y0);
let q01 = self.sample(x0, y1);
let q11 = self.sample(x1, y1);
let top = q00 * (Fixed::ONE - fx) + q10 * fx;
let bottom = q01 * (Fixed::ONE - fx) + q11 * fx;
let value = top * (Fixed::ONE - fy) + bottom * fy;
let dx = (q10 - q00) * (Fixed::ONE - fy) + (q11 - q01) * fy;
let dy = (q01 - q00) * (Fixed::ONE - fx) + (q11 - q10) * fx;
(value, dx, dy)
}
fn quantized(&self, x: i32, y: i32) -> u16 {
let x = x.clamp(0, self.region.width() as i32 - 1) as u64;
let y = y.clamp(0, self.region.height() as i32 - 1) as u64;
let bit = (u64::from(self.region.y()) + y) * u64::from(self.stride) * 8
+ (u64::from(self.region.x()) + x) * u64::from(self.bits);
let byte = self.samples[(bit / 8) as usize];
let shift = 8 - self.bits - (bit % 8) as u8;
u16::from((byte >> shift) & self.max_value as u8)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_msb_first_packed_values_with_row_stride() {
let field = ScalarField::new(
&[0xab, 0xcd, 0x00, 0x12, 0x34, 0x00],
3,
Region::new(1, 1, 2, 1).unwrap(),
4,
)
.unwrap();
assert_eq!(field.quantized(0, 0), 2);
assert_eq!(field.quantized(1, 0), 3);
}
#[test]
fn unpacked_surface_removes_stride_and_expands_alpha() {
let surface = GlyphSurface::new(
&[0b1010_0000, 0, 0b0100_0000, 0],
3,
2,
2,
mirx::image::SampleLayout::A1,
mirx::types::ByteAlignment::ONE,
super::super::FontSurfaceId::new(7),
)
.unwrap();
let mut output = alloc::vec::Vec::new();
unpack_surface(surface, &mut output).unwrap();
assert_eq!(output, [255, 0, 255, 0, 255, 0]);
}
#[test]
fn rejects_regions_outside_the_declared_rows() {
assert!(ScalarField::new(&[0xff], 1, Region::new(0, 0, 9, 1).unwrap(), 1).is_none());
assert!(ScalarField::new(&[0xff], 1, Region::new(0, 1, 1, 1).unwrap(), 1).is_none());
}
#[test]
fn bilinear_sampling_interpolates_between_texels() {
let field = ScalarField::new(&[0, 255], 2, Region::new(0, 0, 2, 1).unwrap(), 8).unwrap();
assert_eq!(field.sample_bilinear(Fixed::ZERO, Fixed::ZERO), Fixed::ZERO);
assert_eq!(
field.sample_bilinear(Fixed::from_ratio(1, 4), Fixed::ZERO),
Fixed::from_ratio(1, 4)
);
assert_eq!(field.sample_bilinear(Fixed::ONE, Fixed::ZERO), Fixed::ONE);
}
#[test]
fn bilinear_gradient_is_derived_from_the_same_four_samples() {
let field =
ScalarField::new(&[0, 255, 255, 255], 2, Region::new(0, 0, 2, 2).unwrap(), 8).unwrap();
assert_eq!(
field.sample_bilinear_with_gradient(Fixed::from_ratio(1, 4), Fixed::HALF),
(
Fixed::from_ratio(5, 8),
Fixed::HALF,
Fixed::from_ratio(3, 4),
)
);
}
}