//! Writing icons out as Android mipmap resources.
use std::io::Write;
use std::path::{Path, PathBuf};
use crate::render;
use crate::spec::IconSpec;
/// Adaptive icon layers are 108dp square.
pub const ADAPTIVE_DP: u32 = 108;
/// The flat launcher icon is 48dp — a different size, not a different
/// rendering of the same one. Writing it at the adaptive size makes it 4.5x
/// larger than every other app's.
pub const LEGACY_DP: u32 = 48;
/// The flat icon's pixel size for a given adaptive size. Exact at every
/// bucket: 108->48, 162->72, 216->96, 324->144, 432->192.
pub fn legacy_size(adaptive: u32) -> u32 {
adaptive * LEGACY_DP / ADAPTIVE_DP
}
/// Android adaptive icons are 108dp; these are the density buckets and the
/// pixel size each works out to.
pub const DENSITIES: &[(&str, u32)] = &[
("mdpi", 108),
("hdpi", 162),
("xhdpi", 216),
("xxhdpi", 324),
("xxxhdpi", 432),
];
/// The `mipmap-anydpi-v26/<name>.xml` that binds the two layers together.
///
/// Without this Android has no idea the background and foreground PNGs are
/// related: it falls back to the flat `<name>.png` and the adaptive layers are
/// dead weight. Exporting the layers without it is the difference between an
/// icon that masks and parallaxes on a modern launcher and one that does not.
pub fn adaptive_icon_xml(base_name: &str) -> String {
format!(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\
<adaptive-icon\n\
\x20 xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\
\x20 <background android:drawable=\"@mipmap/{base_name}_background\" />\n\
\x20 <foreground android:drawable=\"@mipmap/{base_name}_foreground\" />\n\
</adaptive-icon>\n"
)
}
fn write_png(pm: &tiny_skia::Pixmap, path: &Path) -> Result<(), String> {
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
}
pm.save_png(path)
.map_err(|e| format!("{}: {e}", path.display()))
}
/// Writes `ic_icon`, `ic_icon_background` and `ic_icon_foreground` into
/// `res/mipmap-*` under `root`, matching the layout the cbz apps ship.
pub fn export_mipmaps(
spec: &IconSpec,
root: &Path,
base_name: &str,
) -> Result<Vec<PathBuf>, String> {
let mut written = Vec::new();
for (bucket, size) in DENSITIES {
let dir = root.join(format!("mipmap-{bucket}"));
for (suffix, pm) in [
("", render::render_legacy(spec, legacy_size(*size))),
("_background", render::render_background(spec, *size)),
("_foreground", render::render_foreground(spec, *size)),
] {
let p = dir.join(format!("{base_name}{suffix}.png"));
write_png(&pm, &p)?;
written.push(p);
}
}
let xml = root
.join("mipmap-anydpi-v26")
.join(format!("{base_name}.xml"));
if let Some(dir) = xml.parent() {
std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
}
std::fs::write(&xml, adaptive_icon_xml(base_name))
.map_err(|e| format!("{}: {e}", xml.display()))?;
written.push(xml);
Ok(written)
}
/// The mipmap tree as a single `.zip`, for handing to someone who is not
/// pointing at the project on disk.
///
/// The source document goes in beside the images. A bundle that contains only
/// PNGs is a dead end — whoever receives it cannot change the icon without
/// rebuilding the configuration by eye, which is the exact problem this whole
/// program exists to avoid.
pub fn export_zip(
spec: &IconSpec,
path: &Path,
base_name: &str,
document: Option<(&str, &str)>,
) -> Result<Vec<String>, String> {
let file = std::fs::File::create(path).map_err(|e| format!("{}: {e}", path.display()))?;
let mut zip = zip::ZipWriter::new(file);
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
let mut names = Vec::new();
for (bucket, size) in DENSITIES {
for (suffix, pm) in [
("", render::render_legacy(spec, legacy_size(*size))),
("_background", render::render_background(spec, *size)),
("_foreground", render::render_foreground(spec, *size)),
] {
let name = format!("mipmap-{bucket}/{base_name}{suffix}.png");
let png = pm
.encode_png()
.map_err(|e| format!("encoding {name}: {e}"))?;
zip.start_file(&name, options)
.map_err(|e| format!("{name}: {e}"))?;
zip.write_all(&png).map_err(|e| format!("{name}: {e}"))?;
names.push(name);
}
}
let xml_name = format!("mipmap-anydpi-v26/{base_name}.xml");
zip.start_file(&xml_name, options)
.map_err(|e| format!("{xml_name}: {e}"))?;
zip.write_all(adaptive_icon_xml(base_name).as_bytes())
.map_err(|e| format!("{xml_name}: {e}"))?;
names.push(xml_name);
if let Some((doc_name, body)) = document {
zip.start_file(doc_name, options)
.map_err(|e| format!("{doc_name}: {e}"))?;
zip.write_all(body.as_bytes())
.map_err(|e| format!("{doc_name}: {e}"))?;
names.push(doc_name.to_string());
}
zip.finish()
.map_err(|e| format!("{}: {e}", path.display()))?;
Ok(names)
}
neiam
/icon-studio
public · Issues · Pulls · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
5.3 KiB · text
History
aca7335