APE: Add Picture interface to ApeTag

This commit is contained in:
Serial 2024-01-23 16:27:59 -05:00
parent 04ad40381b
commit b697ccee97
No known key found for this signature in database
GPG key ID: DA95198DC17C4568
7 changed files with 151 additions and 15 deletions

View file

@ -6,10 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- **APE**: Picture interface for `ApeTag` to easily insert, remove, and iterate over pictures ([issue](https://github.com/Serial-ATA/lofty-rs/issues/280)) ([PR](https://github.com/Serial-ATA/lofty-rs/pull/348))
## [0.18.2] - 2024-01-23
### Fixed
- **MP4**: Padding for shrinking tags will no longer overwrite unrelated data ([PR](https://github.com/Serial-ATA/lofty-rs/pull/346))
- **MP4**: Padding for shrinking tags will no longer overwrite unrelated data ([PR](https://github.com/Serial-ATA/lofty-rs/pull/347))
## [0.18.1] - 2024-01-20 (YANKED)

View file

@ -18,7 +18,7 @@ byteorder = "1.5.0"
# ID3 compressed frames
flate2 = { version = "1.0.28", optional = true }
# Proc macros
lofty_attr = "0.9.0"
lofty_attr = { path = "lofty_attr" }
# Debug logging
log = "0.4.20"
# OGG Vorbis/Opus

View file

@ -44,9 +44,11 @@ pub(crate) fn init_write_lookup(
}
insert!(map, Ape, {
let (items, pictures) = lofty::ape::tag::tagitems_into_ape(tag);
lofty::ape::tag::ApeTagRef {
read_only: false,
items: lofty::ape::tag::tagitems_into_ape(tag),
items,
pictures,
}
.write_to(data)
});

View file

@ -5,6 +5,7 @@ mod write;
use crate::ape::tag::item::{ApeItem, ApeItemRef};
use crate::error::{LoftyError, Result};
use crate::id3::v2::util::pairs::{format_number_pair, set_number, NUMBER_PAIR_KEYS};
use crate::picture::{Picture, PictureType};
use crate::tag::item::{ItemKey, ItemValue, ItemValueRef, TagItem};
use crate::tag::{try_parse_year, Tag, TagType};
use crate::traits::{Accessor, MergeTag, SplitTag, TagExt};
@ -80,6 +81,7 @@ pub struct ApeTag {
/// Whether or not to mark the tag as read only
pub read_only: bool,
pub(super) items: Vec<ApeItem>,
pub(super) pictures: Vec<Picture>,
}
impl ApeTag {
@ -171,6 +173,85 @@ impl ApeTag {
};
}
/// Inserts a [`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.
///
/// # Examples
///
/// ```rust
/// use lofty::ape::ApeTag;
/// use lofty::Picture;
/// use std::fs::File;
///
/// # fn main() -> lofty::Result<()> {
/// # let picture_path: &str = "tests/files/assets/issue_37.jpg";
///
/// let mut tag = ApeTag::new();
///
/// let mut picture_file = File::open(picture_path)?;
/// tag.insert_picture(Picture::from_reader(&mut picture_file)?);
/// # Ok(()) }
pub fn insert_picture(&mut self, picture: Picture) -> Option<Picture> {
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,
};
self.pictures.push(picture);
ret
}
/// Gets all [`Picture`]s
///
/// # Examples
///
/// ```rust
/// use lofty::ape::ApeTag;
/// use lofty::Picture;
/// use std::fs::File;
///
/// # fn main() -> lofty::Result<()> {
/// # let picture_path: &str = "tests/files/assets/issue_37.jpg";
///
/// let mut tag = ApeTag::new();
///
/// let mut picture_file = File::open(picture_path)?;
/// tag.insert_picture(Picture::from_reader(&mut picture_file)?);
///
/// let pictures: Vec<&Picture> = tag.pictures().collect();
/// assert_eq!(pictures.len(), 1);
/// # Ok(()) }
pub fn pictures(&self) -> impl Iterator<Item = &Picture> {
self.pictures.iter()
}
/// Removes all [`Picture`]s of a certain [`PictureType`]
///
/// # Examples
///
/// ```rust
/// use lofty::ape::ApeTag;
/// use lofty::Picture;
/// use std::fs::File;
///
/// # fn main() -> lofty::Result<()> {
/// # let picture_path: &str = "tests/files/assets/issue_37.jpg";
///
/// let mut tag = ApeTag::new();
///
/// let mut picture_file = File::open(picture_path)?;
/// tag.insert_picture(Picture::from_reader(&mut picture_file)?);
/// # Ok(()) }
pub fn remove_picture_type(&mut self, picture_type: PictureType) {
self.pictures.retain(|p| p.pic_type != picture_type)
}
fn split_num_pair(&self, key: &str) -> (Option<u32>, Option<u32>) {
if let Some(ApeItem {
value: ItemValue::Text(ref text),
@ -325,6 +406,7 @@ impl TagExt for ApeTag {
ApeTagRef {
read_only: self.read_only,
items: self.items.iter().map(Into::into),
pictures: self.pictures.iter(),
}
.write_to(file)
}
@ -338,6 +420,7 @@ impl TagExt for ApeTag {
ApeTagRef {
read_only: self.read_only,
items: self.items.iter().map(Into::into),
pictures: self.pictures.iter(),
}
.dump_to(writer)
}
@ -471,17 +554,20 @@ impl From<Tag> for ApeTag {
}
}
pub(crate) struct ApeTagRef<'a, I>
pub(crate) struct ApeTagRef<'a, I, P>
where
I: Iterator<Item = ApeItemRef<'a>>,
P: Iterator<Item = &'a Picture>,
{
pub(crate) read_only: bool,
pub(crate) items: I,
pub(crate) pictures: P,
}
impl<'a, I> ApeTagRef<'a, I>
impl<'a, I, P> ApeTagRef<'a, I, P>
where
I: Iterator<Item = ApeItemRef<'a>>,
P: Iterator<Item = &'a Picture>,
{
pub(crate) fn write_to(&mut self, file: &mut File) -> Result<()> {
write::write_to(file, self)
@ -495,7 +581,12 @@ where
}
}
pub(crate) fn tagitems_into_ape(tag: &Tag) -> impl Iterator<Item = ApeItemRef<'_>> {
pub(crate) fn tagitems_into_ape(
tag: &Tag,
) -> (
impl Iterator<Item = ApeItemRef<'_>>,
impl Iterator<Item = &Picture>,
) {
fn create_apeitemref_for_number_pair<'a>(
number: Option<&str>,
total: Option<&str>,
@ -508,7 +599,8 @@ pub(crate) fn tagitems_into_ape(tag: &Tag) -> impl Iterator<Item = ApeItemRef<'_
})
}
tag.items()
let items = tag
.items()
.filter(|item| !NUMBER_PAIR_KEYS.contains(item.key()))
.filter_map(|i| {
i.key().map_key(TagType::Ape, true).map(|key| ApeItemRef {
@ -526,7 +618,12 @@ pub(crate) fn tagitems_into_ape(tag: &Tag) -> impl Iterator<Item = ApeItemRef<'_
tag.get_string(&ItemKey::DiscNumber),
tag.get_string(&ItemKey::DiscTotal),
"Disk",
))
));
let pictures = tag
.pictures
.iter()
.filter(|p| p.pic_type.as_ape_key().is_some());
(items, pictures)
}
#[cfg(test)]

View file

@ -1,6 +1,7 @@
use super::item::ApeItem;
use super::ApeTag;
use crate::ape::constants::{APE_PREAMBLE, INVALID_KEYS};
use crate::picture::{APE_PICTURE_TYPES, Picture};
use crate::ape::header::{self, ApeHeader};
use crate::error::Result;
use crate::macros::{decode_err, err, try_vec};
@ -57,6 +58,11 @@ where
let mut value = try_vec![0; value_size as usize];
data.read_exact(&mut value)?;
if APE_PICTURE_TYPES.contains(&&**&key) {
tag.pictures.push(Picture::from_ape_bytes(&key, &value)?);
continue;
}
let parsed_value = match item_type {
0 => ItemValue::Text(utf8_decode(value).map_err(|_| {
decode_err!(Ape, "Failed to convert text item into a UTF-8 string")

View file

@ -11,12 +11,14 @@ use crate::tag::item::ItemValueRef;
use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use crate::Picture;
use byteorder::{LittleEndian, WriteBytesExt};
#[allow(clippy::shadow_unrelated)]
pub(crate) fn write_to<'a, I>(data: &mut File, tag: &mut ApeTagRef<'a, I>) -> Result<()>
pub(crate) fn write_to<'a, I, P>(data: &mut File, tag: &mut ApeTagRef<'a, I, P>) -> Result<()>
where
I: Iterator<Item = ApeItemRef<'a>>,
P: Iterator<Item = &'a Picture>,
{
let probe = Probe::new(data).guess_file_type()?;
@ -93,6 +95,7 @@ where
create_ape_tag(&mut ApeTagRef {
read_only: read_only.read_only,
items: read_only.items.iter().map(Into::into),
pictures: read_only.pictures.iter(),
})?
} else {
create_ape_tag(tag)?
@ -122,9 +125,10 @@ where
Ok(())
}
pub(super) fn create_ape_tag<'a, I>(tag: &mut ApeTagRef<'a, I>) -> Result<Vec<u8>>
pub(super) fn create_ape_tag<'a, I, P>(tag: &mut ApeTagRef<'a, I, P>) -> Result<Vec<u8>>
where
I: Iterator<Item = ApeItemRef<'a>>,
P: Iterator<Item = &'a Picture>,
{
let items = &mut tag.items;
let mut peek = items.peekable();
@ -169,6 +173,26 @@ where
item_count += 1;
}
for picture in &mut tag.pictures {
let Some(ape_picture_key) = picture.pic_type.as_ape_key() else {
log::warn!(
"APE: Discarding unsupported picture type: `{:?}`",
picture.pic_type
);
continue;
};
let value = picture.as_ape_bytes();
tag_write.write_u32::<LittleEndian>(value.len() as u32)?;
tag_write.write_u32::<LittleEndian>(1_u32 << 1)?;
tag_write.write_all(ape_picture_key.as_bytes())?;
tag_write.write_u8(0)?;
tag_write.write_all(&value)?;
item_count += 1;
}
let size = tag_write.get_ref().len();
if size as u64 + 32 > u64::from(u32::MAX) {

View file

@ -40,11 +40,15 @@ pub(crate) fn write_tag(tag: &Tag, file: &mut File, file_type: FileType) -> Resu
#[allow(unreachable_patterns)]
pub(crate) fn dump_tag<W: Write>(tag: &Tag, writer: &mut W) -> Result<()> {
match tag.tag_type() {
TagType::Ape => ApeTagRef {
read_only: false,
items: ape::tag::tagitems_into_ape(tag),
}
.dump_to(writer),
TagType::Ape => {
let (items, pictures) = ape::tag::tagitems_into_ape(tag);
ApeTagRef {
read_only: false,
items,
pictures,
}
.dump_to(writer)
},
TagType::Id3v1 => Into::<Id3v1TagRef<'_>>::into(tag).dump_to(writer),
TagType::Id3v2 => Id3v2TagRef {
flags: Id3v2TagFlags::default(),