use std::fs;
use std::path::Path;
use std::process::Command;
use image::ImageFormat;
use crate::converters::scad::which_tool;
use crate::{Error, Job, Result, extension_of};
pub fn convert(job: &Job) -> Result<()> {
let img = image::open(job.input)
.map_err(|e| Error::other(format!("image decode: {e}")))?;
save_to(&img, job.output)?;
Ok(())
}
pub fn from_psd(job: &Job) -> Result<()> {
let img = load_psd(job.input)?;
save_to(&img, job.output)?;
Ok(())
}
pub fn to_jp2(job: &Job) -> Result<()> {
let img = image::open(job.input)
.map_err(|e| Error::other(format!("image decode: {e}")))?;
encode_jp2(&img, job.output)
}
pub fn psd_to_jp2(job: &Job) -> Result<()> {
let img = load_psd(job.input)?;
encode_jp2(&img, job.output)
}
pub fn from_jp2(job: &Job) -> Result<()> {
let img = load_jp2(job.input)?;
save_to(&img, job.output)?;
Ok(())
}
fn encode_jp2(img: &image::DynamicImage, output: &Path) -> Result<()> {
if !which_tool("opj_compress") {
return Err(Error::MissingTool {
from: String::new(),
to: "jp2".to_string(),
tool: "opj_compress",
});
}
let staged = tempfile::Builder::new()
.prefix("univert-jp2-")
.suffix(".png")
.tempfile()?;
img
.save_with_format(staged.path(), ImageFormat::Png)
.map_err(|e| Error::other(format!("jp2 stage png: {e}")))?;
let status = Command::new("opj_compress")
.arg("-i")
.arg(staged.path())
.arg("-o")
.arg(output)
.status()
.map_err(|e| Error::ToolFailed {
tool: "opj_compress",
message: e.to_string(),
})?;
if !status.success() {
return Err(Error::ToolFailed {
tool: "opj_compress",
message: format!("opj_compress exited with status {status}"),
});
}
Ok(())
}
fn save_to(img: &image::DynamicImage, output: &Path) -> Result<()> {
let ext = extension_of(output)
.ok_or_else(|| Error::OutputNoExtension(output.to_path_buf()))?;
let fmt = match ext.as_str() {
"bmp" => ImageFormat::Bmp,
"gif" => ImageFormat::Gif,
"ico" => ImageFormat::Ico,
"jpeg" | "jpg" => ImageFormat::Jpeg,
"png" => ImageFormat::Png,
"tga" => ImageFormat::Tga,
"tif" | "tiff" => ImageFormat::Tiff,
"webp" => ImageFormat::WebP,
"avif" => ImageFormat::Avif,
"qoi" => ImageFormat::Qoi,
"pnm" | "pbm" | "pgm" | "ppm" => ImageFormat::Pnm,
"hdr" => ImageFormat::Hdr,
"exr" => ImageFormat::OpenExr,
"ff" | "farbfeld" => ImageFormat::Farbfeld,
other => {
return Err(Error::other(format!("no image encoder for .{other}")));
}
};
let img = if matches!(fmt, ImageFormat::Ico) {
let (w, h) = (img.width(), img.height());
if w > 256 || h > 256 {
img.resize(256, 256, image::imageops::FilterType::Lanczos3)
} else {
img.clone()
}
} else {
img.clone()
};
img
.save_with_format(output, fmt)
.map_err(|e| Error::other(format!("image encode: {e}")))?;
Ok(())
}
fn load_psd(path: &Path) -> Result<image::DynamicImage> {
let bytes = fs::read(path)?;
let psd_doc = psd::Psd::from_bytes(&bytes)
.map_err(|e| Error::other(format!("psd parse: {e}")))?;
let rgba = psd_doc.rgba();
let buf = image::RgbaImage::from_raw(psd_doc.width(), psd_doc.height(), rgba)
.ok_or_else(|| Error::other("psd: failed to construct RGBA image"))?;
Ok(image::DynamicImage::ImageRgba8(buf))
}
fn load_jp2(path: &Path) -> Result<image::DynamicImage> {
let bytes = fs::read(path)?;
let img = jpeg2k::Image::from_bytes(&bytes)
.map_err(|e| Error::other(format!("jp2 decode: {e}")))?;
let dyn_img: image::DynamicImage = (&img)
.try_into()
.map_err(|e| Error::other(format!("jp2 convert: {e}")))?;
Ok(dyn_img)
}