crates/veilvoice-video/src/size.rs

what this file is for · veilvoice-video · 732 lines · the same file on GitHub

The file as it is in the tree, in the colours you chose. A line number is a link, and so is every box in this file’s diagram: it opens here with the function it names marked.


// SPDX-License-Identifier: GPL-3.0-or-later
//! The size and frame rate a video is rendered at.
//!
//! # Why this is a module rather than two numbers
//!
//! The renderer used to write `1280x720` at thirty frames a second and offer no
//! way to say otherwise. Both numbers were reasonable defaults and neither was a
//! choice anybody could make, which is the whole of the problem: somebody
//! rendering a conversation to put on a screen wants it to look like the screen
//! they are putting it on.
//!
//! Getting that wrong is expensive in a way a wrong colour is not. Frames are
//! rendered one file at a time before `ffmpeg` is asked for anything, so a
//! choice made at the start decides how long the render takes, how much disk it
//! wants while it runs, and whether it finishes at all. [`Plan::estimate`]
//! exists so a front end can say that before starting rather than after.
//!
//! # The rules a size has to obey, and where they come from
//!
//! **Both dimensions have to be even.** H.264 with `yuv420p`, which is what
//! every player and every platform accepts, stores colour at half resolution in
//! both directions, so an odd dimension has half a chroma sample in it. `ffmpeg`
//! refuses outright: "width not divisible by 2". A person typing 1921 has made a
//! typo rather than a request, so [`Size::new`] says so and
//! [`Size::nearest_valid`] offers the size they meant.
//!
//! **There is a floor and a ceiling**, and both are about somebody's typo
//! rather than about taste. Under [`MIN_EDGE`] the subtitles are unreadable and
//! the waveform is a line. Over [`MAX_EDGE`], which is 8K, an extra digit turns
//! a ten-minute render into one that fills the disk: at 4K a frame is about
//! eight megabytes before compression, and at 60 frames a second that is half a
//! gigabyte for every second of recording.
//!
//! # The default is the screen it will be watched on, when the screen will say
//!
//! [`Choice::Monitor`] is the default and it is *resolved late*: the size is
//! decided when the render starts, from the display the program is actually
//! running on, rather than stored as a number that becomes wrong when somebody
//! plugs in a different monitor.
//!
//! Where nothing can say what the display is running at, and a command line on a
//! machine with no display server is the ordinary case, it falls back to
//! [`Preset::Hd1080`] **and says so**. A guess about somebody's monitor
//! presented as a detection would be worse than a stated default.
//!
//! # In plain words
//!
//! How big the video is and how many pictures a second it has.
//!
//! By default it matches the screen you are using, because that is usually the
//! screen you are going to watch it on. You can pick 720p, 1080p, 1440p or 4K
//! instead, or type your own size, and anything up to 60 frames a second.
//!
//! Bigger and faster is not better here. The picture is a waveform, some
//! circles and words on a flat background, none of which move quickly, so 4K at
//! 60 costs a great deal of time and disk for something that looks the same as
//! 1080p at 30 to almost everybody.

use crate::Error;


/// The shortest edge a render may have, in pixels.
///
/// Below this the subtitles cannot be read and the waveform is one pixel tall,
/// so the file would be a video of nothing. 256 is the smallest that survives a
/// phone screen.
pub const MIN_EDGE: u32 = 256;



/// The longest edge a render may have, in pixels.
///
/// 8K. Not because anybody needs it, but because a limit has to be somewhere
/// and this is the largest thing that exists to be watched on.
pub const MAX_EDGE: u32 = 7680;



/// The most frames a second a render may have.
///
/// **Sixty, and it is a cap rather than a target.** Nothing in this picture
/// moves quickly: a waveform scrolls, a circle brightens, words appear. Sixty
/// doubles the frames, the render time and the file against thirty and looks
/// the same to almost everybody. It is offered because somebody cutting this
/// into 60fps footage needs it to match, which is a real reason, and it is not
/// the default because it is not an improvement.
pub const MAX_FPS: u32 = 60;



/// The fewest frames a second a render may have.
///
/// Under this the waveform stutters rather than scrolls.
pub const MIN_FPS: u32 = 5;



/// A frame size in pixels, known to be one a render can actually use.
///
/// Constructed only through [`Size::new`], so a value of this type has already
/// been checked: both edges even, both inside [`MIN_EDGE`] and [`MAX_EDGE`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Size {
    width: u32,
    height: u32,
}


impl Size {

    /// A size, if it is one a video can be rendered at.
    ///
    /// The error says which rule was broken and what the nearest allowed size
    /// is, because "invalid size" sends somebody to guess and this does not.
    pub fn new(width: u32, height: u32) -> Result<Self, Error> {
        if width < MIN_EDGE || height < MIN_EDGE {
            return Err(Error::Size(format!(
                "{width}x{height} is smaller than {MIN_EDGE} on one side, where \
                 the subtitles cannot be read"
            )));
        }
        if width > MAX_EDGE || height > MAX_EDGE {
            return Err(Error::Size(format!(
                "{width}x{height} is larger than {MAX_EDGE} on one side, which \
                 is 8K and is the largest this renders"
            )));
        }
        if width % 2 == 1 || height % 2 == 1 {
            let near = Self {
                width: width & !1,
                height: height & !1,
            };
            return Err(Error::Size(format!(
                "{width}x{height} has an odd side, which H.264 cannot store in \
                 yuv420p; {} is the nearest size that works",
                near.label()
            )));
        }
        Ok(Self { width, height })
    }



    /// The nearest size that obeys every rule, for offering after a refusal.
    ///
    /// Rounds each edge down to even and clamps it into range. Always returns a
    /// value: the clamping cannot fail, because both bounds are themselves
    /// even and inside the range.
    pub fn nearest_valid(width: u32, height: u32) -> Self {
        let fix = |edge: u32| edge.clamp(MIN_EDGE, MAX_EDGE) & !1;
        Self {
            width: fix(width),
            height: fix(height),
        }
    }



    /// Width in pixels. Always even.
    pub fn width(self) -> u32 {
        self.width
    }



    /// Height in pixels. Always even.
    pub fn height(self) -> u32 {
        self.height
    }



    /// How many pixels one frame holds.
    ///
    /// `u64` deliberately: 7680 by 4320 is 33 million, which fits a `u32`, but
    /// multiplying it by a frame count does not, and this is the number that
    /// gets multiplied.
    pub fn pixels(self) -> u64 {
        u64::from(self.width) * u64::from(self.height)
    }



    /// What `ffmpeg` wants after `-s`, and what a person reads in a menu.
    ///
    /// `1920x1080`, with the familiar name after it where there is one. Nobody
    /// says "1920 by 1080" out loud and everybody says "1080p".
    pub fn label(self) -> String {
        match Preset::matching(self) {
            Some(preset) => format!("{}x{} ({})", self.width, self.height, preset.name()),
            None => format!("{}x{}", self.width, self.height),
        }
    }



    /// Just the digits, for a command line argument.
    pub fn geometry(self) -> String {
        format!("{}x{}", self.width, self.height)
    }

}


/// The sizes offered by name.
///
/// Sixteen by nine throughout, because that is what every player, every phone
/// and every platform expects, and because a conversation rendered to a shape
/// nobody uses gets black bars added by somebody else's software.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Preset {
    /// 1280x720.
    Hd720,
    /// 1920x1080. The fallback when no display can be asked.
    Hd1080,
    /// 2560x1440.
    Qhd1440,
    /// 3840x2160, which is what "4K" means for video.
    Uhd2160,
}


impl Preset {

    /// Every preset, in the order a menu should list them.
    pub const ALL: [Preset; 4] = [

        Preset::Hd720,
        Preset::Hd1080,
        Preset::Qhd1440,
        Preset::Uhd2160,
    ];


    /// The size this preset means.
    pub fn size(self) -> Size {
        let (width, height) = match self {
            Preset::Hd720 => (1280, 720),
            Preset::Hd1080 => (1920, 1080),
            Preset::Qhd1440 => (2560, 1440),
            Preset::Uhd2160 => (3840, 2160),
        };
        // Every one of these is even and in range by construction, so the
        // checking constructor cannot refuse them. Written as an expect with
        // the reason rather than a second unchecked constructor, so there is
        // exactly one way to make a `Size`.
        Size::new(width, height).expect("a preset is a valid size by construction")
    }



    /// What a person calls it.
    pub fn name(self) -> &'static str {
        match self {
            Preset::Hd720 => "720p",
            Preset::Hd1080 => "1080p",
            Preset::Qhd1440 => "1440p",
            Preset::Uhd2160 => "4K",
        }
    }



    /// What it answers to on a command line. Lower case and stable.
    pub fn key(self) -> &'static str {
        match self {
            Preset::Hd720 => "720p",
            Preset::Hd1080 => "1080p",
            Preset::Qhd1440 => "1440p",
            Preset::Uhd2160 => "4k",
        }
    }



    /// The preset a size is, if it is one of them.
    pub fn matching(size: Size) -> Option<Preset> {
        Preset::ALL.into_iter().find(|p| p.size() == size)
    }

}


/// What the user asked for, before anything has looked at the display.
///
/// Held rather than resolved, so that "match my screen" stays true when the
/// screen changes. See the module documentation.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Choice {
    /// Whatever the display this is running on is set to.
    #[default]
    Monitor,
    /// One of the sizes offered by name.
    Named(Preset),
    /// A size somebody typed.
    Exact(Size),
}



/// A size, and why it is that size.
///
/// The reason travels with the number because a front end has to be able to say
/// "1080p, because this machine could not tell us what your display is running
/// at" rather than showing 1080p as though it had been detected.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Resolved {
    /// The size to render at.
    pub size: Size,
    /// What to tell the person, or nothing when the answer needs no explaining.
    pub note: Option<String>,
}


impl Choice {

    /// What this answers to on a command line, in the order help should list it.
    ///
    /// One list, used by the command line's help, the command line's parser and
    /// the window's menu, so a name that works in one works in all of them.
    pub const KEYS: [&'static str; 5] = ["monitor", "720p", "1080p", "1440p", "4k"];



    /// Read a choice somebody typed.
    ///
    /// Accepts a name from [`Choice::KEYS`] or an explicit `WIDTHxHEIGHT`. The
    /// `x` may be an `X` or a `*`, because both are what people type, and the
    /// error names every accepted form rather than only refusing.
    pub fn parse(text: &str) -> Result<Self, Error> {
        let text = text.trim();
        let lower = text.to_ascii_lowercase();
        if lower == "monitor" || lower == "screen" || lower == "auto" {
            return Ok(Choice::Monitor);
        }
        if let Some(preset) = Preset::ALL.into_iter().find(|p| p.key() == lower) {
            return Ok(Choice::Named(preset));
        }
        // `2160p` and `1080` are what somebody means even though the menu does
        // not spell them that way, so they are read rather than refused.
        if let Some(preset) = Preset::ALL.into_iter().find(|p| {
            lower == format!("{}p", p.size().height()) || lower == p.size().height().to_string()
        }) {
            return Ok(Choice::Named(preset));
        }
        let parts: Vec<&str> = lower.split(['x', '*']).collect();
        if parts.len() == 2 {
            if let (Ok(width), Ok(height)) = (
                parts[0].trim().parse::<u32>(),
                parts[1].trim().parse::<u32>(),
            ) {
                return Ok(Choice::Exact(Size::new(width, height)?));
            }
        }
        Err(Error::Size(format!(
            "`{text}` is not a size. Use one of {}, or a size such as 1920x1080.",
            Choice::KEYS.join(", ")
        )))
    }



    /// What this reads as in a menu or a report.
    pub fn describe(self) -> String {
        match self {
            Choice::Monitor => "match this display".to_string(),
            Choice::Named(preset) => preset.size().label(),
            Choice::Exact(size) => size.label(),
        }
    }



    /// Turn a choice into a size, given what the display said.
    ///
    /// `monitor` is the display's size where something could ask for it, and
    /// `None` where nothing could: a headless command line, a platform with no
    /// interface for it, a remote session. Both cases are ordinary and neither
    /// is an error.
    ///
    /// A monitor size that breaks the rules is corrected rather than refused. A
    /// display running at an odd height is a fact about somebody's hardware and
    /// not a mistake they made, so the nearest usable size is taken and the
    /// note says what happened.
    pub fn resolve(self, monitor: Option<(u32, u32)>) -> Resolved {
        match self {
            Choice::Named(preset) => Resolved {
                size: preset.size(),
                note: None,
            },
            Choice::Exact(size) => Resolved { size, note: None },
            Choice::Monitor => match monitor {
                Some((width, height)) => match Size::new(width, height) {
                    Ok(size) => Resolved { size, note: None },
                    Err(_) => {
                        let size = Size::nearest_valid(width, height);
                        Resolved {
                            note: Some(format!(
                                "this display is {width}x{height}, which a video \
                                 cannot be exactly; rendering at {} instead",
                                size.label()
                            )),
                            size,
                        }
                    }
                },
                None => Resolved {
                    size: Preset::Hd1080.size(),
                    note: Some(
                        "nothing here can say what this display is running at, \
                         so this is 1080p rather than a guess"
                            .to_string(),
                    ),
                },
            },
        }
    }

}


/// Frames per second, known to be inside the range a render allows.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FrameRate(u32);


impl FrameRate {

    /// A frame rate, if it is one a render allows.
    pub fn new(fps: u32) -> Result<Self, Error> {
        if !(MIN_FPS..=MAX_FPS).contains(&fps) {
            return Err(Error::Size(format!(
                "{fps} frames a second is outside {MIN_FPS} to {MAX_FPS}"
            )));
        }
        Ok(Self(fps))
    }



    /// The number.
    pub fn get(self) -> u32 {
        self.0
    }



    /// The rates offered by name, in the order a menu should list them.
    ///
    /// Twenty-four because it is what film runs at and what somebody cutting
    /// this into film footage needs; thirty as the default; fifty and sixty for
    /// matching the two broadcast rates.
    pub const OFFERED: [u32; 4] = [24, 30, 50, 60];

}

impl FrameRate {

    /// Read a frame rate somebody typed.
    ///
    /// A trailing `fps` is accepted because it is what people write.
    pub fn parse(text: &str) -> Result<Self, Error> {
        let text = text.trim().to_ascii_lowercase();
        let digits = text.strip_suffix("fps").unwrap_or(&text).trim();
        match digits.parse::<u32>() {
            Ok(fps) => Self::new(fps),
            Err(_) => Err(Error::Size(format!(
                "`{text}` is not a frame rate. Use a whole number from \
                 {MIN_FPS} to {MAX_FPS}, such as 30."
            ))),
        }
    }

}

impl Default for FrameRate {

    fn default() -> Self {
        // Thirty. Enough for a waveform and a circle that brightens, and half
        // the frames of sixty for a picture that does not move fast enough to
        // tell them apart.
        Self(30)
    }

}


/// A size and a frame rate together, with what they will cost.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Plan {
    /// The frame size.
    pub size: Size,
    /// Frames per second.
    pub fps: FrameRate,
}



/// What a render is going to want before it is started.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Estimate {
    /// How many frames will be written.
    pub frames: u64,
    /// Roughly how many bytes those frames occupy on disk while rendering.
    ///
    /// **Before the encode, not after.** The video file is far smaller; this is
    /// the temporary directory the frames pass through, which is what fills a
    /// disk. Deliberately rough and deliberately generous: a flat-coloured PNG
    /// compresses to a small fraction of its pixels, and an estimate that is
    /// too low is the one that hurts.
    pub scratch_bytes: u64,
}



/// A byte count, in the units a person reads.
///
/// Here rather than in a front end because both the window and the command line
/// print the same estimate, and two roundings of one number is two numbers.
/// Binary units, because a disk with "1 GB free" has 1 GiB free and the
/// estimate is about whether the render fits.
pub fn human_bytes(bytes: u64) -> String {
    const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
    let mut value = bytes as f64;
    let mut unit = 0;
    while value >= 1024.0 && unit + 1 < UNITS.len() {
        value /= 1024.0;
        unit += 1;
    }
    if unit == 0 {
        format!("{bytes} B")
    } else if value >= 100.0 {
        format!("{value:.0} {}", UNITS[unit])
    } else {
        format!("{value:.1} {}", UNITS[unit])
    }
}


impl Plan {

    /// A plan.
    pub fn new(size: Size, fps: FrameRate) -> Self {
        Self { size, fps }
    }



    /// What rendering `seconds` of recording will want.
    ///
    /// Saturating throughout. An hour of 8K at sixty frames a second is a
    /// number nobody should reach, and reaching it should produce a large
    /// figure a front end can refuse, not an overflow.
    pub fn estimate(&self, seconds: f64) -> Estimate {
        let seconds = if seconds.is_finite() && seconds > 0.0 {
            seconds
        } else {
            0.0
        };
        let frames = (seconds * f64::from(self.fps.get())).ceil();
        let frames = if frames >= 0.0 && frames <= u64::MAX as f64 {
            frames as u64
        } else {
            u64::MAX
        };
        // A quarter of a byte per pixel. Flat colour, large areas of one shade
        // and a little text is what PNG is best at, and measured output for
        // these frames sits well under this.
        let per_frame = (self.size.pixels() / 4).max(1);
        Estimate {
            frames,
            scratch_bytes: frames.saturating_mul(per_frame),
        }
    }

}

impl Default for Plan {

    fn default() -> Self {
        Self::new(Preset::Hd1080.size(), FrameRate::default())
    }

}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn every_preset_is_a_size_a_video_can_be() {
        for preset in Preset::ALL {
            let size = preset.size();
            assert_eq!(size.width() % 2, 0, "{} width", preset.name());
            assert_eq!(size.height() % 2, 0, "{} height", preset.name());
            assert!(size.width() >= MIN_EDGE && size.width() <= MAX_EDGE);
            assert!(size.height() >= MIN_EDGE && size.height() <= MAX_EDGE);
            // Sixteen by nine, to one part in a thousand.
            let ratio = f64::from(size.width()) / f64::from(size.height());
            assert!((ratio - 16.0 / 9.0).abs() < 0.001, "{}", preset.name());
        }
    }

    #[test]
    fn a_preset_knows_its_own_size_and_back_again() {
        for preset in Preset::ALL {
            assert_eq!(Preset::matching(preset.size()), Some(preset));
        }
        // And a size that is not one of them says so rather than guessing.
        assert_eq!(Preset::matching(Size::new(1000, 1000).unwrap()), None);
    }

    #[test]
    fn an_odd_side_is_refused_and_the_nearest_even_one_is_offered() {
        let refused = Size::new(1921, 1080).unwrap_err().to_string();
        assert!(refused.contains("odd side"), "{refused}");
        assert!(refused.contains("1920x1080"), "{refused}");
        assert!(refused.contains("yuv420p"), "{refused}");
        assert_eq!(
            Size::nearest_valid(1921, 1081),
            Size::new(1920, 1080).unwrap()
        );
    }

    #[test]
    fn sizes_outside_the_range_say_which_end_they_fell_off() {
        let small = Size::new(100, 100).unwrap_err().to_string();
        assert!(small.contains("smaller"), "{small}");
        let large = Size::new(10_000, 10_000).unwrap_err().to_string();
        assert!(large.contains("larger"), "{large}");
        // The bounds themselves are allowed, which is what "inside" means.
        assert!(Size::new(MIN_EDGE, MIN_EDGE).is_ok());
        assert!(Size::new(MAX_EDGE, MAX_EDGE).is_ok());
    }

    #[test]
    fn nearest_valid_always_produces_something_new_accepts() {
        // Including the shapes that broke every rule at once.
        for (w, h) in [(0, 0), (1, 1), (99_999, 3), (7681, 4321), (1921, 1081)] {
            let fixed = Size::nearest_valid(w, h);
            assert!(
                Size::new(fixed.width(), fixed.height()).is_ok(),
                "{w}x{h} became {fixed:?}, which new() refuses"
            );
        }
    }

    #[test]
    fn the_default_choice_follows_the_display() {
        let resolved = Choice::default().resolve(Some((2560, 1440)));
        assert_eq!(resolved.size, Preset::Qhd1440.size());
        assert!(resolved.note.is_none(), "a plain answer needs no note");
    }

    #[test]
    fn a_display_nothing_can_ask_about_falls_back_and_says_so() {
        let resolved = Choice::Monitor.resolve(None);
        assert_eq!(resolved.size, Preset::Hd1080.size());
        let note = resolved.note.expect("a fallback has to be explained");
        assert!(note.contains("1080p"), "{note}");
        assert!(note.contains("guess"), "{note}");
    }

    #[test]
    fn an_awkward_display_is_corrected_rather_than_refused() {
        // A real shape: some laptop panels are 3000x2000, and 1366x768 has an
        // odd sibling in 1365x767 on scaled displays.
        let resolved = Choice::Monitor.resolve(Some((1365, 767)));
        assert_eq!(resolved.size, Size::new(1364, 766).unwrap());
        let note = resolved.note.expect("a correction has to be explained");
        assert!(note.contains("1365x767"), "{note}");
        assert!(note.contains("1364x766"), "{note}");
    }

    #[test]
    fn a_display_below_the_floor_is_lifted_to_it() {
        let resolved = Choice::Monitor.resolve(Some((320, 200)));
        assert_eq!(resolved.size.height(), MIN_EDGE);
        assert!(resolved.note.is_some());
    }

    #[test]
    fn a_named_or_exact_choice_ignores_the_display_entirely() {
        let named = Choice::Named(Preset::Hd720).resolve(Some((3840, 2160)));
        assert_eq!(named.size, Preset::Hd720.size());
        assert!(named.note.is_none());

        let exact = Size::new(1000, 500).unwrap();
        assert_eq!(Choice::Exact(exact).resolve(None).size, exact);
    }

    #[test]
    fn the_frame_rate_stops_at_sixty() {
        assert_eq!(FrameRate::new(60).unwrap().get(), 60);
        let over = FrameRate::new(61).unwrap_err().to_string();
        assert!(over.contains("60"), "{over}");
        assert!(FrameRate::new(MIN_FPS - 1).is_err());
        assert_eq!(FrameRate::default().get(), 30);
        for fps in FrameRate::OFFERED {
            assert!(FrameRate::new(fps).is_ok(), "{fps} is offered and refused");
        }
    }

    #[test]
    fn an_estimate_grows_with_size_and_rate_and_never_overflows() {
        let small = Plan::new(Preset::Hd720.size(), FrameRate::new(24).unwrap());
        let large = Plan::new(Preset::Uhd2160.size(), FrameRate::new(60).unwrap());
        let (a, b) = (small.estimate(60.0), large.estimate(60.0));
        assert!(b.frames > a.frames);
        assert!(b.scratch_bytes > a.scratch_bytes);
        assert_eq!(a.frames, 60 * 24);

        // The shapes that would panic or wrap if this were arithmetic on `u32`.
        for seconds in [0.0, -1.0, f64::NAN, f64::INFINITY, 1e30] {
            let _ = large.estimate(seconds);
        }
        assert_eq!(large.estimate(f64::NAN).frames, 0);
        assert_eq!(large.estimate(-1.0).frames, 0);
    }

    #[test]
    fn every_offered_name_parses_back_to_what_it_names() {
        assert_eq!(Choice::parse("monitor").unwrap(), Choice::Monitor);
        for preset in Preset::ALL {
            assert_eq!(
                Choice::parse(preset.key()).unwrap(),
                Choice::Named(preset),
                "{}",
                preset.key()
            );
        }
        // Every key the help prints has to be one the parser takes, or the
        // help is a list of things that do not work.
        for key in Choice::KEYS {
            assert!(
                Choice::parse(key).is_ok(),
                "help offers `{key}` and it fails"
            );
        }
    }

    #[test]
    fn the_forms_people_actually_type_are_read_rather_than_refused() {
        for text in ["1080P", " 1080p ", "2160p", "2160", "auto", "SCREEN"] {
            assert!(Choice::parse(text).is_ok(), "{text}");
        }
        assert_eq!(
            Choice::parse("2160p").unwrap(),
            Choice::Named(Preset::Uhd2160)
        );
        for text in ["1920x1080", "1920X1080", "1920*1080", " 1920 x 1080 "] {
            assert_eq!(
                Choice::parse(text).unwrap(),
                Choice::Exact(Preset::Hd1080.size()),
                "{text}"
            );
        }
        for text in ["30fps", "30 FPS", " 30 "] {
            assert_eq!(FrameRate::parse(text).unwrap().get(), 30, "{text}");
        }
    }

    #[test]
    fn a_size_that_is_not_one_says_what_would_be() {
        let refused = Choice::parse("enormous").unwrap_err().to_string();
        for key in Choice::KEYS {
            assert!(refused.contains(key), "{refused} omits {key}");
        }
        assert!(refused.contains("1920x1080"), "{refused}");

        // An explicit size that breaks a rule keeps the rule's own message,
        // which is more useful than "not a size".
        let odd = Choice::parse("1921x1080").unwrap_err().to_string();
        assert!(odd.contains("odd side"), "{odd}");

        let bad_rate = FrameRate::parse("lots").unwrap_err().to_string();
        assert!(bad_rate.contains("60"), "{bad_rate}");
    }

    #[test]
    fn a_choice_describes_itself_without_needing_a_display() {
        assert_eq!(Choice::Monitor.describe(), "match this display");
        assert_eq!(Choice::Named(Preset::Uhd2160).describe(), "3840x2160 (4K)");
    }

    #[test]
    fn bytes_are_printed_in_units_somebody_reads() {
        assert_eq!(human_bytes(0), "0 B");
        assert_eq!(human_bytes(999), "999 B");
        assert_eq!(human_bytes(1024), "1.0 KiB");
        assert_eq!(human_bytes(1536), "1.5 KiB");
        assert_eq!(human_bytes(1024 * 1024), "1.0 MiB");
        assert_eq!(human_bytes(200 * 1024 * 1024), "200 MiB");
        // The largest thing this can be handed, which must not panic or wrap.
        assert!(human_bytes(u64::MAX).ends_with("TiB"));
    }

    #[test]
    fn a_label_names_the_preset_where_there_is_one() {
        assert_eq!(Preset::Hd1080.size().label(), "1920x1080 (1080p)");
        assert_eq!(Size::new(1000, 500).unwrap().label(), "1000x500");
        assert_eq!(Preset::Uhd2160.size().geometry(), "3840x2160");
    }
}