1use crate::config::{ImageFormat, StyleConfig};
7use bytes::Bytes;
8use oxigdal_algorithms::resampling::{Resampler, ResamplingMethod};
9use oxigdal_core::buffer::RasterBuffer;
10#[cfg(test)]
11use oxigdal_core::types::RasterDataType;
12use oxigdal_core::types::{BoundingBox, GeoTransform};
13use thiserror::Error;
14
15#[derive(Debug, Error)]
17pub enum RenderError {
18 #[error("Invalid parameter: {0}")]
20 InvalidParameter(String),
21
22 #[error("Failed to read data: {0}")]
24 ReadError(String),
25
26 #[error("Resampling failed: {0}")]
28 ResamplingError(String),
29
30 #[error("Image encoding failed: {0}")]
32 EncodingError(String),
33
34 #[error("Unsupported: {0}")]
36 Unsupported(String),
37}
38
39impl From<oxigdal_core::OxiGdalError> for RenderError {
40 fn from(e: oxigdal_core::OxiGdalError) -> Self {
41 RenderError::ReadError(e.to_string())
42 }
43}
44
45impl From<oxigdal_algorithms::AlgorithmError> for RenderError {
46 fn from(e: oxigdal_algorithms::AlgorithmError) -> Self {
47 RenderError::ResamplingError(e.to_string())
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq)]
53pub enum Colormap {
54 Grayscale,
56 Viridis,
58 Terrain,
60 Jet,
62 Hot,
64 Cool,
66 Spectral,
68 Ndvi,
70}
71
72impl Colormap {
73 pub fn from_name(name: &str) -> Option<Self> {
75 match name.to_lowercase().as_str() {
76 "grayscale" | "gray" | "grey" => Some(Self::Grayscale),
77 "viridis" => Some(Self::Viridis),
78 "terrain" => Some(Self::Terrain),
79 "jet" | "rainbow" => Some(Self::Jet),
80 "hot" => Some(Self::Hot),
81 "cool" => Some(Self::Cool),
82 "spectral" => Some(Self::Spectral),
83 "ndvi" | "vegetation" => Some(Self::Ndvi),
84 _ => None,
85 }
86 }
87
88 #[must_use]
91 pub fn apply(&self, value: f64) -> (u8, u8, u8) {
92 let v = value.clamp(0.0, 1.0);
93
94 match self {
95 Self::Grayscale => {
96 let gray = (v * 255.0) as u8;
97 (gray, gray, gray)
98 }
99 Self::Viridis => Self::viridis(v),
100 Self::Terrain => Self::terrain(v),
101 Self::Jet => Self::jet(v),
102 Self::Hot => Self::hot(v),
103 Self::Cool => Self::cool(v),
104 Self::Spectral => Self::spectral(v),
105 Self::Ndvi => Self::ndvi(v),
106 }
107 }
108
109 fn viridis(t: f64) -> (u8, u8, u8) {
110 let r = ((0.267004 + t * (0.993248 - 0.267004)) * 255.0) as u8;
112 let g = if t < 0.5 {
113 (t * 2.0 * 0.7).clamp(0.0, 1.0) * 255.0
114 } else {
115 (0.7 + (t - 0.5) * 2.0 * 0.3).clamp(0.0, 1.0) * 255.0
116 } as u8;
117 let b = if t < 0.3 {
118 ((0.33 + t / 0.3 * 0.37) * 255.0) as u8
119 } else if t < 0.7 {
120 ((0.7 - (t - 0.3) / 0.4 * 0.5) * 255.0) as u8
121 } else {
122 ((0.2 * (1.0 - (t - 0.7) / 0.3)) * 255.0) as u8
123 };
124 (r, g, b)
125 }
126
127 fn terrain(t: f64) -> (u8, u8, u8) {
128 if t < 0.1 {
130 (0, 0, (t / 0.1 * 128.0 + 64.0) as u8)
132 } else if t < 0.25 {
133 let v = (t - 0.1) / 0.15;
135 (0, (v * 128.0) as u8, (192.0 - v * 64.0) as u8)
136 } else if t < 0.5 {
137 let v = (t - 0.25) / 0.25;
139 (
140 (v * 100.0) as u8,
141 (128.0 + v * 64.0) as u8,
142 (128.0 - v * 64.0) as u8,
143 )
144 } else if t < 0.75 {
145 let v = (t - 0.5) / 0.25;
147 (
148 (100.0 + v * 100.0) as u8,
149 (192.0 - v * 64.0) as u8,
150 (64.0 + v * 32.0) as u8,
151 )
152 } else {
153 let v = (t - 0.75) / 0.25;
155 let c = (200.0 + v * 55.0) as u8;
156 (c, c, c)
157 }
158 }
159
160 fn jet(t: f64) -> (u8, u8, u8) {
161 let r = if t < 0.35 {
163 0.0
164 } else if t < 0.65 {
165 (t - 0.35) / 0.3
166 } else {
167 1.0
168 };
169
170 let g = if t < 0.125 {
171 0.0
172 } else if t < 0.375 {
173 (t - 0.125) / 0.25
174 } else if t < 0.625 {
175 1.0
176 } else if t < 0.875 {
177 1.0 - (t - 0.625) / 0.25
178 } else {
179 0.0
180 };
181
182 let b = if t < 0.35 {
183 0.5 + t / 0.35 * 0.5
184 } else if t < 0.65 {
185 1.0 - (t - 0.35) / 0.3
186 } else {
187 0.0
188 };
189
190 ((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
191 }
192
193 fn hot(t: f64) -> (u8, u8, u8) {
194 let r = if t < 0.4 { t / 0.4 } else { 1.0 };
196 let g = if t < 0.4 {
197 0.0
198 } else if t < 0.8 {
199 (t - 0.4) / 0.4
200 } else {
201 1.0
202 };
203 let b = if t < 0.8 { 0.0 } else { (t - 0.8) / 0.2 };
204
205 ((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
206 }
207
208 fn cool(t: f64) -> (u8, u8, u8) {
209 ((t * 255.0) as u8, ((1.0 - t) * 255.0) as u8, 255)
211 }
212
213 fn spectral(t: f64) -> (u8, u8, u8) {
214 if t < 0.2 {
216 let v = t / 0.2;
217 (
218 (158.0 + v * 60.0) as u8,
219 (1.0 + v * 102.0) as u8,
220 (66.0) as u8,
221 )
222 } else if t < 0.4 {
223 let v = (t - 0.2) / 0.2;
224 (
225 (213.0 + v * 40.0) as u8,
226 (103.0 + v * 96.0) as u8,
227 ((66.0 + v * 8.0) as u8),
228 )
229 } else if t < 0.6 {
230 let v = (t - 0.4) / 0.2;
231 (
232 (253.0 - v * 82.0) as u8,
233 (199.0 + v * 32.0) as u8,
234 (74.0 + v * 92.0) as u8,
235 )
236 } else if t < 0.8 {
237 let v = (t - 0.6) / 0.2;
238 (
239 (171.0 - v * 69.0) as u8,
240 (231.0 - v * 42.0) as u8,
241 (166.0 - v * 22.0) as u8,
242 )
243 } else {
244 let v = (t - 0.8) / 0.2;
245 (
246 (102.0 - v * 49.0) as u8,
247 (189.0 - v * 60.0) as u8,
248 ((144.0 - v * 45.0) as u8),
249 )
250 }
251 }
252
253 fn ndvi(t: f64) -> (u8, u8, u8) {
254 if t < 0.2 {
256 let v = t / 0.2;
258 (
259 (139.0 - v * 30.0) as u8,
260 (69.0 + v * 40.0) as u8,
261 (19.0 + v * 30.0) as u8,
262 )
263 } else if t < 0.4 {
264 let v = (t - 0.2) / 0.2;
266 (
267 (109.0 + v * 100.0) as u8,
268 (109.0 + v * 90.0) as u8,
269 (49.0 - v * 20.0) as u8,
270 )
271 } else if t < 0.6 {
272 let v = (t - 0.4) / 0.2;
274 (
275 (209.0 - v * 77.0) as u8,
276 (199.0 - v * 30.0) as u8,
277 (29.0 + v * 20.0) as u8,
278 )
279 } else if t < 0.8 {
280 let v = (t - 0.6) / 0.2;
282 (
283 (132.0 - v * 66.0) as u8,
284 (169.0 - v * 24.0) as u8,
285 (49.0) as u8,
286 )
287 } else {
288 let v = (t - 0.8) / 0.2;
290 (
291 (66.0 - v * 32.0) as u8,
292 ((145.0 - v * 45.0) as u8),
293 ((49.0 - v * 20.0) as u8),
294 )
295 }
296 }
297}
298
299#[derive(Debug, Clone)]
301pub struct RenderStyle {
302 pub colormap: Option<Colormap>,
304 pub value_range: Option<(f64, f64)>,
306 pub alpha: f32,
308 pub gamma: f32,
310 pub brightness: f32,
312 pub contrast: f32,
314 pub resampling: ResamplingMethod,
316}
317
318impl Default for RenderStyle {
319 fn default() -> Self {
320 Self {
321 colormap: Some(Colormap::Grayscale),
322 value_range: None,
323 alpha: 1.0,
324 gamma: 1.0,
325 brightness: 0.0,
326 contrast: 1.0,
327 resampling: ResamplingMethod::Bilinear,
328 }
329 }
330}
331
332impl RenderStyle {
333 pub fn from_config(config: &StyleConfig) -> Self {
335 let colormap = config
336 .colormap
337 .as_ref()
338 .and_then(|name| Colormap::from_name(name))
339 .or(Some(Colormap::Grayscale));
340
341 Self {
342 colormap,
343 value_range: config.value_range,
344 alpha: config.alpha,
345 gamma: config.gamma,
346 brightness: config.brightness,
347 contrast: config.contrast,
348 resampling: ResamplingMethod::Bilinear,
349 }
350 }
351}
352
353pub struct RasterRenderer;
355
356impl RasterRenderer {
357 pub fn render_to_rgba(
366 buffer: &RasterBuffer,
367 style: &RenderStyle,
368 ) -> Result<Vec<u8>, RenderError> {
369 let width = buffer.width() as usize;
370 let height = buffer.height() as usize;
371 let pixel_count = width * height;
372
373 let (min_val, max_val) = if let Some((min, max)) = style.value_range {
375 (min, max)
376 } else {
377 let stats = buffer.compute_statistics().map_err(|e| {
379 RenderError::ReadError(format!("Failed to compute statistics: {}", e))
380 })?;
381 (stats.min, stats.max)
382 };
383
384 let value_range = max_val - min_val;
385 if value_range.abs() < f64::EPSILON {
386 let alpha = (style.alpha * 255.0) as u8;
388 return Ok([128, 128, 128, alpha].repeat(pixel_count));
389 }
390
391 let mut rgba = vec![0u8; pixel_count * 4];
393
394 let colormap = style.colormap.unwrap_or(Colormap::Grayscale);
396 let gamma = style.gamma;
397 let brightness = style.brightness;
398 let contrast = style.contrast;
399 let alpha = (style.alpha * 255.0) as u8;
400
401 for y in 0..height {
402 for x in 0..width {
403 let pixel_idx = y * width + x;
404 let rgba_idx = pixel_idx * 4;
405
406 let value = buffer.get_pixel(x as u64, y as u64).unwrap_or(f64::NAN);
408
409 if value.is_nan() || buffer.is_nodata(value) {
411 rgba[rgba_idx] = 0;
413 rgba[rgba_idx + 1] = 0;
414 rgba[rgba_idx + 2] = 0;
415 rgba[rgba_idx + 3] = 0;
416 continue;
417 }
418
419 let mut normalized = (value - min_val) / value_range;
421
422 if (gamma - 1.0).abs() > f32::EPSILON {
424 normalized = normalized.powf(gamma as f64);
425 }
426
427 if contrast.abs() > f32::EPSILON || brightness.abs() > f32::EPSILON {
429 normalized = ((normalized - 0.5) * contrast as f64 + 0.5 + brightness as f64)
430 .clamp(0.0, 1.0);
431 }
432
433 let (r, g, b) = colormap.apply(normalized);
435
436 rgba[rgba_idx] = r;
437 rgba[rgba_idx + 1] = g;
438 rgba[rgba_idx + 2] = b;
439 rgba[rgba_idx + 3] = alpha;
440 }
441 }
442
443 Ok(rgba)
444 }
445
446 pub fn render_rgb_to_rgba(
454 red: &RasterBuffer,
455 green: &RasterBuffer,
456 blue: &RasterBuffer,
457 style: &RenderStyle,
458 ) -> Result<Vec<u8>, RenderError> {
459 let width = red.width() as usize;
460 let height = red.height() as usize;
461
462 if green.width() as usize != width
463 || green.height() as usize != height
464 || blue.width() as usize != width
465 || blue.height() as usize != height
466 {
467 return Err(RenderError::InvalidParameter(
468 "RGB bands must have same dimensions".to_string(),
469 ));
470 }
471
472 let pixel_count = width * height;
473 let mut rgba = vec![0u8; pixel_count * 4];
474
475 let alpha = (style.alpha * 255.0) as u8;
476 let gamma = style.gamma;
477 let brightness = style.brightness;
478 let contrast = style.contrast;
479
480 let (r_min, r_max) = if let Some((min, max)) = style.value_range {
482 (min, max)
483 } else {
484 let stats = red.compute_statistics().map_err(|e| {
485 RenderError::ReadError(format!("Failed to compute red stats: {}", e))
486 })?;
487 (stats.min, stats.max)
488 };
489 let r_range = (r_max - r_min).max(1.0);
490
491 let g_stats = green
492 .compute_statistics()
493 .map_err(|e| RenderError::ReadError(format!("Failed to compute green stats: {}", e)))?;
494 let g_range = (g_stats.max - g_stats.min).max(1.0);
495 let g_min = g_stats.min;
496
497 let b_stats = blue
498 .compute_statistics()
499 .map_err(|e| RenderError::ReadError(format!("Failed to compute blue stats: {}", e)))?;
500 let b_range = (b_stats.max - b_stats.min).max(1.0);
501 let b_min = b_stats.min;
502
503 for y in 0..height {
504 for x in 0..width {
505 let pixel_idx = y * width + x;
506 let rgba_idx = pixel_idx * 4;
507
508 let r_val = red.get_pixel(x as u64, y as u64).unwrap_or(0.0);
509 let g_val = green.get_pixel(x as u64, y as u64).unwrap_or(0.0);
510 let b_val = blue.get_pixel(x as u64, y as u64).unwrap_or(0.0);
511
512 let mut r_norm = (r_val - r_min) / r_range;
514 let mut g_norm = (g_val - g_min) / g_range;
515 let mut b_norm = (b_val - b_min) / b_range;
516
517 if (gamma - 1.0).abs() > f32::EPSILON {
519 let g = gamma as f64;
520 r_norm = r_norm.powf(g);
521 g_norm = g_norm.powf(g);
522 b_norm = b_norm.powf(g);
523 }
524
525 if contrast.abs() > f32::EPSILON || brightness.abs() > f32::EPSILON {
527 let c = contrast as f64;
528 let b_adj = brightness as f64;
529 r_norm = ((r_norm - 0.5) * c + 0.5 + b_adj).clamp(0.0, 1.0);
530 g_norm = ((g_norm - 0.5) * c + 0.5 + b_adj).clamp(0.0, 1.0);
531 b_norm = ((b_norm - 0.5) * c + 0.5 + b_adj).clamp(0.0, 1.0);
532 }
533
534 rgba[rgba_idx] = (r_norm * 255.0).clamp(0.0, 255.0) as u8;
535 rgba[rgba_idx + 1] = (g_norm * 255.0).clamp(0.0, 255.0) as u8;
536 rgba[rgba_idx + 2] = (b_norm * 255.0).clamp(0.0, 255.0) as u8;
537 rgba[rgba_idx + 3] = alpha;
538 }
539 }
540
541 Ok(rgba)
542 }
543
544 pub fn resample(
546 buffer: &RasterBuffer,
547 target_width: u64,
548 target_height: u64,
549 method: ResamplingMethod,
550 ) -> Result<RasterBuffer, RenderError> {
551 let resampler = Resampler::new(method);
552 resampler
553 .resample(buffer, target_width, target_height)
554 .map_err(RenderError::from)
555 }
556
557 pub fn read_window(
559 buffer: &RasterBuffer,
560 src_x: u64,
561 src_y: u64,
562 src_width: u64,
563 src_height: u64,
564 ) -> Result<RasterBuffer, RenderError> {
565 let width = buffer.width();
566 let height = buffer.height();
567
568 if src_x >= width || src_y >= height {
570 return Err(RenderError::InvalidParameter(format!(
571 "Window start ({}, {}) is outside buffer bounds ({}x{})",
572 src_x, src_y, width, height
573 )));
574 }
575
576 let actual_width = (src_width).min(width - src_x);
578 let actual_height = (src_height).min(height - src_y);
579
580 let data_type = buffer.data_type();
582 let mut output = RasterBuffer::zeros(actual_width, actual_height, data_type);
583
584 for dy in 0..actual_height {
586 for dx in 0..actual_width {
587 let value = buffer
588 .get_pixel(src_x + dx, src_y + dy)
589 .map_err(|e| RenderError::ReadError(e.to_string()))?;
590 output
591 .set_pixel(dx, dy, value)
592 .map_err(|e| RenderError::ReadError(e.to_string()))?;
593 }
594 }
595
596 Ok(output)
597 }
598}
599
600pub fn encode_png(data: &[u8], width: u32, height: u32) -> Result<Vec<u8>, RenderError> {
602 let mut output = Vec::new();
603 {
604 let mut encoder = png::Encoder::new(&mut output, width, height);
605 encoder.set_color(png::ColorType::Rgba);
606 encoder.set_depth(png::BitDepth::Eight);
607
608 let mut writer = encoder
609 .write_header()
610 .map_err(|e| RenderError::EncodingError(e.to_string()))?;
611
612 writer
613 .write_image_data(data)
614 .map_err(|e| RenderError::EncodingError(e.to_string()))?;
615 }
616
617 Ok(output)
618}
619
620pub fn encode_jpeg(data: &[u8], width: u32, height: u32) -> Result<Vec<u8>, RenderError> {
622 let rgb_data: Vec<u8> = data
624 .chunks(4)
625 .flat_map(|rgba| &rgba[0..3])
626 .copied()
627 .collect();
628
629 let mut jpeg_buffer = Vec::new();
630 let mut encoder = jpeg_encoder::Encoder::new(&mut jpeg_buffer, 90);
631 encoder.set_progressive(true);
632 encoder
633 .encode(
634 &rgb_data,
635 width as u16,
636 height as u16,
637 jpeg_encoder::ColorType::Rgb,
638 )
639 .map_err(|e| RenderError::EncodingError(e.to_string()))?;
640
641 Ok(jpeg_buffer)
642}
643
644pub fn encode_image(
646 data: &[u8],
647 width: u32,
648 height: u32,
649 format: ImageFormat,
650) -> Result<Bytes, RenderError> {
651 let encoded = match format {
652 ImageFormat::Png => encode_png(data, width, height)?,
653 ImageFormat::Jpeg => encode_jpeg(data, width, height)?,
654 ImageFormat::Webp => {
655 return Err(RenderError::Unsupported(
656 "WebP encoding not yet implemented".to_string(),
657 ));
658 }
659 ImageFormat::Geotiff => {
660 return Err(RenderError::Unsupported(
661 "GeoTIFF encoding not yet implemented".to_string(),
662 ));
663 }
664 };
665
666 Ok(Bytes::from(encoded))
667}
668
669pub fn world_to_pixel(
671 geo_transform: &GeoTransform,
672 world_x: f64,
673 world_y: f64,
674) -> Result<(u64, u64), RenderError> {
675 let (px, py) = geo_transform
676 .world_to_pixel(world_x, world_y)
677 .map_err(|e| RenderError::InvalidParameter(format!("Transform error: {}", e)))?;
678
679 if px < 0.0 || py < 0.0 {
680 return Err(RenderError::InvalidParameter(format!(
681 "Negative pixel coordinates: ({}, {})",
682 px, py
683 )));
684 }
685
686 Ok((px as u64, py as u64))
687}
688
689pub fn tile_to_bbox(
691 tile_matrix_set: &str,
692 z: u32,
693 x: u32,
694 y: u32,
695) -> Result<BoundingBox, RenderError> {
696 match tile_matrix_set {
697 "WebMercatorQuad" => {
698 let world_extent = 20_037_508.342_789_244;
699 let tile_count = 1u64 << z;
700 let tile_size = (2.0 * world_extent) / tile_count as f64;
701
702 let min_x = -world_extent + (x as f64) * tile_size;
703 let max_x = min_x + tile_size;
704 let max_y = world_extent - (y as f64) * tile_size;
705 let min_y = max_y - tile_size;
706
707 BoundingBox::new(min_x, min_y, max_x, max_y)
708 .map_err(|e| RenderError::InvalidParameter(format!("Invalid bbox: {}", e)))
709 }
710 "WorldCRS84Quad" => {
711 let tiles_x = 2u64 << z;
712 let tiles_y = 1u64 << z;
713 let tile_width = 360.0 / tiles_x as f64;
714 let tile_height = 180.0 / tiles_y as f64;
715
716 let min_x = -180.0 + (x as f64) * tile_width;
717 let max_x = min_x + tile_width;
718 let max_y = 90.0 - (y as f64) * tile_height;
719 let min_y = max_y - tile_height;
720
721 BoundingBox::new(min_x, min_y, max_x, max_y)
722 .map_err(|e| RenderError::InvalidParameter(format!("Invalid bbox: {}", e)))
723 }
724 _ => Err(RenderError::InvalidParameter(format!(
725 "Unknown tile matrix set: {}",
726 tile_matrix_set
727 ))),
728 }
729}
730
731#[cfg(test)]
733pub fn create_test_buffer(width: u64, height: u64) -> RasterBuffer {
734 let mut buffer = RasterBuffer::zeros(width, height, RasterDataType::Float32);
735
736 for y in 0..height {
737 for x in 0..width {
738 let value = (x as f64 + y as f64 * width as f64) / (width * height) as f64 * 100.0;
739 let _ = buffer.set_pixel(x, y, value);
740 }
741 }
742
743 buffer
744}
745
746#[cfg(test)]
747mod tests {
748 use super::*;
749
750 #[test]
751 fn test_colormap_grayscale() {
752 let cm = Colormap::Grayscale;
753 assert_eq!(cm.apply(0.0), (0, 0, 0));
754 assert_eq!(cm.apply(1.0), (255, 255, 255));
755 assert_eq!(cm.apply(0.5), (127, 127, 127));
756 }
757
758 #[test]
759 fn test_colormap_from_name() {
760 assert!(Colormap::from_name("viridis").is_some());
761 assert!(Colormap::from_name("VIRIDIS").is_some());
762 assert!(Colormap::from_name("terrain").is_some());
763 assert!(Colormap::from_name("unknown").is_none());
764 }
765
766 #[test]
767 fn test_render_to_rgba() {
768 let buffer = create_test_buffer(10, 10);
769 let style = RenderStyle::default();
770
771 let result = RasterRenderer::render_to_rgba(&buffer, &style);
772 assert!(result.is_ok());
773
774 let rgba = result.expect("render should succeed");
775 assert_eq!(rgba.len(), 10 * 10 * 4);
776 }
777
778 #[test]
779 fn test_encode_png() {
780 let rgba = vec![128u8; 4 * 4 * 4]; let result = encode_png(&rgba, 4, 4);
782 assert!(result.is_ok());
783 }
784
785 #[test]
786 fn test_tile_to_bbox_web_mercator() {
787 let bbox = tile_to_bbox("WebMercatorQuad", 0, 0, 0);
788 assert!(bbox.is_ok());
789
790 let bbox = bbox.expect("bbox should be valid");
791 assert!(bbox.min_x < bbox.max_x);
792 assert!(bbox.min_y < bbox.max_y);
793 }
794
795 #[test]
796 fn test_tile_to_bbox_wgs84() {
797 let bbox = tile_to_bbox("WorldCRS84Quad", 0, 0, 0);
798 assert!(bbox.is_ok());
799
800 let bbox = bbox.expect("bbox should be valid");
801 assert_eq!(bbox.min_x, -180.0);
802 assert_eq!(bbox.max_y, 90.0);
803 }
804
805 #[test]
806 fn test_read_window() {
807 let buffer = create_test_buffer(100, 100);
808 let window = RasterRenderer::read_window(&buffer, 10, 10, 20, 20);
809
810 assert!(window.is_ok());
811 let window = window.expect("window should succeed");
812 assert_eq!(window.width(), 20);
813 assert_eq!(window.height(), 20);
814 }
815
816 #[test]
817 fn test_resample() {
818 let buffer = create_test_buffer(100, 100);
819 let resampled = RasterRenderer::resample(&buffer, 50, 50, ResamplingMethod::Bilinear);
820
821 assert!(resampled.is_ok());
822 let resampled = resampled.expect("resample should succeed");
823 assert_eq!(resampled.width(), 50);
824 assert_eq!(resampled.height(), 50);
825 }
826}