diff --git a/embedded-graphics/Cargo.toml b/embedded-graphics/Cargo.toml index 10956d4d2..fcc37458c 100644 --- a/embedded-graphics/Cargo.toml +++ b/embedded-graphics/Cargo.toml @@ -37,4 +37,4 @@ fixed_point = [ "fixed" ] [dev-dependencies] arrayvec = { version = "0.5.1", default-features = false } -tinytga = { version = "0.3.2", features = [ "graphics" ] } +tinytga = { version = "0.3.2" } diff --git a/embedded-graphics/src/image/image_drawable.rs b/embedded-graphics/src/image/image_drawable.rs index 1ff04598e..f53e3b225 100644 --- a/embedded-graphics/src/image/image_drawable.rs +++ b/embedded-graphics/src/image/image_drawable.rs @@ -71,13 +71,13 @@ pub trait ImageDrawableExt: Sized { /// display, with their top-left corners positioned at `(100, 100)` and `(100, 140)`. /// /// ```rust - /// use embedded_graphics::{image::Image, pixelcolor::Rgb565, prelude::*, primitives::Rectangle}; + /// use embedded_graphics::{image::Image, pixelcolor::Rgb888, prelude::*, primitives::Rectangle}; /// # use embedded_graphics::mock_display::MockDisplay as Display; /// use tinytga::Tga; /// - /// let mut display: Display = Display::default(); + /// let mut display: Display = Display::default(); /// - /// let sprite_atlas: Tga = Tga::from_slice(include_bytes!( + /// let sprite_atlas: Tga = Tga::from_slice(include_bytes!( /// "../../../assets/tiles.tga" /// )) /// .unwrap(); diff --git a/embedded-graphics/src/image/mod.rs b/embedded-graphics/src/image/mod.rs index 9af19b1be..0100d6899 100644 --- a/embedded-graphics/src/image/mod.rs +++ b/embedded-graphics/src/image/mod.rs @@ -20,16 +20,16 @@ //! with embedded-graphics. //! //! ```rust -//! use embedded_graphics::{image::Image, pixelcolor::Rgb565, prelude::*}; +//! use embedded_graphics::{image::Image, pixelcolor::Rgb888, prelude::*}; //! # use embedded_graphics::mock_display::MockDisplay as Display; //! use tinytga::Tga; //! -//! let mut display: Display = Display::default(); +//! let mut display: Display = Display::default(); //! //! // Load the TGA file. //! // Note that the color type is set explicitly to match the format used in the TGA file, //! // otherwise the compiler might infer an incorrect type. -//! let tga: Tga = Tga::from_slice(include_bytes!( +//! let tga: Tga = Tga::from_slice(include_bytes!( //! "../../../simulator/examples/assets/rust-pride.tga" //! )) //! .unwrap(); @@ -51,16 +51,16 @@ //! which this example takes advantage of. //! //! ```rust -//! use embedded_graphics::{image::Image, pixelcolor::Rgb565, prelude::*, primitives::Rectangle}; +//! use embedded_graphics::{image::Image, pixelcolor::Rgb888, prelude::*, primitives::Rectangle}; //! # use embedded_graphics::mock_display::MockDisplay as Display; //! use tinytga::Tga; //! -//! let mut display: Display = Display::default(); +//! let mut display: Display = Display::default(); //! //! // Load the TGA file with the sprite atlas. //! // Note that the color type is set explicitly to match the format used in the TGA file, //! // otherwise the compiler might infer an incorrect type. -//! let sprite_atlas: Tga = Tga::from_slice(include_bytes!( +//! let sprite_atlas: Tga = Tga::from_slice(include_bytes!( //! "../../../assets/tiles.tga" //! )) //! .unwrap(); diff --git a/simulator/Cargo.toml b/simulator/Cargo.toml index 0a0f12d95..e591f7f70 100644 --- a/simulator/Cargo.toml +++ b/simulator/Cargo.toml @@ -40,7 +40,7 @@ name = "contains" [dependencies] image = "0.23.0" -tinytga = { version = "0.3.2", features = [ "graphics" ] } +tinytga = { version = "0.3.2" } [dependencies.sdl2] version = "0.32.2" diff --git a/tinytga/CHANGELOG.md b/tinytga/CHANGELOG.md index c4bf5971f..c2ac64e1d 100644 --- a/tinytga/CHANGELOG.md +++ b/tinytga/CHANGELOG.md @@ -9,16 +9,24 @@ ### Changed - **(breaking)** [#407](https://github.com/jamwaffles/embedded-graphics/pull/407) The `image_descriptor` in `TgaHeader` was replaced by `image_origin` and `alpha_channel_bits`. -- **(breaking)** [#407](https://github.com/jamwaffles/embedded-graphics/pull/407) The `Pixel` type returned by `TgaIterator` now uses `u16` coordinates. -- **(breaking)** [#420](https://github.com/jamwaffles/embedded-graphics/pull/420) To support the new embedded-graphics 0.7 image API a color type parameter was added to `Tga`. To use this crate without the `graphics` flag enabled replace `Tga` by `TgaRaw`. +- **(breaking)** [#420](https://github.com/jamwaffles/embedded-graphics/pull/420) To support the new embedded-graphics 0.7 image API a color type parameter was added to `Tga`. +- **(breaking)** [#430](https://github.com/jamwaffles/embedded-graphics/pull/430) The `graphics` feature was removed and the `embedded-graphics` dependency is now non optional. +- **(breaking)** [#430](https://github.com/jamwaffles/embedded-graphics/pull/430) `Tga` no longer implements `IntoIterator`. Pixel iterators can now be created using the `pixels` and `raw_pixels` methods. +- **(breaking)** [#430](https://github.com/jamwaffles/embedded-graphics/pull/430) `Tga::from_slice` now checks that the specified color type matches the bit depth of the image. +- **(breaking)** [#430](https://github.com/jamwaffles/embedded-graphics/pull/430) The `TgaFooter` struct was replaced by the `raw_developer_dictionary` and `raw_extension_area` methods in `Tga`. +- **(breaking)** [#430](https://github.com/jamwaffles/embedded-graphics/pull/430) `Tga::width` and `Tga::height` were replaced by `Tga::size` which requires `embedded_graphics::geometry::OriginDimensions` to be in scope (also included in the embedded-graphics `prelude`). +- **(breaking)** [#430](https://github.com/jamwaffles/embedded-graphics/pull/430) The color map can now be accessed using the new `ColorMap` type. ### Added - [#407](https://github.com/jamwaffles/embedded-graphics/pull/407) Added support for bottom-left origin images to `TgaIterator`. +- [#430](https://github.com/jamwaffles/embedded-graphics/pull/430) The image ID can now be accessed using `Tga::image_id`. ### Fixed - [#407](https://github.com/jamwaffles/embedded-graphics/pull/407) Additional data in `pixel_data`, beyond `width * height` pixels, is now discarded by `TgaIterator`. +- [#430](https://github.com/jamwaffles/embedded-graphics/pull/430) Images with unsupported BPP values in the header no longer cause panics. Instead an error is returned by `from_slice`. +- [#430](https://github.com/jamwaffles/embedded-graphics/pull/430) Errors during the execution of a pixel iterator no longer cause panics. Instead the corrupted portion of the image is filled with black pixels. ## [0.3.2] - 2020-03-20 diff --git a/tinytga/Cargo.toml b/tinytga/Cargo.toml index d7f67a887..ca62d9211 100644 --- a/tinytga/Cargo.toml +++ b/tinytga/Cargo.toml @@ -18,17 +18,9 @@ exclude = [ [badges] circle-ci = { repository = "jamwaffles/embedded-graphics", branch = "master" } -[[test]] -name = "embedded_graphics" -required-features = ["graphics"] - [dependencies.nom] version = "5.1.0" default-features = false [dependencies.embedded-graphics] version = "0.6.0" -optional = true - -[features] -graphics = ["embedded-graphics"] diff --git a/tinytga/README.md b/tinytga/README.md index 9b5bbb08e..5ddbfa685 100644 --- a/tinytga/README.md +++ b/tinytga/README.md @@ -7,81 +7,96 @@ ## [Documentation](https://docs.rs/tinytga) -A small TGA parser designed for embedded, no-std environments but usable anywhere. Beyond -parsing the image header, no other allocations are made. +A small TGA parser designed for use with [embedded-graphics] targetting no-std environments but +usable anywhere. Beyond parsing the image header, no other allocations are made. -To access the individual pixels in an image, the `TgaRaw` struct implements `IntoIterator`. It is -also possible to access the unaltered raw image data by reading the `pixel_data` field. This -data will need to be interpreted according to the `image_type` specified in the header. - -## Features - -* `graphics` - enables [embedded-graphics] integration. +tinytga provides two methods of accessing the pixel data inside a TGA file. The most convenient +way is to use a color type provided by [embedded-graphics] to define the format stored inside +the TGA file. But it is also possible to directly access the raw pixel representation instead. ## Examples ### Load a Run Length Encoded (RLE) TGA image ```rust -use tinytga::{ImageOrigin, ImageType, Pixel, TgaRaw, TgaFooter, TgaHeader}; +use embedded_graphics::{prelude::*, pixelcolor::Rgb888}; +use tinytga::{Bpp, ImageOrigin, ImageType, RawPixel, Tga, TgaHeader}; + +// Include an image from a local path as bytes +let data = include_bytes!("../tests/chessboard_4px_rle.tga"); + +// Create a TGA instance from a byte slice. +// The color type is set by defining the type of the `img` variable. +let img: Tga = Tga::from_slice(data).unwrap(); + +// Check the size of the image. +assert_eq!(img.size(), Size::new(4, 4)); + +// Collect pixels into a vector. +let pixels: Vec<_> = img.pixels().collect(); +``` + +### Drawing an image using `embedded-graphics` + +This example demonstrates how a TGA image can be drawn to a [embedded-graphics] draw target. + +```rust +use embedded_graphics::{image::Image, pixelcolor::Rgb888, prelude::*}; +use tinytga::Tga; // Include an image from a local path as bytes let data = include_bytes!("../tests/chessboard_4px_rle.tga"); +let tga: Tga = Tga::from_slice(data).unwrap(); + +let image = Image::new(&tga, Point::zero()); + +image.draw(&mut display)?; +``` + +### Accessing raw pixel data + +If you do not want to use the color types provided by [embedded-graphics] you can also access +the raw image data. + +```rust +use embedded_graphics::{prelude::*, pixelcolor::Rgb888}; +use tinytga::{Bpp, ImageOrigin, ImageType, RawPixel, Tga, TgaHeader}; + +// Include an image from a local path as bytes. +let data = include_bytes!("../tests/chessboard_4px_rle.tga"); -// Create a TGA instance from a byte slice -let img = TgaRaw::from_slice(data).unwrap(); +// Create a TGA instance from a byte slice. +let img = Tga::from_slice_raw(data).unwrap(); -// Take a look at the header +// Take a look at the raw image header. assert_eq!( - img.header, + img.raw_header(), TgaHeader { id_len: 0, has_color_map: false, image_type: ImageType::RleTruecolor, color_map_start: 0, color_map_len: 0, - color_map_depth: 0, + color_map_depth: None, x_origin: 0, y_origin: 4, width: 4, height: 4, - pixel_depth: 24, + pixel_depth: Bpp::Bits24, image_origin: ImageOrigin::TopLeft, alpha_channel_depth: 0, } ); -// Take a look at the footer -assert_eq!( - img.footer, - Some(TgaFooter { - extension_area_offset: 0, - developer_directory_offset: 0 - }) -); - -// Collect pixels into a `Vec` -let pixels = img.into_iter().collect::>(); +// Collect raw pixels into a vector. +let pixels: Vec<_> = img.raw_pixels().collect(); ``` -### Use with `embedded-graphics` - -This example demonstrates [embedded-graphics] support by rendering a TGA image to a mock -display. +## Embedded-graphics drawing performance -The `graphics` feature of `tinytga` needs to be enabled in `Cargo.toml` to use the `Tga` object -with embedded-graphics. - -```rust -use embedded_graphics::{image::Image, pixelcolor::Rgb888, prelude::*}; -use tinytga::Tga; - -let tga: Tga = Tga::from_slice(include_bytes!("../tests/rust-rle-bw-topleft.tga")).unwrap(); - -let image = Image::new(&tga, Point::zero()); - -image.draw(&mut display)?; -``` +`tinytga` uses different code paths to draw images with different `ImageOrigin` . +The performance difference between the origins will depend on the display driver, but using +images with the origin at the top left corner will generally result in the best performance. [embedded-graphics]: https://docs.rs/embedded-graphics diff --git a/tinytga/src/color_map.rs b/tinytga/src/color_map.rs new file mode 100644 index 000000000..cea1b2ed9 --- /dev/null +++ b/tinytga/src/color_map.rs @@ -0,0 +1,75 @@ +use crate::{parse_error::ParseError, Bpp, TgaHeader}; +use nom::bytes::complete::take; + +/// Color map. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct ColorMap<'a> { + /// First color index. + start_index: u16, + /// Number of entries. + length: u16, + /// Entry bit depth. + entry_bpp: Bpp, + /// Color map data. + data: &'a [u8], +} + +impl<'a> ColorMap<'a> { + pub(crate) fn parse( + input: &'a [u8], + header: &TgaHeader, + ) -> Result<(&'a [u8], Option), ParseError> { + if !header.has_color_map { + return Ok((input, None)); + } + + let entry_bpp = header.color_map_depth.ok_or(ParseError::ColorMap)?; + + let length = usize::from(header.color_map_len) * usize::from(entry_bpp.bytes()); + + let (input, color_map_data) = + take(length)(input).map_err(|_: nom::Err<()>| ParseError::ColorMap)?; + + Ok(( + input, + Some(Self { + start_index: header.color_map_start, + length: header.color_map_len, + entry_bpp, + data: color_map_data, + }), + )) + } + + /// Returns the bit depth for the entries in the color map. + pub fn entry_bpp(&self) -> Bpp { + self.entry_bpp + } + + /// Returns the raw color value for a color map entry. + pub fn get_raw(&self, index: usize) -> Option { + //TODO: use start_index + if index >= usize::from(self.length) { + return None; + } + + let start = index * usize::from(self.entry_bpp.bytes()); + + Some(match self.entry_bpp { + Bpp::Bits8 => self.data[start] as u32, + Bpp::Bits16 => u32::from_le_bytes([self.data[start], self.data[start + 1], 0, 0]), + Bpp::Bits24 => u32::from_le_bytes([ + self.data[start], + self.data[start + 1], + self.data[start + 2], + 0, + ]), + Bpp::Bits32 => u32::from_le_bytes([ + self.data[start], + self.data[start + 1], + self.data[start + 2], + self.data[start + 3], + ]), + }) + } +} diff --git a/tinytga/src/embedded_graphics.rs b/tinytga/src/embedded_graphics.rs deleted file mode 100644 index e53d11de4..000000000 --- a/tinytga/src/embedded_graphics.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::{parse_error::ParseError, ImageOrigin, TgaRaw}; -use core::marker::PhantomData; -use embedded_graphics::prelude::*; - -/// TGA image for use with embedded-graphics. -/// -/// # Performance -/// -/// `tinytga` uses different code paths to draw images with different [`ImageOrigin`]s. -/// The performance difference between the origins will depend on the display driver, but using -/// images with the origin at the top left corner will generally result in the best performance. -/// -/// [`ImageOrigin`]: enum.ImageOrigin.html -#[derive(Debug)] -pub struct Tga<'a, C> { - tga: TgaRaw<'a>, - color_type: PhantomData, -} - -impl<'a, C> Tga<'a, C> { - /// Parse a TGA image from a byte slice - pub fn from_slice(data: &'a [u8]) -> Result { - Ok(Self { - tga: TgaRaw::from_slice(data)?, - color_type: PhantomData, - }) - } -} - -impl ImageDrawable for Tga<'_, C> -where - C: PixelColor + From<::Raw>, -{ - type Color = C; - - fn draw(&self, target: &mut D) -> Result<(), D::Error> - where - D: DrawTarget, - { - // TGA files with the origin in the top left corner can be drawn using `fill_contiguous`. - // All other origins are drawn by falling back to `draw_iter`. - if self.tga.header.image_origin == ImageOrigin::TopLeft { - target.fill_contiguous( - &self.bounding_box(), - self.tga - .into_iter() - .map(|p| C::Raw::from_u32(p.color).into()), - ) - } else { - target.draw_iter(self.tga.into_iter().map(|p| { - Pixel( - Point::new(i32::from(p.x), i32::from(p.y)), - C::Raw::from_u32(p.color).into(), - ) - })) - } - } -} - -impl OriginDimensions for Tga<'_, C> { - fn size(&self) -> Size { - Size::new(self.tga.width().into(), self.tga.height().into()) - } -} diff --git a/tinytga/src/footer.rs b/tinytga/src/footer.rs index d9463475a..c3feb5266 100644 --- a/tinytga/src/footer.rs +++ b/tinytga/src/footer.rs @@ -1,26 +1,101 @@ -use nom::{bytes::complete::tag, number::complete::le_u32, IResult}; +use core::num::NonZeroUsize; +use nom::{bytes::complete::tag, combinator::map, number::complete::le_u32, IResult, Needed}; /// TGA footer length in bytes -pub const FOOTER_LEN: usize = 26; +const TGA_FOOTER_LENGTH: usize = 26; /// TGA footer structure, referenced from #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] -pub struct TgaFooter { - /// Extension area byte offset from beginning of file - pub extension_area_offset: u32, +pub(crate) struct TgaFooter { + /// Footer start offset + footer_start: usize, - /// Developer directory area byte offset from beginning of file - pub developer_directory_offset: u32, + /// Extension area offset + extension_area_offset: Option, + + /// Developer directory + developer_directory_offset: Option, +} + +impl TgaFooter { + /// Parses the TGA footer. + /// + /// Returns `None` if the file doesn't contain a valid footer. + pub fn parse(image_data: &[u8]) -> Option { + parse_footer(image_data).ok().map(|(_, footer)| footer) + } + + /// Returns the length of the footer section of the TGA file. + /// + /// The length includes the footer, extension area and developer directory. + pub fn length(&self, image_data: &[u8]) -> usize { + let mut length = TGA_FOOTER_LENGTH; + + if let Some(offset) = self.extension_area_offset { + length = length.max(image_data.len() - offset.get()); + } + + if let Some(offset) = self.developer_directory_offset { + length = length.max(image_data.len() - offset.get()); + } + + length + } + + /// Returns the extension area. + /// + /// Returns `None` if the file doesn't contain an extension area. + pub fn extension_area<'a>(&self, image_data: &'a [u8]) -> Option<&'a [u8]> { + self.extension_area_offset + .map(NonZeroUsize::get) + .and_then(|start| { + let end = self + .developer_directory_offset + .map(NonZeroUsize::get) + .filter(|offset| *offset > start) + .unwrap_or(self.footer_start); + + image_data.get(start..end) + }) + } + + /// Returns the developer directory. + /// + /// Returns `None` if the file doesn't contain a developer directory. + pub fn developer_directory<'a>(&self, image_data: &'a [u8]) -> Option<&'a [u8]> { + self.developer_directory_offset + .map(NonZeroUsize::get) + .and_then(|start| { + let end = self + .extension_area_offset + .map(NonZeroUsize::get) + .filter(|offset| *offset > start) + .unwrap_or(self.footer_start); + + image_data.get(start..end) + }) + } } -pub fn footer(input: &[u8]) -> IResult<&[u8], TgaFooter> { - let (input, extension_area_offset) = le_u32(input)?; - let (input, developer_directory_offset) = le_u32(input)?; - let (input, _) = tag("TRUEVISION-XFILE.")(input)?; +fn offset(input: &[u8]) -> IResult<&[u8], Option> { + map(le_u32, |offset| NonZeroUsize::new(offset as usize))(input) +} + +fn parse_footer<'a>(input: &'a [u8]) -> IResult<&[u8], TgaFooter> { + let footer_start = input + .len() + .checked_sub(TGA_FOOTER_LENGTH) + .ok_or(nom::Err::Incomplete(Needed::Size(TGA_FOOTER_LENGTH)))?; + let input = &input[footer_start..input.len()]; + + let (input, extension_area_offset) = offset(input)?; + let (input, developer_directory_offset) = offset(input)?; + let (input, _) = tag("TRUEVISION-XFILE.\0")(input)?; Ok(( input, TgaFooter { + footer_start, extension_area_offset, developer_directory_offset, }, diff --git a/tinytga/src/header.rs b/tinytga/src/header.rs index 21af17c94..95511d167 100644 --- a/tinytga/src/header.rs +++ b/tinytga/src/header.rs @@ -1,13 +1,63 @@ use crate::parse_error::ParseError; use nom::{ - bytes::complete::take, - combinator::map_res, + combinator::{map, map_opt, map_res}, number::complete::{le_u16, le_u8}, IResult, }; -/// TGA footer length in bytes -pub const HEADER_LEN: usize = 18; +/// Bits per pixel. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +#[non_exhaustive] +pub enum Bpp { + /// 8 bits per pixel. + Bits8, + /// 16 bits per pixel. + Bits16, + /// 24 bits per pixel. + Bits24, + /// 32 bits per pixel. + Bits32, +} + +impl Bpp { + fn new(value: u8) -> Option { + Some(match value { + 8 => Self::Bits8, + 16 => Self::Bits16, + 24 => Self::Bits24, + 32 => Self::Bits32, + _ => return None, + }) + } + + fn parse(input: &[u8]) -> IResult<&[u8], Self> { + map_opt(le_u8, Bpp::new)(input) + } + + fn parse_opt(input: &[u8]) -> IResult<&[u8], Option> { + map(le_u8, Bpp::new)(input) + } + + /// Returns the number of bits. + pub fn bits(self) -> u8 { + match self { + Self::Bits8 => 8, + Self::Bits16 => 16, + Self::Bits24 => 24, + Self::Bits32 => 32, + } + } + + /// Returns the number of bytes needed to store values with this bit depth. + pub fn bytes(self) -> u8 { + match self { + Self::Bits8 => 1, + Self::Bits16 => 2, + Self::Bits24 => 3, + Self::Bits32 => 4, + } + } +} /// Image type #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] @@ -34,6 +84,29 @@ pub enum ImageType { RleMonochrome = 11, } +impl ImageType { + fn parse(input: &[u8]) -> IResult<&[u8], Self> { + map_res(le_u8, |b| match b { + 0 => Ok(Self::Empty), + 1 => Ok(Self::ColorMapped), + 2 => Ok(Self::Truecolor), + 3 => Ok(Self::Monochrome), + 9 => Ok(Self::RleColorMapped), + 10 => Ok(Self::RleTruecolor), + 11 => Ok(Self::RleMonochrome), + other => Err(ParseError::UnsupportedImageType(other)), + })(input) + } + + /// Returns `true` when the image is RLE encoded. + pub fn is_rle(self) -> bool { + match self { + ImageType::RleColorMapped | ImageType::RleTruecolor | ImageType::RleMonochrome => true, + _ => false, + } + } +} + /// Image origin #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum ImageOrigin { @@ -83,8 +156,8 @@ pub struct TgaHeader { /// Length of color map pub color_map_len: u16, - /// Number of bits in each color palette entry, typically 15, 16, 24, or 32 bits - pub color_map_depth: u8, + /// Number of bits in each color palette entry + pub color_map_depth: Option, /// Image origin (X) pub x_origin: u16, @@ -98,8 +171,8 @@ pub struct TgaHeader { /// Image height in pixels pub height: u16, - /// Pixel bit depth (8, 16, 24, 32 bits) - pub pixel_depth: u8, + /// Pixel bit depth + pub pixel_depth: Bpp, /// Image origin pub image_origin: ImageOrigin, @@ -108,62 +181,49 @@ pub struct TgaHeader { pub alpha_channel_depth: u8, } +impl TgaHeader { + pub(crate) fn parse(input: &[u8]) -> IResult<&[u8], Self> { + let (input, id_len) = le_u8(input)?; + let (input, has_color_map) = has_color_map(input)?; + let (input, image_type) = ImageType::parse(input)?; + let (input, color_map_start) = le_u16(input)?; + let (input, color_map_len) = le_u16(input)?; + let (input, color_map_depth) = Bpp::parse_opt(input)?; + let (input, x_origin) = le_u16(input)?; + let (input, y_origin) = le_u16(input)?; + let (input, width) = le_u16(input)?; + let (input, height) = le_u16(input)?; + let (input, pixel_depth) = Bpp::parse(input)?; + + let (input, image_descriptor) = le_u8(input)?; + let image_origin = ImageOrigin::from_image_descriptor(image_descriptor); + let alpha_channel_depth = image_descriptor & 0xF; + + Ok(( + input, + TgaHeader { + id_len, + has_color_map, + image_type, + color_map_start, + color_map_len, + color_map_depth, + x_origin, + y_origin, + width, + height, + pixel_depth, + image_origin, + alpha_channel_depth, + }, + )) + } +} + fn has_color_map(input: &[u8]) -> IResult<&[u8], bool> { map_res(le_u8, |b| match b { 0 => Ok(false), 1 => Ok(true), - other => Err(ParseError::UnknownColorMap(other)), - })(input) -} - -fn image_type(input: &[u8]) -> IResult<&[u8], ImageType> { - map_res(le_u8, |b| match b { - 0 => Ok(ImageType::Empty), - 1 => Ok(ImageType::ColorMapped), - 2 => Ok(ImageType::Truecolor), - 3 => Ok(ImageType::Monochrome), - 9 => Ok(ImageType::RleColorMapped), - 10 => Ok(ImageType::RleTruecolor), - 11 => Ok(ImageType::RleMonochrome), - other => Err(ParseError::UnknownImageType(other)), + _ => Err(ParseError::ColorMap), })(input) } - -pub fn header(input: &[u8]) -> IResult<&[u8], TgaHeader> { - let (input, id_len) = le_u8(input)?; - let (input, has_color_map) = has_color_map(input)?; - let (input, image_type) = image_type(input)?; - let (input, color_map_start) = le_u16(input)?; - let (input, color_map_len) = le_u16(input)?; - let (input, color_map_depth) = le_u8(input)?; - let (input, x_origin) = le_u16(input)?; - let (input, y_origin) = le_u16(input)?; - let (input, width) = le_u16(input)?; - let (input, height) = le_u16(input)?; - let (input, pixel_depth) = le_u8(input)?; - - let (input, image_descriptor) = le_u8(input)?; - let image_origin = ImageOrigin::from_image_descriptor(image_descriptor); - let alpha_channel_depth = image_descriptor & 0xF; - - let (input, _image_ident) = take(id_len)(input)?; - - Ok(( - input, - TgaHeader { - id_len, - has_color_map, - image_type, - color_map_start, - color_map_len, - color_map_depth, - x_origin, - y_origin, - width, - height, - pixel_depth, - image_origin, - alpha_channel_depth, - }, - )) -} diff --git a/tinytga/src/lib.rs b/tinytga/src/lib.rs index b34c453d5..6b74b2aa4 100644 --- a/tinytga/src/lib.rs +++ b/tinytga/src/lib.rs @@ -1,86 +1,103 @@ -//! A small TGA parser designed for embedded, no-std environments but usable anywhere. Beyond -//! parsing the image header, no other allocations are made. +//! A small TGA parser designed for use with [embedded-graphics] targetting no-std environments but +//! usable anywhere. Beyond parsing the image header, no other allocations are made. //! -//! To access the individual pixels in an image, the [`TgaRaw`] struct implements `IntoIterator`. It is -//! also possible to access the unaltered raw image data by reading the [`pixel_data`] field. This -//! data will need to be interpreted according to the [`image_type`] specified in the header. -//! -//! # Features -//! -//! * `graphics` - enables [embedded-graphics] integration. +//! tinytga provides two methods of accessing the pixel data inside a TGA file. The most convenient +//! way is to use a color type provided by [embedded-graphics] to define the format stored inside +//! the TGA file. But it is also possible to directly access the raw pixel representation instead. //! //! # Examples //! //! ## Load a Run Length Encoded (RLE) TGA image //! //! ```rust -//! use tinytga::{ImageOrigin, ImageType, Pixel, TgaRaw, TgaFooter, TgaHeader}; +//! use embedded_graphics::{prelude::*, pixelcolor::Rgb888}; +//! use tinytga::{Bpp, ImageOrigin, ImageType, RawPixel, Tga, TgaHeader}; //! //! // Include an image from a local path as bytes //! let data = include_bytes!("../tests/chessboard_4px_rle.tga"); //! -//! // Create a TGA instance from a byte slice -//! let img = TgaRaw::from_slice(data).unwrap(); +//! // Create a TGA instance from a byte slice. +//! // The color type is set by defining the type of the `img` variable. +//! let img: Tga = Tga::from_slice(data).unwrap(); +//! +//! // Check the size of the image. +//! assert_eq!(img.size(), Size::new(4, 4)); +//! +//! // Collect pixels into a vector. +//! let pixels: Vec<_> = img.pixels().collect(); +//! ``` +//! +//! ## Drawing an image using `embedded-graphics` +//! +//! This example demonstrates how a TGA image can be drawn to a [embedded-graphics] draw target. +//! +//! ```rust +//! # fn main() -> Result<(), core::convert::Infallible> { +//! # let mut display = embedded_graphics::mock_display::MockDisplay::default(); +//! use embedded_graphics::{image::Image, pixelcolor::Rgb888, prelude::*}; +//! use tinytga::Tga; +//! +//! // Include an image from a local path as bytes +//! let data = include_bytes!("../tests/chessboard_4px_rle.tga"); + +//! let tga: Tga = Tga::from_slice(data).unwrap(); +//! +//! let image = Image::new(&tga, Point::zero()); +//! +//! image.draw(&mut display)?; +//! # Ok::<(), core::convert::Infallible>(()) } +//! ``` +//! +//! ## Accessing raw pixel data +//! +//! If you do not want to use the color types provided by [embedded-graphics] you can also access +//! the raw image data. +//! +//! ```rust +//! use embedded_graphics::{prelude::*, pixelcolor::Rgb888}; +//! use tinytga::{Bpp, ImageOrigin, ImageType, RawPixel, Tga, TgaHeader}; +//! +//! // Include an image from a local path as bytes. +//! let data = include_bytes!("../tests/chessboard_4px_rle.tga"); +//! +//! // Create a TGA instance from a byte slice. +//! let img = Tga::from_slice_raw(data).unwrap(); //! -//! // Take a look at the header +//! // Take a look at the raw image header. //! assert_eq!( -//! img.header, +//! img.raw_header(), //! TgaHeader { //! id_len: 0, //! has_color_map: false, //! image_type: ImageType::RleTruecolor, //! color_map_start: 0, //! color_map_len: 0, -//! color_map_depth: 0, +//! color_map_depth: None, //! x_origin: 0, //! y_origin: 4, //! width: 4, //! height: 4, -//! pixel_depth: 24, +//! pixel_depth: Bpp::Bits24, //! image_origin: ImageOrigin::TopLeft, //! alpha_channel_depth: 0, //! } //! ); //! -//! // Take a look at the footer -//! assert_eq!( -//! img.footer, -//! Some(TgaFooter { -//! extension_area_offset: 0, -//! developer_directory_offset: 0 -//! }) -//! ); -//! -//! // Collect pixels into a `Vec` -//! let pixels = img.into_iter().collect::>(); +//! // Collect raw pixels into a vector. +//! let pixels: Vec<_> = img.raw_pixels().collect(); //! ``` //! -//! ## Use with `embedded-graphics` -//! -//! This example demonstrates [embedded-graphics] support by rendering a TGA image to a mock -//! display. +//! # Embedded-graphics drawing performance //! -//! The `graphics` feature of `tinytga` needs to be enabled in `Cargo.toml` to use the `Tga` object -//! with embedded-graphics. -//! -//! ```rust -//! # #[cfg(feature = "graphics")] { fn main() -> Result<(), core::convert::Infallible> { -//! # let mut display = embedded_graphics::mock_display::MockDisplay::default(); -//! use embedded_graphics::{image::Image, pixelcolor::Rgb888, prelude::*}; -//! use tinytga::Tga; -//! -//! let tga: Tga = Tga::from_slice(include_bytes!("../tests/rust-rle-bw-topleft.tga")).unwrap(); -//! -//! let image = Image::new(&tga, Point::zero()); -//! -//! image.draw(&mut display)?; -//! # Ok::<(), core::convert::Infallible>(()) } } -//! ``` +//! `tinytga` uses different code paths to draw images with different [`ImageOrigin`]s. +//! The performance difference between the origins will depend on the display driver, but using +//! images with the origin at the top left corner will generally result in the best performance. //! +//! [`ImageOrigin`]: enum.ImageOrigin.html //! [embedded-graphics]: https://docs.rs/embedded-graphics -//! [`TgaRaw`]: ./struct.TgaRaw.html +//! [`Tga`]: ./struct.Tga.html //! [`image_type`]: ./struct.TgaHeader.html#structfield.image_type -//! [`pixel_data`]: ./struct.TgaRaw.html#structfield.pixel_data +//! [`pixel_data`]: ./struct.Tga.html#structfield.pixel_data #![no_std] #![deny(missing_docs)] @@ -93,324 +110,223 @@ #![deny(unused_import_braces)] #![deny(unused_qualifications)] +mod color_map; mod footer; mod header; mod packet; mod parse_error; -mod pixel; - -#[cfg(feature = "graphics")] -mod embedded_graphics; +mod pixels; +mod raw_pixels; -use crate::{ - footer::*, - header::*, - packet::{next_rle_packet, Packet}, - parse_error::ParseError, -}; +use ::embedded_graphics::prelude::*; +use core::marker::PhantomData; +use nom::{bytes::complete::take, IResult}; +use crate::footer::TgaFooter; pub use crate::{ - footer::TgaFooter, - header::{ImageOrigin, ImageType, TgaHeader}, - pixel::Pixel, + color_map::ColorMap, + header::{Bpp, ImageOrigin, ImageType, TgaHeader}, + parse_error::ParseError, + pixels::Pixels, + raw_pixels::{RawPixel, RawPixels}, }; -#[cfg(feature = "graphics")] -pub use crate::embedded_graphics::*; - /// TGA image #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -pub struct TgaRaw<'a> { - /// TGA header - pub header: TgaHeader, - - /// TGA footer (last 26 bytes of file) - pub footer: Option, +pub struct Tga<'a, C> { + /// Image data + data: &'a [u8], /// Color map - pub color_map: Option<&'a [u8]>, + pub color_map: Option>, /// Image pixel data - pub pixel_data: &'a [u8], -} + pixel_data: &'a [u8], -impl<'a> TgaRaw<'a> { - /// Parse a TGA image from a byte slice - pub fn from_slice(bytes: &'a [u8]) -> Result { - let (_remaining, header) = header(bytes).map_err(|_| ParseError::Header)?; + /// Image size + size: Size, - // Read last 26 bytes as TGA footer - let footer = footer(&bytes[bytes.len() - FOOTER_LEN..]) - .map(|(_remaining, footer)| footer) - .ok(); + /// Image type + image_type: ImageType, - let header_len = HEADER_LEN + header.id_len as usize; + /// Bits per pixel + bpp: Bpp, - let color_map = if header.has_color_map { - let len = - usize::from(header.color_map_len) * (usize::from(header.color_map_depth + 7) / 8); + /// Image origin + image_origin: ImageOrigin, - Some(&bytes[header_len..header_len + len]) - } else { - None - }; - - let image_data_start = header_len + color_map.map_or(0, |map| map.len()); - - let image_data_end = if let Some(footer) = footer { - [ - footer.extension_area_offset as usize, - footer.developer_directory_offset as usize, - ] - .iter() - .filter_map(|v| if *v > 0 { Some(*v) } else { None }) - .min() - .unwrap_or(bytes.len() - FOOTER_LEN) - } else { - bytes.len() - }; + /// Color type + color_type: PhantomData, +} + +impl<'a, C> Tga<'a, C> { + /// Common part of `from_slice` and `from_slice_raw`. + fn from_slice_common(data: &'a [u8]) -> Result { + let input = data; + let (input, header) = TgaHeader::parse(input).map_err(|_| ParseError::Header)?; + let (input, _image_id) = image_id(input, &header).map_err(|_| ParseError::Header)?; + let (input, color_map) = ColorMap::parse(input, &header)?; - let pixel_data = &bytes[image_data_start..image_data_end]; + let footer_length = TgaFooter::parse(data).map_or(0, |footer| footer.length(data)); + + // Use saturating_sub to make sure this can't panic + let pixel_data = &input[0..input.len().saturating_sub(footer_length)]; + + let size = Size::new(u32::from(header.width), u32::from(header.height)); Ok(Self { - header, - footer, + data, color_map, pixel_data, + size, + bpp: header.pixel_depth, + image_origin: header.image_origin, + image_type: header.image_type, + color_type: PhantomData, }) } - /// Get the bit depth (BPP) of this image - pub fn bpp(&self) -> u8 { - self.header.pixel_depth - } - - /// Get the image width in pixels - pub fn width(&self) -> u16 { - self.header.width + /// Returns the color bit depth (BPP) of this image. + pub fn color_bpp(&self) -> Bpp { + if let Some(color_map) = &self.color_map { + color_map.entry_bpp() + } else { + self.bpp + } } - /// Get the image height in pixels - pub fn height(&self) -> u16 { - self.header.height + /// Returns the image origin. + pub fn image_origin(&self) -> ImageOrigin { + self.image_origin } - /// Get the raw image data contained in this image - pub fn image_data(&self) -> &[u8] { + /// Returns the raw image data contained in this image. + pub fn raw_image_data(&self) -> &'a [u8] { self.pixel_data } -} -impl<'a> IntoIterator for &'a TgaRaw<'a> { - type Item = Pixel; - type IntoIter = TgaIterator<'a>; - - fn into_iter(self) -> Self::IntoIter { - let (bytes_to_consume, current_packet) = match self.header.image_type { - ImageType::Monochrome | ImageType::Truecolor | ImageType::ColorMapped => { - let data = Packet::from_slice(self.image_data()); - - (Some(self.image_data()), data) - } - ImageType::RleMonochrome | ImageType::RleTruecolor | ImageType::RleColorMapped => { - next_rle_packet(self.image_data(), self.bpp() / 8) - .map(|(remaining, packet)| (Some(remaining), packet)) - .expect("Failed to parse first image RLE data packet") - } - image_type => panic!("Image type {:?} not supported", image_type), - }; - - // Explicit match to prevent integer division rounding errors - let stride = match self.bpp() { - 8 => 1, - 16 => 2, - 24 => 3, - 32 => 4, - depth => panic!("Bit depth {} not supported", depth), - }; - - let current_packet_len = current_packet.len(); - - let y = if self.header.image_origin.is_bottom() { - self.height().saturating_sub(1) - } else { - 0 - }; - - TgaIterator { - tga: self, - bytes_to_consume, - current_packet, - current_packet_position: 0, - current_packet_pixel_length: current_packet_len / stride, - stride, - x: 0, - y, - done: false, - } + /// Returns an iterator over the raw pixels in this image. + pub fn raw_pixels<'b>(&'b self) -> RawPixels<'b, 'a, C> { + RawPixels::new(self) } -} - -/// Iterator over individual TGA pixels -/// -/// This can be used to build a raw image buffer to pass around -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -pub struct TgaIterator<'a> { - /// Reference to original TGA image - tga: &'a TgaRaw<'a>, - /// Remaining bytes (after current packet) to consume - bytes_to_consume: Option<&'a [u8]>, - - /// Reference to current packet definition (either RLE or raw) - current_packet: Packet<'a>, + /// Returns the TGA header. + /// + /// The returned object is a direct representation of the header contained + /// in the TGA file. Most of the information contained in the header is also + /// available using other methods, which are the preferred way of accessing + /// them. + /// + /// # Performance + /// + /// To save memory the header is parsed every time this method is called. + pub fn raw_header(&self) -> TgaHeader { + // unwrap can't fail because the header was checked when self was created + TgaHeader::parse(self.data).unwrap().1 + } - /// Current position within the current packet's pixel run - current_packet_position: usize, + /// Returns the developer directory. + /// + /// # Performance + /// + /// To save memory the footer is parsed every time this method is called. + pub fn raw_developer_directory(&self) -> Option<&[u8]> { + TgaFooter::parse(self.data).and_then(|footer| footer.developer_directory(self.data)) + } - /// Current packet length in pixels - current_packet_pixel_length: usize, + /// Returns the extension area. + /// + /// # Performance + /// + /// To save memory the footer is parsed every time this method is called. + pub fn raw_extension_area(&self) -> Option<&[u8]> { + TgaFooter::parse(self.data).and_then(|footer| footer.extension_area(self.data)) + } - /// Number of bytes contained within each pixel - stride: usize, + /// Returns the content of the image ID. + /// + /// If the TGA file doesn't contain an image ID `None` is returned. + /// + /// # Performance + /// + /// To save memory the header is parsed every time this method is called. + pub fn image_id(&self) -> Option<&[u8]> { + let (input, header) = TgaHeader::parse(self.data).ok()?; + + image_id(input, &header) + .ok() + .map(|(_input, id)| id) + .filter(|id| !id.is_empty()) + } +} - /// Current X coordinate from top-left of image - x: u16, +impl<'a, C> Tga<'a, C> +where + C: PixelColor, +{ + /// Parse a TGA image from a byte slice + /// + /// # Errors + /// + /// If the bit depth of the source image does not match the bit depth of the output color type + /// `C`, this method will return a [`ParseError::MismatchedBpp`] error. + /// + /// [`ParseError::MismatchedBpp`]: enum.ParseError.html#variant.MismatchedBpp + pub fn from_slice(data: &'a [u8]) -> Result { + let tga = Tga::from_slice_common(data)?; + + if C::Raw::BITS_PER_PIXEL != usize::from(tga.color_bpp().bits()) { + return Err(ParseError::MismatchedBpp(tga.color_bpp().bits())); + } - /// Current Y coordinate from top-left of image - y: u16, + Ok(tga) + } - /// Iteration is done - done: bool, + /// Returns an iterator over the raw pixels in this image. + pub fn pixels<'b>(&'b self) -> Pixels<'b, 'a, C> { + Pixels::new(self.raw_pixels()) + } } -impl<'a> Iterator for TgaIterator<'a> { - type Item = Pixel; - - fn next(&mut self) -> Option { - if self.done { - return None; - } - - if self.current_packet_position >= self.current_packet_pixel_length { - // Parse next packet from remaining bytes - match self.tga.header.image_type { - ImageType::Monochrome | ImageType::Truecolor | ImageType::ColorMapped => { - return None; - } - ImageType::RleMonochrome | ImageType::RleTruecolor | ImageType::RleColorMapped => { - if self.bytes_to_consume.is_none() { - return None; - } else { - self.current_packet_position = 0; - } - - let (bytes_to_consume, current_packet) = - next_rle_packet(self.bytes_to_consume.unwrap(), self.tga.bpp() / 8) - .map(|(remaining, packet)| { - ( - if !remaining.is_empty() { - Some(remaining) - } else { - None - }, - packet, - ) - }) - .expect("Failed to parse first image RLE data packet"); - - self.bytes_to_consume = bytes_to_consume; - self.current_packet_pixel_length = current_packet.len() / self.stride; - self.current_packet = current_packet; - } - image_type => panic!("Image type {:?} not supported", image_type), - }; - // } - } +impl<'a> Tga<'a, ()> { + /// Parse a TGA image from a byte slice + pub fn from_slice_raw(bytes: &'a [u8]) -> Result { + Tga::from_slice_common(bytes) + } +} - let (start, px): (usize, &[u8]) = match self.current_packet { - // RLE packets use the same 4 bytes for the color of every pixel in the packet, so - // there is no start offet like `RawPacket`s have - Packet::RlePacket(ref p) => (0, p.pixel_data), - // Raw packets need to look within the byte array to find the correct bytes to - // convert to a pixel value, hence the calculation of `start = position * stride` - Packet::RawPacket(ref p) => { - let px = p.pixel_data; - let start = self.current_packet_position * self.stride; - - (start, px) - } - // Uncompressed data just walks along the byte array in steps of `self.stride` - Packet::FullContents(px) => { - let start = self.current_packet_position * self.stride; - - (start, px) - } - }; - - let mut pixel_value = { - let out = match self.stride { - 1 => u32::from(px[start]), - 2 => u32::from_le_bytes([px[start], px[start + 1], 0, 0]), - 3 => u32::from_le_bytes([px[start], px[start + 1], px[start + 2], 0]), - 4 => u32::from_le_bytes([px[start], px[start + 1], px[start + 2], px[start + 3]]), - depth => unreachable!("Depth {} is not supported", depth), - }; - - self.current_packet_position += 1; - - out - }; - - if let Some(color_map) = self.tga.color_map { - let entry_size = usize::from(self.tga.header.color_map_depth + 7) / 8; - let start = pixel_value as usize * entry_size; - - pixel_value = match entry_size { - 1 => color_map[start] as u32, - 2 => u32::from_le_bytes([color_map[start], color_map[start + 1], 0, 0]), - 3 => u32::from_le_bytes([ - color_map[start], - color_map[start + 1], - color_map[start + 2], - 0, - ]), - 4 => u32::from_le_bytes([ - color_map[start], - color_map[start + 1], - color_map[start + 2], - color_map[start + 3], - ]), - depth => unreachable!("Depth {} is not supported", depth), - }; - } +impl OriginDimensions for Tga<'_, C> { + fn size(&self) -> Size { + self.size + } +} - let x = self.x; - let y = self.y; - - self.x += 1; - - if self.x >= self.tga.width() { - self.x = 0; - - if self.tga.header.image_origin.is_bottom() { - if self.y > 0 { - self.y -= 1; - } else { - self.done = true; - } - } else { - self.y += 1; - if self.y >= self.tga.height() { - self.done = true; - } - } +impl ImageDrawable for Tga<'_, C> +where + C: PixelColor + From<::Raw>, +{ + type Color = C; + + fn draw(&self, target: &mut D) -> Result<(), D::Error> + where + D: DrawTarget, + { + // TGA files with the origin in the top left corner can be drawn using `fill_contiguous`. + // All other origins are drawn by falling back to `draw_iter`. + if self.image_origin == ImageOrigin::TopLeft { + target.fill_contiguous( + &self.bounding_box(), + self.raw_pixels().map(|p| C::Raw::from_u32(p.color).into()), + ) + } else { + target.draw_iter( + self.raw_pixels() + .map(|p| Pixel(p.position, C::Raw::from_u32(p.color).into())), + ) } - - Some(Pixel { - x, - y, - color: pixel_value, - }) } } + +fn image_id<'a>(input: &'a [u8], header: &TgaHeader) -> IResult<&'a [u8], &'a [u8]> { + take(header.id_len)(input) +} diff --git a/tinytga/src/packet.rs b/tinytga/src/packet.rs new file mode 100644 index 000000000..375db9190 --- /dev/null +++ b/tinytga/src/packet.rs @@ -0,0 +1,91 @@ +use nom::{bytes::complete::take, number::complete::le_u8, IResult}; + +#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Hash, Eq, Ord)] +pub enum PacketType { + Raw, + Rle, +} + +#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Hash, Eq, Ord)] +pub struct Packet<'a> { + packet_type: PacketType, + pixel_count: usize, + data: &'a [u8], + bytes_per_pixel: u8, +} + +impl<'a> Packet<'a> { + /// Parses a packet in a RLE encoded file. + pub fn parse(input: &'a [u8], bytes_per_pixel: u8) -> IResult<&'a [u8], Self> { + let (input, type_and_count) = le_u8(input)?; + + // The pixel count is encoded in the lower 7 bits and the actual number of pixels + // is one more than the value stored in the packet. + let pixel_count = usize::from(type_and_count & 0x7F) + 1; + + // The packet type is encoded in the upper bit: 0 -> Raw, 1 -> Rle + let packet_type; + let (input, data) = if type_and_count & 0x80 != 0 { + packet_type = PacketType::Rle; + + // RLE packets always contain a single pixel + take(bytes_per_pixel)(input)? + } else { + packet_type = PacketType::Raw; + + // Raw packets contain `pixel_count` pixels + take(pixel_count * usize::from(bytes_per_pixel))(input)? + }; + + Ok(( + input, + Self { + packet_type, + pixel_count, + data, + bytes_per_pixel, + }, + )) + } + + pub fn from_uncompressed( + image_data: &'a [u8], + pixel_count: usize, + bytes_per_pixel: u8, + ) -> Self { + Self { + packet_type: PacketType::Raw, + pixel_count, + data: image_data, + bytes_per_pixel, + } + } +} + +impl Iterator for Packet<'_> { + type Item = u32; + + fn next(&mut self) -> Option { + let bytes_per_pixel = usize::from(self.bytes_per_pixel); + + if self.pixel_count == 0 || self.data.len() < bytes_per_pixel { + return None; + } + + self.pixel_count -= 1; + + let color = match self.bytes_per_pixel { + 1 => u32::from(self.data[0]), + 2 => u32::from_le_bytes([self.data[0], self.data[1], 0, 0]), + 3 => u32::from_le_bytes([self.data[0], self.data[1], self.data[2], 0]), + 4 => u32::from_le_bytes([self.data[0], self.data[1], self.data[2], self.data[3]]), + _ => 0, + }; + + if self.packet_type == PacketType::Raw { + self.data = &self.data[bytes_per_pixel..]; + } + + Some(color) + } +} diff --git a/tinytga/src/packet/mod.rs b/tinytga/src/packet/mod.rs deleted file mode 100644 index b717807c9..000000000 --- a/tinytga/src/packet/mod.rs +++ /dev/null @@ -1,55 +0,0 @@ -mod raw_packet; -mod rle_packet; - -pub use self::{ - raw_packet::{raw_packet, RawPacket}, - rle_packet::{rle_packet, RlePacket}, -}; -use nom::{bits::complete::take, branch::alt, combinator::map, IResult}; - -/// A Run Length Encoded (RLE) packet -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -pub enum Packet<'a> { - /// Data in this packet is Run Length Encoded - RlePacket(RlePacket<'a>), - - /// Data is in a packet, but is not compressed at all - RawPacket(RawPacket<'a>), - - /// File is not packeted. Contains reference to all image data in the file - FullContents(&'a [u8]), -} - -impl<'a> Packet<'a> { - /// Get the length in bytes of the pixel data in this packet - pub fn len(&self) -> usize { - match self { - Packet::RlePacket(p) => p.len(), - Packet::RawPacket(p) => p.len(), - Packet::FullContents(p) => p.len(), - } - } - - /// Create a `FullContents` packet from a slice - pub fn from_slice(data: &'a [u8]) -> Self { - Packet::FullContents(data) - } -} - -pub fn next_rle_packet(input: &[u8], bytes_per_pixel: u8) -> IResult<&[u8], Packet> { - alt(( - map(rle_packet(bytes_per_pixel), Packet::RlePacket), - map(raw_packet(bytes_per_pixel), Packet::RawPacket), - ))(input) -} - -/// Parse pixel count in raw and RLE packets. -/// -/// This parser expects bits as input! -fn pixel_count(input: (&[u8], usize)) -> IResult<(&[u8], usize), u8> { - map( - take(7u8), - // Run length is encoded as 0 = 1 pixel, 1 = 2 pixels, etc, hence this offset - |count: u8| count + 1, - )(input) -} diff --git a/tinytga/src/packet/raw_packet.rs b/tinytga/src/packet/raw_packet.rs deleted file mode 100644 index 831157852..000000000 --- a/tinytga/src/packet/raw_packet.rs +++ /dev/null @@ -1,130 +0,0 @@ -use super::pixel_count; -use nom::{ - bits::{bits, complete::tag}, - bytes::complete::take, - sequence::preceded, - IResult, -}; - -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -pub struct RawPacket<'a> { - /// Number of pixels of this packet - pub num_pixels: u8, - - /// Pixel data in this packet, up to 32 bits (4 bytes) per pixel - pub pixel_data: &'a [u8], -} - -impl<'a> RawPacket<'a> { - /// Get the number of bytes in this packet - pub fn len(&self) -> usize { - self.pixel_data.len() - } -} - -pub fn raw_packet(bytes_per_pixel: u8) -> impl Fn(&[u8]) -> IResult<&[u8], RawPacket> { - move |input| { - // 0x00 = raw packet, 0x01 = RLE packet - let (input, num_pixels) = bits(preceded(tag(0, 1u8), pixel_count))(input)?; - let (input, pixel_data) = take(num_pixels as usize * bytes_per_pixel as usize)(input)?; - - Ok(( - input, - RawPacket { - num_pixels, - pixel_data, - }, - )) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse() { - let input = [ - // 2 pixels worth of RAW data - 0b0000_0001, - // 32BPP pixel - 0xAA, - 0xBB, - 0xCC, - 0xDD, - // 32BPP pixel - 0x11, - 0x22, - 0x33, - 0x44, - ]; - - let (remaining, packet) = raw_packet(4)(&input).unwrap(); - - assert_eq!(remaining.len(), 0); - assert_eq!( - packet, - RawPacket { - num_pixels: 2, - pixel_data: &[ - 0xAA, 0xBB, 0xCC, 0xDD, // - 0x11, 0x22, 0x33, 0x44, // - ] - } - ); - } - - #[test] - fn ignore_rle_packet() { - let input = [ - // 2 pixels worth of RLE data - 0b1000_0001, - // 32BPP pixel - 0xAA, - 0xBB, - 0xCC, - 0xDD, - ]; - - let result = raw_packet(4)(&input); - - assert!(result.is_err()); - } - - #[test] - fn stop_at_packet_end() { - let input = [ - // 2 pixels worth of non-RLE data - 0b0000_0001, - // 32BPP pixel - 0xAA, - 0xBB, - 0xCC, - 0xDD, - // 32BPP pixel - 0x11, - 0x22, - 0x33, - 0x44, - // 32BPP pixel (extra, invalid) - 0x55, - 0x66, - 0x77, - 0x88, - ]; - - let (remaining, packet) = raw_packet(4)(&input).unwrap(); - - assert_eq!(remaining, &[0x55, 0x66, 0x77, 0x88]); - assert_eq!( - packet, - RawPacket { - num_pixels: 2, - pixel_data: &[ - 0xAA, 0xBB, 0xCC, 0xDD, // - 0x11, 0x22, 0x33, 0x44, // - ] - } - ); - } -} diff --git a/tinytga/src/packet/rle_packet.rs b/tinytga/src/packet/rle_packet.rs deleted file mode 100644 index 0a7584504..000000000 --- a/tinytga/src/packet/rle_packet.rs +++ /dev/null @@ -1,114 +0,0 @@ -use super::pixel_count; -use nom::{ - bits::{bits, complete::tag}, - bytes::complete::take, - sequence::preceded, - IResult, -}; - -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -pub struct RlePacket<'a> { - /// Number of pixels in this run - pub run_length: u8, - - /// Pixel data - pub pixel_data: &'a [u8], -} - -impl<'a> RlePacket<'a> { - /// Get the number of pixels in this packet - pub fn len(&self) -> usize { - self.pixel_data.len() * self.run_length as usize - } -} - -pub fn rle_packet(bytes_per_pixel: u8) -> impl Fn(&[u8]) -> IResult<&[u8], RlePacket> { - move |input| { - // 0x00 = raw packet, 0x01 = RLE packet - let (input, run_length) = bits(preceded(tag(1, 1u8), pixel_count))(input)?; - let (input, pixel_data) = take(bytes_per_pixel)(input)?; - - Ok(( - input, - RlePacket { - run_length, - pixel_data, - }, - )) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse() { - let input = [ - // 2 pixels worth of RLE data - 0b1000_0001, - // 32BPP pixel - 0xAA, - 0xBB, - 0xCC, - 0xDD, - ]; - - let (remaining, packet) = rle_packet(4)(&input).unwrap(); - - assert_eq!(remaining.len(), 0); - assert_eq!( - packet, - RlePacket { - run_length: 2, - pixel_data: &[0xAA, 0xBB, 0xCC, 0xDD] - } - ); - } - - #[test] - fn ignore_raw_packet() { - let input = [ - // 2 pixels worth of raw (non-RLE) data - 0b0000_0001, - // 32BPP pixel - 0xAA, - 0xBB, - 0xCC, - 0xDD, - ]; - - let result = rle_packet(4)(&input); - - assert!(result.is_err()); - } - - #[test] - fn stop_at_packet_end() { - let input = [ - // 2 pixels worth of RLE data - 0b1000_0001, - // 32BPP pixel - 0xAA, - 0xBB, - 0xCC, - 0xDD, - // 32BPP pixel - 0x11, - 0x22, - 0x33, - 0x44, - ]; - - let (remaining, packet) = rle_packet(4)(&input).unwrap(); - - assert_eq!(remaining, &[0x11, 0x22, 0x33, 0x44]); - assert_eq!( - packet, - RlePacket { - run_length: 2, - pixel_data: &[0xAA, 0xBB, 0xCC, 0xDD] - } - ); - } -} diff --git a/tinytga/src/parse_error.rs b/tinytga/src/parse_error.rs index 450025f25..c53a47214 100644 --- a/tinytga/src/parse_error.rs +++ b/tinytga/src/parse_error.rs @@ -1,28 +1,27 @@ /// Possible parse errors #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +#[non_exhaustive] pub enum ParseError { - /// An invalid color map value was encountered. Valid values are `0` (no color map) or `1` - /// (color map included) - InvalidColorMap(u8), + /// An error occurred when parsing the color map. + ColorMap, - /// An invalid image type was encountered. Valid values are presented in [`ImageType`] - InvalidImageType(u8), - - /// Parse was incomplete. Holds the remaining number of bytes - Incomplete(usize), - - /// An error occurred when parsing the TGA header + /// An error occurred when parsing the TGA header. Header, - /// An error occurred when parsing the TGA footer + /// An error occurred when parsing the TGA footer. Footer, - /// An unknown image type value was encountered - UnknownImageType(u8), + /// An unsupported image type value was encountered. + UnsupportedImageType(u8), - /// An unknown color map value was encountered - UnknownColorMap(u8), + /// An unsupported bits per pixel value was encountered. + UnsupportedBpp(u8), - /// Any other type of parse error - Other, + /// Mismatched bits per pixel. + /// + /// The bit depth of the image doesn't match the depth that was specified + /// when `Tga::from_slice` was called. + /// + /// [`Tga::from_slice`]: struct.Tga.html#method.from_slice + MismatchedBpp(u8), } diff --git a/tinytga/src/pixel.rs b/tinytga/src/pixel.rs deleted file mode 100644 index f5c48b34f..000000000 --- a/tinytga/src/pixel.rs +++ /dev/null @@ -1,12 +0,0 @@ -/// A single pixel of a TGA image -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] -pub struct Pixel { - /// Pixel X coordinate from top left of image - pub x: u16, - - /// Pixel Y coordinate from top left of image - pub y: u16, - - /// Pixel color - pub color: u32, -} diff --git a/tinytga/src/pixels.rs b/tinytga/src/pixels.rs new file mode 100644 index 000000000..3909ffb8d --- /dev/null +++ b/tinytga/src/pixels.rs @@ -0,0 +1,27 @@ +use crate::RawPixels; +use embedded_graphics::prelude::*; + +/// Iterator over individual TGA pixels +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct Pixels<'a, 'b, C> { + raw: RawPixels<'a, 'b, C>, +} + +impl<'a, 'b, C> Pixels<'a, 'b, C> { + pub(crate) fn new(raw: RawPixels<'a, 'b, C>) -> Self { + Self { raw } + } +} + +impl Iterator for Pixels<'_, '_, C> +where + C: PixelColor + From<::Raw>, +{ + type Item = Pixel; + + fn next(&mut self) -> Option { + self.raw + .next() + .map(|p| Pixel(p.position, C::Raw::from_u32(p.color).into())) + } +} diff --git a/tinytga/src/raw_pixels.rs b/tinytga/src/raw_pixels.rs new file mode 100644 index 000000000..32ebc1508 --- /dev/null +++ b/tinytga/src/raw_pixels.rs @@ -0,0 +1,118 @@ +use crate::{packet::Packet, Tga}; +use embedded_graphics::prelude::*; + +/// Iterator over individual TGA pixels +/// +/// This can be used to build a raw image buffer to pass around +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct RawPixels<'a, 'b, C> { + /// Reference to original TGA image + tga: &'a Tga<'b, C>, + + position: Point, + + packet: Packet<'b>, + + remaining_data: &'b [u8], +} + +impl<'a, 'b, C> RawPixels<'a, 'b, C> { + pub(crate) fn new(tga: &'a Tga<'b, C>) -> Self { + let size = tga.size(); + let remaining_pixels = size.width as usize * size.height as usize; + + let image_data = tga.raw_image_data(); + + let (first_packet_pixels, data) = if tga.image_type.is_rle() { + (0, image_data) + } else { + (remaining_pixels, &image_data[0..0]) + }; + + let packet = + Packet::from_uncompressed(tga.raw_image_data(), first_packet_pixels, tga.bpp.bytes()); + + let start_y = if tga.image_origin.is_bottom() { + tga.size.height.saturating_sub(1) + } else { + 0 + }; + + Self { + tga, + packet, + remaining_data: data, + position: Point::new(0, start_y as i32), + } + } + + /// Returns the next pixel position. + fn next_position(&mut self) -> Option { + if self.position.y < 0 || self.position.y >= self.tga.size.height as i32 { + return None; + } + + let position = self.position; + + self.position.x += 1; + + if self.position.x >= self.tga.size.width as i32 { + self.position.x = 0; + + if self.tga.image_origin.is_bottom() { + self.position.y -= 1; + } else { + self.position.y += 1; + } + } + + Some(position) + } +} + +impl<'a, 'b, C> Iterator for RawPixels<'a, 'b, C> { + type Item = RawPixel; + + fn next(&mut self) -> Option { + let position = self.next_position()?; + + let color = if let Some(color) = self.packet.next() { + color + } else { + match Packet::parse(self.remaining_data, self.tga.bpp.bytes()) { + Ok((data, packet)) => { + self.remaining_data = data; + self.packet = packet; + + self.packet.next().unwrap_or(0) + } + Err(_) => 0, + } + }; + + let color = if let Some(color_map) = &self.tga.color_map { + color_map.get_raw(color as usize).unwrap_or(0) + } else { + color + }; + + Some(RawPixel::new(position, color)) + } +} + +/// Pixel with raw pixel color. +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Default)] +pub struct RawPixel { + /// The position relative to the top left corner of the image. + pub position: Point, + + /// The raw pixel color. + pub color: u32, +} + +impl RawPixel { + /// Creates a new raw pixel. + pub fn new(position: Point, color: u32) -> Self { + Self { position, color } + } +} diff --git a/tinytga/tests/cbw8.rs b/tinytga/tests/cbw8.rs index 46d8c349f..a459e5435 100644 --- a/tinytga/tests/cbw8.rs +++ b/tinytga/tests/cbw8.rs @@ -1,43 +1,41 @@ -use tinytga::{ImageOrigin, ImageType, TgaFooter, TgaHeader, TgaRaw}; +use tinytga::{Bpp, ImageOrigin, ImageType, Tga, TgaHeader}; #[test] fn cbw8() { let data = include_bytes!("./cbw8.tga"); - let img = TgaRaw::from_slice(data).unwrap(); + let img = Tga::from_slice_raw(data).unwrap(); - println!("{:#?}", img.header); - println!("{:#?}", img.footer); - println!("Pixel data len {:#?}", img.pixel_data.len()); + println!("{:#?}", img.raw_header()); + println!("Raw image data len {:#?}", img.raw_image_data().len()); assert_eq!( - img.header, + img.raw_header(), TgaHeader { id_len: 26, has_color_map: false, image_type: ImageType::RleMonochrome, color_map_start: 0, color_map_len: 0, - color_map_depth: 0, + color_map_depth: None, x_origin: 0, y_origin: 0, width: 128, height: 128, - pixel_depth: 8, + pixel_depth: Bpp::Bits8, image_origin: ImageOrigin::BottomLeft, alpha_channel_depth: 0, } ); + const TGA_FOOTER_LENGTH: usize = 26; assert_eq!( - img.footer, - Some(TgaFooter { - extension_area_offset: 8238, - developer_directory_offset: 0 - }) + img.raw_extension_area(), + Some(&data[8238..data.len() - TGA_FOOTER_LENGTH]) ); + assert_eq!(img.raw_developer_directory(), None); - let pixels = img.into_iter().collect::>(); + let pixels = img.raw_pixels().collect::>(); assert_eq!(pixels.len(), 128 * 128); } diff --git a/tinytga/tests/chequerboard-uncompressed-topleft.rs b/tinytga/tests/chequerboard-uncompressed-topleft.rs index 491ecbf16..ca636e1b4 100644 --- a/tinytga/tests/chequerboard-uncompressed-topleft.rs +++ b/tinytga/tests/chequerboard-uncompressed-topleft.rs @@ -1,46 +1,40 @@ -use tinytga::{ImageOrigin, ImageType, TgaFooter, TgaHeader, TgaRaw}; +use tinytga::{Bpp, ImageOrigin, ImageType, Tga, TgaHeader}; #[test] fn chequerboard_uncompressed_topleft() { let data = include_bytes!("./chequerboard-uncompressed-topleft.tga"); - let img = TgaRaw::from_slice(data).unwrap(); + let img = Tga::from_slice_raw(data).unwrap(); - println!("{:#?}", img.header); - println!("{:#?}", img.footer); - println!("Pixel data len {:#?}", img.pixel_data.len()); + println!("{:#?}", img.raw_header()); + println!("Raw image data len {:#?}", img.raw_image_data().len()); - let image_data_len = - img.header.width * img.header.height * (img.header.pixel_depth as u16 / 8u16); + let header = img.raw_header(); + let image_data_len = header.width * header.height * header.pixel_depth.bytes() as u16; // Source image is 8x8px, uncompressed, 8BPP color assert_eq!( - img.header, + img.raw_header(), TgaHeader { id_len: 0, has_color_map: false, image_type: ImageType::Monochrome, color_map_start: 0, color_map_len: 0, - color_map_depth: 0, + color_map_depth: None, x_origin: 0, y_origin: 8, width: 8, height: 8, - pixel_depth: 8, + pixel_depth: Bpp::Bits8, image_origin: ImageOrigin::TopLeft, alpha_channel_depth: 0, } ); // Footer is empty for this image - assert_eq!( - img.footer, - Some(TgaFooter { - extension_area_offset: 0, - developer_directory_offset: 0 - }) - ); + assert_eq!(img.raw_extension_area(), None); + assert_eq!(img.raw_developer_directory(), None); - assert_eq!(img.pixel_data.len(), image_data_len as usize); + assert_eq!(img.raw_image_data().len(), image_data_len as usize); } diff --git a/tinytga/tests/chessboard_4px_raw.rs b/tinytga/tests/chessboard_4px_raw.rs index b1e892825..2879d48b0 100644 --- a/tinytga/tests/chessboard_4px_raw.rs +++ b/tinytga/tests/chessboard_4px_raw.rs @@ -1,44 +1,38 @@ -use tinytga::{ImageOrigin, ImageType, TgaFooter, TgaHeader, TgaRaw}; +use tinytga::{Bpp, ImageOrigin, ImageType, Tga, TgaHeader}; #[test] fn chessboard_4px_raw() { let data = include_bytes!("./chessboard_4px_raw.tga"); - let img = TgaRaw::from_slice(data).unwrap(); + let img = Tga::from_slice_raw(data).unwrap(); - println!("{:#?}", img.header); - println!("{:#?}", img.footer); - println!("Pixel data len {:#?}", img.pixel_data.len()); - println!("Pixel data {:#?}", img.pixel_data); + println!("{:#?}", img.raw_header()); + println!("Raw image data len {:#?}", img.raw_image_data().len()); + println!("Raw image data {:#?}", img.raw_image_data()); assert_eq!( - img.header, + img.raw_header(), TgaHeader { id_len: 0, has_color_map: false, image_type: ImageType::Truecolor, color_map_start: 0, color_map_len: 0, - color_map_depth: 0, + color_map_depth: None, x_origin: 0, y_origin: 4, width: 4, height: 4, - pixel_depth: 24, + pixel_depth: Bpp::Bits24, image_origin: ImageOrigin::TopLeft, alpha_channel_depth: 0, } ); - assert_eq!( - img.footer, - Some(TgaFooter { - extension_area_offset: 0, - developer_directory_offset: 0 - }) - ); + assert_eq!(img.raw_extension_area(), None); + assert_eq!(img.raw_developer_directory(), None); - let pixels = img.into_iter().map(|p| p.color).collect::>(); + let pixels = img.raw_pixels().map(|p| p.color).collect::>(); dbg!(&pixels); diff --git a/tinytga/tests/chessboard_4px_rle.rs b/tinytga/tests/chessboard_4px_rle.rs index 2fda8518c..4148ba736 100644 --- a/tinytga/tests/chessboard_4px_rle.rs +++ b/tinytga/tests/chessboard_4px_rle.rs @@ -1,44 +1,38 @@ -use tinytga::{ImageOrigin, ImageType, TgaFooter, TgaHeader, TgaRaw}; +use tinytga::{Bpp, ImageOrigin, ImageType, Tga, TgaHeader}; #[test] fn chessboard_4px_rle() { let data = include_bytes!("./chessboard_4px_rle.tga"); - let img = TgaRaw::from_slice(data).unwrap(); + let img = Tga::from_slice_raw(data).unwrap(); - println!("{:#?}", img.header); - println!("{:#?}", img.footer); - println!("Pixel data len {:#?}", img.pixel_data.len()); - println!("Pixel data {:#?}", img.pixel_data); + println!("{:#?}", img.raw_header()); + println!("Raw image data len {:#?}", img.raw_image_data().len()); + println!("Raw image data {:#?}", img.raw_image_data()); assert_eq!( - img.header, + img.raw_header(), TgaHeader { id_len: 0, has_color_map: false, image_type: ImageType::RleTruecolor, color_map_start: 0, color_map_len: 0, - color_map_depth: 0, + color_map_depth: None, x_origin: 0, y_origin: 4, width: 4, height: 4, - pixel_depth: 24, + pixel_depth: Bpp::Bits24, image_origin: ImageOrigin::TopLeft, alpha_channel_depth: 0, } ); - assert_eq!( - img.footer, - Some(TgaFooter { - extension_area_offset: 0, - developer_directory_offset: 0 - }) - ); + assert_eq!(img.raw_extension_area(), None); + assert_eq!(img.raw_developer_directory(), None); - let pixels = img.into_iter().map(|p| p.color).collect::>(); + let pixels = img.raw_pixels().map(|p| p.color).collect::>(); // dbg!(&pixels); diff --git a/tinytga/tests/chessboard_rle.rs b/tinytga/tests/chessboard_rle.rs index cff5ecfc4..1fb9c445b 100644 --- a/tinytga/tests/chessboard_rle.rs +++ b/tinytga/tests/chessboard_rle.rs @@ -1,44 +1,38 @@ -use tinytga::{ImageOrigin, ImageType, TgaFooter, TgaHeader, TgaRaw}; +use tinytga::{Bpp, ImageOrigin, ImageType, Tga, TgaHeader}; #[test] fn chessboard_rle() { let data = include_bytes!("./chessboard_rle.tga"); - let img = TgaRaw::from_slice(data).unwrap(); + let img = Tga::from_slice_raw(data).unwrap(); - println!("{:#?}", img.header); - println!("{:#?}", img.footer); - println!("Pixel data len {:#?}", img.pixel_data.len()); - println!("Pixel data {:#?}", img.pixel_data); + println!("{:#?}", img.raw_header()); + println!("Raw image data len {:#?}", img.raw_image_data().len()); + println!("Raw image data {:#?}", img.raw_image_data()); assert_eq!( - img.header, + img.raw_header(), TgaHeader { id_len: 0, has_color_map: false, image_type: ImageType::RleTruecolor, color_map_start: 0, color_map_len: 0, - color_map_depth: 0, + color_map_depth: None, x_origin: 0, y_origin: 8, width: 8, height: 8, - pixel_depth: 24, + pixel_depth: Bpp::Bits24, image_origin: ImageOrigin::TopLeft, alpha_channel_depth: 0, } ); - assert_eq!( - img.footer, - Some(TgaFooter { - extension_area_offset: 0, - developer_directory_offset: 0 - }) - ); + assert_eq!(img.raw_extension_area(), None); + assert_eq!(img.raw_developer_directory(), None); - let pixels = img.into_iter().map(|p| p.color).collect::>(); + let pixels = img.raw_pixels().map(|p| p.color).collect::>(); dbg!(&pixels); diff --git a/tinytga/tests/chessboard_uncompressed.rs b/tinytga/tests/chessboard_uncompressed.rs index 7b34bc9cb..9e3b66a54 100644 --- a/tinytga/tests/chessboard_uncompressed.rs +++ b/tinytga/tests/chessboard_uncompressed.rs @@ -1,46 +1,38 @@ -use tinytga::{ImageOrigin, ImageType, TgaFooter, TgaHeader, TgaRaw}; +use tinytga::{Bpp, ImageOrigin, ImageType, Tga, TgaHeader}; #[test] fn chessboard_uncompressed() { let data = include_bytes!("./chessboard_uncompressed.tga"); - let img = TgaRaw::from_slice(data).unwrap(); + let img = Tga::from_slice_raw(data).unwrap(); - println!("{:#?}", img.header); - println!("{:#?}", img.footer); - println!("Pixel data len {:#?}", img.pixel_data.len()); - println!("Pixel data {:#?}", img.pixel_data); + println!("{:#?}", img.raw_header()); + println!("Raw image data len {:#?}", img.raw_image_data().len()); + println!("Raw image data {:#?}", img.raw_image_data()); assert_eq!( - img.header, + img.raw_header(), TgaHeader { id_len: 0, has_color_map: false, image_type: ImageType::Truecolor, color_map_start: 0, color_map_len: 0, - color_map_depth: 0, + color_map_depth: None, x_origin: 0, y_origin: 8, width: 8, height: 8, - pixel_depth: 24, + pixel_depth: Bpp::Bits24, image_origin: ImageOrigin::TopLeft, alpha_channel_depth: 0, } ); - assert_eq!( - img.footer, - Some(TgaFooter { - extension_area_offset: 0, - developer_directory_offset: 0 - }) - ); - - let pixels = img.into_iter().map(|p| p.color).collect::>(); + assert_eq!(img.raw_extension_area(), None); + assert_eq!(img.raw_developer_directory(), None); - // dbg!(&pixels); + let pixels = img.raw_pixels().map(|p| p.color).collect::>(); assert_eq!(pixels.len(), 8 * 8); assert_eq!( diff --git a/tinytga/tests/coordinates.rs b/tinytga/tests/coordinates.rs index 8fa7b8dfc..56120fd88 100644 --- a/tinytga/tests/coordinates.rs +++ b/tinytga/tests/coordinates.rs @@ -1,68 +1,60 @@ -use tinytga::{ImageOrigin, ImageType, TgaFooter, TgaHeader, TgaRaw}; +use embedded_graphics::prelude::*; +use tinytga::{Bpp, ImageOrigin, ImageType, Tga, TgaHeader}; #[test] fn coordinates() { let data = include_bytes!("./chessboard_4px_raw.tga"); - let img = TgaRaw::from_slice(data).unwrap(); + let img = Tga::from_slice_raw(data).unwrap(); - println!("{:#?}", img.header); - println!("{:#?}", img.footer); - println!("Pixel data len {:#?}", img.pixel_data.len()); - println!("Pixel data {:#?}", img.pixel_data); + println!("{:#?}", img.raw_header()); + println!("Raw image data len {:#?}", img.raw_image_data().len()); + println!("Raw image data {:#?}", img.raw_image_data()); assert_eq!( - img.header, + img.raw_header(), TgaHeader { id_len: 0, has_color_map: false, image_type: ImageType::Truecolor, color_map_start: 0, color_map_len: 0, - color_map_depth: 0, + color_map_depth: None, x_origin: 0, y_origin: 4, width: 4, height: 4, - pixel_depth: 24, + pixel_depth: Bpp::Bits24, image_origin: ImageOrigin::TopLeft, alpha_channel_depth: 0, } ); - assert_eq!( - img.footer, - Some(TgaFooter { - extension_area_offset: 0, - developer_directory_offset: 0 - }) - ); + assert_eq!(img.raw_extension_area(), None); + assert_eq!(img.raw_developer_directory(), None); - let coords = img - .into_iter() - .map(|p| (p.x, p.y)) - .collect::>(); + let coords: Vec<_> = img.raw_pixels().map(|p| p.position).collect(); assert_eq!(coords.len(), 4 * 4); assert_eq!( coords, vec![ - (0, 0), - (1, 0), - (2, 0), - (3, 0), - (0, 1), - (1, 1), - (2, 1), - (3, 1), - (0, 2), - (1, 2), - (2, 2), - (3, 2), - (0, 3), - (1, 3), - (2, 3), - (3, 3), + Point::new(0, 0), + Point::new(1, 0), + Point::new(2, 0), + Point::new(3, 0), + Point::new(0, 1), + Point::new(1, 1), + Point::new(2, 1), + Point::new(3, 1), + Point::new(0, 2), + Point::new(1, 2), + Point::new(2, 2), + Point::new(3, 2), + Point::new(0, 3), + Point::new(1, 3), + Point::new(2, 3), + Point::new(3, 3), ] ); } diff --git a/tinytga/tests/error_color_map.tga b/tinytga/tests/error_color_map.tga new file mode 100644 index 000000000..8c5cbb0cd Binary files /dev/null and b/tinytga/tests/error_color_map.tga differ diff --git a/tinytga/tests/error_no_image_data.tga b/tinytga/tests/error_no_image_data.tga new file mode 100644 index 000000000..cfeeb7117 Binary files /dev/null and b/tinytga/tests/error_no_image_data.tga differ diff --git a/tinytga/tests/error_truncated_image_data.tga b/tinytga/tests/error_truncated_image_data.tga new file mode 100644 index 000000000..f84a06cb8 Binary files /dev/null and b/tinytga/tests/error_truncated_image_data.tga differ diff --git a/tinytga/tests/errors.rs b/tinytga/tests/errors.rs new file mode 100644 index 000000000..a00000337 --- /dev/null +++ b/tinytga/tests/errors.rs @@ -0,0 +1,68 @@ +use embedded_graphics::{ + pixelcolor::{Gray8, Rgb888}, + prelude::*, +}; +use std::iter::repeat; +use tinytga::{ParseError, RawPixel, Tga}; + +#[test] +fn color_map() { + // The color map in "error_color_map.tga" has too many entries and is larger than the file + assert_eq!( + Tga::from_slice_raw(include_bytes!("../tests/error_color_map.tga")), + Err(ParseError::ColorMap) + ); +} + +#[test] +fn image_data_missing() { + // The image data in "error_no_image_data.tga" is missing + let tga = Tga::from_slice_raw(include_bytes!("../tests/error_no_image_data.tga")).unwrap(); + + assert!(tga.raw_image_data().is_empty()); + + let expected: Vec<_> = tga + .bounding_box() + .points() + .map(|p| RawPixel::new(p, 0)) + .collect(); + + let pixels: Vec<_> = tga.raw_pixels().collect(); + + assert_eq!(pixels, expected); +} + +#[test] +fn image_data_truncated() { + // The image data in "error_truncated_image_data.tga" is truncated. + let tga = + Tga::from_slice_raw(include_bytes!("../tests/error_truncated_image_data.tga")).unwrap(); + + assert_eq!(tga.raw_image_data(), &[1, 2, 3, 4, 5, 6, 7, 8]); + + let expected: Vec<_> = tga + .bounding_box() + .points() + .zip((1..=8).chain(repeat(0))) + .map(|(p, c)| RawPixel::new(p, c)) + .collect(); + + let pixels: Vec<_> = tga.raw_pixels().collect(); + + assert_eq!(pixels, expected); +} + +#[test] +fn mismatched_bpp() { + // type2_tl.tga is a 24 BPP image + assert_eq!( + Tga::::from_slice(include_bytes!("../tests/type2_tl.tga")), + Err(ParseError::MismatchedBpp(24)) + ); + + // type3_tl.tga is a 8 BPP image + assert_eq!( + Tga::::from_slice(include_bytes!("../tests/type3_tl.tga")), + Err(ParseError::MismatchedBpp(8)) + ); +} diff --git a/tinytga/tests/image_id.rs b/tinytga/tests/image_id.rs new file mode 100644 index 000000000..3a8c6954d --- /dev/null +++ b/tinytga/tests/image_id.rs @@ -0,0 +1,21 @@ +use tinytga::Tga; + +#[test] +fn has_image_id() { + // image_id.tga contains the image ID: "e-g" + let data = include_bytes!("./image_id.tga"); + + let img = Tga::from_slice_raw(data).unwrap(); + + assert_eq!(img.image_id(), Some("e-g".as_bytes())); +} + +#[test] +fn no_image_id() { + // type1_bl.tga does not contain an image ID + let data = include_bytes!("./type1_bl.tga"); + + let img = Tga::from_slice_raw(data).unwrap(); + + assert_eq!(img.image_id(), None); +} diff --git a/tinytga/tests/image_id.tga b/tinytga/tests/image_id.tga new file mode 100644 index 000000000..fec7ac222 Binary files /dev/null and b/tinytga/tests/image_id.tga differ diff --git a/tinytga/tests/issue_216.rs b/tinytga/tests/issue_216.rs index 077c401e6..104a564d1 100644 --- a/tinytga/tests/issue_216.rs +++ b/tinytga/tests/issue_216.rs @@ -1,16 +1,19 @@ -use tinytga::TgaRaw; +use tinytga::Tga; #[test] fn issue_216() { - let uncompressed = TgaRaw::from_slice(include_bytes!("issue_216_uncompressed.tga")).unwrap(); - let compressed = TgaRaw::from_slice(include_bytes!("issue_216_compressed.tga")).unwrap(); + let uncompressed = Tga::from_slice_raw(include_bytes!("issue_216_uncompressed.tga")).unwrap(); + let compressed = Tga::from_slice_raw(include_bytes!("issue_216_compressed.tga")).unwrap(); - assert_eq!(uncompressed.header.width, compressed.header.width); - assert_eq!(uncompressed.header.height, compressed.header.height); + let uncompressed_header = uncompressed.raw_header(); + let compressed_header = uncompressed.raw_header(); + + assert_eq!(uncompressed_header.width, compressed_header.width); + assert_eq!(uncompressed_header.height, compressed_header.height); assert_eq!( - uncompressed.header.pixel_depth, - compressed.header.pixel_depth + uncompressed_header.pixel_depth, + compressed_header.pixel_depth ); - assert!(uncompressed.into_iter().eq(compressed.into_iter())); + assert!(uncompressed.raw_pixels().eq(compressed.raw_pixels())); } diff --git a/tinytga/tests/types.rs b/tinytga/tests/types.rs index f3b421362..b14f64e52 100644 --- a/tinytga/tests/types.rs +++ b/tinytga/tests/types.rs @@ -1,4 +1,4 @@ -use tinytga::{ImageOrigin, ImageType, TgaHeader, TgaRaw}; +use tinytga::{Bpp, ImageOrigin, ImageType, Tga, TgaHeader}; const HEADER_DEFAULT: TgaHeader = TgaHeader { id_len: 0, @@ -6,206 +6,230 @@ const HEADER_DEFAULT: TgaHeader = TgaHeader { image_type: ImageType::Empty, color_map_start: 0, color_map_len: 0, - color_map_depth: 0, + color_map_depth: None, x_origin: 0, y_origin: 0, width: 9, height: 5, - pixel_depth: 8, + pixel_depth: Bpp::Bits8, image_origin: ImageOrigin::BottomLeft, alpha_channel_depth: 0, }; #[test] fn type1_bl() { - let tga = TgaRaw::from_slice(include_bytes!("../tests/type1_bl.tga")).unwrap(); + let tga = Tga::from_slice_raw(include_bytes!("../tests/type1_bl.tga")).unwrap(); assert_eq!( - tga.header, + tga.raw_header(), TgaHeader { has_color_map: true, image_type: ImageType::ColorMapped, color_map_start: 0, color_map_len: 8, - color_map_depth: 24, + color_map_depth: Some(Bpp::Bits24), ..HEADER_DEFAULT } ); - assert!(tga.footer.is_none()); + assert_eq!(tga.raw_developer_directory(), None); + assert_eq!(tga.raw_extension_area(), None); + assert_eq!(tga.color_bpp(), Bpp::Bits24); } #[test] fn type1_tl() { - let tga = TgaRaw::from_slice(include_bytes!("../tests/type1_tl.tga")).unwrap(); + let tga = Tga::from_slice_raw(include_bytes!("../tests/type1_tl.tga")).unwrap(); assert_eq!( - tga.header, + tga.raw_header(), TgaHeader { has_color_map: true, image_type: ImageType::ColorMapped, color_map_start: 0, color_map_len: 8, - color_map_depth: 24, + color_map_depth: Some(Bpp::Bits24), image_origin: ImageOrigin::TopLeft, ..HEADER_DEFAULT } ); - assert!(tga.footer.is_none()); + assert_eq!(tga.raw_developer_directory(), None); + assert_eq!(tga.raw_extension_area(), None); + assert_eq!(tga.color_bpp(), Bpp::Bits24); } #[test] fn type2_bl() { - let tga = TgaRaw::from_slice(include_bytes!("../tests/type2_bl.tga")).unwrap(); + let tga = Tga::from_slice_raw(include_bytes!("../tests/type2_bl.tga")).unwrap(); assert_eq!( - tga.header, + tga.raw_header(), TgaHeader { image_type: ImageType::Truecolor, - pixel_depth: 24, + pixel_depth: Bpp::Bits24, ..HEADER_DEFAULT } ); - assert!(tga.footer.is_none()); + assert_eq!(tga.raw_developer_directory(), None); + assert_eq!(tga.raw_extension_area(), None); + assert_eq!(tga.color_bpp(), Bpp::Bits24); } #[test] fn type2_tl() { - let tga = TgaRaw::from_slice(include_bytes!("../tests/type2_tl.tga")).unwrap(); + let tga = Tga::from_slice_raw(include_bytes!("../tests/type2_tl.tga")).unwrap(); assert_eq!( - tga.header, + tga.raw_header(), TgaHeader { image_type: ImageType::Truecolor, - pixel_depth: 24, + pixel_depth: Bpp::Bits24, image_origin: ImageOrigin::TopLeft, ..HEADER_DEFAULT } ); - assert!(tga.footer.is_none()); + assert_eq!(tga.raw_developer_directory(), None); + assert_eq!(tga.raw_extension_area(), None); + assert_eq!(tga.color_bpp(), Bpp::Bits24); } #[test] fn type3_bl() { - let tga = TgaRaw::from_slice(include_bytes!("../tests/type3_bl.tga")).unwrap(); + let tga = Tga::from_slice_raw(include_bytes!("../tests/type3_bl.tga")).unwrap(); assert_eq!( - tga.header, + tga.raw_header(), TgaHeader { image_type: ImageType::Monochrome, ..HEADER_DEFAULT } ); - assert!(tga.footer.is_none()); + assert_eq!(tga.raw_developer_directory(), None); + assert_eq!(tga.raw_extension_area(), None); + assert_eq!(tga.color_bpp(), Bpp::Bits8); } #[test] fn type3_tl() { - let tga = TgaRaw::from_slice(include_bytes!("../tests/type3_tl.tga")).unwrap(); + let tga = Tga::from_slice_raw(include_bytes!("../tests/type3_tl.tga")).unwrap(); assert_eq!( - tga.header, + tga.raw_header(), TgaHeader { image_type: ImageType::Monochrome, image_origin: ImageOrigin::TopLeft, ..HEADER_DEFAULT } ); - assert!(tga.footer.is_none()); + assert_eq!(tga.raw_developer_directory(), None); + assert_eq!(tga.raw_extension_area(), None); + assert_eq!(tga.color_bpp(), Bpp::Bits8); } #[test] fn type9_bl() { - let tga = TgaRaw::from_slice(include_bytes!("../tests/type9_bl.tga")).unwrap(); + let tga = Tga::from_slice_raw(include_bytes!("../tests/type9_bl.tga")).unwrap(); assert_eq!( - tga.header, + tga.raw_header(), TgaHeader { has_color_map: true, image_type: ImageType::RleColorMapped, color_map_start: 0, color_map_len: 8, - color_map_depth: 24, + color_map_depth: Some(Bpp::Bits24), ..HEADER_DEFAULT } ); - assert!(tga.footer.is_none()); + assert_eq!(tga.raw_developer_directory(), None); + assert_eq!(tga.raw_extension_area(), None); + assert_eq!(tga.color_bpp(), Bpp::Bits24); } #[test] fn type9_tl() { - let tga = TgaRaw::from_slice(include_bytes!("../tests/type9_tl.tga")).unwrap(); + let tga = Tga::from_slice_raw(include_bytes!("../tests/type9_tl.tga")).unwrap(); assert_eq!( - tga.header, + tga.raw_header(), TgaHeader { has_color_map: true, image_type: ImageType::RleColorMapped, color_map_start: 0, color_map_len: 8, - color_map_depth: 24, + color_map_depth: Some(Bpp::Bits24), image_origin: ImageOrigin::TopLeft, ..HEADER_DEFAULT } ); - assert!(tga.footer.is_none()); + assert_eq!(tga.raw_developer_directory(), None); + assert_eq!(tga.raw_extension_area(), None); + assert_eq!(tga.color_bpp(), Bpp::Bits24); } #[test] fn type10_bl() { - let tga = TgaRaw::from_slice(include_bytes!("../tests/type10_bl.tga")).unwrap(); + let tga = Tga::from_slice_raw(include_bytes!("../tests/type10_bl.tga")).unwrap(); assert_eq!( - tga.header, + tga.raw_header(), TgaHeader { image_type: ImageType::RleTruecolor, - pixel_depth: 24, + pixel_depth: Bpp::Bits24, ..HEADER_DEFAULT } ); - assert!(tga.footer.is_none()); + assert_eq!(tga.raw_developer_directory(), None); + assert_eq!(tga.raw_extension_area(), None); + assert_eq!(tga.color_bpp(), Bpp::Bits24); } #[test] fn type10_tl() { - let tga = TgaRaw::from_slice(include_bytes!("../tests/type10_tl.tga")).unwrap(); + let tga = Tga::from_slice_raw(include_bytes!("../tests/type10_tl.tga")).unwrap(); assert_eq!( - tga.header, + tga.raw_header(), TgaHeader { image_type: ImageType::RleTruecolor, - pixel_depth: 24, + pixel_depth: Bpp::Bits24, image_origin: ImageOrigin::TopLeft, ..HEADER_DEFAULT } ); - assert!(tga.footer.is_none()); + assert_eq!(tga.raw_developer_directory(), None); + assert_eq!(tga.raw_extension_area(), None); + assert_eq!(tga.color_bpp(), Bpp::Bits24); } #[test] fn type11_bl() { - let tga = TgaRaw::from_slice(include_bytes!("../tests/type11_bl.tga")).unwrap(); + let tga = Tga::from_slice_raw(include_bytes!("../tests/type11_bl.tga")).unwrap(); assert_eq!( - tga.header, + tga.raw_header(), TgaHeader { image_type: ImageType::RleMonochrome, ..HEADER_DEFAULT } ); - assert!(tga.footer.is_none()); + assert_eq!(tga.raw_developer_directory(), None); + assert_eq!(tga.raw_extension_area(), None); + assert_eq!(tga.color_bpp(), Bpp::Bits8); } #[test] fn type11_tl() { - let tga = TgaRaw::from_slice(include_bytes!("../tests/type11_tl.tga")).unwrap(); + let tga = Tga::from_slice_raw(include_bytes!("../tests/type11_tl.tga")).unwrap(); assert_eq!( - tga.header, + tga.raw_header(), TgaHeader { image_type: ImageType::RleMonochrome, image_origin: ImageOrigin::TopLeft, ..HEADER_DEFAULT } ); - assert!(tga.footer.is_none()); + assert_eq!(tga.raw_developer_directory(), None); + assert_eq!(tga.raw_extension_area(), None); + assert_eq!(tga.color_bpp(), Bpp::Bits8); } diff --git a/tinytga/tests/ubw8.rs b/tinytga/tests/ubw8.rs index ceef9e357..e36cf5895 100644 --- a/tinytga/tests/ubw8.rs +++ b/tinytga/tests/ubw8.rs @@ -1,43 +1,41 @@ -use tinytga::{ImageOrigin, ImageType, Pixel, TgaFooter, TgaHeader, TgaRaw}; +use tinytga::{Bpp, ImageOrigin, ImageType, Tga, TgaHeader}; #[test] fn ubw8() { let data = include_bytes!("./ubw8.tga"); - let img = TgaRaw::from_slice(data).unwrap(); + let img = Tga::from_slice_raw(data).unwrap(); - println!("{:#?}", img.header); - println!("{:#?}", img.footer); - println!("Pixel data len {:#?}", img.pixel_data.len()); + println!("{:#?}", img.raw_header()); + println!("Raw image data len {:#?}", img.raw_image_data().len()); assert_eq!( - img.header, + img.raw_header(), TgaHeader { id_len: 26, has_color_map: false, image_type: ImageType::Monochrome, color_map_start: 0, color_map_len: 0, - color_map_depth: 0, + color_map_depth: None, x_origin: 0, y_origin: 0, width: 128, height: 128, - pixel_depth: 8, + pixel_depth: Bpp::Bits8, image_origin: ImageOrigin::BottomLeft, alpha_channel_depth: 0 } ); + const TGA_FOOTER_LENGTH: usize = 26; assert_eq!( - img.footer, - Some(TgaFooter { - extension_area_offset: 20526, - developer_directory_offset: 0 - }) + img.raw_extension_area(), + Some(&data[20526..data.len() - TGA_FOOTER_LENGTH]) ); + assert_eq!(img.raw_developer_directory(), None); - let pixels = img.into_iter().collect::>(); + let pixels: Vec<_> = img.raw_pixels().collect(); assert_eq!(pixels.len(), 128 * 128); }