Skip to content

Commit 0e10671

Browse files
committed
docs, tests, cleanup
1 parent 43dc683 commit 0e10671

4 files changed

Lines changed: 119 additions & 30 deletions

File tree

src/picker.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -234,11 +234,10 @@ impl Picker {
234234
&self.capabilities
235235
}
236236

237-
pub(crate) fn new_protocol_unresized(
238-
&self,
239-
image: DynamicImage,
240-
area: Rect,
241-
) -> Result<Protocol> {
237+
/// Returns a new protocol.
238+
///
239+
/// The image must match the given area at the terminal's current font size.
240+
pub(crate) fn new_protocol_raw(&self, image: DynamicImage, area: Rect) -> Result<Protocol> {
242241
match self.protocol_type {
243242
ProtocolType::Halfblocks => Ok(Protocol::Halfblocks(Halfblocks::new(image, area)?)),
244243
ProtocolType::Sixel => Ok(Protocol::Sixel(Sixel::new(image, area, self.is_tmux)?)),
@@ -270,7 +269,7 @@ impl Picker {
270269
None => (source.image, source.desired),
271270
};
272271

273-
self.new_protocol_unresized(image, area)
272+
self.new_protocol_raw(image, area)
274273
}
275274

276275
/// Returns a new *stateful* protocol for [`crate::StatefulImage`] widgets.

src/protocol/halfblocks.rs

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,15 @@ impl Halfblocks {
6161
Ok(Self { data, area })
6262
}
6363

64+
/// Specialized render for [`crate::sliced::SlicedImage`].
6465
pub(crate) fn render_with_skip(&self, area: Rect, buf: &mut Buffer, skip_line_count: u16) {
65-
let range = (self.area.width * skip_line_count) as usize
66-
..(self.area.width * self.area.height) as usize;
67-
let hbs = &self.data[range];
66+
let start = (self.area.width * skip_line_count) as usize;
67+
let end = self.area.width as usize * (skip_line_count as usize + area.height as usize);
68+
let hbs = &self.data[start..end];
69+
self.render_halfblocks(hbs, area, buf);
70+
}
71+
72+
fn render_halfblocks(&self, hbs: &[HalfBlock], area: Rect, buf: &mut Buffer) {
6873
for (i, hb) in hbs.iter().enumerate() {
6974
let x = i as u16 % self.area.width;
7075
let y = i as u16 / self.area.width;
@@ -93,17 +98,7 @@ fn encode(img: &DynamicImage, rect: Rect) -> Vec<HalfBlock> {
9398

9499
impl ProtocolTrait for Halfblocks {
95100
fn render(&self, area: Rect, buf: &mut Buffer) {
96-
for (i, hb) in self.data.iter().enumerate() {
97-
let x = i as u16 % self.area.width;
98-
let y = i as u16 / self.area.width;
99-
if x >= area.width || y >= area.height {
100-
continue;
101-
}
102-
103-
if let Some(cell) = buf.cell_mut((area.x + x, area.y + y)) {
104-
hb.set_cell(cell);
105-
}
106-
}
101+
self.render_halfblocks(&self.data, area, buf);
107102
}
108103
fn area(&self) -> Rect {
109104
self.area

src/protocol/iterm2.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ pub struct Iterm2 {
1818

1919
impl Iterm2 {
2020
pub fn new(image: DynamicImage, area: Rect, is_tmux: bool) -> Result<Self> {
21-
let data = encode(&image, area, is_tmux)?;
21+
let png = encode(&image, area, is_tmux)?;
2222
Ok(Self {
23-
data,
23+
data: png,
2424
area,
2525
is_tmux,
2626
})

src/sliced.rs

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ use ratatui::{
1313

1414
/// An image "sliced" into rows for partially displaying, for example in vertical scrolling.
1515
///
16-
/// Uses a specialized [`SlicedProtocol`], that either really slices the image into rows, or in the
17-
/// case of Kitty, takes advantage of the unicode-placeholder mechanism.
16+
/// Uses a specialized [`SlicedProtocol`] with specialized operations based on the protocol.
1817
pub struct SlicedImage<'a> {
1918
sliced_protocol: &'a SlicedProtocol,
2019
size: Size,
@@ -26,6 +25,8 @@ impl<'a> SlicedImage<'a> {
2625
/// The position is relative to the `area` parameter of [`SlicedImage::render`], which is
2726
/// either a direct argument or stems from `frame.render_widget(w, area)`.
2827
///
28+
/// Example that renders an image as if starting at 3 lines *above* the terminal viewport:
29+
///
2930
/// ```rust
3031
/// # use ratatui_image::picker::Picker;
3132
/// # use ratatui::layout::Size;
@@ -43,11 +44,13 @@ impl<'a> SlicedImage<'a> {
4344
///
4445
/// terminal.draw(|f| {
4546
/// let position = -3;
46-
/// // Will render the image as if starting at 3 lines *above* terminal viewport.
4747
/// f.render_widget(SlicedImage::new(&sliced, size, position), f.area());
4848
/// });
4949
/// # Ok::<(), Box<dyn std::error::Error>>(())
5050
/// ```
51+
///
52+
/// The same works for e.g. ending N lines below viewport, or within any other inner area of
53+
/// the TUI.
5154
pub fn new(sliced_protocol: &'a SlicedProtocol, size: Size, position: i16) -> SlicedImage<'a> {
5255
SlicedImage {
5356
sliced_protocol,
@@ -143,16 +146,27 @@ impl Widget for SlicedImage<'_> {
143146

144147
/// The sliced image for [`SlicedImage`].
145148
///
146-
/// Contains either several images (the "sliced" rows), or is a marker for the Kitty protocol.
149+
/// Contains the sliced data specialized for the protocol.
147150
pub enum SlicedProtocol {
151+
/// Generic, simply a list of image slices (or rows).
152+
/// Not suitable for Sixel, as the foot terminal has some striding glitch. In practice, this is
153+
/// only used for [`crate::protocol::iterm2::Iterm2`].
148154
Sliced(Vec<Protocol>),
155+
/// Takes full advantage of the unicode-placeholder mechanism.
149156
Kitty(Kitty),
157+
/// Strips sixel "bands" at render time to display only relevant parts, since the sixel format
158+
/// already is row based. Not pixel accurate, but good enough. Stores font-height to match
159+
/// against sixel "bands" height.
160+
///
161+
/// TODO: deconstruct at encode-time instead of render-time.
150162
Sixel(Sixel, u16),
163+
/// Renders the full image (with chafa if available) for best ASCII art results, then just
164+
/// renders the relevant rows.
151165
Halfblocks(Halfblocks),
152166
}
153167

154168
impl SlicedProtocol {
155-
/// Create the image rows or normal image for kitty, with the given size.
169+
/// Create a `SlicedProtocol` for the target [`ratatui::layout::Size`].
156170
pub fn new(
157171
picker: &Picker,
158172
dyn_img: DynamicImage,
@@ -199,7 +213,7 @@ impl SlicedProtocol {
199213
row_size.height /= row_count;
200214
let rows = slices
201215
.into_iter()
202-
.map(|row| picker.new_protocol_unresized(row, row_size))
216+
.map(|row| picker.new_protocol_raw(row, row_size))
203217
.collect::<Result<Vec<Protocol>, Errors>>()?;
204218

205219
Ok(SlicedProtocol::Sliced(rows))
@@ -209,8 +223,12 @@ impl SlicedProtocol {
209223

210224
/// Simply slices the DynamicImage into rows.
211225
///
212-
/// Suitable for iterm2 or halfblocks, although halfblocks could make use of a custom
213-
/// implementation.
226+
/// Could work for any protocol, but:
227+
/// * Kitty would transmit multiple times.
228+
/// * Halfblocks would not render as good with chafa.
229+
/// * Sixel glitches in foot, would otherwise be okay.
230+
///
231+
/// So this only is used for Iterm2.
214232
fn slice_rows(
215233
image: DynamicImage,
216234
font_size: &FontSize,
@@ -364,4 +382,81 @@ mod sixel_slice {
364382

365383
i
366384
}
385+
386+
#[cfg(test)]
387+
mod tests {
388+
use super::*;
389+
#[test]
390+
fn test_sixel_slice_bands() {
391+
// Simple data with bands separated by -
392+
// The slice function strips preamble, so we need ESC P in the data
393+
let esc = '\u{1b}';
394+
let bs = '\\';
395+
// Minimal sixel-like: ESC P q "attrs" header-bands-terminator ESC backslash
396+
let data = format!("{esc}Pq\"1;1;8;16#0band1-band2-band3{esc}{bs}");
397+
398+
// Skip 1 row, show 1 row, font height 6 means 1 band per row
399+
let result = slice(&data, 1, 1, 6);
400+
// band1 should be skipped, band2 should be present
401+
assert!(!result.contains("band1"));
402+
assert!(result.contains("band2"));
403+
}
404+
}
405+
}
406+
407+
#[cfg(test)]
408+
mod tests {
409+
use super::*;
410+
411+
#[test]
412+
fn test_slice_rows_basic() {
413+
use image::RgbaImage;
414+
415+
// Create a 4x4 image (4 pixels wide, 4 pixels tall)
416+
let mut img = RgbaImage::new(4, 4);
417+
for y in 0..4u32 {
418+
for x in 0..4u32 {
419+
img.put_pixel(x, y, image::Rgba([(x * 64) as u8, (y * 64) as u8, 0, 255]));
420+
}
421+
}
422+
let dyn_img = DynamicImage::ImageRgba8(img);
423+
424+
let font_size = (1, 1); // 1x1 font means 1 row per pixel row
425+
let size = Size::new(4, 4);
426+
427+
let (rows, image_size) = SlicedProtocol::slice_rows(dyn_img, &font_size, size);
428+
429+
assert_eq!(rows.len(), 4); // 4 rows
430+
assert_eq!(image_size, Rect::new(0, 0, 4, 4));
431+
assert_eq!(rows[0].height(), 1);
432+
assert_eq!(rows[1].height(), 1);
433+
assert_eq!(rows[2].height(), 1);
434+
assert_eq!(rows[3].height(), 1);
435+
}
436+
437+
#[test]
438+
fn test_slice_rows_font_height() {
439+
use image::RgbaImage;
440+
441+
// Create a 4x8 image
442+
let mut img = RgbaImage::new(4, 8);
443+
for y in 0..8u32 {
444+
for x in 0..4u32 {
445+
img.put_pixel(x, y, image::Rgba([(x * 64) as u8, (y * 64) as u8, 0, 255]));
446+
}
447+
}
448+
let dyn_img = DynamicImage::ImageRgba8(img);
449+
450+
let font_size = (1, 2); // font is 2 pixels tall
451+
let size = Size::new(4, 4); // 4 rows
452+
453+
let (rows, image_size) = SlicedProtocol::slice_rows(dyn_img, &font_size, size);
454+
455+
assert_eq!(rows.len(), 4); // 4 rows
456+
assert_eq!(image_size, Rect::new(0, 0, 4, 4));
457+
// Each row should be 2 pixels tall (font height)
458+
for row in &rows {
459+
assert_eq!(row.height(), 2);
460+
}
461+
}
367462
}

0 commit comments

Comments
 (0)