@@ -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.
1817pub 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.
147150pub 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
154168impl 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