id3_image/
lib.rs

1#![warn(missing_docs)]
2
3//! A command-line tool to embed images into mp3 files. The real work is done by the "id3" crate,
4//! but this project makes it easier to deal with embedded cover art in particular.
5
6use std::path::Path;
7
8use anyhow::anyhow;
9use id3::TagLike;
10
11/// Embed the image from `image_filename` into `music_filename`, in-place. Any errors reading ID3
12/// tags from the music file or parsing the image get propagated upwards.
13///
14/// The image is encoded as a JPEG with a 90% quality setting, and embedded as a "Front cover".
15/// Tags get written as ID3v2.3.
16///
17pub fn embed_image(music_filename: &Path, image_filename: &Path) -> anyhow::Result<()> {
18    let mut tag = read_tag(music_filename)?;
19    let image = image::open(&image_filename).
20        map_err(|e| anyhow!("Error reading image {:?}: {}", image_filename, e))?;
21
22    let mut encoded_image_bytes = Vec::new();
23    // Unwrap: Writing to a Vec should always succeed;
24    image.write_to(&mut encoded_image_bytes, image::ImageOutputFormat::Jpeg(90)).unwrap();
25
26    tag.add_frame(id3::frame::Picture {
27        mime_type: "image/jpeg".to_string(),
28        picture_type: id3::frame::PictureType::CoverFront,
29        description: String::new(),
30        data: encoded_image_bytes,
31    });
32
33    tag.write_to_path(music_filename, id3::Version::Id3v23).
34        map_err(|e| anyhow!("Error writing image to music file {:?}: {}", music_filename, e))?;
35
36    Ok(())
37}
38
39/// Extract the first found embedded image from `music_filename` and write it as a file with the
40/// given `image_filename`. The image file will be silently overwritten if it exists.
41///
42/// Any errors from parsing id3 tags will be propagated. The function will also return an error if
43/// there's no embedded images in the mp3 file.
44///
45pub fn extract_first_image(music_filename: &Path, image_filename: &Path) -> anyhow::Result<()> {
46    let tag = read_tag(music_filename)?;
47    let first_picture = tag.pictures().next();
48
49    if let Some(p) = first_picture {
50        match image::load_from_memory(&p.data) {
51            Ok(image) => {
52                image.save(&image_filename).
53                    map_err(|e| anyhow!("Couldn't write image file {:?}: {}", image_filename, e))?;
54            },
55            Err(e) => return Err(anyhow!("Couldn't load image: {}", e)),
56        };
57
58        Ok(())
59    } else {
60        Err(anyhow!("No image found in music file"))
61    }
62}
63
64/// Remove all embedded images from the given `music_filename`. In effect, this removes all tags of
65/// type "APIC".
66///
67/// If the mp3 file's ID3 tags can't be parsed, the error will be propagated upwards.
68///
69pub fn remove_images(music_filename: &Path) -> anyhow::Result<()> {
70    let mut tag = read_tag(music_filename)?;
71    tag.remove("APIC");
72
73    tag.write_to_path(music_filename, id3::Version::Id3v23).
74        map_err(|e| anyhow!("Error updating music file {:?}: {}", music_filename, e))?;
75
76    Ok(())
77}
78
79fn read_tag(path: &Path) -> anyhow::Result<id3::Tag> {
80    id3::Tag::read_from_path(&path).or_else(|e| {
81        eprintln!("Warning: file metadata is corrupted, trying to read partial tag: {}", path.display());
82        e.partial_tag.clone().ok_or_else(|| anyhow!("Error reading music file {:?}: {}", path, e))
83    })
84}