blob: 5f2071efcd667f22857efeba0832c7c046206a44 [file] [log] [blame]
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use clap::value_parser;
use clap::Parser;
use crabby_avif::decoder::track::RepetitionCount;
use crabby_avif::decoder::*;
use crabby_avif::utils::clap::CropRect;
use crabby_avif::*;
mod writer;
use writer::jpeg::JpegWriter;
use writer::png::PngWriter;
use writer::y4m::Y4MWriter;
use writer::Writer;
use std::fs::File;
use std::num::NonZero;
fn depth_parser(s: &str) -> Result<u8, String> {
match s.parse::<u8>() {
Ok(8) => Ok(8),
Ok(16) => Ok(16),
_ => Err("Value must be either 8 or 16".into()),
}
}
#[derive(Parser)]
struct CommandLineArgs {
/// Disable strict decoding, which disables strict validation checks and errors
#[arg(long, default_value = "false")]
no_strict: bool,
/// Decode all frames and display all image information instead of saving to disk
#[arg(short = 'i', long, default_value = "false")]
info: bool,
#[arg(long)]
jobs: Option<u32>,
/// When decoding an image sequence or progressive image, specify which frame index to decode
/// (Default: 0)
#[arg(long, short = 'I')]
index: Option<u32>,
/// Output depth, either 8 or 16. (PNG only; For y4m/yuv, source depth is retained; JPEG is
/// always 8bit)
#[arg(long, short = 'd', value_parser = depth_parser)]
depth: Option<u8>,
/// Output quality in 0..100. (JPEG only, default: 90)
#[arg(long, short = 'q', value_parser = value_parser!(u8).range(0..=100))]
quality: Option<u8>,
/// Enable progressive AVIF processing. If a progressive image is encountered and --progressive
/// is passed, --index will be used to choose which layer to decode (in progressive order).
#[arg(long, default_value = "false")]
progressive: bool,
/// Maximum image size (in total pixels) that should be tolerated (0 means unlimited)
#[arg(long)]
size_limit: Option<u32>,
/// Maximum image dimension (width or height) that should be tolerated (0 means unlimited)
#[arg(long)]
dimension_limit: Option<u32>,
/// If the input file contains embedded Exif metadata, ignore it (no-op if absent)
#[arg(long, default_value = "false")]
ignore_exif: bool,
/// If the input file contains embedded XMP metadata, ignore it (no-op if absent)
#[arg(long, default_value = "false")]
ignore_xmp: bool,
/// Input AVIF file
#[arg(allow_hyphen_values = false)]
input_file: String,
/// Output file
#[arg(allow_hyphen_values = false)]
output_file: Option<String>,
}
fn print_data_as_columns(rows: &[(usize, &str, String)]) {
let rows: Vec<_> = rows
.iter()
.filter(|x| !x.1.is_empty())
.map(|x| (format!("{} * {}", " ".repeat(x.0 * 4), x.1), x.2.as_str()))
.collect();
// Calculate the maximum width for the first column.
let mut max_col1_width = 0;
for (col1, _) in &rows {
max_col1_width = max_col1_width.max(col1.len());
}
for (col1, col2) in &rows {
println!("{col1:<max_col1_width$} : {col2}");
}
}
fn print_vec(data: &[u8]) -> String {
if data.is_empty() {
format!("Absent")
} else {
format!("Present ({} bytes)", data.len())
}
}
fn print_image_info(decoder: &Decoder) {
let image = decoder.image().unwrap();
let mut image_data = vec![
(
0,
"File Format",
format!("{:#?}", decoder.compression_format()),
),
(0, "Resolution", format!("{}x{}", image.width, image.height)),
(0, "Bit Depth", format!("{}", image.depth)),
(0, "Format", format!("{:#?}", image.yuv_format)),
if image.yuv_format == PixelFormat::Yuv420 {
(
0,
"Chroma Sample Position",
format!("{:#?}", image.chroma_sample_position),
)
} else {
(0, "", "".into())
},
(
0,
"Alpha",
format!(
"{}",
match (image.alpha_present, image.alpha_premultiplied) {
(true, true) => "Premultiplied",
(true, false) => "Not premultiplied",
(false, _) => "Absent",
}
),
),
(0, "Range", format!("{:#?}", image.yuv_range)),
(
0,
"Color Primaries",
format!("{:#?}", image.color_primaries),
),
(
0,
"Transfer Characteristics",
format!("{:#?}", image.transfer_characteristics),
),
(
0,
"Matrix Coefficients",
format!("{:#?}", image.matrix_coefficients),
),
(0, "ICC Profile", print_vec(&image.icc)),
(0, "XMP Metadata", print_vec(&image.xmp)),
(0, "Exif Metadata", print_vec(&image.exif)),
];
if image.pasp.is_none()
&& image.clap.is_none()
&& image.irot_angle.is_none()
&& image.imir_axis.is_none()
{
image_data.push((0, "Transformations", format!("None")));
} else {
image_data.push((0, "Transformations", format!("")));
if let Some(pasp) = image.pasp {
image_data.push((
1,
"pasp (Aspect Ratio)",
format!("{}/{}", pasp.h_spacing, pasp.v_spacing),
));
}
if let Some(clap) = image.clap {
image_data.push((1, "clap (Clean Aperture)", format!("")));
image_data.push((2, "W", format!("{}/{}", clap.width.0, clap.width.1)));
image_data.push((2, "H", format!("{}/{}", clap.height.0, clap.height.1)));
image_data.push((
2,
"hOff",
format!("{}/{}", clap.horiz_off.0, clap.horiz_off.1),
));
image_data.push((
2,
"vOff",
format!("{}/{}", clap.vert_off.0, clap.vert_off.1),
));
match CropRect::create_from(&clap, image.width, image.height, image.yuv_format) {
Ok(rect) => image_data.extend_from_slice(&[
(2, "Valid, derived crop rect", format!("")),
(3, "X", format!("{}", rect.x)),
(3, "Y", format!("{}", rect.y)),
(3, "W", format!("{}", rect.width)),
(3, "H", format!("{}", rect.height)),
]),
Err(_) => image_data.push((2, "Invalid", format!(""))),
}
}
if let Some(angle) = image.irot_angle {
image_data.push((1, "irot (Rotation)", format!("{angle}")));
}
if let Some(axis) = image.imir_axis {
image_data.push((1, "imir (Mirror)", format!("{axis}")));
}
}
image_data.push((0, "Progressive", format!("{:#?}", image.progressive_state)));
if let Some(clli) = image.clli {
image_data.push((0, "CLLI", format!("{}, {}", clli.max_cll, clli.max_pall)));
}
if decoder.gainmap_present() {
let gainmap = decoder.gainmap();
let gainmap_image = &gainmap.image;
image_data.extend_from_slice(&[
(
0,
"Gainmap",
format!(
"{}x{} pixels, {} bit, {:#?}, {:#?} Range, Matrix Coeffs. {:#?}, Base Image is {}",
gainmap_image.width,
gainmap_image.height,
gainmap_image.depth,
gainmap_image.yuv_format,
gainmap_image.yuv_range,
gainmap_image.matrix_coefficients,
if gainmap.metadata.base_hdr_headroom.0 == 0 { "SDR" } else { "HDR" },
),
),
(0, "Alternate image", format!("")),
(
1,
"Color Primaries",
format!("{:#?}", gainmap.alt_color_primaries),
),
(
1,
"Transfer Characteristics",
format!("{:#?}", gainmap.alt_transfer_characteristics),
),
(
1,
"Matrix Coefficients",
format!("{:#?}", gainmap.alt_matrix_coefficients),
),
(1, "ICC Profile", print_vec(&gainmap.alt_icc)),
(1, "Bit Depth", format!("{}", gainmap.alt_plane_depth)),
(1, "Planes", format!("{}", gainmap.alt_plane_count)),
if let Some(clli) = gainmap_image.clli {
(1, "CLLI", format!("{}, {}", clli.max_cll, clli.max_pall))
} else {
(1, "", "".into())
},
])
} else {
// TODO: b/394162563 - check if we need to report the present but ignored case.
image_data.push((0, "Gainmap", format!("Absent")));
}
if image.image_sequence_track_present {
image_data.push((
0,
"Repeat Count",
match decoder.repetition_count() {
RepetitionCount::Finite(x) => format!("{x}"),
RepetitionCount::Infinite => format!("Infinite"),
RepetitionCount::Unknown => format!("Unknown"),
},
));
}
print_data_as_columns(&image_data);
}
fn max_threads(jobs: &Option<u32>) -> u32 {
match jobs {
Some(x) => {
if *x == 0 {
match std::thread::available_parallelism() {
Ok(value) => value.get() as u32,
Err(_) => 1,
}
} else {
*x
}
}
None => 1,
}
}
fn create_decoder_and_parse(args: &CommandLineArgs) -> AvifResult<Decoder> {
let mut settings = Settings {
strictness: if args.no_strict { Strictness::None } else { Strictness::All },
image_content_to_decode: ImageContentType::All,
max_threads: max_threads(&args.jobs),
allow_progressive: args.progressive,
ignore_exif: args.ignore_exif,
ignore_xmp: args.ignore_xmp,
..Settings::default()
};
// These values cannot be initialized in the list above since we need the default values to be
// retain unless they are explicitly specified.
if let Some(size_limit) = args.size_limit {
settings.image_size_limit = NonZero::new(size_limit);
}
if let Some(dimension_limit) = args.dimension_limit {
settings.image_dimension_limit = NonZero::new(dimension_limit);
}
let mut decoder = Decoder::default();
decoder.settings = settings;
decoder
.set_io_file(&args.input_file)
.or(Err(AvifError::UnknownError(
"Cannot open input file".into(),
)))?;
decoder.parse()?;
Ok(decoder)
}
fn info(args: &CommandLineArgs) -> AvifResult<()> {
let mut decoder = create_decoder_and_parse(&args)?;
println!("Image decoded: {}", args.input_file);
print_image_info(&decoder);
println!(
" * {} timescales per second, {} seconds ({} timescales), {} frame{}",
decoder.timescale(),
decoder.duration(),
decoder.duration_in_timescales(),
decoder.image_count(),
if decoder.image_count() == 1 { "" } else { "s" },
);
if decoder.image_count() > 1 {
let image = decoder.image().unwrap();
println!(
" * {} Frames: ({} expected frames)",
if image.image_sequence_track_present {
"Image Sequence"
} else {
"Progressive Image"
},
decoder.image_count()
);
} else {
println!(" * Frame:");
}
let mut index = 0;
loop {
match decoder.next_image() {
Ok(_) => {
println!(" * Decoded frame [{}] [pts {} ({} timescales)] [duration {} ({} timescales)] [{}x{}]",
index,
decoder.image_timing().pts,
decoder.image_timing().pts_in_timescales,
decoder.image_timing().duration,
decoder.image_timing().duration_in_timescales,
decoder.image().unwrap().width,
decoder.image().unwrap().height);
index += 1;
}
Err(AvifError::NoImagesRemaining) => {
return Ok(());
}
Err(err) => {
return Err(err);
}
}
}
}
fn get_extension(filename: &str) -> &str {
std::path::Path::new(filename)
.extension()
.and_then(|s| s.to_str())
.unwrap_or("")
}
fn decode(args: &CommandLineArgs) -> AvifResult<()> {
let max_threads = max_threads(&args.jobs);
println!(
"Decoding with {max_threads} worker thread{}, please wait...",
if max_threads == 1 { "" } else { "s" }
);
let mut decoder = create_decoder_and_parse(&args)?;
decoder.nth_image(args.index.unwrap_or(0))?;
println!("Image Decoded: {}", args.input_file);
println!("Image details:");
print_image_info(&decoder);
let output_filename = &args.output_file.as_ref().unwrap().as_str();
let image = decoder.image().unwrap();
let extension = get_extension(output_filename);
let mut writer: Box<dyn Writer> = match extension {
"y4m" | "yuv" => {
if !image.icc.is_empty() || !image.exif.is_empty() || !image.xmp.is_empty() {
println!("Warning: metadata dropped when saving to {extension}");
}
Box::new(Y4MWriter::create(extension == "yuv"))
}
"png" => Box::new(PngWriter { depth: args.depth }),
"jpg" | "jpeg" => Box::new(JpegWriter {
quality: args.quality,
}),
_ => {
return Err(AvifError::UnknownError(format!(
"Unknown output file extension ({extension})"
)));
}
};
let mut output_file = File::create(output_filename).or(Err(AvifError::UnknownError(
"Could not open output file".into(),
)))?;
writer.write_frame(&mut output_file, image)?;
println!(
"Wrote image at index {} to output {}",
args.index.unwrap_or(0),
output_filename,
);
Ok(())
}
fn validate_args(args: &CommandLineArgs) -> AvifResult<()> {
if args.info {
if args.output_file.is_some()
|| args.quality.is_some()
|| args.depth.is_some()
|| args.index.is_some()
{
return Err(AvifError::UnknownError(
"--info contains unsupported extra arguments".into(),
));
}
} else {
if args.output_file.is_none() {
return Err(AvifError::UnknownError("output_file is required".into()));
}
let output_filename = &args.output_file.as_ref().unwrap().as_str();
let extension = get_extension(output_filename);
if args.quality.is_some() && extension != "jpg" && extension != "jpeg" {
return Err(AvifError::UnknownError(
"quality is only supported for jpeg output".into(),
));
}
if args.depth.is_some() && extension != "png" {
return Err(AvifError::UnknownError(
"depth is only supported for png output".into(),
));
}
}
Ok(())
}
fn main() {
let args = CommandLineArgs::parse();
if let Err(err) = validate_args(&args) {
eprintln!("ERROR: {:#?}", err);
std::process::exit(1);
}
let res = if args.info { info(&args) } else { decode(&args) };
match res {
Ok(_) => std::process::exit(0),
Err(err) => {
eprintln!("ERROR: {:#?}", err);
std::process::exit(1);
}
}
}