mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-12-15 23:22:30 +00:00
907 lines
23 KiB
Rust
907 lines
23 KiB
Rust
use crate::error::{ErrorKind, LoftyError, Result};
|
|
#[cfg(feature = "id3v2")]
|
|
use crate::{
|
|
error::{Id3v2Error, Id3v2ErrorKind},
|
|
id3::v2::{util::text_utils::TextEncoding, Id3v2Version},
|
|
};
|
|
|
|
use std::borrow::Cow;
|
|
use std::fmt::{Debug, Formatter};
|
|
#[cfg(any(feature = "vorbis_comments", feature = "ape", feature = "id3v2"))]
|
|
use std::io::Cursor;
|
|
use std::io::Read;
|
|
#[cfg(feature = "id3v2")]
|
|
use std::io::Write;
|
|
#[cfg(any(feature = "vorbis_comments", feature = "ape"))]
|
|
use std::io::{Seek, SeekFrom};
|
|
|
|
#[cfg(any(feature = "vorbis_comments"))]
|
|
use byteorder::BigEndian;
|
|
#[cfg(any(feature = "vorbis_comments", feature = "id3v2", feature = "ape"))]
|
|
use byteorder::ReadBytesExt;
|
|
#[cfg(feature = "id3v2")]
|
|
use byteorder::WriteBytesExt;
|
|
|
|
#[cfg(feature = "ape")]
|
|
/// Common picture item keys for APE
|
|
pub const APE_PICTURE_TYPES: [&str; 21] = [
|
|
"Cover Art (Other)",
|
|
"Cover Art (Png Icon)",
|
|
"Cover Art (Icon)",
|
|
"Cover Art (Front)",
|
|
"Cover Art (Back)",
|
|
"Cover Art (Leaflet)",
|
|
"Cover Art (Media)",
|
|
"Cover Art (Lead Artist)",
|
|
"Cover Art (Artist)",
|
|
"Cover Art (Conductor)",
|
|
"Cover Art (Band)",
|
|
"Cover Art (Composer)",
|
|
"Cover Art (Lyricist)",
|
|
"Cover Art (Recording Location)",
|
|
"Cover Art (During Recording)",
|
|
"Cover Art (During Performance)",
|
|
"Cover Art (Video Capture)",
|
|
"Cover Art (Fish)",
|
|
"Cover Art (Illustration)",
|
|
"Cover Art (Band Logotype)",
|
|
"Cover Art (Publisher Logotype)",
|
|
];
|
|
|
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
|
#[non_exhaustive]
|
|
/// Mime types for pictures.
|
|
pub enum MimeType {
|
|
/// PNG image
|
|
Png,
|
|
/// JPEG image
|
|
Jpeg,
|
|
/// TIFF image
|
|
Tiff,
|
|
/// BMP image
|
|
Bmp,
|
|
/// GIF image
|
|
Gif,
|
|
/// Some unknown mimetype
|
|
Unknown(String),
|
|
/// No mimetype
|
|
None,
|
|
}
|
|
|
|
impl ToString for MimeType {
|
|
fn to_string(&self) -> String {
|
|
match self {
|
|
MimeType::Jpeg => "image/jpeg".to_string(),
|
|
MimeType::Png => "image/png".to_string(),
|
|
MimeType::Tiff => "image/tiff".to_string(),
|
|
MimeType::Bmp => "image/bmp".to_string(),
|
|
MimeType::Gif => "image/gif".to_string(),
|
|
MimeType::Unknown(unknown) => unknown.clone(),
|
|
MimeType::None => String::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MimeType {
|
|
#[allow(clippy::should_implement_trait)]
|
|
/// Get a `MimeType` from a string
|
|
pub fn from_str(mime_type: &str) -> Self {
|
|
match &*mime_type.to_lowercase() {
|
|
"image/jpeg" => Self::Jpeg,
|
|
"image/png" => Self::Png,
|
|
"image/tiff" => Self::Tiff,
|
|
"image/bmp" => Self::Bmp,
|
|
"image/gif" => Self::Gif,
|
|
"" => Self::None,
|
|
_ => Self::Unknown(mime_type.to_string()),
|
|
}
|
|
}
|
|
|
|
/// Get a &str from a `MimeType`
|
|
pub fn as_str(&self) -> &str {
|
|
match self {
|
|
MimeType::Jpeg => "image/jpeg",
|
|
MimeType::Png => "image/png",
|
|
MimeType::Tiff => "image/tiff",
|
|
MimeType::Bmp => "image/bmp",
|
|
MimeType::Gif => "image/gif",
|
|
MimeType::Unknown(unknown) => &*unknown,
|
|
MimeType::None => "",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The picture type, according to ID3v2 APIC
|
|
#[allow(missing_docs)]
|
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
|
#[non_exhaustive]
|
|
pub enum PictureType {
|
|
Other,
|
|
Icon,
|
|
OtherIcon,
|
|
CoverFront,
|
|
CoverBack,
|
|
Leaflet,
|
|
Media,
|
|
LeadArtist,
|
|
Artist,
|
|
Conductor,
|
|
Band,
|
|
Composer,
|
|
Lyricist,
|
|
RecordingLocation,
|
|
DuringRecording,
|
|
DuringPerformance,
|
|
ScreenCapture,
|
|
BrightFish,
|
|
Illustration,
|
|
BandLogo,
|
|
PublisherLogo,
|
|
Undefined(u8),
|
|
}
|
|
|
|
impl PictureType {
|
|
// ID3/OGG specific methods
|
|
|
|
#[cfg(any(feature = "id3v2", feature = "vorbis_comments"))]
|
|
/// Get a u8 from a `PictureType` according to ID3v2 APIC
|
|
pub fn as_u8(&self) -> u8 {
|
|
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) => *i,
|
|
}
|
|
}
|
|
|
|
#[cfg(any(feature = "id3v2", feature = "vorbis_comments"))]
|
|
/// Get a `PictureType` from a u8 according to ID3v2 APIC
|
|
pub fn from_u8(byte: u8) -> Self {
|
|
match byte {
|
|
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),
|
|
}
|
|
}
|
|
|
|
// APE specific methods
|
|
|
|
#[cfg(feature = "ape")]
|
|
/// Get an APE item key from a `PictureType`
|
|
pub fn as_ape_key(&self) -> Option<&str> {
|
|
match self {
|
|
Self::Other => Some("Cover Art (Other)"),
|
|
Self::Icon => Some("Cover Art (Png Icon)"),
|
|
Self::OtherIcon => Some("Cover Art (Icon)"),
|
|
Self::CoverFront => Some("Cover Art (Front)"),
|
|
Self::CoverBack => Some("Cover Art (Back)"),
|
|
Self::Leaflet => Some("Cover Art (Leaflet)"),
|
|
Self::Media => Some("Cover Art (Media)"),
|
|
Self::LeadArtist => Some("Cover Art (Lead Artist)"),
|
|
Self::Artist => Some("Cover Art (Artist)"),
|
|
Self::Conductor => Some("Cover Art (Conductor)"),
|
|
Self::Band => Some("Cover Art (Band)"),
|
|
Self::Composer => Some("Cover Art (Composer)"),
|
|
Self::Lyricist => Some("Cover Art (Lyricist)"),
|
|
Self::RecordingLocation => Some("Cover Art (Recording Location)"),
|
|
Self::DuringRecording => Some("Cover Art (During Recording)"),
|
|
Self::DuringPerformance => Some("Cover Art (During Performance)"),
|
|
Self::ScreenCapture => Some("Cover Art (Video Capture)"),
|
|
Self::BrightFish => Some("Cover Art (Fish)"),
|
|
Self::Illustration => Some("Cover Art (Illustration)"),
|
|
Self::BandLogo => Some("Cover Art (Band Logotype)"),
|
|
Self::PublisherLogo => Some("Cover Art (Publisher Logotype)"),
|
|
Self::Undefined(_) => None,
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "ape")]
|
|
/// Get a `PictureType` from an APE item key
|
|
pub 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),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "vorbis_comments")]
|
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
|
|
/// Information about a [`Picture`]
|
|
///
|
|
/// This information is necessary for FLAC's `METADATA_BLOCK_PICTURE`.
|
|
/// See [`Picture::as_flac_bytes`] for more information.
|
|
pub struct PictureInformation {
|
|
/// The picture's width in pixels
|
|
pub width: u32,
|
|
/// The picture's height in pixels
|
|
pub height: u32,
|
|
/// The picture's color depth in bits per pixel
|
|
pub color_depth: u32,
|
|
/// The number of colors used
|
|
pub num_colors: u32,
|
|
}
|
|
|
|
#[cfg(feature = "vorbis_comments")]
|
|
impl PictureInformation {
|
|
/// Attempt to extract [`PictureInformation`] from a [`Picture`]
|
|
///
|
|
/// NOTE: This only supports PNG and JPEG images. If another image is provided,
|
|
/// the `PictureInformation` will be zeroed out.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// * `picture.data` is less than 8 bytes in length
|
|
/// * See [`PictureInformation::from_png`] and [`PictureInformation::from_jpeg`]
|
|
pub fn from_picture(picture: &Picture) -> Result<Self> {
|
|
let reader = &mut &*picture.data;
|
|
|
|
if reader.len() < 8 {
|
|
return Err(LoftyError::new(ErrorKind::NotAPicture));
|
|
}
|
|
|
|
match reader[..4] {
|
|
[0x89, b'P', b'N', b'G'] => Ok(Self::from_png(reader).unwrap_or_default()),
|
|
[0xFF, 0xD8, 0xFF, ..] => Ok(Self::from_jpeg(reader).unwrap_or_default()),
|
|
_ => Ok(Self::default()),
|
|
}
|
|
}
|
|
|
|
/// Attempt to extract [`PictureInformation`] from a PNG
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// * `reader` is not a valid PNG
|
|
pub fn from_png(mut data: &[u8]) -> Result<Self> {
|
|
let reader = &mut data;
|
|
|
|
let mut sig = [0; 8];
|
|
reader.read_exact(&mut sig)?;
|
|
|
|
if sig != [0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A] {
|
|
return Err(LoftyError::new(ErrorKind::NotAPicture));
|
|
}
|
|
|
|
let mut ihdr = [0; 8];
|
|
reader.read_exact(&mut ihdr)?;
|
|
|
|
// Verify the signature is immediately followed by the IHDR chunk
|
|
if !ihdr.ends_with(&[0x49, 0x48, 0x44, 0x52]) {
|
|
return Err(LoftyError::new(ErrorKind::NotAPicture));
|
|
}
|
|
|
|
let width = reader.read_u32::<BigEndian>()?;
|
|
let height = reader.read_u32::<BigEndian>()?;
|
|
let mut color_depth = u32::from(reader.read_u8()?);
|
|
let color_type = reader.read_u8()?;
|
|
|
|
match color_type {
|
|
2 => color_depth *= 3,
|
|
4 | 6 => color_depth *= 4,
|
|
_ => {},
|
|
}
|
|
|
|
let mut ret = Self {
|
|
width,
|
|
height,
|
|
color_depth,
|
|
num_colors: 0,
|
|
};
|
|
|
|
// The color type 3 (indexed-color) means there should be
|
|
// a "PLTE" chunk, whose data can be used in the `num_colors`
|
|
// field. It isn't really applicable to other color types.
|
|
if color_type != 3 {
|
|
return Ok(ret);
|
|
}
|
|
|
|
let mut reader = Cursor::new(reader);
|
|
|
|
// Skip 7 bytes
|
|
// Compression method (1)
|
|
// Filter method (1)
|
|
// Interlace method (1)
|
|
// CRC (4)
|
|
reader.seek(SeekFrom::Current(7))?;
|
|
|
|
let mut chunk_type = [0; 4];
|
|
|
|
while let (Ok(size), Ok(())) = (
|
|
reader.read_u32::<BigEndian>(),
|
|
reader.read_exact(&mut chunk_type),
|
|
) {
|
|
if &chunk_type == b"PLTE" {
|
|
// The PLTE chunk contains 1-256 3-byte entries
|
|
ret.num_colors = size / 3;
|
|
break;
|
|
}
|
|
|
|
// Skip the chunk's data (size) and CRC (4 bytes)
|
|
reader.seek(SeekFrom::Current(i64::from(size + 4)))?;
|
|
}
|
|
|
|
Ok(ret)
|
|
}
|
|
|
|
/// Attempt to extract [`PictureInformation`] from a JPEG
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// * `reader` is not a JPEG image
|
|
/// * `reader` does not contain a `SOFn` frame
|
|
pub fn from_jpeg(mut data: &[u8]) -> Result<Self> {
|
|
let reader = &mut data;
|
|
|
|
let mut frame_marker = [0; 4];
|
|
reader.read_exact(&mut frame_marker)?;
|
|
|
|
if !matches!(frame_marker, [0xFF, 0xD8, 0xFF, ..]) {
|
|
return Err(LoftyError::new(ErrorKind::NotAPicture));
|
|
}
|
|
|
|
let mut section_len = reader.read_u16::<BigEndian>()?;
|
|
|
|
let mut reader = Cursor::new(reader);
|
|
|
|
// The length contains itself
|
|
reader.seek(SeekFrom::Current(i64::from(section_len - 2)))?;
|
|
|
|
while let Ok(0xFF) = reader.read_u8() {
|
|
let marker = reader.read_u8()?;
|
|
section_len = reader.read_u16::<BigEndian>()?;
|
|
|
|
// This marks the SOS (Start of Scan), which is
|
|
// the end of the header
|
|
if marker == 0xDA {
|
|
break;
|
|
}
|
|
|
|
// We are looking for a frame with a "SOFn" marker,
|
|
// with `n` either being 0 or 2. Since there isn't a
|
|
// header like PNG, we actually need to search for this
|
|
// frame
|
|
if marker == 0xC0 || marker == 0xC2 {
|
|
let precision = reader.read_u8()?;
|
|
let height = u32::from(reader.read_u16::<BigEndian>()?);
|
|
let width = u32::from(reader.read_u16::<BigEndian>()?);
|
|
let components = reader.read_u8()?;
|
|
|
|
return Ok(Self {
|
|
width,
|
|
height,
|
|
color_depth: u32::from(precision * components),
|
|
num_colors: 0,
|
|
});
|
|
}
|
|
|
|
reader.seek(SeekFrom::Current(i64::from(section_len - 2)))?;
|
|
}
|
|
|
|
Err(LoftyError::new(ErrorKind::NotAPicture))
|
|
}
|
|
}
|
|
|
|
/// Represents a picture.
|
|
#[derive(Clone, Eq, PartialEq, Hash)]
|
|
pub struct Picture {
|
|
/// The picture type according to ID3v2 APIC
|
|
pub(crate) pic_type: PictureType,
|
|
/// The picture's mimetype
|
|
pub(crate) mime_type: MimeType,
|
|
/// The picture's description
|
|
pub(crate) description: Option<Cow<'static, str>>,
|
|
/// The binary data of the picture
|
|
pub(crate) data: Cow<'static, [u8]>,
|
|
}
|
|
|
|
impl Debug for Picture {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("Picture")
|
|
.field("pic_type", &self.pic_type)
|
|
.field("mime_type", &self.mime_type)
|
|
.field("description", &self.description)
|
|
.field("data", &format!("<{} bytes>", self.data.len()))
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl Picture {
|
|
/// Create a [`Picture`] from a reader
|
|
///
|
|
/// NOTES:
|
|
///
|
|
/// * This is for reading picture data only, from
|
|
/// a [`File`](std::fs::File) for example.
|
|
/// * `pic_type` will always be [`PictureType::Other`],
|
|
/// be sure to change it accordingly if writing.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// * `reader` contains less than 8 bytes
|
|
/// * `reader` does not contain a supported format.
|
|
/// See [`MimeType`] for valid formats
|
|
pub fn from_reader<R>(reader: &mut R) -> Result<Self>
|
|
where
|
|
R: Read,
|
|
{
|
|
let mut data = Vec::new();
|
|
reader.read_to_end(&mut data)?;
|
|
|
|
if data.len() < 8 {
|
|
return Err(LoftyError::new(ErrorKind::NotAPicture));
|
|
}
|
|
|
|
let mime_type = Self::mimetype_from_bin(&data[..8])?;
|
|
|
|
Ok(Self {
|
|
pic_type: PictureType::Other,
|
|
mime_type,
|
|
description: None,
|
|
data: data.into(),
|
|
})
|
|
}
|
|
|
|
/// Create a new `Picture`
|
|
///
|
|
/// NOTE: This will **not** verify `data`'s signature.
|
|
/// This should only be used if all data has been verified
|
|
/// beforehand.
|
|
pub fn new_unchecked(
|
|
pic_type: PictureType,
|
|
mime_type: MimeType,
|
|
description: Option<String>,
|
|
data: Vec<u8>,
|
|
) -> Self {
|
|
Self {
|
|
pic_type,
|
|
mime_type,
|
|
description: description.map(Cow::from),
|
|
data: Cow::from(data),
|
|
}
|
|
}
|
|
|
|
/// Returns the [`PictureType`]
|
|
pub fn pic_type(&self) -> PictureType {
|
|
self.pic_type
|
|
}
|
|
|
|
/// Sets the [`PictureType`]
|
|
pub fn set_pic_type(&mut self, pic_type: PictureType) {
|
|
self.pic_type = pic_type
|
|
}
|
|
|
|
/// Returns the [`MimeType`]
|
|
///
|
|
/// The `mime_type` is determined from the `data`, and
|
|
/// is immutable.
|
|
pub fn mime_type(&self) -> &MimeType {
|
|
&self.mime_type
|
|
}
|
|
|
|
/// Returns the description
|
|
pub fn description(&self) -> Option<&str> {
|
|
self.description.as_deref()
|
|
}
|
|
|
|
/// Sets the description
|
|
pub fn set_description(&mut self, description: Option<String>) {
|
|
self.description = description.map(Cow::from);
|
|
}
|
|
|
|
/// Returns the picture data
|
|
pub fn data(&self) -> &[u8] {
|
|
&self.data
|
|
}
|
|
|
|
#[cfg(feature = "id3v2")]
|
|
/// Convert a [`Picture`] to a ID3v2 A/PIC byte Vec
|
|
///
|
|
/// NOTE: This does not include the frame header
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// * Too much data was provided
|
|
///
|
|
/// ID3v2.2:
|
|
///
|
|
/// * The mimetype is not [`MimeType::Png`] or [`MimeType::Jpeg`]
|
|
pub fn as_apic_bytes(
|
|
&self,
|
|
version: Id3v2Version,
|
|
text_encoding: TextEncoding,
|
|
) -> Result<Vec<u8>> {
|
|
use crate::id3::v2::util::text_utils;
|
|
|
|
let mut data = vec![text_encoding as u8];
|
|
|
|
let max_size = match version {
|
|
// ID3v2.2 uses a 24-bit number for sizes
|
|
Id3v2Version::V2 => 0xFFFF_FF16_u64,
|
|
_ => u64::from(u32::MAX),
|
|
};
|
|
|
|
if version == Id3v2Version::V2 {
|
|
// ID3v2.2 PIC is pretty limited with formats
|
|
let format = match self.mime_type {
|
|
MimeType::Png => "PNG",
|
|
MimeType::Jpeg => "JPG",
|
|
_ => {
|
|
return Err(Id3v2Error::new(Id3v2ErrorKind::BadPictureFormat(
|
|
self.mime_type.to_string(),
|
|
))
|
|
.into())
|
|
},
|
|
};
|
|
|
|
data.write_all(format.as_bytes())?;
|
|
} else {
|
|
data.write_all(self.mime_type.as_str().as_bytes())?;
|
|
data.write_u8(0)?;
|
|
};
|
|
|
|
data.write_u8(self.pic_type.as_u8())?;
|
|
|
|
match &self.description {
|
|
Some(description) => {
|
|
data.write_all(&*text_utils::encode_text(description, text_encoding, true))?
|
|
},
|
|
None => data.write_u8(0)?,
|
|
}
|
|
|
|
data.write_all(&*self.data)?;
|
|
|
|
if data.len() as u64 > max_size {
|
|
return Err(LoftyError::new(ErrorKind::TooMuchData));
|
|
}
|
|
|
|
Ok(data)
|
|
}
|
|
|
|
#[cfg(feature = "id3v2")]
|
|
/// Get a [`Picture`] and [`TextEncoding`] from ID3v2 A/PIC bytes:
|
|
///
|
|
/// NOTE: This expects *only* the frame content
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// * There isn't enough data present
|
|
/// * The data isn't a picture
|
|
///
|
|
/// ID3v2.2:
|
|
///
|
|
/// * The format is not "PNG" or "JPG"
|
|
pub fn from_apic_bytes(bytes: &[u8], version: Id3v2Version) -> Result<(Self, TextEncoding)> {
|
|
use crate::id3::v2::util::text_utils;
|
|
|
|
let mut cursor = Cursor::new(bytes);
|
|
|
|
let encoding = match TextEncoding::from_u8(cursor.read_u8()?) {
|
|
Some(encoding) => encoding,
|
|
None => return Err(LoftyError::new(ErrorKind::NotAPicture)),
|
|
};
|
|
|
|
let mime_type = if version == Id3v2Version::V2 {
|
|
let mut format = [0; 3];
|
|
cursor.read_exact(&mut format)?;
|
|
|
|
match format {
|
|
[b'P', b'N', b'G'] => MimeType::Png,
|
|
[b'J', b'P', b'G'] => MimeType::Jpeg,
|
|
_ => {
|
|
return Err(Id3v2Error::new(Id3v2ErrorKind::BadPictureFormat(
|
|
String::from_utf8_lossy(&format).into_owned(),
|
|
))
|
|
.into())
|
|
},
|
|
}
|
|
} else {
|
|
(text_utils::decode_text(&mut cursor, TextEncoding::UTF8, true)?)
|
|
.map_or(MimeType::None, |mime_type| MimeType::from_str(&*mime_type))
|
|
};
|
|
|
|
let pic_type = PictureType::from_u8(cursor.read_u8()?);
|
|
|
|
let description = text_utils::decode_text(&mut cursor, encoding, true)?.map(Cow::from);
|
|
|
|
let mut data = Vec::new();
|
|
cursor.read_to_end(&mut data)?;
|
|
|
|
Ok((
|
|
Picture {
|
|
pic_type,
|
|
mime_type,
|
|
description,
|
|
data: Cow::from(data),
|
|
},
|
|
encoding,
|
|
))
|
|
}
|
|
|
|
#[cfg(feature = "vorbis_comments")]
|
|
/// Convert a [`Picture`] to a base64 encoded FLAC `METADATA_BLOCK_PICTURE` String
|
|
///
|
|
/// Use `encode` to convert the picture to a base64 encoded String ([RFC 4648 §4](http://www.faqs.org/rfcs/rfc4648.html))
|
|
///
|
|
/// NOTES:
|
|
///
|
|
/// * This does not include a key (Vorbis comments) or METADATA_BLOCK_HEADER (FLAC blocks)
|
|
/// * FLAC blocks have different size requirements than OGG Vorbis/Opus, size is not checked here
|
|
/// * When writing to Vorbis comments, the data **must** be base64 encoded
|
|
pub fn as_flac_bytes(&self, picture_information: PictureInformation, encode: bool) -> Vec<u8> {
|
|
let mut data = Vec::<u8>::new();
|
|
|
|
let picture_type = u32::from(self.pic_type.as_u8()).to_be_bytes();
|
|
|
|
let mime_str = self.mime_type.to_string();
|
|
let mime_len = mime_str.len() as u32;
|
|
|
|
data.extend(picture_type);
|
|
data.extend(mime_len.to_be_bytes());
|
|
data.extend(mime_str.as_bytes());
|
|
|
|
if let Some(desc) = &self.description {
|
|
let desc_len = desc.len() as u32;
|
|
|
|
data.extend(desc_len.to_be_bytes());
|
|
data.extend(desc.as_bytes());
|
|
} else {
|
|
data.extend([0; 4]);
|
|
}
|
|
|
|
data.extend(picture_information.width.to_be_bytes());
|
|
data.extend(picture_information.height.to_be_bytes());
|
|
data.extend(picture_information.color_depth.to_be_bytes());
|
|
data.extend(picture_information.num_colors.to_be_bytes());
|
|
|
|
let pic_data = &self.data;
|
|
let pic_data_len = pic_data.len() as u32;
|
|
|
|
data.extend(pic_data_len.to_be_bytes());
|
|
data.extend(pic_data.iter());
|
|
|
|
if encode {
|
|
base64::encode(data).into_bytes()
|
|
} else {
|
|
data
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "vorbis_comments")]
|
|
/// Get a [`Picture`] from FLAC `METADATA_BLOCK_PICTURE` bytes:
|
|
///
|
|
/// NOTE: This takes both the base64 encoded string from Vorbis comments, and
|
|
/// the raw data from a FLAC block, specified with `encoded`.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// This function will return [`NotAPicture`][ErrorKind::NotAPicture] if
|
|
/// at any point it's unable to parse the data
|
|
pub fn from_flac_bytes(bytes: &[u8], encoded: bool) -> Result<(Self, PictureInformation)> {
|
|
if encoded {
|
|
let data =
|
|
base64::decode(bytes).map_err(|_| LoftyError::new(ErrorKind::NotAPicture))?;
|
|
Self::from_flac_bytes_inner(&*data)
|
|
} else {
|
|
Self::from_flac_bytes_inner(bytes)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "vorbis_comments")]
|
|
fn from_flac_bytes_inner(content: &[u8]) -> Result<(Self, PictureInformation)> {
|
|
use crate::macros::try_vec;
|
|
|
|
let mut size = content.len();
|
|
let mut reader = Cursor::new(content);
|
|
|
|
if size < 32 {
|
|
return Err(LoftyError::new(ErrorKind::NotAPicture));
|
|
}
|
|
|
|
let pic_ty = reader.read_u32::<BigEndian>()?;
|
|
size -= 4;
|
|
|
|
// ID3v2 APIC uses a single byte for picture type.
|
|
// Anything greater than that is probably invalid, so
|
|
// we just stop early
|
|
if pic_ty > 255 {
|
|
return Err(LoftyError::new(ErrorKind::NotAPicture));
|
|
}
|
|
|
|
let mime_len = reader.read_u32::<BigEndian>()? as usize;
|
|
size -= 4;
|
|
|
|
if mime_len > size {
|
|
return Err(LoftyError::new(ErrorKind::TooMuchData));
|
|
}
|
|
|
|
let mime_type_str = std::str::from_utf8(&content[8..8 + mime_len])?;
|
|
size -= mime_len;
|
|
|
|
reader.seek(SeekFrom::Current(mime_len as i64))?;
|
|
|
|
let desc_len = reader.read_u32::<BigEndian>()? as usize;
|
|
size -= 4;
|
|
|
|
let mut description = None;
|
|
if desc_len > 0 && desc_len < size {
|
|
let pos = 12 + mime_len;
|
|
|
|
if let Ok(desc) = std::str::from_utf8(&content[pos..pos + desc_len]) {
|
|
description = Some(Cow::from(desc.to_string()));
|
|
}
|
|
|
|
size -= desc_len;
|
|
reader.seek(SeekFrom::Current(desc_len as i64))?;
|
|
}
|
|
|
|
let width = reader.read_u32::<BigEndian>()?;
|
|
let height = reader.read_u32::<BigEndian>()?;
|
|
let color_depth = reader.read_u32::<BigEndian>()?;
|
|
let num_colors = reader.read_u32::<BigEndian>()?;
|
|
let data_len = reader.read_u32::<BigEndian>()? as usize;
|
|
size -= 20;
|
|
|
|
if data_len <= size {
|
|
let mut data = try_vec![0; data_len];
|
|
|
|
if let Ok(()) = reader.read_exact(&mut data) {
|
|
return Ok((
|
|
Self {
|
|
pic_type: PictureType::from_u8(pic_ty as u8),
|
|
mime_type: MimeType::from_str(mime_type_str),
|
|
description,
|
|
data: Cow::from(data),
|
|
},
|
|
PictureInformation {
|
|
width,
|
|
height,
|
|
color_depth,
|
|
num_colors,
|
|
},
|
|
));
|
|
}
|
|
}
|
|
|
|
Err(LoftyError::new(ErrorKind::NotAPicture))
|
|
}
|
|
|
|
#[cfg(feature = "ape")]
|
|
/// Convert a [`Picture`] to an APE Cover Art byte vec:
|
|
///
|
|
/// NOTE: This is only the picture data and description, a
|
|
/// key and terminating null byte will not be prepended.
|
|
/// To map a [`PictureType`] to an APE key see [`PictureType::as_ape_key`]
|
|
pub fn as_ape_bytes(&self) -> Vec<u8> {
|
|
let mut data: Vec<u8> = Vec::new();
|
|
|
|
if let Some(desc) = &self.description {
|
|
data.extend(desc.as_bytes());
|
|
}
|
|
|
|
data.push(0);
|
|
data.extend(self.data.iter());
|
|
|
|
data
|
|
}
|
|
|
|
#[cfg(feature = "ape")]
|
|
/// Get a [`Picture`] from an APEv2 binary item:
|
|
///
|
|
/// NOTE: This function expects `bytes` to contain *only* the APE item data
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// This function will return [`NotAPicture`](ErrorKind::NotAPicture)
|
|
/// 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() {
|
|
return Err(LoftyError::new(ErrorKind::NotAPicture));
|
|
}
|
|
|
|
let pic_type = PictureType::from_ape_key(key);
|
|
|
|
let reader = &mut &*bytes;
|
|
let mut pos = 0;
|
|
|
|
let mut description = None;
|
|
let mut desc_text = String::new();
|
|
|
|
while let Ok(ch) = reader.read_u8() {
|
|
pos += 1;
|
|
|
|
if ch == b'\0' {
|
|
break;
|
|
}
|
|
|
|
desc_text.push(char::from(ch));
|
|
}
|
|
|
|
if !desc_text.is_empty() {
|
|
description = Some(Cow::from(desc_text));
|
|
}
|
|
|
|
let mime_type = {
|
|
let mut identifier = [0; 8];
|
|
reader.read_exact(&mut identifier)?;
|
|
|
|
Self::mimetype_from_bin(&identifier[..])?
|
|
};
|
|
|
|
let data = Cow::from(bytes[pos..].to_vec());
|
|
|
|
Ok(Picture {
|
|
pic_type,
|
|
mime_type,
|
|
description,
|
|
data,
|
|
})
|
|
}
|
|
|
|
fn mimetype_from_bin(bytes: &[u8]) -> Result<MimeType> {
|
|
match bytes[..8] {
|
|
[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A] => Ok(MimeType::Png),
|
|
[0xFF, 0xD8, ..] => Ok(MimeType::Jpeg),
|
|
[b'G', b'I', b'F', 0x38, 0x37 | 0x39, b'a', ..] => Ok(MimeType::Gif),
|
|
[b'B', b'M', ..] => Ok(MimeType::Bmp),
|
|
[b'I', b'I', b'*', 0x00, ..] | [b'M', b'M', 0x00, b'*', ..] => Ok(MimeType::Tiff),
|
|
_ => Err(LoftyError::new(ErrorKind::NotAPicture)),
|
|
}
|
|
}
|
|
}
|