use crate::{MediaType, RawArtifact, TransformRequest, WatermarkInput, sniff_artifact, transform};
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use super::{
ClapConvertArgs, ClapOptimizeArgs, CliError, Command, ConvertCommand, EXIT_INPUT, EXIT_IO,
EXIT_RUNTIME, EXIT_USAGE, HelpTopic, InputSource, OutputTarget, TransformFields, convert_error,
convert_usage, map_transform_error, optimize_error, optimize_usage, read_input_bytes,
runtime_error, validate_url,
};
pub(super) fn convert_from_clap(args: ClapConvertArgs) -> Result<Command, CliError> {
if args.help {
return Ok(Command::Help(HelpTopic::Convert));
}
let input = match (&args.url, &args.input) {
(Some(url), None) => {
validate_url(url, "--url")?;
InputSource::Url(url.clone())
}
(None, Some(value)) if value == "-" => InputSource::Stdin,
(None, Some(value)) => InputSource::Path(PathBuf::from(value)),
(None, None) => {
return Err(CliError {
exit_code: EXIT_USAGE,
message: "'convert' requires an input file, URL, or -".to_string(),
usage: Some(convert_usage().to_string()),
hint: Some("try 'truss convert input.png -o output.jpg'".to_string()),
});
}
(Some(_), Some(_)) => {
return Err(convert_error("'convert' accepts exactly one input"));
}
};
let output = match args.output {
Some(ref value) if value == "-" => OutputTarget::Stdout,
Some(ref value) => OutputTarget::Path(PathBuf::from(value)),
None => {
return Err(CliError {
exit_code: EXIT_USAGE,
message: "'convert' requires -o <output>".to_string(),
usage: Some(convert_usage().to_string()),
hint: Some("try 'truss convert input.png -o output.jpg'".to_string()),
});
}
};
let watermark_path = args.watermark.clone();
if watermark_path.is_none()
&& (args.watermark_position.is_some()
|| args.watermark_opacity.is_some()
|| args.watermark_margin.is_some())
{
return Err(CliError {
exit_code: EXIT_USAGE,
message: "--watermark-position, --watermark-opacity, and --watermark-margin require --watermark".to_string(),
usage: Some(convert_usage().to_string()),
hint: Some("provide --watermark <path> when using watermark options".to_string()),
});
}
let watermark_position = args.watermark_position;
let watermark_opacity = args.watermark_opacity;
let watermark_margin = args.watermark_margin;
let options = TransformFields {
width: args.width,
height: args.height,
fit: args.fit,
position: args.position,
format: args.format,
quality: args.quality,
optimize: args.optimize,
target_quality: args.target_quality,
background: args.background,
rotate: args.rotate,
auto_orient: args.auto_orient,
no_auto_orient: args.no_auto_orient,
strip_metadata: args.strip_metadata,
keep_metadata: args.keep_metadata,
preserve_exif: args.preserve_exif,
crop: args.crop,
blur: args.blur,
sharpen: args.sharpen,
grayscale: args.grayscale,
}
.into_options()
.map_err(map_transform_error)?;
Ok(Command::Convert(ConvertCommand {
input,
output,
options,
watermark_path,
watermark_position,
watermark_opacity,
watermark_margin,
}))
}
pub(super) fn optimize_from_clap(args: ClapOptimizeArgs) -> Result<Command, CliError> {
if args.help {
return Ok(Command::Help(HelpTopic::Optimize));
}
let input = match (&args.url, &args.input) {
(Some(url), None) => {
validate_url(url, "--url")?;
InputSource::Url(url.clone())
}
(None, Some(value)) if value == "-" => InputSource::Stdin,
(None, Some(value)) => InputSource::Path(PathBuf::from(value)),
(None, None) => {
return Err(CliError {
exit_code: EXIT_USAGE,
message: "'optimize' requires an input file, URL, or -".to_string(),
usage: Some(optimize_usage().to_string()),
hint: Some("try 'truss optimize input.jpg -o output.jpg'".to_string()),
});
}
(Some(_), Some(_)) => {
return Err(optimize_error("'optimize' accepts exactly one input"));
}
};
let output = match args.output {
Some(ref value) if value == "-" => OutputTarget::Stdout,
Some(ref value) => OutputTarget::Path(PathBuf::from(value)),
None => {
return Err(CliError {
exit_code: EXIT_USAGE,
message: "'optimize' requires -o <output>".to_string(),
usage: Some(optimize_usage().to_string()),
hint: Some("try 'truss optimize input.jpg -o output.jpg'".to_string()),
});
}
};
let options = TransformFields {
width: None,
height: None,
fit: None,
position: None,
format: args.format,
quality: args.quality,
optimize: Some(args.mode.unwrap_or(crate::OptimizeMode::Auto)),
target_quality: args.target_quality,
background: None,
rotate: None,
auto_orient: args.auto_orient,
no_auto_orient: args.no_auto_orient,
strip_metadata: args.strip_metadata,
keep_metadata: args.keep_metadata,
preserve_exif: args.preserve_exif,
crop: None,
blur: None,
sharpen: None,
grayscale: false,
}
.into_options()
.map_err(map_transform_error)?;
Ok(Command::Optimize(ConvertCommand {
input,
output,
options,
watermark_path: None,
watermark_position: None,
watermark_opacity: None,
watermark_margin: None,
}))
}
pub(super) fn execute_convert<R, W>(
command: ConvertCommand,
stdin: &mut R,
stdout: &mut W,
) -> Result<(), CliError>
where
R: Read,
W: Write,
{
let bytes = read_input_bytes(command.input, stdin)?;
let input = sniff_artifact(RawArtifact::new(bytes, None))
.map_err(|error| runtime_error(EXIT_INPUT, &error.to_string()))?;
let mut options = command.options;
if options.format.is_none() {
options.format = infer_output_format(&command.output);
}
let watermark = if let Some(ref wm_path) = command.watermark_path {
let wm_bytes = fs::read(wm_path).map_err(|error| {
runtime_error(EXIT_IO, &format!("failed to read watermark file: {error}"))
})?;
let wm_artifact = sniff_artifact(RawArtifact::new(wm_bytes, None)).map_err(|error| {
runtime_error(
EXIT_INPUT,
&format!(
"failed to decode watermark '{}': {error}",
wm_path.display()
),
)
})?;
Some(WatermarkInput {
image: wm_artifact,
position: command
.watermark_position
.unwrap_or(crate::Position::BottomRight),
opacity: command.watermark_opacity.unwrap_or(50),
margin: command.watermark_margin.unwrap_or(10),
})
} else {
None
};
let mut request = TransformRequest::new(input, options);
request.watermark = watermark;
let result = transform(request).map_err(map_transform_error)?;
for warning in &result.warnings {
eprintln!("warning: {warning}");
}
write_output_bytes(command.output, &result.artifact.bytes, stdout)
}
fn infer_output_format(output: &OutputTarget) -> Option<MediaType> {
match output {
OutputTarget::Stdout => None,
OutputTarget::Path(path) => infer_output_format_from_path(path),
}
}
fn infer_output_format_from_path(path: &Path) -> Option<MediaType> {
let extension = path.extension()?.to_str()?.to_ascii_lowercase();
std::str::FromStr::from_str(&extension).ok()
}
fn write_output_bytes<W>(output: OutputTarget, bytes: &[u8], stdout: &mut W) -> Result<(), CliError>
where
W: Write,
{
match output {
OutputTarget::Stdout => stdout.write_all(bytes).map_err(|error| {
runtime_error(EXIT_RUNTIME, &format!("failed to write stdout: {error}"))
}),
OutputTarget::Path(path) => fs::write(&path, bytes).map_err(|error| {
runtime_error(
EXIT_IO,
&format!("failed to write {}: {error}", path.display()),
)
}),
}
}