pulldown_cmark_mdcat/terminal/capabilities/
sixel.rs1use std::io::{self, Write};
6
7use crate::resources::image::*;
8use icy_sixel::SixelImage;
9use image::GenericImageView;
10
11use crate::resources::MimeData;
12use crate::terminal::size::TerminalSize;
13
14#[derive(Debug, Copy, Clone)]
16pub struct SixelProtocol;
17
18impl SixelProtocol {
19 fn load_image(mime: MimeData) -> io::Result<image::DynamicImage> {
20 match mime.mime_type_essence() {
21 Some("image/svg+xml") => {
22 let png = crate::resources::svg::render_svg_to_png(&mime.data)?;
23
24 image::load_from_memory_with_format(&png, image::ImageFormat::Png)
25 .map_err(io::Error::other)
26 }
27
28 Some("image/png") => {
29 image::load_from_memory_with_format(&mime.data, image::ImageFormat::Png)
30 .map_err(io::Error::other)
31 }
32
33 _ => image::load_from_memory(&mime.data).map_err(io::Error::other),
34 }
35 }
36
37 fn inject_raster_attributes(sixel: &str, width: u32, height: u32) -> String {
48 let intro_end = sixel.find('q').map_or(0, |i| i + 1);
49 format!(
50 "{}\"1;1;{width};{height}{}",
51 &sixel[..intro_end],
52 &sixel[intro_end..]
53 )
54 }
55
56 fn render_sixel(writer: &mut dyn Write, img: image::DynamicImage) -> io::Result<()> {
57 let (w, h) = img.dimensions();
58 let rgba = img.to_rgba8();
59
60 let sixel = SixelImage::try_from_rgba(rgba.into_raw(), w as usize, h as usize)
61 .map_err(io::Error::other)?
62 .encode()
63 .map_err(io::Error::other)?;
64 let sixel = Self::inject_raster_attributes(&sixel, w, h);
65 write!(writer, "\x1b[?8452h{sixel}\x1b[?8452l")
72 }
73
74 pub(crate) fn write_png_data(&self, writer: &mut dyn Write, png_data: &[u8]) -> io::Result<()> {
76 let img = image::load_from_memory_with_format(png_data, image::ImageFormat::Png)
77 .map_err(io::Error::other)?;
78
79 Self::render_sixel(writer, img)
80 }
81}
82
83impl crate::resources::InlineImageProtocol for SixelProtocol {
84 fn write_inline_image(
85 &self,
86 writer: &mut dyn Write,
87 resource_handler: &dyn crate::resources::ResourceUrlHandler,
88 url: &url::Url,
89 terminal_size: TerminalSize,
90 ) -> io::Result<()> {
91 let mime = resource_handler.read_resource(url)?;
92 let image = SixelProtocol::load_image(mime)?;
93
94 let image = if let Some(downsized) = fit_image_to_terminal(&image, terminal_size) {
95 downsized
96 } else {
97 image
98 };
99
100 SixelProtocol::render_sixel(writer, image)
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn raster_attributes_are_inserted_right_after_the_dcs_introducer() {
110 let sixel = "\x1bP9;1;0q#0;2;0;0;0$-";
111 let with_raster = SixelProtocol::inject_raster_attributes(sixel, 108, 20);
112 assert_eq!(with_raster, "\x1bP9;1;0q\"1;1;108;20#0;2;0;0;0$-");
113 }
114
115 #[test]
116 fn raster_attributes_reflect_the_true_dimensions() {
117 let sixel = "\x1bP0;0;0q#0;2;0;0;0$-";
118 let with_raster = SixelProtocol::inject_raster_attributes(sixel, 1, 1);
119 assert!(with_raster.contains("\"1;1;1;1"));
120 }
121}