///|
/// Layer compositing: Porter-Duff source-over with the common separable
/// blend modes, in pure integer math (channel values and alphas both live
/// in 0..255, divisions by 255 are rounded).

///|
/// Separable blend modes applied per channel before alpha compositing.
pub(all) enum BlendMode {
  Normal
  Multiply
  Screen
  Overlay
  Darken
  Lighten
  Difference
  Add
}

///|
/// Rounded division by 255, the workhorse of 8-bit compositing.
fn div255(v : Int) -> Int {
  (v + 127) / 255
}

///|
/// B(cb, cs) for one channel: backdrop and source in, blended value out.
fn blend_channel(mode : BlendMode, cb : Int, cs : Int) -> Int {
  match mode {
    Normal => cs
    Multiply => div255(cb * cs)
    Screen => 255 - div255((255 - cb) * (255 - cs))
    Overlay =>
      if cb < 128 {
        div255(2 * cb * cs)
      } else {
        255 - div255(2 * (255 - cb) * (255 - cs))
      }
    Darken => if cb < cs { cb } else { cs }
    Lighten => if cb > cs { cb } else { cs }
    Difference => iabs(cb - cs)
    Add => if cb + cs > 255 { 255 } else { cb + cs }
  }
}

///|
/// Composites `top` over `self` (source-over) with the given blend mode.
/// Both images must have identical dimensions (aborts otherwise).
///
/// Per pixel, following the W3C compositing model:
/// the source color is first mixed toward `B(cb, cs)` by the backdrop
/// alpha, then source-over composited:
/// `ao = as + ab*(1-as)` and `co = (as*cs' + (1-as)*ab*cb) / ao`.
pub fn Image::composite(self : Image, top : Image, mode : BlendMode) -> Image {
  if self.width != top.width || self.height != top.height {
    abort("Image::composite: images must have identical dimensions")
  }
  let out = Image::new(self.width, self.height)
  for p in 0..