Skip to main content

decode_image

Function decode_image 

Source
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
Hide additional 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}