use std::fmt::Display;
use std::io::{Error, Write};
use std::str;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use tracing::{event, instrument, Level};
use crate::resources::image::*;
use crate::resources::MimeData;
use crate::terminal::size::{PixelSize, TerminalSize};
#[derive(Debug)]
pub enum KittyImageError {
IoError(std::io::Error),
#[cfg(feature = "image-processing")]
ImageError(image::ImageError),
}
impl Display for KittyImageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
KittyImageError::IoError(error) => write!(f, "Failed to render kitty image: {error}"),
#[cfg(feature = "image-processing")]
KittyImageError::ImageError(image_error) => {
write!(f, "Failed to process pixel image: {image_error}")
}
}
}
}
impl std::error::Error for KittyImageError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
KittyImageError::IoError(error) => Some(error),
#[cfg(feature = "image-processing")]
KittyImageError::ImageError(image_error) => Some(image_error),
}
}
}
impl From<KittyImageError> for std::io::Error {
fn from(value: KittyImageError) -> Self {
std::io::Error::other(value)
}
}
impl From<std::io::Error> for KittyImageError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
#[cfg(feature = "image-processing")]
impl From<image::ImageError> for KittyImageError {
fn from(value: image::ImageError) -> Self {
Self::ImageError(value)
}
}
enum KittyImageData {
Png(Vec<u8>),
#[cfg(feature = "image-processing")]
Rgb(PixelSize, Vec<u8>),
#[cfg(feature = "image-processing")]
Rgba(PixelSize, Vec<u8>),
}
impl KittyImageData {
fn f_format_code(&self) -> &str {
match self {
KittyImageData::Png(_) => "100",
#[cfg(feature = "image-processing")]
KittyImageData::Rgb(_, _) => "24",
#[cfg(feature = "image-processing")]
KittyImageData::Rgba(_, _) => "32",
}
}
fn data(&self) -> &[u8] {
match self {
KittyImageData::Png(ref contents) => contents,
#[cfg(feature = "image-processing")]
KittyImageData::Rgb(_, ref contents) => contents,
#[cfg(feature = "image-processing")]
KittyImageData::Rgba(_, ref contents) => contents,
}
}
fn size(&self) -> Option<PixelSize> {
match self {
KittyImageData::Png(_) => None,
#[cfg(feature = "image-processing")]
KittyImageData::Rgb(size, _) => Some(*size),
#[cfg(feature = "image-processing")]
KittyImageData::Rgba(size, _) => Some(*size),
}
}
fn s_width(&self) -> u32 {
self.size().map_or(0, |s| s.x)
}
fn v_height(&self) -> u32 {
self.size().map_or(0, |s| s.y)
}
}
impl KittyImageData {
fn write_to(&self, writer: &mut dyn Write) -> Result<(), Error> {
let image_data = STANDARD.encode(self.data());
let image_data_chunks = image_data.as_bytes().chunks(4096);
let number_of_chunks = image_data_chunks.len();
for (i, chunk_data) in image_data_chunks.enumerate() {
let is_first_chunk = i == 0;
let m = i32::from(i < number_of_chunks - 1);
if is_first_chunk {
let f = self.f_format_code();
let s = self.s_width();
let v = self.v_height();
write!(writer, "\x1b_Ga=T,t=d,I=1,f={f},s={s},v={v},m={m},q=2;")?;
} else {
write!(writer, "\x1b_Gm={m},q=2;")?;
}
writer.write_all(chunk_data)?;
write!(writer, "\x1b\\")?;
}
Ok(())
}
}
#[derive(Debug, Copy, Clone)]
pub struct KittyGraphicsProtocol;
impl KittyGraphicsProtocol {
#[cfg(feature = "image-processing")]
fn render(
mime_data: MimeData,
terminal_size: TerminalSize,
) -> Result<KittyImageData, KittyImageError> {
let image = crate::resources::image::decode_image(&mime_data)?;
match downsize_to_columns(&image, terminal_size) {
Some(downsized_image) => {
event!(
Level::DEBUG,
"Image scaled down to column limit, rendering RGB data"
);
Ok(Self::render_as_rgb_or_rgba(downsized_image))
}
None if mime_data.mime_type_essence() == Some("image/png") => {
event!(
Level::DEBUG,
"PNG image of appropriate size, rendering original image data"
);
Ok(Self::render_as_png(mime_data.data))
}
None => {
event!(Level::DEBUG, "Image not in PNG format, rendering RGB data");
Ok(Self::render_as_rgb_or_rgba(image))
}
}
}
#[cfg(not(feature = "image-processing"))]
fn render(
mime_data: MimeData,
_terminal_size: TerminalSize,
) -> Result<KittyImageData, KittyImageError> {
match mime_data.mime_type_essence() {
Some("image/png") => Ok(Self::render_as_png(mime_data.data)),
_ => {
event!(
Level::DEBUG,
"Only PNG images supported without image-processing feature, but got {:?}",
mime_data.mime_type
);
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
format!(
"Image data with mime type {:?} not supported",
mime_data.mime_type
),
)
.into())
}
}
}
fn render_as_png(data: Vec<u8>) -> KittyImageData {
KittyImageData::Png(data)
}
#[cfg(feature = "image-processing")]
fn render_as_rgb_or_rgba(image: image::DynamicImage) -> KittyImageData {
use image::{ColorType, GenericImageView};
let size = PixelSize::from_xy(image.dimensions());
match image.color() {
ColorType::L8 | ColorType::Rgb8 | ColorType::L16 | ColorType::Rgb16 => {
KittyImageData::Rgb(size, image.into_rgb8().into_raw())
}
_ => KittyImageData::Rgba(size, image.into_rgba8().into_raw()),
}
}
}
impl InlineImageProtocol for KittyGraphicsProtocol {
#[instrument(skip(self, writer, resource_handler, terminal_size))]
fn write_inline_image(
&self,
writer: &mut dyn Write,
resource_handler: &dyn crate::ResourceUrlHandler,
url: &url::Url,
terminal_size: crate::TerminalSize,
) -> std::io::Result<()> {
let mime_data = resource_handler.read_resource(url)?;
event!(
Level::DEBUG,
"Received data of mime type {:?}",
mime_data.mime_type
);
let image = Self::render(mime_data, terminal_size)?;
image.write_to(writer)
}
}