#![forbid(unsafe_code)]
#[cfg(feature = "image")]
use image_rs::DynamicImage;
use std::cmp;
#[cfg(feature = "glyph")]
pub mod glyph;
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Color {
pub r: f64,
pub g: f64,
pub b: f64,
pub a: f64,
}
impl Color {
pub const TRANSPARENT: Self = Self {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.0,
};
pub const BLACK: Self = Self {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
pub const WHITE: Self = Self {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
};
}
#[cfg(feature = "wgpu")]
impl From<Color> for wgpu::Color {
fn from(c: Color) -> Self {
wgpu::Color {
r: c.r,
g: c.g,
b: c.b,
a: c.a,
}
}
}
#[cfg(feature = "wgpu")]
impl From<wgpu::Color> for Color {
fn from(c: wgpu::Color) -> Self {
Color {
r: c.r,
g: c.g,
b: c.b,
a: c.a,
}
}
}
pub const MAX_GRAPHIC_DIMENSIONS: [usize; 2] = [4096, 4096];
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub struct Graphic {
pub id: GraphicId,
pub offset_x: u16,
pub offset_y: u16,
}
#[inline]
pub fn kitty_image_key(image_id: u32) -> u64 {
image_id as u64
}
#[inline]
pub fn atlas_image_key(graphic_id: u64) -> u64 {
(1u64 << 32) + graphic_id
}
#[derive(Debug, Clone)]
pub struct GraphicOverlay {
pub image_id: u64,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub z_index: i32,
pub source_rect: [f32; 4],
}
impl GraphicOverlay {
pub const FULL_SOURCE_RECT: [f32; 4] = [0.0, 0.0, 1.0, 1.0];
}
#[derive(Eq, PartialEq, Clone, Debug, Copy, Hash, PartialOrd, Ord)]
pub struct GraphicId(pub u64);
impl GraphicId {
#[inline]
pub const fn new(value: u64) -> Self {
Self(value)
}
#[inline]
pub const fn get(self) -> u64 {
self.0
}
}
#[derive(Eq, PartialEq, Clone, Debug, Copy)]
pub enum ColorType {
Rgb,
Rgba,
}
#[derive(Eq, PartialEq, Clone, Debug)]
pub struct GraphicData {
pub id: GraphicId,
pub width: usize,
pub height: usize,
pub color_type: ColorType,
pub pixels: Vec<u8>,
pub is_opaque: bool,
pub resize: Option<ResizeCommand>,
pub display_width: Option<usize>,
pub display_height: Option<usize>,
pub transmit_time: std::time::Instant,
}
impl GraphicData {
#[inline]
pub fn maybe_transparent(&self) -> bool {
!self.is_opaque && self.color_type == ColorType::Rgba
}
pub fn is_filled(&self, x: usize, y: usize, width: usize, height: usize) -> bool {
if x + width >= self.width || y + height >= self.height {
return false;
}
if !self.maybe_transparent() {
return true;
}
debug_assert!(self.color_type == ColorType::Rgba);
for offset_y in y..y + height {
let offset = offset_y * self.width * 4;
let row = &self.pixels[offset..offset + width * 4];
if row.chunks_exact(4).any(|pixel| pixel.last() != Some(&255)) {
return false;
}
}
true
}
#[cfg(feature = "image")]
pub fn from_dynamic_image(id: GraphicId, image: DynamicImage) -> Self {
let color_type;
let width;
let height;
let pixels;
match image {
DynamicImage::ImageRgba8(image) => {
color_type = ColorType::Rgba;
width = image.width() as usize;
height = image.height() as usize;
pixels = image.into_raw();
}
_ => {
let image = image.into_rgba8();
color_type = ColorType::Rgba;
width = image.width() as usize;
height = image.height() as usize;
pixels = image.into_raw();
}
}
GraphicData {
id,
width,
height,
color_type,
pixels,
is_opaque: false,
resize: None,
display_width: None,
display_height: None,
transmit_time: std::time::Instant::now(),
}
}
pub fn compute_display_dimensions(
&self,
cell_width: usize,
cell_height: usize,
view_width: usize,
view_height: usize,
) -> (usize, usize) {
let resize = match self.resize {
Some(resize) => resize,
None => return (self.width, self.height),
};
if (resize.width == ResizeParameter::Auto
&& resize.height == ResizeParameter::Auto)
|| self.height == 0
|| self.width == 0
{
return (self.width, self.height);
}
let mut width = match resize.width {
ResizeParameter::Auto => 1,
ResizeParameter::Pixels(n) => n as usize,
ResizeParameter::Cells(n) => n as usize * cell_width,
ResizeParameter::WindowPercent(n) => n as usize * view_width / 100,
};
let mut height = match resize.height {
ResizeParameter::Auto => 1,
ResizeParameter::Pixels(n) => n as usize,
ResizeParameter::Cells(n) => n as usize * cell_height,
ResizeParameter::WindowPercent(n) => n as usize * view_height / 100,
};
if width == 0 || height == 0 {
return (self.width, self.height);
}
if resize.width == ResizeParameter::Auto {
width =
(self.width as f64 * height as f64 / self.height as f64).round() as usize;
}
if resize.height == ResizeParameter::Auto {
height =
(self.height as f64 * width as f64 / self.width as f64).round() as usize;
}
width = cmp::min(width, MAX_GRAPHIC_DIMENSIONS[0]);
height = cmp::min(height, MAX_GRAPHIC_DIMENSIONS[1]);
if resize.preserve_aspect_ratio {
let scale_w = width as f64 / self.width as f64;
let scale_h = height as f64 / self.height as f64;
let scale = scale_w.min(scale_h);
width = (self.width as f64 * scale).round() as usize;
height = (self.height as f64 * scale).round() as usize;
}
(width, height)
}
#[cfg(feature = "image")]
pub fn resized(
self,
cell_width: usize,
cell_height: usize,
view_width: usize,
view_height: usize,
) -> Option<Self> {
let resize = match self.resize {
Some(resize) => resize,
None => return Some(self),
};
if (resize.width == ResizeParameter::Auto
&& resize.height == ResizeParameter::Auto)
|| self.height == 0
|| self.width == 0
{
return Some(self);
}
let mut width = match resize.width {
ResizeParameter::Auto => 1,
ResizeParameter::Pixels(n) => n as usize,
ResizeParameter::Cells(n) => n as usize * cell_width,
ResizeParameter::WindowPercent(n) => n as usize * view_width / 100,
};
let mut height = match resize.height {
ResizeParameter::Auto => 1,
ResizeParameter::Pixels(n) => n as usize,
ResizeParameter::Cells(n) => n as usize * cell_height,
ResizeParameter::WindowPercent(n) => n as usize * view_height / 100,
};
if width == 0 || height == 0 {
return None;
}
if resize.width == ResizeParameter::Auto {
width = self.width * height / self.height;
}
if resize.height == ResizeParameter::Auto {
height = self.height * width / self.width;
}
width = cmp::min(width, MAX_GRAPHIC_DIMENSIONS[0]);
height = cmp::min(height, MAX_GRAPHIC_DIMENSIONS[1]);
tracing::trace!("Resize new graphic to width={}, height={}", width, height,);
let dynimage = match self.color_type {
ColorType::Rgb => {
let buffer = image_rs::RgbImage::from_raw(
self.width as u32,
self.height as u32,
self.pixels,
)?;
DynamicImage::ImageRgb8(buffer)
}
ColorType::Rgba => {
let buffer = image_rs::RgbaImage::from_raw(
self.width as u32,
self.height as u32,
self.pixels,
)?;
DynamicImage::ImageRgba8(buffer)
}
};
let width = width as u32;
let height = height as u32;
let filter = image_rs::imageops::FilterType::Triangle;
let new_image = if resize.preserve_aspect_ratio {
dynimage.resize(width, height, filter)
} else {
dynimage.resize_exact(width, height, filter)
};
Some(Self::from_dynamic_image(self.id, new_image))
}
}
#[derive(Eq, PartialEq, Clone, Copy, Debug)]
pub enum ResizeParameter {
Auto,
Cells(u32),
Pixels(u32),
WindowPercent(u32),
}
#[derive(Eq, PartialEq, Clone, Copy, Debug)]
pub struct ResizeCommand {
pub width: ResizeParameter,
pub height: ResizeParameter,
pub preserve_aspect_ratio: bool,
}
#[test]
fn check_opaque_region() {
let graphic = GraphicData {
id: GraphicId::new(1),
width: 10,
height: 10,
color_type: ColorType::Rgb,
pixels: vec![255; 10 * 10 * 3],
is_opaque: true,
resize: None,
display_width: None,
display_height: None,
transmit_time: std::time::Instant::now(),
};
assert!(graphic.is_filled(1, 1, 3, 3));
assert!(!graphic.is_filled(8, 8, 10, 10));
let pixels = {
let mut data = vec![255; 10 * 10 * 4];
for y in 3..6 {
let offset = y * 10 * 4;
data[offset..offset + 3 * 4].fill(0);
}
data
};
let graphic = GraphicData {
id: GraphicId::new(1),
pixels,
width: 10,
height: 10,
color_type: ColorType::Rgba,
is_opaque: false,
resize: None,
display_width: None,
display_height: None,
transmit_time: std::time::Instant::now(),
};
assert!(graphic.is_filled(0, 0, 3, 3));
assert!(!graphic.is_filled(1, 1, 4, 4));
}