diff options
author | Genny <gen@nyble.dev> | 2021-11-21 18:35:57 -0600 |
---|---|---|
committer | Genny <gen@nyble.dev> | 2021-11-21 18:35:57 -0600 |
commit | 1de64a3818875947f7f1044b1d4cfdf271b04fd3 (patch) | |
tree | 3a5bfbb237d67832dfa1beeb3d28566173484b63 /gifed/src | |
parent | f35e18cb0531e7d6a3544560746d592aa47ed555 (diff) | |
download | gifed-1de64a3818875947f7f1044b1d4cfdf271b04fd3.tar.gz gifed-1de64a3818875947f7f1044b1d4cfdf271b04fd3.zip |
Bring gifprobe into this repository
Diffstat (limited to 'gifed/src')
-rw-r--r-- | gifed/src/block/colortable.rs | 117 | ||||
-rw-r--r-- | gifed/src/block/extension/application.rs | 15 | ||||
-rw-r--r-- | gifed/src/block/extension/graphiccontrol.rs | 116 | ||||
-rw-r--r-- | gifed/src/block/extension/mod.rs | 49 | ||||
-rw-r--r-- | gifed/src/block/imagedescriptor.rs | 73 | ||||
-rw-r--r-- | gifed/src/block/indexedimage.rs | 70 | ||||
-rw-r--r-- | gifed/src/block/mod.rs | 25 | ||||
-rw-r--r-- | gifed/src/block/screendescriptor.rs | 79 | ||||
-rw-r--r-- | gifed/src/block/version.rs | 25 | ||||
-rw-r--r-- | gifed/src/color.rs | 38 | ||||
-rw-r--r-- | gifed/src/colorimage.rs | 61 | ||||
-rw-r--r-- | gifed/src/gif.rs | 306 | ||||
-rw-r--r-- | gifed/src/lib.rs | 51 | ||||
-rw-r--r-- | gifed/src/lzw.rs | 173 | ||||
-rw-r--r-- | gifed/src/reader/mod.rs | 274 | ||||
-rw-r--r-- | gifed/src/writer/gifbuilder.rs | 106 | ||||
-rw-r--r-- | gifed/src/writer/imagebuilder.rs | 133 | ||||
-rw-r--r-- | gifed/src/writer/mod.rs | 5 |
18 files changed, 1716 insertions, 0 deletions
diff --git a/gifed/src/block/colortable.rs b/gifed/src/block/colortable.rs new file mode 100644 index 0000000..01fe00b --- /dev/null +++ b/gifed/src/block/colortable.rs @@ -0,0 +1,117 @@ +pub use crate::Color; +use crate::EncodingError; +use std::{ + convert::{TryFrom, TryInto}, + ops::Deref, +}; + +#[derive(Clone, Debug)] +pub struct ColorTable { + table: Vec<Color>, +} + +impl ColorTable { + pub fn new() -> Self { + Self { table: vec![] } + } + + /// Returns the number of colors in the color table as used by the packed + /// fields in the Logical Screen Descriptor and Image Descriptor. You can + /// get the actual size with the [`len`](struct.ColorTable.html#method.len) method. + pub fn packed_len(&self) -> u8 { + ((self.table.len() as f32).log2().ceil() - 1f32) as u8 + } + + /// Returns the number of items in the table + pub fn len(&self) -> usize { + self.table.len() + } + + /// Pushes a color on to the end of the table + pub fn push(&mut self, color: Color) { + self.table.push(color); + } + + pub fn get(&self, index: u8) -> Option<Color> { + self.table.get(index as usize).map(|v| v.clone()) + } + + pub fn from_color(&self, color: Color) -> Option<u8> { + for (i, &c) in self.table.iter().enumerate() { + if c == color { + return Some(i as u8); + } + } + None + } +} + +impl Deref for ColorTable { + type Target = [Color]; + + fn deref(&self) -> &Self::Target { + &self.table + } +} + +impl From<&ColorTable> for Box<[u8]> { + fn from(table: &ColorTable) -> Self { + let mut vec = vec![]; + + for color in table.iter() { + vec.extend_from_slice(&[color.r, color.g, color.b]); + } + + let packed_len = 2usize.pow(table.packed_len() as u32 + 1); + let padding = (packed_len as usize - table.len()) * 3; + if padding > 0 { + vec.extend_from_slice(&vec![0; padding]); + } + + vec.into_boxed_slice() + } +} + +//TODO: TryFrom Vec<u8> (must be multiple of 3 len) and From Vec<Color> +impl TryFrom<&[u8]> for ColorTable { + type Error = (); + + fn try_from(value: &[u8]) -> Result<Self, Self::Error> { + if value.len() % 3 != 0 { + return Err(()); + } else { + Ok(Self { + table: value + .chunks(3) + .map(|slice| Color::from(TryInto::<[u8; 3]>::try_into(slice).unwrap())) + .collect::<Vec<Color>>(), + }) + } + } +} + +impl TryFrom<Vec<Color>> for ColorTable { + type Error = EncodingError; + + fn try_from(value: Vec<Color>) -> Result<Self, Self::Error> { + if value.len() > 256 { + Err(EncodingError::TooManyColors) + } else { + Ok(Self { table: value }) + } + } +} + +impl TryFrom<Vec<(u8, u8, u8)>> for ColorTable { + type Error = EncodingError; + + fn try_from(value: Vec<(u8, u8, u8)>) -> Result<Self, Self::Error> { + if value.len() > 256 { + Err(EncodingError::TooManyColors) + } else { + Ok(Self { + table: value.into_iter().map(|c| c.into()).collect(), + }) + } + } +} diff --git a/gifed/src/block/extension/application.rs b/gifed/src/block/extension/application.rs new file mode 100644 index 0000000..9ec1814 --- /dev/null +++ b/gifed/src/block/extension/application.rs @@ -0,0 +1,15 @@ +pub struct Application { + pub(crate) identifier: String, // max len 8 + pub(crate) authentication_code: [u8; 3], + pub(crate) data: Vec<u8>, +} + +impl Application { + pub fn identifier(&self) -> &str { + &self.identifier + } + + pub fn authentication_code(&self) -> &[u8] { + &self.authentication_code + } +} diff --git a/gifed/src/block/extension/graphiccontrol.rs b/gifed/src/block/extension/graphiccontrol.rs new file mode 100644 index 0000000..b595554 --- /dev/null +++ b/gifed/src/block/extension/graphiccontrol.rs @@ -0,0 +1,116 @@ +use std::{convert::TryInto, fmt}; + +#[derive(Clone, Debug)] +pub struct GraphicControl { + pub(crate) packed: u8, + pub(crate) delay_time: u16, + pub(crate) transparency_index: u8, +} + +impl GraphicControl { + pub fn new( + disposal_method: DisposalMethod, + user_input_flag: bool, + transparency_flag: bool, + delay_time: u16, + transparency_index: u8, + ) -> Self { + let mut ret = Self { + packed: 0, + delay_time, + transparency_index, + }; + + ret.set_disposal_method(disposal_method); + ret.user_input(user_input_flag); + ret.transparency(transparency_flag); + + ret + } + + pub fn disposal_method(&self) -> Option<DisposalMethod> { + match self.packed & 0b000_111_00 { + 0b000_000_00 => Some(DisposalMethod::NoAction), + 0b000_100_00 => Some(DisposalMethod::DoNotDispose), + 0b000_010_00 => Some(DisposalMethod::RestoreBackground), + 0b000_110_00 => Some(DisposalMethod::RestorePrevious), + _ => None, + } + } + + pub fn set_disposal_method(&mut self, method: DisposalMethod) { + match method { + DisposalMethod::NoAction => self.packed &= 0b111_000_1_1, + DisposalMethod::DoNotDispose => self.packed |= 0b000_100_0_0, + DisposalMethod::RestoreBackground => self.packed |= 0b000_010_0_0, + DisposalMethod::RestorePrevious => self.packed |= 0b000_110_0_0, + }; + } + + pub fn transparency_index(&self) -> u8 { + self.transparency_index + } + + pub fn user_input(&mut self, flag: bool) { + if flag { + self.packed |= 0b000_000_1_0; + } else { + self.packed &= 0b111_111_0_1; + } + } + + pub fn transparency(&mut self, flag: bool) { + if flag { + self.packed |= 0b000_000_0_1; + } else { + self.packed &= 0b111_111_1_0; + } + } + + pub fn delay_time(&self) -> u16 { + self.delay_time + } + + pub fn delay_time_mut(&mut self) -> &mut u16 { + &mut self.delay_time + } + + pub fn is_transparent(&self) -> bool { + self.packed & 0b000_000_0_1 > 0 + } +} + +impl From<[u8; 4]> for GraphicControl { + fn from(arr: [u8; 4]) -> Self { + let packed = arr[0]; + let delay_time = u16::from_le_bytes(arr[1..3].try_into().unwrap()); + let transparency_index = arr[3]; + + Self { + packed, + delay_time, + transparency_index, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum DisposalMethod { + NoAction, + DoNotDispose, + RestoreBackground, + RestorePrevious, +} + +impl fmt::Display for DisposalMethod { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let st = match self { + DisposalMethod::NoAction => "Dispose as Normal", + DisposalMethod::DoNotDispose => "No Dispose", + DisposalMethod::RestoreBackground => "Restore to background", + DisposalMethod::RestorePrevious => "Restore previous image", + }; + + write!(f, "{}", st) + } +} diff --git a/gifed/src/block/extension/mod.rs b/gifed/src/block/extension/mod.rs new file mode 100644 index 0000000..fb5eb20 --- /dev/null +++ b/gifed/src/block/extension/mod.rs @@ -0,0 +1,49 @@ +mod application; +mod graphiccontrol; + +pub use graphiccontrol::{DisposalMethod, GraphicControl}; + +pub use self::application::Application; + +pub enum Extension { + GraphicControl(GraphicControl), + Looping(u16), + Comment(Vec<u8>), // Plain Text + Application(Application), +} + +impl From<&Extension> for Box<[u8]> { + fn from(ext: &Extension) -> Self { + let mut vec = vec![]; + vec.push(0x21); // Push the extension introducer + + match ext { + Extension::GraphicControl(gc) => { + vec.push(0xF9); // Graphic control label + vec.push(0x04); // Block size for this extension is always 4 + vec.push(gc.packed); + vec.extend_from_slice(&gc.delay_time.to_le_bytes()); + vec.push(gc.transparency_index); + } + Extension::Looping(count) => { + vec.push(0xFF); // Application extension label + vec.push(0x0B); // 11 bytes in this block + vec.extend_from_slice(b"NETSCAPE2.0"); // App. ident. and "auth code" + vec.push(0x03); // Sub-block length + vec.push(0x01); // Identifies netscape looping extension + vec.extend_from_slice(&count.to_le_bytes()); + } + Extension::Comment(_) => todo!(), + Extension::Application(_) => todo!(), + } + + vec.push(0x00); // Zero-length data block indicates end of extension + vec.into_boxed_slice() + } +} + +impl From<GraphicControl> for Extension { + fn from(gce: GraphicControl) -> Self { + Extension::GraphicControl(gce) + } +} diff --git a/gifed/src/block/imagedescriptor.rs b/gifed/src/block/imagedescriptor.rs new file mode 100644 index 0000000..25567b2 --- /dev/null +++ b/gifed/src/block/imagedescriptor.rs @@ -0,0 +1,73 @@ +use std::convert::TryInto; + +pub struct ImageDescriptor { + // Image Seperator 0x2C is the first byte // + pub left: u16, + pub top: u16, + pub width: u16, + pub height: u16, + pub packed: u8, +} + +impl ImageDescriptor { + pub fn set_color_table_present(&mut self, is_present: bool) { + if is_present { + self.packed |= 0b1000_0000; + } else { + self.packed &= 0b0111_1111; + } + } + + pub fn set_color_table_size(&mut self, size: u8) { + // GCT size is calulated by raising two to this number plus one, + // so we have to work backwards. + let size = (size as f32).log2().ceil() - 1f32; + self.packed |= size as u8; + } + + //TODO: Setter for sort flag in packed field + //TODO: Setter for interlace flag in packed field + + pub fn color_table_present(&self) -> bool { + self.packed & 0b1000_0000 != 0 + } + + pub fn color_table_size(&self) -> usize { + crate::packed_to_color_table_length(self.packed & 0b0000_0111) + } +} + +impl From<&ImageDescriptor> for Box<[u8]> { + fn from(desc: &ImageDescriptor) -> Self { + let mut vec = vec![]; + + vec.push(0x2C); // Image Seperator + vec.extend_from_slice(&desc.left.to_le_bytes()); + vec.extend_from_slice(&desc.top.to_le_bytes()); + vec.extend_from_slice(&desc.width.to_le_bytes()); + vec.extend_from_slice(&desc.height.to_le_bytes()); + vec.push(desc.packed); + + vec.into_boxed_slice() + } +} + +impl From<[u8; 9]> for ImageDescriptor { + fn from(arr: [u8; 9]) -> Self { + let left = u16::from_le_bytes(arr[0..2].try_into().unwrap()); + let top = u16::from_le_bytes(arr[2..4].try_into().unwrap()); + let width = u16::from_le_bytes(arr[4..6].try_into().unwrap()); + let height = u16::from_le_bytes(arr[6..8].try_into().unwrap()); + let packed = arr[8]; + + Self { + left, + top, + width, + height, + packed, + } + } +} + +//TODO: Impl to allow changing the packed field easier diff --git a/gifed/src/block/indexedimage.rs b/gifed/src/block/indexedimage.rs new file mode 100644 index 0000000..8ed0319 --- /dev/null +++ b/gifed/src/block/indexedimage.rs @@ -0,0 +1,70 @@ +use std::convert::TryFrom; + +use super::{ColorTable, ImageDescriptor}; +use crate::LZW; + +pub struct IndexedImage { + pub image_descriptor: ImageDescriptor, + pub local_color_table: Option<ColorTable>, + pub indicies: Vec<u8>, +} + +impl IndexedImage { + pub fn left(&self) -> u16 { + self.image_descriptor.left + } + + pub fn top(&self) -> u16 { + self.image_descriptor.left + } + + pub fn width(&self) -> u16 { + self.image_descriptor.width + } + + pub fn height(&self) -> u16 { + self.image_descriptor.height + } + + pub fn as_boxed_slice(&self, minimum_code_size: u8) -> Box<[u8]> { + let mut out = vec![]; + + let mut boxed: Box<[u8]> = (&self.image_descriptor).into(); + out.extend_from_slice(&*boxed); + + // Get the mcs while we write out the color table + let mut mcs = if let Some(lct) = &self.local_color_table { + boxed = lct.into(); + out.extend_from_slice(&*boxed); + + lct.packed_len() + } else { + minimum_code_size + 1 + }; + + if mcs < 2 { + mcs = 2; // Must be true: 0 <= mcs <= 8 + } + + // First write out the MCS + out.push(mcs); + + let compressed = LZW::encode(mcs, &self.indicies); + + for chunk in compressed.chunks(255) { + out.push(chunk.len() as u8); + out.extend_from_slice(chunk); + } + // Data block length 0 to indicate an end + out.push(0x00); + + out.into_boxed_slice() + } +} + +pub struct CompressedImage { + pub image_descriptor: ImageDescriptor, + pub local_color_table: Option<ColorTable>, + pub lzw_minimum_code_size: u8, + pub blocks: Vec<Vec<u8>>, +} diff --git a/gifed/src/block/mod.rs b/gifed/src/block/mod.rs new file mode 100644 index 0000000..e35224b --- /dev/null +++ b/gifed/src/block/mod.rs @@ -0,0 +1,25 @@ +mod colortable; +pub mod extension; +mod imagedescriptor; +mod indexedimage; +mod screendescriptor; +mod version; + +pub use colortable::ColorTable; +pub use imagedescriptor::ImageDescriptor; +pub use indexedimage::CompressedImage; +pub use indexedimage::IndexedImage; +pub use screendescriptor::ScreenDescriptor; +pub use version::Version; + +use crate::writer::ImageBuilder; + +pub enum Block { + IndexedImage(IndexedImage), + Extension(extension::Extension), +} + +enum WriteBlock { + ImageBuilder(ImageBuilder), + Block(Block), +} diff --git a/gifed/src/block/screendescriptor.rs b/gifed/src/block/screendescriptor.rs new file mode 100644 index 0000000..dc0257d --- /dev/null +++ b/gifed/src/block/screendescriptor.rs @@ -0,0 +1,79 @@ +use std::convert::TryInto; + +pub struct ScreenDescriptor { + pub width: u16, + pub height: u16, + pub packed: u8, + pub background_color_index: u8, + pub pixel_aspect_ratio: u8, +} + +impl ScreenDescriptor { + pub fn new(width: u16, height: u16) -> Self { + Self { + width, + height, + packed: 0, + background_color_index: 0, + pixel_aspect_ratio: 0, + } + } + + pub fn set_color_table_present(&mut self, is_present: bool) { + if is_present { + self.packed |= 0b1000_0000; + } else { + self.packed &= 0b0111_1111; + } + } + + pub fn set_color_table_size(&mut self, size: u8) { + println!("scts: {}", size); + // GCT size is calulated by raising two to this number plus one, + // so we have to work backwards. + let size = (size as f32).log2().ceil() - 1f32; + self.packed |= size as u8; + } + + //TODO: Setter for sort flag in packed field + //TODO: Setter for color resolution in packed field + + pub fn color_table_present(&self) -> bool { + self.packed & 0b1000_0000 != 0 + } + + pub fn color_table_len(&self) -> usize { + crate::packed_to_color_table_length(self.packed & 0b0000_0111) + } +} + +impl From<&ScreenDescriptor> for Box<[u8]> { + fn from(lsd: &ScreenDescriptor) -> Self { + let mut vec = vec![]; + vec.extend_from_slice(&lsd.width.to_le_bytes()); + vec.extend_from_slice(&lsd.height.to_le_bytes()); + vec.push(lsd.packed); + vec.push(lsd.background_color_index); + vec.push(lsd.pixel_aspect_ratio); + + vec.into_boxed_slice() + } +} + +impl From<[u8; 7]> for ScreenDescriptor { + fn from(arr: [u8; 7]) -> Self { + let width = u16::from_le_bytes(arr[0..2].try_into().unwrap()); + let height = u16::from_le_bytes(arr[2..4].try_into().unwrap()); + let packed = arr[4]; + let background_color_index = arr[5]; + let pixel_aspect_ratio = arr[6]; + + Self { + width, + height, + packed, + background_color_index, + pixel_aspect_ratio, + } + } +} diff --git a/gifed/src/block/version.rs b/gifed/src/block/version.rs new file mode 100644 index 0000000..0171ad4 --- /dev/null +++ b/gifed/src/block/version.rs @@ -0,0 +1,25 @@ +use std::fmt; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Version { + Gif87a, + Gif89a, +} + +impl From<&Version> for &[u8] { + fn from(version: &Version) -> Self { + match version { + Version::Gif87a => b"GIF87a", + Version::Gif89a => b"GIF89a", + } + } +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Version::Gif87a => write!(f, "GIF87a"), + Version::Gif89a => write!(f, "GIF89a"), + } + } +} diff --git a/gifed/src/color.rs b/gifed/src/color.rs new file mode 100644 index 0000000..e18ce58 --- /dev/null +++ b/gifed/src/color.rs @@ -0,0 +1,38 @@ +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct Color { + pub r: u8, + pub g: u8, + pub b: u8, +} + +impl Color { + pub fn new(r: u8, g: u8, b: u8) -> Self { + Self { r, g, b } + } +} + +impl From<[u8; 3]> for Color { + fn from(arr: [u8; 3]) -> Self { + Self { + r: arr[0], + g: arr[1], + b: arr[2], + } + } +} + +impl From<(u8, u8, u8)> for Color { + fn from(t: (u8, u8, u8)) -> Self { + Self { + r: t.0, + g: t.1, + b: t.2, + } + } +} + +impl Into<[u8; 3]> for Color { + fn into(self) -> [u8; 3] { + [self.r, self.g, self.b] + } +} diff --git a/gifed/src/colorimage.rs b/gifed/src/colorimage.rs new file mode 100644 index 0000000..69dac1e --- /dev/null +++ b/gifed/src/colorimage.rs @@ -0,0 +1,61 @@ +use std::convert::TryFrom; + +use crate::{block::ColorTable, gif::Image, reader::DecodingError, Color}; + +pub struct ColorImage { + width: u16, + height: u16, + data: Vec<Pixel>, +} + +impl ColorImage { + pub(crate) fn from_indicies( + width: u16, + height: u16, + indicies: &[u8], + table: &ColorTable, + transindex: Option<u8>, + ) -> Result<Self, DecodingError> { + let mut data = vec![Pixel::Transparent; (width * height) as usize]; + + for (image_index, color_index) in indicies.into_iter().enumerate() { + if let Some(trans) = transindex { + if trans == *color_index { + data[image_index] = Pixel::Transparent; + } + } else { + data[image_index] = Pixel::Color( + table + .get(*color_index) + .ok_or(DecodingError::ColorIndexOutOfBounds)?, + ); + } + } + + Ok(ColorImage { + width, + height, + data, + }) + } +} + +impl<'a> TryFrom<Image<'a>> for ColorImage { + type Error = DecodingError; + + fn try_from(img: Image<'a>) -> Result<Self, Self::Error> { + ColorImage::from_indicies( + img.width, + img.height, + img.indicies, + img.palette, + img.transparent_index, + ) + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Pixel { + Color(Color), + Transparent, +} diff --git a/gifed/src/gif.rs b/gifed/src/gif.rs new file mode 100644 index 0000000..89aaa64 --- /dev/null +++ b/gifed/src/gif.rs @@ -0,0 +1,306 @@ +use std::{fs::File, io::Write, path::Path}; + +use crate::{ + block::{extension::Extension, Block, ColorTable, ScreenDescriptor, Version}, + colorimage, + writer::GifBuilder, + ColorImage, +}; +pub struct Gif { + pub header: Version, + pub screen_descriptor: ScreenDescriptor, + pub global_color_table: Option<ColorTable>, + pub blocks: Vec<Block>, // Trailer at the end of this struct is 0x3B // +} + +impl Gif { + pub fn builder(width: u16, height: u16) -> GifBuilder { + GifBuilder::new(width, height) + } + + pub fn to_vec(&self) -> Vec<u8> { + let mut out = vec![]; + + out.extend_from_slice((&self.header).into()); + + let mut boxed: Box<[u8]> = (&self.screen_descriptor).into(); + out.extend_from_slice(&*boxed); + + // While we output the color table, grab it's length to use when + // outputting the image, or 0 if we don't have a GCT + let mcs = if let Some(gct) = &self.global_color_table { + boxed = gct.into(); + out.extend_from_slice(&*boxed); + + gct.packed_len() + } else { + 0 + }; + + for block in self.blocks.iter() { + match block { + Block::IndexedImage(image) => { + boxed = image.as_boxed_slice(mcs); + out.extend_from_slice(&*boxed); + } + //Block::BlockedImage(_) => unreachable!(), + Block::Extension(ext) => { + boxed = ext.into(); + out.extend_from_slice(&*boxed); + } + } + } + + // Write Trailer + out.push(0x3B); + + out + } + + pub fn save<P: AsRef<Path>>(&self, path: P) -> std::io::Result<()> { + File::create(path.as_ref())?.write_all(&self.to_vec()) + } + + pub fn images<'a>(&'a self) -> ImageIterator<'a> { + ImageIterator { + gif: self, + veciter: self.blocks.iter(), + } + } +} + +pub struct ImageIterator<'a> { + gif: &'a Gif, + veciter: std::slice::Iter<'a, Block>, +} + +impl<'a> Iterator for ImageIterator<'a> { + type Item = Image<'a>; + + fn next(&mut self) -> Option<Self::Item> { + let mut transparent = None; + + let img = loop { + match self.veciter.next() { + Some(block) => match block { + Block::IndexedImage(img) => break img, + Block::Extension(Extension::GraphicControl(gce)) => { + if gce.is_transparent() { + transparent = Some(gce.transparency_index()); + } else { + transparent = None; + } + } + _ => (), + }, + None => return None, + } + }; + + if img.image_descriptor.color_table_present() { + Some(Image { + width: img.image_descriptor.width, + height: img.image_descriptor.height, + left_offset: img.image_descriptor.left, + top_offset: img.image_descriptor.top, + palette: &img.local_color_table.as_ref().unwrap(), + transparent_index: transparent, + indicies: &img.indicies, + }) + } else { + Some(Image { + width: img.image_descriptor.width, + height: img.image_descriptor.height, + left_offset: img.image_descriptor.left, + top_offset: img.image_descriptor.top, + palette: self.gif.global_color_table.as_ref().unwrap(), + transparent_index: transparent, + indicies: &img.indicies, + }) + } + } +} + +pub struct FrameIterator<'a> { + gif: &'a Gif, + veciter: std::slice::Iter<'a, Block>, + buffer: Vec<u8>, +} + +pub struct Image<'a> { + pub(crate) width: u16, + pub(crate) height: u16, + left_offset: u16, + top_offset: u16, + pub(crate) palette: &'a ColorTable, + pub(crate) transparent_index: Option<u8>, + pub(crate) indicies: &'a [u8], +} + +impl<'a> Image<'a> { + pub fn width(&self) -> u16 { + self.width + } + + pub fn height(&self) -> u16 { + self.height + } + + pub fn position(&self) -> (u16, u16) { + (self.left_offset, self.top_offset) + } + + pub fn palette(&self) -> &ColorTable { + self.palette + } + + pub fn transparent_index(&self) -> Option<u8> { + self.transparent_index + } + + pub fn indicies(&self) -> &[u8] { + self.indicies + } +} + +pub struct Frame { + width: u16, + height: u16, + palette: ColorTable, + transparent_index: Option<u8>, + indicies: Vec<u8>, + delay_after_draw: u16, + user_input_flag: bool, +} + +#[cfg(test)] +pub mod gif { + use std::convert::TryInto; + use std::io::Write; + + use crate::block::extension::DisposalMethod; + use crate::writer::{GifBuilder, ImageBuilder}; + use crate::Color; + + #[test] + fn to_vec_gif87a() { + let gct = vec![Color::new(1, 2, 3), Color::new(253, 254, 255)]; + let colortable = vec![Color::new(0, 0, 0), Color::new(128, 0, 255)]; + let indicies = vec![0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0]; + + let expected_out = vec![ + 0x47, + 0x49, + 0x46, + 0x38, + 0x37, + 0x61, // Version - GIF87a + 0x04, + 0x00, + 0x04, + 0x00, + 0b1000_0000, + 0x00, + 0x00, // Logical Screen Descriptor + 1, + 2, + 3, + 253, + 254, + 255, // Global Color Table + 0x2C, + 0x00, + 0x00, + 0x00, + 0x00, + 0x04, + 0x00, + 0x04, + 0x00, + 0b1000_0000, // Image Descriptor 1 + 0, + 0, + 0, + 128, + 0, + 255, // Color Table + 0x02, + 0x05, + 0x84, + 0x1D, + 0x81, + 0x7A, + 0x50, + 0x00, // Image Data 1 + 0x2C, + 0x00, + 0x00, + 0x00, + 0x00, + 0x04, + 0x00, + 0x04, + 0x00, + 0b0000_0000, // Image Descriptor 2 + 0x02, + 0x05, + 0x84, + 0x1D, + 0x81, + 0x7A, + 0x50, + 0x00, // Image Data 2 + 0x3B, // Trailer + ]; + + let actual = GifBuilder::new(4, 4) + .palette(gct.try_into().unwrap()) + .image( + ImageBuilder::new(4, 4) + .palette(colortable.try_into().unwrap()) + .indicies(&indicies), + ) + .image(ImageBuilder::new(4, 4).indicies(&indicies)); + + let bytes = actual.build().unwrap().to_vec(); + assert_eq!(bytes, expected_out); + } + + #[test] + fn to_vec_gif89a() { + let gct = vec![Color::new(1, 2, 3), Color::new(253, 254, 255)]; + let colortable = vec![Color::new(0, 0, 0), Color::new(128, 0, 255)]; + let indicies = vec![0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0]; + + let expected_out = vec![ + 71, 73, 70, 56, 57, 97, 4, 0, 4, 0, 128, 0, 0, 1, 2, 3, 253, 254, 255, 33, 249, 4, 8, + 64, 0, 0, 0, 44, 0, 0, 0, 0, 4, 0, 4, 0, 128, 0, 0, 0, 128, 0, 255, 2, 5, 132, 29, 129, + 122, 80, 0, 44, 0, 0, 0, 0, 4, 0, 4, 0, 0, 2, 5, 132, 29, 129, 122, 80, 0, 59, + ]; + + let actual_out = GifBuilder::new(4, 4) + .palette(gct.try_into().unwrap()) + .image( + ImageBuilder::new(4, 4) + .palette(colortable.try_into().unwrap()) + .indicies(&indicies) + .disposal_method(DisposalMethod::RestoreBackground) + .delay(64), + ) + .image(ImageBuilder::new(4, 4).indicies(&indicies)) + .build() + .unwrap() + .to_vec(); + + std::fs::File::create("ah.gif") + .unwrap() + .write_all(&actual_out) + .unwrap(); + std::fs::File::create("ah_hand.gif") + .unwrap() + .write_all(&expected_out) + .unwrap(); + + assert_eq!(actual_out, expected_out); + } +} diff --git a/gifed/src/lib.rs b/gifed/src/lib.rs new file mode 100644 index 0000000..0a11fdc --- /dev/null +++ b/gifed/src/lib.rs @@ -0,0 +1,51 @@ +mod color; +mod colorimage; +mod gif; +mod lzw; + +pub mod block; +pub mod reader; +pub mod writer; + +use core::fmt; +use std::error::Error; + +pub use color::Color; +pub use colorimage::ColorImage; +pub use gif::Gif; +pub use lzw::LZW; + +/// Perform the algorithm to get the length of a color table from +/// the value of the packed field. The max value here is 256 +pub(crate) fn packed_to_color_table_length(packed: u8) -> usize { + 2usize.pow(packed as u32 + 1) +} + +//TODO: Be sure to check that fields in LSD and Img. Desc. that were reserved +//in 87a aren't set if version is 87a, or that we return a warning, etc. Just +//remember about this. +//bottom of page 24 in 89a + +#[derive(Clone, Copy, Debug)] +pub enum EncodingError { + TooManyColors, + NoColorTable, + IndicieSizeMismatch { expected: usize, got: usize }, +} + +impl fmt::Display for EncodingError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TooManyColors => write!(f, "A palette is limited to 256 colors"), + Self::NoColorTable => write!( + f, + "Refusing to set the background color index when no color table is set!" + ), + Self::IndicieSizeMismatch { expected, got } => { + write!(f, "Expected to have {} indicies but got {}", expected, got) + } + } + } +} + +impl Error for EncodingError {} diff --git a/gifed/src/lzw.rs b/gifed/src/lzw.rs new file mode 100644 index 0000000..dce6a5d --- /dev/null +++ b/gifed/src/lzw.rs @@ -0,0 +1,173 @@ +use std::collections::HashMap; + +pub struct LZW {} +impl LZW { + pub fn encode(minimum_size: u8, indicies: &[u8]) -> Vec<u8> { + let mut dictionary: HashMap<Vec<u8>, u16> = HashMap::new(); + + let cc = 2u16.pow(minimum_size as u32); + let eoi = cc + 1; + + // Fill dictionary with self-descriptive values + for value in 0..cc { + dictionary.insert(vec![value as u8], value); + } + + let mut next_code = eoi + 1; + let mut code_size = minimum_size + 1; + + let mut iter = indicies.into_iter(); + let mut out = BitStream::new(); + let mut buffer = vec![*iter.next().unwrap()]; + + out.push_bits(code_size, cc); + + for &indicie in iter { + buffer.push(indicie); + + if !dictionary.contains_key(&buffer) { + buffer.pop(); + + if let Some(&code) = dictionary.get(&buffer) { + out.push_bits(code_size, code); + + // Put the code back and add the vec to the dict + buffer.push(indicie); + dictionary.insert(buffer.clone(), next_code); + next_code += 1; + + // If the next_code can't fit in the code_size, we have to increase it + if next_code - 1 == 2u16.pow(code_size as u32) { + code_size += 1; + } + + buffer.clear(); + buffer.push(indicie); + } else { + unreachable!() + } + } + } + + if buffer.len() > 0 { + match dictionary.get(&buffer) { + Some(&code) => out.push_bits(code_size, code), + None => { + panic!("Codes left in the buffer but the buffer is not a valid dictionary key!") + } + } + } + out.push_bits(code_size, eoi); + + out.vec() + } +} + +#[cfg(test)] +mod lzw_test { + use super::*; + + #[test] + fn encode() { + let indicies = vec![0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0]; + let output = vec![0x84, 0x1D, 0x81, 0x7A, 0x50]; + + let lzout = LZW::encode(2, &indicies); + + assert_eq!(lzout, output); + } + + #[test] + fn against_weezl() { + let indicies = vec![0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0]; + let weezl = weezl::encode::Encoder::new(weezl::BitOrder::Lsb, 2) + .encode(&indicies) + .unwrap(); + let us = LZW::encode(2, &indicies); + + assert_eq!(weezl, us); + } +} + +struct BitStream { + formed: Vec<u8>, + current: u8, + index: u8, +} + +impl BitStream { + fn new() -> Self { + Self { + formed: vec![], + current: 0, + index: 0, + } + } + + fn push_bits(&mut self, count: u8, data: u16) { + let mut new_index = self.index + count; + let mut current32 = (self.current as u32) | ((data as u32) << self.index); + + loop { + if new_index >= 8 { + self.formed.push(current32 as u8); + current32 = current32 >> 8; + new_index -= 8; + } else { + self.current = current32 as u8; + self.index = new_index; + + break; + } + } + } + + fn vec(self) -> Vec<u8> { + let mut out = self.formed; + + if self.index != 0 { + out.push(self.current); + } + + out + } +} + +#[cfg(test)] +mod bitstream_test { + use super::*; + + #[test] + fn short_push() { + let mut bs = BitStream::new(); + bs.push_bits(2, 3); + bs.push_bits(2, 3); + bs.push_bits(3, 1); + bs.push_bits(2, 3); + + let bsvec = bs.vec(); + + for byte in &bsvec { + print!("{:b} ", byte); + } + println!(""); + + assert_eq!(bsvec, vec![0b1001_1111, 0b0000_0001]); + } + + #[test] + fn long_push() { + let mut bs = BitStream::new(); + bs.push_bits(1, 1); + bs.push_bits(12, 2049); + + let bsvec = bs.vec(); + + for byte in &bsvec { + print!("{:b} ", byte); + } + println!(""); + + assert_eq!(bsvec, vec![0b0000_0011, 0b0001_0000]); + } +} diff --git a/gifed/src/reader/mod.rs b/gifed/src/reader/mod.rs new file mode 100644 index 0000000..41494df --- /dev/null +++ b/gifed/src/reader/mod.rs @@ -0,0 +1,274 @@ +use std::{ + borrow::Cow, + convert::{TryFrom, TryInto}, + error::Error, + fmt, + fs::File, + io::{BufRead, BufReader, Read}, + path::Path, +}; + +use crate::{ + block::{ + extension::{Application, Extension, GraphicControl}, + Block, ColorTable, CompressedImage, ImageDescriptor, IndexedImage, ScreenDescriptor, + Version, + }, + color, Gif, +}; + +pub struct GifReader {} + +impl GifReader { + pub fn file<P: AsRef<Path>>(path: P) -> Result<Gif, DecodingError> { + let mut file = File::open(path)?; + let mut reader = SmartReader { + inner: vec![], + position: 0, + }; + file.read_to_end(&mut reader.inner)?; + + let mut gif = Self::read_required(&mut reader)?; + + if gif.screen_descriptor.color_table_present() { + let gct_size = gif.screen_descriptor.color_table_len() * 3; + gif.global_color_table = Some(Self::read_color_table(&mut reader, gct_size)?); + } + + loop { + match Self::read_block(&mut reader)? { + Some(block) => gif.blocks.push(block), + None => return Ok(gif), + } + } + } + + fn read_required(reader: &mut SmartReader) -> Result<Gif, DecodingError> { + let version = match reader.take_lossy_utf8(6).as_deref() { + Some("GIF87a") => Version::Gif87a, + Some("GIF89a") => Version::Gif89a, + _ => return Err(DecodingError::UnknownVersionString), + }; + + let mut lsd_buffer: [u8; 7] = [0; 7]; + reader + .read_exact(&mut lsd_buffer) + .ok_or(DecodingError::UnexpectedEof)?; + + let lsd = ScreenDescriptor::from(lsd_buffer); + + Ok(Gif { + header: version, + screen_descriptor: lsd, + global_color_table: None, + blocks: vec![], + }) + } + + fn read_color_table( + reader: &mut SmartReader, + size: usize, + ) -> Result<ColorTable, DecodingError> { + let buffer = reader + .take(size as usize) + .ok_or(DecodingError::UnexpectedEof)?; + + // We get the size from the screen descriptor. This should never return Err + Ok(ColorTable::try_from(&buffer[..]).unwrap()) + } + + fn read_block(reader: &mut SmartReader) -> Result<Option<Block>, DecodingError> { + let block_id = reader.u8().ok_or(DecodingError::UnexpectedEof)?; + + //TODO: remove panic + match block_id { + 0x21 => Self::read_extension(reader).map(|block| Some(block)), + 0x2C => Self::read_image(reader).map(|block| Some(block)), + 0x3B => Ok(None), + _ => panic!( + "Unknown block identifier {:X} {:X}", + block_id, reader.position + ), + } + } + + fn read_extension(reader: &mut SmartReader) -> Result<Block, DecodingError> { + let extension_id = reader.u8().expect("File ended early"); + + match extension_id { + 0xF9 => { + reader.skip(1); // Skip block length, we know it + let mut data = [0u8; 4]; + reader + .read_exact(&mut data) + .ok_or(DecodingError::UnexpectedEof)?; + reader.skip(1); // Skip block terminator + + Ok(Block::Extension(Extension::GraphicControl( + GraphicControl::from(data), + ))) + } + 0xFE => Ok(Block::Extension(Extension::Comment( + reader.take_and_collapse_subblocks(), + ))), + 0x01 => todo!(), //TODO: do; plain text extension + 0xFF => { + //TODO: error instead of unwraps + assert_eq!(Some(11), reader.u8()); + let identifier = reader.take_lossy_utf8(8).unwrap().to_string(); + let authentication_code: [u8; 3] = + TryInto::try_into(reader.take(3).unwrap()).unwrap(); + let data = reader.take_and_collapse_subblocks(); + + Ok(Block::Extension(Extension::Application(Application { + identifier, + authentication_code, + data, + }))) + } + _ => panic!("Unknown Extension Identifier!"), + } + } + + fn read_image(mut reader: &mut SmartReader) -> Result<Block, DecodingError> { + let mut buffer = [0u8; 9]; + reader + .read_exact(&mut buffer) + .ok_or(DecodingError::UnexpectedEof)?; + let descriptor = ImageDescriptor::from(buffer); + + let color_table = if descriptor.color_table_present() { + let size = descriptor.color_table_size() * 3; + Some(Self::read_color_table(&mut reader, size)?) + } else { + None + }; + + let lzw_csize = reader.u8().ok_or(DecodingError::UnexpectedEof)?; + + let compressed_data = reader.take_and_collapse_subblocks(); + + let mut decompress = weezl::decode::Decoder::new(weezl::BitOrder::Lsb, lzw_csize); + //TODO: remove unwrap + let mut decompressed_data = decompress.decode(&compressed_data).unwrap(); + + Ok(Block::IndexedImage(IndexedImage { + image_descriptor: descriptor, + local_color_table: color_table, + indicies: decompressed_data, + })) + } +} + +#[derive(Debug)] +pub enum DecodingError { + IoError(std::io::Error), + UnknownVersionString, + UnexpectedEof, + ColorIndexOutOfBounds, +} + +impl Error for DecodingError {} +impl fmt::Display for DecodingError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DecodingError::IoError(error) => write!(f, "{}", error), + DecodingError::UnknownVersionString => { + write!(f, "File did not start with a valid header") + } + DecodingError::UnexpectedEof => { + write!(f, "Found the end of the data at a weird spot") + } + DecodingError::ColorIndexOutOfBounds => { + write!( + f, + "The image contained an index not found in the color table" + ) + } + } + } +} + +impl From<std::io::Error> for DecodingError { + fn from(ioerror: std::io::Error) -> Self { + DecodingError::IoError(ioerror) + } +} + +struct SmartReader { + inner: Vec<u8>, + position: usize, +} + +impl SmartReader { + pub fn u8(&mut self) -> Option<u8> { + self.position += 1; + self.inner.get(self.position - 1).map(|b| *b) + } + + pub fn u16(&mut self) -> Option<u16> { + self.position += 2; + self.inner + .get(self.position - 2..self.position) + .map(|bytes| u16::from_le_bytes(bytes.try_into().unwrap())) + } + + pub fn skip(&mut self, size: usize) { + self.position += size; + } + + pub fn take(&mut self, size: usize) -> Option<&[u8]> { + self.position += size; + self.inner.get(self.position - size..self.position) + } + + //TODO: Result not Option when buffer len + pub fn read_exact(&mut self, buf: &mut [u8]) -> Option<()> { + if self.position + buf.len() > self.inner.len() { + None + } else { + self.position += buf.len(); + buf.copy_from_slice(&self.inner[self.position - buf.len()..self.position]); + Some(()) + } + } + + pub fn take_vec(&mut self, size: usize) -> Option<Vec<u8>> { + self.position += size; + self.inner + .get(self.position - size..self.position) + .map(|bytes| bytes.to_vec()) + } + + pub fn take_lossy_utf8(&mut self, size: usize) -> Option<Cow<'_, str>> { + self.take(size).map(|bytes| String::from_utf8_lossy(bytes)) + } + + pub fn take_data_subblocks(&mut self) -> Vec<Vec<u8>> { + let mut blocks = vec![]; + + loop { + let block_size = self.u8().expect("Failed to read length of sublock"); + + if block_size == 0 { + return blocks; + } + + let block = self + .take_vec(block_size as usize) + .expect("Failed to read sublock"); + + blocks.push(block); + } + } + + pub fn take_and_collapse_subblocks(&mut self) -> Vec<u8> { + let blocks = self.take_data_subblocks(); + let mut ret = vec![]; + for block in blocks { + ret.extend_from_slice(&block) + } + + ret + } +} diff --git a/gifed/src/writer/gifbuilder.rs b/gifed/src/writer/gifbuilder.rs new file mode 100644 index 0000000..57a62e3 --- /dev/null +++ b/gifed/src/writer/gifbuilder.rs @@ -0,0 +1,106 @@ +use std::convert::TryInto; + +use crate::block::{extension::Extension, Block, ColorTable, ScreenDescriptor, Version}; +use crate::writer::ImageBuilder; +use crate::{EncodingError, Gif}; + +pub struct GifBuilder { + version: Version, + width: u16, + height: u16, + background_color_index: u8, + global_color_table: Option<ColorTable>, + blocks: Vec<Block>, + error: Option<EncodingError>, +} + +impl GifBuilder { + pub fn new(width: u16, height: u16) -> Self { + Self { + version: Version::Gif87a, + width, + height, + background_color_index: 0, + global_color_table: None, + blocks: vec![], + error: None, + } + } + + pub fn palette(mut self, palette: ColorTable) -> Self { + self.global_color_table = Some(palette); + self + } + + pub fn background_index(mut self, ind: u8) -> Self { + if self.error.is_some() { + return self; + } + + if self.global_color_table.is_none() { + self.error = Some(EncodingError::NoColorTable); + } else { + self.background_color_index = ind; + } + self + } + + pub fn image(mut self, ib: ImageBuilder) -> Self { + if self.error.is_some() { + return self; + } + + if ib.required_version() == Version::Gif89a { + self.version = Version::Gif89a; + } + + if let Some(gce) = ib.get_graphic_control() { + self.blocks.push(Block::Extension(gce.into())); + } + + match ib.build() { + Ok(image) => self.blocks.push(Block::IndexedImage(image)), + Err(e) => self.error = Some(e), + } + + self + } + + /*pub fn extension(mut self, ext: Extension) -> Self { + self.blocks.push(Block::Extension(ext)); + self + }*/ + + pub fn repeat(mut self, count: u16) -> Self { + self.blocks + .push(Block::Extension(Extension::Looping(count))); + self + } + + pub fn build(self) -> Result<Gif, EncodingError> { + if let Some(error) = self.error { + return Err(error); + } + + let mut lsd = ScreenDescriptor { + width: self.width, + height: self.height, + packed: 0, // Set later + background_color_index: self.background_color_index, + pixel_aspect_ratio: 0, //TODO: Allow configuring + }; + + if let Some(gct) = &self.global_color_table { + println!("build {}", gct.len()); + lsd.set_color_table_present(true); + lsd.set_color_table_size((gct.len() - 1) as u8); + } + + Ok(Gif { + header: self.version, + screen_descriptor: lsd, + global_color_table: self.global_color_table, + blocks: self.blocks, + }) + } +} diff --git a/gifed/src/writer/imagebuilder.rs b/gifed/src/writer/imagebuilder.rs new file mode 100644 index 0000000..f5c9e2b --- /dev/null +++ b/gifed/src/writer/imagebuilder.rs @@ -0,0 +1,133 @@ +use crate::{ + block::{ + extension::{DisposalMethod, GraphicControl}, + ColorTable, ImageDescriptor, IndexedImage, Version, + }, + EncodingError, +}; + +pub struct ImageBuilder { + left_offset: u16, + top_offset: u16, + width: u16, + height: u16, + color_table: Option<ColorTable>, + + delay: u16, + disposal_method: DisposalMethod, + transparent_index: Option<u8>, + + indicies: Vec<u8>, +} + +impl ImageBuilder { + pub fn new(width: u16, height: u16) -> Self { + Self { + left_offset: 0, + top_offset: 0, + width, + height, + color_table: None, + delay: 0, + disposal_method: DisposalMethod::NoAction, + transparent_index: None, + indicies: vec![], + } + } + + pub fn offset(mut self, left: u16, top: u16) -> Self { + self.left_offset = left; + self.top_offset = top; + self + } + + pub fn palette(mut self, table: ColorTable) -> Self { + self.color_table = Some(table); + self + } + + /// Time to wait, in hundreths of a second, before this image is drawn + pub fn delay(mut self, hundreths: u16) -> Self { + self.delay = hundreths; + self + } + + pub fn disposal_method(mut self, method: DisposalMethod) -> Self { + self.disposal_method = method; + self + } + + pub fn transparent_index(mut self, index: Option<u8>) -> Self { + self.transparent_index = index; + self + } + + pub fn required_version(&self) -> Version { + if self.delay > 0 + || self.disposal_method != DisposalMethod::NoAction + || self.transparent_index.is_some() + { + Version::Gif89a + } else { + Version::Gif87a + } + } + + pub fn get_graphic_control(&self) -> Option<GraphicControl> { + if self.required_version() == Version::Gif89a { + if let Some(transindex) = self.transparent_index { + Some(GraphicControl::new( + self.disposal_method, + false, + true, + self.delay, + transindex, + )) + } else { + Some(GraphicControl::new( + self.disposal_method, + false, + false, + self.delay, + 0, + )) + } + } else { + None + } + } + + pub fn indicies(mut self, indicies: &[u8]) -> Self { + self.indicies = indicies.to_vec(); + self + } + + pub fn build(self) -> Result<IndexedImage, EncodingError> { + let expected_len = self.width as usize * self.height as usize; + if self.indicies.len() != expected_len { + return Err(EncodingError::IndicieSizeMismatch { + expected: expected_len, + got: self.indicies.len(), + }); + } + + let mut imgdesc = ImageDescriptor { + left: self.left_offset, + top: self.top_offset, + width: self.width, + height: self.height, + packed: 0, // Set later + }; + + if let Some(lct) = &self.color_table { + imgdesc.set_color_table_present(true); + imgdesc.set_color_table_size(lct.packed_len()); + } + + Ok(IndexedImage { + image_descriptor: imgdesc, + local_color_table: self.color_table, + indicies: self.indicies, + }) + } +} diff --git a/gifed/src/writer/mod.rs b/gifed/src/writer/mod.rs new file mode 100644 index 0000000..88311fc --- /dev/null +++ b/gifed/src/writer/mod.rs @@ -0,0 +1,5 @@ +mod gifbuilder; +mod imagebuilder; + +pub use gifbuilder::GifBuilder; +pub use imagebuilder::ImageBuilder; |