Skip to main content

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#[allow(clippy::unsafe_derive_deserialize)]
134#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
135#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
136#[non_exhaustive]
137pub enum PictureType {
138	Other,
139	Icon,
140	OtherIcon,
141	CoverFront,
142	CoverBack,
143	Leaflet,
144	Media,
145	LeadArtist,
146	Artist,
147	Conductor,
148	Band,
149	Composer,
150	Lyricist,
151	RecordingLocation,
152	DuringRecording,
153	DuringPerformance,
154	ScreenCapture,
155	BrightFish,
156	Illustration,
157	BandLogo,
158	PublisherLogo,
159	Undefined(u8),
160}
161
162impl PictureType {
163	// ID3/OGG specific methods
164
165	/// Get a `u8` from a `PictureType` according to ID3v2 APIC
166	pub fn as_u8(&self) -> u8 {
167		match self {
168			Self::Other => 0,
169			Self::Icon => 1,
170			Self::OtherIcon => 2,
171			Self::CoverFront => 3,
172			Self::CoverBack => 4,
173			Self::Leaflet => 5,
174			Self::Media => 6,
175			Self::LeadArtist => 7,
176			Self::Artist => 8,
177			Self::Conductor => 9,
178			Self::Band => 10,
179			Self::Composer => 11,
180			Self::Lyricist => 12,
181			Self::RecordingLocation => 13,
182			Self::DuringRecording => 14,
183			Self::DuringPerformance => 15,
184			Self::ScreenCapture => 16,
185			Self::BrightFish => 17,
186			Self::Illustration => 18,
187			Self::BandLogo => 19,
188			Self::PublisherLogo => 20,
189			Self::Undefined(i) => *i,
190		}
191	}
192
193	/// Get a `PictureType` from a u8 according to ID3v2 APIC
194	pub fn from_u8(byte: u8) -> Self {
195		match byte {
196			0 => Self::Other,
197			1 => Self::Icon,
198			2 => Self::OtherIcon,
199			3 => Self::CoverFront,
200			4 => Self::CoverBack,
201			5 => Self::Leaflet,
202			6 => Self::Media,
203			7 => Self::LeadArtist,
204			8 => Self::Artist,
205			9 => Self::Conductor,
206			10 => Self::Band,
207			11 => Self::Composer,
208			12 => Self::Lyricist,
209			13 => Self::RecordingLocation,
210			14 => Self::DuringRecording,
211			15 => Self::DuringPerformance,
212			16 => Self::ScreenCapture,
213			17 => Self::BrightFish,
214			18 => Self::Illustration,
215			19 => Self::BandLogo,
216			20 => Self::PublisherLogo,
217			i => Self::Undefined(i),
218		}
219	}
220
221	// APE specific methods
222
223	/// Get an APE item key from a `PictureType`
224	pub fn as_ape_key(&self) -> Option<&str> {
225		match self {
226			Self::Other => Some("Cover Art (Other)"),
227			Self::Icon => Some("Cover Art (Png Icon)"),
228			Self::OtherIcon => Some("Cover Art (Icon)"),
229			Self::CoverFront => Some("Cover Art (Front)"),
230			Self::CoverBack => Some("Cover Art (Back)"),
231			Self::Leaflet => Some("Cover Art (Leaflet)"),
232			Self::Media => Some("Cover Art (Media)"),
233			Self::LeadArtist => Some("Cover Art (Lead Artist)"),
234			Self::Artist => Some("Cover Art (Artist)"),
235			Self::Conductor => Some("Cover Art (Conductor)"),
236			Self::Band => Some("Cover Art (Band)"),
237			Self::Composer => Some("Cover Art (Composer)"),
238			Self::Lyricist => Some("Cover Art (Lyricist)"),
239			Self::RecordingLocation => Some("Cover Art (Recording Location)"),
240			Self::DuringRecording => Some("Cover Art (During Recording)"),
241			Self::DuringPerformance => Some("Cover Art (During Performance)"),
242			Self::ScreenCapture => Some("Cover Art (Video Capture)"),
243			Self::BrightFish => Some("Cover Art (Fish)"),
244			Self::Illustration => Some("Cover Art (Illustration)"),
245			Self::BandLogo => Some("Cover Art (Band Logotype)"),
246			Self::PublisherLogo => Some("Cover Art (Publisher Logotype)"),
247			Self::Undefined(_) => None,
248		}
249	}
250
251	/// Get a `PictureType` from an APE item key
252	pub fn from_ape_key(key: &str) -> Self {
253		match key {
254			"Cover Art (Other)" => Self::Other,
255			"Cover Art (Png Icon)" => Self::Icon,
256			"Cover Art (Icon)" => Self::OtherIcon,
257			"Cover Art (Front)" => Self::CoverFront,
258			"Cover Art (Back)" => Self::CoverBack,
259			"Cover Art (Leaflet)" => Self::Leaflet,
260			"Cover Art (Media)" => Self::Media,
261			"Cover Art (Lead Artist)" => Self::LeadArtist,
262			"Cover Art (Artist)" => Self::Artist,
263			"Cover Art (Conductor)" => Self::Conductor,
264			"Cover Art (Band)" => Self::Band,
265			"Cover Art (Composer)" => Self::Composer,
266			"Cover Art (Lyricist)" => Self::Lyricist,
267			"Cover Art (Recording Location)" => Self::RecordingLocation,
268			"Cover Art (During Recording)" => Self::DuringRecording,
269			"Cover Art (During Performance)" => Self::DuringPerformance,
270			"Cover Art (Video Capture)" => Self::ScreenCapture,
271			"Cover Art (Fish)" => Self::BrightFish,
272			"Cover Art (Illustration)" => Self::Illustration,
273			"Cover Art (Band Logotype)" => Self::BandLogo,
274			"Cover Art (Publisher Logotype)" => Self::PublisherLogo,
275			_ => Self::Undefined(0),
276		}
277	}
278}
279
280/// Information about a [`Picture`]
281///
282/// This information is necessary for FLAC's `METADATA_BLOCK_PICTURE`.
283/// See [`Picture::as_flac_bytes`] for more information.
284#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
285pub struct PictureInformation {
286	/// The picture's width in pixels
287	pub width: u32,
288	/// The picture's height in pixels
289	pub height: u32,
290	/// The picture's color depth in bits per pixel
291	pub color_depth: u32,
292	/// The number of colors used
293	pub num_colors: u32,
294}
295
296impl PictureInformation {
297	/// Attempt to extract [`PictureInformation`] from a [`Picture`]
298	///
299	/// NOTE: This only supports PNG and JPEG images. If another image is provided,
300	/// the `PictureInformation` will be zeroed out.
301	///
302	/// # Errors
303	///
304	/// * `picture.data` is less than 8 bytes in length
305	/// * See [`PictureInformation::from_png`] and [`PictureInformation::from_jpeg`]
306	pub fn from_picture(picture: &Picture) -> Result<Self> {
307		let reader = &mut &*picture.data;
308
309		if reader.len() < 8 {
310			err!(NotAPicture);
311		}
312
313		match reader[..4] {
314			[0x89, b'P', b'N', b'G'] => Ok(Self::from_png(reader).unwrap_or_default()),
315			[0xFF, 0xD8, 0xFF, ..] => Ok(Self::from_jpeg(reader).unwrap_or_default()),
316			_ => Ok(Self::default()),
317		}
318	}
319
320	/// Attempt to extract [`PictureInformation`] from a PNG
321	///
322	/// # Errors
323	///
324	/// * `reader` is not a valid PNG
325	pub fn from_png(mut data: &[u8]) -> Result<Self> {
326		let reader = &mut data;
327
328		let mut sig = [0; 8];
329		reader.read_exact(&mut sig)?;
330
331		if sig != [0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A] {
332			err!(NotAPicture);
333		}
334
335		let mut ihdr = [0; 8];
336		reader.read_exact(&mut ihdr)?;
337
338		// Verify the signature is immediately followed by the IHDR chunk
339		if !ihdr.ends_with(&[0x49, 0x48, 0x44, 0x52]) {
340			err!(NotAPicture);
341		}
342
343		let width = reader.read_u32::<BigEndian>()?;
344		let height = reader.read_u32::<BigEndian>()?;
345		let mut color_depth = u32::from(reader.read_u8()?);
346		let color_type = reader.read_u8()?;
347
348		match color_type {
349			2 => color_depth *= 3,
350			4 | 6 => color_depth *= 4,
351			_ => {},
352		}
353
354		let mut ret = Self {
355			width,
356			height,
357			color_depth,
358			num_colors: 0,
359		};
360
361		// The color type 3 (indexed-color) means there should be
362		// a "PLTE" chunk, whose data can be used in the `num_colors`
363		// field. It isn't really applicable to other color types.
364		if color_type != 3 {
365			return Ok(ret);
366		}
367
368		let mut reader = Cursor::new(reader);
369
370		// Skip 7 bytes
371		// Compression method (1)
372		// Filter method (1)
373		// Interlace method (1)
374		// CRC (4)
375		reader.seek(SeekFrom::Current(7))?;
376
377		let mut chunk_type = [0; 4];
378
379		while let (Ok(size), Ok(())) = (
380			reader.read_u32::<BigEndian>(),
381			reader.read_exact(&mut chunk_type),
382		) {
383			if &chunk_type == b"PLTE" {
384				// The PLTE chunk contains 1-256 3-byte entries
385				ret.num_colors = size / 3;
386				break;
387			}
388
389			// Skip the chunk's data (size) and CRC (4 bytes)
390			let (content_size, overflowed) = size.overflowing_add(4);
391			if overflowed {
392				break;
393			}
394
395			reader.seek(SeekFrom::Current(i64::from(content_size)))?;
396		}
397
398		Ok(ret)
399	}
400
401	/// Attempt to extract [`PictureInformation`] from a JPEG
402	///
403	/// # Errors
404	///
405	/// * `reader` is not a JPEG image
406	/// * `reader` does not contain a `SOFn` frame
407	pub fn from_jpeg(mut data: &[u8]) -> Result<Self> {
408		let reader = &mut data;
409
410		let mut frame_marker = [0; 4];
411		reader.read_exact(&mut frame_marker)?;
412
413		if !matches!(frame_marker, [0xFF, 0xD8, 0xFF, ..]) {
414			err!(NotAPicture);
415		}
416
417		let mut section_len = reader.read_u16::<BigEndian>()?;
418
419		let mut reader = Cursor::new(reader);
420
421		// The length contains itself, so anything < 2 is invalid
422		let (content_len, overflowed) = section_len.overflowing_sub(2);
423		if overflowed {
424			err!(NotAPicture);
425		}
426		reader.seek(SeekFrom::Current(i64::from(content_len)))?;
427
428		while let Ok(0xFF) = reader.read_u8() {
429			let marker = reader.read_u8()?;
430			section_len = reader.read_u16::<BigEndian>()?;
431
432			// This marks the SOS (Start of Scan), which is
433			// the end of the header
434			if marker == 0xDA {
435				break;
436			}
437
438			// We are looking for a frame with a "SOFn" marker,
439			// with `n` either being 0 or 2. Since there isn't a
440			// header like PNG, we actually need to search for this
441			// frame
442			if marker == 0xC0 || marker == 0xC2 {
443				let precision = reader.read_u8()?;
444				let height = u32::from(reader.read_u16::<BigEndian>()?);
445				let width = u32::from(reader.read_u16::<BigEndian>()?);
446				let components = reader.read_u8()?;
447
448				return Ok(Self {
449					width,
450					height,
451					color_depth: u32::from(precision * components),
452					num_colors: 0,
453				});
454			}
455
456			reader.seek(SeekFrom::Current(i64::from(section_len - 2)))?;
457		}
458
459		err!(NotAPicture)
460	}
461}
462
463/// Builder for a [`Picture`]
464///
465/// This is created through [`Picture::unchecked()`].
466pub struct PictureBuilder {
467	pic_type: PictureType,
468	mime_type: Option<MimeType>,
469	description: Option<Cow<'static, str>>,
470	data: Cow<'static, [u8]>,
471}
472
473impl PictureBuilder {
474	fn new(data: Cow<'static, [u8]>) -> Self {
475		Self {
476			pic_type: PictureType::Other,
477			mime_type: None,
478			description: None,
479			data,
480		}
481	}
482
483	/// Set the [`PictureType`] for this picture
484	///
485	/// # Examples
486	///
487	/// ```rust
488	/// use lofty::picture::{Picture, PictureType};
489	///
490	/// # fn main() -> lofty::error::Result<()> {
491	/// let picture_path = "band.jpg";
492	/// # let picture_path = "tests/files/assets/issue_37.jpg";
493	/// let picture_data = std::fs::read(picture_path)?;
494	///
495	/// let picture = Picture::unchecked(picture_data)
496	/// 	.pic_type(PictureType::Band)
497	/// 	.build();
498	/// # Ok(()) }
499	/// ```
500	pub fn pic_type(mut self, pic_type: PictureType) -> Self {
501		self.pic_type = pic_type;
502		self
503	}
504
505	/// Set the [`PictureType`] for this picture
506	///
507	/// # Examples
508	///
509	/// ```rust
510	/// use lofty::picture::{MimeType, Picture};
511	///
512	/// # fn main() -> lofty::error::Result<()> {
513	/// let picture_path = "band.jpg";
514	/// # let picture_path = "tests/files/assets/issue_37.jpg";
515	/// let picture_data = std::fs::read(picture_path)?;
516	///
517	/// let picture = Picture::unchecked(picture_data)
518	/// 	.mime_type(MimeType::Jpeg)
519	/// 	.build();
520	/// # Ok(()) }
521	/// ```
522	pub fn mime_type(mut self, mime_type: MimeType) -> Self {
523		self.mime_type = Some(mime_type);
524		self
525	}
526
527	/// Set the [`PictureType`] for this picture
528	///
529	/// # Examples
530	///
531	/// ```rust
532	/// use lofty::picture::Picture;
533	///
534	/// # fn main() -> lofty::error::Result<()> {
535	/// let picture_path = "band.jpg";
536	/// # let picture_path = "tests/files/assets/issue_37.jpg";
537	/// let picture_data = std::fs::read(picture_path)?;
538	///
539	/// let picture = Picture::unchecked(picture_data)
540	/// 	.description("The band on stage")
541	/// 	.build();
542	/// # Ok(()) }
543	/// ```
544	pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
545		self.description = Some(description.into());
546		self
547	}
548
549	/// Convert this builder into a [`Picture`]
550	pub fn build(self) -> Picture {
551		self.into()
552	}
553}
554
555impl From<PictureBuilder> for Picture {
556	fn from(builder: PictureBuilder) -> Self {
557		Self {
558			pic_type: builder.pic_type,
559			mime_type: builder.mime_type,
560			description: builder.description,
561			data: builder.data,
562		}
563	}
564}
565
566/// Represents a picture.
567#[derive(Clone, Eq, PartialEq, Hash)]
568pub struct Picture {
569	/// The picture type according to ID3v2 APIC
570	pub(crate) pic_type: PictureType,
571	/// The picture's mimetype
572	pub(crate) mime_type: Option<MimeType>,
573	/// The picture's description
574	pub(crate) description: Option<Cow<'static, str>>,
575	/// The binary data of the picture
576	pub(crate) data: Cow<'static, [u8]>,
577}
578
579impl Debug for Picture {
580	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
581		f.debug_struct("Picture")
582			.field("pic_type", &self.pic_type)
583			.field("mime_type", &self.mime_type)
584			.field("description", &self.description)
585			.field("data", &format!("<{} bytes>", self.data.len()))
586			.finish()
587	}
588}
589
590impl Picture {
591	/// Placeholder for conversions
592	pub(crate) const EMPTY: Self = Picture {
593		pic_type: PictureType::Other,
594		mime_type: None,
595		description: None,
596		data: Cow::Owned(Vec::new()),
597	};
598
599	/// Create a [`Picture`] from a reader
600	///
601	/// NOTES:
602	///
603	/// * This is for reading picture data only, from a [`File`](std::fs::File) for example.
604	/// * `pic_type` will always be [`PictureType::Other`], be sure to change it accordingly if
605	///   writing.
606	///
607	/// # Errors
608	///
609	/// * `reader` contains less than 8 bytes
610	/// * `reader` does not contain a supported format. See [`MimeType`] for valid formats
611	pub fn from_reader<R>(reader: &mut R) -> Result<Self>
612	where
613		R: Read,
614	{
615		let mut data = Vec::new();
616		reader.read_to_end(&mut data)?;
617
618		if data.len() < 8 {
619			err!(NotAPicture);
620		}
621
622		let mime_type = Self::mimetype_from_bin(&data[..8])?;
623
624		Ok(Self {
625			pic_type: PictureType::Other,
626			mime_type: Some(mime_type),
627			description: None,
628			data: data.into(),
629		})
630	}
631
632	/// Create a new `Picture` with no verification
633	///
634	/// This will **not** verify `data`'s signature.
635	///
636	/// This should only be used if all data has been verified beforehand.
637	///
638	/// # Examples
639	///
640	/// ```rust
641	/// use lofty::picture::{MimeType, Picture, PictureType};
642	///
643	/// # fn main() -> lofty::error::Result<()> {
644	/// let picture_path = "band.jpg";
645	/// # let picture_path = "tests/files/assets/issue_37.jpg";
646	/// let picture_data = std::fs::read(picture_path)?;
647	///
648	/// let picture = Picture::unchecked(picture_data)
649	/// 	.pic_type(PictureType::Band)
650	/// 	.mime_type(MimeType::Jpeg)
651	/// 	.description("The band on stage")
652	/// 	.build();
653	/// # Ok(()) }
654	/// ```
655	pub fn unchecked(data: Vec<u8>) -> PictureBuilder {
656		PictureBuilder::new(Cow::Owned(data))
657	}
658
659	/// Returns the [`PictureType`]
660	pub fn pic_type(&self) -> PictureType {
661		self.pic_type
662	}
663
664	/// Sets the [`PictureType`]
665	pub fn set_pic_type(&mut self, pic_type: PictureType) {
666		self.pic_type = pic_type
667	}
668
669	/// Returns the [`MimeType`]
670	///
671	/// The `mime_type` is determined from the `data`, and
672	/// is immutable.
673	pub fn mime_type(&self) -> Option<&MimeType> {
674		self.mime_type.as_ref()
675	}
676
677	// Used commonly internally
678	pub(crate) fn mime_str(&self) -> &str {
679		match self.mime_type.as_ref() {
680			Some(mime_type) => mime_type.as_str(),
681			None => "",
682		}
683	}
684
685	/// Returns the description
686	pub fn description(&self) -> Option<&str> {
687		self.description.as_deref()
688	}
689
690	/// Sets the description
691	pub fn set_description(&mut self, description: Option<String>) {
692		self.description = description.map(Cow::from);
693	}
694
695	/// Returns the [`Picture`] data as borrowed bytes.
696	pub fn data(&self) -> &[u8] {
697		&self.data
698	}
699
700	/// Consumes a [`Picture`], returning the data as [`Vec`] without clones or allocation.
701	pub fn into_data(self) -> Vec<u8> {
702		self.data.into_owned()
703	}
704
705	/// Convert a [`Picture`] to a base64 encoded FLAC `METADATA_BLOCK_PICTURE` String
706	///
707	/// Use `encode` to convert the picture to a base64 encoded String ([RFC 4648 ยง4](http://www.faqs.org/rfcs/rfc4648.html))
708	///
709	/// NOTES:
710	///
711	/// * This does not include a key (Vorbis comments) or METADATA_BLOCK_HEADER (FLAC blocks)
712	/// * FLAC blocks have different size requirements than OGG Vorbis/Opus, size is not checked here
713	/// * When writing to Vorbis comments, the data **must** be base64 encoded
714	pub fn as_flac_bytes(&self, picture_information: PictureInformation, encode: bool) -> Vec<u8> {
715		let mut data = Vec::<u8>::new();
716
717		let picture_type = u32::from(self.pic_type.as_u8()).to_be_bytes();
718
719		let mime_str = self.mime_str();
720		let mime_len = mime_str.len() as u32;
721
722		data.extend(picture_type);
723		data.extend(mime_len.to_be_bytes());
724		data.extend(mime_str.as_bytes());
725
726		if let Some(desc) = &self.description {
727			let desc_len = desc.len() as u32;
728
729			data.extend(desc_len.to_be_bytes());
730			data.extend(desc.as_bytes());
731		} else {
732			data.extend([0; 4]);
733		}
734
735		data.extend(picture_information.width.to_be_bytes());
736		data.extend(picture_information.height.to_be_bytes());
737		data.extend(picture_information.color_depth.to_be_bytes());
738		data.extend(picture_information.num_colors.to_be_bytes());
739
740		let pic_data = &self.data;
741		let pic_data_len = pic_data.len() as u32;
742
743		data.extend(pic_data_len.to_be_bytes());
744		data.extend(pic_data.iter());
745
746		if encode {
747			BASE64.encode(&data).into_bytes()
748		} else {
749			data
750		}
751	}
752
753	/// Get a [`Picture`] from FLAC `METADATA_BLOCK_PICTURE` bytes:
754	///
755	/// NOTE: This takes both the base64 encoded string from Vorbis comments, and
756	/// the raw data from a FLAC block, specified with `encoded`.
757	///
758	/// # Errors
759	///
760	/// This function will return [`NotAPicture`][ErrorKind::NotAPicture] if
761	/// at any point it's unable to parse the data
762	pub fn from_flac_bytes(
763		bytes: &[u8],
764		encoded: bool,
765		parse_mode: ParsingMode,
766	) -> Result<(Self, PictureInformation)> {
767		if encoded {
768			let data = BASE64
769				.decode(bytes)
770				.map_err(|_| LoftyError::new(ErrorKind::NotAPicture))?;
771			Self::from_flac_bytes_inner(&data, parse_mode)
772		} else {
773			Self::from_flac_bytes_inner(bytes, parse_mode)
774		}
775	}
776
777	fn from_flac_bytes_inner(
778		content: &[u8],
779		parse_mode: ParsingMode,
780	) -> Result<(Self, PictureInformation)> {
781		use crate::macros::try_vec;
782
783		let mut size = content.len();
784		let mut reader = Cursor::new(content);
785
786		if size < 32 {
787			err!(NotAPicture);
788		}
789
790		let pic_ty = reader.read_u32::<BigEndian>()?;
791		size -= 4;
792
793		// ID3v2 APIC uses a single byte for picture type.
794		// Anything greater than that is probably invalid, so
795		// we just stop early
796		if pic_ty > 255 && parse_mode == ParsingMode::Strict {
797			err!(NotAPicture);
798		}
799
800		let mime_len = reader.read_u32::<BigEndian>()? as usize;
801		size -= 4;
802
803		if mime_len > size {
804			err!(SizeMismatch);
805		}
806
807		let mime_type_str = utf8_decode_str(&content[8..8 + mime_len])?;
808		size -= mime_len;
809
810		reader.seek(SeekFrom::Current(mime_len as i64))?;
811
812		let desc_len = reader.read_u32::<BigEndian>()? as usize;
813		size -= 4;
814
815		let mut description = None;
816		if desc_len > 0 && desc_len < size {
817			let pos = 12 + mime_len;
818
819			if let Ok(desc) = utf8_decode_str(&content[pos..pos + desc_len]) {
820				description = Some(desc.to_owned().into());
821			}
822
823			size -= desc_len;
824			reader.seek(SeekFrom::Current(desc_len as i64))?;
825		}
826
827		let width = reader.read_u32::<BigEndian>()?;
828		let height = reader.read_u32::<BigEndian>()?;
829		let color_depth = reader.read_u32::<BigEndian>()?;
830		let num_colors = reader.read_u32::<BigEndian>()?;
831		let data_len = reader.read_u32::<BigEndian>()? as usize;
832		size -= 20;
833
834		if data_len <= size {
835			let mut data = try_vec![0; data_len];
836
837			if let Ok(()) = reader.read_exact(&mut data) {
838				let mime_type;
839				if mime_type_str.is_empty() {
840					mime_type = None;
841				} else {
842					mime_type = Some(MimeType::from_str(mime_type_str));
843				}
844
845				return Ok((
846					Self {
847						pic_type: PictureType::from_u8(pic_ty as u8),
848						mime_type,
849						description,
850						data: Cow::from(data),
851					},
852					PictureInformation {
853						width,
854						height,
855						color_depth,
856						num_colors,
857					},
858				));
859			}
860		}
861
862		err!(NotAPicture)
863	}
864
865	/// Convert a [`Picture`] to an APE Cover Art byte vec:
866	///
867	/// NOTE: This is only the picture data and description, a
868	/// key and terminating null byte will not be prepended.
869	/// To map a [`PictureType`] to an APE key see [`PictureType::as_ape_key`]
870	pub fn as_ape_bytes(&self) -> Vec<u8> {
871		let mut data: Vec<u8> = Vec::new();
872
873		if let Some(desc) = &self.description {
874			data.extend(desc.as_bytes());
875		}
876
877		data.push(0);
878		data.extend(self.data.iter());
879
880		data
881	}
882
883	/// Get a [`Picture`] from an APEv2 binary item:
884	///
885	/// NOTE: This function expects `bytes` to contain *only* the APE item data
886	///
887	/// # Errors
888	///
889	/// This function will return [`NotAPicture`](ErrorKind::NotAPicture)
890	/// if at any point it's unable to parse the data
891	pub fn from_ape_bytes(key: &str, bytes: &[u8]) -> Result<Self> {
892		if bytes.is_empty() {
893			err!(NotAPicture);
894		}
895
896		let pic_type = PictureType::from_ape_key(key);
897
898		let reader = &mut &*bytes;
899		let mut pos = 0;
900
901		let mut description = None;
902		let mut desc_text = String::new();
903
904		while let Ok(ch) = reader.read_u8() {
905			pos += 1;
906
907			if ch == b'\0' {
908				break;
909			}
910
911			desc_text.push(char::from(ch));
912		}
913
914		if !desc_text.is_empty() {
915			description = Some(Cow::from(desc_text));
916		}
917
918		let mime_type = {
919			let mut identifier = [0; 8];
920			reader.read_exact(&mut identifier)?;
921
922			Self::mimetype_from_bin(&identifier[..])?
923		};
924
925		let data = Cow::from(bytes[pos..].to_vec());
926
927		Ok(Picture {
928			pic_type,
929			mime_type: Some(mime_type),
930			description,
931			data,
932		})
933	}
934
935	pub(crate) fn mimetype_from_bin(bytes: &[u8]) -> Result<MimeType> {
936		match bytes[..8] {
937			[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A] => Ok(MimeType::Png),
938			[0xFF, 0xD8, ..] => Ok(MimeType::Jpeg),
939			[b'G', b'I', b'F', 0x38, 0x37 | 0x39, b'a', ..] => Ok(MimeType::Gif),
940			[b'B', b'M', ..] => Ok(MimeType::Bmp),
941			[b'I', b'I', b'*', 0x00, ..] | [b'M', b'M', 0x00, b'*', ..] => Ok(MimeType::Tiff),
942			_ => err!(NotAPicture),
943		}
944	}
945}
946
947impl From<Picture> for Cow<'_, Picture> {
948	fn from(pic: Picture) -> Self {
949		Cow::Owned(pic)
950	}
951}
952
953impl<'a> From<&'a Picture> for Cow<'a, Picture> {
954	fn from(pic: &'a Picture) -> Self {
955		Cow::Borrowed(pic)
956	}
957}