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#[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 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; 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 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}