• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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