Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/image-premultiply-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'tauri-cli': 'patch:bug'
'@tauri-apps/cli': 'patch:bug'
---

Premultiply Alpha before Resizing which gets rid of the gray fringe around the icons for svg images.
80 changes: 46 additions & 34 deletions crates/tauri-cli/src/icon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
};

use std::{
borrow::Cow,
collections::HashMap,
fs::{create_dir_all, File},
io::{BufWriter, Write},
Expand Down Expand Up @@ -124,7 +125,7 @@ impl Source {
}
}

fn resize_exact(&self, size: u32) -> Result<DynamicImage> {
fn resize_exact(&self, size: u32) -> DynamicImage {
match self {
Self::Svg(svg) => {
let mut pixmap = tiny_skia::Pixmap::new(size, size).unwrap();
Expand All @@ -134,39 +135,49 @@ impl Source {
tiny_skia::Transform::from_scale(scale, scale),
&mut pixmap.as_mut(),
);
let img_buffer = ImageBuffer::from_raw(size, size, pixmap.take()).unwrap();
Ok(DynamicImage::ImageRgba8(img_buffer))
// Switch to use `Pixmap::take_demultiplied` in the future when it's published
// https://github.com/linebender/tiny-skia/blob/624257c0feb394bf6c4d0d688f8ea8030aae320f/src/pixmap.rs#L266
let img_buffer = ImageBuffer::from_par_fn(size, size, |x, y| {
let pixel = pixmap.pixel(x, y).unwrap().demultiply();
Rgba([pixel.red(), pixel.green(), pixel.blue(), pixel.alpha()])
});
DynamicImage::ImageRgba8(img_buffer)
Comment on lines +138 to +144
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So skia by default has all pixels multiplied (whatever that means) so we demultiply all pixels to then be able to multiple the alpha channel specifically later in the same resize function that also multiplies the png alpha channel?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like that tiny-skia does this, the documentation isn't clear here to be honest, but from the code and how they return &[PremultipliedColorU8], it looks like it (Skia marks this in SkPixmap, SkImageInfo, so it's a different story https://api.skia.org/SkAlphaType_8h.html#ad2e3e94ae1ad3b28c96802b77514ab45)

multiplied (whatever that means)

It's used for calculating filtering internally easier, but when we want the final value, we need to undo that

to then be able to multiple the alpha channel specifically later in the same resize function that also multiplies the png alpha channel

This can be optimized but I honestly don't care enough to do that 😂

}
Self::DynamicImage(image) => {
// `image` does not use premultiplied alpha in resize, so we do it manually here,
// see https://github.com/image-rs/image/issues/1655
//
// image.resize_exact(size, size, FilterType::Lanczos3)

// Premultiply alpha
let premultiplied_image =
ImageBuffer::from_par_fn(image.width(), image.height(), |x, y| {
let mut pixel = image.get_pixel(x, y);
let alpha = pixel.0[3] as f32 / u8::MAX as f32;
pixel.apply_without_alpha(|channel_value| (channel_value as f32 * alpha) as u8);
pixel
});

let mut resized =
image::imageops::resize(&premultiplied_image, size, size, FilterType::Lanczos3);

// Unmultiply alpha
resized.par_pixels_mut().for_each(|pixel| {
let alpha = pixel.0[3] as f32 / u8::MAX as f32;
pixel.apply_without_alpha(|channel_value| (channel_value as f32 / alpha) as u8);
});

Ok(DynamicImage::ImageRgba8(resized))
resize_image(image, size, size)
}
}
}
}

// `image` does not use premultiplied alpha in resize, so we do it manually here,
// see https://github.com/image-rs/image/issues/1655
fn resize_image(image: &DynamicImage, new_width: u32, new_height: u32) -> DynamicImage {
// Premultiply alpha
let premultiplied_image = ImageBuffer::from_par_fn(image.width(), image.height(), |x, y| {
let mut pixel = image.get_pixel(x, y);
let alpha = pixel.0[3] as f32 / u8::MAX as f32;
pixel.apply_without_alpha(|channel_value| (channel_value as f32 * alpha) as u8);
pixel
});

let mut resized = image::imageops::resize(
&premultiplied_image,
new_width,
new_height,
FilterType::Lanczos3,
);

// Demultiply alpha
resized.par_pixels_mut().for_each(|pixel| {
let alpha = pixel.0[3] as f32 / u8::MAX as f32;
pixel.apply_without_alpha(|channel_value| (channel_value as f32 / alpha) as u8);
});

DynamicImage::ImageRgba8(resized)
}

fn read_source(path: PathBuf) -> Result<Source> {
if let Some(extension) = path.extension() {
if extension == "svg" {
Expand All @@ -183,7 +194,7 @@ fn read_source(path: PathBuf) -> Result<Source> {
..Default::default()
};

let svg_data = std::fs::read(&path).unwrap();
let svg_data = std::fs::read(&path).fs_context("Failed to read source icon", &path)?;
usvg::Tree::from_data(&svg_data, &opt).unwrap()
};

Expand Down Expand Up @@ -329,7 +340,7 @@ fn icns(source: &Source, out_dir: &Path) -> Result<()> {
let size = entry.size;
let mut buf = Vec::new();

let image = source.resize_exact(size)?;
let image = source.resize_exact(size);

write_png(image.as_bytes(), &mut buf, size).context("failed to write output file")?;

Expand Down Expand Up @@ -364,7 +375,7 @@ fn ico(source: &Source, out_dir: &Path) -> Result<()> {
let mut frames = Vec::new();

for size in [32, 16, 24, 48, 64, 256] {
let image = source.resize_exact(size)?;
let image = source.resize_exact(size);

// Only the 256px layer can be compressed according to the ico specs.
if size == 256 {
Expand Down Expand Up @@ -795,7 +806,7 @@ fn resize_png(
bg: Option<Background>,
scale_percent: Option<f32>,
) -> Result<DynamicImage> {
let mut image = source.resize_exact(size)?;
let mut image = source.resize_exact(size);

match bg {
Some(Background::Color(bg_color)) => {
Expand All @@ -809,7 +820,7 @@ fn resize_png(
image = bg_img.into();
}
Some(Background::Image(bg_source)) => {
let mut bg = bg_source.resize_exact(size)?;
let mut bg = bg_source.resize_exact(size);

let fg = scale_percent
.map(|scale| resize_asset(&image, size, scale))
Expand Down Expand Up @@ -889,9 +900,10 @@ fn content_bounds(img: &DynamicImage) -> Option<(u32, u32, u32, u32)> {

fn resize_asset(img: &DynamicImage, target_size: u32, scale_percent: f32) -> DynamicImage {
let cropped = if let Some((x, y, cw, ch)) = content_bounds(img) {
img.crop_imm(x, y, cw, ch)
// TODO: Use `&` here instead when we raise MSRV to above 1.79
Cow::Owned(img.crop_imm(x, y, cw, ch))
} else {
img.clone()
Cow::Borrowed(img)
};

let (cw, ch) = cropped.dimensions();
Expand All @@ -901,7 +913,7 @@ fn resize_asset(img: &DynamicImage, target_size: u32, scale_percent: f32) -> Dyn
let new_w = (cw as f32 * scale).round() as u32;
let new_h = (ch as f32 * scale).round() as u32;

let resized = image::imageops::resize(&cropped, new_w, new_h, image::imageops::Lanczos3);
let resized = resize_image(&cropped, new_w, new_h);

// Place on transparent square canvas
let mut canvas = ImageBuffer::from_pixel(target_size, target_size, Rgba([0, 0, 0, 0]));
Expand Down
Loading