pub fn decode_image(bytes: &[u8]) -> Result<DecodedImage, ImageError>Expand description
Decode container bytes to raw RGB. The format is auto-detected from the bytes’ header.
Only formats whose cargo features are enabled will be recognised —
e.g. with just png on, this can decode PNG but not WebP. AVIF
decoding additionally requires image/avif-native (libdav1d) which
is not enabled by our avif feature.
Pixels with alpha are dropped (the image crate decodes to RGBA
internally and we keep only the RGB channels).
§Errors
Returns ImageError if the bytes are not in a recognised format
or the decoder fails.
Examples found in repository?
examples/reproject_terrain.rs (line 60)
23fn main() {
24 let mut a = env::args().skip(1);
25 let tz: u8 = a.next().expect("tms_z").parse().unwrap();
26 let tx: u32 = a.next().expect("tms_x").parse().unwrap();
27 let ty: u32 = a.next().expect("tms_y").parse().unwrap();
28 let src_zoom: u8 = a.next().expect("src_zoom").parse().unwrap();
29 let tile_size: u32 = a.next().expect("tile_size").parse().unwrap();
30 let png_dir = PathBuf::from(a.next().expect("png_dir"));
31 let real = a.next();
32
33 // Target geodetic TMS tile.
34 let (w, s, e, n) = geodetic_tms::tile_to_bounds(tz, tx, ty);
35 let bounds = TileBounds::new(w, s, e, n);
36 let grid_size = tile_size + 1; // 2^n + 1
37 println!("target geodetic TMS z{tz}/{tx}/{ty} bounds = [{w:.5}, {s:.5}, {e:.5}, {n:.5}]");
38
39 // Widen by ~one geodetic cell so the halo (buffer=1) stays in coverage.
40 let cell_lon = (e - w) / (grid_size - 1) as f64;
41 let cell_lat = (n - s) / (grid_size - 1) as f64;
42 let (x0, y0, ntx, nty) = MercatorDem::tiles_covering(
43 src_zoom,
44 w - cell_lon,
45 s - cell_lat,
46 e + cell_lon,
47 n + cell_lat,
48 );
49 println!(
50 "covering web-mercator z{src_zoom} tiles: x {x0}..{} y {y0}..{} ({} tiles, {tile_size}px)",
51 x0 + ntx - 1,
52 y0 + nty - 1,
53 ntx * nty
54 );
55
56 // Build the mercator DEM by reading each covering tile from disk.
57 let dem = MercatorDem::from_tiles(src_zoom, x0, y0, ntx, nty, tile_size, |z, x, y| {
58 let path = png_dir.join(format!("{z}_{x}_{y}.png"));
59 let bytes = std::fs::read(&path).unwrap_or_else(|_| panic!("missing tile {path:?}"));
60 let img = decode_image(&bytes).expect("decode png");
61 assert_eq!(img.width, tile_size);
62 assert_eq!(img.height, tile_size);
63 terrarium::decode(&img.rgb, img.width, img.height)
64 });
65
66 // Reproject onto the geodetic grid + a halo grid for seamless normals.
67 let grid = dem.geodetic_grid(&bounds, grid_size);
68 let buffered = dem.buffered_geodetic(&bounds, grid_size, 1);
69
70 let (gmin, gmax) = min_max(&grid);
71 println!("reprojected geodetic grid {grid_size}×{grid_size}: {gmin:.1} .. {gmax:.1} m");
72
73 let bytes = encode_terrain(
74 &grid,
75 grid_size,
76 &bounds,
77 &TerrainOptions {
78 max_error: 4.0,
79 compression_level: 6,
80 normals: NormalMode::BufferedGradient(buffered),
81 ..Default::default()
82 },
83 );
84 let mesh = DecodedMesh::decode(&bytes).expect("decode our output");
85 println!(
86 "ours: {} bytes (gzip), {} verts, {} tris, height {:.1}..{:.1} m, normals={}",
87 bytes.len(),
88 mesh.vertices.len(),
89 mesh.indices.len() / 3,
90 mesh.header.min_height,
91 mesh.header.max_height,
92 mesh.extensions.normals.is_some(),
93 );
94
95 if let Some(path) = real {
96 let raw = std::fs::read(&path).expect("read real .terrain");
97 let rm = DecodedMesh::decode(&raw).expect("decode real");
98 println!(
99 "real: {} bytes, {} verts, {} tris, height {:.1}..{:.1} m, normals={} ({})",
100 raw.len(),
101 rm.vertices.len(),
102 rm.indices.len() / 3,
103 rm.header.min_height,
104 rm.header.max_height,
105 rm.extensions.normals.is_some(),
106 path,
107 );
108 println!(
109 "note: height offset vs real is expected — real is EGM2008 geoid-blended, ours is raw Terrarium (ellipsoidal)."
110 );
111 }
112
113 println!("\nOK ✅ web-mercator → geodetic-TMS reprojection produced valid .terrain");
114}More examples
examples/real_terrain.rs (line 35)
23fn main() {
24 let mut args = env::args().skip(1);
25 let png_path = args
26 .next()
27 .expect("usage: real_terrain <png> <z> <x> <y> [terrain]");
28 let z: u8 = args.next().expect("z").parse().unwrap();
29 let x: u32 = args.next().expect("x").parse().unwrap();
30 let y: u32 = args.next().expect("y").parse().unwrap();
31 let real_terrain = args.next();
32
33 // 1. Decode the Terrarium PNG → RGB → elevation grid (row-major N→S).
34 let png_bytes = std::fs::read(&png_path).expect("read png");
35 let img = decode_image(&png_bytes).expect("decode png");
36 println!(
37 "source PNG: {}×{} ({} bytes)",
38 img.width,
39 img.height,
40 png_bytes.len()
41 );
42 let src = terrarium::decode(&img.rgb, img.width, img.height);
43
44 let (smin, smax) = min_max(&src);
45 println!("source DEM elevation range: {smin:.1} .. {smax:.1} m");
46
47 // 2. Pad N×N → (N+1)×(N+1) so it's 2^n+1 for martini (edge-replicate the
48 // extra south row / east column — a real pipeline would pull these
49 // from the neighbour tiles, but edge-replicate is fine for a smoke test).
50 let n = img.width;
51 assert_eq!(img.width, img.height, "expected a square tile");
52 let grid_size = n + 1;
53 let (w, h) = (n as usize, n as usize);
54 let gs = grid_size as usize;
55 let mut grid = vec![0f32; gs * gs];
56 for yy in 0..gs {
57 for xx in 0..gs {
58 let sx = xx.min(w - 1);
59 let sy = yy.min(h - 1);
60 grid[yy * gs + xx] = src[sy * w + sx];
61 }
62 }
63
64 // 3. Tile bounds (Web-Mercator XYZ). The +1 post extends the footprint by
65 // ~one cell south/east; we ignore that sub-cell offset for the header.
66 let (west, south, east, north) = web_mercator::tile_to_bounds(z, x, y);
67 let bounds = TileBounds::new(west, south, east, north);
68 println!(
69 "tile z{z}/{x}/{y} bounds = [{west:.5}, {south:.5}, {east:.5}, {north:.5}] grid {grid_size}×{grid_size}"
70 );
71
72 // 4. Encode at a few error thresholds, compressed (default level 6).
73 for &max_error in &[1.0_f64, 5.0, 20.0] {
74 let opts = TerrainOptions {
75 max_error,
76 compression_level: 6,
77 normals: NormalMode::FaceNormals,
78 ..Default::default()
79 };
80 let bytes = encode_terrain(&grid, grid_size, &bounds, &opts);
81
82 // also measure the uncompressed size
83 let raw = encode_terrain(
84 &grid,
85 grid_size,
86 &bounds,
87 &TerrainOptions {
88 compression_level: 0,
89 ..opts.clone()
90 },
91 );
92
93 let mesh = DecodedMesh::decode(&bytes).expect("decode our own output");
94 let vc = mesh.vertices.len();
95 let tc = mesh.indices.len() / 3;
96
97 // 5. Height round-trip error: martini vertices sit exactly on grid
98 // posts, so dequantising should match the source grid to within
99 // the quantisation step (height_span / 32767).
100 let span = (mesh.header.max_height - mesh.header.min_height) as f64;
101 let step = span / QUANTIZED_MAX as f64;
102 let mut max_err = 0.0f64;
103 let mut sum_err = 0.0f64;
104 for i in 0..vc {
105 let u = mesh.vertices.u[i] as f64 / QUANTIZED_MAX as f64;
106 let v = mesh.vertices.v[i] as f64 / QUANTIZED_MAX as f64;
107 let gx = (u * (grid_size - 1) as f64).round() as usize;
108 let gy = ((1.0 - v) * (grid_size - 1) as f64).round() as usize;
109 let src_h = grid[gy * gs + gx] as f64;
110 let deq_h = mesh.header.min_height as f64
111 + mesh.vertices.height[i] as f64 / QUANTIZED_MAX as f64 * span;
112 let e = (src_h - deq_h).abs();
113 max_err = max_err.max(e);
114 sum_err += e;
115 }
116 let mean_err = if vc > 0 { sum_err / vc as f64 } else { 0.0 };
117
118 println!(
119 " max_error={max_error:>4} m | verts {vc:>6} tris {tc:>6} | \
120height {:.1}..{:.1} m | gzip {:>6} B (raw {:>7} B, {:.1}×) | \
121roundtrip err mean {mean_err:.3} m max {max_err:.3} m (q-step {step:.3} m)",
122 mesh.header.min_height,
123 mesh.header.max_height,
124 bytes.len(),
125 raw.len(),
126 raw.len() as f64 / bytes.len() as f64,
127 );
128
129 assert!(
130 max_err <= step + 1e-3,
131 "roundtrip error {max_err} exceeds quantisation step {step}"
132 );
133 }
134
135 // 6. Bonus: decode a real production quantized-mesh tile, if supplied.
136 if let Some(path) = real_terrain {
137 let raw = std::fs::read(&path).expect("read .terrain");
138 let mesh = DecodedMesh::decode(&raw).expect("decode real quantized-mesh");
139 println!(
140 "\nreal {} ({} bytes): {} verts, {} tris, height {:.1}..{:.1} m, normals={}, water_mask={}, metadata={}",
141 path,
142 raw.len(),
143 mesh.vertices.len(),
144 mesh.indices.len() / 3,
145 mesh.header.min_height,
146 mesh.header.max_height,
147 mesh.extensions.normals.is_some(),
148 mesh.extensions.water_mask.is_some(),
149 mesh.extensions.metadata.is_some(),
150 );
151 }
152
153 println!("\nOK ✅ real-data pipeline produced valid, decodable .terrain");
154}