dezoomify_rs/iiif/
mod.rs

1use std::sync::Arc;
2
3use custom_error::custom_error;
4use log::{debug, info};
5
6use tile_info::ImageInfo;
7
8use crate::dezoomer::*;
9use crate::iiif::tile_info::TileSizeFormat;
10use crate::json_utils::all_json;
11use crate::max_size_in_rect;
12
13pub mod tile_info;
14
15/// Dezoomer for the International Image Interoperability Framework.
16/// See https://iiif.io/
17#[derive(Default)]
18pub struct IIIF;
19
20custom_error! {pub IIIFError
21    JsonError{source: serde_json::Error} = "Invalid IIIF info.json file: {source}"
22}
23
24impl From<IIIFError> for DezoomerError {
25    fn from(err: IIIFError) -> Self {
26        DezoomerError::Other { source: err.into() }
27    }
28}
29
30impl Dezoomer for IIIF {
31    fn name(&self) -> &'static str {
32        "iiif"
33    }
34
35    fn zoom_levels(&mut self, data: &DezoomerInput) -> Result<ZoomLevels, DezoomerError> {
36        let with_contents = data.with_contents()?;
37        let contents = with_contents.contents;
38        let uri = with_contents.uri;
39        Ok(zoom_levels(uri, contents)?)
40    }
41}
42
43fn zoom_levels(url: &str, raw_info: &[u8]) -> Result<ZoomLevels, IIIFError> {
44    match serde_json::from_slice(raw_info) {
45        Ok(info) => Ok(zoom_levels_from_info(url, info)),
46        Err(e) => {
47            // Due to the very fault-tolerant way we parse iiif manifests, a single javascript
48            // object with a 'width' and a 'height' field is enough to be detected as an IIIF level
49            // See https://github.com/lovasoa/dezoomify-rs/issues/80
50            let levels: Vec<ZoomLevel> = all_json::<ImageInfo>(raw_info)
51                .filter(|info| {
52                    let keep = info.has_distinctive_iiif_properties();
53                    if keep {
54                        debug!("keeping image info {:?} because it has distinctive IIIF properties", info)
55                    } else {
56                        info!("dropping level {:?}", info)
57                    }
58                    keep
59                })
60                .flat_map(|info| zoom_levels_from_info(url, info))
61                .collect();
62            if levels.is_empty() {
63                Err(e.into())
64            } else {
65                info!("No normal info.json parsing failed ({}), \
66                but {} inline json5 zoom level(s) were found.",
67                      e, levels.len());
68                Ok(levels)
69            }
70        }
71    }
72}
73
74fn zoom_levels_from_info(url: &str, mut image_info: ImageInfo) -> ZoomLevels {
75    image_info.remove_test_id();
76    let img = Arc::new(image_info);
77    let tiles = img.tiles();
78    let base_url = &Arc::from(url.replace("/info.json", ""));
79    let levels = tiles
80        .iter()
81        .flat_map(|tile_info| {
82            let tile_size = tile_info.size();
83            let quality = Arc::from(img.best_quality());
84            let format = Arc::from(img.best_format());
85            let size_format = img.preferred_size_format();
86            info!("Chose the following image parameters: tile_size=({}) quality={} format={}",
87                  tile_size, quality, format);
88            let page_info = &img; // Required to allow the move
89            tile_info
90                .scale_factors
91                .iter()
92                .map(move |&scale_factor| IIIFZoomLevel {
93                    scale_factor,
94                    tile_size,
95                    page_info: Arc::clone(page_info),
96                    base_url: Arc::clone(base_url),
97                    quality: Arc::clone(&quality),
98                    format: Arc::clone(&format),
99                    size_format,
100                })
101        })
102        .into_zoom_levels();
103    levels
104}
105
106struct IIIFZoomLevel {
107    scale_factor: u32,
108    tile_size: Vec2d,
109    page_info: Arc<ImageInfo>,
110    base_url: Arc<str>,
111    quality: Arc<str>,
112    format: Arc<str>,
113    size_format: TileSizeFormat,
114}
115
116impl TilesRect for IIIFZoomLevel {
117    fn size(&self) -> Vec2d {
118        self.page_info.size() / self.scale_factor
119    }
120
121    fn tile_size(&self) -> Vec2d {
122        self.tile_size
123    }
124
125    fn tile_url(&self, col_and_row_pos: Vec2d) -> String {
126        let scaled_tile_size = self.tile_size * self.scale_factor;
127        let xy_pos = col_and_row_pos * scaled_tile_size;
128        let scaled_tile_size = max_size_in_rect(xy_pos, scaled_tile_size, self.page_info.size());
129        let tile_size = scaled_tile_size / self.scale_factor;
130        format!(
131            "{base}/{x},{y},{img_w},{img_h}/{tile_size}/{rotation}/{quality}.{format}",
132            base = self.page_info.id.as_deref().unwrap_or_else(|| self.base_url.as_ref()),
133            x = xy_pos.x,
134            y = xy_pos.y,
135            img_w = scaled_tile_size.x,
136            img_h = scaled_tile_size.y,
137            tile_size = TileSizeFormatter { w: tile_size.x, h: tile_size.y, format: self.size_format },
138            rotation = 0,
139            quality = self.quality,
140            format = self.format,
141        )
142    }
143}
144
145struct TileSizeFormatter { w: u32, h: u32, format: TileSizeFormat }
146
147impl std::fmt::Display for TileSizeFormatter {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        match self.format {
150            TileSizeFormat::WidthHeight => write!(f, "{},{}", self.w, self.h),
151            TileSizeFormat::Width => write!(f, "{},", self.w),
152        }
153    }
154}
155
156impl std::fmt::Debug for IIIFZoomLevel {
157    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
158        let name = self
159            .base_url
160            .split('/')
161            .last()
162            .and_then(|s: &str| {
163                let s = s.trim();
164                if s.is_empty() {
165                    None
166                } else {
167                    Some(s)
168                }
169            })
170            .unwrap_or("IIIF Image");
171        write!(f, "{}", name)
172    }
173}
174
175#[test]
176fn test_tiles() {
177    let data = br#"{
178      "@context" : "http://iiif.io/api/image/2/context.json",
179      "@id" : "http://www.asmilano.it/fast/iipsrv.fcgi?IIIF=/opt/divenire/files/./tifs/05/36/536765.tif",
180      "protocol" : "http://iiif.io/api/image",
181      "width" : 15001,
182      "height" : 48002,
183      "tiles" : [
184         { "width" : 512, "height" : 512, "scaleFactors" : [ 1, 2, 4, 8, 16, 32, 64, 128 ] }
185      ],
186      "profile" : [
187         "http://iiif.io/api/image/2/level1.json",
188         { "formats" : [ "jpg" ],
189           "qualities" : [ "native","color","gray" ],
190           "supports" : ["regionByPct","sizeByForcedWh","sizeByWh","sizeAboveFull","rotationBy90s","mirroring","gray"] }
191      ]
192    }"#;
193    let mut levels = zoom_levels("test.com", data).unwrap();
194    let tiles: Vec<String> = levels[6]
195        .next_tiles(None)
196        .into_iter()
197        .map(|t| t.url)
198        .collect();
199    assert_eq!(tiles, vec![
200        "http://www.asmilano.it/fast/iipsrv.fcgi?IIIF=/opt/divenire/files/./tifs/05/36/536765.tif/0,0,15001,32768/234,512/0/default.jpg",
201        "http://www.asmilano.it/fast/iipsrv.fcgi?IIIF=/opt/divenire/files/./tifs/05/36/536765.tif/0,32768,15001,15234/234,238/0/default.jpg",
202    ])
203}
204
205
206#[test]
207fn test_tiles_max_area_filter() {
208    // Predefined tile size (1024x1024) is over maxArea (262144 = 512x512).
209    // See https://github.com/lovasoa/dezoomify-rs/issues/107#issuecomment-862225501
210    let data = br#"{
211      "width" : 1024,
212      "height" : 1024,
213      "tiles" : [{ "width" : 1024, "scaleFactors" : [ 1 ] }],
214      "profile" :  [ { "maxArea": 262144 } ]
215    }"#;
216    let mut levels = zoom_levels("http://ophir.dev/info.json", data).unwrap();
217    let tiles: Vec<String> = levels[0]
218        .next_tiles(None)
219        .into_iter()
220        .map(|t| t.url)
221        .collect();
222    assert_eq!(tiles, vec![
223        "http://ophir.dev/0,0,512,512/512,512/0/default.jpg",
224        "http://ophir.dev/512,0,512,512/512,512/0/default.jpg",
225        "http://ophir.dev/0,512,512,512/512,512/0/default.jpg",
226        "http://ophir.dev/512,512,512,512/512,512/0/default.jpg",
227    ])
228}
229
230#[test]
231fn test_missing_id() {
232    let data = br#"{
233      "width" : 600,
234      "height" : 350
235    }"#;
236    let mut levels = zoom_levels("http://test.com/info.json", data).unwrap();
237    let tiles: Vec<String> = levels[0]
238        .next_tiles(None)
239        .into_iter()
240        .map(|t| t.url)
241        .collect();
242    assert_eq!(
243        tiles,
244        vec![
245            "http://test.com/0,0,512,350/512,350/0/default.jpg",
246            "http://test.com/512,0,88,350/88,350/0/default.jpg"
247        ]
248    )
249}
250
251#[test]
252fn test_false_positive() {
253    let data = br#"
254    var mainImage={
255        type:       "zoomifytileservice",
256        width:      62596,
257        height:     38467,
258        tilesUrl:   "./ORIONFINAL/"
259    };
260    "#;
261    let res = zoom_levels("https://orion2020v5b.spaceforeverybody.com/", data);
262    assert!(res.is_err(), "openseadragon zoomify image should not be misdetected");
263}
264
265#[test]
266fn test_qualities() {
267    let data = br#"{
268        "@context": "http://library.stanford.edu/iiif/image-api/1.1/context.json",
269        "@id": "https://images.britishart.yale.edu/iiif/fd470c3e-ead0-4878-ac97-d63295753f82",
270        "tile_height": 1024,
271        "tile_width": 1024,
272        "width": 5156,
273        "height": 3816,
274        "profile": "http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level0",
275        "qualities": [ "native", "color", "bitonal", "gray", "zorglub" ],
276        "formats" : [ "png", "zorglub" ],
277        "scale_factors": [ 10 ]
278    }"#;
279    let mut levels = zoom_levels("test.com", data).unwrap();
280    let level = &mut levels[0];
281    assert_eq!(level.size_hint(), Some(Vec2d { x: 515, y: 381 }));
282    let tiles: Vec<String> = level
283        .next_tiles(None)
284        .into_iter()
285        .map(|t| t.url)
286        .collect();
287    assert_eq!(tiles, vec![
288        "https://images.britishart.yale.edu/iiif/fd470c3e-ead0-4878-ac97-d63295753f82/0,0,5156,3816/515,381/0/native.png",
289    ])
290}