lerc/lib.rs
1//! Pure Rust implementation of the LERC (Limited Error Raster Compression) format.
2//!
3//! Supports encoding and decoding of raster images with configurable lossy or lossless
4//! compression. Compatible with ESRI's LERC2 format specification.
5
6#![cfg_attr(not(feature = "std"), no_std)]
7#![allow(
8 clippy::cast_possible_truncation,
9 clippy::cast_possible_wrap,
10 clippy::cast_sign_loss,
11 clippy::cast_precision_loss,
12 clippy::cast_lossless
13)]
14
15extern crate alloc;
16
17/// Error types for LERC encoding and decoding.
18pub mod error;
19/// Pixel data types and the `Sample` trait for type-safe encoding/decoding.
20pub mod types;
21
22/// Validity bitmask for tracking valid/invalid pixels.
23pub mod bitmask;
24pub(crate) mod bitstuffer;
25/// Fletcher-32 checksum used by the LERC2 format.
26#[allow(dead_code)]
27pub mod checksum;
28#[allow(dead_code)]
29pub(crate) mod header;
30#[allow(dead_code)]
31pub(crate) mod huffman;
32pub(crate) mod rle;
33
34pub(crate) mod decode;
35pub(crate) mod encode;
36#[allow(dead_code)]
37pub(crate) mod fpl;
38pub(crate) mod lerc1;
39#[allow(dead_code)]
40pub(crate) mod tiles;
41
42pub use error::{LercError, Result};
43pub use types::{DataType, Sample};
44
45use alloc::vec;
46use alloc::vec::Vec;
47
48use bitmask::BitMask;
49
50/// Controls the precision/error tolerance for LERC encoding.
51///
52/// `Lossless` preserves exact values. `Tolerance(x)` allows decoded values
53/// to differ from originals by at most +/-x.
54#[derive(Debug, Clone, Copy, PartialEq, Default)]
55pub enum Precision<T> {
56 /// Lossless compression. Exact round-trip for all pixel values.
57 #[default]
58 Lossless,
59 /// Lossy compression. Decoded values are within the given tolerance of originals.
60 Tolerance(T),
61}
62
63/// Metadata returned from a decode-into operation (no owned pixel data).
64#[derive(Debug, Clone)]
65pub struct DecodeResult {
66 /// Image width in pixels.
67 pub width: u32,
68 /// Image height in pixels.
69 pub height: u32,
70 /// Number of values per pixel (depth slices).
71 pub depth: u32,
72 /// Number of bands in the image.
73 pub bands: u32,
74 /// Pixel data type of the decoded blob.
75 pub data_type: DataType,
76 /// Per-band validity masks indicating which pixels are valid.
77 pub valid_masks: Vec<BitMask>,
78 /// NoData sentinel value, if the blob uses NoData encoding.
79 pub no_data_value: Option<f64>,
80}
81
82/// Header metadata extracted from a LERC blob without decoding pixel data.
83#[derive(Debug, Clone, Default)]
84pub struct LercInfo {
85 /// LERC format version number.
86 pub version: i32,
87 /// Image width in pixels.
88 pub width: u32,
89 /// Image height in pixels.
90 pub height: u32,
91 /// Number of values per pixel (depth slices).
92 pub depth: u32,
93 /// Number of bands in the image.
94 pub bands: u32,
95 /// Pixel data type stored in the blob.
96 pub data_type: DataType,
97 /// Number of valid (non-masked) pixels.
98 pub valid_pixels: u32,
99 /// Maximum error tolerance used during encoding.
100 pub tolerance: f64,
101 /// Minimum pixel value across all valid pixels.
102 pub min_value: f64,
103 /// Maximum pixel value across all valid pixels.
104 pub max_value: f64,
105 /// Total size of the LERC blob in bytes.
106 pub blob_size: u32,
107 /// The original NoData value, if the blob uses NoData encoding (v6+, depth > 1).
108 pub no_data_value: Option<f64>,
109}
110
111/// A decoded raster image with pixel data and validity masks.
112#[derive(Debug, Clone)]
113pub struct Image {
114 /// Image width in pixels.
115 pub width: u32,
116 /// Image height in pixels.
117 pub height: u32,
118 /// Number of values per pixel (depth slices).
119 pub depth: u32,
120 /// Number of bands in the image.
121 pub bands: u32,
122 /// Pixel data type.
123 pub data_type: DataType,
124 /// Per-band validity masks indicating which pixels are valid.
125 pub valid_masks: Vec<BitMask>,
126 /// Pixel sample data stored as a typed vector.
127 pub data: SampleData,
128 /// The original NoData value, if any. When set during encoding with depth > 1,
129 /// pixels matching this value in invalid depth slices are encoded with a sentinel.
130 /// On decode, the sentinel is remapped back to this value.
131 pub no_data_value: Option<f64>,
132}
133
134impl Default for Image {
135 fn default() -> Self {
136 Self {
137 width: 0,
138 height: 0,
139 depth: 1,
140 bands: 1,
141 data_type: DataType::Byte,
142 valid_masks: Vec::new(),
143 data: SampleData::U8(Vec::new()),
144 no_data_value: None,
145 }
146 }
147}
148
149/// Decoded pixel data and metadata returned by [`decode_slice`].
150///
151/// Holds the typed pixel buffer, the validity mask, and the image dimensions
152/// for a single-band, single-depth LERC blob.
153#[derive(Debug, Clone)]
154pub struct DecodedSlice<T> {
155 /// Decoded pixel data, one element per pixel, in row-major order.
156 pub pixels: Vec<T>,
157 /// Validity mask indicating which pixels are valid.
158 pub mask: BitMask,
159 /// Image width in pixels.
160 pub width: u32,
161 /// Image height in pixels.
162 pub height: u32,
163}
164
165/// Type-erased pixel data container, one variant per supported data type.
166#[derive(Debug, Clone)]
167pub enum SampleData {
168 /// Signed 8-bit integer pixel data.
169 I8(Vec<i8>),
170 /// Unsigned 8-bit integer pixel data.
171 U8(Vec<u8>),
172 /// Signed 16-bit integer pixel data.
173 I16(Vec<i16>),
174 /// Unsigned 16-bit integer pixel data.
175 U16(Vec<u16>),
176 /// Signed 32-bit integer pixel data.
177 I32(Vec<i32>),
178 /// Unsigned 32-bit integer pixel data.
179 U32(Vec<u32>),
180 /// 32-bit floating-point pixel data.
181 F32(Vec<f32>),
182 /// 64-bit floating-point pixel data.
183 F64(Vec<f64>),
184}
185
186/// Read header metadata from a LERC blob without decoding pixel data.
187pub fn decode_info(data: &[u8]) -> Result<LercInfo> {
188 decode::decode_info(data)
189}
190
191/// Decode a LERC blob, returning the image with pixel data and validity masks.
192pub fn decode(data: &[u8]) -> Result<Image> {
193 decode::decode(data)
194}
195
196/// Encode an image into a LERC blob with the given precision.
197///
198/// This entry point clones the image's pixel buffer when called via the
199/// owning `Image` path. To avoid that clone, see [`encode_borrowed`], which
200/// takes a borrowed `&[T]` and image-shape parameters directly.
201pub fn encode(image: &Image, precision: Precision<f64>) -> Result<Vec<u8>> {
202 let max_z_error = match precision {
203 Precision::Lossless => {
204 if image.data_type.is_integer() {
205 0.5
206 } else {
207 0.0
208 }
209 }
210 Precision::Tolerance(val) => val,
211 };
212 encode::encode(image, max_z_error)
213}
214
215/// Zero-copy multi-band encode entry point.
216///
217/// Encodes a raster image directly from a borrowed pixel slice, avoiding the
218/// buffer clone forced by the [`Image`]-based [`encode`] API. The pixel type
219/// `T` determines the LERC data type automatically via [`Sample`]; tolerances
220/// are expressed in `f64` regardless of `T` to keep the API boundary uniform.
221///
222/// # Data layout
223///
224/// `data` is band-major: outermost is `band`, then row-major within each band,
225/// then `depth` slices interleaved per pixel. Concretely, the value for band
226/// `b`, row `r`, column `c`, depth `d` is at index
227/// `b * (width * height * depth) + (r * width + c) * depth + d`.
228///
229/// `data.len()` must equal `width * height * depth * bands`.
230///
231/// # Validity masks
232///
233/// `masks.len()` must equal `bands`, with one [`BitMask`] per band (each
234/// having `num_pixels() == width * height`). For fully valid bands, pass
235/// [`BitMask::all_valid(width as usize * height as usize)`](BitMask::all_valid).
236///
237/// # Errors
238///
239/// Returns [`LercError::InvalidData`] if the data length, the number of
240/// masks, or any mask's pixel count does not match the declared shape.
241///
242/// # Examples
243///
244/// ```
245/// use lerc::{Precision, encode_borrowed};
246/// use lerc::bitmask::BitMask;
247///
248/// let width = 4u32;
249/// let height = 3u32;
250/// let pixels: Vec<f32> = (0..12).map(|i| i as f32).collect();
251/// let masks = [BitMask::all_valid((width * height) as usize)];
252/// let blob = encode_borrowed::<f32>(
253/// width, height, 1, 1,
254/// &pixels,
255/// &masks,
256/// None,
257/// Precision::Lossless,
258/// ).expect("encode failed");
259/// assert!(!blob.is_empty());
260/// ```
261#[allow(clippy::too_many_arguments)]
262pub fn encode_borrowed<T: Sample>(
263 width: u32,
264 height: u32,
265 depth: u32,
266 bands: u32,
267 data: &[T],
268 masks: &[BitMask],
269 no_data_value: Option<f64>,
270 precision: Precision<f64>,
271) -> Result<Vec<u8>> {
272 let pixels_per_band = (width as usize) * (height as usize);
273 let expected = pixels_per_band * (depth as usize) * (bands as usize);
274 if data.len() != expected {
275 return Err(LercError::InvalidData(alloc::format!(
276 "data length {} does not match width*height*depth*bands {expected}",
277 data.len()
278 )));
279 }
280 if masks.len() != bands as usize {
281 return Err(LercError::InvalidData(alloc::format!(
282 "masks length {} does not match bands {}",
283 masks.len(),
284 bands
285 )));
286 }
287 for (i, mask) in masks.iter().enumerate() {
288 if mask.num_pixels() != pixels_per_band {
289 return Err(LercError::InvalidData(alloc::format!(
290 "mask[{i}] pixel count {} does not match width*height {pixels_per_band}",
291 mask.num_pixels()
292 )));
293 }
294 }
295 let max_z_error: f64 = match precision {
296 Precision::Lossless => {
297 if T::is_integer() {
298 0.5
299 } else {
300 0.0
301 }
302 }
303 Precision::Tolerance(val) => val,
304 };
305 encode::encode_borrowed_typed(
306 width,
307 height,
308 depth,
309 bands,
310 data,
311 masks,
312 no_data_value,
313 max_z_error,
314 )
315}
316
317// ---------------------------------------------------------------------------
318// Typed convenience encode/decode helpers
319// ---------------------------------------------------------------------------
320
321/// Encode a single-band image with all pixels valid.
322///
323/// The pixel type `T` determines the LERC data type automatically via `Sample`.
324/// Returns an error if `data.len() != width * height`.
325///
326/// This helper internally clones `data` into an owned `Image`. For zero-copy
327/// encoding, including multi-band layouts, see [`encode_borrowed`].
328pub fn encode_slice<T: Sample>(
329 width: u32,
330 height: u32,
331 data: &[T],
332 precision: Precision<T>,
333) -> Result<Vec<u8>> {
334 let expected = (width as usize) * (height as usize);
335 if data.len() != expected {
336 return Err(LercError::InvalidData(alloc::format!(
337 "data length {} does not match width*height {expected}",
338 data.len()
339 )));
340 }
341 let max_z_error: f64 = match precision {
342 Precision::Lossless => {
343 if T::is_integer() {
344 0.5
345 } else {
346 0.0
347 }
348 }
349 Precision::Tolerance(val) => val.to_f64(),
350 };
351 let image = Image {
352 width,
353 height,
354 depth: 1,
355 bands: 1,
356 data_type: T::DATA_TYPE,
357 valid_masks: vec![BitMask::all_valid(expected)],
358 data: T::into_lerc_data(data.to_vec()),
359 no_data_value: None,
360 };
361 encode::encode(&image, max_z_error)
362}
363
364/// Encode a single-band image with a validity mask.
365///
366/// The pixel type `T` determines the LERC data type automatically via `Sample`.
367/// Returns an error if `data.len() != width * height` or if the mask size does not match.
368///
369/// This helper internally clones `data` into an owned `Image`. For zero-copy
370/// encoding, including multi-band layouts, see [`encode_borrowed`].
371pub fn encode_slice_masked<T: Sample>(
372 width: u32,
373 height: u32,
374 data: &[T],
375 mask: &BitMask,
376 precision: Precision<T>,
377) -> Result<Vec<u8>> {
378 let expected = (width as usize) * (height as usize);
379 if data.len() != expected {
380 return Err(LercError::InvalidData(alloc::format!(
381 "data length {} does not match width*height {expected}",
382 data.len()
383 )));
384 }
385 if mask.num_pixels() != expected {
386 return Err(LercError::InvalidData(alloc::format!(
387 "mask pixel count {} does not match width*height {expected}",
388 mask.num_pixels()
389 )));
390 }
391 let max_z_error: f64 = match precision {
392 Precision::Lossless => {
393 if T::is_integer() {
394 0.5
395 } else {
396 0.0
397 }
398 }
399 Precision::Tolerance(val) => val.to_f64(),
400 };
401 let image = Image {
402 width,
403 height,
404 depth: 1,
405 bands: 1,
406 data_type: T::DATA_TYPE,
407 valid_masks: vec![mask.clone()],
408 data: T::into_lerc_data(data.to_vec()),
409 no_data_value: None,
410 };
411 encode::encode(&image, max_z_error)
412}
413
414/// Decode a single-band, single-depth LERC blob into a [`DecodedSlice<T>`]
415/// containing the typed pixel data, validity mask, and image dimensions.
416///
417/// The pixel type `T` must match the blob's data type. Returns an error on type
418/// mismatch or if the blob contains multiple bands or depths (use [`decode`] for
419/// multi-band/multi-depth blobs to get full shape and per-band masks).
420pub fn decode_slice<T: Sample>(blob: &[u8]) -> Result<DecodedSlice<T>> {
421 let image = decode::decode(blob)?;
422 if image.bands > 1 {
423 return Err(LercError::InvalidData(alloc::format!(
424 "decode_slice requires single-band data, got {} bands (use decode() instead)",
425 image.bands
426 )));
427 }
428 if image.depth > 1 {
429 return Err(LercError::InvalidData(alloc::format!(
430 "decode_slice requires single-depth data, got depth={} (use decode() instead)",
431 image.depth
432 )));
433 }
434 let w = image.width;
435 let h = image.height;
436 let pixels = T::try_from_lerc_data(image.data).map_err(|_| {
437 LercError::InvalidData(alloc::format!(
438 "expected {:?} data but blob contains {:?}",
439 T::DATA_TYPE,
440 image.data_type
441 ))
442 })?;
443 let mask = image
444 .valid_masks
445 .into_iter()
446 .next()
447 .unwrap_or_else(|| BitMask::all_valid((w as usize) * (h as usize)));
448 Ok(DecodedSlice {
449 pixels,
450 mask,
451 width: w,
452 height: h,
453 })
454}
455
456// ---------------------------------------------------------------------------
457// Typed accessor methods on Image
458// ---------------------------------------------------------------------------
459
460impl Image {
461 /// Try to borrow the pixel data as `&[T]`.
462 ///
463 /// Returns `None` if the image's data type does not match `T`.
464 pub fn as_typed<T: Sample>(&self) -> Option<&[T]> {
465 T::try_ref_lerc_data(&self.data)
466 }
467
468 /// Return the validity mask for the first band, or `None` if no masks are present.
469 pub fn mask(&self) -> Option<&BitMask> {
470 self.valid_masks.first()
471 }
472
473 /// Get the pixel value at `(row, col)` for single-band, single-depth images.
474 ///
475 /// Returns `None` if the data type does not match `T`, if `bands > 1` or
476 /// `depth > 1`, or if the coordinates are out of bounds.
477 pub fn pixel<T: Sample>(&self, row: u32, col: u32) -> Option<T> {
478 if self.bands != 1 || self.depth != 1 {
479 return None;
480 }
481 if row >= self.height || col >= self.width {
482 return None;
483 }
484 let data = self.as_typed::<T>()?;
485 let idx = row as usize * self.width as usize + col as usize;
486 Some(data[idx])
487 }
488
489 /// Iterate over valid pixels as `(row, col, value)` tuples.
490 ///
491 /// Only works for single-band, single-depth images. Returns `None` if the data
492 /// type does not match `T` or if `bands > 1` or `depth > 1`.
493 /// The iterator respects the validity mask, skipping invalid pixels.
494 pub fn valid_pixels<'a, T: Sample + 'a>(
495 &'a self,
496 ) -> Option<impl Iterator<Item = (u32, u32, T)> + 'a> {
497 if self.bands != 1 || self.depth != 1 {
498 return None;
499 }
500 let data = self.as_typed::<T>()?;
501 let width = self.width;
502 let mask = self.valid_masks.first();
503 Some(data.iter().enumerate().filter_map(move |(idx, &val)| {
504 let is_valid = match mask {
505 Some(m) => m.is_valid(idx),
506 None => true,
507 };
508 if is_valid {
509 let row = (idx / width as usize) as u32;
510 let col = (idx % width as usize) as u32;
511 Some((row, col, val))
512 } else {
513 None
514 }
515 }))
516 }
517
518 /// Get dimensions as `(width, height)`.
519 pub fn dimensions(&self) -> (u32, u32) {
520 (self.width, self.height)
521 }
522
523 /// Total number of pixels (`width * height`).
524 pub fn num_pixels(&self) -> usize {
525 self.width as usize * self.height as usize
526 }
527
528 /// Check if all pixels in the first band are valid.
529 ///
530 /// Returns `true` if there is no mask (all pixels are implicitly valid),
531 /// if the first band's mask is [`BitMask::AllValid`] (O(1)), or if an
532 /// explicit mask happens to have every bit set (O(n) popcount fallback).
533 pub fn all_valid(&self) -> bool {
534 match self.valid_masks.first() {
535 Some(m) => m.is_all_valid(),
536 None => true,
537 }
538 }
539
540 /// Create a single-band, all-valid `Image` from a typed pixel vector
541 /// and dimensions.
542 ///
543 /// Returns an error if `data.len() != width * height`.
544 pub fn from_pixels<T: Sample>(width: u32, height: u32, data: Vec<T>) -> Result<Self> {
545 let expected = width as usize * height as usize;
546 if data.len() != expected {
547 return Err(LercError::InvalidData(alloc::format!(
548 "data length {} does not match width*height {expected}",
549 data.len()
550 )));
551 }
552 Ok(Self {
553 width,
554 height,
555 depth: 1,
556 bands: 1,
557 data_type: T::DATA_TYPE,
558 valid_masks: vec![BitMask::all_valid(expected)],
559 data: T::into_lerc_data(data),
560 no_data_value: None,
561 })
562 }
563}
564
565// ---------------------------------------------------------------------------
566// Zero-copy decode-into API
567// ---------------------------------------------------------------------------
568
569/// Decode a LERC blob into a pre-allocated buffer, returning metadata.
570///
571/// The type `T` must match the blob's data type (e.g., `f32` for `DataType::Float`).
572/// The buffer must have at least `width * height * n_depth * n_bands` elements.
573///
574/// Returns `LercError::TypeMismatch` if `T` does not match the blob's data type.
575/// Returns `LercError::OutputBufferTooSmall` if the buffer is too small.
576pub fn decode_into<T: Sample>(data: &[u8], output: &mut [T]) -> Result<DecodeResult> {
577 decode::decode_into(data, output)
578}
579
580/// Decode a LERC blob into a pre-allocated buffer, filling invalid pixels with
581/// a caller-supplied sentinel value.
582///
583/// This is a convenience layer over [`decode_into`]: it performs the same decode
584/// (so the same constraints on type and buffer size apply), then walks each
585/// band's validity mask and writes `nodata` into every position that the mask
586/// reports as invalid. For pixels with `depth > 1`, all `depth` slices of an
587/// invalid pixel receive the sentinel.
588///
589/// The returned [`DecodeResult`] still carries `valid_masks`, so callers that
590/// want both the sentinel-filled buffer and the masks (e.g. to distinguish
591/// genuine sentinel-valued pixels from mask-driven fills) have access to both.
592///
593/// Bands whose mask is [`BitMask::AllValid`] are skipped entirely — no writes
594/// occur for those bands.
595pub fn decode_into_with_nodata<T: Sample>(
596 data: &[u8],
597 output: &mut [T],
598 nodata: T,
599) -> Result<DecodeResult> {
600 let result = decode::decode_into(data, output)?;
601
602 let n_cols = result.width as usize;
603 let n_rows = result.height as usize;
604 let n_depth = result.depth as usize;
605 let band_size = n_rows * n_cols * n_depth;
606
607 for (band_idx, mask) in result.valid_masks.iter().enumerate() {
608 if mask.is_all_valid() {
609 continue;
610 }
611 let band_offset = band_idx * band_size;
612 for i in 0..n_rows {
613 let row_start = band_offset + i * n_cols * n_depth;
614 for j in 0..n_cols {
615 let k = i * n_cols + j;
616 if !mask.is_valid(k) {
617 let base = row_start + j * n_depth;
618 for m in 0..n_depth {
619 output[base + m] = nodata;
620 }
621 }
622 }
623 }
624 }
625
626 Ok(result)
627}