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//! There is *NO* compiler error for enabling both features at the same time, because some tools
132//! (like cargo-semver-checks) need to build with all features enabled. If both features are
133//! enabled, then `chafa-dyn` takes precedence, because that one makes it easier for running such
134//! tools in CI.
135//!
136//! ### Others
137//!
138//! * `image-defaults` (default) just enables `image/defaults` (`image` has `default-features =
139//! false`). To only support a selection of image formats and cut down dependencies, disable this
140//! feature, add `image` to your crate, and enable its features/formats as desired. See
141//! <https://doc.rust-lang.org/cargo/reference/features.html#feature-unification/>.
142//! * `serde` for `#[derive]`s on [picker::ProtocolType] for convenience, because it might be
143//! useful to save it in some user configuration.
144//! * `tokio` whether to use tokio's `UnboundedSender` in `ThreadProtocol`.
145//!
146//!
147//! [ratatui]: https://github.com/ratatui-org/ratatui
148//! [sixel]: https://en.wikipedia.org/wiki/Sixel
149//! [`render_stateful_widget`]: https://docs.rs/ratatui/latest/ratatui/terminal/struct.Frame.html#method.render_stateful_widget
150use std::{
151 cmp::{max, min},
152 marker::PhantomData,
153};
154
155use image::{DynamicImage, ImageBuffer, Rgba, imageops};
156use protocol::Protocol;
157use ratatui::{
158 buffer::Buffer,
159 layout::{Rect, Size},
160 widgets::{StatefulWidget, Widget},
161};
162
163pub mod errors;
164pub mod picker;
165pub mod protocol;
166pub mod sliced;
167pub mod thread;
168pub use image::imageops::FilterType;
169
170type Result<T> = std::result::Result<T, errors::Errors>;
171
172/// The terminal's font size in `(width, height)`
173#[derive(Copy, Clone, Debug)]
174pub struct FontSize {
175 pub width: u16,
176 pub height: u16,
177}
178
179impl FontSize {
180 pub const fn new(width: u16, height: u16) -> Self {
181 Self { width, height }
182 }
183}
184
185impl From<(u16, u16)> for FontSize {
186 fn from((width, height): (u16, u16)) -> Self {
187 Self::new(width, height)
188 }
189}
190
191/// Fixed size image widget that uses [Protocol].
192///
193/// The widget does **not** react to area resizes.
194/// Its advantage lies in that the [Protocol] needs only one initial resize.
195///
196/// The image won't render if it doesn't fit, unless [`Image::allow_clipping`] has been set.
197/// ```rust
198/// # use ratatui_image::picker::Picker;
199/// # use ratatui::layout::Size;
200/// # use ratatui_image::{*, sliced::{SlicedProtocol, SlicedImage}};
201/// # let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(80, 24))?;
202/// # let picker = Picker::halfblocks(); // Note: use from_query_studio
203/// // let picker = Picker::from_query_studio()?;
204/// let image = image::ImageReader::open("./assets/NixOS.png")?.decode()?;
205/// let proto = picker.new_protocol(image, Size::new(20, 10), Resize::Fit(None))?;
206///
207/// terminal.draw(|f| {
208/// f.render_widget(Image::new(&proto), f.area());
209/// });
210/// # Ok::<(), Box<dyn std::error::Error>>(())
211/// ```
212pub struct Image<'a> {
213 image: &'a Protocol,
214 allow_clipping: bool,
215}
216
217impl<'a> Image<'a> {
218 pub fn new(image: &'a Protocol) -> Self {
219 Self {
220 image,
221 allow_clipping: false,
222 }
223 }
224
225 /// Allow clipping the image if the render area is smaller than the image, and if the protocol
226 /// supports it ([`protocol::kitty`] and [`protocol::halfblocks`]).
227 ///
228 /// This is disabled by default to make the behavior consistent.
229 ///
230 /// See also [`protocol::Protocol::needs_placeholder`], which is an excellent complement if you
231 /// need to render *something* when the image couldn't.
232 ///
233 /// ```rust
234 /// # use ratatui_image::picker::Picker;
235 /// # use ratatui::layout::Size;
236 /// # use ratatui_image::{*, sliced::*};
237 /// # let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(80, 24))?;
238 /// # let picker = Picker::halfblocks();
239 /// # let dyn_img = image::ImageReader::open("./assets/NixOS.png")?.decode()?;
240 /// # let proto = picker.new_protocol(dyn_img, (20, 10).into(), Resize::Fit(None))?;
241 /// terminal.draw(|f| {
242 /// if let Some(placeholder_area) = proto.needs_placeholder(f.area()) {
243 /// // Render `Box` or something with placeholder_area.
244 /// } else {
245 /// f.render_widget(Image::new(&proto).allow_clipping(true), f.area());
246 /// }
247 /// });
248 /// # Ok::<(), Box<dyn std::error::Error>>(())
249 /// ```
250 pub fn allow_clipping(mut self, allow: bool) -> Self {
251 self.allow_clipping = allow;
252 self
253 }
254}
255
256impl Widget for Image<'_> {
257 fn render(self, area: Rect, buf: &mut Buffer) {
258 if area.width == 0 || area.height == 0 {
259 return;
260 }
261
262 if !self.allow_clipping
263 && (self.image.size().width > area.width || self.image.size().height > area.height)
264 {
265 return;
266 }
267
268 self.image.render(area, buf);
269 }
270}
271
272pub trait ResizeEncodeRender {
273 /// Resize and encode if necessary, and render immediately.
274 fn resize_encode_render(&mut self, resize: &Resize, area: Rect, buf: &mut Buffer) {
275 if let Some(rect) = self.needs_resize(resize, area.into()) {
276 self.resize_encode(resize, rect);
277 }
278 self.render(area, buf);
279 }
280
281 /// Resize the image and encode it for rendering. The result should be stored statefully so
282 /// that next call for the given area does not need to redo the work.
283 ///
284 /// This can be done in a background thread, and the result is stored in this [protocol::StatefulProtocol].
285 fn resize_encode(&mut self, resize: &Resize, size: Size);
286
287 /// Render the currently resized and encoded data to the buffer.
288 fn render(&mut self, area: Rect, buf: &mut Buffer);
289
290 /// Check if the current image state would need resizing (grow or shrink) for the given area.
291 ///
292 /// This can be called by the UI thread to check if this [protocol::StatefulProtocol] should be sent off
293 /// to some background thread/task to do the resizing and encoding, instead of rendering. The
294 /// thread should then return the [protocol::StatefulProtocol] so that it can be rendered.
295 fn needs_resize(&self, resize: &Resize, size: Size) -> Option<Size>;
296}
297
298/// Resizeable image widget that uses a [protocol::StatefulProtocol] state.
299///
300/// This stateful widget resizes the image at render time.
301///
302/// **Do not use it withou [`thread::ThreadProtocol`] in a reactive UI**. Rendering the widget
303/// **will** block the UI thread if the image has not been resized by another thread.
304///
305/// ```rust
306/// # use ratatui::Frame;
307/// # use ratatui_image::{Resize, StatefulImage, protocol::{StatefulProtocol}};
308/// struct App {
309/// image_state: StatefulProtocol,
310/// }
311/// fn ui(f: &mut Frame<'_>, app: &mut App) {
312/// let image = StatefulImage::default().resize(Resize::Crop(None));
313/// f.render_stateful_widget(
314/// image,
315/// f.area(),
316/// &mut app.image_state,
317/// );
318/// }
319/// ```
320pub struct StatefulImage<T>
321where
322 T: ResizeEncodeRender,
323{
324 resize: Resize,
325 phantom: PhantomData<T>,
326}
327
328impl<T> Default for StatefulImage<T>
329where
330 T: ResizeEncodeRender,
331{
332 fn default() -> Self {
333 Self::new()
334 }
335}
336impl<T> StatefulImage<T>
337where
338 T: ResizeEncodeRender,
339{
340 pub const fn resize(self, resize: Resize) -> Self {
341 Self { resize, ..self }
342 }
343
344 pub const fn new() -> Self {
345 Self {
346 resize: Resize::Fit(None),
347 phantom: PhantomData,
348 }
349 }
350}
351
352impl<T> StatefulWidget for StatefulImage<T>
353where
354 T: ResizeEncodeRender,
355{
356 type State = T;
357 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
358 if area.width == 0 || area.height == 0 {
359 return;
360 }
361
362 state.resize_encode_render(&self.resize, area, buf);
363 }
364}
365
366#[derive(Debug, Clone)]
367/// Resize accounting for terminal [`FontSize`].
368///
369/// Resizes images with [`FontSize`] grid boundaries.
370pub enum Resize {
371 /// Fit to a [`Size`].
372 ///
373 /// If the image width or height is smaller than the target size, the image will be resized
374 /// maintaining proportions.
375 ///
376 /// The [FilterType] (re-exported from the [image] crate) defaults to [FilterType::Nearest].
377 Fit(Option<FilterType>),
378 /// Crop to size.
379 ///
380 /// If the width or height is smaller than the area, the image will be cropped.
381 /// The behaviour is the same as using [`Image`] widget with the overhead of resizing,
382 /// but some terminals might misbehave when overdrawing characters over graphics.
383 /// For example, the sixel branch of Alacritty never draws text over a cell that is currently
384 /// being rendered by some sixel sequence, not necessarily originating from the same cell.
385 ///
386 /// The [CropOptions] defaults to clipping the bottom and the right sides.
387 Crop(Option<CropOptions>),
388 /// Scale the image
389 ///
390 /// Same as `Resize::Fit` except it resizes the image even if the image is smaller than the render area
391 Scale(Option<FilterType>),
392}
393
394impl Default for Resize {
395 fn default() -> Self {
396 Self::Fit(None)
397 }
398}
399
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401/// Specifies which sides to be clipped when cropping an image.
402pub struct CropOptions {
403 /// If `true`, the top side should be clipped.
404 pub clip_top: bool,
405 /// If `true`, the left side should be clipped.
406 pub clip_left: bool,
407}
408
409const DEFAULT_BACKGROUND: Rgba<u8> = Rgba([0, 0, 0, 0]);
410
411impl Resize {
412 /// Resize [`image::DynamicImage`] to fit into the [`Size`] or smaller.
413 pub fn resize(
414 &self,
415 image: &DynamicImage,
416 font_size: FontSize,
417 size: Size,
418 background_color: Option<Rgba<u8>>,
419 ) -> DynamicImage {
420 let width = u32::from(size.width) * u32::from(font_size.width);
421 let height = u32::from(size.height) * u32::from(font_size.height);
422
423 // Resize/Crop/etc., fitting a multiple of font-size, but not necessarily the `size`.
424 let mut image = self.resize_pixels(image, width, height);
425
426 if image.width() != width || image.height() != height {
427 let mut bg: DynamicImage = ImageBuffer::from_pixel(
428 width,
429 height,
430 background_color.unwrap_or(DEFAULT_BACKGROUND),
431 )
432 .into();
433 imageops::overlay(&mut bg, &image, 0, 0);
434 image = bg;
435 }
436 image
437 }
438
439 /// Calculate the [`Size`] for the [`DynamicImage`] for `available` size after resizing.
440 pub fn size_for(&self, image: &DynamicImage, font_size: FontSize, available: Size) -> Size {
441 let (width, height) = self.needs_resize_pixels(
442 image,
443 (available.width as u32) * (font_size.width as u32),
444 (available.height as u32) * (font_size.height as u32),
445 );
446 Self::round_pixel_size_to_cells(width, height, font_size)
447 }
448
449 /// Calculate the "natural" [`Size`] needed to render the [`DynamicImage`] at the [`FontSize`],
450 /// without any resizing.
451 pub fn natural_size(image: &DynamicImage, font_size: FontSize) -> Size {
452 Self::round_pixel_size_to_cells(image.width(), image.height(), font_size)
453 }
454
455 /// Check if [`image::DynamicImage`]'s "desired" fits into `target` and is different than `current`.
456 ///
457 /// The returned `Size` is the area the image needs to be resized to, depending on the resize
458 /// type, or `None` if the image matches `target` perfectly at the [`FontSize`].
459 pub(crate) fn needs_resize(
460 &self,
461 image: &DynamicImage,
462 desired: Option<Size>,
463 font_size: FontSize,
464 current: Option<Size>,
465 target: Size,
466 force: bool,
467 ) -> Option<Size> {
468 let desired = desired.unwrap_or_else(|| Self::natural_size(image, font_size));
469
470 // Check if resize is needed at all.
471 if !force
472 && !matches!(self, &Resize::Scale(_))
473 && desired.width <= target.width
474 && desired.height <= target.height
475 && (current.is_none() || current == Some(desired))
476 {
477 let width = u32::from(desired.width) * u32::from(font_size.width);
478 let height = u32::from(desired.height) * u32::from(font_size.height);
479 if image.width() == width || image.height() == height {
480 return None;
481 }
482 }
483
484 let rect = self.size_for(image, font_size, target);
485 debug_assert!(
486 rect.width <= target.width,
487 "needs_resize exceeds area width"
488 );
489 debug_assert!(
490 rect.height <= target.height,
491 "needs_resize exceeds area height"
492 );
493 if force || Some(rect) != current {
494 return Some(rect);
495 }
496 None
497 }
498
499 fn resize_pixels(&self, image: &DynamicImage, width: u32, height: u32) -> DynamicImage {
500 const DEFAULT_FILTER_TYPE: FilterType = FilterType::Nearest;
501 const DEFAULT_CROP_OPTIONS: CropOptions = CropOptions {
502 clip_top: false,
503 clip_left: false,
504 };
505 match self {
506 Self::Fit(filter_type) | Self::Scale(filter_type) => {
507 image.resize(width, height, filter_type.unwrap_or(DEFAULT_FILTER_TYPE))
508 }
509 Self::Crop(options) => {
510 let options = options.as_ref().unwrap_or(&DEFAULT_CROP_OPTIONS);
511 let y = if options.clip_top {
512 image.height().saturating_sub(height)
513 } else {
514 0
515 };
516 let x = if options.clip_left {
517 image.width().saturating_sub(width)
518 } else {
519 0
520 };
521 image.crop_imm(x, y, width, height)
522 }
523 }
524 }
525
526 fn needs_resize_pixels(&self, image: &DynamicImage, width: u32, height: u32) -> (u32, u32) {
527 match self {
528 Self::Fit(_) => fit_area_proportionally(
529 image.width(),
530 image.height(),
531 min(width, image.width()),
532 min(height, image.height()),
533 ),
534
535 Self::Crop(_) => (min(image.width(), width), min(image.height(), height)),
536 Self::Scale(_) => fit_area_proportionally(image.width(), image.height(), width, height),
537 }
538 }
539
540 /// Round an image pixel size to the nearest matching cell size, given a font size.
541 fn round_pixel_size_to_cells(img_width: u32, img_height: u32, font_size: FontSize) -> Size {
542 let width = (img_width as f32 / font_size.width as f32).ceil() as u16;
543 let height = (img_height as f32 / font_size.height as f32).ceil() as u16;
544 Size::new(width, height)
545 }
546}
547
548/// Ripped from https://github.com/image-rs/image/blob/master/src/math/utils.rs#L12
549/// Calculates the width and height an image should be resized to.
550/// This preserves aspect ratio, and based on the `fill` parameter
551/// will either fill the dimensions to fit inside the smaller constraint
552/// (will overflow the specified bounds on one axis to preserve
553/// aspect ratio), or will shrink so that both dimensions are
554/// completely contained within the given `width` and `height`,
555/// with empty space on one axis.
556fn fit_area_proportionally(width: u32, height: u32, nwidth: u32, nheight: u32) -> (u32, u32) {
557 let wratio = nwidth as f64 / width as f64;
558 let hratio = nheight as f64 / height as f64;
559
560 let ratio = f64::min(wratio, hratio);
561
562 let nw = max((width as f64 * ratio).round() as u64, 1);
563 let nh = max((height as f64 * ratio).round() as u64, 1);
564
565 if nw > u64::from(u16::MAX) {
566 let ratio = u16::MAX as f64 / width as f64;
567 (u32::MAX, max((height as f64 * ratio).round() as u32, 1))
568 } else if nh > u64::from(u16::MAX) {
569 let ratio = u16::MAX as f64 / height as f64;
570 (max((width as f64 * ratio).round() as u32, 1), u32::MAX)
571 } else {
572 (nw as u32, nh as u32)
573 }
574}
575
576#[cfg(test)]
577mod tests {
578 use image::{ImageBuffer, Rgba};
579
580 use super::*;
581
582 const FONT_SIZE: FontSize = FontSize::new(10, 10);
583
584 fn s(w: u16, h: u16) -> DynamicImage {
585 let image: DynamicImage =
586 ImageBuffer::from_pixel(w as _, h as _, Rgba::<u8>([255, 0, 0, 255])).into();
587 image
588 }
589
590 fn r(w: u16, h: u16) -> Size {
591 Size::new(w, h)
592 }
593
594 #[test]
595 fn needs_resize_fit() {
596 let resize = Resize::Fit(None);
597
598 let to = resize.needs_resize(
599 &s(100, 100),
600 None,
601 FONT_SIZE,
602 Some(r(10, 10)),
603 r(10, 10),
604 false,
605 );
606 assert_eq!(None, to);
607
608 let to = resize.needs_resize(
609 &s(101, 101),
610 None,
611 FONT_SIZE,
612 Some(r(10, 10)),
613 r(10, 10),
614 false,
615 );
616 assert_eq!(None, to);
617
618 let to = resize.needs_resize(
619 &s(80, 100),
620 None,
621 FONT_SIZE,
622 Some(r(8, 10)),
623 r(10, 10),
624 false,
625 );
626 assert_eq!(None, to);
627
628 let to = resize.needs_resize(
629 &s(100, 100),
630 None,
631 FONT_SIZE,
632 Some(r(99, 99)),
633 r(8, 10),
634 false,
635 );
636 assert_eq!(Some(r(8, 8)), to);
637
638 let to = resize.needs_resize(
639 &s(100, 100),
640 None,
641 FONT_SIZE,
642 Some(r(99, 99)),
643 r(10, 8),
644 false,
645 );
646 assert_eq!(Some(r(8, 8)), to);
647
648 let to = resize.needs_resize(
649 &s(100, 50),
650 None,
651 FONT_SIZE,
652 Some(r(99, 99)),
653 r(4, 4),
654 false,
655 );
656 assert_eq!(Some(r(4, 2)), to);
657
658 let to = resize.needs_resize(
659 &s(50, 100),
660 None,
661 FONT_SIZE,
662 Some(r(99, 99)),
663 r(4, 4),
664 false,
665 );
666 assert_eq!(Some(r(2, 4)), to);
667
668 let to = resize.needs_resize(
669 &s(100, 100),
670 None,
671 FONT_SIZE,
672 Some(r(8, 8)),
673 r(11, 11),
674 false,
675 );
676 assert_eq!(Some(r(10, 10)), to);
677
678 let to = resize.needs_resize(
679 &s(100, 100),
680 None,
681 FONT_SIZE,
682 Some(r(10, 10)),
683 r(11, 11),
684 false,
685 );
686 assert_eq!(None, to);
687 }
688
689 #[test]
690 fn needs_resize_crop() {
691 let resize = Resize::Crop(None);
692
693 let to = resize.needs_resize(
694 &s(100, 100),
695 None,
696 FONT_SIZE,
697 Some(r(10, 10)),
698 r(10, 10),
699 false,
700 );
701 assert_eq!(None, to);
702
703 let to = resize.needs_resize(
704 &s(80, 100),
705 None,
706 FONT_SIZE,
707 Some(r(8, 10)),
708 r(10, 10),
709 false,
710 );
711 assert_eq!(None, to);
712
713 let to = resize.needs_resize(
714 &s(100, 100),
715 None,
716 FONT_SIZE,
717 Some(r(10, 10)),
718 r(8, 10),
719 false,
720 );
721 assert_eq!(Some(r(8, 10)), to);
722
723 let to = resize.needs_resize(
724 &s(100, 100),
725 None,
726 FONT_SIZE,
727 Some(r(10, 10)),
728 r(10, 8),
729 false,
730 );
731 assert_eq!(Some(r(10, 8)), to);
732 }
733}