FLAC: Store pictures separately from the VorbisComments tag

This commit is contained in:
Serial 2023-01-08 21:20:54 -05:00 committed by Alex
parent fee737fd4e
commit fd1f9f62a0
6 changed files with 306 additions and 141 deletions

View file

@ -22,11 +22,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **FLAC**: `FlacProperties`
- Previously, FLAC files used `FileProperties`. `FlacProperties` was added to support getting the MD5 signature
of the audio data.
- **OGG**: `OggPictureStorage`
- This was added to cover the overlap in functionality between `VorbisComments` and `FlacFile` in that they both
store `(Picture, PictureInformation)`.
### Changed
- **MP4**: `AtomIdent` stores freeform identifiers as `Cow<str>` opposed to `String` ([PR](https://github.com/Serial-ATA/lofty-rs/pull/95))
- This allows freeform identifiers to be constructed in a const context.
- **ID3v2**: `FrameID` now uses `Cow<str>` opposed to `String` ([PR](https://github.com/Serial-ATA/lofty-rs/pull/102))
- **FLAC**: `FlacFile` now stores pictures separately from its `VorbisComments` tag
### Removed
- **Metadata format features** ([PR](https://github.com/Serial-ATA/lofty-rs/pull/97)):

View file

@ -9,8 +9,16 @@ pub(crate) mod properties;
mod read;
pub(crate) mod write;
use crate::error::Result;
use crate::file::{FileType, TaggedFile};
use crate::id3::v2::tag::ID3v2Tag;
use crate::ogg::VorbisComments;
use crate::ogg::tag::VorbisCommentsRef;
use crate::ogg::{OggPictureStorage, VorbisComments};
use crate::picture::{Picture, PictureInformation};
use crate::traits::TagExt;
use std::fs::File;
use std::io::Seek;
use lofty_attr::LoftyFile;
@ -23,21 +31,104 @@ pub use properties::FlacProperties;
/// ## Notes
///
/// * The ID3v2 tag is **read only**, and it's use is discouraged by spec
/// * Picture blocks will be stored in the `VorbisComments` tag, meaning a file could have no vorbis
/// comments block, but `FlacFile::vorbis_comments` will exist.
/// * When writing, the pictures will be stored in their own picture blocks
/// * This behavior will likely change in the future
/// * Pictures are stored in the `FlacFile` itself, rather than the tag. Any pictures inside the tag will
/// be extracted out and stored in their own picture blocks.
/// * It is possible to put pictures inside of the tag, that will not be accessible using the available
/// methods on `FlacFile` ([`FlacFile::pictures`], [`FlacFile::remove_picture_type`], etc.)
/// * When converting to [`TaggedFile`], all pictures will be put inside of a [`VorbisComments`] tag, even if the
/// file did not originally contain one.
#[derive(LoftyFile)]
#[lofty(read_fn = "read::read_from")]
#[lofty(write_fn = "Self::write_to")]
#[lofty(no_into_taggedfile_impl)]
pub struct FlacFile {
/// An ID3v2 tag
#[lofty(tag_type = "ID3v2")]
pub(crate) id3v2_tag: Option<ID3v2Tag>,
/// The vorbis comments contained in the file
///
/// NOTE: This field being `Some` does not mean the file has vorbis comments, as Picture blocks exist.
#[lofty(tag_type = "VorbisComments")]
pub(crate) vorbis_comments_tag: Option<VorbisComments>,
pub(crate) pictures: Vec<(Picture, PictureInformation)>,
/// The file's audio properties
pub(crate) properties: FlacProperties,
}
impl FlacFile {
// We need a special write fn to append our pictures into a `VorbisComments` tag
fn write_to(&self, file: &mut File) -> Result<()> {
if let Some(ref id3v2) = self.id3v2_tag {
id3v2.save_to(file)?;
file.rewind()?;
}
// We have an existing vorbis comments tag, we can just append our pictures to it
if let Some(ref vorbis_comments) = self.vorbis_comments_tag {
return VorbisCommentsRef {
vendor: vorbis_comments.vendor.as_str(),
items: vorbis_comments
.items
.iter()
.map(|(k, v)| (k.as_str(), v.as_str())),
pictures: vorbis_comments
.pictures
.iter()
.map(|(p, i)| (p, *i))
.chain(self.pictures.iter().map(|(p, i)| (p, *i))),
}
.write_to(file);
}
// We have pictures, but no vorbis comments tag, we'll need to create a dummy one
if !self.pictures.is_empty() {
return VorbisCommentsRef {
vendor: "",
items: std::iter::empty(),
pictures: self.pictures.iter().map(|(p, i)| (p, *i)),
}
.write_to(file);
}
Ok(())
}
}
impl OggPictureStorage for FlacFile {
fn pictures(&self) -> &[(Picture, PictureInformation)] {
&self.pictures
}
}
impl From<FlacFile> for TaggedFile {
fn from(mut value: FlacFile) -> Self {
TaggedFile {
ty: FileType::FLAC,
properties: value.properties.into(),
tags: {
let mut tags = Vec::with_capacity(2);
if let Some(id3v2) = value.id3v2_tag {
tags.push(id3v2.into());
}
// Move our pictures into a `VorbisComments` tag, creating one if necessary
match value.vorbis_comments_tag {
Some(mut vorbis_comments) => {
vorbis_comments.pictures.append(&mut value.pictures);
tags.push(vorbis_comments.into());
},
None if !value.pictures.is_empty() => tags.push(
VorbisComments {
vendor: String::new(),
items: Vec::new(),
pictures: value.pictures,
}
.into(),
),
_ => {},
}
tags
},
}
}
}

View file

@ -39,6 +39,7 @@ where
let mut flac_file = FlacFile {
id3v2_tag: None,
vorbis_comments_tag: None,
pictures: Vec::new(),
properties: FlacProperties::default(),
};
@ -75,7 +76,7 @@ where
match block.ty {
4 => read_comments(&mut &*block.content, block.content.len() as u64, &mut tag)?,
6 => tag
6 => flac_file
.pictures
.push(Picture::from_flac_bytes(&block.content, false)?),
_ => {},

View file

@ -5,6 +5,7 @@
//! The only supported tag format is [`VorbisComments`]
pub(crate) mod constants;
pub(crate) mod opus;
mod picture_storage;
pub(crate) mod read;
pub(crate) mod speex;
pub(crate) mod tag;
@ -22,6 +23,7 @@ use ogg_pager::Page;
pub use opus::properties::OpusProperties;
pub use opus::OpusFile;
pub use picture_storage::OggPictureStorage;
pub use speex::properties::SpeexProperties;
pub use speex::SpeexFile;
pub use tag::VorbisComments;

193
src/ogg/picture_storage.rs Normal file
View file

@ -0,0 +1,193 @@
use crate::error::Result;
use crate::picture::{Picture, PictureInformation, PictureType};
/// Defines methods for interacting with an item storing OGG pictures
///
/// This exists due to *both* [`VorbisComments`](crate::ogg::VorbisComments) and [`FlacFile`](crate::flac::FlacFile) needing to store
/// pictures in their own ways.
///
/// It cannot be implemented downstream.
pub trait OggPictureStorage: private::Sealed {
/// Inserts a [`Picture`]
///
/// NOTES:
///
/// * If `information` is `None`, the [`PictureInformation`] will be inferred using [`PictureInformation::from_picture`].
/// * According to spec, there can only be one picture of type [`PictureType::Icon`] and [`PictureType::OtherIcon`].
/// When attempting to insert these types, if another is found it will be removed and returned.
///
/// # Errors
///
/// * See [`PictureInformation::from_picture`]
fn insert_picture(
&mut self,
picture: Picture,
information: Option<PictureInformation>,
) -> Result<Option<(Picture, PictureInformation)>> {
let ret = match picture.pic_type {
PictureType::Icon | PictureType::OtherIcon => self
.pictures()
.iter()
.position(|(p, _)| p.pic_type == picture.pic_type)
.map(|pos| self.remove_picture(pos)),
_ => None,
};
let info = match information {
Some(pic_info) => pic_info,
None => PictureInformation::from_picture(&picture)?,
};
self.pictures_mut().push((picture, info));
Ok(ret)
}
/// Removes a certain [`PictureType`]
fn remove_picture_type(&mut self, picture_type: PictureType) {
self.pictures_mut()
.retain(|(pic, _)| pic.pic_type != picture_type);
}
/// Returns the stored [`Picture`]s as a slice
///
/// # Examples
///
/// ```rust
/// use lofty::ogg::{OggPictureStorage, VorbisComments};
///
/// let mut tag = VorbisComments::default();
///
/// assert!(tag.pictures().is_empty());
/// ```
fn pictures(&self) -> &[(Picture, PictureInformation)];
/// Replaces the picture at the given `index`
///
/// NOTE: If `index` is out of bounds, the `picture` will be appended
/// to the list.
///
/// # Examples
///
/// ```rust
/// use lofty::ogg::{VorbisComments, OggPictureStorage};
/// # use lofty::{Picture, PictureInformation, PictureType, MimeType};
///
/// # fn main() -> lofty::Result<()> {
/// # let front_cover = Picture::new_unchecked(PictureType::CoverFront, MimeType::Png, None, Vec::new());
/// # let front_cover_info = PictureInformation::default();
/// # let back_cover = Picture::new_unchecked(PictureType::CoverBack, MimeType::Png, None, Vec::new());
/// # let back_cover_info = PictureInformation::default();
/// # let another_picture = Picture::new_unchecked(PictureType::Band, MimeType::Png, None, Vec::new());
/// let mut tag = VorbisComments::default();
///
/// // Add a front cover
/// tag.insert_picture(front_cover, Some(front_cover_info))?;
///
/// assert_eq!(tag.pictures().len(), 1);
/// assert_eq!(tag.pictures()[0].0.pic_type(), PictureType::CoverFront);
///
/// // Replace the front cover with a back cover
/// tag.set_picture(0, back_cover, back_cover_info);
///
/// assert_eq!(tag.pictures().len(), 1);
/// assert_eq!(tag.pictures()[0].0.pic_type(), PictureType::CoverBack);
///
/// // Use an out of bounds index
/// tag.set_picture(100, another_picture, PictureInformation::default());
///
/// assert_eq!(tag.pictures().len(), 2);
/// # Ok(()) }
/// ```
#[allow(clippy::missing_panics_doc)]
fn set_picture(&mut self, index: usize, picture: Picture, info: PictureInformation) {
if index >= self.pictures().len() {
// Safe to unwrap, since `info` is guaranteed to exist
self.insert_picture(picture, Some(info)).unwrap();
} else {
self.pictures_mut()[index] = (picture, info);
}
}
/// Removes and returns the picture at the given `index`
///
/// # Panics
///
/// Panics if `index` is out of bounds.
///
/// # Examples
///
/// ```rust
/// use lofty::ogg::{VorbisComments, OggPictureStorage};
/// # use lofty::{Picture, PictureType, MimeType, PictureInformation};
///
/// # fn main() -> lofty::Result<()> {
/// # let front_cover = Picture::new_unchecked(PictureType::CoverFront, MimeType::Png, None, Vec::new());
/// # let front_cover_info = PictureInformation::default();
/// let mut tag = VorbisComments::default();
///
/// // Add a front cover
/// tag.insert_picture(front_cover, Some(front_cover_info))?;
///
/// assert_eq!(tag.pictures().len(), 1);
///
/// tag.remove_picture(0);
///
/// assert_eq!(tag.pictures().len(), 0);
/// # Ok(()) }
/// ```
fn remove_picture(&mut self, index: usize) -> (Picture, PictureInformation) {
self.pictures_mut().remove(index)
}
/// Removes all pictures and returns them
///
/// # Examples
///
/// ```rust
/// use lofty::ogg::{VorbisComments, OggPictureStorage};
/// # use lofty::{Picture, PictureType, MimeType, PictureInformation};
///
/// # fn main() -> lofty::Result<()> {
/// # let front_cover = Picture::new_unchecked(PictureType::CoverFront, MimeType::Png, None, Vec::new());
/// # let front_cover_info = PictureInformation::default();
/// # let back_cover = Picture::new_unchecked(PictureType::CoverBack, MimeType::Png, None, Vec::new());
/// # let back_cover_info = PictureInformation::default();
/// let mut tag = VorbisComments::default();
///
/// // Add front and back covers
/// tag.insert_picture(front_cover, Some(front_cover_info))?;
/// tag.insert_picture(back_cover, Some(front_cover_info))?;
///
/// assert_eq!(tag.pictures().len(), 2);
///
/// let pictures = tag.remove_pictures();
/// assert_eq!(pictures.len(), 2);
///
/// // The tag no longer contains any pictures
/// assert_eq!(tag.pictures().len(), 0);
/// # Ok(()) }
/// ```
fn remove_pictures(&mut self) -> Vec<(Picture, PictureInformation)> {
core::mem::take(self.pictures_mut())
}
}
mod private {
use crate::{Picture, PictureInformation};
pub trait Sealed {
fn pictures_mut(&mut self) -> &mut Vec<(Picture, PictureInformation)>;
}
impl Sealed for crate::ogg::tag::VorbisComments {
fn pictures_mut(&mut self) -> &mut Vec<(Picture, PictureInformation)> {
&mut self.pictures
}
}
impl Sealed for crate::flac::FlacFile {
fn pictures_mut(&mut self) -> &mut Vec<(Picture, PictureInformation)> {
&mut self.pictures
}
}
}

View file

@ -1,8 +1,9 @@
use crate::error::{LoftyError, Result};
use crate::file::FileType;
use crate::macros::err;
use crate::ogg::picture_storage::OggPictureStorage;
use crate::ogg::write::OGGFormat;
use crate::picture::{Picture, PictureInformation, PictureType};
use crate::picture::{Picture, PictureInformation};
use crate::probe::Probe;
use crate::tag::item::{ItemKey, ItemValue, TagItem};
use crate::tag::{Tag, TagType};
@ -127,139 +128,12 @@ impl VorbisComments {
self.items.drain(..split_idx).map(|(_, v)| v)
}
}
/// Inserts a [`Picture`]
///
/// NOTES:
///
/// * If `information` is `None`, the [`PictureInformation`] will be inferred using [`PictureInformation::from_picture`].
/// * According to spec, there can only be one picture of type [`PictureType::Icon`] and [`PictureType::OtherIcon`].
/// When attempting to insert these types, if another is found it will be removed and returned.
///
/// # Errors
///
/// * See [`PictureInformation::from_picture`]
pub fn insert_picture(
&mut self,
picture: Picture,
information: Option<PictureInformation>,
) -> Result<Option<(Picture, PictureInformation)>> {
let ret = match picture.pic_type {
PictureType::Icon | PictureType::OtherIcon => self
.pictures
.iter()
.position(|(p, _)| p.pic_type == picture.pic_type)
.map(|pos| self.pictures.remove(pos)),
_ => None,
};
let info = match information {
Some(pic_info) => pic_info,
None => PictureInformation::from_picture(&picture)?,
};
self.pictures.push((picture, info));
Ok(ret)
}
/// Removes a certain [`PictureType`]
pub fn remove_picture_type(&mut self, picture_type: PictureType) {
self.pictures.retain(|(p, _)| p.pic_type != picture_type)
}
/// Returns the stored [`Picture`]s as a slice
///
/// # Examples
///
/// ```rust
/// use lofty::ogg::VorbisComments;
///
/// let mut tag = VorbisComments::default();
///
/// assert!(tag.pictures().is_empty());
/// ```
pub fn pictures(&self) -> &[(Picture, PictureInformation)] {
impl OggPictureStorage for VorbisComments {
fn pictures(&self) -> &[(Picture, PictureInformation)] {
&self.pictures
}
/// Replaces the picture at the given `index`
///
/// NOTE: If `index` is out of bounds, the `picture` will be appended
/// to the list.
///
/// # Examples
///
/// ```rust
/// use lofty::ogg::VorbisComments;
/// # use lofty::{Picture, PictureInformation, PictureType, MimeType};
///
/// # fn main() -> lofty::Result<()> {
/// # let front_cover = Picture::new_unchecked(PictureType::CoverFront, MimeType::Png, None, Vec::new());
/// # let front_cover_info = PictureInformation::default();
/// # let back_cover = Picture::new_unchecked(PictureType::CoverBack, MimeType::Png, None, Vec::new());
/// # let back_cover_info = PictureInformation::default();
/// # let another_picture = Picture::new_unchecked(PictureType::Band, MimeType::Png, None, Vec::new());
/// let mut tag = VorbisComments::default();
///
/// // Add a front cover
/// tag.insert_picture(front_cover, Some(front_cover_info))?;
///
/// assert_eq!(tag.pictures().len(), 1);
/// assert_eq!(tag.pictures()[0].0.pic_type(), PictureType::CoverFront);
///
/// // Replace the front cover with a back cover
/// tag.set_picture(0, back_cover, back_cover_info);
///
/// assert_eq!(tag.pictures().len(), 1);
/// assert_eq!(tag.pictures()[0].0.pic_type(), PictureType::CoverBack);
///
/// // Use an out of bounds index
/// tag.set_picture(100, another_picture, PictureInformation::default());
///
/// assert_eq!(tag.pictures().len(), 2);
/// # Ok(()) }
/// ```
#[allow(clippy::missing_panics_doc)]
pub fn set_picture(&mut self, index: usize, picture: Picture, info: PictureInformation) {
if index >= self.pictures.len() {
// Safe to unwrap, since `info` is guaranteed to exist
self.insert_picture(picture, Some(info)).unwrap();
} else {
self.pictures[index] = (picture, info);
}
}
/// Removes and returns the picture at the given `index`
///
/// # Panics
///
/// Panics if `index` is out of bounds.
///
/// # Examples
///
/// ```rust
/// use lofty::ogg::VorbisComments;
/// # use lofty::{Picture, PictureType, MimeType, PictureInformation};
///
/// # fn main() -> lofty::Result<()> {
/// # let front_cover = Picture::new_unchecked(PictureType::CoverFront, MimeType::Png, None, Vec::new());
/// # let front_cover_info = PictureInformation::default();
/// let mut tag = VorbisComments::default();
///
/// // Add a front cover
/// tag.insert_picture(front_cover, Some(front_cover_info))?;
///
/// assert_eq!(tag.pictures().len(), 1);
///
/// tag.remove_picture(0);
///
/// assert_eq!(tag.pictures().len(), 0);
/// # Ok(()) }
/// ```
pub fn remove_picture(&mut self, index: usize) -> (Picture, PictureInformation) {
self.pictures.remove(index)
}
}
impl Accessor for VorbisComments {
@ -527,7 +401,7 @@ where
IP: Iterator<Item = (&'a Picture, PictureInformation)>,
{
#[allow(clippy::shadow_unrelated)]
fn write_to(&mut self, file: &mut File) -> Result<()> {
pub(crate) fn write_to(&mut self, file: &mut File) -> Result<()> {
let probe = Probe::new(file).guess_file_type()?;
let f_ty = probe.file_type();
@ -586,7 +460,7 @@ pub(crate) fn create_vorbis_comments_ref(
#[cfg(test)]
mod tests {
use crate::ogg::VorbisComments;
use crate::ogg::{OggPictureStorage, VorbisComments};
use crate::{Tag, TagExt, TagType};
fn read_tag(tag: &[u8]) -> VorbisComments {