use std::fmt::Display;
#[cfg(not(feature = "image-processing"))]
use std::io::ErrorKind;
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 {
data: Vec<u8>,
pixel_size: Option<PixelSize>,
},
#[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 { data, .. } => data,
#[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 { pixel_size, .. } => *pixel_size,
#[cfg(feature = "image-processing")]
KittyImageData::Rgb(size, _) => Some(*size),
#[cfg(feature = "image-processing")]
KittyImageData::Rgba(size, _) => Some(*size),
}
}
}
impl KittyImageData {
fn write_to(&self, writer: &mut dyn Write, move_cursor: bool) -> 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 = if i < number_of_chunks - 1 { 1 } else { 0 };
if is_first_chunk {
let f = self.f_format_code();
write!(writer, "\x1b_Ga=T,t=d,I=1,f={f}")?;
if let Some(size) = self.size() {
write!(writer, ",s={},v={}", size.x, size.y)?;
}
if !move_cursor {
write!(writer, ",C=1")?;
}
write!(writer, ",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(
self,
mime_data: MimeData,
terminal_size: TerminalSize,
) -> Result<KittyImageData, KittyImageError> {
use image::ImageFormat;
let image = if let Some("image/svg+xml") = mime_data.mime_type_essence() {
event!(Level::DEBUG, "Rendering mime data to SVG");
let png_data = crate::resources::svg::render_svg_to_png(&mime_data.data)?;
image::load_from_memory_with_format(&png_data, ImageFormat::Png)?
} else {
let image_format = mime_data
.mime_type_essence()
.and_then(image::ImageFormat::from_mime_type);
match image_format {
Some(format) => image::load_from_memory_with_format(&mime_data.data, format)?,
None => image::load_from_memory(&mime_data.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(
self,
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(
ErrorKind::Unsupported,
format!(
"Image data with mime type {:?} not supported",
mime_data.mime_type
),
)
.into())
}
}
}
fn render_as_png(self, data: Vec<u8>) -> KittyImageData {
KittyImageData::Png {
data,
pixel_size: None,
}
}
#[cfg(feature = "image-processing")]
fn render_as_rgb_or_rgba(self, 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 KittyGraphicsProtocol {
pub(crate) fn write_png_data(
&self,
writer: &mut dyn Write,
png_data: Vec<u8>,
move_cursor: bool,
) -> std::io::Result<()> {
KittyImageData::Png {
data: png_data,
pixel_size: None,
}
.write_to(writer, move_cursor)
}
}
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, true)
}
}