use std::borrow::Cow;
use std::collections::HashSet;
use std::sync::LazyLock;
use log::debug;
use log::info;
use log::warn;
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::Vec2d;
use crate::network::resolve_relative;
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct ImageInfo {
#[serde(rename = "@context", skip_serializing_if = "Option::is_none")]
pub context: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
#[serde(alias = "@type")]
pub iiif_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub protocol: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile: Option<Profile>,
#[serde(rename = "@id", skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub width: u32,
pub height: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub qualities: Option<Vec<String>>,
#[serde(alias = "preferredFormats", skip_serializing_if = "Option::is_none")]
pub formats: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tiles: Option<Vec<TileInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scale_factors: Option<Vec<u32>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tile_width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tile_height: Option<u32>,
}
static QUALITY_ORDER: [&str; 5] = ["bitonal", "gray", "color", "native", "default"];
static FORMAT_ORDER: [&str; 7] = ["webp", "gif", "bmp", "tif", "jpg", "jpeg", "png"];
static TEST_ID_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^https?://((www\.)?example\.|localhost)").unwrap());
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum TileSizeFormat {
WidthHeight,
Width,
}
impl ImageInfo {
#[must_use]
pub fn size(&self) -> Vec2d {
Vec2d {
x: self.width,
y: self.height,
}
}
fn profile_info(&self) -> Cow<'_, ProfileInfo> {
self.profile
.as_ref()
.map(|p| p.profile_info())
.unwrap_or_default()
}
#[must_use]
pub fn best_quality(&self) -> String {
let pinfo = self.profile_info();
self.qualities
.iter()
.flat_map(|v| v.iter())
.chain(pinfo.qualities.iter().flat_map(|x| x.iter()))
.max_by_key(|&s| QUALITY_ORDER.iter().position(|&x| x == s))
.cloned()
.unwrap_or_else(|| {
debug!("No image quality specified. Using 'default'.");
"default".into()
})
}
#[must_use]
pub fn best_format(&self) -> String {
let pinfo = self.profile_info();
self.formats
.iter()
.flat_map(|v| v.iter())
.chain(pinfo.formats.iter().flat_map(|x| x.iter()))
.max_by_key(|&s| FORMAT_ORDER.iter().position(|&x| x == s))
.cloned()
.unwrap_or_else(|| {
debug!("No image format specified. Using 'jpg'.");
"jpg".into()
})
}
#[must_use]
pub fn preferred_size_format(&self) -> TileSizeFormat {
let pinfo = self.profile_info();
let s: HashSet<&str> = pinfo
.supports
.iter()
.flat_map(|x| x.iter())
.map(std::string::String::as_str)
.collect();
if s.contains("sizeByW") && !s.contains("sizeByWh") {
TileSizeFormat::Width
} else {
TileSizeFormat::WidthHeight
}
}
#[must_use]
pub fn tiles(&self) -> Vec<TileInfo> {
let profile_info = self.profile_info();
let mut tiles: Vec<_> = self
.tiles
.as_ref()
.map(|v| {
v.iter()
.filter_map(|info| {
if profile_info.tile_size_fits(info.size()) {
Some(info.clone())
} else {
None
}
})
.collect()
})
.unwrap_or_default();
if !tiles.iter().any(|t| t.scale_factors.contains(&1)) {
let mut info = TileInfo::default();
if let Some(width) = self.tile_width {
info.width = width;
}
if let Some(height) = self.tile_height {
info.height = Some(height);
}
let cropped_size = profile_info.crop_tile_size(info.size());
info.width = cropped_size.x;
info.height = Some(cropped_size.y);
if let Some(scale_factors) = &self.scale_factors {
info.scale_factors.clone_from(scale_factors);
}
tiles.push(info);
}
tiles
}
#[must_use]
pub fn has_distinctive_iiif_properties(&self) -> bool {
self.id.is_some()
|| self.protocol.is_some()
|| self.context.is_some()
|| self.tiles.is_some()
|| self.formats.is_some()
|| self
.iiif_type
.as_ref()
.as_ref()
.is_some_and(|&s| s == "iiif:ImageProfile" || s == "ImageService3")
}
pub fn remove_test_id(&mut self) {
if let Some(id) = &self.id
&& TEST_ID_RE.is_match(id)
{
info!("Removing probably invalid IIIF id '{id}'");
self.id = None;
}
}
pub fn resolve_relative_urls(&mut self, base: &str) {
if let Some(id) = &self.id {
self.id = Some(resolve_relative(base, id));
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct TileInfo {
pub width: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<u32>,
#[serde(rename = "scaleFactors")]
pub scale_factors: Vec<u32>,
}
impl TileInfo {
#[must_use]
pub fn size(&self) -> Vec2d {
Vec2d {
x: self.width,
y: self.height.unwrap_or(self.width),
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
#[serde(untagged)]
pub enum Profile {
Reference(String),
Info(ProfileInfo),
Multiple(Option<Vec<Profile>>),
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)]
pub struct ProfileInfo {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "extraFormats")]
formats: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "extraQualities")]
qualities: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "extraFeatures")]
supports: Option<Vec<String>>,
#[serde(rename = "maxWidth")]
max_width: Option<u32>,
#[serde(rename = "maxHeight")]
max_height: Option<u32>,
#[serde(rename = "maxArea")]
max_area: Option<u64>,
}
impl ProfileInfo {
fn crop_tile_size(&self, mut size: Vec2d) -> Vec2d {
if let Some(max_width) = self.max_width {
size.x = size.x.min(max_width);
let max_height = self.max_height.unwrap_or(max_width);
size.y = size.y.min(max_height);
}
if let Some(max_area) = self.max_area
&& size.area() > max_area
{
let sqrt = u32::try_from(max_area.isqrt())
.expect("the square root of a u64 always fits in a u32");
size.y = sqrt.min(size.y);
size.x = sqrt.min(size.x);
}
size
}
fn tile_size_fits(&self, size: Vec2d) -> bool {
self.crop_tile_size(size) == size
}
}
static PROFILE_REFERENCES: LazyLock<std::collections::HashMap<String, ProfileInfo>> =
LazyLock::new(|| {
let json_str = include_str!("./levels.json");
serde_json::from_str(json_str).unwrap()
});
fn update_max<T: Ord + Copy>(target: &mut Option<T>, new: Option<T>) {
if let Some(new) = new {
*target = Some(if let Some(old) = target {
new.min(*old)
} else {
new
});
}
}
impl Profile {
fn profile_info(&self) -> Cow<'_, ProfileInfo> {
match self {
Profile::Reference(s) => PROFILE_REFERENCES.get(s).map_or_else(
|| {
warn!("Unknown IIIF profile reference: {s}");
Cow::Owned(ProfileInfo::default())
},
Cow::Borrowed,
),
Profile::Info(info) => Cow::Borrowed(info),
Profile::Multiple(profiles) => {
let mut formats = vec![];
let mut qualities = vec![];
let mut supports = vec![];
let mut max_width = None;
let mut max_height = None;
let mut max_area = None;
for profile in profiles.iter().flat_map(|x| x.iter()) {
let p = profile.profile_info();
if let Some(x) = &p.formats {
formats.extend_from_slice(x);
}
if let Some(x) = &p.qualities {
qualities.extend_from_slice(x);
}
if let Some(x) = &p.supports {
supports.extend_from_slice(x);
}
update_max(&mut max_width, p.max_width);
update_max(&mut max_height, p.max_height);
update_max(&mut max_area, p.max_area);
}
Cow::Owned(ProfileInfo {
formats: Some(formats),
qualities: Some(qualities),
supports: Some(supports),
max_width,
max_height,
max_area,
})
}
}
}
}
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();
}
#[test]
fn test_profile_info() {
let profiles = Profile::Multiple(Some(vec![
Profile::Reference("http://iiif.io/api/image/2/level0.json".into()),
Profile::Info(ProfileInfo {
supports: Some(vec!["sizeByWh".into()]),
max_width: Some(56),
..Default::default()
}),
Profile::Info(ProfileInfo {
max_width: Some(78),
max_height: Some(94),
..Default::default()
}),
]));
assert_eq!(
*profiles.profile_info(),
ProfileInfo {
formats: Some(vec!["jpg".into()]), qualities: Some(vec!["default".into()]), supports: Some(vec![
"sizeByWhListed".into(), "sizeByWh".into(), ]),
max_width: Some(56),
max_height: Some(94),
..Default::default()
}
);
}
#[test]
fn test_best_quality() {
let pairs = vec![
(None, "default"),
(Some(vec![]), "default"),
(Some(vec!["color".into()]), "color"),
(Some(vec!["grey".into()]), "grey"),
(Some(vec!["zorglub".into()]), "zorglub"),
(Some(vec!["zorglub".into(), "color".into()]), "color"),
(Some(vec!["bitonal".into(), "gray".into()]), "gray"),
(
Some(vec!["bitonal".into(), "gray".into(), "color".into()]),
"color",
),
(
Some(vec![
"default".into(),
"bitonal".into(),
"gray".into(),
"color".into(),
]),
"default",
),
];
for (qualities, expected_best_quality) in pairs {
let info = ImageInfo {
qualities,
..ImageInfo::default()
};
assert_eq!(info.best_quality(), expected_best_quality);
}
}