id3-image-rs/src/lib.rs

117 lines
4.1 KiB
Rust
Raw Normal View History

2021-02-24 18:22:54 +00:00
#![warn(missing_docs)]
//! A command-line tool to embed images into mp3 files. The real work is done by the "id3" crate,
//! but this project makes it easier to deal with embedded cover art in particular.
2022-03-22 16:50:38 +00:00
use std::io::Cursor;
use std::path::Path;
2021-02-22 19:56:59 +00:00
use anyhow::anyhow;
2022-03-22 16:50:38 +00:00
use id3::TagLike;
2022-03-22 16:44:13 +00:00
use image::DynamicImage;
2019-02-16 08:52:45 +00:00
2019-02-24 15:46:47 +00:00
/// Embed the image from `image_filename` into `music_filename`, in-place. Any errors reading ID3
2019-02-24 15:41:24 +00:00
/// tags from the music file or parsing the image get propagated upwards.
///
2019-02-24 15:46:47 +00:00
/// The image is encoded as a JPEG with a 90% quality setting, and embedded as a "Front cover".
/// Tags get written as ID3v2.3.
2019-02-24 15:41:24 +00:00
///
2021-02-22 19:56:59 +00:00
pub fn embed_image(music_filename: &Path, image_filename: &Path) -> anyhow::Result<()> {
2023-07-26 06:54:13 +00:00
let image = image::open(image_filename)
2022-03-22 16:50:38 +00:00
.map_err(|e| anyhow!("Error reading image {:?}: {}", image_filename, e))?;
2019-02-16 08:52:45 +00:00
2022-03-22 16:44:13 +00:00
embed_image_from_memory(music_filename, &image)
}
/// Embed the image `image` into `music_filename`, in-place. Any errors reading ID3
/// tags from the music file get propagated upwards.
///
/// The image is encoded as a JPEG with a 90% quality setting, and embedded as a "Front cover".
/// Tags get written as ID3v2.3.
///
2022-03-22 16:50:38 +00:00
pub fn embed_image_from_memory(
music_filename: &Path,
image: &image::DynamicImage,
) -> anyhow::Result<()> {
2022-03-22 16:44:13 +00:00
let mut tag = read_tag(music_filename)?;
2022-03-22 16:50:38 +00:00
let mut encoded_image_bytes = Cursor::new(Vec::new());
2019-02-16 08:52:45 +00:00
// Unwrap: Writing to a Vec should always succeed;
2022-03-22 16:50:38 +00:00
image
.write_to(&mut encoded_image_bytes, image::ImageFormat::Jpeg)
2022-03-22 16:50:38 +00:00
.unwrap();
2019-02-16 08:52:45 +00:00
2022-03-22 16:50:38 +00:00
tag.add_frame(id3::frame::Picture {
2019-02-16 08:52:45 +00:00
mime_type: "image/jpeg".to_string(),
2019-02-17 14:11:07 +00:00
picture_type: id3::frame::PictureType::CoverFront,
2019-02-16 08:52:45 +00:00
description: String::new(),
2022-03-22 16:50:38 +00:00
data: encoded_image_bytes.into_inner(),
2019-02-16 08:52:45 +00:00
});
2022-03-22 16:50:38 +00:00
tag.write_to_path(music_filename, id3::Version::Id3v23)
.map_err(|e| {
anyhow!(
"Error writing image to music file {:?}: {}",
music_filename,
e
)
})?;
2019-02-16 08:52:45 +00:00
Ok(())
}
2019-02-24 15:46:47 +00:00
/// Extract the first found embedded image from `music_filename` and write it as a file with the
/// given `image_filename`. The image file will be silently overwritten if it exists.
2019-02-24 15:41:24 +00:00
///
/// Any errors from parsing id3 tags will be propagated. The function will also return an error if
/// there's no embedded images in the mp3 file.
///
2021-02-22 19:56:59 +00:00
pub fn extract_first_image(music_filename: &Path, image_filename: &Path) -> anyhow::Result<()> {
2022-03-22 16:44:13 +00:00
extract_first_image_as_img(music_filename)?
2023-07-26 06:54:13 +00:00
.save(image_filename)
2022-03-22 16:50:38 +00:00
.map_err(|e| anyhow!("Couldn't write image file {:?}: {}", image_filename, e))
2022-03-22 16:44:13 +00:00
}
/// Extract the first found embedded image from `music_filename` and return it as image object
///
/// Any errors from parsing id3 tags will be propagated. The function will also return an error if
/// there's no embedded images in the mp3 file.
///
pub fn extract_first_image_as_img(music_filename: &Path) -> anyhow::Result<DynamicImage> {
2021-02-22 19:56:59 +00:00
let tag = read_tag(music_filename)?;
2019-02-16 08:52:45 +00:00
let first_picture = tag.pictures().next();
if let Some(p) = first_picture {
2022-03-22 16:51:40 +00:00
image::load_from_memory(&p.data).map_err(|e| anyhow!("Couldn't load image: {}", e))
} else {
2021-02-22 19:56:59 +00:00
Err(anyhow!("No image found in music file"))
}
2019-02-16 08:52:45 +00:00
}
2019-02-24 15:41:24 +00:00
/// Remove all embedded images from the given `music_filename`. In effect, this removes all tags of
/// type "APIC".
///
/// If the mp3 file's ID3 tags can't be parsed, the error will be propagated upwards.
///
2021-02-22 19:56:59 +00:00
pub fn remove_images(music_filename: &Path) -> anyhow::Result<()> {
let mut tag = read_tag(music_filename)?;
tag.remove("APIC");
2022-03-22 16:50:38 +00:00
tag.write_to_path(music_filename, id3::Version::Id3v23)
.map_err(|e| anyhow!("Error updating music file {:?}: {}", music_filename, e))?;
Ok(())
}
2021-02-22 19:56:59 +00:00
fn read_tag(path: &Path) -> anyhow::Result<id3::Tag> {
2023-07-26 06:54:13 +00:00
id3::Tag::read_from_path(path).or_else(|e| {
2022-03-22 16:50:38 +00:00
eprintln!(
"Warning: file metadata is corrupted, trying to read partial tag: {}",
path.display()
);
e.partial_tag
.clone()
.ok_or_else(|| anyhow!("Error reading music file {:?}: {}", path, e))
2021-02-22 19:56:59 +00:00
})
}