mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-12-12 13:42:34 +00:00
Picture overhaul
This commit is contained in:
parent
81e561263e
commit
25368a428f
1 changed files with 397 additions and 21 deletions
|
@ -1,6 +1,34 @@
|
|||
use crate::{Error, Result};
|
||||
use id3::frame::PictureType as id3PicType;
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use std::borrow::Cow;
|
||||
use std::convert::TryFrom;
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
#[cfg(feature = "format-ape")]
|
||||
pub const APE_PICTYPES: [&str; 21] = [
|
||||
"Other",
|
||||
"Png Icon",
|
||||
"Icon",
|
||||
"Front",
|
||||
"Back",
|
||||
"Leaflet",
|
||||
"Media",
|
||||
"Lead Artist",
|
||||
"Artist",
|
||||
"Conductor",
|
||||
"Band",
|
||||
"Composer",
|
||||
"Lyricist",
|
||||
"Recording Location",
|
||||
"During Recording",
|
||||
"During Performance",
|
||||
"Video Capture",
|
||||
"Fish",
|
||||
"Illustration",
|
||||
"Band Logotype",
|
||||
"Publisher Logotype",
|
||||
];
|
||||
|
||||
/// Mime types for covers.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
|
@ -17,6 +45,19 @@ pub enum MimeType {
|
|||
Gif,
|
||||
}
|
||||
|
||||
impl MimeType {
|
||||
/// Converts the `MimeType` to an ape str
|
||||
pub fn as_ape(self) -> &'static [u8; 4] {
|
||||
match self {
|
||||
MimeType::Png => b"PNG\0",
|
||||
MimeType::Jpeg => b"JPEG",
|
||||
MimeType::Tiff => b"TIFF",
|
||||
MimeType::Bmp => b"BMP\0",
|
||||
MimeType::Gif => b"GIF\0",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for MimeType {
|
||||
type Error = Error;
|
||||
fn try_from(inp: &str) -> Result<Self> {
|
||||
|
@ -49,33 +90,182 @@ impl From<MimeType> for String {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait PicType {
|
||||
#[cfg(any(
|
||||
feature = "format-id3",
|
||||
feature = "format-vorbis",
|
||||
feature = "format-opus",
|
||||
feature = "format-flac"
|
||||
))]
|
||||
fn as_u32(&self) -> u32;
|
||||
#[cfg(feature = "format-ape")]
|
||||
fn as_ape_key(&self) -> &str;
|
||||
#[cfg(any(
|
||||
feature = "format-id3",
|
||||
feature = "format-vorbis",
|
||||
feature = "format-opus",
|
||||
feature = "format-flac"
|
||||
))]
|
||||
fn from_u32(bytes: u32) -> PictureType;
|
||||
#[cfg(feature = "format-ape")]
|
||||
fn from_ape_key(key: &str) -> PictureType;
|
||||
}
|
||||
|
||||
/// The picture type
|
||||
#[cfg(not(feature = "format-id3"))]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum PictureType {
|
||||
/// Represents the front cover of an album
|
||||
CoverFront,
|
||||
/// Represents the back cover of an album
|
||||
CoverBack,
|
||||
/// Covers all other possible types
|
||||
Other,
|
||||
Icon,
|
||||
OtherIcon,
|
||||
CoverFront,
|
||||
CoverBack,
|
||||
Leaflet,
|
||||
Media,
|
||||
LeadArtist,
|
||||
Artist,
|
||||
Conductor,
|
||||
Band,
|
||||
Composer,
|
||||
Lyricist,
|
||||
RecordingLocation,
|
||||
DuringRecording,
|
||||
DuringPerformance,
|
||||
ScreenCapture,
|
||||
BrightFish,
|
||||
Illustration,
|
||||
BandLogo,
|
||||
PublisherLogo,
|
||||
Undefined(u8),
|
||||
}
|
||||
|
||||
impl From<&id3PicType> for PictureType {
|
||||
fn from(inp: &id3PicType) -> Self {
|
||||
match inp {
|
||||
id3PicType::CoverFront => PictureType::CoverFront,
|
||||
id3PicType::CoverBack => PictureType::CoverBack,
|
||||
_ => PictureType::Other,
|
||||
/// Alias for PictureType
|
||||
#[cfg(not(feature = "format-id3"))]
|
||||
pub type PictureType = PictureType;
|
||||
/// Alias for PictureType
|
||||
#[cfg(feature = "format-id3")]
|
||||
pub type PictureType = id3::frame::PictureType;
|
||||
|
||||
impl PicType for PictureType {
|
||||
#[cfg(any(
|
||||
feature = "format-id3",
|
||||
feature = "format-vorbis",
|
||||
feature = "format-opus",
|
||||
feature = "format-flac"
|
||||
))]
|
||||
fn as_u32(&self) -> u32 {
|
||||
match self {
|
||||
Self::Other => 0,
|
||||
Self::Icon => 1,
|
||||
Self::OtherIcon => 2,
|
||||
Self::CoverFront => 3,
|
||||
Self::CoverBack => 4,
|
||||
Self::Leaflet => 5,
|
||||
Self::Media => 6,
|
||||
Self::LeadArtist => 7,
|
||||
Self::Artist => 8,
|
||||
Self::Conductor => 9,
|
||||
Self::Band => 10,
|
||||
Self::Composer => 11,
|
||||
Self::Lyricist => 12,
|
||||
Self::RecordingLocation => 13,
|
||||
Self::DuringRecording => 14,
|
||||
Self::DuringPerformance => 15,
|
||||
Self::ScreenCapture => 16,
|
||||
Self::BrightFish => 17,
|
||||
Self::Illustration => 18,
|
||||
Self::BandLogo => 19,
|
||||
Self::PublisherLogo => 20,
|
||||
Self::Undefined(i) => u32::from(i.to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PictureType> for id3PicType {
|
||||
fn from(inp: PictureType) -> Self {
|
||||
match inp {
|
||||
PictureType::CoverFront => id3PicType::CoverFront,
|
||||
PictureType::CoverBack => id3PicType::CoverBack,
|
||||
_ => id3PicType::Other,
|
||||
#[cfg(feature = "format-ape")]
|
||||
fn as_ape_key(&self) -> &str {
|
||||
match self {
|
||||
Self::Other => "Cover Art (Other)",
|
||||
Self::Icon => "Cover Art (Png Icon)",
|
||||
Self::OtherIcon => "Cover Art (Icon)",
|
||||
Self::CoverFront => "Cover Art (Front)",
|
||||
Self::CoverBack => "Cover Art (Back)",
|
||||
Self::Leaflet => "Cover Art (Leaflet)",
|
||||
Self::Media => "Cover Art (Media)",
|
||||
Self::LeadArtist => "Cover Art (Lead Artist)",
|
||||
Self::Artist => "Cover Art (Artist)",
|
||||
Self::Conductor => "Cover Art (Conductor)",
|
||||
Self::Band => "Cover Art (Band)",
|
||||
Self::Composer => "Cover Art (Composer)",
|
||||
Self::Lyricist => "Cover Art (Lyricist)",
|
||||
Self::RecordingLocation => "Cover Art (Recording Location)",
|
||||
Self::DuringRecording => "Cover Art (During Recording)",
|
||||
Self::DuringPerformance => "Cover Art (During Performance)",
|
||||
Self::ScreenCapture => "Cover Art (Video Capture)",
|
||||
Self::BrightFish => "Cover Art (Fish)",
|
||||
Self::Illustration => "Cover Art (Illustration)",
|
||||
Self::BandLogo => "Cover Art (Band Logotype)",
|
||||
Self::PublisherLogo => "Cover Art (Publisher Logotype)",
|
||||
Self::Undefined(_) => "",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
feature = "format-id3",
|
||||
feature = "format-vorbis",
|
||||
feature = "format-opus",
|
||||
feature = "format-flac"
|
||||
))]
|
||||
fn from_u32(bytes: u32) -> Self {
|
||||
match bytes {
|
||||
0 => Self::Other,
|
||||
1 => Self::Icon,
|
||||
2 => Self::OtherIcon,
|
||||
3 => Self::CoverFront,
|
||||
4 => Self::CoverBack,
|
||||
5 => Self::Leaflet,
|
||||
6 => Self::Media,
|
||||
7 => Self::LeadArtist,
|
||||
8 => Self::Artist,
|
||||
9 => Self::Conductor,
|
||||
10 => Self::Band,
|
||||
11 => Self::Composer,
|
||||
12 => Self::Lyricist,
|
||||
13 => Self::RecordingLocation,
|
||||
14 => Self::DuringRecording,
|
||||
15 => Self::DuringPerformance,
|
||||
16 => Self::ScreenCapture,
|
||||
17 => Self::BrightFish,
|
||||
18 => Self::Illustration,
|
||||
19 => Self::BandLogo,
|
||||
20 => Self::PublisherLogo,
|
||||
i => Self::Undefined(i as u8),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "format-ape")]
|
||||
fn from_ape_key(key: &str) -> Self {
|
||||
match key {
|
||||
"Cover Art (Other)" => Self::Other,
|
||||
"Cover Art (Png Icon)" => Self::Icon,
|
||||
"Cover Art (Icon)" => Self::OtherIcon,
|
||||
"Cover Art (Front)" => Self::CoverFront,
|
||||
"Cover Art (Back)" => Self::CoverBack,
|
||||
"Cover Art (Leaflet)" => Self::Leaflet,
|
||||
"Cover Art (Media)" => Self::Media,
|
||||
"Cover Art (Lead Artist)" => Self::LeadArtist,
|
||||
"Cover Art (Artist)" => Self::Artist,
|
||||
"Cover Art (Conductor)" => Self::Conductor,
|
||||
"Cover Art (Band)" => Self::Band,
|
||||
"Cover Art (Composer)" => Self::Composer,
|
||||
"Cover Art (Lyricist)" => Self::Lyricist,
|
||||
"Cover Art (Recording Location)" => Self::RecordingLocation,
|
||||
"Cover Art (During Recording)" => Self::DuringRecording,
|
||||
"Cover Art (During Performance)" => Self::DuringPerformance,
|
||||
"Cover Art (Video Capture)" => Self::ScreenCapture,
|
||||
"Cover Art (Fish)" => Self::BrightFish,
|
||||
"Cover Art (Illustration)" => Self::Illustration,
|
||||
"Cover Art (Band Logotype)" => Self::BandLogo,
|
||||
"Cover Art (Publisher Logotype)" => Self::PublisherLogo,
|
||||
_ => Self::Undefined(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -87,17 +277,203 @@ pub struct Picture {
|
|||
pub pic_type: PictureType,
|
||||
/// The picture's mimetype
|
||||
pub mime_type: MimeType,
|
||||
/// The picture's description
|
||||
pub description: Option<Cow<'static, str>>,
|
||||
/// The picture's actual data
|
||||
pub data: Vec<u8>,
|
||||
pub data: Cow<'static, [u8]>,
|
||||
}
|
||||
|
||||
impl Picture {
|
||||
/// Create a new `Picture`
|
||||
pub fn new(pic_type: PictureType, data: Vec<u8>, mime_type: MimeType) -> Self {
|
||||
pub fn new(
|
||||
pic_type: PictureType,
|
||||
mime_type: MimeType,
|
||||
description: Option<Cow<'static, str>>,
|
||||
data: Cow<'static, [u8]>,
|
||||
) -> Self {
|
||||
Self {
|
||||
pic_type,
|
||||
mime_type,
|
||||
description,
|
||||
data,
|
||||
}
|
||||
}
|
||||
/// Convert the `Picture` back to an APIC byte vec:
|
||||
///
|
||||
/// * Id3v2 APIC
|
||||
/// * Vorbis METADATA_BLOCK_PICTURE
|
||||
pub fn as_apic_bytes(&self) -> Vec<u8> {
|
||||
let picture_type = self.pic_type.as_u32().to_le_bytes();
|
||||
|
||||
let mime_str = String::from(self.mime_type);
|
||||
let mime_len = mime_str.len() as u32;
|
||||
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
data.extend(picture_type.iter());
|
||||
data.extend(mime_len.to_le_bytes().iter());
|
||||
data.extend(mime_str.as_bytes().iter());
|
||||
|
||||
if let Some(desc) = self.description.clone() {
|
||||
let desc_str = desc.to_string();
|
||||
let desc_len = desc_str.len() as u32;
|
||||
|
||||
data.extend(desc_len.to_le_bytes().iter());
|
||||
data.extend(desc_str.as_bytes().iter());
|
||||
}
|
||||
|
||||
data.extend(self.data.iter());
|
||||
|
||||
data
|
||||
}
|
||||
/// Convert the `Picture` back to an APEv2 byte vec:
|
||||
///
|
||||
/// * APEv2 Cover Art
|
||||
pub fn as_ape_bytes(&self) -> Vec<u8> {
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
|
||||
if let Some(desc) = self.description.clone() {
|
||||
let desc_str = desc.to_string();
|
||||
data.extend(desc_str.as_bytes().iter());
|
||||
data.extend([0].iter())
|
||||
}
|
||||
|
||||
data.extend(self.mime_type.as_ape().iter());
|
||||
data.extend(self.data.iter());
|
||||
|
||||
data
|
||||
}
|
||||
/// Get a `Picture` from APIC bytes:
|
||||
///
|
||||
/// * Id3v2 APIC
|
||||
/// * Vorbis METADATA_BLOCK_PICTURE
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return [`Error::InvalidData`] if at any point it's unable to parse the data
|
||||
pub fn from_apic_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
let data = match base64::decode(bytes) {
|
||||
Ok(o) => o,
|
||||
Err(_) => bytes.to_vec(),
|
||||
};
|
||||
|
||||
let mut cursor = Cursor::new(data);
|
||||
|
||||
if let Ok(bytes) = cursor.read_u32::<LittleEndian>() {
|
||||
let picture_type = PictureType::from_u32(bytes);
|
||||
|
||||
if let Ok(mime_len) = cursor.read_u32::<LittleEndian>() {
|
||||
let mut buf = vec![0; mime_len as usize];
|
||||
cursor.read_exact(&mut buf)?;
|
||||
|
||||
if let Ok(mime_type_str) = String::from_utf8(buf) {
|
||||
if let Ok(mime_type) = MimeType::try_from(&*mime_type_str) {
|
||||
let mut description = None;
|
||||
|
||||
if let Ok(desc_len) = cursor.read_u32::<LittleEndian>() {
|
||||
if cursor.get_ref().len()
|
||||
>= (cursor.position() as u32 + desc_len) as usize
|
||||
{
|
||||
let mut buf = vec![0; desc_len as usize];
|
||||
cursor.read_exact(&mut buf)?;
|
||||
|
||||
if let Ok(desc) = String::from_utf8(buf) {
|
||||
description = Some(Cow::from(desc));
|
||||
}
|
||||
} else {
|
||||
cursor.set_position(cursor.position() - 4)
|
||||
}
|
||||
}
|
||||
|
||||
let pos = (cursor.position()) as usize;
|
||||
let content = &cursor.into_inner()[pos..];
|
||||
|
||||
return Ok(Self {
|
||||
pic_type: picture_type,
|
||||
mime_type,
|
||||
description,
|
||||
data: Cow::from(content.to_vec()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::InvalidData)
|
||||
}
|
||||
/// Get a `Picture` from APEv2 bytes:
|
||||
///
|
||||
/// * APEv2 Cover Art
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return [`Error::InvalidData`] if at any point it's unable to parse the data
|
||||
pub fn from_ape_bytes(key: &str, bytes: &[u8]) -> Result<Self> {
|
||||
if !bytes.is_empty() {
|
||||
fn read_text(c: &mut Cursor<Vec<u8>>) -> String {
|
||||
let mut text = String::new();
|
||||
|
||||
while let Ok(ch) = c.read_u8() {
|
||||
if ch != b'\0' {
|
||||
text.push(char::from(ch));
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
text
|
||||
}
|
||||
|
||||
let pic_type = PictureType::from_ape_key(key);
|
||||
|
||||
let mut description = None;
|
||||
let mut mime_type = None;
|
||||
|
||||
let mut cursor = Cursor::new(bytes.to_vec());
|
||||
|
||||
let mut i = 0;
|
||||
|
||||
loop {
|
||||
i += 1;
|
||||
|
||||
if i == 3 {
|
||||
break;
|
||||
}
|
||||
|
||||
let text = read_text(&mut cursor);
|
||||
|
||||
let mime = match text.as_bytes() {
|
||||
b"PNG\0" => Some(MimeType::Png),
|
||||
b"JPEG" => Some(MimeType::Jpeg),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if mime.is_none() {
|
||||
description = Some(Cow::from(text));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
mime_type = mime;
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(mime_type) = mime_type {
|
||||
let pos = cursor.position() as usize;
|
||||
let mut data = cursor.into_inner()[pos..].to_vec();
|
||||
data.insert(0, 0);
|
||||
|
||||
let data = Cow::from(data);
|
||||
|
||||
return Ok(Picture {
|
||||
pic_type,
|
||||
mime_type,
|
||||
description,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::InvalidData)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue