Skip to content

Commit e37e187

Browse files
authored
fix(bundler): dmg volume icon (#1730)
1 parent f1aa120 commit e37e187

File tree

5 files changed

+136
-115
lines changed

5 files changed

+136
-115
lines changed

.changes/fix-dmg-volume-icon.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri-bundler": patch
3+
---
4+
5+
Fixes DMG volume icon.

tooling/bundler/src/bundle/macos/app.rs

Lines changed: 3 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,16 @@
2323

2424
use super::{
2525
super::common,
26+
icon::create_icns_file,
2627
sign::{notarize, notarize_auth_args, setup_keychain_if_needed, sign},
2728
};
2829
use crate::Settings;
2930

3031
use anyhow::Context;
31-
use image::{self, GenericImageView};
3232

3333
use std::{
34-
cmp::min,
35-
ffi::OsStr,
36-
fs::{self, File},
37-
io::{self, prelude::*, BufWriter},
34+
fs,
35+
io::prelude::*,
3836
path::{Path, PathBuf},
3937
process::{Command, Stdio},
4038
};
@@ -362,102 +360,3 @@ fn copy_frameworks_to_bundle(bundle_directory: &Path, settings: &Settings) -> cr
362360
}
363361
Ok(())
364362
}
365-
366-
// Given a list of icon files, try to produce an ICNS file in the resources
367-
// directory and return the path to it. Returns `Ok(None)` if no usable icons
368-
// were provided.
369-
fn create_icns_file(resources_dir: &Path, settings: &Settings) -> crate::Result<Option<PathBuf>> {
370-
if settings.icon_files().count() == 0 {
371-
return Ok(None);
372-
}
373-
374-
// If one of the icon files is already an ICNS file, just use that.
375-
for icon_path in settings.icon_files() {
376-
let icon_path = icon_path?;
377-
if icon_path.extension() == Some(OsStr::new("icns")) {
378-
let mut dest_path = resources_dir.to_path_buf();
379-
dest_path.push(icon_path.file_name().expect("Could not get icon filename"));
380-
common::copy_file(&icon_path, &dest_path)?;
381-
return Ok(Some(dest_path));
382-
}
383-
}
384-
385-
// Otherwise, read available images and pack them into a new ICNS file.
386-
let mut family = icns::IconFamily::new();
387-
388-
fn add_icon_to_family(
389-
icon: image::DynamicImage,
390-
density: u32,
391-
family: &mut icns::IconFamily,
392-
) -> io::Result<()> {
393-
// Try to add this image to the icon family. Ignore images whose sizes
394-
// don't map to any ICNS icon type; print warnings and skip images that
395-
// fail to encode.
396-
match icns::IconType::from_pixel_size_and_density(icon.width(), icon.height(), density) {
397-
Some(icon_type) => {
398-
if !family.has_icon_with_type(icon_type) {
399-
let icon = make_icns_image(icon)?;
400-
family.add_icon_with_type(&icon, icon_type)?;
401-
}
402-
Ok(())
403-
}
404-
None => Err(io::Error::new(
405-
io::ErrorKind::InvalidData,
406-
"No matching IconType",
407-
)),
408-
}
409-
}
410-
411-
let mut images_to_resize: Vec<(image::DynamicImage, u32, u32)> = vec![];
412-
for icon_path in settings.icon_files() {
413-
let icon_path = icon_path?;
414-
let icon = image::open(&icon_path)?;
415-
let density = if common::is_retina(&icon_path) { 2 } else { 1 };
416-
let (w, h) = icon.dimensions();
417-
let orig_size = min(w, h);
418-
let next_size_down = 2f32.powf((orig_size as f32).log2().floor()) as u32;
419-
if orig_size > next_size_down {
420-
images_to_resize.push((icon, next_size_down, density));
421-
} else {
422-
add_icon_to_family(icon, density, &mut family)?;
423-
}
424-
}
425-
426-
for (icon, next_size_down, density) in images_to_resize {
427-
let icon = icon.resize_exact(
428-
next_size_down,
429-
next_size_down,
430-
image::imageops::FilterType::Lanczos3,
431-
);
432-
add_icon_to_family(icon, density, &mut family)?;
433-
}
434-
435-
if !family.is_empty() {
436-
fs::create_dir_all(resources_dir)?;
437-
let mut dest_path = resources_dir.to_path_buf();
438-
dest_path.push(settings.product_name());
439-
dest_path.set_extension("icns");
440-
let icns_file = BufWriter::new(File::create(&dest_path)?);
441-
family.write(icns_file)?;
442-
Ok(Some(dest_path))
443-
} else {
444-
Err(crate::Error::GenericError(
445-
"No usable Icon files found".to_owned(),
446-
))
447-
}
448-
}
449-
450-
// Converts an image::DynamicImage into an icns::Image.
451-
fn make_icns_image(img: image::DynamicImage) -> io::Result<icns::Image> {
452-
let pixel_format = match img.color() {
453-
image::ColorType::Rgba8 => icns::PixelFormat::RGBA,
454-
image::ColorType::Rgb8 => icns::PixelFormat::RGB,
455-
image::ColorType::La8 => icns::PixelFormat::GrayAlpha,
456-
image::ColorType::L8 => icns::PixelFormat::Gray,
457-
_ => {
458-
let msg = format!("unsupported ColorType: {:?}", img.color());
459-
return Err(io::Error::new(io::ErrorKind::InvalidData, msg));
460-
}
461-
};
462-
icns::Image::from_data(pixel_format, img.width(), img.height(), img.to_bytes())
463-
}

tooling/bundler/src/bundle/macos/dmg.rs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33
// SPDX-License-Identifier: MIT
44

5-
use super::{super::common, app};
5+
use super::{super::common, app, icon::create_icns_file};
66
use crate::{bundle::Bundle, PackageType::MacOsBundle, Settings};
77

88
use anyhow::Context;
@@ -41,7 +41,8 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
4141
let dmg_name = format!("{}.dmg", &package_base_name);
4242
let dmg_path = output_path.join(&dmg_name);
4343

44-
let product_name = &format!("{}.app", settings.main_binary_name());
44+
let product_name = settings.main_binary_name();
45+
let bundle_file_name = format!("{}.app", product_name);
4546
let bundle_dir = settings.project_out_directory().join("bundle/macos");
4647

4748
let support_directory_path = output_path.join("support");
@@ -79,20 +80,15 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
7980
Command::new("chmod")
8081
.arg("777")
8182
.arg(&bundle_script_path)
82-
.current_dir(output_path)
83+
.current_dir(&output_path)
8384
.stdout(Stdio::piped())
8485
.stderr(Stdio::piped())
8586
.output()
8687
.expect("Failed to chmod script");
8788

8889
let mut args = vec![
8990
"--volname",
90-
&package_base_name,
91-
// todo: volume icon
92-
// make sure this is a valid path?
93-
94-
//"--volicon",
95-
//"../../../../icons/icon.icns",
91+
&product_name,
9692
"--icon",
9793
&product_name,
9894
"180",
@@ -104,9 +100,16 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
104100
"660",
105101
"400",
106102
"--hide-extension",
107-
&product_name,
103+
&bundle_file_name,
108104
];
109105

106+
let icns_icon_path =
107+
create_icns_file(&output_path, &settings)?.map(|path| path.to_string_lossy().to_string());
108+
if let Some(icon) = &icns_icon_path {
109+
args.push("--volicon");
110+
args.push(icon);
111+
}
112+
110113
#[allow(unused_assignments)]
111114
let mut license_path_ref = "".to_string();
112115
if let Some(license_path) = &settings.macos().license {
@@ -131,7 +134,7 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
131134
cmd
132135
.current_dir(bundle_dir.clone())
133136
.args(args)
134-
.args(vec![dmg_name.as_str(), product_name.as_str()]);
137+
.args(vec![dmg_name.as_str(), bundle_file_name.as_str()]);
135138

136139
common::print_info("running bundle_dmg.sh")?;
137140
common::execute_with_verbosity(&mut cmd, &settings).map_err(|_| {
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
2+
// SPDX-License-Identifier: Apache-2.0
3+
// SPDX-License-Identifier: MIT
4+
5+
use crate::bundle::{common, Settings};
6+
use std::{
7+
cmp::min,
8+
ffi::OsStr,
9+
fs::{self, File},
10+
io::{self, BufWriter},
11+
path::{Path, PathBuf},
12+
};
13+
14+
use image::GenericImageView;
15+
16+
// Given a list of icon files, try to produce an ICNS file in the out_dir
17+
// nd return the path to it. Returns `Ok(None)` if no usable icons
18+
// were provided.
19+
pub fn create_icns_file(out_dir: &Path, settings: &Settings) -> crate::Result<Option<PathBuf>> {
20+
if settings.icon_files().count() == 0 {
21+
return Ok(None);
22+
}
23+
24+
// If one of the icon files is already an ICNS file, just use that.
25+
for icon_path in settings.icon_files() {
26+
let icon_path = icon_path?;
27+
if icon_path.extension() == Some(OsStr::new("icns")) {
28+
let mut dest_path = out_dir.to_path_buf();
29+
dest_path.push(icon_path.file_name().expect("Could not get icon filename"));
30+
common::copy_file(&icon_path, &dest_path)?;
31+
return Ok(Some(dest_path));
32+
}
33+
}
34+
35+
// Otherwise, read available images and pack them into a new ICNS file.
36+
let mut family = icns::IconFamily::new();
37+
38+
fn add_icon_to_family(
39+
icon: image::DynamicImage,
40+
density: u32,
41+
family: &mut icns::IconFamily,
42+
) -> io::Result<()> {
43+
// Try to add this image to the icon family. Ignore images whose sizes
44+
// don't map to any ICNS icon type; print warnings and skip images that
45+
// fail to encode.
46+
match icns::IconType::from_pixel_size_and_density(icon.width(), icon.height(), density) {
47+
Some(icon_type) => {
48+
if !family.has_icon_with_type(icon_type) {
49+
let icon = make_icns_image(icon)?;
50+
family.add_icon_with_type(&icon, icon_type)?;
51+
}
52+
Ok(())
53+
}
54+
None => Err(io::Error::new(
55+
io::ErrorKind::InvalidData,
56+
"No matching IconType",
57+
)),
58+
}
59+
}
60+
61+
let mut images_to_resize: Vec<(image::DynamicImage, u32, u32)> = vec![];
62+
for icon_path in settings.icon_files() {
63+
let icon_path = icon_path?;
64+
let icon = image::open(&icon_path)?;
65+
let density = if common::is_retina(&icon_path) { 2 } else { 1 };
66+
let (w, h) = icon.dimensions();
67+
let orig_size = min(w, h);
68+
let next_size_down = 2f32.powf((orig_size as f32).log2().floor()) as u32;
69+
if orig_size > next_size_down {
70+
images_to_resize.push((icon, next_size_down, density));
71+
} else {
72+
add_icon_to_family(icon, density, &mut family)?;
73+
}
74+
}
75+
76+
for (icon, next_size_down, density) in images_to_resize {
77+
let icon = icon.resize_exact(
78+
next_size_down,
79+
next_size_down,
80+
image::imageops::FilterType::Lanczos3,
81+
);
82+
add_icon_to_family(icon, density, &mut family)?;
83+
}
84+
85+
if !family.is_empty() {
86+
fs::create_dir_all(out_dir)?;
87+
let mut dest_path = out_dir.to_path_buf();
88+
dest_path.push(settings.product_name());
89+
dest_path.set_extension("icns");
90+
let icns_file = BufWriter::new(File::create(&dest_path)?);
91+
family.write(icns_file)?;
92+
Ok(Some(dest_path))
93+
} else {
94+
Err(crate::Error::GenericError(
95+
"No usable Icon files found".to_owned(),
96+
))
97+
}
98+
}
99+
100+
// Converts an image::DynamicImage into an icns::Image.
101+
fn make_icns_image(img: image::DynamicImage) -> io::Result<icns::Image> {
102+
let pixel_format = match img.color() {
103+
image::ColorType::Rgba8 => icns::PixelFormat::RGBA,
104+
image::ColorType::Rgb8 => icns::PixelFormat::RGB,
105+
image::ColorType::La8 => icns::PixelFormat::GrayAlpha,
106+
image::ColorType::L8 => icns::PixelFormat::Gray,
107+
_ => {
108+
let msg = format!("unsupported ColorType: {:?}", img.color());
109+
return Err(io::Error::new(io::ErrorKind::InvalidData, msg));
110+
}
111+
};
112+
icns::Image::from_data(pixel_format, img.width(), img.height(), img.to_bytes())
113+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod app;
22
pub mod dmg;
3+
pub mod icon;
34
pub mod ios;
45
pub mod sign;

0 commit comments

Comments
 (0)