dezoomify_rs/dzi/
mod.rs

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/// A dezoomer for Deep Zoom Images
15/// See https://docs.microsoft.com/en-us/previous-versions/windows/silverlight/dotnet-windows-silverlight/cc645043%28v%3dvs.95%29
16#[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    // Workaround for https://github.com/netvl/xml-rs/issues/155
53    // which the original author seems unwilling to fix
54    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    // See https://github.com/lovasoa/dezoomify-rs/issues/45
179    // Trying to parse a file with a byte order mark
180    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    // See https://github.com/lovasoa/dezoomify-rs/issues/45
190    // Trying to parse a file with a byte order mark
191    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}