Browse Source

Initial skeleton work.

Kestrel 1 month ago
commit
f7f7c23ddd
14 changed files with 950 additions and 0 deletions
  1. 3 0
      .gitignore
  2. 15 0
      Cargo.toml
  3. 180 0
      src/colour.rs
  4. 90 0
      src/data.rs
  5. 258 0
      src/formats.rs
  6. 172 0
      src/lib.rs
  7. 121 0
      src/ops.rs
  8. 0 0
      src/ops/blend.rs
  9. 28 0
      src/ops/helpers.rs
  10. 31 0
      src/ops/impls.rs
  11. BIN
      test_resources/fill_test.png
  12. 4 0
      tests/blending.rs
  13. 35 0
      tests/common/mod.rs
  14. 13 0
      tests/unary_ops.rs

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+/target
+/Cargo.lock
+.*.sw*

+ 15 - 0
Cargo.toml

@@ -0,0 +1,15 @@
+[package]
+name = "kahlo"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+euclid = { version = "0.22" }
+enum-map = { version = "2.7" }
+lazy_static = { version = "1.4" }
+bitvec = { version = "1" }
+
+[dev-dependencies]
+image = { version = "0.25.1", default-features = false, features = ["png", "jpeg"] }

+ 180 - 0
src/colour.rs

@@ -0,0 +1,180 @@
+pub type Color = Colour;
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct Colour([u8; 4]);
+
+impl Colour {
+    pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
+        Self([r, g, b, a])
+    }
+
+    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
+        Self([r, g, b, 255])
+    }
+
+    pub(crate) fn from_le_bytes(b: &[u8]) -> Self {
+        Self(b[0..4].try_into().unwrap())
+    }
+
+    pub(crate) fn as_le_bytes(&self) -> [u8; 4] {
+        self.0
+    }
+
+    pub const fn r(&self) -> u8 {
+        self.0[0]
+    }
+    pub const fn with_r(mut self, r: u8) -> Self {
+        self.0[0] = r;
+        self
+    }
+
+    pub const fn g(&self) -> u8 {
+        self.0[1]
+    }
+    pub const fn with_g(mut self, g: u8) -> Self {
+        self.0[1] = g;
+        self
+    }
+
+    pub const fn b(&self) -> u8 {
+        self.0[2]
+    }
+    pub const fn with_b(mut self, b: u8) -> Self {
+        self.0[2] = b;
+        self
+    }
+
+    pub const fn a(&self) -> u8 {
+        self.0[3]
+    }
+    pub const fn with_a(mut self, a: u8) -> Self {
+        self.0[3] = a;
+        self
+    }
+}
+
+/// Colour constants
+impl Colour {
+    pub const BLACK: Colour = Self::rgba(0, 0, 0, 255);
+    pub const WHITE: Colour = Self::rgba(255, 255, 255, 255);
+    pub const RED: Colour = Self::rgba(255, 0, 0, 255);
+    pub const GREEN: Colour = Self::rgba(0, 255, 0, 255);
+    pub const BLUE: Colour = Self::rgba(0, 0, 255, 255);
+    pub const TRANSPARENT_GREY: Colour = Self::rgba(128, 128, 128, 128);
+}
+
+/// Describes a blending mode between a backdrop and a source pixel.
+pub enum BlendMode {
+    /// Use source colour without doing any blending.
+    SourceCopy,
+    /// Use the alpha value of both the source and backdrop pixels.
+    Simple,
+    /// Only use the alpha value of the source.
+    SourceAlpha,
+    /// Only use the alpha value of the backdrop.
+    BackdropAlpha,
+    /// Multiply colour channels together.
+    Multiply,
+}
+
+impl BlendMode {
+    // computes a*alpha + b*(1-alpha)
+    const fn mix(a: u8, b: u8, alpha: u8) -> u8 {
+        (((a as u16 * alpha as u16) + (b as u16 * (255 - alpha as u16))) / 255) as u8
+    }
+
+    // computes a*alpha1 + b*alpha2*(1-alpha)
+    const fn mix2(a: u8, b: u8, alpha1: u8, alpha2: u8) -> u8 {
+        let a_term = a as u32 * alpha1 as u32 * 255;
+        let b_term = b as u32 * alpha2 as u32 * (255 - alpha1) as u32;
+        ((a_term + b_term) / (255 * 255)) as u8
+    }
+
+    pub const fn blend(&self, source: &Colour, backdrop: &Colour) -> Colour {
+        // Alpha compositing colour and alpha formulae: [1]
+        // - co = Cs x αs + Cb x αb x (1 - αs)
+        // - αo = αs + αb x (1 - αs)
+        // [1]: https://www.w3.org/TR/compositing/#simplealphacompositing
+        match self {
+            Self::SourceCopy => *source,
+            Self::Simple => Colour::rgba(
+                Self::mix2(source.r(), backdrop.r(), source.a(), backdrop.a()),
+                Self::mix2(source.g(), backdrop.g(), source.a(), backdrop.a()),
+                Self::mix2(source.b(), backdrop.b(), source.a(), backdrop.a()),
+                Self::mix(255, backdrop.a(), source.a()),
+            ),
+            Self::SourceAlpha => Colour::rgba(
+                Self::mix(source.r(), backdrop.r(), source.a()),
+                Self::mix(source.g(), backdrop.g(), source.a()),
+                Self::mix(source.b(), backdrop.b(), source.a()),
+                Self::mix(255, backdrop.a(), source.a()),
+            ),
+            Self::BackdropAlpha => Colour::rgba(
+                Self::mix(source.r(), backdrop.r(), backdrop.a()),
+                Self::mix(source.g(), backdrop.g(), backdrop.a()),
+                Self::mix(source.b(), backdrop.b(), backdrop.a()),
+                Self::mix(255, source.a(), backdrop.a()),
+            ),
+            Self::Multiply => Colour::rgba(
+                ((source.r() as u16 * backdrop.r() as u16) / 255) as u8,
+                ((source.g() as u16 * backdrop.g() as u16) / 255) as u8,
+                ((source.b() as u16 * backdrop.b() as u16) / 255) as u8,
+                ((source.a() as u16 * backdrop.a() as u16) / 255) as u8,
+            ),
+        }
+    }
+}
+
+#[test]
+fn test_simple_blending() {
+    // Opaque white over opaque black
+    assert_eq!(
+        BlendMode::Simple.blend(&Colour::WHITE, &Colour::BLACK),
+        Colour::WHITE
+    );
+
+    // Opaque black over opaque white
+    assert_eq!(
+        BlendMode::Simple.blend(&Colour::BLACK, &Colour::WHITE),
+        Colour::BLACK
+    );
+
+    // Transparent grey over opaque red
+    assert_eq!(
+        BlendMode::Simple.blend(&Colour::TRANSPARENT_GREY, &Colour::RED),
+        Colour::rgba(191, 64, 64, 255)
+    );
+
+    // Transparent grey over transparent grey
+    assert_eq!(
+        BlendMode::Simple.blend(&Colour::TRANSPARENT_GREY, &Colour::TRANSPARENT_GREY),
+        Colour::rgba(96, 96, 96, 191)
+    );
+}
+
+#[test]
+fn test_source_blending() {
+    // Opaque white over opaque black
+    assert_eq!(
+        BlendMode::SourceAlpha.blend(&Colour::WHITE, &Colour::BLACK),
+        Colour::WHITE
+    );
+
+    // Opaque black over opaque white
+    assert_eq!(
+        BlendMode::SourceAlpha.blend(&Colour::BLACK, &Colour::WHITE),
+        Colour::BLACK
+    );
+
+    // Transparent grey over opaque red
+    assert_eq!(
+        BlendMode::SourceAlpha.blend(&Colour::TRANSPARENT_GREY, &Colour::RED),
+        Colour::rgba(191, 64, 64, 255)
+    );
+
+    // Transparent grey over transparent grey
+    assert_eq!(
+        BlendMode::SourceAlpha.blend(&Colour::TRANSPARENT_GREY, &Colour::TRANSPARENT_GREY),
+        Colour::rgba(128, 128, 128, 191)
+    );
+}

+ 90 - 0
src/data.rs

@@ -0,0 +1,90 @@
+pub trait BitmapData {
+    fn data(&self) -> &[u8];
+    fn width(&self) -> usize;
+    fn height(&self) -> usize;
+    fn stride(&self) -> usize;
+}
+
+pub trait BitmapDataMut: BitmapData {
+    fn data_mut(&mut self) -> &mut [u8];
+}
+
+#[derive(PartialEq, Clone)]
+pub struct OwnedBitmapData {
+    pub(crate) data: Vec<u8>,
+    pub(crate) width: usize,
+    pub(crate) height: usize,
+    pub(crate) stride: usize,
+}
+
+impl BitmapData for OwnedBitmapData {
+    fn data(&self) -> &[u8] {
+        self.data.as_slice()
+    }
+    fn width(&self) -> usize {
+        self.width
+    }
+    fn height(&self) -> usize {
+        self.height
+    }
+    fn stride(&self) -> usize {
+        self.stride
+    }
+}
+
+impl BitmapDataMut for OwnedBitmapData {
+    fn data_mut(&mut self) -> &mut [u8] {
+        self.data.as_mut_slice()
+    }
+}
+
+#[derive(Clone)]
+pub struct BorrowedBitmapData<'l> {
+    pub(crate) data: &'l [u8],
+    pub(crate) width: usize,
+    pub(crate) height: usize,
+    pub(crate) stride: usize,
+}
+
+impl<'l> BitmapData for BorrowedBitmapData<'l> {
+    fn data(&self) -> &[u8] {
+        self.data
+    }
+    fn width(&self) -> usize {
+        self.width
+    }
+    fn height(&self) -> usize {
+        self.height
+    }
+    fn stride(&self) -> usize {
+        self.stride
+    }
+}
+
+pub struct MutableBitmapData<'l> {
+    pub(crate) data: &'l mut [u8],
+    pub(crate) width: usize,
+    pub(crate) height: usize,
+    pub(crate) stride: usize,
+}
+
+impl<'l> BitmapData for MutableBitmapData<'l> {
+    fn data(&self) -> &[u8] {
+        self.data
+    }
+    fn width(&self) -> usize {
+        self.width
+    }
+    fn height(&self) -> usize {
+        self.height
+    }
+    fn stride(&self) -> usize {
+        self.stride
+    }
+}
+
+impl<'l> BitmapDataMut for MutableBitmapData<'l> {
+    fn data_mut(&mut self) -> &mut [u8] {
+        self.data
+    }
+}

+ 258 - 0
src/formats.rs

@@ -0,0 +1,258 @@
+use bitvec::{field::BitField, order::Lsb0, view::BitView};
+
+trait Sealed {}
+
+#[derive(Clone, PartialEq, Debug, Default)]
+pub struct Mask(std::ops::Range<usize>);
+
+impl Mask {
+    const fn empty() -> Self {
+        Self(0..0)
+    }
+    const fn select_byte(index: usize) -> Self {
+        Self((index * 8)..(index * 8 + 8))
+    }
+}
+
+/*
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, enum_map::Enum)]
+pub enum DynamicPixelFormat {
+    A8,
+    Abgr32,
+    Bgr24,
+    Bgr32,
+    Rgb24,
+    Rgb32,
+    Rgba32,
+}
+
+macro_rules! masks_for {
+    ($n:ident) => {
+        ($n::RED_MASK, $n::GREEN_MASK, $n::BLUE_MASK, $n::ALPHA_MASK)
+    }
+}
+
+impl DynamicPixelFormat {
+    pub fn pixel_width(&self) -> usize {
+        match self {
+            Self::A8 => A8::PIXEL_WIDTH,
+            Self::Bgr24 => Bgr24::PIXEL_WIDTH,
+            Self::Bgr32 => Bgr32::PIXEL_WIDTH,
+            Self::Abgr32 => Abgr32::PIXEL_WIDTH,
+            Self::Rgb24 => Rgb24::PIXEL_WIDTH,
+            Self::Rgb32 => Rgb32::PIXEL_WIDTH,
+            Self::Rgba32 => Rgba32::PIXEL_WIDTH,
+        }
+    }
+
+    pub const fn masks(&self) -> (Mask, Mask, Mask, Mask) {
+        match self {
+            Self::A8 => masks_for!(A8),
+            Self::Abgr32 => masks_for!(Abgr32),
+            Self::Bgr24 => masks_for!(Bgr24),
+            Self::Bgr32 => masks_for!(Bgr32),
+            Self::Rgb24 => masks_for!(Rgb24),
+            Self::Rgb32 => masks_for!(Rgb32),
+            Self::Rgba32 => masks_for!(Rgba32),
+        }
+    }
+
+    pub fn red_mask(&self) -> Mask {
+        self.masks().0
+    }
+    pub fn green_mask(&self) -> Mask {
+        self.masks().1
+    }
+    pub fn blue_mask(&self) -> Mask {
+        self.masks().2
+    }
+    pub fn alpha_mask(&self) -> Mask {
+        self.masks().3
+    }
+
+    /*pub fn write_colour(&self, c: Colour, to: &mut [u8]) {
+        let bits = to.view_bits_mut::<Lsb0>();
+        bits[self.masks().0.0].store_be::<u8>(c.r);
+        bits[self.masks().1.0].store_be::<u8>(c.g);
+        bits[self.masks().2.0].store_be::<u8>(c.b);
+        bits[self.masks().3.0].store_be::<u8>(c.a);
+    }*/
+}
+*/
+
+#[allow(private_bounds)]
+pub trait PixelFormat: Sealed + Clone + Copy {
+    const CHANNELS: usize;
+    const PIXEL_WIDTH: usize;
+
+    const NAME: &'static str;
+
+    const RED_MASK: Mask;
+    const GREEN_MASK: Mask;
+    const BLUE_MASK: Mask;
+    const ALPHA_MASK: Mask;
+}
+
+//-----------------------------------------------------------------------------
+// Pixel formats
+//-----------------------------------------------------------------------------
+
+/// 8-bit alpha map
+#[derive(Clone, Copy, PartialEq)]
+pub struct A8;
+impl Sealed for A8 {}
+impl PixelFormat for A8 {
+    const CHANNELS: usize = 1;
+    const PIXEL_WIDTH: usize = 1;
+
+    const NAME: &'static str = "A8";
+
+    const RED_MASK: Mask = Mask::empty();
+    const GREEN_MASK: Mask = Mask::empty();
+    const BLUE_MASK: Mask = Mask::empty();
+    const ALPHA_MASK: Mask = Mask::select_byte(0);
+}
+
+/// RGBA with 8 bits per channel, stored big-endian
+///
+/// This assumes the following memory layout:
+/// ```text
+///  0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ...
+/// ---+---+---+---+---+---+---+---+---
+///  A | B | G | R | A | B | G | R | ...
+/// ```
+#[derive(Clone, Copy, PartialEq)]
+pub struct Abgr32;
+
+impl Sealed for Abgr32 {}
+impl PixelFormat for Abgr32 {
+    const CHANNELS: usize = 4;
+    const PIXEL_WIDTH: usize = 4;
+
+    const NAME: &'static str = "Abgr32";
+
+    const RED_MASK: Mask = Mask::select_byte(3);
+    const GREEN_MASK: Mask = Mask::select_byte(2);
+    const BLUE_MASK: Mask = Mask::select_byte(1);
+    const ALPHA_MASK: Mask = Mask::select_byte(0);
+}
+
+/// 24bpp colour densly packed into 24-bit words
+///
+/// This assumes the following memory layout:
+///
+/// ```text
+///  0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ...
+/// ---+---+---+---+---+---+---+---+---
+///  R | G | B | R | G | B | R | G | ...
+/// ```
+#[derive(Clone, Copy, PartialEq)]
+pub struct Bgr24;
+impl Sealed for Bgr24 {}
+impl PixelFormat for Bgr24 {
+    const CHANNELS: usize = 3;
+    const PIXEL_WIDTH: usize = 3;
+
+    const NAME: &'static str = "Bgr24";
+
+    const RED_MASK: Mask = Mask::select_byte(0);
+    const GREEN_MASK: Mask = Mask::select_byte(1);
+    const BLUE_MASK: Mask = Mask::select_byte(2);
+    const ALPHA_MASK: Mask = Mask::empty();
+}
+
+/// 24bpp colour packed into 32-bit words
+///
+/// This assumes the following memory layout:
+///
+/// ```text
+///  0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ...
+/// ---+---+---+---+---+---+---+---+---
+///  R | G | B | - | R | G | B | - | ...
+/// ```
+#[derive(Clone, Copy, PartialEq)]
+pub struct Bgr32;
+impl Sealed for Bgr32 {}
+impl PixelFormat for Bgr32 {
+    const CHANNELS: usize = 3;
+    const PIXEL_WIDTH: usize = 4;
+
+    const NAME: &'static str = "Bgr32";
+
+    const RED_MASK: Mask = Mask::select_byte(2);
+    const GREEN_MASK: Mask = Mask::select_byte(1);
+    const BLUE_MASK: Mask = Mask::select_byte(0);
+    const ALPHA_MASK: Mask = Mask::empty();
+}
+
+/// 24bpp colour densly packed into 24-bit words
+///
+/// This assumes the following memory layout:
+///
+/// ```text
+///  0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ...
+/// ---+---+---+---+---+---+---+---+---
+///  B | G | R | B | G | R | B | G | ...
+/// ```
+#[derive(Clone, Copy, PartialEq)]
+pub struct Rgb24;
+impl Sealed for Rgb24 {}
+impl PixelFormat for Rgb24 {
+    const CHANNELS: usize = 3;
+    const PIXEL_WIDTH: usize = 3;
+
+    const NAME: &'static str = "Rgb24";
+
+    const RED_MASK: Mask = Mask::select_byte(2);
+    const GREEN_MASK: Mask = Mask::select_byte(1);
+    const BLUE_MASK: Mask = Mask::select_byte(0);
+    const ALPHA_MASK: Mask = Mask::empty();
+}
+
+/// 24bpp colour packed into 32-bit words
+///
+/// This assumes the following memory layout:
+///
+/// ```text
+///  0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ...
+/// ---+---+---+---+---+---+---+---+---
+///  B | G | R | - | B | G | R | - | ...
+/// ```
+#[derive(Clone, Copy, PartialEq)]
+pub struct Rgb32;
+impl Sealed for Rgb32 {}
+impl PixelFormat for Rgb32 {
+    const CHANNELS: usize = 3;
+    const PIXEL_WIDTH: usize = 4;
+
+    const NAME: &'static str = "Rgb32";
+
+    const RED_MASK: Mask = Mask::select_byte(2);
+    const GREEN_MASK: Mask = Mask::select_byte(1);
+    const BLUE_MASK: Mask = Mask::select_byte(0);
+    const ALPHA_MASK: Mask = Mask::empty();
+}
+
+/// RGBA with 8 bits per channel, stored little-endian
+///
+/// This assumes the following memory layout:
+/// ```text
+///  0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ...
+/// ---+---+---+---+---+---+---+---+---
+///  R | G | B | A | R | G | B | A | ...
+/// ```
+#[derive(Clone, Copy, PartialEq)]
+pub struct Rgba32;
+
+impl Sealed for Rgba32 {}
+impl PixelFormat for Rgba32 {
+    const CHANNELS: usize = 4;
+    const PIXEL_WIDTH: usize = 4;
+
+    const NAME: &'static str = "Rgba32";
+
+    const RED_MASK: Mask = Mask::select_byte(0);
+    const GREEN_MASK: Mask = Mask::select_byte(1);
+    const BLUE_MASK: Mask = Mask::select_byte(2);
+    const ALPHA_MASK: Mask = Mask::select_byte(3);
+}

+ 172 - 0
src/lib.rs

@@ -0,0 +1,172 @@
+mod data;
+mod ops;
+
+#[cfg(target_endian = "big")]
+std::compile_error!("kahlo only works on little-endian targets!");
+
+pub mod colour;
+pub mod formats;
+
+pub mod prelude {
+    pub use super::ops::Paintable;
+    pub use super::{BitmapAccess, BitmapMutAccess};
+}
+
+pub mod math {
+    pub struct PixelSpace;
+    pub type PixelPoint = euclid::Point2D<i32, PixelSpace>;
+    pub type PixelSize = euclid::Size2D<i32, PixelSpace>;
+    pub type PixelRect = euclid::Rect<i32, PixelSpace>;
+    pub type PixelBox = euclid::Box2D<i32, PixelSpace>;
+    pub type PixelVector = euclid::Vector2D<i32, PixelSpace>;
+    pub type PixelSideOffsets = euclid::SideOffsets2D<i32, PixelSpace>;
+}
+
+use formats::PixelFormat;
+
+pub trait BitmapAccess<Format: PixelFormat> {
+    fn data(&self) -> &[u8];
+    fn width(&self) -> usize;
+    fn height(&self) -> usize;
+
+    fn size(&self) -> math::PixelSize {
+        math::PixelSize::new(self.width() as i32, self.height() as i32)
+    }
+
+    fn row_stride(&self) -> usize;
+}
+
+pub trait BitmapMutAccess<Format: PixelFormat>: BitmapAccess<Format> {
+    fn data_mut(&mut self) -> &mut [u8];
+}
+
+/// Owned bitmap with static pixel format information.
+#[derive(Clone, PartialEq)]
+pub struct Bitmap<Format: PixelFormat> {
+    data: data::OwnedBitmapData,
+    _ghost: std::marker::PhantomData<Format>,
+}
+
+impl<Format: PixelFormat> Bitmap<Format> {
+    pub fn new(width: usize, height: usize) -> Self {
+        Self {
+            data: data::OwnedBitmapData {
+                data: vec![0; width * height * Format::PIXEL_WIDTH],
+                width,
+                height,
+                stride: width * Format::PIXEL_WIDTH,
+            },
+            _ghost: Default::default(),
+        }
+    }
+
+    pub fn new_from_vec(data: Vec<u8>, width: usize, height: usize, stride: usize) -> Self {
+        Self {
+            data: data::OwnedBitmapData {
+                data,
+                width,
+                height,
+                stride,
+            },
+            _ghost: Default::default(),
+        }
+    }
+}
+
+impl<Format: PixelFormat> BitmapAccess<Format> for Bitmap<Format> {
+    fn data(&self) -> &[u8] {
+        &self.data.data
+    }
+    fn width(&self) -> usize {
+        self.data.width
+    }
+    fn height(&self) -> usize {
+        self.data.height
+    }
+    fn row_stride(&self) -> usize {
+        self.data.stride
+    }
+}
+
+impl<'l, Format: PixelFormat, T: BitmapAccess<Format>> BitmapAccess<Format> for &'l T {
+    fn data(&self) -> &[u8] {
+        (*self).data()
+    }
+    fn size(&self) -> math::PixelSize {
+        (*self).size()
+    }
+    fn width(&self) -> usize {
+        (*self).width()
+    }
+    fn height(&self) -> usize {
+        (*self).height()
+    }
+    fn row_stride(&self) -> usize {
+        (*self).row_stride()
+    }
+}
+
+impl<Format: PixelFormat> BitmapMutAccess<Format> for Bitmap<Format> {
+    fn data_mut(&mut self) -> &mut [u8] {
+        &mut self.data.data
+    }
+}
+
+impl<Format: PixelFormat> std::fmt::Debug for Bitmap<Format> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("Bitmap")
+            .field("width", &self.data.width)
+            .field("height", &self.data.height)
+            .field("stride", &self.data.stride)
+            .finish_non_exhaustive()
+    }
+}
+
+/// Borrowed bitmap with static pixel format information.
+#[derive(Clone)]
+pub struct BitmapRef<'l, Format: PixelFormat> {
+    data: data::BorrowedBitmapData<'l>,
+    _ghost: std::marker::PhantomData<Format>,
+}
+
+impl<'l, Format: PixelFormat> BitmapAccess<Format> for BitmapRef<'l, Format> {
+    fn data(&self) -> &[u8] {
+        &self.data.data
+    }
+    fn width(&self) -> usize {
+        self.data.width
+    }
+    fn height(&self) -> usize {
+        self.data.height
+    }
+    fn row_stride(&self) -> usize {
+        self.data.stride
+    }
+}
+
+/// Mutably borrowed bitmap with static pixel format information.
+pub struct BitmapMut<'l, Format: PixelFormat> {
+    data: data::MutableBitmapData<'l>,
+    _ghost: std::marker::PhantomData<Format>,
+}
+
+impl<'l, Format: PixelFormat> BitmapAccess<Format> for BitmapMut<'l, Format> {
+    fn data(&self) -> &[u8] {
+        &self.data.data
+    }
+    fn width(&self) -> usize {
+        self.data.width
+    }
+    fn height(&self) -> usize {
+        self.data.height
+    }
+    fn row_stride(&self) -> usize {
+        self.data.stride
+    }
+}
+
+impl<'l, Format: PixelFormat> BitmapMutAccess<Format> for BitmapMut<'l, Format> {
+    fn data_mut(&mut self) -> &mut [u8] {
+        &mut self.data.data
+    }
+}

+ 121 - 0
src/ops.rs

@@ -0,0 +1,121 @@
+use crate::{
+    colour::{BlendMode, Colour},
+    formats::{Rgba32, A8},
+    math, BitmapAccess, BitmapMutAccess, PixelFormat,
+};
+
+mod helpers;
+mod impls;
+
+pub trait Readable<PF: PixelFormat> {
+    type ReadResult;
+    fn get_pixel(&self, x: usize, y: usize) -> Self::ReadResult;
+}
+
+pub trait Writable<PF: PixelFormat>: Readable<PF> {
+    fn set_pixel(&mut self, x: usize, y: usize, to: Self::ReadResult);
+}
+
+/// RGBA32-specific painting struct.
+pub trait Paintable:
+    BitmapAccess<Rgba32> + BitmapMutAccess<Rgba32> + Writable<Rgba32, ReadResult = Colour>
+{
+    /// Fill the entire image with a given colour.
+    fn fill(&mut self, col: Colour) {
+        for y in 0..self.height() {
+            for x in 0..self.width() {
+                self.set_pixel(x, y, col);
+            }
+        }
+    }
+
+    /// Fill a region with a given colour.
+    fn fill_region(&mut self, region: &math::PixelBox, col: Colour) {
+        let image_box =
+            math::PixelBox::from_origin_and_size(math::PixelPoint::origin(), self.size());
+        let Some(isect) = region.intersection(&image_box) else {
+            // nothing to do
+            return;
+        };
+
+        for y in isect.y_range() {
+            for x in isect.x_range() {
+                self.set_pixel(x as usize, y as usize, col);
+            }
+        }
+    }
+
+    /// Fill a region with a given colour, using the values from an alphamap as the alpha value.
+    fn fill_masked(
+        &mut self,
+        amap: &impl BitmapAccess<A8>,
+        amap_region: &math::PixelBox,
+        dst: &math::PixelPoint,
+        col: Colour,
+        mode: BlendMode,
+    ) {
+        let Some((write_box, read_box)) =
+            helpers::clip_to(self.size(), *dst, amap.size(), amap_region.min, amap_region.size())
+        else {
+            return;
+        };
+
+        for y in 0..read_box.height() {
+            for x in 0..read_box.width() {
+                let rx = (read_box.min.x + x) as usize;
+                let ry = (read_box.min.y + y) as usize;
+                let wx = (write_box.min.x + x) as usize;
+                let wy = (write_box.min.y + y) as usize;
+                let source = col.with_a(amap.get_pixel(rx, ry));
+                self.set_pixel(wx, wy, mode.blend(&self.get_pixel(wx, wy), &source));
+            }
+        }
+    }
+
+    /// Copy a region of an image to here, blending colours.
+    fn blend(
+        &mut self,
+        src: &impl BitmapAccess<Rgba32>,
+        src_area: &math::PixelBox,
+        dst: &math::PixelPoint,
+        mode: BlendMode,
+    ) {
+        let Some((write_box, read_box)) =
+            helpers::clip_to(self.size(), *dst, src.size(), src_area.min, src_area.size())
+        else {
+            return;
+        };
+
+        for y in 0..read_box.height() {
+            for x in 0..read_box.width() {
+                let rx = (read_box.min.x + x) as usize;
+                let ry = (read_box.min.y + y) as usize;
+                let wx = (write_box.min.x + x) as usize;
+                let wy = (write_box.min.y + y) as usize;
+                let source = src.get_pixel(rx, ry);
+                self.set_pixel(wx, wy, mode.blend(&self.get_pixel(wx, wy), &source));
+            }
+        }
+    }
+
+    fn copy_from(
+        &mut self,
+        src: &impl BitmapAccess<Rgba32>,
+        area: &math::PixelBox,
+        dst: &math::PixelPoint,
+    ) {
+        let line_width = area.width() as usize * Rgba32::PIXEL_WIDTH;
+        for y in 0..area.height() {
+            let src_offset = y as usize * src.row_stride();
+            let dst_offset = y as usize * self.row_stride();
+
+            let src_start = src_offset + area.min.x as usize * Rgba32::PIXEL_WIDTH;
+            let dst_start = dst_offset + dst.x as usize * Rgba32::PIXEL_WIDTH;
+
+            self.data_mut()[dst_start..(dst_start + line_width)]
+                .copy_from_slice(&src.data()[src_start..(src_start + line_width)]);
+        }
+    }
+}
+
+impl<T: BitmapMutAccess<Rgba32>> Paintable for T {}

+ 0 - 0
src/ops/blend.rs


+ 28 - 0
src/ops/helpers.rs

@@ -0,0 +1,28 @@
+use crate::{formats::PixelFormat, math, BitmapAccess};
+
+pub fn clip_to(
+    img1_size: math::PixelSize,
+    pt1: math::PixelPoint,
+    img2_size: math::PixelSize,
+    pt2: math::PixelPoint,
+    sz: math::PixelSize,
+) -> Option<(math::PixelBox, math::PixelBox)> {
+    let left = (-pt1.x).max(-pt2.x).max(0);
+    let top = (-pt1.y).max(-pt2.y).max(0);
+    let right = ((sz.width + pt1.x) - img1_size.width as i32).max((sz.width + pt2.x) - img2_size.width as i32).max(0);
+    let bottom = ((sz.height + pt1.y) - img2_size.height as i32).max((sz.height + pt2.y) - img2_size.height as i32).max(0);
+
+    let adj = math::PixelSideOffsets {
+        left, right, top, bottom, ..Default::default()
+    };
+
+    Some((
+        math::PixelBox::from_origin_and_size(pt1, sz).inner_box(adj),
+        math::PixelBox::from_origin_and_size(pt2, sz).inner_box(adj),
+    ))
+}
+
+#[test]
+fn clip_test() {
+    
+}

+ 31 - 0
src/ops/impls.rs

@@ -0,0 +1,31 @@
+use super::{Readable, Writable};
+use crate::{colour::Colour, formats::*, BitmapAccess, BitmapMutAccess};
+
+impl<T: BitmapAccess<A8>> Readable<A8> for T {
+    type ReadResult = u8;
+    fn get_pixel(&self, x: usize, y: usize) -> u8 {
+        self.data()[x + (y * self.row_stride())]
+    }
+}
+
+impl<T: BitmapMutAccess<A8>> Writable<A8> for T {
+    fn set_pixel(&mut self, x: usize, y: usize, to: Self::ReadResult) {
+        let index = x + (y * self.row_stride());
+        self.data_mut()[index] = to;
+    }
+}
+
+impl<T: BitmapAccess<Rgba32>> Readable<Rgba32> for T {
+    type ReadResult = Colour;
+    fn get_pixel(&self, x: usize, y: usize) -> Colour {
+        let index = (x + (y * self.row_stride())) * Rgba32::PIXEL_WIDTH;
+        Colour::from_le_bytes(&self.data()[index..(index + 4)])
+    }
+}
+
+impl<T: BitmapMutAccess<Rgba32>> Writable<Rgba32> for T {
+    fn set_pixel(&mut self, x: usize, y: usize, to: Self::ReadResult) {
+        let index = x * Rgba32::PIXEL_WIDTH + (y * self.row_stride());
+        self.data_mut()[index..(index + 4)].copy_from_slice(&to.as_le_bytes());
+    }
+}

BIN
test_resources/fill_test.png


+ 4 - 0
tests/blending.rs

@@ -0,0 +1,4 @@
+/*#[test]
+fn colour_blending() {
+
+}*/

+ 35 - 0
tests/common/mod.rs

@@ -0,0 +1,35 @@
+use kahlo::{
+    Bitmap, BitmapAccess
+};
+
+pub fn load_test_resource(name: &'static str) -> Bitmap<kahlo::formats::Rgba32> {
+    let mut path = std::path::PathBuf::new();
+    path.push(env!("CARGO_MANIFEST_DIR"));
+    path.push("test_resources");
+    path.push(name);
+    let file_reader =
+        std::io::BufReader::new(std::fs::File::open(path).expect("couldn't open test resource"));
+    let img = image::load(file_reader, image::ImageFormat::from_path(name).unwrap())
+        .expect("couldn't parse test resource")
+        .into_rgba8();
+
+    let (w, h) = (img.width(), img.height());
+    Bitmap::new_from_vec(img.into_vec(), w as usize, h as usize, w as usize * 4)
+}
+
+pub fn save_test_result_rgba32(name: &'static str, bitmap: &impl BitmapAccess<kahlo::formats::Rgba32>) {
+    let mut path = std::path::PathBuf::new();
+    path.push(env!("CARGO_TARGET_TMPDIR"));
+    path.push(name);
+
+    assert_eq!(bitmap.row_stride(), bitmap.width() * 4);
+
+    image::save_buffer(
+        path,
+        bitmap.data(),
+        bitmap.width() as u32,
+        bitmap.height() as u32,
+        image::ExtendedColorType::Rgba8,
+    )
+    .expect("couldn't save test result");
+}

+ 13 - 0
tests/unary_ops.rs

@@ -0,0 +1,13 @@
+use kahlo::{colour::Colour, formats::Rgba32, prelude::*};
+
+mod common;
+
+#[test]
+fn fill_test() {
+    let mut generate = kahlo::Bitmap::<Rgba32>::new(16, 16);
+    generate.fill(Colour::rgba(0xaa, 0xbb, 0xcc, 0xff));
+    common::save_test_result_rgba32("fill_test.png", &generate);
+
+    let expected = common::load_test_resource("fill_test.png");
+    assert_eq!(generate, expected);
+}