diff options
Diffstat (limited to 'alacritty_terminal/src/graphics')
-rw-r--r-- | alacritty_terminal/src/graphics/mod.rs | 146 | ||||
-rw-r--r-- | alacritty_terminal/src/graphics/sixel.rs | 772 |
2 files changed, 918 insertions, 0 deletions
diff --git a/alacritty_terminal/src/graphics/mod.rs b/alacritty_terminal/src/graphics/mod.rs new file mode 100644 index 00000000..a285228f --- /dev/null +++ b/alacritty_terminal/src/graphics/mod.rs @@ -0,0 +1,146 @@ +//! This module implements the logic to manage graphic items included in a +//! `Grid` instance. + +pub mod sixel; + +use std::mem; +use std::sync::{Arc, Weak}; + +use parking_lot::Mutex; +use serde::{Deserialize, Serialize}; + +use crate::term::color::Rgb; + +/// Max allowed dimensions (width, height) for the graphic, in pixels. +pub const MAX_GRAPHIC_DIMENSIONS: [usize; 2] = [4096, 4096]; + +/// Unique identifier for every graphic added to a grid. +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug, Copy, Hash, PartialOrd, Ord)] +pub struct GraphicId(u64); + +/// Reference to a texture stored in the display. +/// +/// When all references to a single texture are removed, its identifier is +/// added to the remove queue. +#[derive(Clone, Debug)] +pub struct TextureRef { + /// Graphic identifier. + pub id: GraphicId, + + /// Queue to track removed references. + pub remove_queue: Weak<Mutex<Vec<GraphicId>>>, +} + +impl PartialEq for TextureRef { + fn eq(&self, t: &Self) -> bool { + // Ignore remove_queue. + self.id == t.id + } +} + +impl Eq for TextureRef {} + +impl Drop for TextureRef { + fn drop(&mut self) { + if let Some(remove_queue) = self.remove_queue.upgrade() { + remove_queue.lock().push(self.id); + } + } +} + +/// Graphic data stored in a single cell. +#[derive(Eq, PartialEq, Clone, Debug)] +pub struct GraphicCell { + /// Texture to draw the graphic in this cell. + pub texture: Arc<TextureRef>, + + /// Offset in the x direction. + pub offset_x: u16, + + /// Offset in the y direction. + pub offset_y: u16, +} + +impl GraphicCell { + /// Graphic identifier of the texture in this cell. + #[inline] + pub fn graphic_id(&self) -> GraphicId { + self.texture.id + } +} + +/// Specifies the format of the pixel data. +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug, Copy)] +pub enum ColorType { + /// 3 bytes per pixel (red, green, blue). + Rgb, + + /// 4 bytes per pixel (red, green, blue, alpha). + Rgba, +} + +/// Defines a single graphic read from the PTY. +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] +pub struct GraphicData { + /// Graphics identifier. + pub id: GraphicId, + + /// Width, in pixels, of the graphic. + pub width: usize, + + /// Height, in pixels, of the graphic. + pub height: usize, + + /// Color type of the pixels. + pub color_type: ColorType, + + /// Pixels data. + pub pixels: Vec<u8>, +} + +/// Queues to add or to remove the textures in the display. +pub struct UpdateQueues { + /// Graphics read from the PTY. + pub pending: Vec<GraphicData>, + + /// Graphics removed from the grid. + pub remove_queue: Vec<GraphicId>, +} + +/// Track changes in the grid to add or to remove graphics. +#[derive(Clone, Debug, Default)] +pub struct Graphics { + /// Last generated identifier. + pub last_id: u64, + + /// New graphics, received from the PTY. + pub pending: Vec<GraphicData>, + + /// Graphics removed from the grid. + pub remove_queue: Arc<Mutex<Vec<GraphicId>>>, + + /// Shared palette for Sixel graphics. + pub sixel_shared_palette: Option<Vec<Rgb>>, +} + +impl Graphics { + /// Generate a new graphic identifier. + pub fn next_id(&mut self) -> GraphicId { + self.last_id += 1; + GraphicId(self.last_id) + } + + /// Get queues to update graphics in the grid. + /// + /// If all queues are empty, it returns `None`. + pub fn take_queues(&mut self) -> Option<UpdateQueues> { + let mut remove_queue = self.remove_queue.lock(); + if remove_queue.is_empty() && self.pending.is_empty() { + return None; + } + + let remove_queue = mem::take(&mut *remove_queue); + + Some(UpdateQueues { pending: mem::take(&mut self.pending), remove_queue }) + } +} diff --git a/alacritty_terminal/src/graphics/sixel.rs b/alacritty_terminal/src/graphics/sixel.rs new file mode 100644 index 00000000..d617e53b --- /dev/null +++ b/alacritty_terminal/src/graphics/sixel.rs @@ -0,0 +1,772 @@ +//! This module implements a parser for the Sixel protocol, and it is based on the +//! chapter [SIXEL GRAPHICS EXTENSION] of the DEC reference manual. +//! +//! [SIXEL GRAPHICS EXTENSION]: https://archive.org/details/bitsavers_decstandar0VideoSystemsReferenceManualDec91_74264381/page/n907/mode/2up +//! +//! # Limitations +//! +//! The parser have the following limitations: +//! +//! * A single image can use up to 1024 different colors. +//! +//! The Sixel reference requires 256, but allow more colors. +//! +//! * Image dimensions are limited to 4096 x 4096. +//! +//! * Pixel aspect ratio parameters are ignored. +//! +//! The Sixel references specifies some parameters to change the pixel +//! aspect ratio, but multiple implementations always use 1:1, so these +//! parameters have no real effect. +use std::cmp::max; +use std::fmt; +use std::mem; + +use crate::graphics::{ColorType, GraphicData, GraphicId, MAX_GRAPHIC_DIMENSIONS}; +use crate::term::color::Rgb; + +use log::trace; +use vte::Params; + +/// Type for color registers. +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] +struct ColorRegister(u16); + +/// Number of color registers. +pub const MAX_COLOR_REGISTERS: usize = 1024; + +/// Color register for transparent pixels. +const REG_TRANSPARENT: ColorRegister = ColorRegister(u16::MAX); + +/// Number of parameters allowed in a single Sixel command. +const MAX_COMMAND_PARAMS: usize = 5; + +#[derive(Debug)] +pub enum Error { + /// Image dimensions are too big. + TooBigImage { width: usize, height: usize }, + + /// A component in a color introducer is not valid. + InvalidColorComponent { register: u16, component_value: u16 }, + + /// The coordinate system to define the color register is not valid. + InvalidColorCoordinateSystem { register: u16, coordinate_system: u16 }, +} + +impl fmt::Display for Error { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::TooBigImage { width, height } => { + write!(fmt, "The image dimensions are too big ({}, {})", width, height) + }, + + Error::InvalidColorComponent { register, component_value } => { + write!(fmt, "Invalid color component {} for register {}", component_value, register) + }, + + Error::InvalidColorCoordinateSystem { register, coordinate_system } => { + write!( + fmt, + "Invalid color coordinate system {} for register {}", + coordinate_system, register + ) + }, + } + } +} + +/// Commands found in the data stream. +#[derive(Debug)] +enum SixelCommand { + /// Specifies a repeat count before a sixel. + /// + /// Its only parameter is the repeat count. + RepeatIntroducer, + + /// Defines raster attributes for the following data. + /// + /// It expects 4 parameters: + /// + /// 1. Pixel aspect ratio numerator (relative height). + /// 2. Pixel aspect ratio denominator (relative width). + /// 3. Horizontal Extent. + /// 4. Vertical Extent. + SetRasterAttributes, + + /// Starts a color selection sequence. + /// + /// The first parameter is the register number. + /// + /// Optionally, it can receive 4 more parameters: + /// + /// 1. Color coordinate system. `1` for HLS, `2` for RGB. + /// 2. Hue angle, or red. + /// 3. Lightness, or green. + /// 4. Saturation, or blue. + ColorIntroducer, + + /// Moves the active position to the graphic left margin. + CarriageReturn, + + /// Moves the active position to the graphic left margin + /// and one row of sixels. + NextLine, +} + +/// Parser for commands found in the picture definition. +#[derive(Debug)] +struct CommandParser { + /// Active command. + command: SixelCommand, + + /// Parameter values. + /// + /// If a value is greater than `u16::MAX`, it will be kept as `u16::MAX`. + /// + /// Parameters after `MAX_COMMAND_PARAMS` are ignored. + params: [u16; MAX_COMMAND_PARAMS], + + /// Current position. + params_position: usize, +} + +impl CommandParser { + fn new(command: SixelCommand) -> CommandParser { + CommandParser { command, params: [0; MAX_COMMAND_PARAMS], params_position: 0 } + } + + fn put(&mut self, byte: u8) { + let pos = self.params_position; + if pos < MAX_COMMAND_PARAMS { + match byte { + b'0'..=b'9' => { + self.params[pos] = + self.params[pos].saturating_mul(10).saturating_add((byte - b'0') as u16); + }, + + b';' => { + self.params_position += 1; + }, + + _ => (), // Ignore unknown bytes. + } + } + } + + /// Apply the execution of the active command to the parser. + fn finish(self, parser: &mut Parser) -> Result<(), Error> { + match self.command { + SixelCommand::RepeatIntroducer => { + parser.repeat_count = self.params[0] as usize; + }, + + SixelCommand::SetRasterAttributes => { + if self.params_position >= 3 { + let width = self.params[2] as usize; + let height = self.params[3] as usize; + parser.ensure_size(width, height)?; + } + }, + + SixelCommand::ColorIntroducer => { + let register = ColorRegister(self.params[0]); + + if self.params_position >= 4 { + macro_rules! p { + ($index:expr) => { + match self.params[$index] { + x if x <= 100 => x, + x => { + return Err(Error::InvalidColorComponent { + register: register.0, + component_value: x, + }) + }, + } + }; + } + + let (r, g, b) = match self.params[1] { + // HLS. + 1 => hls_to_rgb(p!(2), p!(3), p!(4)), + + // RGB. + 2 => (p!(2), p!(3), p!(4)), + + // Invalid coordinate system. + x => { + return Err(Error::InvalidColorCoordinateSystem { + register: register.0, + coordinate_system: x, + }) + }, + }; + + parser.set_color_register(register, r, g, b); + } + + parser.selected_color_register = register; + }, + + SixelCommand::CarriageReturn => { + parser.x = 0; + }, + + SixelCommand::NextLine => { + parser.x = 0; + parser.y += 6; + }, + } + + Ok(()) + } +} + +/// A group of 6 vertical pixels. +struct Sixel(u8); + +impl Sixel { + /// Create a new sixel. + /// + /// It expects the byte value from the picture definition stream. + #[inline] + fn new(byte: u8) -> Sixel { + debug_assert!((0x3F..=0x7E).contains(&byte)); + Sixel(byte - 0x3F) + } + + /// Return how many rows are printed in the sixel. + #[inline] + fn height(&self) -> usize { + 8 - self.0.leading_zeros() as usize + } + + /// Return an iterator to get dots in the sixel. + #[inline] + fn dots(&self) -> impl Iterator<Item = bool> { + let sixel = self.0; + (0..6).map(move |position| sixel & (1 << position) != 0) + } +} + +/// Parser of the picture definition in a Sixel data stream. +#[derive(Default, Debug)] +pub struct Parser { + /// Active command to be parsed. + command_parser: Option<CommandParser>, + + /// Current picture width. + width: usize, + + /// Current picture height. + height: usize, + + /// Current picture pixels. + pixels: Vec<ColorRegister>, + + /// Indicates the register color for empty pixels. + background: ColorRegister, + + /// RGB values for every register. + color_registers: Vec<Rgb>, + + /// Selected color register. + selected_color_register: ColorRegister, + + /// Repeat count for the next sixel. + repeat_count: usize, + + /// Horizontal position of the active sixel. + x: usize, + + /// Vertical position of the active sixel. + y: usize, +} + +impl Parser { + /// Creates a new parser. + pub fn new(params: &Params, shared_palette: Option<Vec<Rgb>>) -> Parser { + trace!("Start Sixel parser"); + + let mut parser = Parser::default(); + + // According to the Sixel reference, the second parameter (Ps2) is + // the background selector. It controls how to show pixels without + // an explicit color, and it accepts the following values: + // + // 0 device default action + // 1 no action (don't change zero value pixels) + // 2 set zero value pixels to background color + // + // We replicate the xterm's behaviour: + // + // - If it is set to `1`, the background is transparent. + // - For any other value, the background is the color register 0. + + let ps2 = params.iter().nth(1).and_then(|param| param.iter().next().copied()).unwrap_or(0); + parser.background = if ps2 == 1 { REG_TRANSPARENT } else { ColorRegister(0) }; + + if let Some(color_registers) = shared_palette { + parser.color_registers = color_registers; + } else { + init_color_registers(&mut parser); + } + + parser + } + + /// Parse a byte from the Sixel stream. + pub fn put(&mut self, byte: u8) -> Result<(), Error> { + match byte { + b'!' => self.start_command(SixelCommand::RepeatIntroducer)?, + + b'"' => self.start_command(SixelCommand::SetRasterAttributes)?, + + b'#' => self.start_command(SixelCommand::ColorIntroducer)?, + + b'$' => self.start_command(SixelCommand::CarriageReturn)?, + + b'-' => self.start_command(SixelCommand::NextLine)?, + + b'0'..=b'9' | b';' => { + if let Some(command_parser) = &mut self.command_parser { + command_parser.put(byte); + } + }, + + 0x3F..=0x7E => self.add_sixel(Sixel::new(byte))?, + + _ => { + // Invalid bytes are ignored, but we still have to finish any + // active command. + + self.finish_command()?; + }, + } + + Ok(()) + } + + #[inline] + fn start_command(&mut self, command: SixelCommand) -> Result<(), Error> { + self.finish_command()?; + self.command_parser = Some(CommandParser::new(command)); + Ok(()) + } + + #[inline] + fn finish_command(&mut self) -> Result<(), Error> { + if let Some(command_parser) = self.command_parser.take() { + command_parser.finish(self)?; + } + + Ok(()) + } + + /// Set the RGB color for a register. + /// + /// Color components are expected to be in the range of 0..=100. + fn set_color_register(&mut self, register: ColorRegister, r: u16, g: u16, b: u16) { + let register = register.0 as usize; + + if register >= MAX_COLOR_REGISTERS { + return; + } + + if self.color_registers.len() <= register { + self.color_registers.resize(register + 1, Rgb { r: 0, g: 0, b: 0 }) + } + + let r = ((r * 255 + 50) / 100) as u8; + let g = ((g * 255 + 50) / 100) as u8; + let b = ((b * 255 + 50) / 100) as u8; + self.color_registers[register] = Rgb { r, g, b }; + } + + /// Check if the current picture is big enough for the given dimensions. If + /// not, the picture is resized. + fn ensure_size(&mut self, width: usize, height: usize) -> Result<(), Error> { + // Do nothing if the current picture is big enough. + if self.width >= width && self.height >= height { + return Ok(()); + } + + if width > MAX_GRAPHIC_DIMENSIONS[0] || height > MAX_GRAPHIC_DIMENSIONS[1] { + return Err(Error::TooBigImage { width, height }); + } + + trace!( + "Set Sixel image dimensions to {}x{}", + max(self.width, width), + max(self.height, height), + ); + + // If there is no current picture, creates a new one. + if self.pixels.is_empty() { + self.width = width; + self.height = height; + self.pixels = vec![self.background; width * height]; + return Ok(()); + } + + // If current width is big enough, we only need to add more pixels + // after the current buffer. + if self.width >= width { + self.pixels.resize(height * self.width, self.background); + self.height = height; + return Ok(()); + } + + // At this point, we know that the new width is greater than the + // current one, so we have to extend the buffer and move the rows to + // their new positions. + let height = usize::max(height, self.height); + + self.pixels.resize(height * width, self.background); + + for y in (0..self.height).rev() { + for x in (0..self.width).rev() { + let old = y * self.width + x; + let new = y * width + x; + self.pixels.swap(old, new); + } + } + + self.width = width; + self.height = height; + Ok(()) + } + + /// Add a sixel using the selected color register, and move the active + /// position. + fn add_sixel(&mut self, sixel: Sixel) -> Result<(), Error> { + self.finish_command()?; + + // Take the repeat count and reset it. + // + // `max` function is used because the Sixel reference specifies + // that a repeat count of zero implies a repeat count of 1. + let repeat = max(1, mem::take(&mut self.repeat_count)); + + self.ensure_size(self.x + repeat, self.y + sixel.height())?; + + if sixel.0 != 0 { + let mut index = self.width * self.y + self.x; + for dot in sixel.dots() { + if dot { + for pixel in &mut self.pixels[index..index + repeat] { + *pixel = self.selected_color_register; + } + } + + index += self.width; + } + } + + self.x += repeat; + + Ok(()) + } + + /// Returns the final graphic to append to the grid, with the palette + /// built in the process. + pub fn finish(mut self) -> Result<(GraphicData, Vec<Rgb>), Error> { + self.finish_command()?; + + trace!( + "Finish Sixel parser: width={}, height={}, color_registers={}", + self.width, + self.height, + self.color_registers.len() + ); + + let mut rgba_pixels = Vec::with_capacity(self.pixels.len() * 4); + + for ®ister in &self.pixels { + let pixel = { + if register == REG_TRANSPARENT { + [0; 4] + } else { + match self.color_registers.get(register.0 as usize) { + None => [0, 0, 0, 255], + Some(color) => [color.r, color.g, color.b, 255], + } + } + }; + + rgba_pixels.extend_from_slice(&pixel); + } + + let data = GraphicData { + id: GraphicId(0), + height: self.height, + width: self.width, + color_type: ColorType::Rgba, + pixels: rgba_pixels, + }; + + Ok((data, self.color_registers)) + } +} + +/// Compute a RGB value from HLS. +/// +/// Input and output values are in the range of `0..=100`. +/// +/// The implementation is a direct port of the same function in the +/// xterm's code. +#[allow(clippy::many_single_char_names)] +fn hls_to_rgb(h: u16, l: u16, s: u16) -> (u16, u16, u16) { + if s == 0 { + return (l, l, l); + } + + let hs = ((h + 240) / 60) % 6; + let lv = l as f64 / 100.0; + + let c2 = f64::abs((2.0 * lv as f64) - 1.0); + let c = (1.0 - c2) * (s as f64 / 100.0); + let x = if hs & 1 == 1 { c } else { 0.0 }; + + let rgb = match hs { + 0 => (c, x, 0.), + 1 => (x, c, 0.), + 2 => (0., c, x), + 3 => (0., x, c), + 4 => (x, 0., c), + _ => (c, 0., c), + }; + + fn clamp(x: f64) -> u16 { + let x = x * 100. + 0.5; + if x > 100. { + 100 + } else if x < 0. { + 0 + } else { + x as u16 + } + } + + let m = lv - 0.5 * c; + let r = clamp(rgb.0 + m); + let g = clamp(rgb.1 + m); + let b = clamp(rgb.2 + m); + + (r, g, b) +} + +/// Initialize the color registers using the colors from the VT-340 terminal. +/// +/// There is no official documentation about these colors, but multiple Sixel +/// implementations assume this palette. +fn init_color_registers(parser: &mut Parser) { + parser.set_color_register(ColorRegister(0), 0, 0, 0); + parser.set_color_register(ColorRegister(1), 20, 20, 80); + parser.set_color_register(ColorRegister(2), 80, 13, 13); + parser.set_color_register(ColorRegister(3), 20, 80, 20); + parser.set_color_register(ColorRegister(4), 80, 20, 80); + parser.set_color_register(ColorRegister(5), 20, 80, 80); + parser.set_color_register(ColorRegister(6), 80, 80, 20); + parser.set_color_register(ColorRegister(7), 53, 53, 53); + parser.set_color_register(ColorRegister(8), 26, 26, 26); + parser.set_color_register(ColorRegister(9), 33, 33, 60); + parser.set_color_register(ColorRegister(10), 60, 26, 26); + parser.set_color_register(ColorRegister(11), 33, 60, 33); + parser.set_color_register(ColorRegister(12), 60, 33, 60); + parser.set_color_register(ColorRegister(13), 33, 60, 60); + parser.set_color_register(ColorRegister(14), 60, 60, 33); + parser.set_color_register(ColorRegister(15), 80, 80, 80); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::path::Path; + + macro_rules! put_bytes { + ($parser:expr, $data:expr) => { + #[allow(clippy::string_lit_as_bytes)] + for &byte in $data.as_bytes() { + let _ = $parser.put(byte); + } + }; + } + + #[test] + fn parse_command_parameters() { + let mut command_parser = CommandParser::new(SixelCommand::ColorIntroducer); + put_bytes!(command_parser, "65535;1;2;3;4;5"); + + assert_eq!(command_parser.params_position, 5); + assert_eq!(command_parser.params[0], 65535); + assert_eq!(command_parser.params[1], 1); + assert_eq!(command_parser.params[2], 2); + assert_eq!(command_parser.params[3], 3); + assert_eq!(command_parser.params[4], 4); + } + + #[test] + fn set_color_registers() { + let mut parser = Parser::default(); + put_bytes!(parser, "#1;2;30;100;0#200;1;20;75;50."); + + assert!(parser.color_registers.len() >= 200); + + assert_eq!(parser.color_registers[1], Rgb { r: 77, g: 255, b: 0 }); + assert_eq!(parser.color_registers[200], Rgb { r: 161, g: 161, b: 224 }); + + assert_eq!(parser.selected_color_register.0, 200); + } + + #[test] + fn convert_hls_colors() { + // This test converts values from HLS to RBG, and compares those + // results with the values generated by the xterm implementation + // of the same function. + + assert_eq!(hls_to_rgb(100, 60, 60), (84, 36, 84)); + assert_eq!(hls_to_rgb(60, 100, 60), (100, 100, 100)); + assert_eq!(hls_to_rgb(30, 30, 60), (12, 12, 48)); + assert_eq!(hls_to_rgb(100, 90, 100), (100, 80, 100)); + assert_eq!(hls_to_rgb(100, 0, 90), (0, 0, 0)); + assert_eq!(hls_to_rgb(0, 90, 30), (87, 87, 93)); + assert_eq!(hls_to_rgb(60, 0, 60), (0, 0, 0)); + assert_eq!(hls_to_rgb(30, 0, 0), (0, 0, 0)); + assert_eq!(hls_to_rgb(30, 90, 30), (87, 87, 93)); + assert_eq!(hls_to_rgb(30, 30, 30), (21, 21, 39)); + assert_eq!(hls_to_rgb(90, 100, 60), (100, 100, 100)); + assert_eq!(hls_to_rgb(0, 0, 0), (0, 0, 0)); + assert_eq!(hls_to_rgb(30, 0, 90), (0, 0, 0)); + assert_eq!(hls_to_rgb(100, 60, 90), (96, 24, 96)); + assert_eq!(hls_to_rgb(30, 30, 0), (30, 30, 30)); + } + + #[test] + fn resize_picture() -> Result<(), Error> { + let mut parser = Parser { background: REG_TRANSPARENT, ..Parser::default() }; + + const WIDTH: usize = 30; + const HEIGHT: usize = 20; + + // Initialize a transparent picture with Set Raster Attributes. + put_bytes!(parser, format!("\"1;1;{};{}.", WIDTH, HEIGHT)); + + assert_eq!(parser.width, WIDTH); + assert_eq!(parser.height, HEIGHT); + assert_eq!(parser.pixels.len(), WIDTH * HEIGHT); + + assert!(parser.pixels.iter().all(|&pixel| pixel == REG_TRANSPARENT)); + + // Fill each row with a different color register. + for (n, row) in parser.pixels.chunks_mut(WIDTH).enumerate() { + row.iter_mut().for_each(|pixel| *pixel = ColorRegister(n as u16)); + } + + // Increase height. + // + // New rows must be transparent. + parser.ensure_size(WIDTH, HEIGHT + 5)?; + + assert_eq!(parser.width, WIDTH); + assert_eq!(parser.height, HEIGHT + 5); + assert_eq!(parser.pixels.len(), WIDTH * (HEIGHT + 5)); + + for (n, row) in parser.pixels.chunks(WIDTH).enumerate() { + let expected = if n < HEIGHT { ColorRegister(n as u16) } else { REG_TRANSPARENT }; + assert!(row.iter().all(|pixel| *pixel == expected)); + } + + // Increase both width and height. + // + // New rows and columns must be transparent. + parser.ensure_size(WIDTH + 5, HEIGHT + 10)?; + + assert_eq!(parser.width, WIDTH + 5); + assert_eq!(parser.height, HEIGHT + 10); + assert_eq!(parser.pixels.len(), (WIDTH + 5) * (HEIGHT + 10)); + + for (n, row) in parser.pixels.chunks(WIDTH + 5).enumerate() { + if n < HEIGHT { + assert!(row[..WIDTH].iter().all(|pixel| *pixel == ColorRegister(n as u16))); + assert!(row[WIDTH..].iter().all(|pixel| *pixel == REG_TRANSPARENT)); + } else { + assert!(row.iter().all(|pixel| *pixel == REG_TRANSPARENT)); + } + } + + Ok(()) + } + + #[test] + fn sixel_height() { + assert_eq!(Sixel(0b000000).height(), 0); + assert_eq!(Sixel(0b000001).height(), 1); + assert_eq!(Sixel(0b000100).height(), 3); + assert_eq!(Sixel(0b000101).height(), 3); + assert_eq!(Sixel(0b101111).height(), 6); + } + + #[test] + fn sixel_positions() { + macro_rules! dots { + ($sixel:expr) => { + Sixel($sixel).dots().collect::<Vec<_>>() + }; + } + + assert_eq!(dots!(0b000000), &[false, false, false, false, false, false,]); + assert_eq!(dots!(0b000001), &[true, false, false, false, false, false,]); + assert_eq!(dots!(0b000100), &[false, false, true, false, false, false,]); + assert_eq!(dots!(0b000101), &[true, false, true, false, false, false,]); + assert_eq!(dots!(0b101111), &[true, true, true, true, false, true,]); + } + + #[test] + fn load_sixel_files() { + let images_dir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/sixel")); + + let test_images = ["testimage_im6", "testimage_libsixel", "testimage_ppmtosixel"]; + + for test_image in &test_images { + // Load Sixel data. + let mut sixel = { + let mut path = images_dir.join(test_image); + path.set_extension("sixel"); + fs::read(path).unwrap() + }; + + // Remove DCS sequence from Sixel data. + let dcs_end = sixel.iter().position(|&byte| byte == b'q').unwrap(); + sixel.drain(..=dcs_end); + + // Remove ST, which can be either "1B 5C" or "9C". To simplify the + // code, we assume that any ESC byte is the start of the ST. + if let Some(pos) = sixel.iter().position(|&b| b == 0x1B || b == 0x9C) { + sixel.truncate(pos); + } + + // Parse the data and get the GraphicData item. + let mut parser = Parser::default(); + for byte in sixel { + parser.put(byte).unwrap(); + } + + let graphics = parser.finish().unwrap().0; + + assert_eq!(graphics.width, 64); + assert_eq!(graphics.height, 64); + + // Read the RGBA stream generated by ImageMagick and compare it + // with our picture. + let expected_rgba = { + let mut path = images_dir.join(test_image); + path.set_extension("rgba"); + fs::read(path).unwrap() + }; + + assert_eq!(graphics.pixels, expected_rgba); + } + } +} |