use std::path::Path;
use naut_core::image;
use crate::errors::{FormatError, SicIoError};
pub trait EncodingFormatByExtension {
fn by_extension<P: AsRef<Path>>(&self, path: P)
-> Result<image::ImageOutputFormat, SicIoError>;
}
pub trait EncodingFormatByIdentifier {
fn by_identifier(&self, identifier: &str) -> Result<image::ImageOutputFormat, SicIoError>;
}
pub trait EncodingFormatJPEGQuality {
fn jpeg_quality(&self) -> Result<JPEGQuality, SicIoError>;
}
pub trait EncodingFormatPNMSampleEncoding {
fn pnm_encoding_type(&self) -> Result<image::pnm::SampleEncoding, SicIoError>;
}
#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)]
pub struct JPEGQuality {
quality: u8,
}
impl Default for JPEGQuality {
fn default() -> Self {
Self { quality: 80 }
}
}
impl JPEGQuality {
pub fn try_from(quality: u8) -> Result<Self, SicIoError> {
if (1u8..=100u8).contains(&quality) {
Ok(JPEGQuality { quality })
} else {
Err(SicIoError::FormatError(
FormatError::JPEGQualityLevelNotInRange,
))
}
}
pub fn as_u8(self) -> u8 {
self.quality
}
}
impl EncodingFormatByExtension for DetermineEncodingFormat {
fn by_extension<P: AsRef<Path>>(
&self,
path: P,
) -> Result<image::ImageOutputFormat, SicIoError> {
let extension = path.as_ref().extension().and_then(|v| v.to_str());
match extension {
Some(some) => self.by_identifier(some),
None => Err(SicIoError::UnableToDetermineImageFormatFromFileExtension(
path.as_ref().to_path_buf(),
)),
}
}
}
impl EncodingFormatByIdentifier for DetermineEncodingFormat {
fn by_identifier(&self, identifier: &str) -> Result<image::ImageOutputFormat, SicIoError> {
match identifier.to_ascii_lowercase().as_str() {
"avif" => {
std::env::set_var("SIC_AVIF_HACK", "1");
Ok(image::ImageOutputFormat::Farbfeld)
}
"bmp" => Ok(image::ImageOutputFormat::Bmp),
"farbfeld" => Ok(image::ImageOutputFormat::Farbfeld),
"gif" => Ok(image::ImageOutputFormat::Gif),
"ico" => Ok(image::ImageOutputFormat::Ico),
"jpeg" | "jpg" => Ok(image::ImageOutputFormat::Jpeg(self.jpeg_quality()?.as_u8())),
"pam" => Ok(image::ImageOutputFormat::Pnm(
image::pnm::PNMSubtype::ArbitraryMap,
)),
"pbm" => Ok(image::ImageOutputFormat::Pnm(
image::pnm::PNMSubtype::Bitmap(self.pnm_encoding_type()?),
)),
"pgm" => Ok(image::ImageOutputFormat::Pnm(
image::pnm::PNMSubtype::Graymap(self.pnm_encoding_type()?),
)),
"png" => Ok(image::ImageOutputFormat::Png),
"ppm" => Ok(image::ImageOutputFormat::Pnm(
image::pnm::PNMSubtype::Pixmap(self.pnm_encoding_type()?),
)),
"tga" => Ok(image::ImageOutputFormat::Tga),
_ => Err(SicIoError::UnknownImageIdentifier(identifier.to_string())),
}
}
}
pub struct DetermineEncodingFormat {
pub pnm_sample_encoding: Option<image::pnm::SampleEncoding>,
pub jpeg_quality: Option<JPEGQuality>,
}
impl Default for DetermineEncodingFormat {
fn default() -> Self {
Self {
pnm_sample_encoding: Some(image::pnm::SampleEncoding::Binary),
jpeg_quality: Some(Default::default()),
}
}
}
impl EncodingFormatPNMSampleEncoding for DetermineEncodingFormat {
fn pnm_encoding_type(&self) -> Result<image::pnm::SampleEncoding, SicIoError> {
self.pnm_sample_encoding
.ok_or_else(|| SicIoError::FormatError(FormatError::PNMSamplingEncodingNotSet))
}
}
impl EncodingFormatJPEGQuality for DetermineEncodingFormat {
fn jpeg_quality(&self) -> Result<JPEGQuality, SicIoError> {
self.jpeg_quality
.ok_or_else(|| SicIoError::FormatError(FormatError::JPEGQualityLevelNotSet))
}
}
#[cfg(test)]
mod tests {
use super::*;
const INPUT_FORMATS: &[&str] = &[
"bmp", "farbfeld", "gif", "ico", "jpg", "jpeg", "png", "pbm", "pgm", "ppm", "pam", "tga",
];
const EXPECTED_VALUES: &[image::ImageOutputFormat] = &[
image::ImageOutputFormat::Bmp,
image::ImageOutputFormat::Farbfeld,
image::ImageOutputFormat::Gif,
image::ImageOutputFormat::Ico,
image::ImageOutputFormat::Jpeg(80),
image::ImageOutputFormat::Jpeg(80),
image::ImageOutputFormat::Png,
image::ImageOutputFormat::Pnm(image::pnm::PNMSubtype::Bitmap(
image::pnm::SampleEncoding::Binary,
)),
image::ImageOutputFormat::Pnm(image::pnm::PNMSubtype::Graymap(
image::pnm::SampleEncoding::Binary,
)),
image::ImageOutputFormat::Pnm(image::pnm::PNMSubtype::Pixmap(
image::pnm::SampleEncoding::Binary,
)),
image::ImageOutputFormat::Pnm(image::pnm::PNMSubtype::ArbitraryMap),
image::ImageOutputFormat::Tga,
];
fn setup_default_format_determiner() -> DetermineEncodingFormat {
DetermineEncodingFormat {
pnm_sample_encoding: Some(image::pnm::SampleEncoding::Binary),
jpeg_quality: Some(JPEGQuality::try_from(80).unwrap()),
}
}
fn test_with_extensions(ext: &str, expected: &image::ImageOutputFormat) {
let path = format!("w_ext.{}", ext);
let format_determiner = setup_default_format_determiner();
let result = format_determiner.by_extension(path.as_str());
assert_eq!(result.unwrap(), *expected);
}
#[test]
fn extension_with_defaults() {
let zipped = INPUT_FORMATS.iter().zip(EXPECTED_VALUES.iter());
for (ext, exp) in zipped {
test_with_extensions(ext, exp);
}
}
#[test]
#[should_panic]
fn extension_unknown_extension() {
let path = "w_ext.h";
let format_determiner = setup_default_format_determiner();
let result = format_determiner.by_extension(path);
result.unwrap();
}
#[test]
#[should_panic]
fn extension_no_extension() {
let path = "png";
let format_determiner = setup_default_format_determiner();
let result = format_determiner.by_extension(path);
result.unwrap();
}
#[test]
#[should_panic]
fn extension_invalid_extension() {
let path = ".png";
let format_determiner = setup_default_format_determiner();
let result = format_determiner.by_extension(path);
result.unwrap();
}
fn test_with_identifier(identifier: &str, expected: &image::ImageOutputFormat) {
let format_determiner = setup_default_format_determiner();
let result = format_determiner.by_identifier(identifier);
assert_eq!(result.unwrap(), *expected);
}
#[test]
fn identifier_with_defaults() {
let zipped = INPUT_FORMATS.iter().zip(EXPECTED_VALUES.iter());
for (id, exp) in zipped {
test_with_identifier(id, exp);
}
}
#[test]
fn uppercase_formats() {
let uppercase_formats = INPUT_FORMATS
.iter()
.map(|v| v.to_ascii_uppercase())
.zip(EXPECTED_VALUES.iter());
for (id, exp) in uppercase_formats {
test_with_identifier(id.as_str(), exp);
}
}
#[test]
#[should_panic]
fn identifier_unknown_identifier() {
let format_determiner = setup_default_format_determiner();
let result = format_determiner.by_identifier("");
result.unwrap();
}
#[test]
fn identifier_custom_pnm_sample_encoding_ascii_pbm() {
let format_determiner = DetermineEncodingFormat {
pnm_sample_encoding: Some(image::pnm::SampleEncoding::Ascii),
jpeg_quality: None,
};
let result = format_determiner.by_identifier("pbm").unwrap();
let expected = image::ImageOutputFormat::Pnm(image::pnm::PNMSubtype::Bitmap(
image::pnm::SampleEncoding::Ascii,
));
assert_eq!(result, expected);
}
#[test]
fn identifier_custom_pnm_sample_encoding_ascii_pgm() {
let format_determiner = DetermineEncodingFormat {
pnm_sample_encoding: Some(image::pnm::SampleEncoding::Ascii),
jpeg_quality: None,
};
let result = format_determiner.by_identifier("pgm").unwrap();
let expected = image::ImageOutputFormat::Pnm(image::pnm::PNMSubtype::Graymap(
image::pnm::SampleEncoding::Ascii,
));
assert_eq!(result, expected);
}
#[test]
fn identifier_custom_pnm_sample_encoding_ascii_ppm() {
let format_determiner = DetermineEncodingFormat {
pnm_sample_encoding: Some(image::pnm::SampleEncoding::Ascii),
jpeg_quality: None,
};
let result = format_determiner.by_identifier("ppm").unwrap();
let expected = image::ImageOutputFormat::Pnm(image::pnm::PNMSubtype::Pixmap(
image::pnm::SampleEncoding::Ascii,
));
assert_eq!(result, expected);
}
#[test]
fn identifier_custom_jpeg_quality_in_range_lower() {
let format_determiner = DetermineEncodingFormat {
pnm_sample_encoding: None,
jpeg_quality: Some(JPEGQuality::try_from(1).unwrap()),
};
let result = format_determiner.by_identifier("jpg").unwrap();
let expected = image::ImageOutputFormat::Jpeg(1);
assert_eq!(result, expected);
}
#[test]
fn identifier_custom_jpeg_quality_in_range_upper() {
let format_determiner = DetermineEncodingFormat {
pnm_sample_encoding: None,
jpeg_quality: Some(JPEGQuality::try_from(100).unwrap()),
};
let result = format_determiner.by_identifier("jpg").unwrap();
let expected = image::ImageOutputFormat::Jpeg(100);
assert_eq!(result, expected);
}
#[test]
fn jpeg_quality_in_range_lower() {
let result = JPEGQuality::try_from(1).unwrap();
let expected = JPEGQuality { quality: 1 };
assert_eq!(result, expected);
}
#[test]
fn jpeg_quality_in_range_upper() {
let result = JPEGQuality::try_from(100).unwrap();
let expected = JPEGQuality { quality: 100 };
assert_eq!(result, expected);
}
#[test]
#[should_panic]
fn jpeg_quality_out_range_lower() {
let result = JPEGQuality::try_from(0).unwrap();
let expected = JPEGQuality { quality: 0 };
assert_eq!(result, expected);
}
#[test]
#[should_panic]
fn jpeg_quality_out_range_upper() {
let result = JPEGQuality::try_from(101).unwrap();
let expected = JPEGQuality { quality: 101 };
assert_eq!(result, expected);
}
#[test]
#[should_panic]
fn identifier_requires_pnm_sample_encoding_to_be_set_pbm() {
let format_determiner = DetermineEncodingFormat {
pnm_sample_encoding: None,
jpeg_quality: None,
};
format_determiner.by_identifier("pbm").unwrap();
}
#[test]
#[should_panic]
fn identifier_requires_pnm_sample_encoding_to_be_set_pgm() {
let format_determiner = DetermineEncodingFormat {
pnm_sample_encoding: None,
jpeg_quality: None,
};
format_determiner.by_identifier("pgm").unwrap();
}
#[test]
#[should_panic]
fn identifier_requires_pnm_sample_encoding_to_be_set_ppm() {
let format_determiner = DetermineEncodingFormat {
pnm_sample_encoding: None,
jpeg_quality: None,
};
format_determiner.by_identifier("ppm").unwrap();
}
#[test]
#[should_panic]
fn identifier_requires_pnm_sample_encoding_to_be_set_jpg() {
let format_determiner = DetermineEncodingFormat {
pnm_sample_encoding: None,
jpeg_quality: None,
};
format_determiner.by_identifier("jpg").unwrap();
}
}