From 7e5a6a9f6ba75aecba9194e02dee19cb1eaf5eef Mon Sep 17 00:00:00 2001 From: red-001 Date: Tue, 6 Aug 2024 16:22:32 +0100 Subject: [PATCH] Support 16-bit encoded TGA images. The decoder does the same thing as the BMP decoder, and converts 16-bit images to 32-bit ones on load. --- src/codecs/bmp/decoder.rs | 119 +++--------------- src/codecs/tga/decoder.rs | 119 ++++++++++++++++-- src/color.rs | 8 ++ src/utils/mod.rs | 2 + src/utils/packed_color.rs | 116 +++++++++++++++++ .../tga/testsuite/ccm8.tga.b7096b3.png | Bin 0 -> 832 bytes .../tga/testsuite/ucm8.tga.b7096b3.png | Bin 0 -> 832 bytes .../tga/testsuite/utc16.tga.134a83ba.png | Bin 0 -> 848 bytes 8 files changed, 250 insertions(+), 114 deletions(-) create mode 100644 src/utils/packed_color.rs create mode 100644 tests/reference/tga/testsuite/ccm8.tga.b7096b3.png create mode 100644 tests/reference/tga/testsuite/ucm8.tga.b7096b3.png create mode 100644 tests/reference/tga/testsuite/utc16.tga.134a83ba.png diff --git a/src/codecs/bmp/decoder.rs b/src/codecs/bmp/decoder.rs index 88339d7eef..7ac4193b70 100644 --- a/src/codecs/bmp/decoder.rs +++ b/src/codecs/bmp/decoder.rs @@ -13,6 +13,9 @@ use crate::error::{ use crate::image::{self, ImageDecoder, ImageFormat}; use crate::ImageDecoderRect; +use crate::utils::packed_color::Bitfield; +use crate::utils::packed_color::{BitfieldError, Bitfields}; + const BITMAPCOREHEADER_SIZE: u32 = 12; const BITMAPINFOHEADER_SIZE: u32 = 40; const BITMAPV2HEADER_SIZE: u32 = 52; @@ -20,21 +23,6 @@ const BITMAPV3HEADER_SIZE: u32 = 56; const BITMAPV4HEADER_SIZE: u32 = 108; const BITMAPV5HEADER_SIZE: u32 = 124; -static LOOKUP_TABLE_3_BIT_TO_8_BIT: [u8; 8] = [0, 36, 73, 109, 146, 182, 219, 255]; -static LOOKUP_TABLE_4_BIT_TO_8_BIT: [u8; 16] = [ - 0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255, -]; -static LOOKUP_TABLE_5_BIT_TO_8_BIT: [u8; 32] = [ - 0, 8, 16, 25, 33, 41, 49, 58, 66, 74, 82, 90, 99, 107, 115, 123, 132, 140, 148, 156, 165, 173, - 181, 189, 197, 206, 214, 222, 230, 239, 247, 255, -]; -static LOOKUP_TABLE_6_BIT_TO_8_BIT: [u8; 64] = [ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, - 97, 101, 105, 109, 113, 117, 121, 125, 130, 134, 138, 142, 146, 150, 154, 158, 162, 166, 170, - 174, 178, 182, 186, 190, 194, 198, 202, 206, 210, 215, 219, 223, 227, 231, 235, 239, 243, 247, - 251, 255, -]; - static R5_G5_B5_COLOR_MASK: Bitfields = Bitfields { r: Bitfield { len: 5, shift: 10 }, g: Bitfield { len: 5, shift: 5 }, @@ -120,14 +108,8 @@ enum DecoderError { // Failed to decompress RLE data. CorruptRleData, - /// The bitfield mask interleaves set and unset bits - BitfieldMaskNonContiguous, - /// Bitfield mask invalid (e.g. too long for specified type) - BitfieldMaskInvalid, - /// Bitfield (of the specified width – 16- or 32-bit) mask not present - BitfieldMaskMissing(u32), /// Bitfield (of the specified width – 16- or 32-bit) masks not present - BitfieldMasksMissing(u32), + BitfieldError(BitfieldError), /// BMP's "BM" signature wrong or missing BmpSignatureInvalid, @@ -164,13 +146,8 @@ impl fmt::Display for DecoderError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { DecoderError::CorruptRleData => f.write_str("Corrupt RLE data"), - DecoderError::BitfieldMaskNonContiguous => f.write_str("Non-contiguous bitfield mask"), - DecoderError::BitfieldMaskInvalid => f.write_str("Invalid bitfield mask"), - DecoderError::BitfieldMaskMissing(bb) => { - f.write_fmt(format_args!("Missing {bb}-bit bitfield mask")) - } - DecoderError::BitfieldMasksMissing(bb) => { - f.write_fmt(format_args!("Missing {bb}-bit bitfield masks")) + DecoderError::BitfieldError(bb) => { + f.write_fmt(format_args!("Bitfield error: {bb}")) } DecoderError::BmpSignatureInvalid => f.write_str("BMP signature not found"), DecoderError::MoreThanOnePlane => f.write_str("More than one plane"), @@ -207,6 +184,15 @@ impl From for ImageError { } } +impl From for ImageError { + fn from(e: BitfieldError) -> ImageError { + ImageError::Decoding(DecodingError::new( + ImageFormat::Bmp.into(), + DecoderError::BitfieldError(e), + )) + } +} + impl error::Error for DecoderError {} /// Distinct image types whose saved channel width can be invalid @@ -397,77 +383,6 @@ fn set_1bit_pixel_run<'a, T: Iterator>( } } -#[derive(PartialEq, Eq)] -struct Bitfield { - shift: u32, - len: u32, -} - -impl Bitfield { - fn from_mask(mask: u32, max_len: u32) -> ImageResult { - if mask == 0 { - return Ok(Bitfield { shift: 0, len: 0 }); - } - let mut shift = mask.trailing_zeros(); - let mut len = (!(mask >> shift)).trailing_zeros(); - if len != mask.count_ones() { - return Err(DecoderError::BitfieldMaskNonContiguous.into()); - } - if len + shift > max_len { - return Err(DecoderError::BitfieldMaskInvalid.into()); - } - if len > 8 { - shift += len - 8; - len = 8; - } - Ok(Bitfield { shift, len }) - } - - fn read(&self, data: u32) -> u8 { - let data = data >> self.shift; - match self.len { - 1 => ((data & 0b1) * 0xff) as u8, - 2 => ((data & 0b11) * 0x55) as u8, - 3 => LOOKUP_TABLE_3_BIT_TO_8_BIT[(data & 0b00_0111) as usize], - 4 => LOOKUP_TABLE_4_BIT_TO_8_BIT[(data & 0b00_1111) as usize], - 5 => LOOKUP_TABLE_5_BIT_TO_8_BIT[(data & 0b01_1111) as usize], - 6 => LOOKUP_TABLE_6_BIT_TO_8_BIT[(data & 0b11_1111) as usize], - 7 => ((data & 0x7f) << 1 | (data & 0x7f) >> 6) as u8, - 8 => (data & 0xff) as u8, - _ => panic!(), - } - } -} - -#[derive(PartialEq, Eq)] -struct Bitfields { - r: Bitfield, - g: Bitfield, - b: Bitfield, - a: Bitfield, -} - -impl Bitfields { - fn from_mask( - r_mask: u32, - g_mask: u32, - b_mask: u32, - a_mask: u32, - max_len: u32, - ) -> ImageResult { - let bitfields = Bitfields { - r: Bitfield::from_mask(r_mask, max_len)?, - g: Bitfield::from_mask(g_mask, max_len)?, - b: Bitfield::from_mask(b_mask, max_len)?, - a: Bitfield::from_mask(a_mask, max_len)?, - }; - if bitfields.r.len == 0 || bitfields.g.len == 0 || bitfields.b.len == 0 { - return Err(DecoderError::BitfieldMaskMissing(max_len).into()); - } - Ok(bitfields) - } -} - /// A bmp decoder pub struct BmpDecoder { reader: R, @@ -1308,7 +1223,7 @@ impl BmpDecoder { ImageType::RLE4 => self.read_rle_data(buf, ImageType::RLE4), ImageType::Bitfields16 => match self.bitfields { Some(_) => self.read_16_bit_pixel_data(buf, None), - None => Err(DecoderError::BitfieldMasksMissing(16).into()), + None => Err(BitfieldError::MasksMissing(16).into()), }, ImageType::Bitfields32 => match self.bitfields { Some(R8_G8_B8_COLOR_MASK) => { @@ -1318,7 +1233,7 @@ impl BmpDecoder { self.read_full_byte_pixel_data(buf, &FormatFullBytes::RGBA32) } Some(_) => self.read_32_bit_pixel_data(buf), - None => Err(DecoderError::BitfieldMasksMissing(32).into()), + None => Err(BitfieldError::MasksMissing(32).into()), }, } } diff --git a/src/codecs/tga/decoder.rs b/src/codecs/tga/decoder.rs index 33045934ff..eaf93545e9 100644 --- a/src/codecs/tga/decoder.rs +++ b/src/codecs/tga/decoder.rs @@ -2,12 +2,29 @@ use super::header::{Header, ImageType, ALPHA_BIT_MASK, SCREEN_ORIGIN_BIT_MASK}; use crate::{ color::{ColorType, ExtendedColorType}, error::{ - ImageError, ImageResult, LimitError, LimitErrorKind, UnsupportedError, UnsupportedErrorKind, + ImageError, ImageFormatHint, ImageResult, LimitError, LimitErrorKind, UnsupportedError, + UnsupportedErrorKind, }, image::{ImageDecoder, ImageFormat}, + utils::packed_color::{Bitfield, Bitfields}, +}; +use byteorder_lite::{LittleEndian, ReadBytesExt}; +use num_traits::ToPrimitive; +use std::io::{self, Cursor, Read}; + +static R5_G5_B5_COLOR_MASK: Bitfields = Bitfields { + r: Bitfield { len: 5, shift: 10 }, + g: Bitfield { len: 5, shift: 5 }, + b: Bitfield { len: 5, shift: 0 }, + a: Bitfield { len: 0, shift: 0 }, +}; + +static R5_G5_B5_A1_COLOR_MASK: Bitfields = Bitfields { + r: Bitfield { len: 5, shift: 10 }, + g: Bitfield { len: 5, shift: 5 }, + b: Bitfield { len: 5, shift: 0 }, + a: Bitfield { len: 1, shift: 15 }, }; -use byteorder_lite::ReadBytesExt; -use std::io::{self, Read}; struct ColorMap { /// sizes in bytes @@ -57,6 +74,8 @@ pub struct TgaDecoder { header: Header, color_map: Option, + + packing: Option<&'static Bitfields>, } impl TgaDecoder { @@ -76,6 +95,8 @@ impl TgaDecoder { header: Header::default(), color_map: None, + + packing: None, }; decoder.read_metadata()?; Ok(decoder) @@ -103,12 +124,13 @@ impl TgaDecoder { /// Loads the color information for the decoder /// - /// To keep things simple, we won't handle bit depths that aren't divisible - /// by 8 and are larger than 32. + /// To keep things simple, we won't handle bit depths that are larger than 32. fn read_color_information(&mut self) -> ImageResult<()> { - if self.header.pixel_depth % 8 != 0 || self.header.pixel_depth > 32 { + if self.header.pixel_depth != 15 + && (self.header.pixel_depth % 8 != 0 || self.header.pixel_depth > 32) + { // Bit depth must be divisible by 8, and must be less than or equal - // to 32. + // to 32 OR be 15-bit return Err(ImageError::Unsupported( UnsupportedError::from_format_and_kind( ImageFormat::Tga.into(), @@ -137,6 +159,7 @@ impl TgaDecoder { self.header.pixel_depth - num_alpha_bits }; + let color = self.image_type.is_color(); match (num_alpha_bits, other_channel_bits, color) { @@ -152,6 +175,16 @@ impl TgaDecoder { self.color_type = ColorType::L8; self.original_color_type = Some(ExtendedColorType::A8); } + (0, 15 | 16, true) => { + self.color_type = ColorType::Rgb8; + self.original_color_type = Some(ExtendedColorType::Rgb5); + self.packing = Some(&R5_G5_B5_COLOR_MASK); + } + (1, 15, true) => { + self.color_type = ColorType::Rgba8; + self.original_color_type = Some(ExtendedColorType::Rgb5a1); + self.packing = Some(&R5_G5_B5_A1_COLOR_MASK); + } _ => { return Err(ImageError::Unsupported( UnsupportedError::from_format_and_kind( @@ -350,16 +383,21 @@ impl ImageDecoder for TgaDecoder { .unwrap_or_else(|| self.color_type().into()) } + #[allow(clippy::identity_op)] fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); // In indexed images, we might need more bytes than pixels to read them. That's nonsensical // to encode but we'll not want to crash. + // + // also used for packed (<8 bit per channel) images let mut fallback_buf = vec![]; // read the pixels from the data region let rawbuf = if self.image_type.is_encoded() { let pixel_data = self.read_all_encoded_data()?; - if self.bytes_per_pixel <= usize::from(self.color_type.bytes_per_pixel()) { + if self.bytes_per_pixel <= usize::from(self.color_type.bytes_per_pixel()) + && self.packing.is_none() + { buf[..pixel_data.len()].copy_from_slice(&pixel_data); &buf[..pixel_data.len()] } else { @@ -368,7 +406,9 @@ impl ImageDecoder for TgaDecoder { } } else { let num_raw_bytes = self.width * self.height * self.bytes_per_pixel; - if self.bytes_per_pixel <= usize::from(self.color_type.bytes_per_pixel()) { + if self.bytes_per_pixel <= usize::from(self.color_type.bytes_per_pixel()) + && self.packing.is_none() + { self.r.by_ref().read_exact(&mut buf[..num_raw_bytes])?; &buf[..num_raw_bytes] } else { @@ -384,15 +424,70 @@ impl ImageDecoder for TgaDecoder { if self.image_type.is_color_mapped() { let pixel_data = self.expand_color_map(rawbuf)?; // not enough data to fill the buffer, or would overflow the buffer - if pixel_data.len() != buf.len() { + if !self.packing.is_some() && pixel_data.len() != buf.len() { return Err(ImageError::Limits(LimitError::from_kind( LimitErrorKind::DimensionError, ))); } - buf.copy_from_slice(&pixel_data); + + if self.packing.is_none() { + buf.copy_from_slice(&pixel_data); + } else { + fallback_buf.resize(pixel_data.len(), 0); + fallback_buf.copy_from_slice(&pixel_data); + } } - self.reverse_encoding_in_output(buf); + if let Some(bitfields) = &self.packing { + let num_pixels = self.width * self.height; + let bytes_per_unpacked_pixel = if bitfields.a.len > 0 { 4 } else { 3 }; + let mut stream = Cursor::new(fallback_buf); + let bytes_per_packed_pixel = if self.header.map_type == 0 { + self.bytes_per_pixel + } else { + (self.header.map_entry_size as usize + 7)/8 + }; + + if num_pixels * bytes_per_unpacked_pixel != buf.len() { + return Err(ImageError::Limits(LimitError::from_kind( + LimitErrorKind::DimensionError, + ))); + } + + // this check shouldn't get hit, unsupported formats should have been rejected in `read_color_information` + // but it seemed better to check this here instead of panicing below if there is an issue + if !(1..=3).contains(&bytes_per_packed_pixel) { + return Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormatHint::Exact(ImageFormat::Tga), + UnsupportedErrorKind::GenericFeature( + "Unsupported packed pixel format".to_string(), + ), + ), + )); + } + + for i in 0..num_pixels { + let value = match bytes_per_packed_pixel { + 1 => stream.read_u8().map(|i| -> u32 { i.to_u32().unwrap() }), + 2 => stream + .read_u16::() + .map(|i| -> u32 { i.to_u32().unwrap() }), + 3 => stream.read_u24::(), + _ => unimplemented!(), + } + .map_err(|e| -> ImageError { ImageError::IoError(e) })?; + + buf[i * bytes_per_unpacked_pixel + 0] = bitfields.r.read(value); + buf[i * bytes_per_unpacked_pixel + 1] = bitfields.g.read(value); + buf[i * bytes_per_unpacked_pixel + 2] = bitfields.b.read(value); + if bytes_per_unpacked_pixel == 4 { + buf[i * bytes_per_unpacked_pixel + 3] = bitfields.a.read(value); + } + } + } else { + self.reverse_encoding_in_output(buf); + } self.flip_vertically(buf); diff --git a/src/color.rs b/src/color.rs index 1027611858..b940e8108c 100644 --- a/src/color.rs +++ b/src/color.rs @@ -120,6 +120,10 @@ pub enum ExtendedColorType { Rgb4, /// Pixel is 4-bit RGB with an alpha channel Rgba4, + /// Pixel is 5-bit RGB with one unused bit + Rgb5, + /// Pixel is 5-bit RGB with one bit alpha + Rgb5a1, /// Pixel is 8-bit luminance L8, /// Pixel is 8-bit luminance with an alpha channel @@ -179,6 +183,7 @@ impl ExtendedColorType { ExtendedColorType::Rgb1 | ExtendedColorType::Rgb2 | ExtendedColorType::Rgb4 + | ExtendedColorType::Rgb5 | ExtendedColorType::Rgb8 | ExtendedColorType::Rgb16 | ExtendedColorType::Rgb32F @@ -186,6 +191,7 @@ impl ExtendedColorType { ExtendedColorType::Rgba1 | ExtendedColorType::Rgba2 | ExtendedColorType::Rgba4 + | ExtendedColorType::Rgb5a1 | ExtendedColorType::Rgba8 | ExtendedColorType::Rgba16 | ExtendedColorType::Rgba32F @@ -213,6 +219,8 @@ impl ExtendedColorType { ExtendedColorType::Rgba4 => 16, ExtendedColorType::L8 => 8, ExtendedColorType::La8 => 16, + ExtendedColorType::Rgb5 => 16, // somewhat of a lie, it's actually 15 bits of image data and one unused bit. + ExtendedColorType::Rgb5a1 => 16, ExtendedColorType::Rgb8 => 24, ExtendedColorType::Rgba8 => 32, ExtendedColorType::L16 => 16, diff --git a/src/utils/mod.rs b/src/utils/mod.rs index dff6cc760a..8e26b99e17 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -2,6 +2,8 @@ use std::iter::repeat; +pub(crate) mod packed_color; + #[inline(always)] pub(crate) fn expand_packed(buf: &mut [u8], channels: usize, bit_depth: u8, mut func: F) where diff --git a/src/utils/packed_color.rs b/src/utils/packed_color.rs new file mode 100644 index 0000000000..7969fb6135 --- /dev/null +++ b/src/utils/packed_color.rs @@ -0,0 +1,116 @@ +use std::fmt; + +static LOOKUP_TABLE_3_BIT_TO_8_BIT: [u8; 8] = [0, 36, 73, 109, 146, 182, 219, 255]; +static LOOKUP_TABLE_4_BIT_TO_8_BIT: [u8; 16] = [ + 0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255, +]; +static LOOKUP_TABLE_5_BIT_TO_8_BIT: [u8; 32] = [ + 0, 8, 16, 25, 33, 41, 49, 58, 66, 74, 82, 90, 99, 107, 115, 123, 132, 140, 148, 156, 165, 173, + 181, 189, 197, 206, 214, 222, 230, 239, 247, 255, +]; +static LOOKUP_TABLE_6_BIT_TO_8_BIT: [u8; 64] = [ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, + 97, 101, 105, 109, 113, 117, 121, 125, 130, 134, 138, 142, 146, 150, 154, 158, 162, 166, 170, + 174, 178, 182, 186, 190, 194, 198, 202, 206, 210, 215, 219, 223, 227, 231, 235, 239, 243, 247, + 251, 255, +]; + +/// All errors that can occur when working with packed bitmaps +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) enum BitfieldError { + /// Bitfield (of the specified width – 16- or 32-bit) masks not present + MasksMissing(u32), + + /// The bitfield mask interleaves set and unset bits + MaskNonContiguous, + /// Bitfield mask invalid (e.g. too long for specified type) + MaskInvalid, + /// Bitfield (of the specified width – 16- or 32-bit) mask not present + MaskMissing(u32), +} + +impl fmt::Display for BitfieldError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BitfieldError::MasksMissing(bb) => { + f.write_fmt(format_args!("Missing {bb}-bit bitfield masks")) + } + BitfieldError::MaskNonContiguous => f.write_str("Non-contiguous bitfield mask"), + BitfieldError::MaskInvalid => f.write_str("Invalid bitfield mask"), + BitfieldError::MaskMissing(bb) => { + f.write_fmt(format_args!("Missing {bb}-bit bitfield mask")) + } + } + } +} + +#[derive(PartialEq, Eq)] +pub(crate) struct Bitfield { + pub(crate) shift: u32, + pub(crate) len: u32, +} + +impl Bitfield { + fn from_mask(mask: u32, max_len: u32) -> Result { + if mask == 0 { + return Ok(Bitfield { shift: 0, len: 0 }); + } + let mut shift = mask.trailing_zeros(); + let mut len = (!(mask >> shift)).trailing_zeros(); + if len != mask.count_ones() { + return Err(BitfieldError::MaskNonContiguous); + } + if len + shift > max_len { + return Err(BitfieldError::MaskInvalid); + } + if len > 8 { + shift += len - 8; + len = 8; + } + Ok(Bitfield { shift, len }) + } + + pub(crate) fn read(&self, data: u32) -> u8 { + let data = data >> self.shift; + match self.len { + 1 => ((data & 0b1) * 0xff) as u8, + 2 => ((data & 0b11) * 0x55) as u8, + 3 => LOOKUP_TABLE_3_BIT_TO_8_BIT[(data & 0b00_0111) as usize], + 4 => LOOKUP_TABLE_4_BIT_TO_8_BIT[(data & 0b00_1111) as usize], + 5 => LOOKUP_TABLE_5_BIT_TO_8_BIT[(data & 0b01_1111) as usize], + 6 => LOOKUP_TABLE_6_BIT_TO_8_BIT[(data & 0b11_1111) as usize], + 7 => ((data & 0x7f) << 1 | (data & 0x7f) >> 6) as u8, + 8 => (data & 0xff) as u8, + _ => panic!(), + } + } +} + +#[derive(PartialEq, Eq)] +pub(crate) struct Bitfields { + pub(crate) r: Bitfield, + pub(crate) g: Bitfield, + pub(crate) b: Bitfield, + pub(crate) a: Bitfield, +} + +impl Bitfields { + pub(crate) fn from_mask( + r_mask: u32, + g_mask: u32, + b_mask: u32, + a_mask: u32, + max_len: u32, + ) -> Result { + let bitfields = Bitfields { + r: Bitfield::from_mask(r_mask, max_len)?, + g: Bitfield::from_mask(g_mask, max_len)?, + b: Bitfield::from_mask(b_mask, max_len)?, + a: Bitfield::from_mask(a_mask, max_len)?, + }; + if bitfields.r.len == 0 || bitfields.g.len == 0 || bitfields.b.len == 0 { + return Err(BitfieldError::MaskMissing(max_len)); + } + Ok(bitfields) + } +} diff --git a/tests/reference/tga/testsuite/ccm8.tga.b7096b3.png b/tests/reference/tga/testsuite/ccm8.tga.b7096b3.png new file mode 100644 index 0000000000000000000000000000000000000000..e8635eca5bd76d7bbb76461ea6269e9f1337c14d GIT binary patch literal 832 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^U}pDpaSW+oeEWcLf{N!P6;Gk= zRWom;0XwS?QBx zdVcm;0XwS?QBx zdVc5guaSW+oeEWcLf{N!P6;Gk= zRWom;0XwS?QBx zsz2upxBBNpo#My#6c~Xx$)68-?lAx})#D^SA36yV?>Wou{B+g-^DAP${+FD(`k(#c zqO1SYL)QKJ-*YPT|NOw6ul^frh5xO08&xzK1cXz;Kf!CU0{