univert 0.4.0

Universal file converter (library and CLI)
Documentation
//! Image conversions that require an external tool.

use std::process::Command;

use crate::converters::scad::which_tool;
use crate::{Error, Job, Result, extension_of};

pub fn heic_to_image(job: &Job) -> Result<()> {
  // Prefer heif-convert (libheif) since it handles HEIC natively. Fall back
  // to ffmpeg, then ImageMagick. On macOS, `sips` is always available.
  if which_tool("heif-convert") {
    return run_status(
      Command::new("heif-convert").arg(job.input).arg(job.output),
      "heif-convert",
    );
  }
  if which_tool("magick") {
    return run_status(
      Command::new("magick").arg(job.input).arg(job.output),
      "magick",
    );
  }
  if which_tool("ffmpeg") {
    return run_status(
      Command::new("ffmpeg")
        .args(["-y", "-hide_banner", "-loglevel", "error", "-i"])
        .arg(job.input)
        .arg(job.output),
      "ffmpeg",
    );
  }
  if cfg!(target_os = "macos") && which_tool("sips") {
    let to = extension_of(job.output).unwrap_or_default();
    let format = match to.as_str() {
      "jpeg" | "jpg" => "jpeg",
      "png" => "png",
      "tif" | "tiff" => "tiff",
      _ => "png",
    };
    return run_status(
      Command::new("sips")
        .args(["-s", "format", format])
        .arg(job.input)
        .args(["--out"])
        .arg(job.output),
      "sips",
    );
  }
  Err(Error::MissingTool {
    from: "heic".to_string(),
    to: extension_of(job.output).unwrap_or_default(),
    tool: "heif-convert",
  })
}

pub fn pdf_to_png(job: &Job) -> Result<()> {
  // pdftoppm (from poppler-utils) is the most common. It appends a page
  // suffix by default, so we render only the first page and rename.
  if which_tool("pdftoppm") {
    let staging = tempfile::tempdir()?;
    let prefix = staging.path().join("page");
    let status = Command::new("pdftoppm")
      .args(["-png", "-f", "1", "-l", "1", "-singlefile"])
      .arg(job.input)
      .arg(&prefix)
      .status()
      .map_err(|e| Error::ToolFailed {
        tool: "pdftoppm",
        message: e.to_string(),
      })?;
    if !status.success() {
      return Err(Error::ToolFailed {
        tool: "pdftoppm",
        message: format!("pdftoppm exited with status {status}"),
      });
    }
    let produced = staging.path().join("page.png");
    std::fs::rename(&produced, job.output)
      .or_else(|_| std::fs::copy(&produced, job.output).map(|_| ()))?;
    return Ok(());
  }
  if which_tool("magick") {
    return run_status(
      Command::new("magick")
        .arg("-density")
        .arg("150")
        .arg(format!("{}[0]", job.input.display()))
        .arg(job.output),
      "magick",
    );
  }
  Err(Error::MissingTool {
    from: "pdf".to_string(),
    to: "png".to_string(),
    tool: "pdftoppm",
  })
}

fn run_status(cmd: &mut Command, tool: &'static str) -> Result<()> {
  let status = cmd.status().map_err(|e| Error::ToolFailed {
    tool,
    message: e.to_string(),
  })?;
  if !status.success() {
    return Err(Error::ToolFailed {
      tool,
      message: format!("{tool} exited with status {status}"),
    });
  }
  Ok(())
}