Skip to content

Commit b586ecf

Browse files
fix(cli): demultiply tiny skia pixels (#14416)
* fix(cli): demultiply tiny skia pixels * Pull resize out to a function `resize_image` * Move comments as well * Use cow for older rust versions
1 parent dd70d21 commit b586ecf

File tree

2 files changed

+52
-34
lines changed

2 files changed

+52
-34
lines changed

.changes/image-premultiply-fix.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'tauri-cli': 'patch:bug'
3+
'@tauri-apps/cli': 'patch:bug'
4+
---
5+
6+
Premultiply Alpha before Resizing which gets rid of the gray fringe around the icons for svg images.

crates/tauri-cli/src/icon.rs

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::{
99
};
1010

1111
use std::{
12+
borrow::Cow,
1213
collections::HashMap,
1314
fs::{create_dir_all, File},
1415
io::{BufWriter, Write},
@@ -124,7 +125,7 @@ impl Source {
124125
}
125126
}
126127

127-
fn resize_exact(&self, size: u32) -> Result<DynamicImage> {
128+
fn resize_exact(&self, size: u32) -> DynamicImage {
128129
match self {
129130
Self::Svg(svg) => {
130131
let mut pixmap = tiny_skia::Pixmap::new(size, size).unwrap();
@@ -134,39 +135,49 @@ impl Source {
134135
tiny_skia::Transform::from_scale(scale, scale),
135136
&mut pixmap.as_mut(),
136137
);
137-
let img_buffer = ImageBuffer::from_raw(size, size, pixmap.take()).unwrap();
138-
Ok(DynamicImage::ImageRgba8(img_buffer))
138+
// Switch to use `Pixmap::take_demultiplied` in the future when it's published
139+
// https://github.com/linebender/tiny-skia/blob/624257c0feb394bf6c4d0d688f8ea8030aae320f/src/pixmap.rs#L266
140+
let img_buffer = ImageBuffer::from_par_fn(size, size, |x, y| {
141+
let pixel = pixmap.pixel(x, y).unwrap().demultiply();
142+
Rgba([pixel.red(), pixel.green(), pixel.blue(), pixel.alpha()])
143+
});
144+
DynamicImage::ImageRgba8(img_buffer)
139145
}
140146
Self::DynamicImage(image) => {
141-
// `image` does not use premultiplied alpha in resize, so we do it manually here,
142-
// see https://github.com/image-rs/image/issues/1655
143-
//
144147
// image.resize_exact(size, size, FilterType::Lanczos3)
145-
146-
// Premultiply alpha
147-
let premultiplied_image =
148-
ImageBuffer::from_par_fn(image.width(), image.height(), |x, y| {
149-
let mut pixel = image.get_pixel(x, y);
150-
let alpha = pixel.0[3] as f32 / u8::MAX as f32;
151-
pixel.apply_without_alpha(|channel_value| (channel_value as f32 * alpha) as u8);
152-
pixel
153-
});
154-
155-
let mut resized =
156-
image::imageops::resize(&premultiplied_image, size, size, FilterType::Lanczos3);
157-
158-
// Unmultiply alpha
159-
resized.par_pixels_mut().for_each(|pixel| {
160-
let alpha = pixel.0[3] as f32 / u8::MAX as f32;
161-
pixel.apply_without_alpha(|channel_value| (channel_value as f32 / alpha) as u8);
162-
});
163-
164-
Ok(DynamicImage::ImageRgba8(resized))
148+
resize_image(image, size, size)
165149
}
166150
}
167151
}
168152
}
169153

154+
// `image` does not use premultiplied alpha in resize, so we do it manually here,
155+
// see https://github.com/image-rs/image/issues/1655
156+
fn resize_image(image: &DynamicImage, new_width: u32, new_height: u32) -> DynamicImage {
157+
// Premultiply alpha
158+
let premultiplied_image = ImageBuffer::from_par_fn(image.width(), image.height(), |x, y| {
159+
let mut pixel = image.get_pixel(x, y);
160+
let alpha = pixel.0[3] as f32 / u8::MAX as f32;
161+
pixel.apply_without_alpha(|channel_value| (channel_value as f32 * alpha) as u8);
162+
pixel
163+
});
164+
165+
let mut resized = image::imageops::resize(
166+
&premultiplied_image,
167+
new_width,
168+
new_height,
169+
FilterType::Lanczos3,
170+
);
171+
172+
// Demultiply alpha
173+
resized.par_pixels_mut().for_each(|pixel| {
174+
let alpha = pixel.0[3] as f32 / u8::MAX as f32;
175+
pixel.apply_without_alpha(|channel_value| (channel_value as f32 / alpha) as u8);
176+
});
177+
178+
DynamicImage::ImageRgba8(resized)
179+
}
180+
170181
fn read_source(path: PathBuf) -> Result<Source> {
171182
if let Some(extension) = path.extension() {
172183
if extension == "svg" {
@@ -183,7 +194,7 @@ fn read_source(path: PathBuf) -> Result<Source> {
183194
..Default::default()
184195
};
185196

186-
let svg_data = std::fs::read(&path).unwrap();
197+
let svg_data = std::fs::read(&path).fs_context("Failed to read source icon", &path)?;
187198
usvg::Tree::from_data(&svg_data, &opt).unwrap()
188199
};
189200

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

332-
let image = source.resize_exact(size)?;
343+
let image = source.resize_exact(size);
333344

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

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

366377
for size in [32, 16, 24, 48, 64, 256] {
367-
let image = source.resize_exact(size)?;
378+
let image = source.resize_exact(size);
368379

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

800811
match bg {
801812
Some(Background::Color(bg_color)) => {
@@ -809,7 +820,7 @@ fn resize_png(
809820
image = bg_img.into();
810821
}
811822
Some(Background::Image(bg_source)) => {
812-
let mut bg = bg_source.resize_exact(size)?;
823+
let mut bg = bg_source.resize_exact(size);
813824

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

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

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

904-
let resized = image::imageops::resize(&cropped, new_w, new_h, image::imageops::Lanczos3);
916+
let resized = resize_image(&cropped, new_w, new_h);
905917

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

0 commit comments

Comments
 (0)