use crate::exif::{PanoExif, calc_cylindrical_pano_angles, exif_to_partial_pano_config};
use crate::{PanoAngles, Projection, TilerError};
use exif::Exif;
use std::io::Cursor;
use std::path::Path;
use xmpkit::XmpMeta;
fn get_exif_metadata(bytes: &[u8]) -> Option<Exif> {
let mut cursor = Cursor::new(bytes);
let exif_reader = exif::Reader::new();
exif_reader.read_from_container(&mut cursor).ok()
}
fn get_xmp_metadata(bytes: &[u8]) -> Option<XmpMeta> {
let mut xmp_file = xmpkit::XmpFile::new();
xmp_file
.from_bytes(bytes)
.ok()
.and_then(|()| xmp_file.get_xmp())
.cloned()
}
fn get_dimensions(bytes: &[u8]) -> Result<(u32, u32), TilerError> {
let reader = image::ImageReader::new(Cursor::new(bytes)).with_guessed_format()?;
Ok(reader.into_dimensions()?)
}
pub fn guess_pano_angles(file: &Path) -> Result<PanoAngles, TilerError> {
let bytes = std::fs::read(file)?;
guess_pano_angles_from_bytes(&bytes)
}
pub fn guess_pano_angles_from_bytes(bytes: &[u8]) -> Result<PanoAngles, TilerError> {
let xmp_metadata = get_xmp_metadata(bytes);
let north_offset = if let Some(meta) = &xmp_metadata {
let gpano_ns = "http://ns.google.com/photos/1.0/panorama/";
let get_gpano_f64 = |name: &str| -> Option<f64> {
meta.get_property(gpano_ns, name)
.and_then(|v| v.to_string().parse::<f64>().ok())
};
let projection_type = meta
.get_property(gpano_ns, "ProjectionType")
.map(|v| v.to_string());
let cropped_area_height = get_gpano_f64("CroppedAreaImageHeightPixels");
let cropped_area_width = get_gpano_f64("CroppedAreaImageWidthPixels");
let full_pano_height = get_gpano_f64("FullPanoHeightPixels");
let full_pano_width = get_gpano_f64("FullPanoWidthPixels");
let cropped_area_top = get_gpano_f64("CroppedAreaTopPixels");
let pose_heading = get_gpano_f64("PoseHeadingDegrees");
if let (Some(cropped_w), Some(cropped_h), Some(full_w), Some(full_h), Some(cropped_t)) = (
cropped_area_width,
cropped_area_height,
full_pano_width,
full_pano_height,
cropped_area_top,
) {
let exif_info = PanoExif {
full_pano_width_pixels: full_w as u32,
full_pano_height_pixels: full_h as u32,
cropped_area_top_pixels: cropped_t as u32,
cropped_area_image_width_pixels: cropped_w as u32,
cropped_area_image_height_pixels: cropped_h as u32,
projection_type,
pose_heading_degrees: pose_heading,
};
return Ok(exif_to_partial_pano_config(&exif_info));
}
pose_heading
} else {
None
};
let mut haov = None;
let mut vaov = None;
let mut projection = Projection::Equirectangular;
let exif_metadata = get_exif_metadata(bytes);
let focal_length_35mm = if let Some(exif) = &exif_metadata
&& let Some(field) = exif.get_field(exif::Tag::FocalLengthIn35mmFilm, exif::In::PRIMARY)
{
match &field.value {
exif::Value::Rational(rationals) => rationals.first().map(exif::Rational::to_f64),
_ => field.value.get_uint(0).map(f64::from),
}
} else {
None
};
let (width, height) = get_dimensions(bytes)?;
if let Some(focal) = focal_length_35mm
&& focal > 0.0
{
projection = Projection::Cylindrical;
let crop_factor = 0.90; if let Some(angles) = calc_cylindrical_pano_angles(focal, width, height, crop_factor) {
haov = Some(angles.haov);
vaov = Some(angles.vaov);
}
}
let aspect_ratio = f64::from(width) / f64::from(height);
if haov.is_none() && vaov.is_none() {
if (aspect_ratio - 2.0).abs() <= 0.1 {
projection = Projection::Equirectangular;
haov = Some(360.0);
vaov = Some(180.0);
} else if aspect_ratio >= 2.2 {
projection = Projection::Cylindrical;
let crop_factor = 0.90;
if let Some(angles) = calc_cylindrical_pano_angles(24.0, width, height, crop_factor) {
haov = Some(angles.haov);
vaov = Some(angles.vaov);
}
} else {
projection = Projection::Equirectangular;
haov = Some(360.0);
vaov = Some(180.0);
}
}
Ok(PanoAngles {
haov: haov.unwrap_or(360.0),
vaov: vaov.unwrap_or(180.0),
north_offset,
projection,
v_offset: 0.0,
horizon_pixels: 0,
})
}