Skip to main content

ratatui_image/
lib.rs

1//! # Image widgets with multiple graphics protocol backends for [ratatui]
2//!
3//! **Unify terminal image rendering across Sixels, Kitty, and iTerm2 protocols.**
4//!
5//! [ratatui] is an immediate-mode TUI library.
6//! ratatui-image tackles 3 general problems when rendering images with an immediate-mode TUI:
7//!
8//! **Query the terminal for available graphics protocols**
9//!
10//! Some terminals may implement one or more graphics protocols, such as Sixels, or the iTerm2 or
11//! Kitty graphics protocols. Guess by env vars. If that fails, query the terminal with some
12//! control sequences.
13//! Fallback to "halfblocks" which uses some unicode half-block characters with fore- and
14//! background colors.
15//!
16//! **Query the terminal for the font-size in pixels.**
17//!
18//! If there is an actual graphics protocol available, it is necessary to know the font-size to
19//! be able to map the image pixels to character cell area.
20//! Query the terminal with some control sequences for either the font-size directly, or the
21//! window-size in pixels and derive the font-size together with row/column count.
22//!
23//! **Render the image by the means of the guessed protocol.**
24//!
25//! Some protocols, like Sixels, are essentially "immediate-mode", but we still need to avoid the
26//! TUI from overwriting the image area, even with blank characters.
27//! Other protocols, like Kitty, are essentially stateful, but at least provide a way to re-render
28//! an image that has been loaded, at a different or same position.
29//! Since we have the font-size in pixels, we can precisely map the characters/cells/rows-columns
30//! that will be covered by the image and skip drawing over the image.
31//!
32//! # Quick start
33//! ```rust
34//! use ratatui::{backend::TestBackend, layout::Size, Terminal, Frame};
35//! use ratatui_image::{Image, picker::Picker, protocol::Protocol, Resize};
36//!
37//! struct App {
38//!     // We need to hold the image data somewhere.
39//!     image: Protocol,
40//! }
41//!
42//! fn main() -> Result<(), Box<dyn std::error::Error>> {
43//!     let backend = TestBackend::new(80, 30);
44//!     let mut terminal = Terminal::new(backend)?;
45//!
46//!     // Should use `Picker::from_query_stdio()?` to get the font size and protocol,
47//!     // but we can't put that here because that would break doctests!
48//!     let mut picker = Picker::halfblocks();
49//!
50//!     // Load an image with the image crate.
51//!     let dyn_img = image::ImageReader::open("./assets/Ada.png")?.decode()?;
52//!
53//!     let font_size = picker.font_size();
54//!     let size = Size::new(
55//!         dyn_img.width().div_ceil(font_size.width as u32) as u16,
56//!         dyn_img.height().div_ceil(font_size.height as u32) as u16,
57//!     );
58//!
59//!     // Create the Protocol once, or in other words, transform the image data to Sixels, Kitty
60//!     // data, iTerm2 base64 PNG data, or some kind of ASCII-art.
61//!     let image = picker.new_protocol(dyn_img, size, Resize::Fit(None))?;
62//!
63//!     let mut app = App { image };
64//!
65//!     // This would be your typical `loop {` in a real app:
66//!     terminal.draw(|f| {
67//!         let image = Image::new(&app.image);
68//!         // Rendering the transformed data is now cheap.
69//!         f.render_widget(image, f.area());
70//!     });
71//!
72//!     Ok(())
73//! }
74//! ```
75//! While this approach is usually sufficient, and leaves a lot of room for customizing where the
76//! image actually gets transformed, for more advanced usage I really recommend using
77//! [`thread::ThreadProtocol`] and looking at `excamples/thread.rs` to get an idea how to
78//! dynamically resize images to fit into some area but without blocking the UI.
79//!
80//! The [picker::Picker] helper is there to do all this font-size and graphics-protocol guessing,
81//! and also to map character-cell-size to pixel size so that we can e.g. "fit" an image inside
82//! a desired columns+rows bound, and so on.
83//!
84//! # Widget choice
85//! * The [`Image`] widget has a fixed size in rows/columns. If the image pixel size exceeds the
86//!   pixel area of the rows/columns, the image is scaled down proportionally to "fit" once, at the
87//!   creation time of the [`Protocol`].  
88//!   The big upside is that this widget is _stateless_ (in terms of ratatui, i.e. immediate-mode),
89//!   and thus can never block the rendering thread/task. A lot of ratatui apps only use stateless
90//!   widgets, so this factor is also important when chosing.  
91//!   What happens when the image does not fit into the render area can be controlled with
92//!   [`Image::allow_clipping`].
93//! * The [StatefulImage] widget adapts to its render area at render-time. It can be set to fit,
94//!   crop, or scale to the available render area.
95//!   This means the widget must be stateful, i.e. use `render_stateful_widget` which takes a
96//!   mutable state parameter.
97//!   The resizing and encoding is blocking, and since it happens at render-time, it should always
98//!   be offloaded to another thread or async task, to keep the UI responsive (see
99//!   `examples/thread.rs` and `examples/tokio.rs` on how to use [`thread::ThreadProtocol`]).
100//!
101//! # Examples
102//!
103//! * `examples/demo.rs` is a fully fledged demo.
104//! * `examples/thread.rs` shows how to offload resize and encoding to another thread, to avoid
105//!   blocking the UI thread.
106//! * `examples/tokio.rs` same as `thread.rs` but with tokio.
107//! * `examples/sliced.rs` shows how to use an image that can have "rows" or "horizontal slices"
108//!   partially hidden with any protocol.
109//!
110//! The lib also includes a binary that renders an image file, but it is focused on testing.
111//!
112//! # Features
113//!
114//! ### Backend
115//!
116//! * `crossterm` (default) if this matches your ratatui backend (most likely).
117//! * `termion` if this matches your ratatui backend.
118//! * `termwiz` is available, but not working correctly with ratatui-image.
119//!
120//! ### Chafa library
121//!
122//! * `chafa-dyn` (default) to use the amazing [chafa](https://hpjansson.org/chafa/) library for
123//!   rendering without image protocols. Dynamically link against libchafa.so at compile time.
124//!   Requires libchafa to be available at runtime in the same way.
125//! * `chafa-static` to statically link against libchafa.a at compile time. The library is embedded
126//!   in the binary.
127//! * If you absolutely don't want to deal with libchafa, then you should use
128//!   `--no-default-features --features image-defaults,crossterm` or a variation thereof.
129//!
130//! Note: The chafa features are mutually exclusive - enable only one at a time.
131//!
132//! ### Others
133//!
134//! * `image-defaults` (default) just enables `image/defaults` (`image` has `default-features =
135//!   false`). To only support a selection of image formats and cut down dependencies, disable this
136//!   feature, add `image` to your crate, and enable its features/formats as desired. See
137//!   <https://doc.rust-lang.org/cargo/reference/features.html#feature-unification/>.
138//! * `serde` for `#[derive]`s on [picker::ProtocolType] for convenience, because it might be
139//!   useful to save it in some user configuration.
140//! * `tokio` whether to use tokio's `UnboundedSender` in `ThreadProtocol`.
141//!
142//!
143//! [ratatui]: https://github.com/ratatui-org/ratatui
144//! [sixel]: https://en.wikipedia.org/wiki/Sixel
145//! [`render_stateful_widget`]: https://docs.rs/ratatui/latest/ratatui/terminal/struct.Frame.html#method.render_stateful_widget
146use std::{
147    cmp::{max, min},
148    marker::PhantomData,
149};
150
151use image::{DynamicImage, ImageBuffer, Rgba, imageops};
152use protocol::Protocol;
153use ratatui::{
154    buffer::Buffer,
155    layout::{Rect, Size},
156    widgets::{StatefulWidget, Widget},
157};
158
159pub mod errors;
160pub mod picker;
161pub mod protocol;
162pub mod sliced;
163pub mod thread;
164pub use image::imageops::FilterType;
165
166type Result<T> = std::result::Result<T, errors::Errors>;
167
168/// The terminal's font size in `(width, height)`
169#[derive(Copy, Clone, Debug)]
170pub struct FontSize {
171    pub width: u16,
172    pub height: u16,
173}
174
175impl FontSize {
176    pub const fn new(width: u16, height: u16) -> Self {
177        Self { width, height }
178    }
179}
180
181impl From<(u16, u16)> for FontSize {
182    fn from((width, height): (u16, u16)) -> Self {
183        Self::new(width, height)
184    }
185}
186
187/// Fixed size image widget that uses [Protocol].
188///
189/// The widget does **not** react to area resizes.
190/// Its advantage lies in that the [Protocol] needs only one initial resize.
191///
192/// The image won't render if it doesn't fit, unless [`Image::allow_clipping`] has been set.
193/// ```rust
194/// # use ratatui_image::picker::Picker;
195/// # use ratatui::layout::Size;
196/// # use ratatui_image::{*, sliced::{SlicedProtocol, SlicedImage}};
197/// # let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(80, 24))?;
198/// # let picker = Picker::halfblocks(); // Note: use from_query_studio
199/// // let picker = Picker::from_query_studio()?;
200/// let image = image::ImageReader::open("./assets/NixOS.png")?.decode()?;
201/// let proto = picker.new_protocol(image, Size::new(20, 10), Resize::Fit(None))?;
202///
203/// terminal.draw(|f| {
204///     f.render_widget(Image::new(&proto), f.area());
205/// });
206/// # Ok::<(), Box<dyn std::error::Error>>(())
207/// ```
208pub struct Image<'a> {
209    image: &'a Protocol,
210    allow_clipping: bool,
211}
212
213impl<'a> Image<'a> {
214    pub fn new(image: &'a Protocol) -> Self {
215        Self {
216            image,
217            allow_clipping: false,
218        }
219    }
220
221    /// Allow clipping the image if the render area is smaller than the image, and if the protocol
222    /// supports it ([`protocol::kitty`] and [`protocol::halfblocks`]).
223    ///
224    /// This is disabled by default to make the behavior consistent.
225    ///
226    /// See also [`protocol::Protocol::needs_placeholder`], which is an excellent complement if you
227    /// need to render *something* when the image couldn't.
228    ///
229    /// ```rust
230    /// # use ratatui_image::picker::Picker;
231    /// # use ratatui::layout::Size;
232    /// # use ratatui_image::{*, sliced::*};
233    /// # let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(80, 24))?;
234    /// # let picker = Picker::halfblocks();
235    /// # let dyn_img = image::ImageReader::open("./assets/NixOS.png")?.decode()?;
236    /// # let proto = picker.new_protocol(dyn_img, (20, 10).into(), Resize::Fit(None))?;
237    /// terminal.draw(|f| {
238    ///     if let Some(placeholder_area) = proto.needs_placeholder(f.area()) {
239    ///         // Render `Box` or something with placeholder_area.
240    ///     } else {
241    ///         f.render_widget(Image::new(&proto).allow_clipping(true), f.area());
242    ///     }
243    /// });
244    /// # Ok::<(), Box<dyn std::error::Error>>(())
245    /// ```
246    pub fn allow_clipping(mut self, allow: bool) -> Self {
247        self.allow_clipping = allow;
248        self
249    }
250}
251
252impl Widget for Image<'_> {
253    fn render(self, area: Rect, buf: &mut Buffer) {
254        if area.width == 0 || area.height == 0 {
255            return;
256        }
257
258        if !self.allow_clipping
259            && (self.image.size().width > area.width || self.image.size().height > area.height)
260        {
261            return;
262        }
263
264        self.image.render(area, buf);
265    }
266}
267
268pub trait ResizeEncodeRender {
269    /// Resize and encode if necessary, and render immediately.
270    fn resize_encode_render(&mut self, resize: &Resize, area: Rect, buf: &mut Buffer) {
271        if let Some(rect) = self.needs_resize(resize, area.into()) {
272            self.resize_encode(resize, rect);
273        }
274        self.render(area, buf);
275    }
276
277    /// Resize the image and encode it for rendering. The result should be stored statefully so
278    /// that next call for the given area does not need to redo the work.
279    ///
280    /// This can be done in a background thread, and the result is stored in this [protocol::StatefulProtocol].
281    fn resize_encode(&mut self, resize: &Resize, size: Size);
282
283    /// Render the currently resized and encoded data to the buffer.
284    fn render(&mut self, area: Rect, buf: &mut Buffer);
285
286    /// Check if the current image state would need resizing (grow or shrink) for the given area.
287    ///
288    /// This can be called by the UI thread to check if this [protocol::StatefulProtocol] should be sent off
289    /// to some background thread/task to do the resizing and encoding, instead of rendering. The
290    /// thread should then return the [protocol::StatefulProtocol] so that it can be rendered.
291    fn needs_resize(&self, resize: &Resize, size: Size) -> Option<Size>;
292}
293
294/// Resizeable image widget that uses a [protocol::StatefulProtocol] state.
295///
296/// This stateful widget resizes the image at render time.
297///
298/// **Do not use it withou [`thread::ThreadProtocol`] in a reactive UI**. Rendering the widget
299/// **will** block the UI thread if the image has not been resized by another thread.
300///
301/// ```rust
302/// # use ratatui::Frame;
303/// # use ratatui_image::{Resize, StatefulImage, protocol::{StatefulProtocol}};
304/// struct App {
305///     image_state: StatefulProtocol,
306/// }
307/// fn ui(f: &mut Frame<'_>, app: &mut App) {
308///     let image = StatefulImage::default().resize(Resize::Crop(None));
309///     f.render_stateful_widget(
310///         image,
311///         f.area(),
312///         &mut app.image_state,
313///     );
314/// }
315/// ```
316pub struct StatefulImage<T>
317where
318    T: ResizeEncodeRender,
319{
320    resize: Resize,
321    phantom: PhantomData<T>,
322}
323
324impl<T> Default for StatefulImage<T>
325where
326    T: ResizeEncodeRender,
327{
328    fn default() -> Self {
329        Self::new()
330    }
331}
332impl<T> StatefulImage<T>
333where
334    T: ResizeEncodeRender,
335{
336    pub const fn resize(self, resize: Resize) -> Self {
337        Self { resize, ..self }
338    }
339
340    pub const fn new() -> Self {
341        Self {
342            resize: Resize::Fit(None),
343            phantom: PhantomData,
344        }
345    }
346}
347
348impl<T> StatefulWidget for StatefulImage<T>
349where
350    T: ResizeEncodeRender,
351{
352    type State = T;
353    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
354        if area.width == 0 || area.height == 0 {
355            return;
356        }
357
358        state.resize_encode_render(&self.resize, area, buf);
359    }
360}
361
362#[derive(Debug, Clone)]
363/// Resize accounting for terminal [`FontSize`].
364///
365/// Resizes images with [`FontSize`] grid boundaries.
366pub enum Resize {
367    /// Fit to a [`Size`].
368    ///
369    /// If the image width or height is smaller than the target size, the image will be resized
370    /// maintaining proportions.
371    ///
372    /// The [FilterType] (re-exported from the [image] crate) defaults to [FilterType::Nearest].
373    Fit(Option<FilterType>),
374    /// Crop to size.
375    ///
376    /// If the width or height is smaller than the area, the image will be cropped.
377    /// The behaviour is the same as using [`Image`] widget with the overhead of resizing,
378    /// but some terminals might misbehave when overdrawing characters over graphics.
379    /// For example, the sixel branch of Alacritty never draws text over a cell that is currently
380    /// being rendered by some sixel sequence, not necessarily originating from the same cell.
381    ///
382    /// The [CropOptions] defaults to clipping the bottom and the right sides.
383    Crop(Option<CropOptions>),
384    /// Scale the image
385    ///
386    /// Same as `Resize::Fit` except it resizes the image even if the image is smaller than the render area
387    Scale(Option<FilterType>),
388}
389
390impl Default for Resize {
391    fn default() -> Self {
392        Self::Fit(None)
393    }
394}
395
396#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397/// Specifies which sides to be clipped when cropping an image.
398pub struct CropOptions {
399    /// If `true`, the top side should be clipped.
400    pub clip_top: bool,
401    /// If `true`, the left side should be clipped.
402    pub clip_left: bool,
403}
404
405const DEFAULT_BACKGROUND: Rgba<u8> = Rgba([0, 0, 0, 0]);
406
407impl Resize {
408    /// Resize [`image::DynamicImage`] to fit into the [`Size`] or smaller.
409    pub fn resize(
410        &self,
411        image: &DynamicImage,
412        font_size: FontSize,
413        size: Size,
414        background_color: Option<Rgba<u8>>,
415    ) -> DynamicImage {
416        let width = (size.width * font_size.width) as u32;
417        let height = (size.height * font_size.height) as u32;
418
419        // Resize/Crop/etc., fitting a multiple of font-size, but not necessarily the `size`.
420        let mut image = self.resize_pixels(image, width, height);
421
422        if image.width() != width || image.height() != height {
423            let mut bg: DynamicImage = ImageBuffer::from_pixel(
424                width,
425                height,
426                background_color.unwrap_or(DEFAULT_BACKGROUND),
427            )
428            .into();
429            imageops::overlay(&mut bg, &image, 0, 0);
430            image = bg;
431        }
432        image
433    }
434
435    /// Calculate the [`Size`] for the [`DynamicImage`] for `available` size after resizing.
436    pub fn size_for(&self, image: &DynamicImage, font_size: FontSize, available: Size) -> Size {
437        let (width, height) = self.needs_resize_pixels(
438            image,
439            (available.width as u32) * (font_size.width as u32),
440            (available.height as u32) * (font_size.height as u32),
441        );
442        Self::round_pixel_size_to_cells(width, height, font_size)
443    }
444
445    /// Calculate the "natural" [`Size`] needed to render the [`DynamicImage`] at the [`FontSize`],
446    /// without any resizing.
447    pub fn natural_size(image: &DynamicImage, font_size: FontSize) -> Size {
448        Self::round_pixel_size_to_cells(image.width(), image.height(), font_size)
449    }
450
451    /// Check if [`image::DynamicImage`]'s "desired" fits into `target` and is different than `current`.
452    ///
453    /// The returned `Size` is the area the image needs to be resized to, depending on the resize
454    /// type, or `None` if the image matches `target` perfectly at the [`FontSize`].
455    pub(crate) fn needs_resize(
456        &self,
457        image: &DynamicImage,
458        desired: Option<Size>,
459        font_size: FontSize,
460        current: Option<Size>,
461        target: Size,
462        force: bool,
463    ) -> Option<Size> {
464        let desired = desired.unwrap_or_else(|| Self::natural_size(image, font_size));
465
466        // Check if resize is needed at all.
467        if !force
468            && !matches!(self, &Resize::Scale(_))
469            && desired.width <= target.width
470            && desired.height <= target.height
471            && (current.is_none() || current == Some(desired))
472        {
473            let width = (desired.width * font_size.width) as u32;
474            let height = (desired.height * font_size.height) as u32;
475            if image.width() == width || image.height() == height {
476                return None;
477            }
478        }
479
480        let rect = self.size_for(image, font_size, target);
481        debug_assert!(
482            rect.width <= target.width,
483            "needs_resize exceeds area width"
484        );
485        debug_assert!(
486            rect.height <= target.height,
487            "needs_resize exceeds area height"
488        );
489        if force || Some(rect) != current {
490            return Some(rect);
491        }
492        None
493    }
494
495    fn resize_pixels(&self, image: &DynamicImage, width: u32, height: u32) -> DynamicImage {
496        const DEFAULT_FILTER_TYPE: FilterType = FilterType::Nearest;
497        const DEFAULT_CROP_OPTIONS: CropOptions = CropOptions {
498            clip_top: false,
499            clip_left: false,
500        };
501        match self {
502            Self::Fit(filter_type) | Self::Scale(filter_type) => {
503                image.resize(width, height, filter_type.unwrap_or(DEFAULT_FILTER_TYPE))
504            }
505            Self::Crop(options) => {
506                let options = options.as_ref().unwrap_or(&DEFAULT_CROP_OPTIONS);
507                let y = if options.clip_top {
508                    image.height().saturating_sub(height)
509                } else {
510                    0
511                };
512                let x = if options.clip_left {
513                    image.width().saturating_sub(width)
514                } else {
515                    0
516                };
517                image.crop_imm(x, y, width, height)
518            }
519        }
520    }
521
522    fn needs_resize_pixels(&self, image: &DynamicImage, width: u32, height: u32) -> (u32, u32) {
523        match self {
524            Self::Fit(_) => fit_area_proportionally(
525                image.width(),
526                image.height(),
527                min(width, image.width()),
528                min(height, image.height()),
529            ),
530
531            Self::Crop(_) => (min(image.width(), width), min(image.height(), height)),
532            Self::Scale(_) => fit_area_proportionally(image.width(), image.height(), width, height),
533        }
534    }
535
536    /// Round an image pixel size to the nearest matching cell size, given a font size.
537    fn round_pixel_size_to_cells(img_width: u32, img_height: u32, font_size: FontSize) -> Size {
538        let width = (img_width as f32 / font_size.width as f32).ceil() as u16;
539        let height = (img_height as f32 / font_size.height as f32).ceil() as u16;
540        Size::new(width, height)
541    }
542}
543
544/// Ripped from https://github.com/image-rs/image/blob/master/src/math/utils.rs#L12
545/// Calculates the width and height an image should be resized to.
546/// This preserves aspect ratio, and based on the `fill` parameter
547/// will either fill the dimensions to fit inside the smaller constraint
548/// (will overflow the specified bounds on one axis to preserve
549/// aspect ratio), or will shrink so that both dimensions are
550/// completely contained within the given `width` and `height`,
551/// with empty space on one axis.
552fn fit_area_proportionally(width: u32, height: u32, nwidth: u32, nheight: u32) -> (u32, u32) {
553    let wratio = nwidth as f64 / width as f64;
554    let hratio = nheight as f64 / height as f64;
555
556    let ratio = f64::min(wratio, hratio);
557
558    let nw = max((width as f64 * ratio).round() as u64, 1);
559    let nh = max((height as f64 * ratio).round() as u64, 1);
560
561    if nw > u64::from(u16::MAX) {
562        let ratio = u16::MAX as f64 / width as f64;
563        (u32::MAX, max((height as f64 * ratio).round() as u32, 1))
564    } else if nh > u64::from(u16::MAX) {
565        let ratio = u16::MAX as f64 / height as f64;
566        (max((width as f64 * ratio).round() as u32, 1), u32::MAX)
567    } else {
568        (nw as u32, nh as u32)
569    }
570}
571
572#[cfg(test)]
573mod tests {
574    use image::{ImageBuffer, Rgba};
575
576    use super::*;
577
578    const FONT_SIZE: FontSize = FontSize::new(10, 10);
579
580    fn s(w: u16, h: u16) -> DynamicImage {
581        let image: DynamicImage =
582            ImageBuffer::from_pixel(w as _, h as _, Rgba::<u8>([255, 0, 0, 255])).into();
583        image
584    }
585
586    fn r(w: u16, h: u16) -> Size {
587        Size::new(w, h)
588    }
589
590    #[test]
591    fn needs_resize_fit() {
592        let resize = Resize::Fit(None);
593
594        let to = resize.needs_resize(
595            &s(100, 100),
596            None,
597            FONT_SIZE,
598            Some(r(10, 10)),
599            r(10, 10),
600            false,
601        );
602        assert_eq!(None, to);
603
604        let to = resize.needs_resize(
605            &s(101, 101),
606            None,
607            FONT_SIZE,
608            Some(r(10, 10)),
609            r(10, 10),
610            false,
611        );
612        assert_eq!(None, to);
613
614        let to = resize.needs_resize(
615            &s(80, 100),
616            None,
617            FONT_SIZE,
618            Some(r(8, 10)),
619            r(10, 10),
620            false,
621        );
622        assert_eq!(None, to);
623
624        let to = resize.needs_resize(
625            &s(100, 100),
626            None,
627            FONT_SIZE,
628            Some(r(99, 99)),
629            r(8, 10),
630            false,
631        );
632        assert_eq!(Some(r(8, 8)), to);
633
634        let to = resize.needs_resize(
635            &s(100, 100),
636            None,
637            FONT_SIZE,
638            Some(r(99, 99)),
639            r(10, 8),
640            false,
641        );
642        assert_eq!(Some(r(8, 8)), to);
643
644        let to = resize.needs_resize(
645            &s(100, 50),
646            None,
647            FONT_SIZE,
648            Some(r(99, 99)),
649            r(4, 4),
650            false,
651        );
652        assert_eq!(Some(r(4, 2)), to);
653
654        let to = resize.needs_resize(
655            &s(50, 100),
656            None,
657            FONT_SIZE,
658            Some(r(99, 99)),
659            r(4, 4),
660            false,
661        );
662        assert_eq!(Some(r(2, 4)), to);
663
664        let to = resize.needs_resize(
665            &s(100, 100),
666            None,
667            FONT_SIZE,
668            Some(r(8, 8)),
669            r(11, 11),
670            false,
671        );
672        assert_eq!(Some(r(10, 10)), to);
673
674        let to = resize.needs_resize(
675            &s(100, 100),
676            None,
677            FONT_SIZE,
678            Some(r(10, 10)),
679            r(11, 11),
680            false,
681        );
682        assert_eq!(None, to);
683    }
684
685    #[test]
686    fn needs_resize_crop() {
687        let resize = Resize::Crop(None);
688
689        let to = resize.needs_resize(
690            &s(100, 100),
691            None,
692            FONT_SIZE,
693            Some(r(10, 10)),
694            r(10, 10),
695            false,
696        );
697        assert_eq!(None, to);
698
699        let to = resize.needs_resize(
700            &s(80, 100),
701            None,
702            FONT_SIZE,
703            Some(r(8, 10)),
704            r(10, 10),
705            false,
706        );
707        assert_eq!(None, to);
708
709        let to = resize.needs_resize(
710            &s(100, 100),
711            None,
712            FONT_SIZE,
713            Some(r(10, 10)),
714            r(8, 10),
715            false,
716        );
717        assert_eq!(Some(r(8, 10)), to);
718
719        let to = resize.needs_resize(
720            &s(100, 100),
721            None,
722            FONT_SIZE,
723            Some(r(10, 10)),
724            r(10, 8),
725            false,
726        );
727        assert_eq!(Some(r(10, 8)), to);
728    }
729}