id3_image_rs/
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::io::Cursor;
7use std::path::Path;
8
9use anyhow::anyhow;
10use id3::TagLike;
11use image::DynamicImage;
12
13/// Embed the image from `image_filename` into `music_filename`, in-place. Any errors reading ID3
14/// tags from the music file or parsing the image get propagated upwards.
15///
16/// The image is encoded as a JPEG with a 90% quality setting, and embedded as a "Front cover".
17/// Tags get written as ID3v2.3.
18///
19pub fn embed_image(music_filename: &Path, image_filename: &Path) -> anyhow::Result<()> {
20    let image = image::open(&image_filename)
21        .map_err(|e| anyhow!("Error reading image {:?}: {}", image_filename, e))?;
22
23    embed_image_from_memory(music_filename, &image)
24}
25
26/// Embed the image `image` into `music_filename`, in-place. Any errors reading ID3
27/// tags from the music file get propagated upwards.
28///
29/// The image is encoded as a JPEG with a 90% quality setting, and embedded as a "Front cover".
30/// Tags get written as ID3v2.3.
31///
32pub fn embed_image_from_memory(
33    music_filename: &Path,
34    image: &image::DynamicImage,
35) -> anyhow::Result<()> {
36    let mut tag = read_tag(music_filename)?;
37
38    let mut encoded_image_bytes = Cursor::new(Vec::new());
39    // Unwrap: Writing to a Vec should always succeed;
40    image
41        .write_to(&mut encoded_image_bytes, image::ImageOutputFormat::Jpeg(90))
42        .unwrap();
43
44    tag.add_frame(id3::frame::Picture {
45        mime_type: "image/jpeg".to_string(),
46        picture_type: id3::frame::PictureType::CoverFront,
47        description: String::new(),
48        data: encoded_image_bytes.into_inner(),
49    });
50
51    tag.write_to_path(music_filename, id3::Version::Id3v23)
52        .map_err(|e| {
53            anyhow!(
54                "Error writing image to music file {:?}: {}",
55                music_filename,
56                e
57            )
58        })?;
59
60    Ok(())
61}
62
63/// Extract the first found embedded image from `music_filename` and write it as a file with the
64/// given `image_filename`. The image file will be silently overwritten if it exists.
65///
66/// Any errors from parsing id3 tags will be propagated. The function will also return an error if
67/// there's no embedded images in the mp3 file.
68///
69pub fn extract_first_image(music_filename: &Path, image_filename: &Path) -> anyhow::Result<()> {
70    extract_first_image_as_img(music_filename)?
71        .save(&image_filename)
72        .map_err(|e| anyhow!("Couldn't write image file {:?}: {}", image_filename, e))
73}
74
75/// Extract the first found embedded image from `music_filename` and return it as image object
76///
77/// Any errors from parsing id3 tags will be propagated. The function will also return an error if
78/// there's no embedded images in the mp3 file.
79///
80pub fn extract_first_image_as_img(music_filename: &Path) -> anyhow::Result<DynamicImage> {
81    let tag = read_tag(music_filename)?;
82    let first_picture = tag.pictures().next();
83
84    if let Some(p) = first_picture {
85        image::load_from_memory(&p.data).map_err(|e| anyhow!("Couldn't load image: {}", e))
86    } else {
87        Err(anyhow!("No image found in music file"))
88    }
89}
90
91/// Remove all embedded images from the given `music_filename`. In effect, this removes all tags of
92/// type "APIC".
93///
94/// If the mp3 file's ID3 tags can't be parsed, the error will be propagated upwards.
95///
96pub fn remove_images(music_filename: &Path) -> anyhow::Result<()> {
97    let mut tag = read_tag(music_filename)?;
98    tag.remove("APIC");
99
100    tag.write_to_path(music_filename, id3::Version::Id3v23)
101        .map_err(|e| anyhow!("Error updating music file {:?}: {}", music_filename, e))?;
102
103    Ok(())
104}
105
106fn read_tag(path: &Path) -> anyhow::Result<id3::Tag> {
107    id3::Tag::read_from_path(&path).or_else(|e| {
108        eprintln!(
109            "Warning: file metadata is corrupted, trying to read partial tag: {}",
110            path.display()
111        );
112        e.partial_tag
113            .clone()
114            .ok_or_else(|| anyhow!("Error reading music file {:?}: {}", path, e))
115    })
116}