lofty/
picture.rs

1//! Format-agnostic picture handling
2
3use crate::config::ParsingMode;
4use crate::error::{ErrorKind, LoftyError, Result};
5use crate::macros::err;
6use crate::util::text::utf8_decode_str;
7
8use std::borrow::Cow;
9use std::fmt::{Debug, Display, Formatter};
10use std::io::{Cursor, Read, Seek, SeekFrom};
11
12use byteorder::{BigEndian, ReadBytesExt as _};
13use data_encoding::BASE64;
14
15/// Common picture item keys for APE
16pub const APE_PICTURE_TYPES: [&str; 21] = [
17	"Cover Art (Other)",
18	"Cover Art (Png Icon)",
19	"Cover Art (Icon)",
20	"Cover Art (Front)",
21	"Cover Art (Back)",
22	"Cover Art (Leaflet)",
23	"Cover Art (Media)",
24	"Cover Art (Lead Artist)",
25	"Cover Art (Artist)",
26	"Cover Art (Conductor)",
27	"Cover Art (Band)",
28	"Cover Art (Composer)",
29	"Cover Art (Lyricist)",
30	"Cover Art (Recording Location)",
31	"Cover Art (During Recording)",
32	"Cover Art (During Performance)",
33	"Cover Art (Video Capture)",
34	"Cover Art (Fish)",
35	"Cover Art (Illustration)",
36	"Cover Art (Band Logotype)",
37	"Cover Art (Publisher Logotype)",
38];
39
40/// MIME types for pictures.
41#[derive(Debug, Clone, Eq, PartialEq, Hash)]
42#[non_exhaustive]
43pub enum MimeType {
44	/// PNG image
45	Png,
46	/// JPEG image
47	Jpeg,
48	/// TIFF image
49	Tiff,
50	/// BMP image
51	Bmp,
52	/// GIF image
53	Gif,
54	/// Some unknown MIME type
55	Unknown(String),
56}
57
58impl MimeType {
59	/// Get a `MimeType` from a string
60	///
61	/// # Examples
62	///
63	/// ```rust
64	/// use lofty::picture::MimeType;
65	///
66	/// let jpeg_mimetype_str = "image/jpeg";
67	/// assert_eq!(MimeType::from_str(jpeg_mimetype_str), MimeType::Jpeg);
68	/// ```
69	#[must_use]
70	#[allow(clippy::should_implement_trait)] // Infallible in contrast to FromStr
71	pub fn from_str(mime_type: &str) -> Self {
72		match &*mime_type.to_lowercase() {
73			"image/jpeg" | "image/jpg" => Self::Jpeg,
74			"image/png" => Self::Png,
75			"image/tiff" => Self::Tiff,
76			"image/bmp" => Self::Bmp,
77			"image/gif" => Self::Gif,
78			_ => Self::Unknown(mime_type.to_owned()),
79		}
80	}
81
82	/// Get a &str from a `MimeType`
83	///
84	/// # Examples
85	///
86	/// ```rust
87	/// use lofty::picture::MimeType;
88	///
89	/// let jpeg_mimetype = MimeType::Jpeg;
90	/// assert_eq!(jpeg_mimetype.as_str(), "image/jpeg")
91	/// ```
92	#[must_use]
93	pub fn as_str(&self) -> &str {
94		match self {
95			MimeType::Jpeg => "image/jpeg",
96			MimeType::Png => "image/png",
97			MimeType::Tiff => "image/tiff",
98			MimeType::Bmp => "image/bmp",
99			MimeType::Gif => "image/gif",
100			MimeType::Unknown(unknown) => unknown,
101		}
102	}
103
104	/// Returns the extension for the `MimeType` if it is known
105	///
106	/// # Examples
107	///
108	/// ```rust
109	/// use lofty::picture::MimeType;
110	///
111	/// assert_eq!(MimeType::Jpeg.ext(), Some("jpg"));
112	/// ```
113	pub fn ext(&self) -> Option<&str> {
114		match self {
115			MimeType::Jpeg => Some("jpg"),
116			MimeType::Png => Some("png"),
117			MimeType::Tiff => Some("tif"),
118			MimeType::Bmp => Some("bmp"),
119			MimeType::Gif => Some("gif"),
120			MimeType::Unknown(_) => None,
121		}
122	}
123}
124
125impl Display for MimeType {
126	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
127		f.write_str(self.as_str())
128	}
129}
130
131/// The picture type, according to ID3v2 APIC
132#[allow(missing_docs)]
133#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
134#[non_exhaustive]
135pub enum PictureType {
136	Other,
137	Icon,
138	OtherIcon,
139	CoverFront,
140	CoverBack,
141	Leaflet,
142	Media,
143	LeadArtist,
144	Artist,
145	Conductor,
146	Band,
147	Composer,
148	Lyricist,
149	RecordingLocation,
150	DuringRecording,
151	DuringPerformance,
152	ScreenCapture,
153	BrightFish,
154	Illustration,
155	BandLogo,
156	PublisherLogo,
157	Undefined(u8),
158}
159
160impl PictureType {
161	// ID3/OGG specific methods
162
163	/// Get a `u8` from a `PictureType` according to ID3v2 APIC
164	pub fn as_u8(&self) -> u8 {
165		match self {
166			Self::Other => 0,
167			Self::Icon => 1,
168			Self::OtherIcon => 2,
169			Self::CoverFront => 3,
170			Self::CoverBack => 4,
171			Self::Leaflet => 5,
172			Self::Media => 6,
173			Self::LeadArtist => 7,
174			Self::Artist => 8,
175			Self::Conductor => 9,
176			Self::Band => 10,
177			Self::Composer => 11,
178			Self::Lyricist => 12,
179			Self::RecordingLocation => 13,
180			Self::DuringRecording => 14,
181			Self::DuringPerformance => 15,
182			Self::ScreenCapture => 16,
183			Self::BrightFish => 17,
184			Self::Illustration => 18,
185			Self::BandLogo => 19,
186			Self::PublisherLogo => 20,
187			Self::Undefined(i) => *i,
188		}
189	}
190
191	/// Get a `PictureType` from a u8 according to ID3v2 APIC
192	pub fn from_u8(byte: u8) -> Self {
193		match byte {
194			0 => Self::Other,
195			1 => Self::Icon,
196			2 => Self::OtherIcon,
197			3 => Self::CoverFront,
198			4 => Self::CoverBack,
199			5 => Self::Leaflet,
200			6 => Self::Media,
201			7 => Self::LeadArtist,
202			8 => Self::Artist,
203			9 => Self::Conductor,
204			10 => Self::Band,
205			11 => Self::Composer,
206			12 => Self::Lyricist,
207			13 => Self::RecordingLocation,
208			14 => Self::DuringRecording,
209			15 => Self::DuringPerformance,
210			16 => Self::ScreenCapture,
211			17 => Self::BrightFish,
212			18 => Self::Illustration,
213			19 => Self::BandLogo,
214			20 => Self::PublisherLogo,
215			i => Self::Undefined(i),
216		}
217	}
218
219	// APE specific methods
220
221	/// Get an APE item key from a `PictureType`
222	pub fn as_ape_key(&self) -> Option<&str> {
223		match self {
224			Self::Other => Some("Cover Art (Other)"),
225			Self::Icon => Some("Cover Art (Png Icon)"),
226			Self::OtherIcon => Some("Cover Art (Icon)"),
227			Self::CoverFront => Some("Cover Art (Front)"),
228			Self::CoverBack => Some("Cover Art (Back)"),
229			Self::Leaflet => Some("Cover Art (Leaflet)"),
230			Self::Media => Some("Cover Art (Media)"),
231			Self::LeadArtist => Some("Cover Art (Lead Artist)"),
232			Self::Artist => Some("Cover Art (Artist)"),
233			Self::Conductor => Some("Cover Art (Conductor)"),
234			Self::Band => Some("Cover Art (Band)"),
235			Self::Composer => Some("Cover Art (Composer)"),
236			Self::Lyricist => Some("Cover Art (Lyricist)"),
237			Self::RecordingLocation => Some("Cover Art (Recording Location)"),
238			Self::DuringRecording => Some("Cover Art (During Recording)"),
239			Self::DuringPerformance => Some("Cover Art (During Performance)"),
240			Self::ScreenCapture => Some("Cover Art (Video Capture)"),
241			Self::BrightFish => Some("Cover Art (Fish)"),
242			Self::Illustration => Some("Cover Art (Illustration)"),
243			Self::BandLogo => Some("Cover Art (Band Logotype)"),
244			Self::PublisherLogo => Some("Cover Art (Publisher Logotype)"),
245			Self::Undefined(_) => None,
246		}
247	}
248
249	/// Get a `PictureType` from an APE item key
250	pub fn from_ape_key(key: &str) -> Self {
251		match key {
252			"Cover Art (Other)" => Self::Other,
253			"Cover Art (Png Icon)" => Self::Icon,
254			"Cover Art (Icon)" => Self::OtherIcon,
255			"Cover Art (Front)" => Self::CoverFront,
256			"Cover Art (Back)" => Self::CoverBack,
257			"Cover Art (Leaflet)" => Self::Leaflet,
258			"Cover Art (Media)" => Self::Media,
259			"Cover Art (Lead Artist)" => Self::LeadArtist,
260			"Cover Art (Artist)" => Self::Artist,
261			"Cover Art (Conductor)" => Self::Conductor,
262			"Cover Art (Band)" => Self::Band,
263			"Cover Art (Composer)" => Self::Composer,
264			"Cover Art (Lyricist)" => Self::Lyricist,
265			"Cover Art (Recording Location)" => Self::RecordingLocation,
266			"Cover Art (During Recording)" => Self::DuringRecording,
267			"Cover Art (During Performance)" => Self::DuringPerformance,
268			"Cover Art (Video Capture)" => Self::ScreenCapture,
269			"Cover Art (Fish)" => Self::BrightFish,
270			"Cover Art (Illustration)" => Self::Illustration,
271			"Cover Art (Band Logotype)" => Self::BandLogo,
272			"Cover Art (Publisher Logotype)" => Self::PublisherLogo,
273			_ => Self::Undefined(0),
274		}
275	}
276}
277
278/// Information about a [`Picture`]
279///
280/// This information is necessary for FLAC's `METADATA_BLOCK_PICTURE`.
281/// See [`Picture::as_flac_bytes`] for more information.
282#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
283pub struct PictureInformation {
284	/// The picture's width in pixels
285	pub width: u32,
286	/// The picture's height in pixels
287	pub height: u32,
288	/// The picture's color depth in bits per pixel
289	pub color_depth: u32,
290	/// The number of colors used
291	pub num_colors: u32,
292}
293
294impl PictureInformation {
295	/// Attempt to extract [`PictureInformation`] from a [`Picture`]
296	///
297	/// NOTE: This only supports PNG and JPEG images. If another image is provided,
298	/// the `PictureInformation` will be zeroed out.
299	///
300	/// # Errors
301	///
302	/// * `picture.data` is less than 8 bytes in length
303	/// * See [`PictureInformation::from_png`] and [`PictureInformation::from_jpeg`]
304	pub fn from_picture(picture: &Picture) -> Result<Self> {
305		let reader = &mut &*picture.data;
306
307		if reader.len() < 8 {
308			err!(NotAPicture);
309		}
310
311		match reader[..4] {
312			[0x89, b'P', b'N', b'G'] => Ok(Self::from_png(reader).unwrap_or_default()),
313			[0xFF, 0xD8, 0xFF, ..] => Ok(Self::from_jpeg(reader).unwrap_or_default()),
314			_ => Ok(Self::default()),
315		}
316	}
317
318	/// Attempt to extract [`PictureInformation`] from a PNG
319	///
320	/// # Errors
321	///
322	/// * `reader` is not a valid PNG
323	pub fn from_png(mut data: &[u8]) -> Result<Self> {
324		let reader = &mut data;
325
326		let mut sig = [0; 8];
327		reader.read_exact(&mut sig)?;
328
329		if sig != [0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A] {
330			err!(NotAPicture);
331		}
332
333		let mut ihdr = [0; 8];
334		reader.read_exact(&mut ihdr)?;
335
336		// Verify the signature is immediately followed by the IHDR chunk
337		if !ihdr.ends_with(&[0x49, 0x48, 0x44, 0x52]) {
338			err!(NotAPicture);
339		}
340
341		let width = reader.read_u32::<BigEndian>()?;
342		let height = reader.read_u32::<BigEndian>()?;
343		let mut color_depth = u32::from(reader.read_u8()?);
344		let color_type = reader.read_u8()?;
345
346		match color_type {
347			2 => color_depth *= 3,
348			4 | 6 => color_depth *= 4,
349			_ => {},
350		}
351
352		let mut ret = Self {
353			width,
354			height,
355			color_depth,
356			num_colors: 0,
357		};
358
359		// The color type 3 (indexed-color) means there should be
360		// a "PLTE" chunk, whose data can be used in the `num_colors`
361		// field. It isn't really applicable to other color types.
362		if color_type != 3 {
363			return Ok(ret);
364		}
365
366		let mut reader = Cursor::new(reader);
367
368		// Skip 7 bytes
369		// Compression method (1)
370		// Filter method (1)
371		// Interlace method (1)
372		// CRC (4)
373		reader.seek(SeekFrom::Current(7))?;
374
375		let mut chunk_type = [0; 4];
376
377		while let (Ok(size), Ok(())) = (
378			reader.read_u32::<BigEndian>(),
379			reader.read_exact(&mut chunk_type),
380		) {
381			if &chunk_type == b"PLTE" {
382				// The PLTE chunk contains 1-256 3-byte entries
383				ret.num_colors = size / 3;
384				break;
385			}
386
387			// Skip the chunk's data (size) and CRC (4 bytes)
388			let (content_size, overflowed) = size.overflowing_add(4);
389			if overflowed {
390				break;
391			}
392
393			reader.seek(SeekFrom::Current(i64::from(content_size)))?;
394		}
395
396		Ok(ret)
397	}
398
399	/// Attempt to extract [`PictureInformation`] from a JPEG
400	///
401	/// # Errors
402	///
403	/// * `reader` is not a JPEG image
404	/// * `reader` does not contain a `SOFn` frame
405	pub fn from_jpeg(mut data: &[u8]) -> Result<Self> {
406		let reader = &mut data;
407
408		let mut frame_marker = [0; 4];
409		reader.read_exact(&mut frame_marker)?;
410
411		if !matches!(frame_marker, [0xFF, 0xD8, 0xFF, ..]) {
412			err!(NotAPicture);
413		}
414
415		let mut section_len = reader.read_u16::<BigEndian>()?;
416
417		let mut reader = Cursor::new(reader);
418
419		// The length contains itself, so anything < 2 is invalid
420		let (content_len, overflowed) = section_len.overflowing_sub(2);
421		if overflowed {
422			err!(NotAPicture);
423		}
424		reader.seek(SeekFrom::Current(i64::from(content_len)))?;
425
426		while let Ok(0xFF) = reader.read_u8() {
427			let marker = reader.read_u8()?;
428			section_len = reader.read_u16::<BigEndian>()?;
429
430			// This marks the SOS (Start of Scan), which is
431			// the end of the header
432			if marker == 0xDA {
433				break;
434			}
435
436			// We are looking for a frame with a "SOFn" marker,
437			// with `n` either being 0 or 2. Since there isn't a
438			// header like PNG, we actually need to search for this
439			// frame
440			if marker == 0xC0 || marker == 0xC2 {
441				let precision = reader.read_u8()?;
442				let height = u32::from(reader.read_u16::<BigEndian>()?);
443				let width = u32::from(reader.read_u16::<BigEndian>()?);
444				let components = reader.read_u8()?;
445
446				return Ok(Self {
447					width,
448					height,
449					color_depth: u32::from(precision * components),
450					num_colors: 0,
451				});
452			}
453
454			reader.seek(SeekFrom::Current(i64::from(section_len - 2)))?;
455		}
456
457		err!(NotAPicture)
458	}
459}
460
461/// Represents a picture.
462#[derive(Clone, Eq, PartialEq, Hash)]
463pub struct Picture {
464	/// The picture type according to ID3v2 APIC
465	pub(crate) pic_type: PictureType,
466	/// The picture's mimetype
467	pub(crate) mime_type: Option<MimeType>,
468	/// The picture's description
469	pub(crate) description: Option<Cow<'static, str>>,
470	/// The binary data of the picture
471	pub(crate) data: Cow<'static, [u8]>,
472}
473
474impl Debug for Picture {
475	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
476		f.debug_struct("Picture")
477			.field("pic_type", &self.pic_type)
478			.field("mime_type", &self.mime_type)
479			.field("description", &self.description)
480			.field("data", &format!("<{} bytes>", self.data.len()))
481			.finish()
482	}
483}
484
485impl Picture {
486	/// Create a [`Picture`] from a reader
487	///
488	/// NOTES:
489	///
490	/// * This is for reading picture data only, from a [`File`](std::fs::File) for example.
491	/// * `pic_type` will always be [`PictureType::Other`], be sure to change it accordingly if
492	///   writing.
493	///
494	/// # Errors
495	///
496	/// * `reader` contains less than 8 bytes
497	/// * `reader` does not contain a supported format. See [`MimeType`] for valid formats
498	pub fn from_reader<R>(reader: &mut R) -> Result<Self>
499	where
500		R: Read,
501	{
502		let mut data = Vec::new();
503		reader.read_to_end(&mut data)?;
504
505		if data.len() < 8 {
506			err!(NotAPicture);
507		}
508
509		let mime_type = Self::mimetype_from_bin(&data[..8])?;
510
511		Ok(Self {
512			pic_type: PictureType::Other,
513			mime_type: Some(mime_type),
514			description: None,
515			data: data.into(),
516		})
517	}
518
519	/// Create a new `Picture`
520	///
521	/// NOTE: This will **not** verify `data`'s signature.
522	/// This should only be used if all data has been verified
523	/// beforehand.
524	pub fn new_unchecked(
525		pic_type: PictureType,
526		mime_type: Option<MimeType>,
527		description: Option<String>,
528		data: Vec<u8>,
529	) -> Self {
530		Self {
531			pic_type,
532			mime_type,
533			description: description.map(Cow::Owned),
534			data: Cow::Owned(data),
535		}
536	}
537
538	/// Returns the [`PictureType`]
539	pub fn pic_type(&self) -> PictureType {
540		self.pic_type
541	}
542
543	/// Sets the [`PictureType`]
544	pub fn set_pic_type(&mut self, pic_type: PictureType) {
545		self.pic_type = pic_type
546	}
547
548	/// Returns the [`MimeType`]
549	///
550	/// The `mime_type` is determined from the `data`, and
551	/// is immutable.
552	pub fn mime_type(&self) -> Option<&MimeType> {
553		self.mime_type.as_ref()
554	}
555
556	// Used commonly internally
557	pub(crate) fn mime_str(&self) -> &str {
558		match self.mime_type.as_ref() {
559			Some(mime_type) => mime_type.as_str(),
560			None => "",
561		}
562	}
563
564	/// Returns the description
565	pub fn description(&self) -> Option<&str> {
566		self.description.as_deref()
567	}
568
569	/// Sets the description
570	pub fn set_description(&mut self, description: Option<String>) {
571		self.description = description.map(Cow::from);
572	}
573
574	/// Returns the [`Picture`] data as borrowed bytes.
575	pub fn data(&self) -> &[u8] {
576		&self.data
577	}
578
579	/// Consumes a [`Picture`], returning the data as [`Vec`] without clones or allocation.
580	pub fn into_data(self) -> Vec<u8> {
581		self.data.into_owned()
582	}
583
584	/// Convert a [`Picture`] to a base64 encoded FLAC `METADATA_BLOCK_PICTURE` String
585	///
586	/// Use `encode` to convert the picture to a base64 encoded String ([RFC 4648 ยง4](http://www.faqs.org/rfcs/rfc4648.html))
587	///
588	/// NOTES:
589	///
590	/// * This does not include a key (Vorbis comments) or METADATA_BLOCK_HEADER (FLAC blocks)
591	/// * FLAC blocks have different size requirements than OGG Vorbis/Opus, size is not checked here
592	/// * When writing to Vorbis comments, the data **must** be base64 encoded
593	pub fn as_flac_bytes(&self, picture_information: PictureInformation, encode: bool) -> Vec<u8> {
594		let mut data = Vec::<u8>::new();
595
596		let picture_type = u32::from(self.pic_type.as_u8()).to_be_bytes();
597
598		let mime_str = self.mime_str();
599		let mime_len = mime_str.len() as u32;
600
601		data.extend(picture_type);
602		data.extend(mime_len.to_be_bytes());
603		data.extend(mime_str.as_bytes());
604
605		if let Some(desc) = &self.description {
606			let desc_len = desc.len() as u32;
607
608			data.extend(desc_len.to_be_bytes());
609			data.extend(desc.as_bytes());
610		} else {
611			data.extend([0; 4]);
612		}
613
614		data.extend(picture_information.width.to_be_bytes());
615		data.extend(picture_information.height.to_be_bytes());
616		data.extend(picture_information.color_depth.to_be_bytes());
617		data.extend(picture_information.num_colors.to_be_bytes());
618
619		let pic_data = &self.data;
620		let pic_data_len = pic_data.len() as u32;
621
622		data.extend(pic_data_len.to_be_bytes());
623		data.extend(pic_data.iter());
624
625		if encode {
626			BASE64.encode(&data).into_bytes()
627		} else {
628			data
629		}
630	}
631
632	/// Get a [`Picture`] from FLAC `METADATA_BLOCK_PICTURE` bytes:
633	///
634	/// NOTE: This takes both the base64 encoded string from Vorbis comments, and
635	/// the raw data from a FLAC block, specified with `encoded`.
636	///
637	/// # Errors
638	///
639	/// This function will return [`NotAPicture`][ErrorKind::NotAPicture] if
640	/// at any point it's unable to parse the data
641	pub fn from_flac_bytes(
642		bytes: &[u8],
643		encoded: bool,
644		parse_mode: ParsingMode,
645	) -> Result<(Self, PictureInformation)> {
646		if encoded {
647			let data = BASE64
648				.decode(bytes)
649				.map_err(|_| LoftyError::new(ErrorKind::NotAPicture))?;
650			Self::from_flac_bytes_inner(&data, parse_mode)
651		} else {
652			Self::from_flac_bytes_inner(bytes, parse_mode)
653		}
654	}
655
656	fn from_flac_bytes_inner(
657		content: &[u8],
658		parse_mode: ParsingMode,
659	) -> Result<(Self, PictureInformation)> {
660		use crate::macros::try_vec;
661
662		let mut size = content.len();
663		let mut reader = Cursor::new(content);
664
665		if size < 32 {
666			err!(NotAPicture);
667		}
668
669		let pic_ty = reader.read_u32::<BigEndian>()?;
670		size -= 4;
671
672		// ID3v2 APIC uses a single byte for picture type.
673		// Anything greater than that is probably invalid, so
674		// we just stop early
675		if pic_ty > 255 && parse_mode == ParsingMode::Strict {
676			err!(NotAPicture);
677		}
678
679		let mime_len = reader.read_u32::<BigEndian>()? as usize;
680		size -= 4;
681
682		if mime_len > size {
683			err!(SizeMismatch);
684		}
685
686		let mime_type_str = utf8_decode_str(&content[8..8 + mime_len])?;
687		size -= mime_len;
688
689		reader.seek(SeekFrom::Current(mime_len as i64))?;
690
691		let desc_len = reader.read_u32::<BigEndian>()? as usize;
692		size -= 4;
693
694		let mut description = None;
695		if desc_len > 0 && desc_len < size {
696			let pos = 12 + mime_len;
697
698			if let Ok(desc) = utf8_decode_str(&content[pos..pos + desc_len]) {
699				description = Some(desc.to_owned().into());
700			}
701
702			size -= desc_len;
703			reader.seek(SeekFrom::Current(desc_len as i64))?;
704		}
705
706		let width = reader.read_u32::<BigEndian>()?;
707		let height = reader.read_u32::<BigEndian>()?;
708		let color_depth = reader.read_u32::<BigEndian>()?;
709		let num_colors = reader.read_u32::<BigEndian>()?;
710		let data_len = reader.read_u32::<BigEndian>()? as usize;
711		size -= 20;
712
713		if data_len <= size {
714			let mut data = try_vec![0; data_len];
715
716			if let Ok(()) = reader.read_exact(&mut data) {
717				let mime_type;
718				if mime_type_str.is_empty() {
719					mime_type = None;
720				} else {
721					mime_type = Some(MimeType::from_str(mime_type_str));
722				}
723
724				return Ok((
725					Self {
726						pic_type: PictureType::from_u8(pic_ty as u8),
727						mime_type,
728						description,
729						data: Cow::from(data),
730					},
731					PictureInformation {
732						width,
733						height,
734						color_depth,
735						num_colors,
736					},
737				));
738			}
739		}
740
741		err!(NotAPicture)
742	}
743
744	/// Convert a [`Picture`] to an APE Cover Art byte vec:
745	///
746	/// NOTE: This is only the picture data and description, a
747	/// key and terminating null byte will not be prepended.
748	/// To map a [`PictureType`] to an APE key see [`PictureType::as_ape_key`]
749	pub fn as_ape_bytes(&self) -> Vec<u8> {
750		let mut data: Vec<u8> = Vec::new();
751
752		if let Some(desc) = &self.description {
753			data.extend(desc.as_bytes());
754		}
755
756		data.push(0);
757		data.extend(self.data.iter());
758
759		data
760	}
761
762	/// Get a [`Picture`] from an APEv2 binary item:
763	///
764	/// NOTE: This function expects `bytes` to contain *only* the APE item data
765	///
766	/// # Errors
767	///
768	/// This function will return [`NotAPicture`](ErrorKind::NotAPicture)
769	/// if at any point it's unable to parse the data
770	pub fn from_ape_bytes(key: &str, bytes: &[u8]) -> Result<Self> {
771		if bytes.is_empty() {
772			err!(NotAPicture);
773		}
774
775		let pic_type = PictureType::from_ape_key(key);
776
777		let reader = &mut &*bytes;
778		let mut pos = 0;
779
780		let mut description = None;
781		let mut desc_text = String::new();
782
783		while let Ok(ch) = reader.read_u8() {
784			pos += 1;
785
786			if ch == b'\0' {
787				break;
788			}
789
790			desc_text.push(char::from(ch));
791		}
792
793		if !desc_text.is_empty() {
794			description = Some(Cow::from(desc_text));
795		}
796
797		let mime_type = {
798			let mut identifier = [0; 8];
799			reader.read_exact(&mut identifier)?;
800
801			Self::mimetype_from_bin(&identifier[..])?
802		};
803
804		let data = Cow::from(bytes[pos..].to_vec());
805
806		Ok(Picture {
807			pic_type,
808			mime_type: Some(mime_type),
809			description,
810			data,
811		})
812	}
813
814	pub(crate) fn mimetype_from_bin(bytes: &[u8]) -> Result<MimeType> {
815		match bytes[..8] {
816			[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A] => Ok(MimeType::Png),
817			[0xFF, 0xD8, ..] => Ok(MimeType::Jpeg),
818			[b'G', b'I', b'F', 0x38, 0x37 | 0x39, b'a', ..] => Ok(MimeType::Gif),
819			[b'B', b'M', ..] => Ok(MimeType::Bmp),
820			[b'I', b'I', b'*', 0x00, ..] | [b'M', b'M', 0x00, b'*', ..] => Ok(MimeType::Tiff),
821			_ => err!(NotAPicture),
822		}
823	}
824}
825
826// A placeholder that is needed during conversions.
827pub(crate) const TOMBSTONE_PICTURE: Picture = Picture {
828	pic_type: PictureType::Other,
829	mime_type: None,
830	description: None,
831	data: Cow::Owned(Vec::new()),
832};