1use std::fmt::Display;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum Format {
9 Gif,
10 Jpeg,
11 Json,
12 Mvt,
13 Png,
14 Webp,
15}
16
17impl Format {
18 #[must_use]
19 pub fn from_suffix(value: &str) -> Option<Self> {
20 Some(match value.to_ascii_lowercase().as_str() {
21 "gif" => Self::Gif,
22 "jpg" | "jpeg" => Self::Jpeg,
23 "json" => Self::Json,
24 "pbf" | "mvt" => Self::Mvt,
25 "png" => Self::Png,
26 "webp" => Self::Webp,
27 _ => None?,
28 })
29 }
30
31 pub fn from_content_type(mime: &str) -> Option<Self> {
32 Some(match mime {
33 "image/gif" => Self::Gif,
34 "image/jpeg" => Self::Jpeg,
35 "application/json" => Self::Json,
36 "application/x-protobuf" => Self::Mvt,
37 "image/png" => Self::Png,
38 "image/webp" => Self::Webp,
39 _ => None?,
40 })
41 }
42
43 pub fn file_suffix(&self) -> &str {
44 match *self {
45 Self::Gif => "gif",
46 Self::Jpeg => "jpg",
47 Self::Json => "json",
48 Self::Mvt => "pbf",
49 Self::Png => "png",
50 Self::Webp => "webp",
51 }
52 }
53
54 #[must_use]
55 pub fn content_type(&self) -> &str {
56 match *self {
57 Self::Gif => "image/gif",
58 Self::Jpeg => "image/jpeg",
59 Self::Json => "application/json",
60 Self::Mvt => "application/x-protobuf",
61 Self::Png => "image/png", Self::Webp => "image/webp",
63 }
64 }
65
66 #[must_use]
67 pub fn is_detectable(&self) -> bool {
68 match *self {
69 Self::Png | Self::Jpeg | Self::Gif | Self::Webp => true,
70 Self::Mvt | Self::Json => false,
74 }
75 }
76}
77
78impl Display for Format {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 match *self {
81 Self::Gif => write!(f, "gif"),
82 Self::Jpeg => write!(f, "jpeg"),
83 Self::Json => write!(f, "json"),
84 Self::Mvt => write!(f, "mvt"),
85 Self::Png => write!(f, "png"),
86 Self::Webp => write!(f, "webp"),
87 }
88 }
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
92pub enum Encoding {
93 Uncompressed = 0b0000_0000,
95 Internal = 0b0000_0001,
97 Gzip = 0b0000_0010,
98 Zlib = 0b0000_0100,
99 Brotli = 0b0000_1000,
100 Zstd = 0b0001_0000,
101}
102
103impl Encoding {
104 #[must_use]
105 pub fn parse(value: &str) -> Option<Self> {
106 Some(match value.to_ascii_lowercase().as_str() {
107 "none" => Self::Uncompressed,
108 "gzip" => Self::Gzip,
109 "zlib" => Self::Zlib,
110 "brotli" => Self::Brotli,
111 "zstd" => Self::Zstd,
112 _ => None?,
113 })
114 }
115
116 #[must_use]
117 pub fn content_encoding(&self) -> Option<&str> {
118 match *self {
119 Self::Uncompressed | Self::Internal => None,
120 Self::Gzip => Some("gzip"),
121 Self::Zlib => Some("deflate"),
122 Self::Brotli => Some("br"),
123 Self::Zstd => Some("zstd"),
124 }
125 }
126
127 #[must_use]
128 pub fn is_encoded(&self) -> bool {
129 match *self {
130 Self::Uncompressed | Self::Internal => false,
131 Self::Gzip | Self::Zlib | Self::Brotli | Self::Zstd => true,
132 }
133 }
134}
135
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub struct TileInfo {
138 pub format: Format,
139 pub encoding: Encoding,
140}
141
142impl TileInfo {
143 #[must_use]
144 pub fn new(format: Format, encoding: Encoding) -> Self {
145 Self { format, encoding }
146 }
147
148 #[must_use]
150 #[allow(clippy::enum_glob_use)]
151 pub fn detect(value: &[u8]) -> Option<Self> {
152 use Encoding::*;
153 use Format::*;
154
155 Some(match value {
161 v if v.starts_with(b"\x1f\x8b") => Self::new(Mvt, Gzip),
163 v if v.starts_with(b"\x78\x9c") => Self::new(Mvt, Zlib),
164 v if v.starts_with(b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A") => Self::new(Png, Internal),
165 v if v.starts_with(b"\x47\x49\x46\x38\x39\x61") => Self::new(Gif, Internal),
166 v if v.starts_with(b"\xFF\xD8\xFF") => Self::new(Jpeg, Internal),
167 v if v.starts_with(b"RIFF") && v.len() > 8 && v[8..].starts_with(b"WEBP") => {
168 Self::new(Webp, Internal)
169 }
170 v if v.starts_with(b"{") => Self::new(Json, Uncompressed),
171 _ => None?,
172 })
173 }
174
175 #[must_use]
176 pub fn encoding(self, encoding: Encoding) -> Self {
177 Self { encoding, ..self }
178 }
179}
180
181impl From<Format> for TileInfo {
182 fn from(format: Format) -> Self {
183 Self::new(
184 format,
185 match format {
186 Format::Png | Format::Jpeg | Format::Webp | Format::Gif => Encoding::Internal,
187 Format::Mvt | Format::Json => Encoding::Uncompressed,
188 },
189 )
190 }
191}
192
193impl Display for TileInfo {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 write!(f, "{}", self.format.content_type())?;
196 if let Some(encoding) = self.encoding.content_encoding() {
197 write!(f, "; encoding={encoding}")?;
198 } else if self.encoding != Encoding::Uncompressed {
199 write!(f, "; uncompressed")?;
200 }
201 Ok(())
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use std::fs::read;
208
209 use Encoding::{Internal, Uncompressed};
210 use Format::{Jpeg, Json, Png, Webp};
211
212 use super::*;
213
214 fn detect(path: &str) -> Option<TileInfo> {
215 TileInfo::detect(&read(path).unwrap())
216 }
217
218 #[allow(clippy::unnecessary_wraps)]
219 fn info(format: Format, encoding: Encoding) -> Option<TileInfo> {
220 Some(TileInfo::new(format, encoding))
221 }
222
223 #[test]
224 fn test_data_format_png() {
225 assert_eq!(detect("./fixtures/world.png"), info(Png, Internal));
226 }
227
228 #[test]
229 fn test_data_format_jpg() {
230 assert_eq!(detect("./fixtures/world.jpg"), info(Jpeg, Internal));
231 }
232
233 #[test]
234 fn test_data_format_webp() {
235 assert_eq!(detect("./fixtures/dc.webp"), info(Webp, Internal));
236 assert_eq!(TileInfo::detect(br#"RIFF"#), None);
237 }
238
239 #[test]
240 fn test_data_format_json() {
241 assert_eq!(
242 TileInfo::detect(br#"{"foo":"bar"}"#),
243 info(Json, Uncompressed)
244 );
245 }
246}