versatiles_core/types/
tile_format.rs

1//! This module defines the `TileFormat` enum, representing various tile formats and their associated
2//! extensions. It includes methods for converting between tile formats and file extensions, and
3//! extracting the format from a filename.
4//!
5//! The `TileFormat` enum supports a variety of tile formats such as `AVIF`, `BIN`, `GEOJSON`, `JPG`,
6//! `JSON`, `PBF`, `PNG`, `SVG`, `TOPOJSON`, and `WEBP`. Each variant provides its canonical file extension
7//! and can be derived from a filename or string representation.
8//!
9//! # Examples
10//!
11//! ```rust
12//! use versatiles_core::TileFormat;
13//!
14//! // Getting the file extension for a tile format
15//! let format = TileFormat::PNG;
16//! assert_eq!(format.as_extension(), ".png");
17//!
18//! // Extracting the tile format from a filename
19//! let mut filename = String::from("map.pbf");
20//! let format = TileFormat::from_filename(&mut filename).unwrap();
21//! assert_eq!(format, TileFormat::MVT);
22//! assert_eq!(filename, "map");
23//!
24//! // Parsing a tile format from a string (case-insensitive)
25//! let format = TileFormat::parse_str("JPEG").unwrap();
26//! assert_eq!(format, TileFormat::JPG);
27//! ```
28
29use super::TileType;
30use anyhow::{Result, bail};
31#[cfg(feature = "cli")]
32use clap::ValueEnum;
33use std::{
34	fmt::{Display, Formatter},
35	path::Path,
36};
37
38/// Enum representing supported tile formats.
39///
40/// Each variant corresponds to a common file extension used for map tiles,
41/// images, or related data formats. Variants like `JPG` also map from
42/// alternative extensions (e.g., `.jpeg`).
43///
44/// # Variants
45/// - `AVIF` - AVIF image format
46/// - `BIN` - Raw binary data
47/// - `GEOJSON` - `GeoJSON` vector data
48/// - `JPG` - JPEG image format (including `.jpeg`)
49/// - `JSON` - Generic JSON data
50/// - `PBF` - Mapbox Vector Tile in Protocol Buffer format
51/// - `PNG` - PNG image format
52/// - `SVG` - SVG image format
53/// - `TOPOJSON` - `TopoJSON` vector data
54/// - `WEBP` - WEBP image format
55#[allow(clippy::upper_case_acronyms)]
56#[cfg_attr(feature = "cli", derive(ValueEnum))]
57#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
58pub enum TileFormat {
59	AVIF,
60	#[default]
61	BIN,
62	GEOJSON,
63	JPG,
64	JSON,
65	MVT,
66	PNG,
67	SVG,
68	TOPOJSON,
69	WEBP,
70}
71
72impl TileFormat {
73	/// Returns a lowercase string identifier for this tile format.
74	///
75	/// # Examples
76	/// ```
77	/// use versatiles_core::TileFormat;
78	/// let format = TileFormat::PNG;
79	/// assert_eq!(format.as_str(), "png");
80	/// ```
81	#[must_use]
82	pub fn as_str(&self) -> &str {
83		match self {
84			TileFormat::AVIF => "avif",
85			TileFormat::BIN => "bin",
86			TileFormat::GEOJSON => "geojson",
87			TileFormat::JPG => "jpg",
88			TileFormat::JSON => "json",
89			TileFormat::MVT => "mvt",
90			TileFormat::PNG => "png",
91			TileFormat::SVG => "svg",
92			TileFormat::TOPOJSON => "topojson",
93			TileFormat::WEBP => "webp",
94		}
95	}
96
97	pub fn try_from_str(value: &str) -> Result<Self> {
98		Ok(match value.to_lowercase().trim() {
99			"avif" => TileFormat::AVIF,
100			"bin" => TileFormat::BIN,
101			"geojson" => TileFormat::GEOJSON,
102			"jpeg" | "jpg" => TileFormat::JPG,
103			"json" => TileFormat::JSON,
104			"pbf" | "mvt" => TileFormat::MVT,
105			"png" => TileFormat::PNG,
106			"svg" => TileFormat::SVG,
107			"topojson" => TileFormat::TOPOJSON,
108			"webp" => TileFormat::WEBP,
109			_ => bail!("Unknown tile format: '{value}'"),
110		})
111	}
112
113	pub fn try_from_path(path: &Path) -> Result<Self> {
114		Self::try_from_str(path.extension().and_then(|s| s.to_str()).unwrap_or_default())
115	}
116
117	/// Returns a string describing the broad data type of this tile format.
118	///
119	/// Possible values are `"image"`, `"vector"`, or `"unknown"`.
120	///
121	/// # Examples
122	/// ```
123	/// use versatiles_core::TileFormat;
124	/// let format = TileFormat::GEOJSON;
125	/// assert_eq!(format.as_type_str(), "vector");
126	/// ```
127	#[must_use]
128	pub fn as_type_str(&self) -> &str {
129		match self {
130			TileFormat::AVIF | TileFormat::JPG | TileFormat::PNG | TileFormat::SVG | TileFormat::WEBP => "image",
131			TileFormat::BIN | TileFormat::JSON => "unknown",
132			TileFormat::GEOJSON | TileFormat::MVT | TileFormat::TOPOJSON => "vector",
133		}
134	}
135
136	/// Returns a MIME type string typically associated with this tile format.
137	///
138	/// These MIME types are approximate and may vary based on context.
139	///
140	/// # Examples
141	/// ```
142	/// use versatiles_core::TileFormat;
143	/// let format = TileFormat::PNG;
144	/// assert_eq!(format.as_mime_str(), "image/png");
145	/// ```
146	#[must_use]
147	pub fn as_mime_str(&self) -> &str {
148		match self {
149			TileFormat::BIN => "application/octet-stream",
150			TileFormat::PNG => "image/png",
151			TileFormat::JPG => "image/jpeg",
152			TileFormat::WEBP => "image/webp",
153			TileFormat::AVIF => "image/avif",
154			TileFormat::SVG => "image/svg+xml",
155			TileFormat::MVT => "vnd.mapbox-vector-tile",
156			TileFormat::GEOJSON => "application/geo+json",
157			TileFormat::TOPOJSON => "application/topo+json",
158			TileFormat::JSON => "application/json",
159		}
160	}
161
162	pub fn try_from_mime(mime: &str) -> Result<Self> {
163		Ok(match mime.to_lowercase().as_str() {
164			"application/octet-stream" => TileFormat::BIN,
165			"image/png" => TileFormat::PNG,
166			"image/jpeg" => TileFormat::JPG,
167			"image/webp" => TileFormat::WEBP,
168			"image/avif" => TileFormat::AVIF,
169			"image/svg+xml" => TileFormat::SVG,
170			"vnd.mapbox-vector-tile" => TileFormat::MVT,
171			"application/geo+json" => TileFormat::GEOJSON,
172			"application/topo+json" => TileFormat::TOPOJSON,
173			"application/json" => TileFormat::JSON,
174			_ => bail!("Unknown MIME type: '{mime}'"),
175		})
176	}
177
178	/// Returns the canonical file extension for this tile format (with a leading dot).
179	///
180	/// # Examples
181	/// ```
182	/// use versatiles_core::TileFormat;
183	/// let format = TileFormat::SVG;
184	/// assert_eq!(format.as_extension(), ".svg");
185	/// ```
186	#[must_use]
187	pub fn as_extension(&self) -> &str {
188		match self {
189			TileFormat::AVIF => ".avif",
190			TileFormat::BIN => ".bin",
191			TileFormat::GEOJSON => ".geojson",
192			TileFormat::JPG => ".jpg",
193			TileFormat::JSON => ".json",
194			TileFormat::MVT => ".pbf",
195			TileFormat::PNG => ".png",
196			TileFormat::SVG => ".svg",
197			TileFormat::TOPOJSON => ".topojson",
198			TileFormat::WEBP => ".webp",
199		}
200	}
201
202	/// Attempts to extract a `TileFormat` from the file extension in `filename`.
203	///
204	/// If a matching extension (e.g. `.pbf` or `.jpeg`) is found, the `TileFormat`
205	/// is returned and the filename is truncated to remove the extension.
206	/// If no known extension is found, returns `None`.
207	///
208	/// # Arguments
209	///
210	/// * `filename` - A mutable `String` representing a filename.\
211	///   If an extension is matched, the filename is truncated (the extension removed).
212	///
213	/// # Examples
214	/// ```
215	/// use versatiles_core::TileFormat;
216	///
217	/// let mut filename = String::from("picture.jpeg");
218	/// let format = TileFormat::from_filename(&mut filename);
219	/// assert_eq!(Some(TileFormat::JPG), format);
220	/// assert_eq!("picture", filename);
221	///
222	/// let mut unknown = String::from("file.abc");
223	/// let format_none = TileFormat::from_filename(&mut unknown);
224	/// assert_eq!(None, format_none);
225	/// assert_eq!("file.abc", unknown);
226	/// ```
227	pub fn from_filename(filename: &mut String) -> Option<Self> {
228		if let Some(index) = filename.rfind('.') {
229			let extension = filename[index..].to_lowercase();
230			let format = match extension.as_str() {
231				".avif" => TileFormat::AVIF,
232				".bin" => TileFormat::BIN,
233				".geojson" => TileFormat::GEOJSON,
234				".jpg" | ".jpeg" => TileFormat::JPG,
235				".json" => TileFormat::JSON,
236				".pbf" => TileFormat::MVT,
237				".png" => TileFormat::PNG,
238				".svg" => TileFormat::SVG,
239				".topojson" => TileFormat::TOPOJSON,
240				".webp" => TileFormat::WEBP,
241				_ => return None,
242			};
243			filename.truncate(index);
244			Some(format)
245		} else {
246			None
247		}
248	}
249
250	/// Attempts to parse a `TileFormat` from a string, ignoring leading dots and whitespace.
251	///
252	/// For instance, `".jpeg"`, `" JPeG "`, or `"svg"` all resolve to recognized tile formats.
253	///
254	/// # Arguments
255	///
256	/// * `value` - The string to parse.
257	///
258	/// # Errors
259	///
260	/// Returns an error if the format is not recognized.
261	///
262	/// # Examples
263	/// ```
264	/// use versatiles_core::TileFormat;
265	///
266	/// // Recognizes .jpeg as JPG.
267	/// let format = TileFormat::parse_str(".jpeg").unwrap();
268	/// assert_eq!(format, TileFormat::JPG);
269	///
270	/// // Returns an error if unknown.
271	/// assert!(TileFormat::parse_str(".abc").is_err());
272	/// ```
273	pub fn parse_str(value: &str) -> Result<Self> {
274		Ok(match value.to_lowercase().trim_matches([' ', '.']) {
275			"avif" => TileFormat::AVIF,
276			"bin" => TileFormat::BIN,
277			"geojson" => TileFormat::GEOJSON,
278			"jpeg" | "jpg" => TileFormat::JPG,
279			"json" => TileFormat::JSON,
280			"mvt" => TileFormat::MVT,
281			"png" => TileFormat::PNG,
282			"svg" => TileFormat::SVG,
283			"topojson" => TileFormat::TOPOJSON,
284			"webp" => TileFormat::WEBP,
285			_ => bail!("Unknown tile format: '{}'", value.trim()),
286		})
287	}
288
289	#[must_use]
290	pub fn get_type(&self) -> TileType {
291		use TileFormat::*;
292		use TileType::*;
293		match self {
294			AVIF | PNG | JPG | WEBP => Raster,
295			MVT => Vector,
296			BIN | GEOJSON | JSON | SVG | TOPOJSON => Unknown,
297		}
298	}
299}
300
301impl TryFrom<&str> for TileFormat {
302	type Error = anyhow::Error;
303
304	fn try_from(value: &str) -> Result<Self> {
305		Self::try_from_str(value)
306	}
307}
308
309impl Display for TileFormat {
310	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
311		f.write_str(self.as_str())
312	}
313}
314
315#[cfg(test)]
316mod tests {
317	use super::*;
318
319	#[test]
320	fn should_return_correct_extension_for_format() {
321		#[rustfmt::skip]
322        let cases = vec![
323            (TileFormat::AVIF, ".avif"),
324            (TileFormat::BIN, ".bin"),
325            (TileFormat::GEOJSON, ".geojson"),
326            (TileFormat::JPG, ".jpg"),
327            (TileFormat::JSON, ".json"),
328            (TileFormat::MVT, ".pbf"),
329            (TileFormat::PNG, ".png"),
330            (TileFormat::SVG, ".svg"),
331            (TileFormat::TOPOJSON, ".topojson"),
332            (TileFormat::WEBP, ".webp"),
333        ];
334
335		for (format, expected) in cases {
336			assert_eq!(format.as_extension(), expected);
337		}
338	}
339
340	#[test]
341	fn should_extract_correct_format_and_truncate_filename_when_extension_found() {
342		struct Case(&'static str, Option<TileFormat>, &'static str);
343
344		let cases = vec![
345			Case("image.avif", Some(TileFormat::AVIF), "image"),
346			Case("archive.zip", None, "archive.zip"),
347			Case("binary.bin", Some(TileFormat::BIN), "binary"),
348			Case("noextensionfile", None, "noextensionfile"),
349			Case("unknown.ext", None, "unknown.ext"),
350			Case("data.geojson", Some(TileFormat::GEOJSON), "data"),
351			Case("image.jpeg", Some(TileFormat::JPG), "image"),
352			Case("image.jpg", Some(TileFormat::JPG), "image"),
353			Case("document.json", Some(TileFormat::JSON), "document"),
354			Case("map.pbf", Some(TileFormat::MVT), "map"),
355			Case("picture.png", Some(TileFormat::PNG), "picture"),
356			Case("diagram.svg", Some(TileFormat::SVG), "diagram"),
357			Case("vector.SVG", Some(TileFormat::SVG), "vector"),
358			Case("topography.topojson", Some(TileFormat::TOPOJSON), "topography"),
359			Case("photo.webp", Some(TileFormat::WEBP), "photo"),
360		];
361
362		for case in cases {
363			let mut filename = String::from(case.0);
364			let format = TileFormat::from_filename(&mut filename);
365			assert_eq!(format, case.1);
366			assert_eq!(filename, case.2);
367		}
368	}
369
370	#[test]
371	fn should_parse_str_into_tileformat() {
372		struct Case(&'static str, Option<TileFormat>);
373
374		let cases = vec![
375			Case("avif", Some(TileFormat::AVIF)),
376			Case(".bin", Some(TileFormat::BIN)),
377			Case("GEOJSON", Some(TileFormat::GEOJSON)),
378			Case("jpeg", Some(TileFormat::JPG)),
379			Case("jpg", Some(TileFormat::JPG)),
380			Case(".json", Some(TileFormat::JSON)),
381			Case(" mvt ", Some(TileFormat::MVT)),
382			Case("png", Some(TileFormat::PNG)),
383			Case(".topojson", Some(TileFormat::TOPOJSON)),
384			Case(".webp", Some(TileFormat::WEBP)),
385			Case("unknown", None),
386		];
387
388		for case in cases {
389			let result = TileFormat::parse_str(case.0);
390			match case.1 {
391				Some(expected_format) => {
392					assert_eq!(result.unwrap(), expected_format);
393				}
394				None => {
395					assert!(result.is_err());
396				}
397			}
398		}
399	}
400
401	#[test]
402	fn should_provide_meaningful_strings_for_debug_and_display() {
403		let format = TileFormat::PNG;
404		assert!(format!("{format:?}").contains("PNG"));
405		assert_eq!(format!("{format}"), "png");
406	}
407
408	#[test]
409	fn should_return_lowercase_string_for_as_str() {
410		#![allow(clippy::enum_variant_names)]
411		#[rustfmt::skip]
412        let cases = vec![
413            (TileFormat::AVIF, "avif"),
414            (TileFormat::BIN, "bin"),
415            (TileFormat::GEOJSON, "geojson"),
416            (TileFormat::JPG, "jpg"),
417            (TileFormat::JSON, "json"),
418            (TileFormat::MVT, "mvt"),
419            (TileFormat::PNG, "png"),
420            (TileFormat::SVG, "svg"),
421            (TileFormat::TOPOJSON, "topojson"),
422            (TileFormat::WEBP, "webp"),
423        ];
424		for (format, expected) in cases {
425			assert_eq!(format.as_str(), expected);
426		}
427	}
428
429	#[test]
430	fn should_return_correct_type_str() {
431		assert_eq!(TileFormat::PNG.as_type_str(), "image");
432		assert_eq!(TileFormat::MVT.as_type_str(), "vector");
433		assert_eq!(TileFormat::BIN.as_type_str(), "unknown");
434	}
435
436	#[test]
437	fn should_return_correct_mime_str() {
438		assert_eq!(TileFormat::PNG.as_mime_str(), "image/png");
439		assert_eq!(TileFormat::JPG.as_mime_str(), "image/jpeg");
440		assert_eq!(TileFormat::GEOJSON.as_mime_str(), "application/geo+json");
441	}
442
443	#[test]
444	fn should_try_from_str_parse_valid_and_error_invalid() {
445		assert_eq!(TileFormat::try_from_str("png").unwrap(), TileFormat::PNG);
446		assert!(TileFormat::try_from_str("invalid").is_err());
447	}
448
449	#[test]
450	fn should_try_from_mime_parse_valid_and_error_invalid() {
451		assert_eq!(TileFormat::try_from_mime("image/webp").unwrap(), TileFormat::WEBP);
452		assert!(TileFormat::try_from_mime("application/x-unknown").is_err());
453	}
454
455	#[test]
456	fn should_get_type_return_expected() {
457		use super::TileType::*;
458		assert_eq!(TileFormat::PNG.get_type(), Raster);
459		assert_eq!(TileFormat::MVT.get_type(), Vector);
460		assert_eq!(TileFormat::BIN.get_type(), Unknown);
461	}
462}