#![allow(dead_code)]
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TestPattern {
GradientH,
GradientV,
GradientD,
Checkerboard,
Noise,
SolidColor,
ColorBars,
HighFrequency,
}
#[derive(Debug, Clone)]
pub struct TestImage {
pub width: u32,
pub height: u32,
pub components: u32,
pub pixels: Vec<u8>,
}
impl TestImage {
pub fn new(width: u32, height: u32, components: u32) -> Self {
let size = (width * height * components) as usize;
Self {
width,
height,
components,
pixels: vec![0; size],
}
}
pub fn from_pixels(width: u32, height: u32, components: u32, pixels: Vec<u8>) -> Self {
assert_eq!(
pixels.len(),
(width * height * components) as usize,
"Pixel data size mismatch"
);
Self {
width,
height,
components,
pixels,
}
}
pub fn get_pixel(&self, x: u32, y: u32, c: u32) -> u8 {
let idx = ((y * self.width + x) * self.components + c) as usize;
self.pixels[idx]
}
pub fn set_pixel(&mut self, x: u32, y: u32, c: u32, value: u8) {
let idx = ((y * self.width + x) * self.components + c) as usize;
self.pixels[idx] = value;
}
pub fn to_grayscale(&self) -> Self {
assert_eq!(self.components, 3, "Can only convert RGB to grayscale");
let mut gray = TestImage::new(self.width, self.height, 1);
for y in 0..self.height {
for x in 0..self.width {
let r = self.get_pixel(x, y, 0) as f32;
let g = self.get_pixel(x, y, 1) as f32;
let b = self.get_pixel(x, y, 2) as f32;
let y_val = (0.299 * r + 0.587 * g + 0.114 * b).round() as u8;
gray.set_pixel(x, y, 0, y_val);
}
}
gray
}
}
pub fn generate_test_image(
width: u32,
height: u32,
pattern: TestPattern,
components: u32,
) -> TestImage {
match pattern {
TestPattern::GradientH => generate_gradient_h(width, height, components),
TestPattern::GradientV => generate_gradient_v(width, height, components),
TestPattern::GradientD => generate_gradient_d(width, height, components),
TestPattern::Checkerboard => generate_checkerboard(width, height, 8, components),
TestPattern::Noise => generate_noise(width, height, 12345, components),
TestPattern::SolidColor => generate_solid(width, height, 128, components),
TestPattern::ColorBars => generate_color_bars(width, height),
TestPattern::HighFrequency => generate_high_frequency(width, height, components),
}
}
pub fn generate_gradient_h(width: u32, height: u32, components: u32) -> TestImage {
let mut img = TestImage::new(width, height, components);
for y in 0..height {
for x in 0..width {
let value = ((x as f32 / (width - 1).max(1) as f32) * 255.0).round() as u8;
for c in 0..components {
img.set_pixel(x, y, c, value);
}
}
}
img
}
pub fn generate_gradient_v(width: u32, height: u32, components: u32) -> TestImage {
let mut img = TestImage::new(width, height, components);
for y in 0..height {
let value = ((y as f32 / (height - 1).max(1) as f32) * 255.0).round() as u8;
for x in 0..width {
for c in 0..components {
img.set_pixel(x, y, c, value);
}
}
}
img
}
pub fn generate_gradient_d(width: u32, height: u32, components: u32) -> TestImage {
let mut img = TestImage::new(width, height, components);
for y in 0..height {
for x in 0..width {
let t = ((x + y) as f32 / ((width + height - 2).max(1) as f32)).min(1.0);
let value = (t * 255.0).round() as u8;
for c in 0..components {
img.set_pixel(x, y, c, value);
}
}
}
img
}
pub fn generate_checkerboard(
width: u32,
height: u32,
block_size: u32,
components: u32,
) -> TestImage {
let mut img = TestImage::new(width, height, components);
for y in 0..height {
for x in 0..width {
let bx = x / block_size;
let by = y / block_size;
let value = if (bx + by) % 2 == 0 { 255 } else { 0 };
for c in 0..components {
img.set_pixel(x, y, c, value);
}
}
}
img
}
pub fn generate_noise(width: u32, height: u32, seed: u64, components: u32) -> TestImage {
let mut img = TestImage::new(width, height, components);
let mut state = seed;
let a: u64 = 1103515245;
let c: u64 = 12345;
let m: u64 = 1 << 31;
for y in 0..height {
for x in 0..width {
for comp in 0..components {
state = (a.wrapping_mul(state).wrapping_add(c)) % m;
let value = ((state >> 16) & 0xFF) as u8;
img.set_pixel(x, y, comp, value);
}
}
}
img
}
pub fn generate_solid(width: u32, height: u32, value: u8, components: u32) -> TestImage {
let mut img = TestImage::new(width, height, components);
for pixel in img.pixels.iter_mut() {
*pixel = value;
}
img
}
pub fn generate_solid_rgb(width: u32, height: u32, r: u8, g: u8, b: u8) -> TestImage {
let mut img = TestImage::new(width, height, 3);
for y in 0..height {
for x in 0..width {
img.set_pixel(x, y, 0, r);
img.set_pixel(x, y, 1, g);
img.set_pixel(x, y, 2, b);
}
}
img
}
pub fn generate_color_bars(width: u32, height: u32) -> TestImage {
let mut img = TestImage::new(width, height, 3);
let colors: [(u8, u8, u8); 8] = [
(255, 255, 255), (255, 255, 0), (0, 255, 255), (0, 255, 0), (255, 0, 255), (255, 0, 0), (0, 0, 255), (0, 0, 0), ];
let bar_width = width / 8;
for y in 0..height {
for x in 0..width {
let bar_idx = ((x / bar_width.max(1)) as usize).min(7);
let (r, g, b) = colors[bar_idx];
img.set_pixel(x, y, 0, r);
img.set_pixel(x, y, 1, g);
img.set_pixel(x, y, 2, b);
}
}
img
}
pub fn generate_high_frequency(width: u32, height: u32, components: u32) -> TestImage {
let mut img = TestImage::new(width, height, components);
for y in 0..height {
for x in 0..width {
let value = if (x + y) % 2 == 0 { 255 } else { 0 };
for c in 0..components {
img.set_pixel(x, y, c, value);
}
}
}
img
}
pub fn distance_rms(original: &[u8], decoded: &[u8]) -> f64 {
assert_eq!(
original.len(),
decoded.len(),
"Images must have same size for RMS comparison"
);
if original.is_empty() {
return 0.0;
}
let sum_sq: f64 = original
.iter()
.zip(decoded.iter())
.map(|(&a, &b)| {
let diff = a as f64 - b as f64;
diff * diff
})
.sum();
let rms_normalized = (sum_sq / original.len() as f64).sqrt() / 255.0;
rms_normalized * 255.0 }
pub fn distance_rms_images(original: &TestImage, decoded: &TestImage) -> f64 {
assert_eq!(original.width, decoded.width);
assert_eq!(original.height, decoded.height);
assert_eq!(original.components, decoded.components);
distance_rms(&original.pixels, &decoded.pixels)
}
pub fn max_pixel_diff(original: &[u8], decoded: &[u8]) -> u8 {
assert_eq!(
original.len(),
decoded.len(),
"Images must have same size for max diff comparison"
);
original
.iter()
.zip(decoded.iter())
.map(|(&a, &b)| (a as i16 - b as i16).unsigned_abs() as u8)
.max()
.unwrap_or(0)
}
pub fn max_pixel_diff_images(original: &TestImage, decoded: &TestImage) -> u8 {
assert_eq!(original.width, decoded.width);
assert_eq!(original.height, decoded.height);
assert_eq!(original.components, decoded.components);
max_pixel_diff(&original.pixels, &decoded.pixels)
}
pub fn verify_output(original: &[u8], decoded: &[u8], max_rms: f64, max_diff: u8) {
let rms = distance_rms(original, decoded);
let diff = max_pixel_diff(original, decoded);
assert!(
rms <= max_rms,
"RMS distance {:.4} exceeds threshold {:.4}",
rms,
max_rms
);
assert!(
diff <= max_diff,
"Max pixel diff {} exceeds threshold {}",
diff,
max_diff
);
}
pub fn verify_output_images(original: &TestImage, decoded: &TestImage, max_rms: f64, max_diff: u8) {
assert_eq!(original.width, decoded.width, "Width mismatch");
assert_eq!(original.height, decoded.height, "Height mismatch");
assert_eq!(
original.components, decoded.components,
"Component count mismatch"
);
verify_output(&original.pixels, &decoded.pixels, max_rms, max_diff);
}
#[track_caller]
pub fn require_testdata_dir() -> PathBuf {
let dir = get_testdata_dir();
if !dir.exists() {
panic!(
"Testdata directory not found.\n\
Set JPEGLI_TESTDATA environment variable to the testdata directory.\n\
Expected structure: $JPEGLI_TESTDATA/jxl/flower/flower_small.rgb.png"
);
}
dir
}
#[track_caller]
pub fn require_flower_small_path() -> PathBuf {
let path = get_testdata_dir().join("jxl/flower/flower_small.rgb.png");
if !path.exists() {
panic!(
"Test image flower_small.rgb.png not found.\n\
Set JPEGLI_TESTDATA environment variable or ensure testdata is available.\n\
Expected at: jxl/flower/flower_small.rgb.png"
);
}
path
}
pub fn get_testdata_dir() -> PathBuf {
if let Ok(path) = std::env::var("JPEGLI_TESTDATA") {
return PathBuf::from(path);
}
if let Ok(manifest) = std::env::var("CARGO_MANIFEST_DIR") {
let testdata = PathBuf::from(&manifest).join("testdata");
if testdata.exists() {
return testdata;
}
let parent_testdata = PathBuf::from(&manifest).join("../testdata");
if parent_testdata.exists() {
return parent_testdata;
}
}
if let Ok(manifest) = std::env::var("CARGO_MANIFEST_DIR") {
let cpp_testdata = PathBuf::from(&manifest)
.parent()
.map(|p| p.join("internal/jpegli-cpp/testdata"))
.filter(|p| p.exists());
if let Some(testdata) = cpp_testdata {
return testdata;
}
}
PathBuf::from("testdata")
}
pub fn get_test_data_path(filename: &str) -> PathBuf {
get_testdata_dir().join(filename)
}
#[track_caller]
pub fn require_cpp_testdata_path(filename: &str) -> PathBuf {
get_cpp_testdata_path(filename).unwrap_or_else(|| {
panic!(
"C++ testdata file '{}' not found.\n\
Set CPP_TESTDATA_DIR environment variable to the directory containing .testdata files.\n\
Generate testdata with: GENERATE_RUST_TEST_DATA=1 cjpegli input.png output.jpg",
filename
)
})
}
pub fn get_cpp_testdata_path(filename: &str) -> Option<PathBuf> {
if let Ok(dir) = std::env::var("CPP_TESTDATA_DIR") {
let path = PathBuf::from(dir).join(filename);
if path.exists() {
return Some(path);
}
}
if let Ok(manifest) = std::env::var("CARGO_MANIFEST_DIR") {
let candidates = [
PathBuf::from(&manifest).join("cpp_testdata").join(filename),
PathBuf::from(&manifest)
.join("../cpp_testdata")
.join(filename),
PathBuf::from(&manifest)
.join("../internal/jpegli-cpp")
.join(filename),
PathBuf::from(&manifest).join("testdata").join(filename),
PathBuf::from(&manifest).join("..").join(filename),
];
for path in candidates {
if path.exists() {
return Some(path);
}
}
}
None
}
#[track_caller]
pub fn require_cjpegli() -> PathBuf {
find_cjpegli().unwrap_or_else(|| {
panic!(
"cjpegli binary not found.\n\
Set CJPEGLI_PATH environment variable or build jpegli:\n\
cd internal/jpegli-cpp && cmake -B build && cmake --build build"
)
})
}
pub fn find_cjpegli() -> Option<PathBuf> {
if let Ok(path) = std::env::var("CJPEGLI_PATH") {
let p = PathBuf::from(&path);
if p.exists() {
return Some(p);
}
}
if let Ok(manifest) = std::env::var("CARGO_MANIFEST_DIR") {
let base = PathBuf::from(&manifest);
let candidates = [
base.join("../internal/jpegli-cpp/build/tools/cjpegli"),
base.join("../internal/jpegli-cpp/build/tools/cjpegli.exe"),
base.join("../internal/jpegli-cpp/build/tools/Release/cjpegli.exe"),
base.join("../../jpegli/build/tools/cjpegli"),
base.join("../../jpegli/build/tools/cjpegli.exe"),
];
for path in candidates {
if path.exists() {
return Some(path);
}
}
}
let which_cmd = if cfg!(windows) { "where" } else { "which" };
std::process::Command::new(which_cmd)
.arg("cjpegli")
.output()
.ok()
.and_then(|o| {
if o.status.success() {
String::from_utf8(o.stdout)
.ok()
.map(|s| PathBuf::from(s.lines().next().unwrap_or("").trim()))
} else {
None
}
})
}
#[track_caller]
pub fn require_djpegli() -> PathBuf {
find_djpegli().unwrap_or_else(|| {
panic!(
"djpegli binary not found.\n\
Set DJPEGLI_PATH environment variable or build jpegli:\n\
cd internal/jpegli-cpp && cmake -B build && cmake --build build"
)
})
}
pub fn find_djpegli() -> Option<PathBuf> {
if let Ok(path) = std::env::var("DJPEGLI_PATH") {
let p = PathBuf::from(&path);
if p.exists() {
return Some(p);
}
}
if let Ok(manifest) = std::env::var("CARGO_MANIFEST_DIR") {
let base = PathBuf::from(&manifest);
let candidates = [
base.join("../internal/jpegli-cpp/build/tools/djpegli"),
base.join("../internal/jpegli-cpp/build/tools/djpegli.exe"),
base.join("../internal/jpegli-cpp/build/tools/Release/djpegli.exe"),
base.join("../../jpegli/build/tools/djpegli"),
base.join("../../jpegli/build/tools/djpegli.exe"),
];
for path in candidates {
if path.exists() {
return Some(path);
}
}
}
let which_cmd = if cfg!(windows) { "where" } else { "which" };
std::process::Command::new(which_cmd)
.arg("djpegli")
.output()
.ok()
.and_then(|o| {
if o.status.success() {
String::from_utf8(o.stdout)
.ok()
.map(|s| PathBuf::from(s.lines().next().unwrap_or("").trim()))
} else {
None
}
})
}
#[macro_export]
macro_rules! skip_if_missing {
($path:expr) => {
if !$path.exists() {
eprintln!("Skipping test: {:?} not found", $path);
return;
}
};
($path:expr, $msg:expr) => {
if !$path.exists() {
eprintln!("Skipping test: {} ({:?} not found)", $msg, $path);
return;
}
};
}
pub fn read_test_data(filename: &str) -> Option<Vec<u8>> {
let path = get_test_data_path(filename);
std::fs::read(&path).ok()
}
pub fn read_test_data_required(filename: &str) -> Vec<u8> {
let path = get_test_data_path(filename);
std::fs::read(&path).unwrap_or_else(|e| panic!("Failed to read test data {:?}: {}", path, e))
}
pub mod thresholds {
pub const Q50_MAX_RMS: f64 = 20.0;
pub const Q75_MAX_RMS: f64 = 10.0;
pub const Q85_MAX_RMS: f64 = 8.0;
pub const Q90_MAX_RMS: f64 = 4.0;
pub const Q95_MAX_RMS: f64 = 2.1;
#[inline]
pub fn max_rms_for_quality(quality: u8, h_samp: u8, v_samp: u8) -> f64 {
let subsample_factor = (h_samp as f64) * (v_samp as f64);
let base = if quality >= 95 {
2.1
} else if quality >= 90 {
4.0
} else if quality >= 85 {
8.0
} else {
20.0
};
base * subsample_factor
}
pub const SOURCE_MANAGER_MAX_RMS: f64 = 1.0;
pub const OUTPUT_SUSPENSION_PIXEL_MAX_RMS: f64 = 2.5;
pub const OUTPUT_SUSPENSION_RAW_MAX_RMS: f64 = 3.5;
pub const STREAMING_MAX_RMS: f64 = 3.8;
pub const DECODE_REUSE_MAX_RMS: f64 = 2.35;
pub const DECODE_COEFFICIENTS_MAX_RMS: f64 = 0.0;
pub const DECODE_LIBJPEG_COMPAT_MAX_RMS: f64 = 5.0;
pub const INPUT_SUSPENSION_BASE_MAX_RMS: f64 = 1.0;
pub const INPUT_SUSPENSION_NO_SUBSAMPLE_MAX_RMS: f64 = 1.75;
pub const INPUT_SUSPENSION_SUBSAMPLE_MAX_RMS: f64 = 3.0;
pub const INPUT_SUSPENSION_PROGRESSIVE_MAX_RMS: f64 = 8.0;
pub const XYB_BUTTERAUGLI: f64 = 1.32;
pub const XYB_MAX_BPP: f64 = 1.45;
pub const SMOOTH_BUTTERAUGLI: f64 = 3.0;
pub const YUV_BUTTERAUGLI: f64 = 1.32;
pub const YUV_MAX_BPP: f64 = 1.7;
pub const YUV_SUBSAMPLE_BUTTERAUGLI: f64 = 1.82;
pub const YUV_SUBSAMPLE_MAX_BPP: f64 = 1.55;
pub const YUV_NO_AQ_BUTTERAUGLI: f64 = 1.25;
pub const YUV_NO_AQ_MAX_BPP: f64 = 1.85;
pub const HDR_BUTTERAUGLI: f64 = 1.05;
pub const HDR_MAX_BPP: f64 = 2.95;
pub const DEFAULT_MAX_DIFF: u8 = 255;
pub const STRICT_MAX_DIFF: u8 = 35;
pub const REC2408_TONE_MAP_ERROR: f64 = 2.75e-5;
pub const HLG_OOTF_ERROR: f64 = 7.2e-7;
pub const GAMUT_MAP_ERROR: f64 = 1e-10;
pub const PQ_ENCODE_ERROR: f64 = 6e-7;
pub const HLG_ENCODE_ERROR: f64 = 4e-7;
pub const PQ_DECODE_ERROR: f64 = 3e-6;
pub const HLG_DECODE_ERROR: f64 = 6e-7;
}
pub fn ensure_testdata() -> Option<PathBuf> {
let testdata_dir = get_testdata_dir();
if testdata_dir.exists() {
return Some(testdata_dir);
}
eprintln!(
"Testdata not found at {:?}, attempting sparse checkout...",
testdata_dir
);
let jpegli_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.map(|p| p.to_path_buf());
if let Some(root) = jpegli_root {
let testdata_in_root = root.join("testdata");
if testdata_in_root.exists() {
return Some(testdata_in_root);
}
let status = std::process::Command::new("git")
.args(["submodule", "update", "--init", "--depth", "1", "testdata"])
.current_dir(&root)
.status();
if status.is_ok() && testdata_in_root.exists() {
return Some(testdata_in_root);
}
}
eprintln!("Could not find or checkout testdata. Some tests will be skipped.");
None
}
pub const ESSENTIAL_TEST_FILES: &[&str] = &[
"jxl/flower/flower_small.rgb.depth8.ppm",
"jxl/flower/flower_small.g.depth8.pgm",
"jxl/hdr_room.png",
"jxl/flower/flower.png.im_q85_444.jpg",
"jxl/flower/flower.png.im_q85_420.jpg",
"jxl/flower/flower.png.im_q85_420_progr.jpg",
"jxl/flower/flower.png.im_q85_420_R13B.jpg",
"jxl/flower/flower.png.im_q85_422.jpg",
"jxl/flower/flower.png.im_q85_440.jpg",
"jxl/flower/flower.png.im_q85_444_1x2.jpg",
"jxl/flower/flower.png.im_q85_asymmetric.jpg",
"jxl/flower/flower.png.im_q85_gray.jpg",
"jxl/flower/flower.png.im_q85_luma_subsample.jpg",
"jxl/flower/flower.png.im_q85_rgb.jpg",
"jxl/flower/flower.png.im_q85_rgb_subsample_blue.jpg",
"jxl/flower/flower_small.cmyk.jpg",
"jxl/flower/flower_small.q85_444_non_interleaved.jpg",
"jxl/flower/flower_small.q85_420_non_interleaved.jpg",
"jxl/flower/flower_small.q85_444_partially_interleaved.jpg",
"jxl/flower/flower_small.q85_420_partially_interleaved.jpg",
"jxl/flower/non_interleaved_scan.txt",
"jxl/flower/partially_interleaved_scan.txt",
"jxl/color_management/sRGB-D2700.icc",
];
pub fn has_test_file(filename: &str) -> bool {
get_test_data_path(filename).exists()
}
pub fn has_essential_test_files() -> bool {
ESSENTIAL_TEST_FILES.iter().all(|f| has_test_file(f))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_gradient_h() {
let img = generate_gradient_h(256, 64, 1);
assert_eq!(img.width, 256);
assert_eq!(img.height, 64);
assert_eq!(img.components, 1);
assert_eq!(img.get_pixel(0, 0, 0), 0);
assert_eq!(img.get_pixel(255, 0, 0), 255);
let mid = img.get_pixel(127, 0, 0);
assert!((126..=129).contains(&mid));
}
#[test]
fn test_generate_gradient_v() {
let img = generate_gradient_v(64, 256, 1);
assert_eq!(img.width, 64);
assert_eq!(img.height, 256);
assert_eq!(img.get_pixel(0, 0, 0), 0);
assert_eq!(img.get_pixel(0, 255, 0), 255);
}
#[test]
fn test_generate_checkerboard() {
let img = generate_checkerboard(64, 64, 8, 1);
assert_eq!(img.get_pixel(0, 0, 0), 255);
assert_eq!(img.get_pixel(8, 0, 0), 0);
assert_eq!(img.get_pixel(8, 8, 0), 255);
}
#[test]
fn test_generate_noise() {
let img1 = generate_noise(64, 64, 12345, 3);
let img2 = generate_noise(64, 64, 12345, 3);
let img3 = generate_noise(64, 64, 54321, 3);
assert_eq!(img1.pixels, img2.pixels);
assert_ne!(img1.pixels, img3.pixels);
}
#[test]
fn test_generate_color_bars() {
let img = generate_color_bars(64, 32);
assert_eq!(img.components, 3);
assert_eq!(img.get_pixel(0, 0, 0), 255);
assert_eq!(img.get_pixel(0, 0, 1), 255);
assert_eq!(img.get_pixel(0, 0, 2), 255);
}
#[test]
fn test_distance_rms_identical() {
let data: Vec<u8> = vec![100, 150, 200, 50, 75, 125];
let rms = distance_rms(&data, &data);
assert_eq!(rms, 0.0);
}
#[test]
fn test_distance_rms_different() {
let orig: Vec<u8> = vec![0, 0, 0, 0];
let decoded: Vec<u8> = vec![10, 10, 10, 10];
let rms = distance_rms(&orig, &decoded);
assert!((rms - 10.0).abs() < 0.01);
}
#[test]
fn test_max_pixel_diff() {
let orig: Vec<u8> = vec![100, 100, 100, 100];
let decoded: Vec<u8> = vec![100, 105, 95, 120];
let max_diff = max_pixel_diff(&orig, &decoded);
assert_eq!(max_diff, 20); }
#[test]
fn test_verify_output_pass() {
let orig: Vec<u8> = vec![100; 64];
let decoded: Vec<u8> = vec![105; 64];
verify_output(&orig, &decoded, 10.0, 10);
}
#[test]
#[should_panic(expected = "RMS distance")]
fn test_verify_output_fail_rms() {
let orig: Vec<u8> = vec![0; 64];
let decoded: Vec<u8> = vec![100; 64];
verify_output(&orig, &decoded, 10.0, 255);
}
#[test]
#[should_panic(expected = "Max pixel diff")]
fn test_verify_output_fail_diff() {
let orig: Vec<u8> = vec![100; 64];
let decoded: Vec<u8> = vec![105; 64];
verify_output(&orig, &decoded, 100.0, 3);
}
#[test]
fn test_to_grayscale() {
let mut rgb = TestImage::new(2, 2, 3);
rgb.set_pixel(0, 0, 0, 255);
rgb.set_pixel(0, 0, 1, 255);
rgb.set_pixel(0, 0, 2, 255);
rgb.set_pixel(1, 0, 0, 0);
rgb.set_pixel(1, 0, 1, 0);
rgb.set_pixel(1, 0, 2, 0);
let gray = rgb.to_grayscale();
assert_eq!(gray.components, 1);
assert_eq!(gray.get_pixel(0, 0, 0), 255);
assert_eq!(gray.get_pixel(1, 0, 0), 0);
}
}