From 6d005d39a29225ddb76749a95e0ddc1e555b1abe Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Tue, 13 Aug 2019 22:41:33 +0200 Subject: [PATCH] Add preliminary support for the IIIF format --- Cargo.lock | 3 +- Cargo.toml | 5 +- README.md | 7 ++ src/auto.rs | 1 + src/dezoomer.rs | 77 +--------------- src/google_arts_and_culture/mod.rs | 3 +- src/iiif/mod.rs | 135 +++++++++++++++++++++++++++++ src/iiif/tile_info.rs | 51 +++++++++++ src/main.rs | 10 ++- src/vec2d.rs | 99 +++++++++++++++++++++ 10 files changed, 309 insertions(+), 82 deletions(-) create mode 100644 src/iiif/mod.rs create mode 100644 src/iiif/tile_info.rs create mode 100644 src/vec2d.rs diff --git a/Cargo.lock b/Cargo.lock index 74ac391..7efa43a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -375,7 +375,7 @@ dependencies = [ [[package]] name = "dezoomify-rs" -version = "1.1.2" +version = "1.2.0" dependencies = [ "aes 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -392,6 +392,7 @@ dependencies = [ "reqwest 0.9.14 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)", "serde-xml-rs 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", "serde_yaml 0.8.9 (registry+https://github.com/rust-lang/crates.io-index)", "sha-1 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.2.15 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index b36b07e..8759109 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dezoomify-rs" -version = "1.1.3" +version = "1.2.0" authors = ["Ophir LOJKINE "] edition = "2018" license-file = "LICENSE" @@ -16,11 +16,12 @@ lazy_static = "1.3" itertools = "0.8" serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.8" +serde-xml-rs = "0.3" +serde_json = "1.0" rayon = "1.1" block-modes = "0.3" aes = "0.3" hmac = "0.7" sha-1 = "0.8" base64 = "0.10" -serde-xml-rs = "0.3" indicatif = "0.11" \ No newline at end of file diff --git a/README.md b/README.md index 016cae8..167935c 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ because of memory constraints. The following dezoomers are currently available: - [**zoomify**](#zoomify) supports the popular zoomable image format *Zoomify*. + - [**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/); - [**custom**](#Custom) for advanced users. @@ -83,6 +84,12 @@ If the image tile URLs have the form then the URL to enter is `http://example.com/path/to/ImageProperties.xml`. +### IIIF + +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. + ## Command-line options When using dezoomify-rs from the command-line diff --git a/src/auto.rs b/src/auto.rs index a50f180..a50b9ef 100644 --- a/src/auto.rs +++ b/src/auto.rs @@ -5,6 +5,7 @@ pub fn all_dezoomers(include_generic: bool) -> Vec> { Box::new(crate::custom_yaml::CustomDezoomer::default()), Box::new(crate::google_arts_and_culture::GAPDezoomer::default()), Box::new(crate::zoomify::ZoomifyDezoomer::default()), + Box::new(crate::iiif::IIIF::default()), ]; if include_generic { dezoomers.push(Box::new(AutoDezoomer::default())) diff --git a/src/dezoomer.rs b/src/dezoomer.rs index 534a217..d1681ab 100644 --- a/src/dezoomer.rs +++ b/src/dezoomer.rs @@ -1,11 +1,11 @@ use std::collections::HashMap; use std::error::Error; use std::fmt::Debug; -use std::ops::{Add, Div, Mul, Sub}; use std::str::FromStr; use custom_error::custom_error; +pub use super::Vec2d; use super::ZoomError; pub struct DezoomerInput { @@ -160,80 +160,9 @@ impl TileProvider for T { } } -#[derive(Debug, PartialEq, Default, Clone, Copy)] -pub struct Vec2d { - pub x: u32, - pub y: u32, -} - -impl Vec2d { - pub fn max(self, other: Vec2d) -> Vec2d { - Vec2d { - x: self.x.max(other.x), - y: self.y.max(other.y), - } - } - pub fn min(self, other: Vec2d) -> Vec2d { - Vec2d { - x: self.x.min(other.x), - y: self.y.min(other.y), - } - } - pub fn ceil_div(self, other: Vec2d) -> Vec2d { - let x = self.x / other.x + if self.x % other.x == 0 { 0 } else { 1 }; - let y = self.y / other.y + if self.y % other.y == 0 { 0 } else { 1 }; - Vec2d { x, y } - } -} -impl std::fmt::Display for Vec2d { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "x={} y={}", self.x, self.y) - } -} - -impl Add for Vec2d { - type Output = Vec2d; - - fn add(self, rhs: Vec2d) -> Self::Output { - Vec2d { - x: self.x + rhs.x, - y: self.y + rhs.y, - } - } -} - -impl Sub for Vec2d { - type Output = Vec2d; - - fn sub(self, rhs: Vec2d) -> Self::Output { - Vec2d { - x: self.x - rhs.x, - y: self.y - rhs.y, - } - } -} - -impl Mul for Vec2d { - type Output = Vec2d; - - fn mul(self, rhs: Vec2d) -> Self::Output { - Vec2d { - x: self.x * rhs.x, - y: self.y * rhs.y, - } - } -} - -impl Div for Vec2d { - type Output = Vec2d; - - fn div(self, rhs: Vec2d) -> Self::Output { - Vec2d { - x: self.x / rhs.x, - y: self.y / rhs.y, - } - } +pub fn max_size_in_rect(position: Vec2d, tile_size: Vec2d, canvas_size: Vec2d) -> Vec2d { + (position + tile_size).min(canvas_size) - position } #[derive(Debug, PartialEq, Clone)] diff --git a/src/google_arts_and_culture/mod.rs b/src/google_arts_and_culture/mod.rs index 9c89117..30f0b2a 100644 --- a/src/google_arts_and_culture/mod.rs +++ b/src/google_arts_and_culture/mod.rs @@ -1,8 +1,9 @@ use std::error::Error; use std::sync::Arc; +use tile_info::{PageInfo, TileInfo}; + use crate::dezoomer::*; -use crate::google_arts_and_culture::tile_info::{PageInfo, TileInfo}; mod decryption; mod tile_info; diff --git a/src/iiif/mod.rs b/src/iiif/mod.rs new file mode 100644 index 0000000..4a15286 --- /dev/null +++ b/src/iiif/mod.rs @@ -0,0 +1,135 @@ +use std::sync::Arc; + +use custom_error::custom_error; +use tile_info::ImageInfo; + +use crate::dezoomer::*; + +mod tile_info; + +#[derive(Default)] +pub struct IIIF; + +custom_error! {pub IIIFError + JsonError{source: serde_json::Error} = "Invalid IIIF info.json file: {source}" +} + +impl From for DezoomerError { + fn from(err: IIIFError) -> Self { + DezoomerError::Other { source: err.into() } + } +} + +impl Dezoomer for IIIF { + fn name(&self) -> &'static str { + "iiif" + } + + fn zoom_levels(&mut self, data: &DezoomerInput) -> Result { + self.assert(data.uri.ends_with("/info.json"))?; + let contents = data.with_contents()?.contents; + Ok(zoom_levels(contents)?) + } +} + +fn zoom_levels(raw_info: &[u8]) -> Result { + let image_info: ImageInfo = serde_json::from_slice(raw_info)?; + let img = Arc::new(image_info); + let default_tiles = vec![Default::default()]; + let tiles = img.tiles.as_ref().unwrap_or(&default_tiles); + let levels = tiles + .iter() + .flat_map(|tile_info| { + let tile_size = Vec2d { + x: tile_info.width, + y: tile_info.height.unwrap_or(tile_info.width), + }; + let page_info = &img; // Required to allow the move + tile_info + .scale_factors + .iter() + .map(move |&scale_factor| IIIFZoomLevel { + scale_factor, + tile_size, + page_info: Arc::clone(page_info), + }) + }) + .into_zoom_levels(); + Ok(levels) +} + +struct IIIFZoomLevel { + scale_factor: u32, + tile_size: Vec2d, + page_info: Arc, +} + +impl TilesRect for IIIFZoomLevel { + fn size(&self) -> Vec2d { + Vec2d { + x: self.page_info.width / self.scale_factor, + y: self.page_info.height / self.scale_factor, + } + } + + fn tile_size(&self) -> Vec2d { + self.tile_size + } + + fn tile_url(&self, col_and_row_pos: Vec2d) -> String { + let scaled_tile_size = self.tile_size * self.scale_factor; + let xy_pos = col_and_row_pos * scaled_tile_size; + let scaled_tile_size = max_size_in_rect(xy_pos, scaled_tile_size, self.size() * self.scale_factor); + let tile_size = scaled_tile_size / self.scale_factor; + format!( + "{base}/{x},{y},{img_w},{img_h}/{tile_w},{tile_h}/{rotation}/{quality}.{format}", + base = self.page_info.id, + x = xy_pos.x, + y = xy_pos.y, + img_w = scaled_tile_size.x, + img_h = scaled_tile_size.y, + tile_w = tile_size.x, + tile_h = tile_size.y, + rotation = 0, + quality = "default", + format = "jpg" + ) + } +} + +impl std::fmt::Debug for IIIFZoomLevel { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "IIIF image with {}x{} tiles", + self.tile_size.x, self.tile_size.y + ) + } +} + +#[test] +fn test_tiles() { + let data = br#"{ + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "http://www.asmilano.it/fast/iipsrv.fcgi?IIIF=/opt/divenire/files/./tifs/05/36/536765.tif", + "protocol" : "http://iiif.io/api/image", + "width" : 15001, + "height" : 48002, + "tiles" : [ + { "width" : 512, "height" : 512, "scaleFactors" : [ 1, 2, 4, 8, 16, 32, 64, 128 ] } + ], + "profile" : [ + "http://iiif.io/api/image/2/level1.json", + { "formats" : [ "jpg" ], + "qualities" : [ "native","color","gray" ], + "supports" : ["regionByPct","sizeByForcedWh","sizeByWh","sizeAboveFull","rotationBy90s","mirroring","gray"] } + ] + }"#; + let levels = zoom_levels(data).unwrap(); + let tiles: Vec = levels[6].tiles().into_iter() + .map(|t| t.unwrap().url) + .collect(); + assert_eq!(tiles, vec![ + "", + ]) +} \ No newline at end of file diff --git a/src/iiif/tile_info.rs b/src/iiif/tile_info.rs new file mode 100644 index 0000000..ed4b3c4 --- /dev/null +++ b/src/iiif/tile_info.rs @@ -0,0 +1,51 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize, PartialEq)] +pub struct ImageInfo { + #[serde(rename = "@id")] + pub id: String, + pub width: u32, + pub height: u32, + pub tiles: Option>, +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct TileInfo { + pub width: u32, + pub height: Option, + #[serde(rename = "scaleFactors")] + pub scale_factors: Vec, +} + +impl Default for TileInfo { + fn default() -> Self { + TileInfo { + width: 512, + height: None, + scale_factors: vec![1], + } + } +} + +#[test] +fn test_deserialisation() { + let _: ImageInfo = serde_json::from_str( + r#"{ + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "protocol" : "http://iiif.io/api/image", + "width" : 6000, + "height" : 4000, + "sizes" : [ + {"width" : 150, "height" : 100}, + {"width" : 600, "height" : 400}, + {"width" : 3000, "height": 2000} + ], + "tiles": [ + {"width" : 512, "scaleFactors" : [1,2,4,8,16]} + ], + "profile" : [ "http://iiif.io/api/image/2/level2.json" ] + }"#, + ) + .unwrap(); +} diff --git a/src/main.rs b/src/main.rs index 9d42bbc..7d9eb58 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,14 +13,16 @@ use structopt::StructOpt; use custom_error::custom_error; use dezoomer::{Dezoomer, DezoomerError, DezoomerInput, ZoomLevels}; use dezoomer::TileReference; -use dezoomer::Vec2d; +pub use vec2d::Vec2d; -use crate::dezoomer::ZoomLevel; +use crate::dezoomer::{max_size_in_rect, ZoomLevel}; +mod vec2d; +mod auto; mod custom_yaml; mod dezoomer; -mod auto; mod google_arts_and_culture; +mod iiif; mod zoomify; #[derive(StructOpt, Debug)] @@ -343,7 +345,7 @@ impl Canvas { } fn add_tile(self: &mut Self, tile: &Tile) -> Result<(), ZoomError> { let Vec2d { x: xmax, y: ymax } = - (tile.position + tile.size()).min(self.size()) - tile.position; + max_size_in_rect(tile.position, tile.size(), self.size()); let sub_tile = tile.image.view(0, 0, xmax, ymax); let Vec2d { x, y } = tile.position; let success = self.image.copy_from(&sub_tile, x, y); diff --git a/src/vec2d.rs b/src/vec2d.rs new file mode 100644 index 0000000..76fd964 --- /dev/null +++ b/src/vec2d.rs @@ -0,0 +1,99 @@ +use std::ops::{Add, Div, Mul, Sub}; + +#[derive(Debug, PartialEq, Default, Clone, Copy)] +pub struct Vec2d { + pub x: u32, + pub y: u32, +} + +impl Vec2d { + pub fn max(self, other: Vec2d) -> Vec2d { + Vec2d { + x: self.x.max(other.x), + y: self.y.max(other.y), + } + } + pub fn min(self, other: Vec2d) -> Vec2d { + Vec2d { + x: self.x.min(other.x), + y: self.y.min(other.y), + } + } + pub fn ceil_div(self, other: Vec2d) -> Vec2d { + let x = self.x / other.x + if self.x % other.x == 0 { 0 } else { 1 }; + let y = self.y / other.y + if self.y % other.y == 0 { 0 } else { 1 }; + Vec2d { x, y } + } +} + +impl std::fmt::Display for Vec2d { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "x={} y={}", self.x, self.y) + } +} + +impl Add for Vec2d { + type Output = Vec2d; + + fn add(self, rhs: Vec2d) -> Self::Output { + Vec2d { + x: self.x + rhs.x, + y: self.y + rhs.y, + } + } +} + +impl Sub for Vec2d { + type Output = Vec2d; + + fn sub(self, rhs: Vec2d) -> Self::Output { + Vec2d { + x: self.x.saturating_sub(rhs.x), + y: self.y.saturating_sub(rhs.y), + } + } +} + +impl Mul for Vec2d { + type Output = Vec2d; + + fn mul(self, rhs: Vec2d) -> Self::Output { + Vec2d { + x: self.x * rhs.x, + y: self.y * rhs.y, + } + } +} + +impl Mul for Vec2d { + type Output = Vec2d; + + fn mul(self, rhs: u32) -> Self::Output { + Vec2d { + x: self.x * rhs, + y: self.y * rhs, + } + } +} + +impl Div for Vec2d { + type Output = Vec2d; + + fn div(self, rhs: Vec2d) -> Self::Output { + Vec2d { + x: self.x / rhs.x, + y: self.y / rhs.y, + } + } +} + +impl Div for Vec2d { + type Output = Vec2d; + + fn div(self, rhs: u32) -> Self::Output { + Vec2d { + x: self.x / rhs, + y: self.y / rhs, + } + } +} \ No newline at end of file