tikz-gallery-generator

Custum build of stapix for tikz.pablopie.xyz

Commit
c8c8976a20630694e2fd2f88904e7bec022b6523
Parent
4193a04e494e6fb5203a986666667d767b683839
Author
Pablo <pablo-pie@riseup.net>
Date

Improved dowmsampling of thumbnails

We now use linear interpolation instead of nearest-neighbord

Diffstats

2 files changed, 310 insertions, 190 deletions

Status Name Changes Insertions Deletions
Modified .gitignore 2 files changed 1 0
Modified src/image.rs 2 files changed 309 190
diff --git a/.gitignore b/.gitignore
@@ -1,4 +1,5 @@
 /target
 /examples/site
 .project.gf
+profile/
 tags
diff --git a/src/image.rs b/src/image.rs
@@ -14,6 +14,11 @@
 //! feed the YUV pixel data directly to `libwebp` instead of first converting
 //! it to  and then converting it back to YUV (which is what `image` would
 //! have done).
+//!
+//! ## Possible improvements
+//!
+//! * SIMD optimizations           (doesn't seem necessary for now)
+//! * use the GPU for downsampling (doesn't seem worth the coplexity/overhead)
 #![allow(clippy::identity_op)]
 #![allow(clippy::needless_range_loop)]
 
@@ -59,7 +64,6 @@ enum PixelBuffer {
 
 /// Parses the Y Cb Cr data from a JPEG, downsampling to fit `TARGET_HEIGHT`
 /// along the way
-// NOTE: using an approximation of nearest neighbor for efficiency
 pub fn parse_jpeg(path: &Path, target_height: usize) -> Result<Image, ()> {
   let options = DecoderOptions::default()
     .jpeg_set_out_colorspace(ColorSpace::YCbCr);
@@ -81,9 +85,10 @@ pub fn parse_jpeg(path: &Path, target_height: usize) -> Result {
 
   let src_width  = info.width  as usize;
   let src_height = info.height as usize;
+  let target_width = src_width * target_height / src_height;
 
   let (width, height) = if src_height > target_height {
-    (src_width * target_height / src_height, target_height)
+    (target_width, target_height)
   } else {
     (src_width, src_height)
   };
@@ -92,49 +97,32 @@ pub fn parse_jpeg(path: &Path, target_height: usize) -> Result {
   let uv_height = height.div_ceil(2);
 
   assert_expected_pixels_len(src_width, src_height, 3, pixels.len());
-  let y_len  = width * height;
-  let uv_len = uv_width * uv_height;
 
   let colorspace = options.jpeg_get_out_colorspace();
   debug_assert!(colorspace == ColorSpace::YCbCr,
                 "unexpected colorspace when parsing JPEG: {colorspace:?}");
 
   // ==========================================================================
-  let mut y  = vec![MaybeUninit::uninit(); y_len];
-  let mut cb = vec![MaybeUninit::uninit(); uv_len];
-  let mut cr = vec![MaybeUninit::uninit(); uv_len];
-
-  if height == target_height {
-    // TODO: [optmize]: can we use SIMD here?
-    for dst_i in 0..y_len {
-      let dst_x = dst_i % width;
-      let dst_y = dst_i / width;
-
-      let src_x = dst_x *  src_width / width;
-      let src_y = dst_y * src_height / height;
-
-      let src_i = src_y * src_width + src_x;
-      y[dst_i] = MaybeUninit::new(pixels[src_i*3]);
-    }
-  } else {
-    for i in 0..y_len { y[i] = MaybeUninit::new(pixels[i*3]); }
-  }
-
-  // TODO: [optmize]: can we use SIMD here?
-  for dst_i in 0..uv_len {
-    let dst_x = dst_i % uv_width;
-    let dst_y = dst_i / uv_width;
-
-    let src_x = dst_x * src_width  / uv_width;
-    let src_y = dst_y * src_height / uv_height;
+  let [y] = unsafe {
+    load_and_downsample_channels_linear_checked::<1>(
+      &pixels,
+      src_width, src_height,
+      3,
+      target_width, target_height,
+    )
+  };
 
-    let src_i = src_y * src_width + src_x;
-    cb[dst_i] = MaybeUninit::new(pixels[src_i*3+1]);
-    cr[dst_i] = MaybeUninit::new(pixels[src_i*3+2]);
-  }
+  let [cb, cr] = unsafe {
+    load_and_downsample_channels_linear::<2>(
+      &pixels,
+      src_width, src_height,
+      3, 1,
+      uv_width, uv_height,
+    )
+  };
 
   // ==========================================================================
-  let y  = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(y) };
+  let y  = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(y)  };
   let cb = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(cb) };
   let cr = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(cr) };
 
@@ -181,9 +169,10 @@ pub fn parse_png(path: &Path, target_height: usize) -> Result {
   let (src_width, src_height) = decoder
     .dimensions()
     .expect("should already have decoded PNG headers");
+  let target_width = src_width * target_height / src_height;
 
-  let (width, height) = if src_height > target_height {
-    (src_width * target_height / src_height, target_height)
+  let (width, height) = if src_height >= target_height {
+    (target_width, target_height)
   } else {
     (src_width, src_height)
   };
@@ -213,178 +202,81 @@ pub fn parse_png(path: &Path, target_height: usize) -> Result {
   assert_expected_pixels_len(src_width, src_height, bps, pixels.len());
 
   // ==========================================================================
-  // handle grayscale input
-
-  struct GrayscaleData {
-    y: Vec<u8>,
-    a: Option<Vec<u8>>,
-    uv_stride: usize,
-    uv_len:    usize,
-  }
-
-  #[inline]
-  #[allow(clippy::too_many_arguments)]
-  fn parse_ya_channels(
-    width:     usize, height:     usize,
-    src_width: usize, src_height: usize,
-    pixels: Vec<u8>,
-    target_height: usize,
-    bps: usize, has_alpha: bool,
-  ) -> GrayscaleData {
-    let uv_width  = width.div_ceil(2);
-    let uv_height = height.div_ceil(2);
-    let y_len  =    width * height;
-    let uv_len = uv_width * uv_height;
-
-    if !has_alpha && height < target_height {
-      return GrayscaleData {
-        y: pixels,
-        a: None,
-        uv_stride: uv_width,
-        uv_len,
-      };
-    }
-
-    if has_alpha && height < target_height {
-      let mut y = vec![MaybeUninit::uninit(); y_len];
-      let mut a = vec![MaybeUninit::uninit(); y_len];
+  // handle RGB/RGBA input
 
-      // TODO: [optimize]: can we optmize this?
-      for i in 0..y_len {
-        y[i] = MaybeUninit::new(pixels[i*2+0]);
-        a[i] = MaybeUninit::new(pixels[i*2+1]);
+  if !is_grayscale {
+    let bgra_data = if src_height < target_height {
+      unsafe { load_bgra_from_rgba(&pixels, src_width, src_height, has_alpha) }
+    } else {
+      unsafe {
+        load_and_downsample_bgra_from_rgba(
+          &pixels,
+          src_width,    src_height,
+          has_alpha,
+          target_width, target_height,
+        )
       }
+    };
 
-      let y = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(y) };
-      let a = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(a) };
-      return GrayscaleData { y, a: Some(a), uv_stride: uv_width, uv_len, };
-    }
-
-    let mut y = vec![MaybeUninit::uninit(); y_len];
-    for dst_i in 0..y_len {
-      let dst_x = dst_i % width;
-      let dst_y = dst_i / width;
-
-      let src_x = dst_x * src_width  / width;
-      let src_y = dst_y * src_height / height;
+    return Ok(Image { width, height, pixels: PixelBuffer::BGRA(bgra_data), });
+  }
 
-      let src_i = src_y * src_width + src_x;
-      y[dst_i] = MaybeUninit::new(pixels[src_i*bps]);
-    }
-    let y = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(y) };
+  // ==========================================================================
+  // handle grayscale input
 
-    if !has_alpha {
-      return GrayscaleData { y, a: None, uv_stride: uv_width, uv_len, };
-    }
+  let uv_width  = width.div_ceil(2);
+  let uv_height = height.div_ceil(2);
+  let uv_stride = uv_width;
+  let uv_len = uv_width * uv_height;
 
-    let mut a = vec![MaybeUninit::uninit(); y_len];
-    for dst_i in 0..y_len {
-      let dst_x = dst_i % width;
-      let dst_y = dst_i / width;
+  let cb = vec![128; uv_len];
+  let cr = vec![128; uv_len];
 
-      let src_x = dst_x * src_width  / width;
-      let src_y = dst_y * src_height / height;
+  if !has_alpha {
+    if src_height < target_height {
+      return Ok(Image {
+        width,
+        height,
 
-      let src_i = src_y * src_width + src_x;
-      a[dst_i] = MaybeUninit::new(pixels[src_i*2+1]);
+        pixels: PixelBuffer::YCbCr { y: pixels, cb, cr, a: None, uv_stride, }
+      });
     }
-    let a = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(a) };
-
-    GrayscaleData { y, a: Some(a), uv_stride: uv_width, uv_len, }
-  }
 
-  if is_grayscale {
-    let GrayscaleData { y, a, uv_stride, uv_len, } = parse_ya_channels(
-      width,     height,
-      src_width, src_height,
-      pixels,
-      target_height,
-      bps, has_alpha,
-    );
+    let [y] = unsafe {
+      load_and_downsample_channels_linear::<1>(
+        &pixels,
+        src_width,    src_height,
+        1, 0,
+        target_width, target_height,
+      )
+    };
+    let y = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(y) };
 
     return Ok(Image {
       width,
       height,
 
-      pixels: PixelBuffer::YCbCr {
-        y, a, cb: vec![128; uv_len], cr: vec![128; uv_len],
-        uv_stride,
-      },
+      pixels: PixelBuffer::YCbCr {y, a: None, cb, cr, uv_stride, }
     });
   }
 
-  // ==========================================================================
-  // handle RGB/RGBA input
-
-  #[inline]
-  #[allow(clippy::too_many_arguments)]
-  fn parse_bgra_channels(
-    width:     usize, height:     usize,
-    src_width: usize, src_height: usize,
-    pixels: &[u8],
-    target_height: usize,
-    bps: usize, has_alpha: bool,
-  ) -> Vec<MaybeUninit<u8>> {
-    let len = width * height;
-    let mut bgra_data = vec![MaybeUninit::uninit(); len * bps];
-
-    if height < target_height {
-      for i in 0..len {
-        bgra_data[i*bps+0] = MaybeUninit::new(pixels[i*bps+2]);
-        bgra_data[i*bps+1] = MaybeUninit::new(pixels[i*bps+1]);
-        bgra_data[i*bps+2] = MaybeUninit::new(pixels[i*bps+0]);
-      }
-
-      if has_alpha {
-        for i in 0..len {
-          bgra_data[i*4+3] = MaybeUninit::new(pixels[i*4+3]);
-        }
-      }
-
-      return bgra_data;
-    }
-
-    for dst_i in 0..len {
-      let dst_x = dst_i % width;
-      let dst_y = dst_i / width;
-
-      let src_x = dst_x * src_width  / width;
-      let src_y = dst_y * src_height / height;
-
-      let src_i = src_y * src_width + src_x;
-      bgra_data[dst_i*bps+0] = MaybeUninit::new(pixels[src_i*bps+2]);
-      bgra_data[dst_i*bps+1] = MaybeUninit::new(pixels[src_i*bps+1]);
-      bgra_data[dst_i*bps+2] = MaybeUninit::new(pixels[src_i*bps+0]);
-    }
-
-    if has_alpha {
-      for dst_i in 0..len {
-        let dst_x = dst_i % width;
-        let dst_y = dst_i / width;
-
-        let src_x = dst_x * src_width  / width;
-        let src_y = dst_y * src_height / height;
-
-        let src_i = src_y * src_width + src_x;
-        bgra_data[dst_i*4+3] = MaybeUninit::new(pixels[src_i*4+3]);
-      }
-    }
-
-    bgra_data
-  }
-  
-  let bgra_data = parse_bgra_channels(
-    width,     height,
-    src_width, src_height,
-    &pixels,
-    target_height,
-    bps, has_alpha,
-  );
-  let bgra_data = unsafe {
-    mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(bgra_data)
+  let [y, a] = unsafe {
+    load_and_downsample_channels_linear_checked::<2>(
+      &pixels,
+      src_width,    src_height,
+      2,
+      target_width, target_height,
+    )
   };
+  let y = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(y) };
+  let a = unsafe { mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(a) };
+
+  Ok(Image {
+    width,
+    height,
 
-  Ok(Image { width, height, pixels: PixelBuffer::BGRA(bgra_data), })
+    pixels: PixelBuffer::YCbCr { y, a: Some(a), cb, cr, uv_stride, },
+  })
 }
 
 pub fn encode_webp(img: &Image, output_path: &Path) -> Result<(), ()> {
@@ -477,6 +369,233 @@ pub fn encode_webp(img: &Image, output_path: &Path) -> Result<(), ()> {
 }
 
 #[inline]
+unsafe fn load_and_downsample_channels_linear<const CHANNELS: usize>(
+  pixels: &[u8],
+  src_width: usize, src_height: usize,
+  bps: usize, first_channel_offset: usize,
+  target_width: usize, target_height: usize,
+) -> [Vec<MaybeUninit<u8>>; CHANNELS] {
+  debug_assert!(CHANNELS > 0);
+
+  let mut result: [Vec<MaybeUninit<u8>>; CHANNELS] = {
+    [const { Vec::new() }; CHANNELS]
+  };
+
+  for j in 0..CHANNELS {
+    result[j].resize(target_width * target_height, MaybeUninit::uninit());
+  }
+
+  let mut dst_i = 0;
+  for dst_y in 0..target_height {
+    let bottom = (      dst_y * src_height).div_ceil(target_height);
+    let top    = ((dst_y + 1) * src_height).div_ceil(target_height);
+
+    for dst_x in 0..target_width {
+      let left  = (      dst_x * src_width).div_ceil(target_width);
+      let right = ((dst_x + 1) * src_width).div_ceil(target_width);
+
+      // TODO: handle this
+      assert!(top != bottom && left != right);
+      let area: u16 = ((right - left)*(top - bottom)) as u16;
+
+      let mut acc: [u16; CHANNELS] = [0; CHANNELS];
+      let mut i = bottom * src_width + left;
+      for _ in bottom..top {
+        for _ in left..right {
+          for j in 0..CHANNELS {
+            acc[j] += pixels[i*bps+first_channel_offset+j] as u16;
+          }
+          i += 1;
+        }
+        i += src_width + left;
+        i -= right;
+      }
+
+      for j in 0..CHANNELS {
+        result[j][dst_i] = MaybeUninit::new((acc[j]/area) as u8);
+      }
+
+      dst_i += 1;
+    }
+  }
+
+  result
+}
+
+#[inline]
+unsafe fn load_channels<const CHANNELS: usize>(
+  pixels: &[u8],
+  src_width: usize, src_height: usize,
+  bps: usize,
+) -> [Vec<MaybeUninit<u8>>; CHANNELS] {
+  debug_assert!(CHANNELS > 0);
+
+  let mut result = [const { Vec::new() }; CHANNELS];
+
+  for j in 0..CHANNELS {
+    result[j].resize(src_width * src_height, MaybeUninit::uninit());
+  }
+
+  for j in 0..CHANNELS {
+    for i in 0..src_width*src_height {
+      result[j][i] = MaybeUninit::new(pixels[i*bps+j]);
+    }
+  }
+
+  result
+}
+
+#[inline]
+unsafe fn load_and_downsample_channels_linear_checked<const CHANNELS: usize>(
+  pixels: &[u8],
+  src_width: usize, src_height: usize,
+  bps: usize,
+  target_width: usize, target_height: usize,
+) -> [Vec<MaybeUninit<u8>>; CHANNELS] {
+  if src_height < target_height {
+    load_channels::<CHANNELS>(pixels, src_width, src_height, bps)
+  } else {
+    load_and_downsample_channels_linear::<CHANNELS>(
+      pixels,
+      src_width, src_height,
+      bps, 0,
+      target_width, target_height,
+    )
+  }
+}
+
+#[inline]
+unsafe fn load_and_downsample_bgra_from_rgba(
+  pixels: &[u8],
+  src_width: usize, src_height: usize,
+  has_alpha: bool,
+  target_width: usize, target_height: usize,
+) -> Vec<u8> {
+  if has_alpha {
+    return load_and_downsample_bgra_from_rgba_internal::<true>(
+      pixels,
+      src_width,    src_height,
+      target_width, target_height,
+    );
+  } else {
+    return load_and_downsample_bgra_from_rgba_internal::<false>(
+      pixels,
+      src_width,    src_height,
+      target_width, target_height,
+    );
+  }
+
+  #[inline]
+  unsafe fn load_and_downsample_bgra_from_rgba_internal<const HAS_ALPHA: bool>(
+    pixels: &[u8],
+    src_width:    usize, src_height:    usize,
+    target_width: usize, target_height: usize,
+  ) -> Vec<u8> {
+    let bps = if HAS_ALPHA { 4 } else { 3 };
+
+    let len = target_width * target_height;
+    let mut result = vec![MaybeUninit::uninit(); len * 4];
+
+    let mut dst_i = 0;
+    for dst_y in 0..target_height {
+      let bottom = (      dst_y * src_height).div_ceil(target_height);
+      let top    = ((dst_y + 1) * src_height).div_ceil(target_height);
+
+      for dst_x in 0..target_width {
+        let left  = (      dst_x * src_width).div_ceil(target_width);
+        let right = ((dst_x + 1) * src_width).div_ceil(target_width);
+
+        // TODO: handle this
+        assert!(top != bottom && left != right);
+
+        let area: u16 = ((right - left)*(top - bottom)) as u16;
+
+        #[allow(unused_variables)]
+        let mut acc_a: u16 = 0;
+        let mut acc_r: u16 = 0;
+        let mut acc_g: u16 = 0;
+        let mut acc_b: u16 = 0;
+
+        let mut i = bottom * src_width + left;
+        for _ in bottom..top {
+          for _ in left..right {
+            acc_r += pixels[i*bps+0] as u16;
+            acc_g += pixels[i*bps+1] as u16;
+            acc_b += pixels[i*bps+2] as u16;
+            // NOTE: compile time branches should never branch at runtime
+            #[allow(unused_assignments)]
+            if HAS_ALPHA { acc_a += pixels[i*bps+3] as u16; }
+            i += 1;
+          }
+          i += src_width + left;
+          i -= right;
+        }
+
+        result[dst_i*4+0] = MaybeUninit::new((acc_b/area) as u8);
+        result[dst_i*4+1] = MaybeUninit::new((acc_g/area) as u8);
+        result[dst_i*4+2] = MaybeUninit::new((acc_r/area) as u8);
+
+        // NOTE: compile time branches should never branch at runtime
+        if HAS_ALPHA {
+          result[dst_i*4+3] = MaybeUninit::new((acc_a/area) as u8);
+        } else {
+          result[dst_i*4+3] = MaybeUninit::new(0xff);
+        }
+
+        dst_i += 1;
+      }
+    }
+
+    mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(result)
+  }
+}
+
+#[inline]
+unsafe fn load_bgra_from_rgba(
+  pixels: &[u8],
+  src_width: usize, src_height: usize,
+  has_alpha: bool,
+) -> Vec<u8> {
+  if has_alpha {
+    return load_bgra_from_rgba_internal::<true>(
+      pixels, src_width, src_height,
+    );
+  } else {
+    return load_bgra_from_rgba_internal::<false>(
+      pixels, src_width, src_height,
+    );
+  }
+
+  #[inline]
+  unsafe fn load_bgra_from_rgba_internal<const HAS_ALPHA: bool>(
+    pixels: &[u8],
+    src_width: usize, src_height: usize,
+  ) -> Vec<u8> {
+    let bps = if HAS_ALPHA { 4 } else { 3 };
+
+    let len = src_width * src_height;
+    let mut result = vec![MaybeUninit::uninit(); len * 4];
+
+    for i in 0..len {
+      let r = pixels[i*bps+0];
+      let g = pixels[i*bps+1];
+      let b = pixels[i*bps+2];
+
+      // NOTE: compile time branches should not actually branch at runtime
+      let mut a = 0xff;
+      if HAS_ALPHA { a = pixels[i*bps+3]; }
+
+      result[i*4+0] = MaybeUninit::new(b);
+      result[i*4+1] = MaybeUninit::new(g);
+      result[i*4+2] = MaybeUninit::new(r);
+      result[i*4+3] = MaybeUninit::new(a);
+    }
+
+    mem::transmute::<Vec<MaybeUninit<u8>>, Vec<u8>>(result)
+  }
+}
+
+#[inline]
 fn assert_expected_pixels_len(
   width: usize,
   height: usize,