1use std::sync::Arc;
2
3use custom_error::custom_error;
4use log::debug;
5
6use dzi_file::DziFile;
7
8use crate::dezoomer::*;
9use crate::json_utils::all_json;
10use crate::network::remove_bom;
11use regex::Regex;
12mod dzi_file;
13
14#[derive(Default)]
17pub struct DziDezoomer;
18
19impl Dezoomer for DziDezoomer {
20 fn name(&self) -> &'static str {
21 "deepzoom"
22 }
23
24 fn zoom_levels(&mut self, data: &DezoomerInput) -> Result<ZoomLevels, DezoomerError> {
25 let tile_re = Regex::new("_files/\\d+/\\d+_\\d+\\.(jpe?g|png)$").unwrap();
26 if let Some(m) = tile_re.find(&data.uri) {
27 let meta_uri = data.uri[..m.start()].to_string() + ".dzi";
28 debug!("'{}' looks like a dzi image tile URL. Trying to fetch the DZI file at '{}'.", data.uri, meta_uri);
29 Err(DezoomerError::NeedsData { uri: meta_uri })
30 } else {
31 let DezoomerInputWithContents { uri, contents } = data.with_contents()?;
32 let levels = load_from_properties(uri, contents)?;
33 Ok(levels)
34 }
35 }
36}
37
38custom_error! {pub DziError
39 XmlError{source: serde_xml_rs::Error} = "Unable to parse the dzi file: {source}",
40 NoSize = "Expected a size in the DZI file",
41 InvalidTileSize = "Invalid tile size. The tile size cannot be zero.",
42}
43
44impl From<DziError> for DezoomerError {
45 fn from(err: DziError) -> Self {
46 DezoomerError::Other { source: err.into() }
47 }
48}
49
50fn load_from_properties(url: &str, contents: &[u8]) -> Result<ZoomLevels, DziError> {
51
52 serde_xml_rs::from_reader::<_, DziFile>(remove_bom(contents))
55 .map_err(DziError::from)
56 .and_then(|dzi| load_from_dzi(url, dzi))
57 .or_else(|e| {
58 let levels: Vec<ZoomLevel> = all_json::<DziFile>(contents)
59 .flat_map(|dzi| load_from_dzi(url, dzi))
60 .flatten()
61 .collect();
62 if levels.is_empty() { Err(e) } else { Ok(levels) }
63 })
64}
65
66fn load_from_dzi(url: &str, image_properties: DziFile) -> Result<ZoomLevels, DziError> {
67 debug!("Found dzi meta-information: {:?}", image_properties);
68
69 if image_properties.tile_size == 0 {
70 return Err(DziError::InvalidTileSize);
71 }
72
73 let base_url = &Arc::from(image_properties.base_url(url));
74
75 let size = image_properties.get_size()?;
76 let max_level = image_properties.max_level();
77 let levels = std::iter::successors(Some(size), |&size| {
78 if size.x > 1 || size.y > 1 {
79 Some(size.ceil_div(Vec2d::square(2)))
80 } else {
81 None
82 }
83 })
84 .enumerate()
85 .map(|(level_num, size)| DziLevel {
86 base_url: Arc::clone(base_url),
87 size,
88 tile_size: image_properties.get_tile_size(),
89 format: image_properties.format.clone(),
90 overlap: image_properties.overlap,
91 level: max_level - level_num as u32,
92 })
93 .into_zoom_levels();
94 Ok(levels)
95}
96
97struct DziLevel {
98 base_url: Arc<str>,
99 size: Vec2d,
100 tile_size: Vec2d,
101 format: String,
102 overlap: u32,
103 level: u32,
104}
105
106impl TilesRect for DziLevel {
107 fn size(&self) -> Vec2d {
108 self.size
109 }
110
111 fn tile_size(&self) -> Vec2d {
112 self.tile_size
113 }
114
115 fn tile_url(&self, pos: Vec2d) -> String {
116 format!(
117 "{base}/{level}/{x}_{y}.{format}",
118 base = self.base_url,
119 level = self.level,
120 x = pos.x,
121 y = pos.y,
122 format = self.format
123 )
124 }
125
126 fn tile_ref(&self, pos: Vec2d) -> TileReference {
127 let delta = Vec2d {
128 x: if pos.x == 0 { 0 } else { self.overlap },
129 y: if pos.y == 0 { 0 } else { self.overlap },
130 };
131 TileReference {
132 url: self.tile_url(pos),
133 position: self.tile_size() * pos - delta,
134 }
135 }
136
137 fn title(&self) -> Option<String> {
138 let (_, suffix) = self.base_url.rsplit_once( '/').unwrap_or_default();
139 let name = suffix.trim_end_matches("_files");
140 Some(name.to_string())
141 }
142}
143
144impl std::fmt::Debug for DziLevel {
145 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
146 write!(f, "{} (Deep Zoom Image)", TileProvider::title(self).unwrap_or_default())
147 }
148}
149
150#[test]
151fn test_panorama() {
152 let url = "http://x.fr/y/test.dzi";
153 let contents = br#"
154 <Image
155 TileSize="256"
156 Overlap="2"
157 Format="jpg"
158 >
159 <Size Width="600" Height="300"/>
160 <DisplayRects></DisplayRects>
161 </Image>"#;
162 let mut props = load_from_properties(url, contents).unwrap();
163 assert_eq!(props.len(), 11);
164 let level = &mut props[1];
165 let tiles: Vec<String> = level.next_tiles(None).into_iter().map(|t| t.url).collect();
166 assert_eq!(
167 tiles,
168 vec![
169 "http://x.fr/y/test_files/9/0_0.jpg",
170 "http://x.fr/y/test_files/9/1_0.jpg"
171 ]
172 );
173}
174
175
176#[test]
177fn test_dzi_with_bom() {
178 let contents = "\u{feff}<?xml version=\"1.0\" encoding=\"utf-8\"?>
181 <Image TileSize=\"256\" Overlap=\"0\" Format=\"jpg\" xmlns=\"http://schemas.microsoft.com/deepzoom/2008\">
182 <Size Width=\"6261\" Height=\"6047\" />
183 </Image>";
184 load_from_properties("http://test.com/test.xml", contents.as_ref()).unwrap();
185}
186
187#[test]
188fn test_openseadragon_javascript() {
189 let contents = r#"OpenSeadragon({
192 id: "example-inline-configuration-for-dzi",
193 prefixUrl: "/openseadragon/images/",
194 showNavigator: true,
195 tileSources: {
196 Image: {
197 xmlns: "http://schemas.microsoft.com/deepzoom/2008",
198 Url: "/example-images/highsmith/highsmith_files/",
199 Format: "jpg",
200 Overlap: "2",
201 TileSize: "256",
202 Size: {
203 Height: "9221",
204 Width: "7026"
205 }
206 }
207 }
208 });
209 "#;
210 let level =
211 &mut load_from_properties("http://test.com/x/test.xml", contents.as_ref()).unwrap()[0];
212 assert_eq!(Some(Vec2d { y: 9221, x: 7026 }), level.size_hint());
213 let tiles: Vec<String> = level.next_tiles(None).into_iter().map(|t| t.url).collect();
214 assert_eq!(tiles[0], "http://test.com/example-images/highsmith/highsmith_files/14/0_0.jpg");
215}