utiles_core/
tile_type.rs

1//! tile-type module
2//!
3//! This is strongly influenced by the `TileInfo` struct from the `martin` crate.
4//! The original version of this module was written and much more aligned with
5//! the npm package `@mapbox/tiletype` and did not include `TileEncoding`.
6
7use std::fmt::Display;
8use std::str::FromStr;
9
10/// `TileKind` over arching type of tile data
11#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
12pub enum TileKind {
13    /// Unknown tile kind
14    Unknown,
15
16    /// Vector tile
17    Vector,
18
19    /// Raster (image) tile
20    Raster,
21
22    /// `JSON` tile
23    Json,
24
25    /// `GeoJSON` tile
26    GeoJson,
27}
28
29impl Display for TileKind {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        let s = match self {
32            Self::Vector => "vector",
33            Self::Raster => "raster",
34            Self::Json => "json",
35            Self::GeoJson => "geojson",
36            Self::Unknown => "unknown",
37        };
38        write!(f, "{s}")
39    }
40}
41
42impl TileKind {
43    #[must_use]
44    pub fn parse(value: &str) -> Option<Self> {
45        Some(match value.to_ascii_lowercase().as_str() {
46            "vector" | "vec" => Self::Vector,
47            "raster" | "image" | "img" => Self::Raster,
48            "json" => Self::Json,
49            "geojson" => Self::GeoJson,
50            _ => None?,
51        })
52    }
53}
54
55/// Tile format
56#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
57pub enum RasterTileFormat {
58    /// GIF image
59    Gif,
60
61    /// JPEG image
62    Jpg,
63
64    /// PNG image
65    Png,
66
67    /// TIFF image
68    Tiff,
69
70    /// `WebP` image
71    Webp,
72}
73
74/// Tile format
75#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
76pub enum TileFormat {
77    /// Unknown format
78    Unknown,
79
80    // ===================
81    // VECTOR TILE FORMATS
82    // ===================
83    /// MVT Protocol Buffer format (AKA mvt)
84    Pbf,
85
86    /// MLT (Maplibre vector tile) future format
87    Mlt,
88
89    // =============
90    // Image formats
91    // =============
92    /// GIF image
93    Gif,
94
95    /// JPEG image
96    Jpg,
97
98    /// PNG image
99    Png,
100
101    /// TIFF image
102    Tiff,
103
104    /// `WebP` image
105    Webp,
106
107    // ============
108    // JSON FORMATS
109    // ============
110    /// JSON string
111    Json,
112
113    /// `GeoJSON` string
114    GeoJson,
115}
116
117impl From<RasterTileFormat> for TileFormat {
118    fn from(raster_format: RasterTileFormat) -> Self {
119        match raster_format {
120            RasterTileFormat::Gif => Self::Gif,
121            RasterTileFormat::Jpg => Self::Jpg,
122            RasterTileFormat::Png => Self::Png,
123            RasterTileFormat::Tiff => Self::Tiff,
124            RasterTileFormat::Webp => Self::Webp,
125        }
126    }
127}
128
129impl Display for TileFormat {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        let s = match self {
132            Self::Png => "png",
133            Self::Jpg => "jpg",
134            Self::Gif => "gif",
135            Self::Webp => "webp",
136            Self::Pbf => "pbf",
137            Self::Mlt => "mlt",
138            Self::Json => "json",
139            Self::GeoJson => "geojson",
140            Self::Tiff => "tiff",
141            Self::Unknown => "unknown",
142        };
143        write!(f, "{s}")
144    }
145}
146impl RasterTileFormat {
147    #[must_use]
148    pub fn parse(value: &str) -> Option<Self> {
149        Some(match value.to_ascii_lowercase().as_str() {
150            "png" => Self::Png,
151            "webp" => Self::Webp,
152            "gif" => Self::Gif,
153            "jpg" | "jpeg" => Self::Jpg,
154            _ => None?,
155        })
156    }
157
158    #[must_use]
159    pub fn content_type(&self) -> &'static str {
160        match self {
161            Self::Png => "image/png",
162            Self::Jpg => "image/jpeg",
163            Self::Gif => "image/gif",
164            Self::Webp => "image/webp",
165            Self::Tiff => "image/tiff",
166        }
167    }
168}
169
170impl FromStr for TileFormat {
171    type Err = ();
172
173    fn from_str(s: &str) -> Result<Self, Self::Err> {
174        match s.to_ascii_lowercase().as_str() {
175            "png" => Ok(Self::Png),
176            "webp" => Ok(Self::Webp),
177            "pbf" | "mvt" => Ok(Self::Pbf),
178            "gif" => Ok(Self::Gif),
179            "jpg" | "jpeg" => Ok(Self::Jpg),
180            "json" => Ok(Self::Json),
181            "geojson" => Ok(Self::GeoJson),
182            _ => Err(()),
183        }
184    }
185}
186
187impl TileFormat {
188    #[must_use]
189    pub fn try_parse(value: &str) -> Option<Self> {
190        Self::from_str(value).map(Some).unwrap_or(None)
191    }
192
193    #[must_use]
194    pub fn is_img(&self) -> bool {
195        matches!(
196            self,
197            Self::Png | Self::Jpg | Self::Gif | Self::Webp | Self::Tiff
198        )
199    }
200
201    #[must_use]
202    pub fn kind(&self) -> TileKind {
203        match self {
204            Self::Pbf | Self::Mlt => TileKind::Vector,
205            Self::Gif | Self::Jpg | Self::Png | Self::Webp | Self::Tiff => {
206                TileKind::Raster
207            }
208            Self::Json => TileKind::Json,
209            Self::GeoJson => TileKind::GeoJson,
210            Self::Unknown => TileKind::Unknown,
211        }
212    }
213
214    #[must_use]
215    pub fn content_type(&self) -> &'static str {
216        match self {
217            Self::Png => "image/png",
218            Self::Jpg => "image/jpeg",
219            Self::Gif => "image/gif",
220            Self::Webp => "image/webp",
221            Self::Pbf | Self::Mlt => "application/x-protobuf",
222            Self::Json => "application/json",
223            Self::GeoJson => "application/geo+json",
224            Self::Tiff => "image/tiff",
225            Self::Unknown => "application/octet-stream",
226        }
227    }
228}
229
230/// Encoding of the tile data (based on maplibre/martin)
231#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
232pub enum TileEncoding {
233    /// Data is not compressed, but it can be
234    Uncompressed = 0b0000_0000,
235    /// Data is compressed w/ internal encoding (e.g. jpg/png/webp)
236    Internal = 0b0000_0001,
237    /// Data is compressed w/ `gzip`
238    Gzip = 0b0000_0010,
239    /// Data is compressed w/ `zlib`
240    Zlib = 0b0000_0100,
241    /// Data is compressed w/ `brotli`
242    Brotli = 0b0000_1000,
243    /// Data is compressed w/ `zstd`
244    Zstd = 0b0001_0000,
245}
246
247impl TileEncoding {
248    #[must_use]
249    pub fn parse(value: &str) -> Option<Self> {
250        Some(match value.to_ascii_lowercase().as_str() {
251            "none" => Self::Uncompressed,
252            "gzip" | "gz" => Self::Gzip,
253            "zlib" | "deflate" | "zz" => Self::Zlib,
254            "brotli" | "br" => Self::Brotli,
255            "zstd" | "zst" => Self::Zstd,
256            "internal" | "png" | "jpg" | "jpeg" | "webp" | "gif" => Self::Internal,
257            _ => None?,
258        })
259    }
260
261    #[must_use]
262    pub fn content_encoding(&self) -> Option<&'static str> {
263        match self {
264            Self::Internal | Self::Uncompressed => None,
265            Self::Gzip => Some("gzip"),
266            Self::Zlib => Some("deflate"),
267            Self::Brotli => Some("br"),
268            Self::Zstd => Some("zstd"),
269        }
270    }
271
272    #[must_use]
273    pub fn as_str(&self) -> &'static str {
274        match self {
275            Self::Uncompressed => "none",
276            Self::Internal => "internal",
277            Self::Gzip => "gzip",
278            Self::Zlib => "zlib",
279            Self::Brotli => "brotli",
280            Self::Zstd => "zstd",
281        }
282    }
283}
284
285impl Display for TileEncoding {
286    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287        let s = self.as_str();
288        write!(f, "{s}")
289    }
290}
291
292#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
293pub struct TileType {
294    pub encoding: TileEncoding,
295    pub format: TileFormat,
296    pub kind: TileKind,
297}
298
299impl TileType {
300    #[must_use]
301    pub fn new(format: TileFormat, encoding: TileEncoding) -> Self {
302        Self {
303            encoding,
304            format,
305            kind: format.kind(),
306        }
307    }
308
309    #[must_use]
310    pub fn parse(s: &str) -> Option<Self> {
311        Some(match s.to_ascii_lowercase().as_str() {
312            "geojson" => Self::new(TileFormat::GeoJson, TileEncoding::Uncompressed),
313            "gif" => Self::new(TileFormat::Gif, TileEncoding::Internal),
314            "jpg" | "jpeg" => Self::new(TileFormat::Jpg, TileEncoding::Internal),
315            "json" => Self::new(TileFormat::Json, TileEncoding::Uncompressed),
316            "mlt" => Self::new(TileFormat::Mlt, TileEncoding::Uncompressed),
317            "pbf" | "mvt" => Self::new(TileFormat::Pbf, TileEncoding::Uncompressed),
318            "pbf.gz" => Self::new(TileFormat::Pbf, TileEncoding::Gzip),
319            "pbf.zlib" => Self::new(TileFormat::Pbf, TileEncoding::Zlib),
320            "pbf.zst" => Self::new(TileFormat::Pbf, TileEncoding::Zstd),
321            "png" => Self::new(TileFormat::Png, TileEncoding::Internal),
322            "tiff" => Self::new(TileFormat::Tiff, TileEncoding::Uncompressed),
323            "webp" => Self::new(TileFormat::Webp, TileEncoding::Internal),
324            _ => None?,
325        })
326    }
327
328    #[must_use]
329    pub fn from_bytes(buffer: &[u8]) -> Self {
330        if buffer.len() >= 8 {
331            match buffer {
332                v if v.starts_with(b"\x1f\x8b") => {
333                    Self::new(TileFormat::Pbf, TileEncoding::Gzip)
334                }
335                v if zlib_magic_headers(v) => {
336                    Self::new(TileFormat::Pbf, TileEncoding::Zlib)
337                }
338                v if zstd_magic_headers(v) => {
339                    Self::new(TileFormat::Pbf, TileEncoding::Zstd)
340                }
341                v if v.starts_with(b"\x89PNG\r\n\x1a\n") => {
342                    Self::new(TileFormat::Png, TileEncoding::Internal)
343                }
344                v if v.starts_with(b"\xff\xd8") => {
345                    Self::new(TileFormat::Jpg, TileEncoding::Internal)
346                }
347                v if is_webp_buf(v) => {
348                    Self::new(TileFormat::Webp, TileEncoding::Internal)
349                }
350                v if v.starts_with(b"GIF87a") || v.starts_with(b"GIF89a") => {
351                    Self::new(TileFormat::Gif, TileEncoding::Internal)
352                }
353                v if v.starts_with(b"{") || v.starts_with(b"[") => {
354                    Self::new(TileFormat::Json, TileEncoding::Uncompressed)
355                }
356                _ => {
357                    if is_mvt_like(buffer) {
358                        Self::new(TileFormat::Pbf, TileEncoding::Uncompressed)
359                    } else {
360                        Self::new(TileFormat::Unknown, TileEncoding::Uncompressed)
361                    }
362                }
363            }
364        } else {
365            Self::new(TileFormat::Unknown, TileEncoding::Uncompressed)
366        }
367    }
368
369    #[must_use]
370    pub fn content_type(&self) -> &'static str {
371        self.format.content_type()
372    }
373
374    #[must_use]
375    pub fn content_encoding(&self) -> Option<&'static str> {
376        self.encoding.content_encoding()
377    }
378
379    #[must_use]
380    pub fn headers_vec(&self) -> Vec<(&'static str, &'static str)> {
381        if let Some(content_encoding) = self.content_encoding() {
382            vec![
383                ("Content-Type", self.content_type()),
384                ("Content-Encoding", content_encoding),
385            ]
386        } else {
387            vec![("Content-Type", self.content_type())]
388        }
389    }
390
391    #[must_use]
392    pub fn extension(&self) -> String {
393        let fmt_ext = self.format.to_string();
394        if self.format.is_img() {
395            fmt_ext
396        } else {
397            match self.encoding {
398                TileEncoding::Internal | TileEncoding::Uncompressed => fmt_ext,
399                TileEncoding::Gzip => format!("{fmt_ext}.gz"),
400                TileEncoding::Zlib => format!("{fmt_ext}.zlib"),
401                TileEncoding::Brotli => format!("{fmt_ext}.br"),
402                TileEncoding::Zstd => format!("{fmt_ext}.zst"),
403            }
404        }
405    }
406}
407
408impl Display for TileType {
409    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410        write!(f, "{} {}", self.format, self.encoding)
411    }
412}
413
414///////////////////////////////////////////////////////////////////////////////
415// legacy
416///////////////////////////////////////////////////////////////////////////////
417
418/// `TileType` or format of the tile data
419pub enum TileTypeV1 {
420    /// Unknown format
421    Unknown = 0,
422
423    /// GIF image
424    Gif = 1,
425
426    /// JPEG image
427    Jpg = 2,
428
429    /// JSON string
430    Json = 3,
431
432    /// Protocol Buffer format (AKA mvt)
433    Pbf = 4,
434
435    /// Protocol Buffer format (AKA mvt) compressed with gzip
436    Pbfgz = 5,
437
438    /// PNG image
439    Png = 6,
440
441    /// `WebP` image
442    Webp = 7,
443}
444impl TileTypeV1 {
445    #[must_use]
446    pub fn headers(&self) -> Vec<(&'static str, &'static str)> {
447        match self {
448            TileTypeV1::Png => vec![("Content-Type", "image/png")],
449            TileTypeV1::Jpg => vec![("Content-Type", "image/jpeg")],
450            TileTypeV1::Json => vec![("Content-Type", "application/json")],
451            TileTypeV1::Gif => vec![("Content-Type", "image/gif")],
452            TileTypeV1::Webp => vec![("Content-Type", "image/webp")],
453            TileTypeV1::Pbf => vec![
454                ("Content-Type", "application/x-protobuf"),
455                ("Content-Encoding", "deflate"),
456            ],
457            TileTypeV1::Pbfgz => vec![
458                ("Content-Type", "application/x-protobuf"),
459                ("Content-Encoding", "gzip"),
460            ],
461            TileTypeV1::Unknown => vec![],
462        }
463    }
464}
465
466/// constant for unknown tile type
467pub const TILETYPE_UNKNOWN: usize = 0;
468
469/// constant for gif tile type
470pub const TILETYPE_GIF: usize = 1;
471
472/// constant for jpg tile type
473pub const TILETYPE_JPG: usize = 2;
474
475/// constant for json tile type
476pub const TILETYPE_JSON: usize = 3;
477
478/// constant for pbf tile type
479pub const TILETYPE_PBF: usize = 4;
480
481/// constant for pbfgz tile type
482pub const TILETYPE_PBFGZ: usize = 5;
483
484/// constant for png tile type
485pub const TILETYPE_PNG: usize = 6;
486
487/// constant for webp tile type
488pub const TILETYPE_WEBP: usize = 7;
489
490/// Return true if buffer starts with zlib magic headers
491/// 78 01 - No Compression/low
492/// 78 5E - Fast Compression
493/// 78 9C - Default Compression
494/// 78 DA - Best Compression
495#[must_use]
496#[inline]
497pub fn zlib_magic_headers(buffer: &[u8]) -> bool {
498    buffer.starts_with(
499        b"\x78\x01", // No Compression/low
500    ) || buffer.starts_with(
501        b"\x78\x5E", // Fast Compression
502    ) || buffer.starts_with(
503        b"\x78\x9C", // Default Compression
504    ) || buffer.starts_with(
505        b"\x78\xDA", // Best Compression
506    )
507}
508
509/// zstd magic headers
510/// 28 B5 2F FD
511#[must_use]
512#[inline]
513pub fn zstd_magic_headers(buffer: &[u8]) -> bool {
514    buffer.starts_with(b"\x28\xB5\x2F\xFD")
515}
516
517#[must_use]
518#[inline]
519pub fn is_webp_buf(data: &[u8]) -> bool {
520    data.starts_with(b"RIFF") && data.len() > 8 && data[8..].starts_with(b"WEBP")
521}
522
523/// Return true if buffer is **maybe** a mapbox-vector-tile (without parsing)
524fn is_mvt_like(buffer: &[u8]) -> bool {
525    if buffer.len() < 2 {
526        return false; // Too small to be a valid MVT
527    }
528
529    // Check the first few bytes for common MVT protobuf key-value indicators
530    let mut i = 0;
531    while i < buffer.len() {
532        let key = buffer[i] >> 3; // Protobuf field number is in the higher bits
533        let wire_type = buffer[i] & 0x07; // Lower bits for wire type
534        i += 1;
535
536        if key == 0 || key > 15 {
537            return false; // Not a valid field number for MVT
538        }
539
540        match wire_type {
541            0 => {
542                // Varint
543                while i < buffer.len() && buffer[i] & 0x80 != 0 {
544                    i += 1;
545                }
546                i += 1;
547            }
548            1 => i += 8, // 64-bit
549            2 => {
550                let mut length = 0;
551                let mut shift = 0;
552                while i < buffer.len() && buffer[i] & 0x80 != 0 {
553                    length |= ((buffer[i] & 0x7F) as usize) << shift;
554                    shift += 7;
555                    i += 1;
556                }
557                if i < buffer.len() {
558                    length |= (buffer[i] as usize) << shift;
559                }
560                i += 1;
561                i += length;
562            }
563            5 => i += 4,
564            _ => return false,
565        }
566
567        if i > buffer.len() {
568            return false;
569        }
570    }
571
572    true
573}
574
575/// Return type of the tile data from a buffer
576#[must_use]
577pub fn tiletype(buffer: &[u8]) -> TileType {
578    TileType::from_bytes(buffer)
579}
580
581/// Return the tile type as a constant
582#[must_use]
583pub fn enum2const(tiletype: &TileTypeV1) -> usize {
584    match tiletype {
585        TileTypeV1::Unknown => TILETYPE_UNKNOWN,
586        TileTypeV1::Gif => TILETYPE_GIF,
587        TileTypeV1::Jpg => TILETYPE_JPG,
588        TileTypeV1::Json => TILETYPE_JSON,
589        TileTypeV1::Pbf => TILETYPE_PBF,
590        TileTypeV1::Pbfgz => TILETYPE_PBFGZ,
591        TileTypeV1::Png => TILETYPE_PNG,
592        TileTypeV1::Webp => TILETYPE_WEBP,
593    }
594}
595
596/// Return the tile type as an enum
597#[must_use]
598pub fn const2enum(tiletype: usize) -> TileTypeV1 {
599    match tiletype {
600        TILETYPE_GIF => TileTypeV1::Gif,
601        TILETYPE_JPG => TileTypeV1::Jpg,
602        TILETYPE_JSON => TileTypeV1::Json,
603        TILETYPE_PBF => TileTypeV1::Pbf,
604        TILETYPE_PBFGZ => TileTypeV1::Pbfgz,
605        TILETYPE_PNG => TileTypeV1::Png,
606        TILETYPE_WEBP => TileTypeV1::Webp,
607        _ => TileTypeV1::Unknown,
608    }
609}
610
611/// Return vector of http headers for a tile type
612#[must_use]
613pub fn headers(tiletype: &TileTypeV1) -> Vec<(&'static str, &'static str)> {
614    tiletype.headers()
615}
616
617/// Return vector of http headers for a tile type from a tile's buffer
618#[must_use]
619pub fn blob2headers(b: &[u8]) -> Vec<(&'static str, &'static str)> {
620    tiletype(b).headers_vec()
621}
622
623/// Return the tile type as a string
624#[must_use]
625pub fn tiletype_str(buffer: &[u8]) -> String {
626    let tiletype = tiletype(buffer);
627    tiletype.extension()
628}