i3f 0.0.3

A library for IIIF API, including Image, Presentation.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
use std::{fmt::Display, str::FromStr};

use image::{DynamicImage, imageops::FilterType};

use crate::IiifError;

/// Size 大小尺寸的定义
///
/// Example:
/// ```
/// use i3f::image::Size;
/// use std::str::FromStr;
///
/// let size = Size::from_str("max").unwrap();
/// assert_eq!(size, Size::Max);
///
/// let size: Size = "max".parse().unwrap();
/// assert_eq!(size, Size::Max);
/// ```
#[derive(Debug, PartialEq)]
pub enum Size {
    /// Format: `max`
    /// The extracted region is returned at the maximum size available, but will not be upscaled.
    /// The resulting image will have the pixel dimensions of the extracted region,
    /// unless it is constrained to a smaller size by `maxWidth`, `maxHeight`,
    /// or `maxArea` as defined in the Technical Properties section.
    ///
    /// 提取的区域将以最大可用尺寸返回,但不会进行放大处理。
    /// 除非受到技术属性章节中定义的 `maxWidth`, `maxHeight` 或 `maxArea` 参数限制而缩小尺寸,
    /// 否则生成图像的像素尺寸将与提取区域保持一致。
    Max,
    /// Format: `^max`
    /// The extracted region is scaled to the maximum size permitted by maxWidth, maxHeight, or maxArea as
    /// defined in the Technical Properties section. If the resulting dimensions are greater than the pixel
    /// width and height of the extracted region, the extracted region is upscaled.
    ///
    /// 提取的区域将缩放至技术属性章节中定义的 `maxWidth`, `maxHeight` 或 `maxArea` 所允许的最大尺寸。
    /// 若最终尺寸大于提取区域的像素宽度和高度,则对提取区域进行放大处理。
    CMax,
    /// Format: `w,`
    /// The extracted region should be scaled so that the width of the returned image is exactly equal to `w`.
    /// The value of `w` must not be greater than the width of the extracted region.
    ///
    /// 提取的区域应按比例缩放,使返回图像的宽度精确等于 `w` 。 `w` 的值不得大于提取区域的宽度。
    W { w: u32 },
    /// Format: `^w,`
    /// The extracted region should be scaled so that the width of the returned image is exactly equal to `w`.
    /// If `w` is greater than the pixel width of the extracted region, the extracted region is upscaled.
    ///
    /// 提取的区域应按比例缩放,使返回图像的宽度精确等于 `w` 。如果 `w` 大于提取区域的像素宽度,则提取区域将被放大。
    CW { w: u32 },
    /// Format: `,h`
    /// The extracted region should be scaled so that the height of the returned image is exactly equal to `h`.
    /// The value of `h` must not be greater than the height of the extracted region.
    ///
    /// 提取的区域应按比例缩放,使返回图像的高度精确等于 `h` 。 `h` 的值不得大于提取区域的高度。
    H { h: u32 },
    /// Format: `^,h`
    /// The extracted region should be scaled so that the height of the returned image is exactly equal to `h`.
    /// If `h` is greater than the pixel height of the extracted region, the extracted region is upscaled.
    ///
    /// 提取的区域应按比例缩放,使返回图像的高度精确等于 `h` 。如果 `h` 大于提取区域的像素高度,则提取区域将被放大。
    CH { h: u32 },
    /// Format: `pct:n`
    /// The width and height of the returned image is scaled to `n` percent of the width and height of the
    /// extracted region. The value of `n` must not be greater than 100.
    ///
    /// 返回图像的宽度和高度将缩放至提取区域宽高的 `n` 百分比。 `n` 的取值不得超过 100。
    Pct { n: f32 },
    /// Format: `^pct:n`
    /// The width and height of the returned image is scaled to `n` percent of the width and height of the
    /// extracted region. For values of `n` greater than 100, the extracted region is upscaled.
    ///
    /// 返回图像的宽度和高度将缩放至提取区域宽高的 `n` 百分比。当 `n` 取值超过 100 时,提取区域将被放大。
    CPct { n: f32 },
    /// Format: `w,h`
    /// The width and height of the returned image are exactly `w` and `h`.
    /// The aspect ratio of the returned image may be significantly different than the extracted region,
    /// resulting in a distorted image. The values of `w` and `h` must not be greater than the corresponding
    /// pixel dimensions of the extracted region.
    ///
    /// 返回图像的宽度和高度严格限定为 `w` 和 `h` 。返回图像的宽高比可能与提取区域存在显著差异,导致图像变形。
    /// `w` 和 `h` 的取值不得超过提取区域对应的像素尺寸。
    WH { w: u32, h: u32 },
    /// Format: `^w,h`
    /// The width and height of the returned image are exactly `w` and `h`. The aspect ratio of the returned
    /// image may be significantly different than the extracted region, resulting in a distorted image.
    /// If `w` and/or `h` are greater than the corresponding pixel dimensions of the extracted region, the
    /// extracted region is upscaled.
    ///
    /// 返回图像的宽度和高度精确为 `w` 和 `h` 。返回图像的宽高比可能与提取区域存在显著差异,导致图像变形。
    /// 若 `w` 和/或 `h` 大于提取区域的对应像素尺寸,则提取区域将被放大。
    CWH { w: u32, h: u32 },
    /// Format: `!w,h`
    /// The extracted region is scaled so that the width and height of the returned image are not greater
    /// than `w` and `h`, while maintaining the aspect ratio. The returned image must be as large as possible
    /// but not larger than the extracted region, `w` or `h`, or server-imposed limits.
    ///
    /// 提取的区域会进行缩放,使返回图像的宽度和高度不超过 `w` 和 `h` ,同时保持宽高比。返回的图像应尽可能大,但不得超过提取区域、
    /// `w` 或 `h` 的尺寸,或服务器设定的限制。
    LWH { w: u32, h: u32 },
    /// Format: `^!w,h`
    /// The extracted region is scaled so that the width and height of the returned image are not greater than `w` and `h`,
    /// while maintaining the aspect ratio. The returned image must be as large as possible but not larger than `w`, `h`,
    /// or server-imposed limits.
    ///
    /// 提取的区域会进行缩放,使返回图像的宽度和高度不超过 `w` 和 `h` ,同时保持宽高比。返回的图像应尽可能大,但不得超过 `w` 、 `h` 的尺寸,
    /// 或服务器设定的限制。
    CLWH { w: u32, h: u32 },
}

impl FromStr for Size {
    type Err = IiifError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let s = s.trim().to_lowercase();

        // 处理关键词
        match s.as_str() {
            "max" => return Ok(Self::Max),
            "^max" => return Ok(Self::CMax),
            _ => {}
        }

        // 分离 caret 前缀
        let (caret, content) = s
            .strip_prefix('^')
            .map(|c| (true, c))
            .unwrap_or((false, s.as_str()));

        // 解析具体格式
        Self::parse_content(content, caret)
            .ok_or(IiifError::BadRequest("Invalid size format".to_string()))
    }
}

impl Size {
    /// 处理图片缩放,返回缩放后的图片
    pub fn process(&self, image: DynamicImage) -> Result<DynamicImage, IiifError> {
        let filter_type = FilterType::Nearest;
        match self {
            // TODO: maxWidth, maxHeight, maxArea
            Self::Max => Ok(image),
            // TODO: maxWidth, maxHeight, maxArea
            Self::CMax => Ok(image),
            Self::W { w } => {
                if *w > image.width() {
                    return Err(IiifError::BadRequest(
                        "Width is greater than image width".to_string(),
                    ));
                }
                Ok(image.resize(*w, image.height(), filter_type))
            }
            Self::CW { w } => {
                let height =
                    (image.height() as f32 * *w as f32 / image.width() as f32).round() as u32;
                Ok(image.resize(*w, height, filter_type))
            }
            Self::H { h } => {
                if *h > image.height() {
                    return Err(IiifError::BadRequest(
                        "Height is greater than image height".to_string(),
                    ));
                }
                Ok(image.resize(image.width(), *h, filter_type))
            }
            Self::CH { h } => {
                let width =
                    (image.width() as f32 * *h as f32 / image.height() as f32).round() as u32;
                Ok(image.resize(width, *h, filter_type))
            }
            Self::Pct { n } => {
                if *n > 100.0 {
                    return Err(IiifError::BadRequest(
                        "Percentage is greater than 100".to_string(),
                    ));
                }
                Ok(image.resize(
                    (image.width() as f32 * *n / 100.0).round() as u32,
                    (image.height() as f32 * *n / 100.0).round() as u32,
                    filter_type,
                ))
            }
            Self::CPct { n } => Ok(image.resize(
                (image.width() as f32 * *n / 100.0).round() as u32,
                (image.height() as f32 * *n / 100.0).round() as u32,
                filter_type,
            )),
            Self::WH { w, h } => {
                if *w > image.width() || *h > image.height() {
                    return Err(IiifError::BadRequest(
                        "Width or height is greater than image width or height".to_string(),
                    ));
                }
                Ok(image.resize_exact(*w, *h, filter_type))
            }
            Self::CWH { w, h } => Ok(image.resize_exact(*w, *h, filter_type)),
            Self::LWH { w, h } => {
                if *w > image.width() || *h > image.height() {
                    return Err(IiifError::BadRequest(
                        "Width or height is greater than image width or height".to_string(),
                    ));
                }
                Ok(image.resize(*w, *h, filter_type))
            }
            Self::CLWH { w, h } => Ok(image.resize(*w, *h, filter_type)),
        }
    }

    // 解析内容
    fn parse_content(content: &str, caret: bool) -> Option<Self> {
        if let Some(pct) = content.strip_prefix("pct:") {
            Self::parse_pct(pct, caret)
        } else if let Some(coords) = content.strip_prefix('!') {
            Self::parse_fit(coords, caret)
        } else if content.contains(',') {
            Self::parse_dims(content, caret)
        } else {
            None
        }
    }

    // 解析百分比
    fn parse_pct(pct_str: &str, caret: bool) -> Option<Self> {
        let n = pct_str.parse().ok()?;
        if n < 0.0 {
            return None;
        }
        if caret {
            Some(Self::CPct { n })
        } else if n <= 100.0 {
            Some(Self::Pct { n })
        } else {
            return None;
        }
    }

    // 解析最佳适配
    fn parse_fit(coords: &str, caret: bool) -> Option<Self> {
        let (w, h) = Self::parse_two_nums(coords)?;
        Some(if caret {
            Self::CLWH { w, h }
        } else {
            Self::LWH { w, h }
        })
    }

    // 解析尺寸
    fn parse_dims(content: &str, mut caret: bool) -> Option<Self> {
        let mut parts = content.split(',');
        let w_str = parts.next()?;
        let h_str = parts.next()?;
        if parts.next().is_some() {
            return None;
        }

        let w = w_str.parse().ok();
        let h = h_str
            .strip_prefix('^')
            .inspect(|_| {
                caret = true;
            })
            .unwrap_or(h_str)
            .parse()
            .ok();

        match (w, h, caret) {
            (Some(w), Some(h), true) => Some(Self::CWH { w, h }),
            (Some(w), Some(h), false) => Some(Self::WH { w, h }),
            (Some(w), None, true) => Some(Self::CW { w }),
            (Some(w), None, false) => Some(Self::W { w }),
            (None, Some(h), true) => Some(Self::CH { h }),
            (None, Some(h), false) => Some(Self::H { h }),
            _ => None,
        }
    }

    // 解析两个数字
    fn parse_two_nums(coords: &str) -> Option<(u32, u32)> {
        let mut parts = coords.split(',');
        let w = parts.next()?.parse().ok()?;
        let h = parts.next()?.parse().ok()?;
        parts.next().is_none().then_some((w, h))
    }
}

impl Display for Size {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Max => write!(f, "max"),
            Self::CMax => write!(f, "^max"),
            Self::W { w } => write!(f, "{w},"),
            Self::CW { w } => write!(f, "^{w},"),
            Self::H { h } => write!(f, ",{h}"),
            Self::CH { h } => write!(f, "^,{h}"),
            Self::Pct { n } => write!(f, "pct:{n}"),
            Self::CPct { n } => write!(f, "^pct:{n}"),
            Self::WH { w, h } => write!(f, "{w},{h}"),
            Self::CWH { w, h } => write!(f, "^{w},{h}"),
            Self::LWH { w, h } => write!(f, "!{w},{h}"),
            Self::CLWH { w, h } => write!(f, "^!{w},{h}"),
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::storage::LocalStorage;
    use crate::storage::Storage;

    use super::*;

    #[test]
    fn test_size_parsing() {
        // 基本格式
        assert_eq!(Size::from_str("max").unwrap(), Size::Max);
        assert_eq!(Size::from_str("^max").unwrap(), Size::CMax);

        // 宽度/高度
        assert_eq!(Size::from_str("150,").unwrap(), Size::W { w: 150 });
        assert_eq!(Size::from_str("^360,").unwrap(), Size::CW { w: 360 });
        assert_eq!(Size::from_str(",150").unwrap(), Size::H { h: 150 });
        assert_eq!(Size::from_str("^,240").unwrap(), Size::CH { h: 240 });
        assert_eq!(Size::from_str(",^240").unwrap(), Size::CH { h: 240 });

        // 百分比
        assert_eq!(Size::from_str("pct:50").unwrap(), Size::Pct { n: 50.0 });
        assert!(Size::from_str("pct:150").is_err());
        assert_eq!(Size::from_str("^pct:150").unwrap(), Size::CPct { n: 150.0 });

        // 精确尺寸
        assert_eq!(
            Size::from_str("225,100").unwrap(),
            Size::WH { w: 225, h: 100 }
        );
        assert_eq!(
            Size::from_str("^360,360").unwrap(),
            Size::CWH { w: 360, h: 360 }
        );

        // 最佳适配
        assert_eq!(
            Size::from_str("!225,100").unwrap(),
            Size::LWH { w: 225, h: 100 }
        );
        assert_eq!(
            Size::from_str("^!360,360").unwrap(),
            Size::CLWH { w: 360, h: 360 }
        );

        assert!(Size::from_str("pct:150").is_err());
        assert!(Size::from_str("aaaaaaa").is_err());
        assert!(Size::from_str("pct:-10").is_err());
        assert!(Size::from_str("100,100,100,100,100").is_err());
    }

    #[test]
    fn test_size_display() {
        assert_eq!(format!("{}", Size::Max), "max");
        assert_eq!(format!("{}", Size::Pct { n: 50.0 }), "pct:50");
        assert_eq!(format!("{}", Size::WH { w: 100, h: 200 }), "100,200");
        assert_eq!(format!("{}", Size::LWH { w: 100, h: 200 }), "!100,200");
        assert_eq!(format!("{}", Size::CH { h: 150 }), "^,150");
    }

    #[test]
    fn test_roundtrip() {
        let cases = [
            "max",
            "^max",
            "150,",
            "^360,",
            ",150",
            "pct:50",
            "^pct:150",
            "225,100",
            "^360,360",
            "!225,100",
            "^!360,360",
        ];

        for case in cases {
            let size = Size::from_str(case).unwrap();
            assert_eq!(format!("{size}"), case);
        }
    }

    #[test]
    fn test_size_process() {
        let storage = LocalStorage::new("./fixtures", "./fixtures/out");
        let cases = vec![
            ("max", 300, 200),
            ("^max", 300, 200),
            ("150,", 150, 100),
            ("^360,", 360, 240),
            (",150", 225, 150),
            ("^,240", 360, 240),
            ("pct:50", 150, 100),
            ("^pct:120", 360, 240),
            ("225,100", 225, 100),
            ("^360,360", 360, 360),
            ("!225,100", 150, 100),
            ("^!360,360", 360, 240),
        ];
        for case in cases {
            let size = case.0.parse::<Size>().unwrap();
            let image = storage.get_origin_file("demo.jpg").unwrap();
            let image = image::load_from_memory(&image).unwrap();
            let resized_image = size.process(image).unwrap();
            assert_eq!(resized_image.width(), case.1);
            assert_eq!(resized_image.height(), case.2);
        }
    }

    #[test]
    fn test_size_process_error() {
        let storage = LocalStorage::new("./fixtures", "./fixtures/out");
        let cases = vec!["500,", ",500", "500,200", "200,500", "!500,500"];
        for case in cases {
            let size = case.parse::<Size>().unwrap();
            let image = storage.get_origin_file("demo.jpg").unwrap();
            let image = image::load_from_memory(&image).unwrap();
            let result = size.process(image);
            assert!(result.is_err());
        }
    }
}