Skip to content

Commit fd4fc07

Browse files
committed
Embed multiple sizes into ICO icons (closes #705)
1 parent 7938f46 commit fd4fc07

File tree

7 files changed

+191
-100
lines changed

7 files changed

+191
-100
lines changed

native/Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

native/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ urlencoding = "2.1.3"
5252
web_app_manifest = { git = "https://github.com/filips123/WebAppManifestRS", branch = "main" }
5353

5454
[target.'cfg(target_os = "windows")'.dependencies]
55+
ico = "0.4.0"
5556
windows-registry = "0.6.0"
5657
sanitize-filename = "0.6.0"
5758
phf = { version = "0.13.1", features = ["macros"], optional = true }

native/src/integrations/implementation/linux.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use web_app_manifest::types::{ImagePurpose, ImageSize};
1616

1717
use crate::components::site::Site;
1818
use crate::integrations::categories::XDG_CATEGORIES;
19-
use crate::integrations::utils::{download_icon, normalize_category_name, process_icons};
19+
use crate::integrations::utils::{download_icon, normalize_category_name, store_icon};
2020
use crate::integrations::{IntegrationInstallArgs, IntegrationUninstallArgs};
2121
use crate::utils::sanitize_string;
2222

@@ -72,14 +72,14 @@ impl SiteIds {
7272
/// Obtain and process icons from the icon list.
7373
///
7474
/// All supported icons from the icon list are downloaded and stored to
75-
/// the correct locations the comply with the Icon Theme Specification.
75+
/// the correct locations to comply with the Icon Theme Specification.
7676
///
7777
/// All SVG icons are directly stored as `scalable` or `symbolic` icons,
7878
/// and other supported icons are converted to PNG and then stored.
7979
///
8080
/// The 48x48 icon has to exist as required by the Icon Theme Specification.
81-
/// In case it is not provided by the icon list, is is obtained using
82-
/// the [`process_icons`] function.
81+
/// In case it is not provided by the icon list, it is obtained using
82+
/// the [`store_icon`] function.
8383
///
8484
/// # Parameters
8585
///
@@ -179,7 +179,7 @@ fn store_icons(
179179
warn!("No required 48x48 icon is provided");
180180
warn!("Generating it from other available icons");
181181
let size = &ImageSize::Fixed(48, 48);
182-
return process_icons(icons, name, size, &filename, client);
182+
return store_icon(icons, name, size, &filename, client);
183183
}
184184

185185
Ok(())

native/src/integrations/implementation/macos.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::process::{Child, Command, Stdio};
77

88
use anyhow::{Context, Result, bail};
99
use icns::{IconFamily, IconType, Image, PixelFormat};
10-
use image::imageops::FilterType::Gaussian;
10+
use image::imageops::FilterType::Lanczos3;
1111
use image::imageops::resize;
1212
use image::{DynamicImage, Rgba, RgbaImage};
1313
use log::{debug, error, warn};
@@ -21,7 +21,7 @@ use crate::components::site::Site;
2121
use crate::integrations::categories::MACOS_CATEGORIES;
2222
use crate::integrations::utils::{
2323
download_icon,
24-
generate_icon,
24+
generate_fallback_icon,
2525
normalize_category_name,
2626
sanitize_name,
2727
};
@@ -44,7 +44,7 @@ const STORE_ICONS_ERROR: &str = "Failed to store icons";
4444
const LAUNCH_APPLICATION_BUNDLE: &str = "Failed to launch web app via system integration";
4545
const APP_BUNDLE_NAME_ERROR: &str = "Failed to get name of app bundle";
4646
const APP_BUNDLE_UNICODE_ERROR: &str = "Failed to check name of app bundle for Unicode validity";
47-
const GENERATE_ICON_ERROR: &str = "Failed to generate icon";
47+
const GENERATE_FALLBACK_ICON_ERROR: &str = "Failed to generate fallback icon";
4848
const GET_LETTER_ERROR: &str = "Failed to get first letter";
4949

5050
const ICON_SAFE_ZONE_FACTOR: f64 = 0.697265625;
@@ -221,7 +221,8 @@ fn store_icons(target: &Path, name: &str, icons: &[IconResource], client: &Clien
221221

222222
for size in &icon_sizes {
223223
let image_size = ImageSize::Fixed(size.size(), size.size());
224-
let image_data = generate_icon(letter, &image_size).context(GENERATE_ICON_ERROR)?;
224+
let image_data = generate_fallback_icon(letter, &image_size)
225+
.context(GENERATE_FALLBACK_ICON_ERROR)?;
225226

226227
let mut img = DynamicImage::ImageRgb8(image_data).into_rgba8();
227228
mask_icon(&mut img, true).context(MASK_ICON_ERROR)?;
@@ -270,7 +271,7 @@ fn load_icon(content: &[u8], content_type: &str, size: u32) -> Result<RgbaImage>
270271
// Parse raster icons using the `image` crate and resize it to the correct size
271272
debug!("Processing as raster icon");
272273
let img = image::load_from_memory(content).context("Failed to load raster icon")?;
273-
let img = img.resize_to_fill(size, size, Gaussian).into_rgba8();
274+
let img = img.resize_to_fill(size, size, Lanczos3).into_rgba8();
274275
Ok(img)
275276
}
276277

@@ -286,8 +287,8 @@ fn mask_icon(icon: &mut RgbaImage, maskable: bool) -> Result<()> {
286287
let icon_size = Point { x: icon.width(), y: icon.height() };
287288
let mask = image::load_from_memory(include_bytes!("../../../assets/icon-mask-macos.png"))?;
288289
let shadow = image::load_from_memory(include_bytes!("../../../assets/icon-shadow-macos.png"))?;
289-
let scaled_mask = mask.resize(icon_size.x, icon_size.y, Gaussian); // This is really slow in debug builds, up to ~1s
290-
let scaled_shadow = shadow.resize(icon_size.x, icon_size.y, Gaussian); // This is really slow in debug builds, up to ~1s
290+
let scaled_mask = mask.resize(icon_size.x, icon_size.y, Lanczos3); // This is really slow in debug builds, up to ~1s
291+
let scaled_shadow = shadow.resize(icon_size.x, icon_size.y, Lanczos3); // This is really slow in debug builds, up to ~1s
291292
let mask_data = scaled_mask.into_rgba8();
292293
let shadow_data = scaled_shadow.into_rgba8();
293294

@@ -308,7 +309,7 @@ fn mask_icon(icon: &mut RgbaImage, maskable: bool) -> Result<()> {
308309
let scaled_icon_data: RgbaImage = if maskable {
309310
icon.clone()
310311
} else {
311-
resize(icon, scaled_icon_size.x, scaled_icon_size.y, Gaussian)
312+
resize(icon, scaled_icon_size.x, scaled_icon_size.y, Lanczos3)
312313
};
313314

314315
let background = RgbaImage::from_pixel(icon_size.x, icon_size.y, Rgba([255, 255, 255, 255]));

native/src/integrations/implementation/portableapps.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use log::warn;
99
use web_app_manifest::types::ImageSize;
1010

1111
use crate::integrations::categories::PORTABLEAPPS_CATEGORIES;
12-
use crate::integrations::utils::{normalize_category_name, process_icons};
12+
use crate::integrations::utils::{normalize_category_name, store_icon, store_multisize_icon};
1313
use crate::integrations::{IntegrationInstallArgs, IntegrationUninstallArgs};
1414
use crate::utils::sanitize_string;
1515

@@ -95,17 +95,20 @@ fn store_icons(args: &IntegrationInstallArgs, path: &Path) -> Result<()> {
9595
PortableAppIcon { size: 75, format: "png" },
9696
PortableAppIcon { size: 128, format: "png" },
9797
PortableAppIcon { size: 256, format: "png" },
98-
PortableAppIcon { size: 256, format: "ico" },
9998
];
10099

101100
let icons = &args.site.icons();
102101
let fallback = &args.site.name();
103102
let client = args.client.unwrap();
104103

105104
for icon in required {
106-
process_icons(icons, fallback, &icon.size(), &path.join(icon.filename()), client)?;
105+
store_icon(icons, fallback, &icon.size(), &path.join(icon.filename()), client)?;
107106
}
108107

108+
// Skip sizes 32 and smaller as that causes bad rendering
109+
let sizes = [48, 64, 128, 256];
110+
store_multisize_icon(icons, fallback, &sizes, &path.join("appicon.ico"), client)?;
111+
109112
Ok(())
110113
}
111114

native/src/integrations/implementation/windows.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use log::warn;
66
use reqwest::blocking::Client;
77
use url::Url;
88
use web_app_manifest::resources::IconResource;
9-
use web_app_manifest::types::ImageSize;
109
use windows::Win32::Storage::EnhancedStorage::{PKEY_AppUserModel_ID, PKEY_Title};
1110
use windows::Win32::System::Com::StructuredStorage::InitPropVariantFromStringVector;
1211
use windows::Win32::System::Com::{
@@ -31,7 +30,7 @@ use windows::core::{GUID, HSTRING, Interface, PCWSTR, Result as WindowsResult};
3130
use windows_registry::{CURRENT_USER, Key};
3231

3332
use crate::components::site::Site;
34-
use crate::integrations::utils::{process_icons, sanitize_name};
33+
use crate::integrations::utils::{sanitize_name, store_multisize_icon};
3534
use crate::integrations::{IntegrationInstallArgs, IntegrationUninstallArgs};
3635
use crate::utils::sanitize_string;
3736

@@ -86,7 +85,7 @@ impl SiteIds {
8685
/// the next icons are tried. If no provided icons are working, the icon is generated
8786
/// from the first letter of the name.
8887
///
89-
/// See [`process_icons`] for more details.
88+
/// See [`store_multisize_icon`] for more details.
9089
///
9190
/// # Parameters
9291
///
@@ -96,10 +95,8 @@ impl SiteIds {
9695
/// - `client`: An instance of a blocking HTTP client.
9796
///
9897
fn store_icon(name: &str, icons: &[IconResource], path: &Path, client: &Client) -> Result<()> {
99-
// Currently only one embedded image per ICO is supported: https://github.com/image-rs/image/issues/884
100-
// Until more embedded images are supported, use the max ICO size (256x256)
101-
let size = &ImageSize::Fixed(256, 256);
102-
process_icons(icons, name, size, path, client)
98+
let sizes = [16, 24, 32, 48, 64, 128, 256];
99+
store_multisize_icon(icons, name, &sizes, path, client)
103100
}
104101

105102
fn create_arp_entry(

0 commit comments

Comments
 (0)