use crate::prelude::*;
use std::{env, error::Error, io::Write, process::Command};
use tempfile::NamedTempFile;
pub fn path_to_resource(relative: impl AsRef<Path>) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(relative)
}
pub fn is_imagemagick_installed() -> bool {
if std::process::Command::new("magick")
.arg("-version")
.output()
.is_ok()
{
return true;
}
if std::process::Command::new("convert")
.arg("-version")
.output()
.is_ok()
{
return true;
}
false
}
pub fn fixture(relative: impl AsRef<Path>) -> PathBuf {
path_to_resource("fixtures").join(relative)
}
pub fn running_in_ci() -> bool {
env::var("CI").is_ok()
}
pub fn compare_image_against_expected<Period: IsPeriod>(
sample: Data<Period>,
input: ValidInput,
path_to_expected_image: impl AsRef<Path>,
fetcher: impl FetchExchangeRates,
) {
assert!(
is_imagemagick_installed(),
"Imagemagick not installed, but required to run. `brew install imagemagick`"
);
let new_image =
generate_pdf_into_png_image(L18n::new(Language::EN).unwrap(), sample, input, fetcher);
let save_new_image_as_expected = |new_image: Vec<u8>| {
if !running_in_ci() {
std::fs::write(&path_to_expected_image, new_image)
.inspect_err(|e| {
panic!(
"should be able to write image, got error: {:?}, when using path: {:?}",
e,
path_to_expected_image.as_ref()
)
})
.unwrap();
}
};
save_new_image_as_expected(new_image.clone());
let image_one = image::load_from_memory(&new_image)
.expect("Could convert new image bytes to image")
.into_rgb8();
let Ok(image_two) = image::open(&path_to_expected_image) else {
warn!(
"Failed to locate the expected image at {:?}, saving new image as expected (if not CI).",
path_to_expected_image.as_ref()
);
save_new_image_as_expected(new_image);
return;
};
let image_two = image_two.into_rgb8();
let comparison_result = image_compare::rgb_similarity_structure(
&image_compare::Algorithm::RootMeanSquared,
&image_one,
&image_two,
);
if let Err(failure) = comparison_result {
let msg = format!(
"Failed to compare images, did you change DPI or the image format? Image compare error: {:?}",
failure
);
save_new_image_as_expected(new_image);
panic!("{}, replacing expected image.", msg);
};
let similarity = comparison_result.expect("Already checked for error above");
if similarity.score != 1.0 {
save_new_image_as_expected(new_image);
panic!(
"Expected similarity to be 1.0, but was {}",
similarity.score
)
}
}
#[cfg(test)]
#[derive(derive_more::From, Default)]
pub struct MockedExchangeRatesFetcher(ExchangeRatesMap);
#[cfg(test)]
impl FetchExchangeRates for MockedExchangeRatesFetcher {
fn fetch_for_items(
&self,
target_currency: Currency,
_items: Vec<Item>,
) -> Result<ExchangeRates> {
Ok(ExchangeRates::builder()
.rates(self.0.clone())
.target_currency(target_currency)
.build())
}
}
fn generate_pdf_into_png_image<Period: IsPeriod>(
l18n: L18n,
sample: Data<Period>,
input: ValidInput,
fetcher: impl FetchExchangeRates,
) -> Vec<u8> {
let layout = *input.layout();
let data = prepare_invoice_input_data(sample, input, fetcher).unwrap();
let pdf = render(l18n, data, layout).unwrap();
convert_pdf_to_pngs(pdf.as_ref(), 85.0).expect("Should be able to convert")
}
fn convert_pdf_to_pngs(pdf_bytes: &[u8], dpi: f64) -> Result<Vec<u8>, Box<dyn Error>> {
let mut temp_pdf = NamedTempFile::new()?;
temp_pdf.write_all(pdf_bytes)?;
let pdf_path = temp_pdf.path();
let temp_png = NamedTempFile::new()?;
let png_path = temp_png.path().with_extension("png");
let dpi_arg = format!("{}", dpi as u32);
let status = Command::new("magick")
.arg("-density")
.arg(&dpi_arg)
.arg("-units")
.arg("PixelsPerInch")
.arg(pdf_path)
.arg("-colorspace")
.arg("RGB")
.arg("-background")
.arg("white")
.arg("-alpha")
.arg("remove")
.arg("-flatten")
.arg("+profile")
.arg("*")
.arg("-strip")
.arg("-define")
.arg("png:compression-filter=0")
.arg("-define")
.arg("png:compression-level=9")
.arg("-define")
.arg("png:compression-strategy=1")
.arg(&png_path)
.status()?;
if !status.success() {
return Err("ImageMagick convert command failed".into());
}
let png_bytes = std::fs::read(&png_path)?;
Ok(png_bytes)
}