Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encapsulate the delay ratio #1128

Merged
merged 4 commits into from Feb 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
251 changes: 244 additions & 7 deletions src/animation.rs
@@ -1,4 +1,5 @@
use std::iter::Iterator;
use std::time::Duration;

use num_rational::Ratio;

Expand Down Expand Up @@ -37,38 +38,44 @@ impl<'a> Iterator for Frames<'a> {
#[derive(Clone)]
pub struct Frame {
/// Delay between the frames in milliseconds
delay_ms: Ratio<u32>,
delay: Delay,
/// x offset
left: u32,
/// y offset
top: u32,
buffer: RgbaImage,
}

/// The delay of a frame relative to the previous one.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)]
pub struct Delay {
ratio: Ratio<u32>,
}

impl Frame {
/// Contructs a new frame
/// Contructs a new frame without any delay.
pub fn new(buffer: RgbaImage) -> Frame {
Frame {
delay_ms: Ratio::from_integer(0),
delay: Delay::from_ratio(Ratio::from_integer(0)),
left: 0,
top: 0,
buffer,
}
}

/// Contructs a new frame
pub fn from_parts(buffer: RgbaImage, left: u32, top: u32, delay_ms: Ratio<u32>) -> Frame {
pub fn from_parts(buffer: RgbaImage, left: u32, top: u32, delay: Delay) -> Frame {
Frame {
delay_ms,
delay,
left,
top,
buffer,
}
}

/// Delay of this frame
pub fn delay_ms(&self) -> Ratio<u32> {
self.delay_ms
pub fn delay(&self) -> Delay {
self.delay
}

/// Returns the image buffer
Expand All @@ -91,3 +98,233 @@ impl Frame {
self.top
}
}

impl Delay {
/// Create a delay from a ratio of milliseconds.
///
/// # Examples
///
/// ```
/// use image::Delay;
/// let delay_10ms = Delay::from_numer_denom_ms(10, 1);
/// ```
pub fn from_numer_denom_ms(numerator: u32, denominator: u32) -> Self {
Delay { ratio: Ratio::new_raw(numerator, denominator) }
}

/// Convert from a duration, clamped between 0 and an implemented defined maximum.
///
/// The maximum is *at least* `i32::MAX` milliseconds. It should be noted that the accuracy of
/// the result may be relative and very large delays have a coarse resolution.
///
/// # Examples
///
/// ```
/// use std::time::Duration;
/// use image::Delay;
///
/// let duration = Duration::from_millis(20);
/// let delay = Delay::from_saturating_duration(duration);
/// ```
pub fn from_saturating_duration(duration: Duration) -> Self {
// A few notes: The largest number we can represent as a ratio is u32::MAX but we can
// sometimes represent much smaller numbers.
//
// We can represent duration as `millis+a/b` (where a < b, b > 0).
// We must thus bound b with `b·millis + (b-1) <= u32::MAX` or
// > `0 < b <= (u32::MAX + 1)/(millis + 1)`
// Corollary: millis <= u32::MAX

const MILLIS_BOUND: u128 = u32::max_value() as u128;

let millis = duration.as_millis().min(MILLIS_BOUND);
let submillis = (duration.as_nanos() % 1_000_000) as u32;

let max_b = if millis > 0 {
((MILLIS_BOUND + 1)/(millis + 1)) as u32
} else {
MILLIS_BOUND as u32
};
let millis = millis as u32;

let (a, b) = Self::closest_bounded_fraction(max_b, submillis, 1_000_000);
Self::from_numer_denom_ms(a + b*millis, b)
}

/// The numerator and denominator of the delay in milliseconds.
///
/// This is guaranteed to be an exact conversion if the `Delay` was previously created with the
/// `from_numer_denom_ms` constructor.
pub fn numer_denom_ms(self) -> (u32, u32) {
(*self.ratio.numer(), *self.ratio.denom())
}

pub(crate) fn from_ratio(ratio: Ratio<u32>) -> Self {
Delay { ratio }
}

pub(crate) fn into_ratio(self) -> Ratio<u32> {
self.ratio
}

/// Given some fraction, compute an approximation with denominator bounded.
///
/// Note that `denom_bound` bounds nominator and denominator of all intermediate
/// approximations and the end result.
fn closest_bounded_fraction(denom_bound: u32, nom: u32, denom: u32) -> (u32, u32) {
use std::cmp::Ordering::{self, *};
assert!(0 < denom);
assert!(0 < denom_bound);
assert!(nom < denom);

// Avoid a few type troubles. All intermediate results are bounded by `denom_bound` which
// is in turn bounded by u32::MAX. Representing with u64 allows multiplication of any two
// values without fears of overflow.

// Compare two fractions whose parts fit into a u32.
fn compare_fraction((an, ad): (u64, u64), (bn, bd): (u64, u64)) -> Ordering {
(an*bd).cmp(&(bn*ad))
}

// Computes the nominator of the absolute difference between two such fractions.
fn abs_diff_nom((an, ad): (u64, u64), (bn, bd): (u64, u64)) -> u64 {
let c0 = an*bd;
let c1 = ad*bn;

let d0 = c0.max(c1);
let d1 = c0.min(c1);
d0 - d1
}

let exact = (u64::from(nom), u64::from(denom));
// The lower bound fraction, numerator and denominator.
let mut lower = (0u64, 1u64);
// The upper bound fraction, numerator and denominator.
let mut upper = (1u64, 1u64);
// The closest approximation for now.
let mut guess = (u64::from(nom*2 > denom), 1u64);

// loop invariant: ad, bd <= denom_bound
// iterates the Farey sequence.
loop {
// Break if we are done.
if compare_fraction(guess, exact) == Equal {
break;
}

// Break if next Farey number is out-of-range.
if u64::from(denom_bound) - lower.1 < upper.1 {
break;
}

// Next Farey approximation n between a and b
let next = (lower.0 + upper.0, lower.1 + upper.1);
// if F < n then replace the upper bound, else replace lower.
if compare_fraction(exact, next) == Less {
upper = next;
} else {
lower = next;
}

// Now correct the closest guess.
// In other words, if |c - f| > |n - f| then replace it with the new guess.
// This favors the guess with smaller denominator on equality.

// |g - f| = |g_diff_nom|/(gd*fd);
let g_diff_nom = abs_diff_nom(guess, exact);
// |n - f| = |n_diff_nom|/(nd*fd);
let n_diff_nom = abs_diff_nom(next, exact);

// The difference |n - f| is smaller than |g - f| if either the integral part of the
// fraction |n_diff_nom|/nd is smaller than the one of |g_diff_nom|/gd or if they are
// the same but the fractional part is larger.
if match (n_diff_nom/next.1).cmp(&(g_diff_nom/guess.1)) {
Less => true,
Greater => false,
// Note that the nominator for the fractional part is smaller than its denominator
// which is smaller than u32 and can't overflow the multiplication with the other
// denominator, that is we can compare these fractions by multiplication with the
// respective other denominator.
Equal => compare_fraction((n_diff_nom%next.1, next.1), (g_diff_nom%guess.1, guess.1)) == Less,
} {
guess = next;
}
}

(guess.0 as u32, guess.1 as u32)
}
}

impl From<Delay> for Duration {
fn from(delay: Delay) -> Self {
let ratio = delay.into_ratio();
let ms = ratio.to_integer();
let rest = ratio.numer() % ratio.denom();
let nanos = (u64::from(rest) * 1_000_000) / u64::from(*ratio.denom());
Duration::from_millis(ms.into()) + Duration::from_nanos(nanos)
}
}

#[cfg(test)]
mod tests {
use super::{Delay, Duration, Ratio};

#[test]
fn simple() {
let second = Delay::from_numer_denom_ms(1000, 1);
assert_eq!(Duration::from(second), Duration::from_secs(1));
}

#[test]
fn fps_30() {
let thirtieth = Delay::from_numer_denom_ms(1000, 30);
let duration = Duration::from(thirtieth);
assert_eq!(duration.as_secs(), 0);
assert_eq!(duration.subsec_millis(), 33);
assert_eq!(duration.subsec_nanos(), 33_333_333);
}

#[test]
fn duration_outlier() {
let oob = Duration::from_secs(0xFFFF_FFFF);
let delay = Delay::from_saturating_duration(oob);
assert_eq!(delay.numer_denom_ms(), (0xFFFF_FFFF, 1));
}

#[test]
fn duration_approx() {
let oob = Duration::from_millis(0xFFFF_FFFF) + Duration::from_micros(1);
let delay = Delay::from_saturating_duration(oob);
assert_eq!(delay.numer_denom_ms(), (0xFFFF_FFFF, 1));

let inbounds = Duration::from_millis(0xFFFF_FFFF) - Duration::from_micros(1);
let delay = Delay::from_saturating_duration(inbounds);
assert_eq!(delay.numer_denom_ms(), (0xFFFF_FFFF, 1));

let fine = Duration::from_millis(0xFFFF_FFFF/1000) + Duration::from_micros(0xFFFF_FFFF%1000);
let delay = Delay::from_saturating_duration(fine);
// Funnily, 0xFFFF_FFFF is divisble by 5, thus we compare with a `Ratio`.
assert_eq!(delay.into_ratio(), Ratio::new(0xFFFF_FFFF, 1000));
}

#[test]
fn precise() {
// The ratio has only 32 bits in the numerator, too imprecise to get more than 11 digits
// correct. But it may be expressed as 1_000_000/3 instead.
let exceed = Duration::from_secs(333) + Duration::from_nanos(333_333_333);
let delay = Delay::from_saturating_duration(exceed);
assert_eq!(Duration::from(delay), exceed);
}


#[test]
fn small() {
// Not quite a delay of `1 ms`.
let delay = Delay::from_numer_denom_ms(1 << 16, (1 << 16) + 1);
let duration = Duration::from(delay);
assert_eq!(duration.as_millis(), 0);
// Not precisely the original but should be smaller than 0.
let delay = Delay::from_saturating_duration(duration);
assert_eq!(delay.into_ratio().to_integer(), 0);
}
}
4 changes: 2 additions & 2 deletions src/gif.rs
Expand Up @@ -244,7 +244,7 @@ impl<R: Read> Iterator for GifFrameIterator<R> {
}

let frame = animation::Frame::from_parts(
image_buffer.clone(), 0, 0, delay
image_buffer.clone(), 0, 0, animation::Delay::from_ratio(delay),
);

match dispose {
Expand Down Expand Up @@ -375,7 +375,7 @@ impl<W: Write> Encoder<W> {
-> ImageResult<Frame<'static>>
{
// get the delay before converting img_frame
let frame_delay = img_frame.delay_ms().to_integer();
let frame_delay = img_frame.delay().into_ratio().to_integer();
// convert img_frame into RgbaImage
let mut rbga_frame = img_frame.into_buffer();
let (width, height) = self.gif_dimensions(
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Expand Up @@ -68,7 +68,7 @@ pub use crate::dynimage::{load_from_memory, load_from_memory_with_format, open,

pub use crate::dynimage::DynamicImage;

pub use crate::animation::{Frame, Frames};
pub use crate::animation::{Delay, Frame, Frames};

// More detailed error type
pub mod error;
Expand Down