1use std::fmt;
44use std::io::{self, Write};
45use std::str::FromStr;
46
47pub mod cesium;
48#[cfg(any(feature = "png", feature = "webp", feature = "avif"))]
49pub mod container;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
54pub enum HeightmapFormat {
55 Terrarium,
57 Mapbox,
59 Gsi,
61}
62
63impl HeightmapFormat {
64 pub const ALL: [HeightmapFormat; 3] = [Self::Terrarium, Self::Mapbox, Self::Gsi];
66
67 pub const fn name(self) -> &'static str {
69 match self {
70 Self::Terrarium => "terrarium",
71 Self::Mapbox => "mapbox",
72 Self::Gsi => "gsi",
73 }
74 }
75}
76
77impl fmt::Display for HeightmapFormat {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 f.write_str(self.name())
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct ParseHeightmapFormatError {
86 pub input: String,
88}
89
90impl fmt::Display for ParseHeightmapFormatError {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 write!(
93 f,
94 "unknown heightmap format `{}` (expected one of: terrarium, mapbox, gsi)",
95 self.input
96 )
97 }
98}
99
100impl std::error::Error for ParseHeightmapFormatError {}
101
102impl FromStr for HeightmapFormat {
103 type Err = ParseHeightmapFormatError;
104
105 fn from_str(s: &str) -> Result<Self, Self::Err> {
106 match s.to_ascii_lowercase().as_str() {
107 "terrarium" => Ok(Self::Terrarium),
108 "mapbox" | "mapbox-rgb" | "terrain-rgb" => Ok(Self::Mapbox),
109 "gsi" | "gsi-dem" => Ok(Self::Gsi),
110 _ => Err(ParseHeightmapFormatError {
111 input: s.to_string(),
112 }),
113 }
114 }
115}
116
117#[inline]
120pub fn encode_pixel(format: HeightmapFormat, elevation: f32) -> [u8; 3] {
121 match format {
122 HeightmapFormat::Terrarium => terrarium::encode_pixel(elevation),
123 HeightmapFormat::Mapbox => mapbox::encode_pixel(elevation),
124 HeightmapFormat::Gsi => gsi::encode_pixel(elevation),
125 }
126}
127
128#[inline]
131pub fn decode_pixel(format: HeightmapFormat, rgb: [u8; 3]) -> f32 {
132 match format {
133 HeightmapFormat::Terrarium => terrarium::decode_pixel(rgb),
134 HeightmapFormat::Mapbox => mapbox::decode_pixel(rgb),
135 HeightmapFormat::Gsi => gsi::decode_pixel(rgb),
136 }
137}
138
139pub fn encode_into(format: HeightmapFormat, elevations: &[f32], out: &mut [u8]) {
143 encode_into_with(elevations, out, |e| encode_pixel(format, e))
144}
145
146pub fn decode_into(format: HeightmapFormat, rgb: &[u8], out: &mut [f32]) {
150 decode_into_with(rgb, out, |px| decode_pixel(format, px))
151}
152
153pub fn encode_to<W: Write>(
157 format: HeightmapFormat,
158 elevations: &[f32],
159 writer: W,
160) -> io::Result<()> {
161 encode_to_with(elevations, writer, |e| encode_pixel(format, e))
162}
163
164pub fn encode(format: HeightmapFormat, elevations: &[f32], width: u32, height: u32) -> Vec<u8> {
170 let expected = (width as usize) * (height as usize);
171 assert_eq!(
172 elevations.len(),
173 expected,
174 "elevations length mismatch: expected {expected}, got {}",
175 elevations.len()
176 );
177 let mut out = vec![0u8; expected * 3];
178 encode_into(format, elevations, &mut out);
179 out
180}
181
182pub fn decode(format: HeightmapFormat, rgb: &[u8], width: u32, height: u32) -> Vec<f32> {
188 let pixels = (width as usize) * (height as usize);
189 assert_eq!(
190 rgb.len(),
191 pixels * 3,
192 "rgb length mismatch: expected {}, got {}",
193 pixels * 3,
194 rgb.len()
195 );
196 let mut out = vec![0f32; pixels];
197 decode_into(format, rgb, &mut out);
198 out
199}
200
201#[derive(Debug, Clone, Copy)]
212pub struct HeightmapView<'a> {
213 pub format: HeightmapFormat,
215 pub rgb: &'a [u8],
217 pub width: u32,
219 pub height: u32,
221}
222
223impl<'a> HeightmapView<'a> {
224 pub fn new(format: HeightmapFormat, rgb: &'a [u8], width: u32, height: u32) -> Self {
226 let pixels = (width as usize) * (height as usize);
227 assert_eq!(
228 rgb.len(),
229 pixels * 3,
230 "rgb length mismatch: expected {}, got {}",
231 pixels * 3,
232 rgb.len()
233 );
234 Self {
235 format,
236 rgb,
237 width,
238 height,
239 }
240 }
241
242 #[inline]
244 pub fn len(&self) -> usize {
245 (self.width as usize) * (self.height as usize)
246 }
247
248 #[inline]
250 pub fn is_empty(&self) -> bool {
251 self.rgb.is_empty()
252 }
253
254 #[inline]
256 pub fn get(&self, x: u32, y: u32) -> f32 {
257 let i = (y as usize) * (self.width as usize) + (x as usize);
258 let o = i * 3;
259 decode_pixel(self.format, [self.rgb[o], self.rgb[o + 1], self.rgb[o + 2]])
260 }
261
262 pub fn iter(&self) -> impl Iterator<Item = f32> + '_ {
264 let fmt = self.format;
265 self.rgb
266 .chunks_exact(3)
267 .map(move |c| decode_pixel(fmt, [c[0], c[1], c[2]]))
268 }
269
270 pub fn decode_into(&self, out: &mut [f32]) {
272 decode_into(self.format, self.rgb, out);
273 }
274
275 pub fn to_vec(&self) -> Vec<f32> {
277 let mut out = vec![0f32; self.len()];
278 self.decode_into(&mut out);
279 out
280 }
281}
282
283#[inline]
288fn encode_into_with(elevations: &[f32], out: &mut [u8], encode_pixel: impl Fn(f32) -> [u8; 3]) {
289 assert_eq!(
290 out.len(),
291 elevations.len() * 3,
292 "rgb buffer length mismatch: expected {}, got {}",
293 elevations.len() * 3,
294 out.len()
295 );
296 for (&e, chunk) in elevations.iter().zip(out.chunks_exact_mut(3)) {
297 chunk.copy_from_slice(&encode_pixel(e));
298 }
299}
300
301#[inline]
302fn decode_into_with(rgb: &[u8], out: &mut [f32], decode_pixel: impl Fn([u8; 3]) -> f32) {
303 assert_eq!(
304 rgb.len(),
305 out.len() * 3,
306 "rgb buffer length mismatch: expected {}, got {}",
307 out.len() * 3,
308 rgb.len()
309 );
310 for (chunk, dst) in rgb.chunks_exact(3).zip(out.iter_mut()) {
311 *dst = decode_pixel([chunk[0], chunk[1], chunk[2]]);
312 }
313}
314
315#[inline]
316fn encode_to_with<W: Write>(
317 elevations: &[f32],
318 mut writer: W,
319 encode_pixel: impl Fn(f32) -> [u8; 3],
320) -> io::Result<()> {
321 let mut buf = [0u8; 4095]; let mut len = 0;
323 for &e in elevations {
324 let px = encode_pixel(e);
325 buf[len] = px[0];
326 buf[len + 1] = px[1];
327 buf[len + 2] = px[2];
328 len += 3;
329 if len + 3 > buf.len() {
330 writer.write_all(&buf[..len])?;
331 len = 0;
332 }
333 }
334 if len > 0 {
335 writer.write_all(&buf[..len])?;
336 }
337 Ok(())
338}
339
340pub mod terrarium {
350 use std::io::{self, Write};
351
352 #[inline]
357 pub fn encode_pixel(elevation: f32) -> [u8; 3] {
358 let v = if elevation.is_nan() {
359 0.0
360 } else {
361 (elevation + 32768.0) * 256.0
362 };
363 let v = v.clamp(0.0, (1u32 << 24) as f32 - 1.0) as u32;
364 [
365 ((v >> 16) & 0xff) as u8,
366 ((v >> 8) & 0xff) as u8,
367 (v & 0xff) as u8,
368 ]
369 }
370
371 #[inline]
373 pub fn decode_pixel(rgb: [u8; 3]) -> f32 {
374 let r = rgb[0] as f32;
375 let g = rgb[1] as f32;
376 let b = rgb[2] as f32;
377 r * 256.0 + g + b / 256.0 - 32768.0
378 }
379
380 pub fn encode_into(elevations: &[f32], out: &mut [u8]) {
382 super::encode_into_with(elevations, out, encode_pixel);
383 }
384
385 pub fn decode_into(rgb: &[u8], out: &mut [f32]) {
387 super::decode_into_with(rgb, out, decode_pixel);
388 }
389
390 pub fn encode_to<W: Write>(elevations: &[f32], writer: W) -> io::Result<()> {
392 super::encode_to_with(elevations, writer, encode_pixel)
393 }
394
395 pub fn encode(elevations: &[f32], width: u32, height: u32) -> Vec<u8> {
401 super::encode(super::HeightmapFormat::Terrarium, elevations, width, height)
402 }
403
404 pub fn decode(rgb: &[u8], width: u32, height: u32) -> Vec<f32> {
410 super::decode(super::HeightmapFormat::Terrarium, rgb, width, height)
411 }
412}
413
414pub mod mapbox {
420 use std::io::{self, Write};
421
422 #[inline]
424 pub fn encode_pixel(elevation: f32) -> [u8; 3] {
425 let v = if elevation.is_nan() {
426 0.0
427 } else {
428 ((elevation + 10000.0) * 10.0).round()
429 };
430 let v = v.clamp(0.0, (1u32 << 24) as f32 - 1.0) as u32;
431 [
432 ((v >> 16) & 0xff) as u8,
433 ((v >> 8) & 0xff) as u8,
434 (v & 0xff) as u8,
435 ]
436 }
437
438 #[inline]
440 pub fn decode_pixel(rgb: [u8; 3]) -> f32 {
441 let r = rgb[0] as f32;
442 let g = rgb[1] as f32;
443 let b = rgb[2] as f32;
444 -10000.0 + (r * 65536.0 + g * 256.0 + b) * 0.1
445 }
446
447 pub fn encode_into(elevations: &[f32], out: &mut [u8]) {
449 super::encode_into_with(elevations, out, encode_pixel);
450 }
451
452 pub fn decode_into(rgb: &[u8], out: &mut [f32]) {
454 super::decode_into_with(rgb, out, decode_pixel);
455 }
456
457 pub fn encode_to<W: Write>(elevations: &[f32], writer: W) -> io::Result<()> {
459 super::encode_to_with(elevations, writer, encode_pixel)
460 }
461
462 pub fn encode(elevations: &[f32], width: u32, height: u32) -> Vec<u8> {
464 super::encode(super::HeightmapFormat::Mapbox, elevations, width, height)
465 }
466
467 pub fn decode(rgb: &[u8], width: u32, height: u32) -> Vec<f32> {
469 super::decode(super::HeightmapFormat::Mapbox, rgb, width, height)
470 }
471}
472
473pub mod gsi {
481 use std::io::{self, Write};
482
483 pub const SENTINEL_RGB: [u8; 3] = [0x80, 0x00, 0x00];
485 const SIGN_BIT: u32 = 1 << 23;
486 const RANGE: i64 = 1 << 24;
487
488 #[inline]
492 pub fn encode_pixel(elevation: f32) -> [u8; 3] {
493 if elevation.is_nan() {
494 return SENTINEL_RGB;
495 }
496 let raw = (elevation as f64 * 100.0).round() as i64;
497 let x = raw.rem_euclid(RANGE) as u32;
498 [
499 ((x >> 16) & 0xff) as u8,
500 ((x >> 8) & 0xff) as u8,
501 (x & 0xff) as u8,
502 ]
503 }
504
505 #[inline]
507 pub fn decode_pixel(rgb: [u8; 3]) -> f32 {
508 let r = rgb[0] as u32;
509 let g = rgb[1] as u32;
510 let b = rgb[2] as u32;
511 let x = (r << 16) | (g << 8) | b;
512 if x == SIGN_BIT {
513 f32::NAN
514 } else if x >= SIGN_BIT {
515 (x as i64 - RANGE) as f32 * 0.01
516 } else {
517 x as f32 * 0.01
518 }
519 }
520
521 pub fn encode_into(elevations: &[f32], out: &mut [u8]) {
523 super::encode_into_with(elevations, out, encode_pixel);
524 }
525
526 pub fn decode_into(rgb: &[u8], out: &mut [f32]) {
528 super::decode_into_with(rgb, out, decode_pixel);
529 }
530
531 pub fn encode_to<W: Write>(elevations: &[f32], writer: W) -> io::Result<()> {
533 super::encode_to_with(elevations, writer, encode_pixel)
534 }
535
536 pub fn encode(elevations: &[f32], width: u32, height: u32) -> Vec<u8> {
538 super::encode(super::HeightmapFormat::Gsi, elevations, width, height)
539 }
540
541 pub fn decode(rgb: &[u8], width: u32, height: u32) -> Vec<f32> {
543 super::decode(super::HeightmapFormat::Gsi, rgb, width, height)
544 }
545}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550
551 fn approx_eq(a: f32, b: f32, tol: f32) -> bool {
552 (a - b).abs() <= tol
553 }
554
555 #[test]
556 fn terrarium_pixel_roundtrip() {
557 for e in [-100.0_f32, 0.0, 100.0, 1234.5, 8000.0, -500.0] {
558 let back = terrarium::decode_pixel(terrarium::encode_pixel(e));
559 assert!(approx_eq(e, back, 0.01), "{e} → {back}");
560 }
561 }
562
563 #[test]
564 fn terrarium_zero_sea_level_is_8000() {
565 assert_eq!(terrarium::encode_pixel(0.0), [0x80, 0x00, 0x00]);
566 }
567
568 #[test]
569 fn terrarium_bulk_matches_pixel() {
570 let elevations: Vec<f32> = vec![-100.0, 0.0, 100.0, 1234.5, 8000.0, -500.0];
571 let bulk = terrarium::encode(&elevations, 6, 1);
572 let from_pixels: Vec<u8> = elevations
573 .iter()
574 .flat_map(|&e| terrarium::encode_pixel(e))
575 .collect();
576 assert_eq!(bulk, from_pixels);
577 }
578
579 #[test]
580 fn mapbox_pixel_roundtrip() {
581 for e in [-100.0_f32, 0.0, 100.0, 1234.5, 8000.0, -500.0] {
582 let back = mapbox::decode_pixel(mapbox::encode_pixel(e));
583 assert!(approx_eq(e, back, 0.1), "{e} → {back}");
584 }
585 }
586
587 #[test]
588 fn mapbox_minimum_value_is_minus_10000() {
589 assert_eq!(mapbox::encode_pixel(-10000.0), [0, 0, 0]);
590 assert_eq!(mapbox::decode_pixel([0, 0, 0]), -10000.0);
591 }
592
593 #[test]
594 fn gsi_sentinel_decodes_to_nan() {
595 assert!(gsi::decode_pixel([0x80, 0x00, 0x00]).is_nan());
596 }
597
598 #[test]
599 fn gsi_nan_encodes_to_sentinel() {
600 assert_eq!(gsi::encode_pixel(f32::NAN), [0x80, 0x00, 0x00]);
601 }
602
603 #[test]
604 fn gsi_pixel_roundtrip_positive_and_negative() {
605 for e in [0.0_f32, 100.0, 3776.24, -10.5, -429.4] {
606 let back = gsi::decode_pixel(gsi::encode_pixel(e));
607 assert!(approx_eq(e, back, 0.01), "{e} → {back}");
608 }
609 }
610
611 #[test]
612 fn gsi_zero_is_all_zero_rgb() {
613 assert_eq!(gsi::encode_pixel(0.0), [0, 0, 0]);
614 }
615
616 #[test]
617 fn format_from_str_accepts_aliases() {
618 assert_eq!("terrarium".parse(), Ok(HeightmapFormat::Terrarium));
619 assert_eq!("TERRARIUM".parse(), Ok(HeightmapFormat::Terrarium));
620 assert_eq!("mapbox".parse(), Ok(HeightmapFormat::Mapbox));
621 assert_eq!("mapbox-rgb".parse(), Ok(HeightmapFormat::Mapbox));
622 assert_eq!("terrain-rgb".parse(), Ok(HeightmapFormat::Mapbox));
623 assert_eq!("gsi".parse(), Ok(HeightmapFormat::Gsi));
624 assert_eq!("gsi-dem".parse(), Ok(HeightmapFormat::Gsi));
625 assert!("bogus".parse::<HeightmapFormat>().is_err());
626 }
627
628 #[test]
629 fn format_display_roundtrips_through_from_str() {
630 for fmt in HeightmapFormat::ALL {
631 let parsed: HeightmapFormat = fmt.to_string().parse().unwrap();
632 assert_eq!(parsed, fmt);
633 }
634 }
635
636 #[test]
637 fn dispatch_matches_per_module_for_every_format() {
638 let elevations: Vec<f32> = vec![-100.0, 0.0, 100.0, 1234.5, -500.0];
639 for fmt in HeightmapFormat::ALL {
640 let dispatched = encode(fmt, &elevations, elevations.len() as u32, 1);
641 let direct = match fmt {
642 HeightmapFormat::Terrarium => terrarium::encode(&elevations, 5, 1),
643 HeightmapFormat::Mapbox => mapbox::encode(&elevations, 5, 1),
644 HeightmapFormat::Gsi => gsi::encode(&elevations, 5, 1),
645 };
646 assert_eq!(dispatched, direct, "encode mismatch for {fmt}");
647
648 for &e in &elevations {
649 let px = encode_pixel(fmt, e);
650 let back = decode_pixel(fmt, px);
651 assert!((e - back).abs() <= 0.1, "[{fmt}] {e} → {px:?} → {back}");
652 }
653 }
654 }
655
656 #[test]
657 fn gsi_bulk_matches_pixel() {
658 let elevations: Vec<f32> = vec![0.0, 100.0, -10.5, f32::NAN, 3776.24];
659 let bulk = gsi::encode(&elevations, elevations.len() as u32, 1);
660 let from_pixels: Vec<u8> = elevations
661 .iter()
662 .flat_map(|&e| gsi::encode_pixel(e))
663 .collect();
664 assert_eq!(bulk, from_pixels);
665 }
666
667 #[test]
668 fn encode_into_matches_encode_vec() {
669 let elevations: Vec<f32> = (0..16).map(|i| i as f32 * 10.0).collect();
670 for fmt in HeightmapFormat::ALL {
671 let expected = encode(fmt, &elevations, 16, 1);
672 let mut buf = vec![0u8; elevations.len() * 3];
673 encode_into(fmt, &elevations, &mut buf);
674 assert_eq!(expected, buf, "encode_into mismatch for {fmt}");
675 }
676 }
677
678 #[test]
679 fn encode_to_writer_matches_encode_vec() {
680 let elevations: Vec<f32> = (0..2000).map(|i| i as f32 * 0.5).collect();
681 for fmt in HeightmapFormat::ALL {
682 let expected = encode(fmt, &elevations, elevations.len() as u32, 1);
683 let mut buf = Vec::new();
684 encode_to(fmt, &elevations, &mut buf).unwrap();
685 assert_eq!(expected, buf, "encode_to mismatch for {fmt}");
686 }
687 }
688
689 #[test]
690 fn heightmap_view_iter_matches_decode() {
691 let elevations: Vec<f32> = vec![0.0, 100.0, 200.0, -50.0];
692 for fmt in HeightmapFormat::ALL {
693 let rgb = encode(fmt, &elevations, 4, 1);
694 let view = HeightmapView::new(fmt, &rgb, 4, 1);
695 let decoded: Vec<f32> = view.iter().collect();
696 let direct = decode(fmt, &rgb, 4, 1);
697 assert_eq!(decoded, direct);
698 assert_eq!(view.get(0, 0), direct[0]);
699 assert_eq!(view.get(3, 0), direct[3]);
700 assert_eq!(view.to_vec(), direct);
701 }
702 }
703}