1use 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
15pub 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#[derive(Debug, Clone, Eq, PartialEq, Hash)]
42#[non_exhaustive]
43pub enum MimeType {
44 Png,
46 Jpeg,
48 Tiff,
50 Bmp,
52 Gif,
54 Unknown(String),
56}
57
58impl MimeType {
59 #[must_use]
70 #[allow(clippy::should_implement_trait)] 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 #[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 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#[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 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 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 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 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#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
283pub struct PictureInformation {
284 pub width: u32,
286 pub height: u32,
288 pub color_depth: u32,
290 pub num_colors: u32,
292}
293
294impl PictureInformation {
295 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 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 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 if color_type != 3 {
363 return Ok(ret);
364 }
365
366 let mut reader = Cursor::new(reader);
367
368 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 ret.num_colors = size / 3;
384 break;
385 }
386
387 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 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 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 if marker == 0xDA {
433 break;
434 }
435
436 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#[derive(Clone, Eq, PartialEq, Hash)]
463pub struct Picture {
464 pub(crate) pic_type: PictureType,
466 pub(crate) mime_type: Option<MimeType>,
468 pub(crate) description: Option<Cow<'static, str>>,
470 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 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 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 pub fn pic_type(&self) -> PictureType {
540 self.pic_type
541 }
542
543 pub fn set_pic_type(&mut self, pic_type: PictureType) {
545 self.pic_type = pic_type
546 }
547
548 pub fn mime_type(&self) -> Option<&MimeType> {
553 self.mime_type.as_ref()
554 }
555
556 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 pub fn description(&self) -> Option<&str> {
566 self.description.as_deref()
567 }
568
569 pub fn set_description(&mut self, description: Option<String>) {
571 self.description = description.map(Cow::from);
572 }
573
574 pub fn data(&self) -> &[u8] {
576 &self.data
577 }
578
579 pub fn into_data(self) -> Vec<u8> {
581 self.data.into_owned()
582 }
583
584 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 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 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 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 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
826pub(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};