Skip to content

Commit

Permalink
Add support for DZI (Microsoft Deep Zoom)
Browse files Browse the repository at this point in the history
  • Loading branch information
lovasoa committed Aug 24, 2019
1 parent 8031355 commit a5f372b
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 2 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dezoomify-rs"
version = "1.4.0"
version = "1.5.0"
authors = ["Ophir LOJKINE <[email protected]>"]
edition = "2018"
license-file = "LICENSE"
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ because of memory constraints.

The following dezoomers are currently available:
- [**zoomify**](#zoomify) supports the popular zoomable image format *Zoomify*.
- [**deepzoom**](#DeepZoom) supports Microsoft's *DZI* format (Deep Zoom Image),
that is often used with the seadragon viewer.
- [**IIIF**](#IIIF) supports the widely used International Image Interoperability Framework format.
- [**Google Arts & Culture**](#google-arts-culture) supports downloading images from
[artsandculture.google.com](https://artsandculture.google.com/);
Expand Down Expand Up @@ -55,6 +57,15 @@ The IIIF dezoomer takes the URL of an
[`info.json`](https://iiif.io/api/image/2.1/#image-information) file as input.
You can find this url in your browser's network inspector when loading the image.

### DeepZoom

The DeepZoom dezoomer takes the URL of a `dzi` file as input.
You can find this url in your browser's network inspector when loading the image.
If the image tile URLs have the form
`http://test.com/y/xy_files/1/2_3.jpg`,
then the URL to enter is
`http://test.com/y/xy.dzi`.

### Generic

You can use this dezoomer if you know the format of tile URLs.
Expand Down
1 change: 1 addition & 0 deletions src/auto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub fn all_dezoomers(include_generic: bool) -> Vec<Box<dyn Dezoomer>> {
Box::new(crate::google_arts_and_culture::GAPDezoomer::default()),
Box::new(crate::zoomify::ZoomifyDezoomer::default()),
Box::new(crate::iiif::IIIF::default()),
Box::new(crate::dzi::DziDezoomer::default()),
Box::new(crate::generic::GenericDezoomer::default()),
];
if include_generic {
Expand Down
57 changes: 57 additions & 0 deletions src/dzi/dzi_file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use serde::Deserialize;

use crate::Vec2d;

use super::DziError;

#[derive(Debug, Deserialize, PartialEq)]
pub struct DziFile {
#[serde(rename = "Overlap", default)]
pub overlap: u32,
#[serde(rename = "TileSize", default)]
pub tile_size: u32,
#[serde(rename = "Format", default)]
pub format: String,
#[serde(rename = "Size", default)]
pub sizes: Vec<Size>,
}

impl DziFile {
pub fn get_size(&self) -> Result<Vec2d, DziError> {
self.sizes.iter().next()
.map(|s| Vec2d { x: s.width, y: s.height })
.ok_or(DziError::NoSize)
}
pub fn get_tile_size(&self) -> Vec2d {
Vec2d::square(self.tile_size)
}
pub fn max_level(&self) -> u32 {
let size = self.get_size().unwrap();
log2(size.x.max(size.y))
}
}

fn log2(n: u32) -> u32 {
32 - (n - 1).leading_zeros()
}

#[derive(Debug, Deserialize, PartialEq)]
pub struct Size {
#[serde(rename = "Width", default)]
pub width: u32,
#[serde(rename = "Height", default)]
pub height: u32,
}

#[test]
fn test_dzi() {
let dzi: DziFile = serde_xml_rs::from_str(r#"
<Image
Format="png" Overlap="2" TileSize="256">
<Size Height="3852" Width="5393"/>
</Image>"#
).unwrap();
assert_eq!(dzi.get_size().unwrap(), Vec2d { x: 5393, y: 3852 });
assert_eq!(dzi.get_tile_size(), Vec2d { x: 256, y: 256 });
assert_eq!(dzi.max_level(), 13);
}
130 changes: 130 additions & 0 deletions src/dzi/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use std::sync::Arc;

use custom_error::custom_error;
use dzi_file::DziFile;

use crate::dezoomer::*;

mod dzi_file;

#[derive(Default)]
pub struct DziDezoomer;

impl Dezoomer for DziDezoomer {
fn name(&self) -> &'static str {
"deepzoom"
}

fn zoom_levels(&mut self, data: &DezoomerInput) -> Result<ZoomLevels, DezoomerError> {
let DezoomerInputWithContents { uri, contents } = data.with_contents()?;
let levels = load_from_properties(uri, contents)?;
Ok(levels)
}
}

custom_error! {pub DziError
XmlError{source: serde_xml_rs::Error} = "Unable to parse the dzi file: {source}",
NoSize = "Expected a size in the DZI file",
InvalidTileSize = "Invalid tile size",
}

impl From<DziError> for DezoomerError {
fn from(err: DziError) -> Self {
DezoomerError::Other { source: err.into() }
}
}

fn load_from_properties(url: &str, contents: &[u8]) -> Result<ZoomLevels, DziError> {
let image_properties: DziFile = serde_xml_rs::from_reader(contents)?;

if image_properties.tile_size == 0 {
return Err(DziError::InvalidTileSize);
}

let dot_pos = url.rfind('.').unwrap_or(url.len() - 1);
let base_url = &Arc::new(format!("{}_files", &url[0..dot_pos]));

let size = image_properties.get_size()?;
let max_level = image_properties.max_level();
let levels = std::iter::successors(
Some(size),
|&size| {
if size.x > 1 || size.y > 1 {
Some(size.ceil_div(Vec2d::square(2)))
} else {
None
}
},
).enumerate()
.map(|(level_num, size)| {
DziLevel {
base_url: Arc::clone(base_url),
size,
tile_size: image_properties.get_tile_size(),
format: image_properties.format.clone(),
level: max_level - level_num as u32,
}
})
.into_zoom_levels();
Ok(levels)
}

struct DziLevel {
base_url: Arc<String>,
size: Vec2d,
tile_size: Vec2d,
format: String,
level: u32,
}

impl TilesRect for DziLevel {
fn size(&self) -> Vec2d {
self.size
}

fn tile_size(&self) -> Vec2d {
self.tile_size
}

fn tile_url(&self, pos: Vec2d) -> String {
format!(
"{base}/{level}/{x}_{y}.{format}",
base = self.base_url,
level = self.level,
x = pos.x,
y = pos.y,
format = self.format
)
}
}

impl std::fmt::Debug for DziLevel {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Deep Zoom Image")
}
}

#[test]
fn test_panorama() {
let url = "http://x.fr/y/test.dzi";
let contents = br#"
<Image
TileSize="256"
Overlap="2"
Format="jpg"
>
<Size Width="600" Height="300"/>
<DisplayRects></DisplayRects>
</Image>"#;
let mut props = load_from_properties(url, contents).unwrap();
assert_eq!(props.len(), 11);
let level = &mut props[1];
let tiles: Vec<String> = level.next_tiles(None).into_iter().map(|t| t.url).collect();
assert_eq!(
tiles,
vec![
"http://x.fr/y/test_files/9/0_0.jpg",
"http://x.fr/y/test_files/9/1_0.jpg"
]
);
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ mod generic;
mod google_arts_and_culture;
mod iiif;
mod zoomify;
mod dzi;

#[derive(StructOpt, Debug)]
struct Arguments {
Expand Down
3 changes: 3 additions & 0 deletions src/vec2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ pub struct Vec2d {
}

impl Vec2d {
pub fn square(size: u32) -> Vec2d {
Vec2d { x: size, y: size }
}
pub fn max(self, other: Vec2d) -> Vec2d {
Vec2d {
x: self.x.max(other.x),
Expand Down

0 comments on commit a5f372b

Please sign in to comment.