1use base64::Engine;
4use image::{DynamicImage, GenericImageView, RgbImage};
5use std::collections::HashMap;
6use std::convert::AsRef;
7use std::fs::File;
8use std::io::{Cursor, Read, Seek, Write};
9use std::path::Path;
10use thiserror::Error;
11
12use crate::utils::read_picture_block;
13
14mod reading;
15mod utils;
16mod writing;
17
18const VORBIS_HEADER: [u8; 7] = [3, 118, 111, 114, 98, 105, 115];
19const THEORA_HEADER: [u8; 7] = [0x81, 0x74, 0x68, 0x65, 0x6F, 0x72, 0x61];
20
21#[derive(Error, Debug)]
25#[non_exhaustive]
26pub enum Error {
27 #[error("No vorbis or theora comment packet found. oggmeta only supports vorbis and theora comments. your file may be malformed.")]
30 NoComments,
31 #[error("{0}")]
33 IoError(#[from] std::io::Error),
34 #[error("{0}")]
37 InvalidString(#[from] std::string::FromUtf8Error),
38 #[error("{0}")]
41 InvalidLength(#[from] std::num::TryFromIntError),
42 #[error("there was an error parsing the ogg file. your file is most likely malformed.")]
45 ParseError,
46 #[error("{0}")]
49 NullError(#[from] std::ffi::NulError),
50 #[error("{0}")]
52 ImageError(#[from] image::error::ImageError),
53 #[error("{0}")]
55 StrError(#[from] std::str::Utf8Error),
56 #[error("{0}")]
58 OggError(#[from] ogg::OggReadError),
59 #[error("{0}")]
61 Base64Error(#[from] base64::DecodeError),
62}
63
64#[derive(Clone, Debug, Default)]
66pub struct Tag {
67 pub vendor: String,
68 pub comments: HashMap<String, Vec<String>>,
71 pub pictures: Vec<Picture>,
72}
73
74#[derive(Clone, Debug)]
77pub struct Picture {
78 pub picture_type: PictureType,
80 pub media_type: String,
83 pub description: String,
85 pub width: u32,
87 pub height: u32,
89 pub color_depth: u32,
91 pub number_colors: u32,
93 pub data: Vec<u8>,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99#[repr(u32)]
100pub enum PictureType {
101 Other = 0,
102 PngIcon = 1,
103 GeneralIcon = 2,
104 FrontCover = 3,
105 BackCover = 4,
106 LinerNotesPage = 5,
107 MediaLabel = 6,
108 LeadArtist = 7,
109 Artist = 8,
110 Conductor = 9,
111 Band = 10,
112 Composer = 11,
113 Lyricist = 12,
114 RecordingLocation = 13,
115 DuringRecording = 14,
116 DuringPerformance = 15,
117 MovieScreenCapture = 16,
118 BrightColoredFish = 17,
119 Illustration = 18,
120 BandLogo = 19,
121 PublisherLogo = 20,
122}
123
124impl TryFrom<u32> for PictureType {
125 type Error = ();
126
127 fn try_from(value: u32) -> Result<Self, Self::Error> {
128 match value {
129 0 => Ok(PictureType::Other),
130 1 => Ok(PictureType::PngIcon),
131 2 => Ok(PictureType::GeneralIcon),
132 3 => Ok(PictureType::FrontCover),
133 4 => Ok(PictureType::BackCover),
134 5 => Ok(PictureType::LinerNotesPage),
135 6 => Ok(PictureType::MediaLabel),
136 7 => Ok(PictureType::LeadArtist),
137 8 => Ok(PictureType::Artist),
138 9 => Ok(PictureType::Conductor),
139 10 => Ok(PictureType::Band),
140 11 => Ok(PictureType::Composer),
141 12 => Ok(PictureType::Lyricist),
142 13 => Ok(PictureType::RecordingLocation),
143 14 => Ok(PictureType::DuringRecording),
144 15 => Ok(PictureType::DuringPerformance),
145 16 => Ok(PictureType::MovieScreenCapture),
146 17 => Ok(PictureType::BrightColoredFish),
147 18 => Ok(PictureType::Illustration),
148 19 => Ok(PictureType::BandLogo),
149 20 => Ok(PictureType::PublisherLogo),
150 _ => Err(()),
151 }
152 }
153}
154
155impl Tag {
156 pub fn read_from<R: Read + Seek>(read: &mut R) -> Result<Tag, Error> {
168 reading::parse_file(read)
169 }
170
171 pub fn read_from_path<P: AsRef<Path>>(path: &P) -> Result<Tag, Error> {
176 let mut file = File::open(path)?;
177
178 reading::parse_file(&mut file)
179 }
180
181 pub fn write_to<W: Read + Write + Seek>(&mut self, mut f_in: W) -> Result<(), crate::Error> {
185 let mut buf = Vec::new();
186 crate::writing::insert_comments(&mut f_in, &mut buf, self)?;
187 f_in.rewind()?;
188 std::io::copy(&mut buf.as_slice(), &mut f_in)?;
189 Ok(())
190 }
191
192 pub fn write_to_path<P: AsRef<Path>>(&mut self, path: P) -> Result<(), crate::Error> {
196 let mut file = File::options()
197 .read(true)
198 .write(true)
199 .create(false)
200 .open(path)?;
201
202 self.write_to(&mut file)?;
203
204 Ok(())
205 }
206}
207
208impl Picture {
209 pub fn from_raw_block(data: &Vec<u8>) -> Result<Picture, crate::Error> {
210 let mut buf = base64::engine::general_purpose::STANDARD_NO_PAD.decode(data)?;
211
212 read_picture_block(&mut Cursor::new(&mut buf))
213 }
214}
215
216impl From<RgbImage> for Picture {
217 fn from(img: RgbImage) -> Self {
218 let mut img_buf = Cursor::new(vec![]);
219 let (width, height) = img.dimensions();
220 img.write_to(&mut img_buf, image::ImageFormat::Jpeg)
221 .unwrap();
222
223 Picture {
224 picture_type: PictureType::FrontCover,
225 media_type: "image/jpeg".to_string(),
226 description: "Cover (front)".to_string(),
227 width,
228 height,
229 color_depth: 24,
230 number_colors: 0,
231 data: img_buf.into_inner(),
232 }
233 }
234}
235
236impl From<DynamicImage> for Picture {
237 fn from(img: DynamicImage) -> Self {
238 let mut img_buf = Cursor::new(vec![]);
239 let (width, height) = img.dimensions();
240 img.write_to(&mut img_buf, image::ImageFormat::Jpeg)
241 .unwrap();
242
243 Picture {
244 picture_type: PictureType::FrontCover,
245 media_type: "image/jpeg".to_string(),
246 description: "Cover (front)".to_string(),
247 width,
248 height,
249 color_depth: 24,
250 number_colors: 0,
251 data: img_buf.into_inner(),
252 }
253 }
254}
255
256impl TryFrom<&[u8]> for Picture {
257 type Error = crate::Error;
258
259 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
260 let dyn_img = image::load_from_memory(value)?;
261 Ok(dyn_img.into())
262 }
263}