Skip to main content

document_graphics/images/
regions.rs

1use serde::{Deserialize, Serialize};
2
3pub const SCALE_UNIT: u32 = 4096;
4pub const TILE_EDGE: u32 = 512;
5pub const OVERVIEW_EDGE: u32 = 1600;
6pub const MAX_BATCH_PIXELS: u64 = 4 * 1024 * 1024;
7pub const MAX_REGIONS: usize = 16;
8pub const MAX_SOURCE_PIXELS: u64 = 2_000_000_000;
9
10#[derive(Clone, Debug, Serialize, Deserialize)]
11pub struct Info {
12    pub width: u32,
13    pub height: u32,
14    pub frames: u32,
15    /// Total plays; None means repeat indefinitely. Still images have one play.
16    pub plays: Option<u32>,
17    pub format: String,
18    pub color: String,
19}
20impl Info {
21    pub fn validate(&self) -> Result<(), String> {
22        if self.width == 0
23            || self.height == 0
24            || self.width > 100_000
25            || self.height > 100_000
26            || u64::from(self.width) * u64::from(self.height) > MAX_SOURCE_PIXELS
27            || self.frames == 0
28            || self.frames > 10_000
29            || self.format.len() > 128
30            || self.color.len() > 256
31        {
32            return Err("Image dimensions or frame count exceed preview limits".into());
33        }
34        Ok(())
35    }
36    pub fn overview(&self, frame: u32) -> Region {
37        self.overview_at(frame, OVERVIEW_EDGE)
38    }
39    pub fn overview_at(&self, frame: u32, edge: u32) -> Region {
40        let scale = (edge.min(OVERVIEW_EDGE) as f64 / self.width.max(self.height) as f64).min(1.);
41        let scale = (scale * SCALE_UNIT as f64).floor().max(1.) as u32;
42        let (width, height) = self.scaled_size(scale);
43        Region {
44            frame,
45            scale,
46            x: 0,
47            y: 0,
48            width,
49            height,
50        }
51    }
52    pub fn scaled_size(&self, scale: u32) -> (u32, u32) {
53        let side = |n| (u64::from(n) * u64::from(scale)).div_ceil(SCALE_UNIT as u64) as u32;
54        (side(self.width), side(self.height))
55    }
56}
57
58/// A rectangle in the scaled image, measured in physical output pixels.
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
60pub struct Region {
61    pub frame: u32,
62    pub scale: u32,
63    pub x: u32,
64    pub y: u32,
65    pub width: u32,
66    pub height: u32,
67}
68impl Region {
69    pub fn validate(self, info: &Info) -> Result<(), String> {
70        info.validate()?;
71        let (w, h) = info.scaled_size(self.scale);
72        if self.frame >= info.frames
73            || !(1..=64 * SCALE_UNIT).contains(&self.scale)
74            || self.width == 0
75            || self.height == 0
76            || self.width > OVERVIEW_EDGE
77            || self.height > OVERVIEW_EDGE
78            || u64::from(self.x) + u64::from(self.width) > u64::from(w)
79            || u64::from(self.y) + u64::from(self.height) > u64::from(h)
80        {
81            return Err("Invalid image region".into());
82        }
83        Ok(())
84    }
85    pub fn bytes(self) -> usize {
86        self.width as usize * self.height as usize * 4
87    }
88    pub fn source_x(self, x: u32, width: u32) -> u32 {
89        (((u64::from(self.x) + u64::from(x)) * SCALE_UNIT as u64) / self.scale as u64)
90            .min(width.saturating_sub(1) as u64) as u32
91    }
92    pub fn source_y(self, y: u32, height: u32) -> u32 {
93        (((u64::from(self.y) + u64::from(y)) * SCALE_UNIT as u64) / self.scale as u64)
94            .min(height.saturating_sub(1) as u64) as u32
95    }
96}
97
98pub fn validate_regions(info: &Info, regions: &[Region]) -> Result<(), String> {
99    if regions.is_empty() || regions.len() > MAX_REGIONS {
100        return Err("Invalid image region count".into());
101    }
102    let mut pixels = 0u64;
103    for &region in regions {
104        region.validate(info)?;
105        if region.frame != regions[0].frame {
106            return Err("Mixed image frames in one request".into());
107        }
108        pixels += u64::from(region.width) * u64::from(region.height);
109    }
110    if pixels > MAX_BATCH_PIXELS {
111        return Err("Image request exceeds the decoded-pixel budget".into());
112    }
113    Ok(())
114}
115
116pub struct Pixels {
117    pub region: Region,
118    pub rgba: Vec<u8>,
119}
120pub struct Frame {
121    pub pixels: Vec<Pixels>,
122    pub delay_ms: u32,
123}
124
125/// Match browser behavior for absent/very short delays; never busy-loop a zero-delay GIF.
126pub fn frame_delay(seconds: f64) -> u32 {
127    if !seconds.is_finite() || seconds < 0.011 {
128        100
129    } else {
130        (seconds * 1000.).round().clamp(11., 60_000.) as u32
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    #[test]
138    fn oversized_metadata_regions_and_batches_are_rejected_before_allocating() {
139        let info = Info {
140            width: 40_000,
141            height: 40_000,
142            frames: 1,
143            plays: Some(1),
144            format: "PNG".into(),
145            color: "RGB".into(),
146        };
147        let overview = info.overview(0);
148        validate_regions(&info, &[overview]).unwrap();
149        assert!(overview.bytes() <= 1600 * 1600 * 4);
150        for region in [
151            Region {
152                scale: 0,
153                ..overview
154            },
155            Region {
156                x: u32::MAX,
157                ..overview
158            },
159            Region {
160                frame: 1,
161                ..overview
162            },
163            Region {
164                width: 5000,
165                ..overview
166            },
167        ] {
168            assert!(validate_regions(&info, &[region]).is_err());
169        }
170        assert!(validate_regions(&info, &[overview, overview]).is_err());
171        assert!(
172            Info {
173                width: 100_001,
174                ..info
175            }
176            .validate()
177            .is_err()
178        );
179    }
180    #[test]
181    fn animation_delays_never_spin_or_schedule_invalid_timers() {
182        for seconds in [0., -1., f64::NAN, 0.01] {
183            assert_eq!(frame_delay(seconds), 100);
184        }
185        assert_eq!(frame_delay(0.04), 40);
186        assert_eq!(frame_delay(1000.), 60_000);
187    }
188}