Skip to content

Commit

Permalink
just use ECH and CUD and CUU to erase
Browse files Browse the repository at this point in the history
  • Loading branch information
benjajaja committed Dec 7, 2024
1 parent 16210b4 commit 75ada30
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 86 deletions.
15 changes: 8 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,13 +255,14 @@ impl Resize {
let height = (rect.height * font_size.1) as u32;
// Resize/Crop/etc. but not necessarily fitting cell size
let mut image = self.resize_image(source, width, height);
// Pad to cell size with some background color.
if image.width() != width || image.height() != height {
let mut bg: DynamicImage =
ImageBuffer::from_pixel(width, height, background_color).into();
imageops::overlay(&mut bg, &image, 0, 0);
image = bg;
}
// Always pad to cell size with background color, Sixel doesn't have transparency
// and would get a white background by the sixel library.
// Once Sixel gets transparency support, only pad
// `if image.width() != width || image.height() != height`.
let mut bg: DynamicImage =
ImageBuffer::from_pixel(width, height, background_color).into();
imageops::overlay(&mut bg, &image, 0, 0);
image = bg;
(image, rect)
})
}
Expand Down
8 changes: 2 additions & 6 deletions src/picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ use crate::{

pub mod cap_parser;

// Transparent background is not well tested enough.
const DEFAULT_BACKGROUND: Rgba<u8> = Rgba([0, 0, 0, 255]);
const DEFAULT_BACKGROUND: Rgba<u8> = Rgba([0, 0, 0, 0]);

#[derive(Clone, Copy, Debug)]
pub struct Picker {
Expand Down Expand Up @@ -147,10 +146,7 @@ impl Picker {
self.font_size
}

// Change the default background color.
//
// The background color is always underlayed. It may be transparent, but this is
// experimental, as some terminals may not clear stale characters behind images.
// Change the default background color (transparent black).
pub fn set_background_color<T: Into<Rgba<u8>>>(&mut self, background_color: T) {
self.background_color = background_color.into();
}
Expand Down
83 changes: 45 additions & 38 deletions src/protocol/iterm2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ use std::{cmp::min, format, io::Cursor};

use crate::{errors, picker::cap_parser::Parser, FontSize, ImageSource, Resize, Result};

use super::{slice_image, ProtocolTrait, StatefulProtocolTrait};
use super::{ProtocolTrait, StatefulProtocolTrait};

// Fixed sixel protocol
#[derive(Clone, Default)]
pub struct Iterm2 {
// One literal image slice per row, see below why this is necessary.
pub data: Vec<String>,
pub data: String,
pub area: Rect,
pub is_tmux: bool,
}
Expand All @@ -39,7 +37,7 @@ impl Iterm2 {
None => (&source.image, source.area),
};

let data = encode(image, font_size.1, is_tmux)?;
let data = encode(image, area, is_tmux)?;
Ok(Self {
data,
area,
Expand All @@ -48,30 +46,36 @@ impl Iterm2 {
}
}

// Slice the image into N rows matching the terminal font size height, so that
// we can output the `Erase in Line (EL)` for each row before the potentially
// transparent image without showing stale character artifacts.
fn encode(img: &DynamicImage, font_height: u16, is_tmux: bool) -> Result<Vec<String>> {
let results = slice_image(img, font_height as u32)
.into_iter()
.flat_map(|img| {
let mut png: Vec<u8> = vec![];
img.write_to(&mut Cursor::new(&mut png), image::ImageFormat::Png)?;

let data = general_purpose::STANDARD.encode(&png);

let (start, escape, end) = Parser::escape_tmux(is_tmux);
Ok::<String, errors::Errors>(format!(
// Clear row from cursor on, for stale characters behind transparent images.
"{start}{escape}[0K{escape}]1337;File=inline=1;size={};width={}px;height={}px;doNotMoveCursor=1:{}\x07{end}",
png.len(),
img.width(),
img.height(),
data,
))
})
.collect();
Ok(results)
fn encode(img: &DynamicImage, render_area: Rect, is_tmux: bool) -> Result<String> {
let mut png: Vec<u8> = vec![];
img.write_to(&mut Cursor::new(&mut png), image::ImageFormat::Png)?;

let data = general_purpose::STANDARD.encode(&png);

let (start, escape, end) = Parser::escape_tmux(is_tmux);

// Transparency needs explicit erasing of stale characters, or they stay behind the rendered
// image due to skipping of the following characters _in the buffer_.
// DECERA does not work in WezTerm, however ECH and and cursor CUD and CUU do.
// For each line, erase `width` characters, then move back and place image.
let width = render_area.width;
let height = render_area.height;
let mut seq = String::from(start);
for _ in 0..height {
seq.push_str(&format!("\x1b[{width}X\x1b[1B").to_string());
}
seq.push_str(&format!("\x1b[{height}A").to_string());

seq.push_str(&format!(
"{escape}]1337;File=inline=1;size={};width={}px;height={}px;doNotMoveCursor=1:{}\x07",
png.len(),
img.width(),
img.height(),
data,
));
seq.push_str(end);

Ok::<String, errors::Errors>(seq)
}

impl ProtocolTrait for Iterm2 {
Expand All @@ -80,7 +84,7 @@ impl ProtocolTrait for Iterm2 {
}
}

fn render(rect: Rect, data: &[String], area: Rect, buf: &mut Buffer, overdraw: bool) {
fn render(rect: Rect, data: &str, area: Rect, buf: &mut Buffer, overdraw: bool) {
let render_area = match render_area(rect, area, overdraw) {
None => {
// If we render out of area, then the buffer will attempt to write regular text (or
Expand All @@ -93,13 +97,16 @@ fn render(rect: Rect, data: &[String], area: Rect, buf: &mut Buffer, overdraw: b
Some(r) => r,
};

// Render each slice, see `encode()` for details.
for (i, slice) in data.iter().enumerate() {
let x = render_area.left();
let y = render_area.top() + (i as u16);
buf.cell_mut((x, y)).map(|cell| cell.set_symbol(slice));
for x in (render_area.left() + 1)..render_area.right() {
// Skip following columns to avoid writing over the image.
buf.cell_mut(render_area).map(|cell| cell.set_symbol(data));
let mut skip_first = false;

// Skip entire area
for y in render_area.top()..render_area.bottom() {
for x in render_area.left()..render_area.right() {
if !skip_first {
skip_first = true;
continue;
}
buf.cell_mut((x, y)).map(|cell| cell.set_skip(true));
}
}
Expand Down Expand Up @@ -165,7 +172,7 @@ impl StatefulProtocolTrait for StatefulIterm2 {
force,
) {
let is_tmux = self.current.is_tmux;
match encode(&img, self.font_size.1, is_tmux) {
match encode(&img, rect, is_tmux) {
Ok(data) => {
self.current = Iterm2 {
data,
Expand Down
27 changes: 1 addition & 26 deletions src/protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::{
hash::{Hash, Hasher},
};

use image::{imageops, DynamicImage, GenericImageView, ImageBuffer, Rgba};
use image::{imageops, DynamicImage, ImageBuffer, Rgba};
use ratatui::{buffer::Buffer, layout::Rect};

use crate::FontSize;
Expand Down Expand Up @@ -216,28 +216,3 @@ impl ImageSource {
Rect::new(0, 0, width, height)
}
}

// Slice the image into `(image.height() / slice_height)` slices.
// The image height must be a multiple of slice_height.
// This is necessary for some protocols to be able to render transparent images
// over stale characters, together with the Erase in Line (EL) CSI (currently
// iTerm2, but Sixel may also need this once transparency works).
fn slice_image(image: &DynamicImage, slice_height: u32) -> Vec<DynamicImage> {
let (width, height) = image.dimensions();
assert!(
height % slice_height == 0,
"Height must be a multiple of slice height"
);

let num_slices = height / slice_height;
let mut slices = Vec::with_capacity(num_slices as usize);

for i in 0..num_slices {
let y_offset = i * slice_height;
// Crop the region and convert it back into a DynamicImage
let cropped = image.crop_imm(0, y_offset, width, slice_height);
slices.push(cropped);
}

slices
}
12 changes: 3 additions & 9 deletions src/protocol/sixel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,14 @@ static TMUX_START: &str = "\x1bPtmux;";
// TODO: change E to sixel_rs::status::Error and map when calling
fn encode(img: &DynamicImage, is_tmux: bool) -> Result<String> {
let (w, h) = (img.width(), img.height());
let img_rgba8 = img.to_rgba8();
let bytes = img_rgba8.as_raw();
let img_rgb8 = img.to_rgb8();
let bytes = img_rgb8.as_raw();

let data = sixel_string(
bytes,
w as i32,
h as i32,
PixelFormat::RGBA8888,
PixelFormat::RGB888,
DiffusionMethod::Stucki,
MethodForLargest::Auto,
MethodForRep::Auto,
Expand Down Expand Up @@ -123,12 +123,6 @@ fn render(rect: Rect, data: &str, area: Rect, buf: &mut Buffer, overdraw: bool)
Some(r) => r,
};

// DECERA
// let top = render_area.top() + 1;
// let left = render_area.left() + 1;
// let bottom = render_area.bottom() + 1;
// let right = render_area.right() + 1;
// let seq = format!("\x1b[{top};{left};{bottom};{right}$z{data}");
buf.cell_mut(render_area).map(|cell| cell.set_symbol(data));
let mut skip_first = false;

Expand Down

0 comments on commit 75ada30

Please sign in to comment.