1 // Copyright 2025 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 use clap::value_parser;
16 use clap::Parser;
17
18 use crabby_avif::decoder::track::RepetitionCount;
19 use crabby_avif::decoder::*;
20 use crabby_avif::utils::clap::CropRect;
21 use crabby_avif::*;
22
23 mod writer;
24
25 use writer::jpeg::JpegWriter;
26 use writer::png::PngWriter;
27 use writer::y4m::Y4MWriter;
28 use writer::Writer;
29
30 use std::fs::File;
31 use std::num::NonZero;
32
depth_parser(s: &str) -> Result<u8, String>33 fn depth_parser(s: &str) -> Result<u8, String> {
34 match s.parse::<u8>() {
35 Ok(8) => Ok(8),
36 Ok(16) => Ok(16),
37 _ => Err("Value must be either 8 or 16".into()),
38 }
39 }
40
41 #[derive(Parser)]
42 struct CommandLineArgs {
43 /// Disable strict decoding, which disables strict validation checks and errors
44 #[arg(long, default_value = "false")]
45 no_strict: bool,
46
47 /// Decode all frames and display all image information instead of saving to disk
48 #[arg(short = 'i', long, default_value = "false")]
49 info: bool,
50
51 #[arg(long)]
52 jobs: Option<u32>,
53
54 /// When decoding an image sequence or progressive image, specify which frame index to decode
55 /// (Default: 0)
56 #[arg(long, short = 'I')]
57 index: Option<u32>,
58
59 /// Output depth, either 8 or 16. (PNG only; For y4m/yuv, source depth is retained; JPEG is
60 /// always 8bit)
61 #[arg(long, short = 'd', value_parser = depth_parser)]
62 depth: Option<u8>,
63
64 /// Output quality in 0..100. (JPEG only, default: 90)
65 #[arg(long, short = 'q', value_parser = value_parser!(u8).range(0..=100))]
66 quality: Option<u8>,
67
68 /// Enable progressive AVIF processing. If a progressive image is encountered and --progressive
69 /// is passed, --index will be used to choose which layer to decode (in progressive order).
70 #[arg(long, default_value = "false")]
71 progressive: bool,
72
73 /// Maximum image size (in total pixels) that should be tolerated (0 means unlimited)
74 #[arg(long)]
75 size_limit: Option<u32>,
76
77 /// Maximum image dimension (width or height) that should be tolerated (0 means unlimited)
78 #[arg(long)]
79 dimension_limit: Option<u32>,
80
81 /// If the input file contains embedded Exif metadata, ignore it (no-op if absent)
82 #[arg(long, default_value = "false")]
83 ignore_exif: bool,
84
85 /// If the input file contains embedded XMP metadata, ignore it (no-op if absent)
86 #[arg(long, default_value = "false")]
87 ignore_xmp: bool,
88
89 /// Input AVIF file
90 #[arg(allow_hyphen_values = false)]
91 input_file: String,
92
93 /// Output file
94 #[arg(allow_hyphen_values = false)]
95 output_file: Option<String>,
96 }
97
print_data_as_columns(rows: &[(usize, &str, String)])98 fn print_data_as_columns(rows: &[(usize, &str, String)]) {
99 let rows: Vec<_> = rows
100 .iter()
101 .filter(|x| !x.1.is_empty())
102 .map(|x| (format!("{} * {}", " ".repeat(x.0 * 4), x.1), x.2.as_str()))
103 .collect();
104
105 // Calculate the maximum width for the first column.
106 let mut max_col1_width = 0;
107 for (col1, _) in &rows {
108 max_col1_width = max_col1_width.max(col1.len());
109 }
110
111 for (col1, col2) in &rows {
112 println!("{col1:<max_col1_width$} : {col2}");
113 }
114 }
115
print_vec(data: &[u8]) -> String116 fn print_vec(data: &[u8]) -> String {
117 if data.is_empty() {
118 format!("Absent")
119 } else {
120 format!("Present ({} bytes)", data.len())
121 }
122 }
123
print_image_info(decoder: &Decoder)124 fn print_image_info(decoder: &Decoder) {
125 let image = decoder.image().unwrap();
126 let mut image_data = vec![
127 (
128 0,
129 "File Format",
130 format!("{:#?}", decoder.compression_format()),
131 ),
132 (0, "Resolution", format!("{}x{}", image.width, image.height)),
133 (0, "Bit Depth", format!("{}", image.depth)),
134 (0, "Format", format!("{:#?}", image.yuv_format)),
135 if image.yuv_format == PixelFormat::Yuv420 {
136 (
137 0,
138 "Chroma Sample Position",
139 format!("{:#?}", image.chroma_sample_position),
140 )
141 } else {
142 (0, "", "".into())
143 },
144 (
145 0,
146 "Alpha",
147 format!(
148 "{}",
149 match (image.alpha_present, image.alpha_premultiplied) {
150 (true, true) => "Premultiplied",
151 (true, false) => "Not premultiplied",
152 (false, _) => "Absent",
153 }
154 ),
155 ),
156 (0, "Range", format!("{:#?}", image.yuv_range)),
157 (
158 0,
159 "Color Primaries",
160 format!("{:#?}", image.color_primaries),
161 ),
162 (
163 0,
164 "Transfer Characteristics",
165 format!("{:#?}", image.transfer_characteristics),
166 ),
167 (
168 0,
169 "Matrix Coefficients",
170 format!("{:#?}", image.matrix_coefficients),
171 ),
172 (0, "ICC Profile", print_vec(&image.icc)),
173 (0, "XMP Metadata", print_vec(&image.xmp)),
174 (0, "Exif Metadata", print_vec(&image.exif)),
175 ];
176 if image.pasp.is_none()
177 && image.clap.is_none()
178 && image.irot_angle.is_none()
179 && image.imir_axis.is_none()
180 {
181 image_data.push((0, "Transformations", format!("None")));
182 } else {
183 image_data.push((0, "Transformations", format!("")));
184 if let Some(pasp) = image.pasp {
185 image_data.push((
186 1,
187 "pasp (Aspect Ratio)",
188 format!("{}/{}", pasp.h_spacing, pasp.v_spacing),
189 ));
190 }
191 if let Some(clap) = image.clap {
192 image_data.push((1, "clap (Clean Aperture)", format!("")));
193 image_data.push((2, "W", format!("{}/{}", clap.width.0, clap.width.1)));
194 image_data.push((2, "H", format!("{}/{}", clap.height.0, clap.height.1)));
195 image_data.push((
196 2,
197 "hOff",
198 format!("{}/{}", clap.horiz_off.0, clap.horiz_off.1),
199 ));
200 image_data.push((
201 2,
202 "vOff",
203 format!("{}/{}", clap.vert_off.0, clap.vert_off.1),
204 ));
205 match CropRect::create_from(&clap, image.width, image.height, image.yuv_format) {
206 Ok(rect) => image_data.extend_from_slice(&[
207 (2, "Valid, derived crop rect", format!("")),
208 (3, "X", format!("{}", rect.x)),
209 (3, "Y", format!("{}", rect.y)),
210 (3, "W", format!("{}", rect.width)),
211 (3, "H", format!("{}", rect.height)),
212 ]),
213 Err(_) => image_data.push((2, "Invalid", format!(""))),
214 }
215 }
216 if let Some(angle) = image.irot_angle {
217 image_data.push((1, "irot (Rotation)", format!("{angle}")));
218 }
219 if let Some(axis) = image.imir_axis {
220 image_data.push((1, "imir (Mirror)", format!("{axis}")));
221 }
222 }
223 image_data.push((0, "Progressive", format!("{:#?}", image.progressive_state)));
224 if let Some(clli) = image.clli {
225 image_data.push((0, "CLLI", format!("{}, {}", clli.max_cll, clli.max_pall)));
226 }
227 if decoder.gainmap_present() {
228 let gainmap = decoder.gainmap();
229 let gainmap_image = &gainmap.image;
230 image_data.extend_from_slice(&[
231 (
232 0,
233 "Gainmap",
234 format!(
235 "{}x{} pixels, {} bit, {:#?}, {:#?} Range, Matrix Coeffs. {:#?}, Base Image is {}",
236 gainmap_image.width,
237 gainmap_image.height,
238 gainmap_image.depth,
239 gainmap_image.yuv_format,
240 gainmap_image.yuv_range,
241 gainmap_image.matrix_coefficients,
242 if gainmap.metadata.base_hdr_headroom.0 == 0 { "SDR" } else { "HDR" },
243 ),
244 ),
245 (0, "Alternate image", format!("")),
246 (
247 1,
248 "Color Primaries",
249 format!("{:#?}", gainmap.alt_color_primaries),
250 ),
251 (
252 1,
253 "Transfer Characteristics",
254 format!("{:#?}", gainmap.alt_transfer_characteristics),
255 ),
256 (
257 1,
258 "Matrix Coefficients",
259 format!("{:#?}", gainmap.alt_matrix_coefficients),
260 ),
261 (1, "ICC Profile", print_vec(&gainmap.alt_icc)),
262 (1, "Bit Depth", format!("{}", gainmap.alt_plane_depth)),
263 (1, "Planes", format!("{}", gainmap.alt_plane_count)),
264 if let Some(clli) = gainmap_image.clli {
265 (1, "CLLI", format!("{}, {}", clli.max_cll, clli.max_pall))
266 } else {
267 (1, "", "".into())
268 },
269 ])
270 } else {
271 // TODO: b/394162563 - check if we need to report the present but ignored case.
272 image_data.push((0, "Gainmap", format!("Absent")));
273 }
274 if image.image_sequence_track_present {
275 image_data.push((
276 0,
277 "Repeat Count",
278 match decoder.repetition_count() {
279 RepetitionCount::Finite(x) => format!("{x}"),
280 RepetitionCount::Infinite => format!("Infinite"),
281 RepetitionCount::Unknown => format!("Unknown"),
282 },
283 ));
284 }
285 print_data_as_columns(&image_data);
286 }
287
max_threads(jobs: &Option<u32>) -> u32288 fn max_threads(jobs: &Option<u32>) -> u32 {
289 match jobs {
290 Some(x) => {
291 if *x == 0 {
292 match std::thread::available_parallelism() {
293 Ok(value) => value.get() as u32,
294 Err(_) => 1,
295 }
296 } else {
297 *x
298 }
299 }
300 None => 1,
301 }
302 }
303
create_decoder_and_parse(args: &CommandLineArgs) -> AvifResult<Decoder>304 fn create_decoder_and_parse(args: &CommandLineArgs) -> AvifResult<Decoder> {
305 let mut settings = Settings {
306 strictness: if args.no_strict { Strictness::None } else { Strictness::All },
307 image_content_to_decode: ImageContentType::All,
308 max_threads: max_threads(&args.jobs),
309 allow_progressive: args.progressive,
310 ignore_exif: args.ignore_exif,
311 ignore_xmp: args.ignore_xmp,
312 ..Settings::default()
313 };
314 // These values cannot be initialized in the list above since we need the default values to be
315 // retain unless they are explicitly specified.
316 if let Some(size_limit) = args.size_limit {
317 settings.image_size_limit = NonZero::new(size_limit);
318 }
319 if let Some(dimension_limit) = args.dimension_limit {
320 settings.image_dimension_limit = NonZero::new(dimension_limit);
321 }
322 let mut decoder = Decoder::default();
323 decoder.settings = settings;
324 decoder
325 .set_io_file(&args.input_file)
326 .or(Err(AvifError::UnknownError(
327 "Cannot open input file".into(),
328 )))?;
329 decoder.parse()?;
330 Ok(decoder)
331 }
332
info(args: &CommandLineArgs) -> AvifResult<()>333 fn info(args: &CommandLineArgs) -> AvifResult<()> {
334 let mut decoder = create_decoder_and_parse(&args)?;
335 println!("Image decoded: {}", args.input_file);
336 print_image_info(&decoder);
337 println!(
338 " * {} timescales per second, {} seconds ({} timescales), {} frame{}",
339 decoder.timescale(),
340 decoder.duration(),
341 decoder.duration_in_timescales(),
342 decoder.image_count(),
343 if decoder.image_count() == 1 { "" } else { "s" },
344 );
345 if decoder.image_count() > 1 {
346 let image = decoder.image().unwrap();
347 println!(
348 " * {} Frames: ({} expected frames)",
349 if image.image_sequence_track_present {
350 "Image Sequence"
351 } else {
352 "Progressive Image"
353 },
354 decoder.image_count()
355 );
356 } else {
357 println!(" * Frame:");
358 }
359
360 let mut index = 0;
361 loop {
362 match decoder.next_image() {
363 Ok(_) => {
364 println!(" * Decoded frame [{}] [pts {} ({} timescales)] [duration {} ({} timescales)] [{}x{}]",
365 index,
366 decoder.image_timing().pts,
367 decoder.image_timing().pts_in_timescales,
368 decoder.image_timing().duration,
369 decoder.image_timing().duration_in_timescales,
370 decoder.image().unwrap().width,
371 decoder.image().unwrap().height);
372 index += 1;
373 }
374 Err(AvifError::NoImagesRemaining) => {
375 return Ok(());
376 }
377 Err(err) => {
378 return Err(err);
379 }
380 }
381 }
382 }
383
get_extension(filename: &str) -> &str384 fn get_extension(filename: &str) -> &str {
385 std::path::Path::new(filename)
386 .extension()
387 .and_then(|s| s.to_str())
388 .unwrap_or("")
389 }
390
decode(args: &CommandLineArgs) -> AvifResult<()>391 fn decode(args: &CommandLineArgs) -> AvifResult<()> {
392 let max_threads = max_threads(&args.jobs);
393 println!(
394 "Decoding with {max_threads} worker thread{}, please wait...",
395 if max_threads == 1 { "" } else { "s" }
396 );
397 let mut decoder = create_decoder_and_parse(&args)?;
398 decoder.nth_image(args.index.unwrap_or(0))?;
399 println!("Image Decoded: {}", args.input_file);
400 println!("Image details:");
401 print_image_info(&decoder);
402
403 let output_filename = &args.output_file.as_ref().unwrap().as_str();
404 let image = decoder.image().unwrap();
405 let extension = get_extension(output_filename);
406 let mut writer: Box<dyn Writer> = match extension {
407 "y4m" | "yuv" => {
408 if !image.icc.is_empty() || !image.exif.is_empty() || !image.xmp.is_empty() {
409 println!("Warning: metadata dropped when saving to {extension}");
410 }
411 Box::new(Y4MWriter::create(extension == "yuv"))
412 }
413 "png" => Box::new(PngWriter { depth: args.depth }),
414 "jpg" | "jpeg" => Box::new(JpegWriter {
415 quality: args.quality,
416 }),
417 _ => {
418 return Err(AvifError::UnknownError(format!(
419 "Unknown output file extension ({extension})"
420 )));
421 }
422 };
423 let mut output_file = File::create(output_filename).or(Err(AvifError::UnknownError(
424 "Could not open output file".into(),
425 )))?;
426 writer.write_frame(&mut output_file, image)?;
427 println!(
428 "Wrote image at index {} to output {}",
429 args.index.unwrap_or(0),
430 output_filename,
431 );
432 Ok(())
433 }
434
validate_args(args: &CommandLineArgs) -> AvifResult<()>435 fn validate_args(args: &CommandLineArgs) -> AvifResult<()> {
436 if args.info {
437 if args.output_file.is_some()
438 || args.quality.is_some()
439 || args.depth.is_some()
440 || args.index.is_some()
441 {
442 return Err(AvifError::UnknownError(
443 "--info contains unsupported extra arguments".into(),
444 ));
445 }
446 } else {
447 if args.output_file.is_none() {
448 return Err(AvifError::UnknownError("output_file is required".into()));
449 }
450 let output_filename = &args.output_file.as_ref().unwrap().as_str();
451 let extension = get_extension(output_filename);
452 if args.quality.is_some() && extension != "jpg" && extension != "jpeg" {
453 return Err(AvifError::UnknownError(
454 "quality is only supported for jpeg output".into(),
455 ));
456 }
457 if args.depth.is_some() && extension != "png" {
458 return Err(AvifError::UnknownError(
459 "depth is only supported for png output".into(),
460 ));
461 }
462 }
463 Ok(())
464 }
465
main()466 fn main() {
467 let args = CommandLineArgs::parse();
468 if let Err(err) = validate_args(&args) {
469 eprintln!("ERROR: {:#?}", err);
470 std::process::exit(1);
471 }
472 let res = if args.info { info(&args) } else { decode(&args) };
473 match res {
474 Ok(_) => std::process::exit(0),
475 Err(err) => {
476 eprintln!("ERROR: {:#?}", err);
477 std::process::exit(1);
478 }
479 }
480 }
481