document_graphics/images/
regions.rs1use 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 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#[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 ®ion 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
125pub 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}