1mod sys;
42
43use std::ffi::c_int;
44
45use thiserror::Error;
46
47#[derive(Debug, Error)]
49pub enum LepccError {
50 #[error("LEPCC returned status code {0}")]
51 Status(u32),
52 #[error("input buffer too large for c_int")]
53 BufferTooLarge,
54}
55
56pub type Result<T> = std::result::Result<T, LepccError>;
57
58fn buf_len(data: &[u8]) -> Result<c_int> {
59 c_int::try_from(data.len()).map_err(|_| LepccError::BufferTooLarge)
60}
61
62fn check(status: u32) -> Result<()> {
63 if status == 0 {
64 Ok(())
65 } else {
66 Err(LepccError::Status(status))
67 }
68}
69
70pub struct Context {
76 hdl: sys::lepcc_ContextHdl,
77}
78
79impl Context {
80 pub fn new() -> Self {
86 let hdl = unsafe { sys::lepcc_createContext() };
87 assert!(!hdl.is_null(), "lepcc_createContext returned null");
88 Self { hdl }
89 }
90
91 pub fn blob_info(&self, data: &[u8]) -> Result<(u32, u32)> {
95 let mut blob_type = 0u32;
96 let mut blob_size = 0u32;
97 let status = unsafe {
98 sys::lepcc_getBlobInfo(
99 self.hdl,
100 data.as_ptr(),
101 buf_len(data)?,
102 &mut blob_type,
103 &mut blob_size,
104 )
105 };
106 check(status)?;
107 Ok((blob_type, blob_size))
108 }
109
110 pub fn decode_xyz(&self, data: &[u8]) -> Result<Vec<[f64; 3]>> {
112 let len = buf_len(data)?;
113
114 let mut n_pts = 0u32;
116 let status = unsafe { sys::lepcc_getPointCount(self.hdl, data.as_ptr(), len, &mut n_pts) };
117 check(status)?;
118
119 let mut out = vec![[0.0f64; 3]; n_pts as usize];
120 let mut ptr = data.as_ptr();
121 let mut n_out = n_pts;
122 let status = unsafe {
123 sys::lepcc_decodeXYZ(
124 self.hdl,
125 &mut ptr,
126 len,
127 &mut n_out,
128 out.as_mut_ptr() as *mut f64,
129 )
130 };
131 check(status)?;
132 Ok(out)
133 }
134
135 pub fn decode_rgb(&self, data: &[u8]) -> Result<Vec<[u8; 3]>> {
137 let len = buf_len(data)?;
138
139 let mut n_rgb = 0u32;
140 let status = unsafe { sys::lepcc_getRGBCount(self.hdl, data.as_ptr(), len, &mut n_rgb) };
141 check(status)?;
142
143 let mut out = vec![[0u8; 3]; n_rgb as usize];
144 let mut ptr = data.as_ptr();
145 let mut n_out = n_rgb;
146 let status = unsafe {
147 sys::lepcc_decodeRGB(
148 self.hdl,
149 &mut ptr,
150 len,
151 &mut n_out,
152 out.as_mut_ptr() as *mut u8,
153 )
154 };
155 check(status)?;
156 Ok(out)
157 }
158
159 pub fn decode_intensity(&self, data: &[u8]) -> Result<Vec<u16>> {
161 let len = buf_len(data)?;
162
163 let mut n_vals = 0u32;
164 let status =
165 unsafe { sys::lepcc_getIntensityCount(self.hdl, data.as_ptr(), len, &mut n_vals) };
166 check(status)?;
167
168 let mut out = vec![0u16; n_vals as usize];
169 let mut ptr = data.as_ptr();
170 let mut n_out = n_vals;
171 let status = unsafe {
172 sys::lepcc_decodeIntensity(self.hdl, &mut ptr, len, &mut n_out, out.as_mut_ptr())
173 };
174 check(status)?;
175 Ok(out)
176 }
177
178 pub fn decode_flag_bytes(&self, data: &[u8]) -> Result<Vec<u8>> {
180 let len = buf_len(data)?;
181
182 let mut n_vals = 0u32;
183 let status =
184 unsafe { sys::lepcc_getFlagByteCount(self.hdl, data.as_ptr(), len, &mut n_vals) };
185 check(status)?;
186
187 let mut out = vec![0u8; n_vals as usize];
188 let mut ptr = data.as_ptr();
189 let mut n_out = n_vals;
190 let status = unsafe {
191 sys::lepcc_decodeFlagBytes(self.hdl, &mut ptr, len, &mut n_out, out.as_mut_ptr())
192 };
193 check(status)?;
194 Ok(out)
195 }
196
197 pub fn encode_xyz(&self, points: &[[f64; 3]], max_err: f64) -> Result<Vec<u8>> {
201 let n = points.len() as u32;
202 let raw = points.as_ptr() as *const f64;
203
204 let mut n_bytes = 0u32;
205 let status = unsafe {
206 sys::lepcc_computeCompressedSizeXYZ(
207 self.hdl,
208 n,
209 raw,
210 max_err,
211 max_err,
212 max_err,
213 &mut n_bytes,
214 std::ptr::null_mut(), )
216 };
217 check(status)?;
218
219 let mut buf = vec![0u8; n_bytes as usize];
220 let mut ptr = buf.as_mut_ptr();
221 let status = unsafe { sys::lepcc_encodeXYZ(self.hdl, &mut ptr, n_bytes as c_int) };
222 check(status)?;
223 Ok(buf)
224 }
225
226 pub fn encode_rgb(&self, colours: &[[u8; 3]]) -> Result<Vec<u8>> {
228 let n = colours.len() as u32;
229 let raw = colours.as_ptr() as *const u8;
230
231 let mut n_bytes = 0u32;
232 let status = unsafe { sys::lepcc_computeCompressedSizeRGB(self.hdl, n, raw, &mut n_bytes) };
233 check(status)?;
234
235 let mut buf = vec![0u8; n_bytes as usize];
236 let mut ptr = buf.as_mut_ptr();
237 let status = unsafe { sys::lepcc_encodeRGB(self.hdl, &mut ptr, n_bytes as c_int) };
238 check(status)?;
239 Ok(buf)
240 }
241
242 pub fn encode_intensity(&self, values: &[u16]) -> Result<Vec<u8>> {
244 let n = values.len() as u32;
245
246 let mut n_bytes = 0u32;
247 let status = unsafe {
248 sys::lepcc_computeCompressedSizeIntensity(self.hdl, n, values.as_ptr(), &mut n_bytes)
249 };
250 check(status)?;
251
252 let mut buf = vec![0u8; n_bytes as usize];
253 let mut ptr = buf.as_mut_ptr();
254 let status = unsafe {
255 sys::lepcc_encodeIntensity(self.hdl, &mut ptr, n_bytes as c_int, values.as_ptr(), n)
256 };
257 check(status)?;
258 Ok(buf)
259 }
260
261 pub fn encode_flag_bytes(&self, flags: &[u8]) -> Result<Vec<u8>> {
263 let n = flags.len() as u32;
264
265 let mut n_bytes = 0u32;
266 let status = unsafe {
267 sys::lepcc_computeCompressedSizeFlagBytes(self.hdl, n, flags.as_ptr(), &mut n_bytes)
268 };
269 check(status)?;
270
271 let mut buf = vec![0u8; n_bytes as usize];
272 let mut ptr = buf.as_mut_ptr();
273 let status = unsafe {
274 sys::lepcc_encodeFlagBytes(self.hdl, &mut ptr, n_bytes as c_int, flags.as_ptr(), n)
275 };
276 check(status)?;
277 Ok(buf)
278 }
279}
280
281impl Default for Context {
282 fn default() -> Self {
283 Self::new()
284 }
285}
286
287impl Drop for Context {
288 fn drop(&mut self) {
289 unsafe { sys::lepcc_deleteContext(&mut self.hdl) };
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 fn sample_points() -> Vec<[f64; 3]> {
298 vec![
299 [0.0, 0.0, 0.0],
300 [1.0, 2.0, 3.0],
301 [100.0, 200.0, 50.5],
302 [-10.5, 300.0, 0.001],
303 ]
304 }
305
306 #[test]
307 fn roundtrip_xyz() {
308 let ctx = Context::new();
309 let pts = sample_points();
310 let blob = ctx.encode_xyz(&pts, 0.001).expect("encode_xyz failed");
311 assert!(!blob.is_empty());
312
313 let ctx2 = Context::new();
314 let decoded = ctx2.decode_xyz(&blob).expect("decode_xyz failed");
315 assert_eq!(decoded.len(), pts.len());
316 for (orig, dec) in pts.iter().zip(decoded.iter()) {
317 assert!(
318 (orig[0] - dec[0]).abs() <= 0.002,
319 "x mismatch: {} vs {}",
320 orig[0],
321 dec[0]
322 );
323 assert!(
324 (orig[1] - dec[1]).abs() <= 0.002,
325 "y mismatch: {} vs {}",
326 orig[1],
327 dec[1]
328 );
329 assert!(
330 (orig[2] - dec[2]).abs() <= 0.002,
331 "z mismatch: {} vs {}",
332 orig[2],
333 dec[2]
334 );
335 }
336 }
337
338 #[test]
339 fn roundtrip_rgb() {
340 let ctx = Context::new();
341 let colours: Vec<[u8; 3]> = vec![[255, 0, 0], [0, 255, 0], [0, 0, 255], [128, 128, 128]];
342 let blob = ctx.encode_rgb(&colours).expect("encode_rgb failed");
343 assert!(!blob.is_empty());
344
345 let ctx2 = Context::new();
346 let decoded = ctx2.decode_rgb(&blob).expect("decode_rgb failed");
347 assert_eq!(decoded, colours);
348 }
349
350 #[test]
351 fn roundtrip_intensity() {
352 let ctx = Context::new();
353 let values: Vec<u16> = vec![0, 1000, 32768, 65535, 512];
354 let blob = ctx
355 .encode_intensity(&values)
356 .expect("encode_intensity failed");
357 assert!(!blob.is_empty());
358
359 let ctx2 = Context::new();
360 let decoded = ctx2
361 .decode_intensity(&blob)
362 .expect("decode_intensity failed");
363 assert_eq!(decoded, values);
364 }
365
366 #[test]
367 fn roundtrip_flag_bytes() {
368 let ctx = Context::new();
369 let flags: Vec<u8> = vec![0, 1, 2, 64, 128, 255];
370 let blob = ctx
371 .encode_flag_bytes(&flags)
372 .expect("encode_flag_bytes failed");
373 assert!(!blob.is_empty());
374
375 let ctx2 = Context::new();
376 let decoded = ctx2
377 .decode_flag_bytes(&blob)
378 .expect("decode_flag_bytes failed");
379 assert_eq!(decoded, flags);
380 }
381
382 #[test]
383 fn blob_info_xyz() {
384 let ctx = Context::new();
385 let pts = sample_points();
386 let blob = ctx.encode_xyz(&pts, 0.001).unwrap();
387
388 let ctx2 = Context::new();
389 let (blob_type, blob_size) = ctx2.blob_info(&blob).unwrap();
390 assert!(blob_size > 0);
391 assert_eq!(blob_type, 0, "unexpected blob type {blob_type}");
393 }
394
395 const SLPK: &str = concat!(
397 env!("CARGO_MANIFEST_DIR"),
398 "/extern/lepcc/testData/SMALL_AUTZEN_LAS_All.slpk"
399 );
400 const GT_BIN: &str = concat!(
401 env!("CARGO_MANIFEST_DIR"),
402 "/extern/lepcc/testData/SMALL_AUTZEN_LAS_All.bin"
403 );
404
405 fn read_file(path: &str) -> Vec<u8> {
406 use std::io::Read;
407 let mut f =
408 std::fs::File::open(path).unwrap_or_else(|_| panic!("test data not found: {path}"));
409 let mut buf = Vec::new();
410 f.read_to_end(&mut buf).unwrap();
411 buf
412 }
413
414 fn read_gt_block<'a>(cursor: &mut &'a [u8], stride: usize) -> (u32, &'a [u8]) {
417 let n = u32::from_le_bytes(cursor[..4].try_into().unwrap());
418 *cursor = &cursor[4..];
419 let bytes = n as usize * stride;
420 let block = &cursor[..bytes];
421 *cursor = &cursor[bytes..];
422 (n, block)
423 }
424
425 fn bytes_as_f64_le(bytes: &[u8]) -> Vec<f64> {
426 bytes
427 .chunks_exact(8)
428 .map(|b| f64::from_le_bytes(b.try_into().unwrap()))
429 .collect()
430 }
431
432 fn bytes_as_u16_le(bytes: &[u8]) -> Vec<u16> {
433 bytes
434 .chunks_exact(2)
435 .map(|b| u16::from_le_bytes(b.try_into().unwrap()))
436 .collect()
437 }
438
439 #[test]
448 #[ignore = "requires extern/lepcc/testData/ from the git submodule"]
449 fn decode_slpk_matches_ground_truth() {
450 let slpk = read_file(SLPK);
451 let gt = read_file(GT_BIN);
452 let mut gt_cursor: &[u8] = >
453
454 const MAGIC_LEN: usize = 10;
456 const MAGICS: [(&[u8; MAGIC_LEN], &str); 3] = [
457 (b"LEPCC ", "xyz"),
458 (b"ClusterRGB", "rgb"),
459 (b"Intensity ", "intensity"),
460 ];
461
462 let info_size = unsafe { sys::lepcc_getBlobInfoSize() } as usize;
463
464 let mut pos = 0usize;
465 let mut blob_count = 0u32;
466
467 while pos + MAGIC_LEN <= slpk.len() {
468 let mut matched = false;
469 for &(magic, kind) in &MAGICS {
470 if &slpk[pos..pos + MAGIC_LEN] != magic.as_ref() {
471 continue;
472 }
473
474 let remaining = &slpk[pos..];
475 let ctx = Context::new();
476
477 let blob_size = if remaining.len() >= info_size {
479 ctx.blob_info(remaining)
480 .map(|(_, sz)| sz as usize)
481 .unwrap_or(MAGIC_LEN)
482 } else {
483 MAGIC_LEN
484 };
485
486 match kind {
487 "xyz" => {
488 let pts = ctx.decode_xyz(remaining).expect("decode_xyz failed");
489 let (n_gt, gt_bytes) = read_gt_block(&mut gt_cursor, 24); let gt_flat = bytes_as_f64_le(gt_bytes);
491 assert_eq!(pts.len(), n_gt as usize, "XYZ point count mismatch");
492 let max_err = pts
493 .iter()
494 .zip(gt_flat.chunks_exact(3))
495 .flat_map(|(p, g)| {
496 [
497 (p[0] - g[0]).abs(),
498 (p[1] - g[1]).abs(),
499 (p[2] - g[2]).abs(),
500 ]
501 })
502 .fold(0.0_f64, f64::max);
503 assert!(
505 max_err < 1e-4,
506 "XYZ max error {max_err:.2e} exceeds 1e-4 tolerance"
507 );
508 }
509 "rgb" => {
510 let rgb = ctx.decode_rgb(remaining).expect("decode_rgb failed");
511 let (n_gt, gt_bytes) = read_gt_block(&mut gt_cursor, 3); assert_eq!(rgb.len(), n_gt as usize, "RGB count mismatch");
513 let rgb_flat: Vec<u8> = rgb.iter().flat_map(|p| *p).collect();
515 assert_eq!(rgb_flat.as_slice(), gt_bytes, "RGB values mismatch");
516 }
517 "intensity" => {
518 let intensity = ctx
519 .decode_intensity(remaining)
520 .expect("decode_intensity failed");
521 let (n_gt, gt_bytes) = read_gt_block(&mut gt_cursor, 2); let gt_vals = bytes_as_u16_le(gt_bytes);
523 assert_eq!(intensity.len(), n_gt as usize, "Intensity count mismatch");
524 assert_eq!(intensity, gt_vals, "Intensity values mismatch");
525 }
526 _ => unreachable!(),
527 }
528
529 blob_count += 1;
530 pos += blob_size.max(MAGIC_LEN);
531 matched = true;
532 break;
533 }
534 if !matched {
535 pos += 1;
536 }
537 }
538
539 assert!(blob_count > 0, "no LEPCC blobs found in the SLPK");
540 assert!(
541 gt_cursor.is_empty(),
542 "{} bytes of ground truth not consumed — blob count mismatch",
543 gt_cursor.len()
544 );
545 }
546}