3.3 KiB · text History bf97ff0
//! Writing icons out as Android mipmap resources.

use std::io::Write;
use std::path::{Path, PathBuf};

use crate::render;
use crate::spec::IconSpec;

/// 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),
];

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_composed(spec, *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);
        }
    }
    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::FileOptions<()> =
        zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated);

    let mut names = Vec::new();

    for (bucket, size) in DENSITIES {
        for (suffix, pm) in [
            ("", render::render_composed(spec, *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);
        }
    }

    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)
}