1use std::io::{Read, Write};
8use std::time::Duration;
9
10use oxideav_core::frame::{VideoFrame, VideoPlane};
11use oxideav_core::{CodecId, CodecParameters, ColorSignal, Packet, TimeBase};
12
13use crate::api::{
14 burst_members, exif_item_body, exif_tiff_bytes, has_moov, item_colrs, AvifImage, ColorInfo,
15 ColorRange, DecodeOptions, EncodeOptions, Frame, Metadata, RgbImage, RgbaImage, StillChroma,
16};
17use crate::av1_config::{parse_av1c, Av1CodecConfig};
18use crate::avis::{parse_avis, sample_bytes};
19use crate::decoder::{
20 core_to_avif_frame, decode_item_output, infer_av1_pixmap, tone_map_of, validate_av1_config,
21 DecodeCtx, MAX_AV1_ITEM_BYTES,
22};
23use crate::error::{AvifError as Error, Result};
24use crate::frame_bridge::from_heif;
25use crate::image::{AvifPixelFormat, AvifPlane};
26use crate::inspect::{build_info, build_info_derived, build_info_grid, AvifInfo};
27use crate::meta::{Colr, ITEM_TYPE_IDEN, ITEM_TYPE_IOVL, ITEM_TYPE_TMAP};
28use crate::parser::{
29 audit_mif1, classify_brands, parse, parse_header, AvifHeader, BrandClass, ITEM_TYPE_AV01,
30 ITEM_TYPE_GRID,
31};
32use crate::signal::{color_signal_for, layout_of_record};
33use crate::still::{encode_still_auto, StillImage, StillProperties};
34use oxideav_heif::{Chroma, HeifFrame};
35
36pub(crate) fn from_core_err(e: oxideav_core::Error) -> Error {
39 match e {
40 oxideav_core::Error::InvalidData(s) => Error::InvalidData(s),
41 oxideav_core::Error::Unsupported(s) => Error::Unsupported(s),
42 oxideav_core::Error::ResourceExhausted(s) => Error::LimitExceeded(s),
43 oxideav_core::Error::Io(e) => Error::Io(e),
44 other => Error::InvalidData(other.to_string()),
45 }
46}
47
48impl From<AvifImage> for VideoFrame {
57 fn from(img: AvifImage) -> Self {
58 let signal = color_signal_for(Some(&img.color.to_colr()));
59 let mut vf = VideoFrame {
60 pts: None,
61 planes: img
62 .planes
63 .into_iter()
64 .map(|p| VideoPlane {
65 stride: p.stride,
66 data: p.data,
67 })
68 .collect(),
69 };
70 vf.set_color_signal(signal);
71 if img.format == AvifPixelFormat::Ya16Le && img.bit_depth < 16 {
72 vf.set_significant_bits(vec![img.bit_depth]);
73 }
74 vf
75 }
76}
77
78impl From<ColorSignal> for ColorInfo {
79 fn from(s: ColorSignal) -> Self {
80 let range = match s.range {
81 oxideav_core::ColorRange::Full => ColorRange::Full,
82 oxideav_core::ColorRange::Limited => ColorRange::Limited,
83 _ => ColorRange::Unspecified,
84 };
85 ColorInfo::new(range, s.primaries.0, s.transfer.0, s.matrix.0)
86 }
87}
88
89fn storage_layout_of(
94 fmt: oxideav_core::PixelFormat,
95) -> Result<(AvifPixelFormat, Option<ColorRange>, bool)> {
96 use oxideav_core::PixelFormat as P;
97 Ok(match fmt {
98 P::YuvJ420P => (AvifPixelFormat::Yuv420P, Some(ColorRange::Full), false),
99 P::YuvJ422P => (AvifPixelFormat::Yuv422P, Some(ColorRange::Full), false),
100 P::YuvJ444P => (AvifPixelFormat::Yuv444P, Some(ColorRange::Full), false),
101 P::Gbrp8 => (AvifPixelFormat::Yuv444P, None, true),
102 P::Gbrap8 => (AvifPixelFormat::Yuva444P, None, true),
103 P::Gbrp10Le => (AvifPixelFormat::Yuv444P10Le, None, true),
104 P::Gbrap10Le => (AvifPixelFormat::Yuva444P10Le, None, true),
105 P::Gbrp12Le => (AvifPixelFormat::Yuv444P12Le, None, true),
106 P::Gbrap12Le => (AvifPixelFormat::Yuva444P12Le, None, true),
107 other => (AvifPixelFormat::try_from(other)?, None, false),
108 })
109}
110
111impl AvifImage {
112 pub fn from_video_frame(frame: &VideoFrame, params: &CodecParameters) -> Result<Self> {
122 let width = params
123 .width
124 .ok_or_else(|| Error::invalid("avif: CodecParameters.width required"))?;
125 let height = params
126 .height
127 .ok_or_else(|| Error::invalid("avif: CodecParameters.height required"))?;
128 let label = params
129 .pixel_format
130 .ok_or_else(|| Error::invalid("avif: CodecParameters.pixel_format required"))?;
131 let (format, label_range, identity) = storage_layout_of(label)?;
132 let signal = frame.color_signal().unwrap_or(params.color_signal);
135 let mut color = if signal.is_unspecified() {
136 ColorInfo::default()
137 } else {
138 ColorInfo::from(signal)
139 };
140 if let Some(range) = label_range {
141 color.range = range;
142 }
143 if identity {
144 color.matrix = 0;
145 }
146 if color.range == ColorRange::Unspecified {
147 color.range = ColorRange::Full;
148 }
149 let bit_depth = match frame.significant_bits() {
150 Some(bits) if format == AvifPixelFormat::Ya16Le && !bits.is_empty() => bits[0],
151 _ => format.bit_depth(),
152 };
153 let planes = core_to_avif_frame(frame.clone()).planes;
154 Ok(Self::new(width, height, format, planes)?
155 .with_color(color)
156 .with_bit_depth(bit_depth))
157 }
158}
159
160impl TryFrom<(&VideoFrame, &CodecParameters)> for AvifImage {
161 type Error = Error;
162
163 fn try_from((frame, params): (&VideoFrame, &CodecParameters)) -> Result<Self> {
164 AvifImage::from_video_frame(frame, params)
165 }
166}
167
168pub(crate) struct Decoded {
175 pub image: AvifImage,
176 pub info: AvifInfo,
177}
178
179fn strict_checks(brands: &BrandClass, mif1: &crate::derived::Mif1Compliance) -> Result<()> {
181 if !(brands.is_image || brands.is_sequence || brands.is_intra_only) {
182 return Err(Error::invalid(
183 "avif (strict): ftyp declares no AVIF brand (avif / avis / avio, av1-avif §6)",
184 ));
185 }
186 if mif1.claims_mif1
187 && !(mif1.has_hdlr && mif1.has_pitm && mif1.has_iinf && mif1.has_iloc && mif1.has_iprp)
188 {
189 return Err(Error::invalid(
190 "avif (strict): file claims mif1 but lacks a HEIF §10.2.1.1 mandatory box",
191 ));
192 }
193 Ok(())
194}
195
196pub(crate) fn decode_primary(file: &[u8], opts: &DecodeOptions) -> Result<Decoded> {
200 opts.check_bytes(file.len())?;
201 let hdr = parse_header(file)?;
202 let primary_id = hdr
203 .meta
204 .primary_item_id
205 .ok_or_else(|| Error::invalid("avif: missing pitm"))?;
206 let primary_info = hdr
207 .meta
208 .item_by_id(primary_id)
209 .ok_or_else(|| Error::invalid("avif: pitm references unknown item"))?
210 .clone();
211 let brands = classify_brands(&hdr.major_brand, &hdr.compatible_brands)?;
212 let mif1 = audit_mif1(file)?;
213 if opts.strict {
214 strict_checks(&brands, &mif1)?;
215 }
216 let info = if primary_info.item_type == ITEM_TYPE_GRID {
217 build_info_grid(&hdr, primary_id, brands, mif1)?
218 } else if primary_info.item_type == ITEM_TYPE_AV01 {
219 let img = parse(file)?;
220 let has_alpha = crate::alpha::find_alpha_item_id(&hdr.meta, primary_id).is_some();
221 build_info(&img, has_alpha, brands, mif1, file)?
222 } else if primary_info.item_type == ITEM_TYPE_IOVL
223 || primary_info.item_type == ITEM_TYPE_IDEN
224 || primary_info.item_type == ITEM_TYPE_TMAP
225 {
226 build_info_derived(&hdr, primary_id, brands, mif1)?
227 } else {
228 return Err(Error::unsupported(format!(
229 "avif: primary item type '{}' not supported",
230 String::from_utf8_lossy(&primary_info.item_type)
231 )));
232 };
233 opts.check_dims(info.width, info.height)?;
235 let image = picture_of(
236 &hdr,
237 primary_id,
238 opts,
239 info.colour.as_ref(),
240 (info.exif_item_id, info.xmp_item_id),
241 )?;
242 Ok(Decoded { image, info })
243}
244
245fn picture_of(
251 hdr: &AvifHeader<'_>,
252 item_id: u32,
253 opts: &DecodeOptions,
254 fallback_colour: Option<&Colr>,
255 meta_items: (Option<u32>, Option<u32>),
256) -> Result<AvifImage> {
257 let is_tmap = hdr
258 .meta
259 .item_by_id(item_id)
260 .map(|i| i.item_type == ITEM_TYPE_TMAP)
261 .unwrap_or(false);
262 let mut ctx = DecodeCtx::new(opts);
263 let target = if opts.tone_mapped {
266 tone_map_of(hdr, item_id).unwrap_or(item_id)
267 } else {
268 item_id
269 };
270 let composed: HeifFrame =
271 decode_item_output(hdr, target, 0, &mut ctx, true).map_err(from_core_err)?;
272 opts.check_dims(composed.width, composed.height)?;
273 let bit_depth = composed.format.bit_depth;
274 let (frame, format) = from_heif(&composed)?;
275 let (nclx, icc) = item_colrs(&hdr.meta, target);
281 let colour = if target != item_id || is_tmap {
282 nclx.or_else(|| nclx_of(fallback_colour))
283 } else {
284 nclx.or_else(|| nclx_of(fallback_colour))
285 };
286 let icc = icc.or_else(|| match fallback_colour {
287 Some(Colr::Icc(bytes)) => Some(bytes.clone()),
288 _ => None,
289 });
290 let exif = meta_items
291 .0
292 .and_then(|id| hdr.item_data(id).ok())
293 .and_then(|body| exif_tiff_bytes(&body));
294 let xmp = meta_items
295 .1
296 .and_then(|id| hdr.item_data(id).ok())
297 .map(|b| b.into_owned());
298 Ok(AvifImage {
299 width: composed.width,
300 height: composed.height,
301 format,
302 planes: frame.planes,
303 color: ColorInfo::from_colr(colour.as_ref()),
304 metadata: Metadata::new(icc, exif, xmp, None),
305 bit_depth,
306 })
307}
308
309fn nclx_of(colr: Option<&Colr>) -> Option<Colr> {
310 match colr {
311 Some(c @ Colr::Nclx { .. }) => Some(c.clone()),
312 _ => None,
313 }
314}
315
316pub(crate) fn is_sequence_file(bytes: &[u8]) -> bool {
319 let Ok(Some((ftyp_payload, _))) = crate::box_parser::find_box(bytes, b"ftyp") else {
320 return false;
321 };
322 let Ok((major, _minor, compat)) = crate::parser::parse_ftyp(ftyp_payload) else {
323 return false;
324 };
325 let Ok(brands) = classify_brands(&major, &compat) else {
326 return false;
327 };
328 (brands.is_sequence || brands.has_msf1) && has_moov(bytes)
329}
330
331pub(crate) struct SequenceDecode {
333 pub signal: ColorSignal,
336 pub layout: Option<AvifPixelFormat>,
338 pub config: Av1CodecConfig,
340}
341
342pub(crate) fn decode_sequence_with<F>(
349 file: &[u8],
350 opts: &DecodeOptions,
351 limit: Option<usize>,
352 mut sink: F,
353) -> Result<SequenceDecode>
354where
355 F: FnMut(oxideav_core::Frame, &SequenceDecode, Duration) -> Result<()>,
356{
357 opts.check_bytes(file.len())?;
358 let meta = parse_avis(file)?;
359 if meta.samples.is_empty() {
360 return Err(Error::invalid("avis: track has zero samples"));
361 }
362 let av1c = meta.av1_codec_config.clone().ok_or_else(|| {
363 Error::invalid(
364 "avis: track stsd → av01 → av1C is missing — cannot seed AV1 decoder \
365 (av1-avif §2.2.1)",
366 )
367 })?;
368 let cfg = parse_av1c(&av1c).map_err(from_core_err)?;
375 validate_av1_config(&cfg).map_err(from_core_err)?;
376 if let Some((w, h)) = meta.display_dims {
377 opts.check_dims(w, h)?;
378 }
379 let timescale = if meta.timescale == 0 {
380 1
381 } else {
382 meta.timescale
383 };
384 let media_timescale = meta.media_timescale.filter(|&t| t > 0).unwrap_or(timescale);
385 let seq = SequenceDecode {
386 signal: color_signal_for(meta.colr.as_ref()),
387 layout: layout_of_record(&cfg),
388 config: cfg,
389 };
390 let mut params = CodecParameters::video(CodecId::new("av1"));
391 if let Some((w, h)) = meta.display_dims {
392 params.width = Some(w);
393 params.height = Some(h);
394 }
395 params.extradata = av1c;
396
397 let mut av1 = oxideav_av1::registry::make_decoder(¶ms).map_err(from_core_err)?;
398 if opts.worker_count() > 1 {
399 av1.set_execution_context(&oxideav_core::ExecutionContext::with_threads(
400 opts.worker_count(),
401 ));
402 }
403 let mut delivered = 0usize;
404 let done = |delivered: usize| limit.is_some_and(|n| delivered >= n);
405 let mut cumulative_pts: u64 = 0;
406 let mut last_duration = Duration::ZERO;
407 for (i, s) in meta.samples.iter().enumerate() {
408 if done(delivered) {
409 break;
410 }
411 let bytes = sample_bytes(file, s)?;
412 if bytes.len() > MAX_AV1_ITEM_BYTES {
413 return Err(Error::invalid(format!(
414 "avis: sample {i} payload {} bytes exceeds soft cap {} bytes",
415 bytes.len(),
416 MAX_AV1_ITEM_BYTES
417 )));
418 }
419 let pkt = Packet::new(0, TimeBase::new(1, timescale as i64), bytes.to_vec())
423 .with_pts(cumulative_pts as i64);
424 cumulative_pts = cumulative_pts.saturating_add(s.duration as u64);
425 last_duration = Duration::from_secs_f64(f64::from(s.duration) / f64::from(media_timescale));
426 av1.send_packet(&pkt).map_err(|e| {
427 Error::invalid(format!(
428 "avis: av1 decoder rejected sample {i} (offset={}, size={}, sync={}): {e}",
429 s.offset, s.size, s.is_sync
430 ))
431 })?;
432 loop {
437 match av1.receive_frame() {
438 Ok(frame) => {
439 sink(frame, &seq, last_duration)?;
440 delivered += 1;
441 if done(delivered) {
442 break;
443 }
444 }
445 Err(oxideav_core::Error::NeedMore) => break,
446 Err(e) => return Err(from_core_err(e)),
447 }
448 }
449 }
450 if !done(delivered) {
451 let _ = av1.flush();
455 while let Ok(frame) = av1.receive_frame() {
456 sink(frame, &seq, last_duration)?;
457 delivered += 1;
458 if done(delivered) {
459 break;
460 }
461 }
462 }
463 Ok(seq)
464}
465
466fn sequence_picture(
470 frame: oxideav_core::Frame,
471 seq: &SequenceDecode,
472 opts: &DecodeOptions,
473) -> Result<AvifImage> {
474 let vf = match frame {
475 oxideav_core::Frame::Video(v) => v,
476 other => {
477 return Err(Error::unsupported(format!(
478 "avis: AV1 decoder returned a non-video frame: {other:?}"
479 )))
480 }
481 };
482 let (fmt_core, w, h) = infer_av1_pixmap(&vf, &seq.config).map_err(from_core_err)?;
483 opts.check_dims(w, h)?;
484 let format = AvifPixelFormat::try_from(fmt_core)?;
485 let planes = core_to_avif_frame(vf).planes;
486 Ok(AvifImage::new(w, h, format, planes)?.with_color(ColorInfo::from(seq.signal)))
487}
488
489pub fn decode(bytes: &[u8]) -> Result<AvifImage> {
493 decode_with(bytes, &DecodeOptions::default())
494}
495
496pub fn decode_with(bytes: &[u8], opts: &DecodeOptions) -> Result<AvifImage> {
499 opts.check_bytes(bytes.len())?;
500 if is_sequence_file(bytes) {
501 let mut first = None;
502 decode_sequence_with(bytes, opts, Some(1), |frame, seq, _| {
503 first = Some(sequence_picture(frame, seq, opts)?);
504 Ok(())
505 })?;
506 return first.ok_or_else(|| Error::invalid("avis: the sequence decoded to no frame"));
507 }
508 decode_primary(bytes, opts).map(|d| d.image)
509}
510
511pub fn decode_rgb8(bytes: &[u8]) -> Result<RgbImage> {
513 let img = decode(bytes)?;
514 let data = img.try_to_rgb8()?;
515 Ok(RgbImage::new(img.width, img.height, data))
516}
517
518pub fn decode_rgba8(bytes: &[u8]) -> Result<RgbaImage> {
521 let img = decode(bytes)?;
522 let data = img.try_to_rgba8()?;
523 Ok(RgbaImage::new(img.width, img.height, data))
524}
525
526pub fn decode_all(bytes: &[u8]) -> Result<Vec<Frame>> {
530 decode_all_with(bytes, &DecodeOptions::default())
531}
532
533pub fn decode_all_with(bytes: &[u8], opts: &DecodeOptions) -> Result<Vec<Frame>> {
535 opts.check_bytes(bytes.len())?;
536 if is_sequence_file(bytes) {
537 let mut frames = Vec::new();
538 decode_sequence_with(bytes, opts, None, |frame, seq, delay| {
539 frames.push(Frame::new(sequence_picture(frame, seq, opts)?, Some(delay)));
540 Ok(())
541 })?;
542 return Ok(frames);
543 }
544 let decoded = decode_primary(bytes, opts)?;
545 let hdr = parse_header(bytes)?;
546 let primary_id = hdr
547 .meta
548 .primary_item_id
549 .ok_or_else(|| Error::invalid("avif: missing pitm"))?;
550 let burst = burst_members(&hdr.meta, primary_id);
551 if burst.len() < 2 {
552 return Ok(vec![Frame::new(decoded.image, None)]);
553 }
554 let mut frames = Vec::with_capacity(burst.len());
555 for id in burst {
556 let image = if id == primary_id {
557 decoded.image.clone()
558 } else {
559 picture_of(&hdr, id, opts, None, (None, None))?
560 };
561 frames.push(Frame::new(image, None));
562 }
563 Ok(frames)
564}
565
566pub fn decode_from<R: Read>(mut r: R) -> Result<AvifImage> {
569 let mut bytes = Vec::new();
570 r.read_to_end(&mut bytes)?;
571 decode(&bytes)
572}
573
574fn plane_samples(
582 plane: &AvifPlane,
583 cols: usize,
584 rows: usize,
585 bps: usize,
586 label: &str,
587) -> Result<Vec<u16>> {
588 let row_bytes = cols * bps;
589 if rows > 0
590 && (plane.stride < row_bytes || plane.data.len() < plane.stride * (rows - 1) + row_bytes)
591 {
592 return Err(Error::invalid(format!(
593 "avif encode: {label} plane too short ({} bytes, stride {}, need {rows} rows of {row_bytes})",
594 plane.data.len(),
595 plane.stride
596 )));
597 }
598 let mut out = Vec::with_capacity(cols * rows);
599 for r in 0..rows {
600 let row = &plane.data[r * plane.stride..r * plane.stride + row_bytes];
601 if bps == 1 {
602 out.extend(row.iter().map(|&b| u16::from(b)));
603 } else {
604 out.extend(
605 row.chunks_exact(2)
606 .map(|w| u16::from_le_bytes([w[0], w[1]])),
607 );
608 }
609 }
610 Ok(out)
611}
612
613fn still_of(image: &AvifImage, opts: &EncodeOptions) -> Result<StillImage> {
616 if image.width == 0 || image.height == 0 {
617 return Err(Error::invalid(
618 "avif encode: dimensions must be at least 1x1",
619 ));
620 }
621 let format = image.format;
622 if image.planes.len() != format.plane_count() {
623 return Err(Error::invalid(format!(
624 "avif encode: {format:?} needs {} planes, image carries {}",
625 format.plane_count(),
626 image.planes.len()
627 )));
628 }
629 let bit_depth = image.bit_depth;
630 if !matches!(bit_depth, 8 | 10 | 12) {
631 return Err(Error::unsupported(format!(
632 "avif encode: {bit_depth}-bit samples — AV1 codes 8, 10 or 12"
633 )));
634 }
635 if format != AvifPixelFormat::Ya16Le && format.bit_depth() != bit_depth {
636 return Err(Error::invalid(format!(
637 "avif encode: bit_depth {bit_depth} does not match the {format:?} layout"
638 )));
639 }
640 let (w, h) = (image.width as usize, image.height as usize);
641 let bps = format.bytes_per_sample();
642 let mono = format.plane_count() == 1 || format.is_packed_ya();
643 let (sx, sy) = format.chroma_subsampling();
644 let chroma = if mono {
645 StillChroma::Monochrome
646 } else {
647 match (sx, sy) {
648 (1, 1) => StillChroma::Yuv420,
649 (1, 0) => StillChroma::Yuv422,
650 _ => StillChroma::Yuv444,
651 }
652 };
653 if (sx == 1 && image.width % 2 != 0) || (sy == 1 && image.height % 2 != 0) {
654 return Err(Error::unsupported(format!(
655 "avif encode: {format:?} at {}x{} — this encoder codes sub-sampled chroma only at \
656 even extents; use 4:4:4 (or `encode_rgb8`, which falls back to it)",
657 image.width, image.height
658 )));
659 }
660 let (y, u, v, alpha) = if format.is_packed_ya() {
661 let packed = plane_samples(&image.planes[0], w * 2, h, bps, "packed YA")?;
662 let mut y = Vec::with_capacity(w * h);
663 let mut a = Vec::with_capacity(w * h);
664 for px in packed.chunks_exact(2) {
665 y.push(px[0]);
666 a.push(px[1]);
667 }
668 (y, Vec::new(), Vec::new(), Some(a))
669 } else {
670 let y = plane_samples(&image.planes[0], w, h, bps, "Y")?;
671 let (u, v) = if mono {
672 (Vec::new(), Vec::new())
673 } else {
674 let cw = w.div_ceil(1 << sx);
675 let ch = h.div_ceil(1 << sy);
676 (
677 plane_samples(&image.planes[1], cw, ch, bps, "Cb")?,
678 plane_samples(&image.planes[2], cw, ch, bps, "Cr")?,
679 )
680 };
681 let alpha = if format.has_alpha() {
682 let idx = image.planes.len() - 1;
683 Some(plane_samples(&image.planes[idx], w, h, bps, "alpha")?)
684 } else {
685 None
686 };
687 (y, u, v, alpha)
688 };
689 let mut still = StillImage::yuv(image.width, image.height, bit_depth, chroma, y, u, v)?;
690 if let Some(a) = alpha {
691 still = still.with_alpha(a)?;
692 }
693 still = still.with_colr(image.color.to_colr());
694 let mut props = StillProperties::default();
695 if opts.embed_exif {
696 props.exif = image.metadata.exif.as_deref().map(exif_item_body);
697 }
698 if opts.embed_xmp {
699 props.xmp = image.metadata.xmp.clone();
700 }
701 if opts.embed_icc {
702 props.icc = image.metadata.icc.clone();
703 }
704 Ok(still.with_props(props))
705}
706
707pub fn encode_all(frames: &[Frame], opts: &EncodeOptions) -> Result<Vec<u8>> {
725 if frames.is_empty() {
726 return Err(Error::invalid("avif encode_all: no frames"));
727 }
728 let timed = frames.iter().filter(|f| f.delay.is_some()).count();
729 if timed == 0 {
730 if let [single] = frames {
731 return encode(&single.image, opts);
732 }
733 let stills: Vec<StillImage> = frames
734 .iter()
735 .map(|f| still_of(&f.image, opts))
736 .collect::<Result<_>>()?;
737 return crate::still::encode_still_burst(&stills, opts);
738 }
739 if timed != frames.len() {
740 return Err(Error::invalid(
741 "avif encode_all: an avis image sequence holds timed samples only — give every \
742 frame a delay, or none for an image burst",
743 ));
744 }
745 let stills: Vec<StillImage> = frames
746 .iter()
747 .map(|f| still_of(&f.image, opts))
748 .collect::<Result<_>>()?;
749 let durations: Vec<u32> = frames
750 .iter()
751 .map(|f| {
752 f.delay
753 .map(|d| u32::try_from(d.as_millis()).unwrap_or(u32::MAX))
754 .unwrap_or(0)
755 })
756 .collect();
757 let seq_opts = crate::sequence::SequenceEncodeOptions::default()
758 .with_timescale(1000)
759 .with_base_q_idx(opts.base_q_idx);
760 crate::sequence::encode_sequence_timed(&stills, &seq_opts, Some(&durations))
761}
762
763pub fn encode(image: &AvifImage, opts: &EncodeOptions) -> Result<Vec<u8>> {
773 let still = still_of(image, opts)?;
774 encode_still_auto(&still, opts)
775}
776
777fn still_of_rgb(
785 width: u32,
786 height: u32,
787 data: &[u8],
788 channels: usize,
789 opts: &EncodeOptions,
790) -> Result<StillImage> {
791 still_of_rgb_in(
792 width,
793 height,
794 data,
795 channels,
796 ColorInfo::miaf_default(),
797 opts.chroma,
798 )
799}
800
801pub(crate) fn still_of_rgb_in(
806 width: u32,
807 height: u32,
808 data: &[u8],
809 channels: usize,
810 color: ColorInfo,
811 chroma: StillChroma,
812) -> Result<StillImage> {
813 let n = width as usize * height as usize;
814 if width == 0 || height == 0 {
815 return Err(Error::invalid(
816 "avif encode: dimensions must be at least 1x1",
817 ));
818 }
819 if data.len() != n * channels {
820 return Err(Error::invalid(format!(
821 "avif encode: {width}x{height} with {channels} channels needs {} bytes, got {}",
822 n * channels,
823 data.len()
824 )));
825 }
826 let colr = oxideav_heif::props::Colr::from(&color.to_colr());
827 let rgb = oxideav_heif::rgb::RgbImage16::new(
828 width,
829 height,
830 channels,
831 8,
832 data.iter().map(|&b| u16::from(b)).collect(),
833 );
834 let mut chroma = chroma;
835 let (sx, sy) = chroma.subsampling();
836 if (sx == 1 && width % 2 != 0) || (sy == 1 && height % 2 != 0) {
837 chroma = StillChroma::Yuv444;
838 }
839 let target = if chroma == StillChroma::Monochrome {
840 Chroma::Mono
841 } else {
842 Chroma::Yuv444
843 };
844 let frame = oxideav_heif::rgb::from_rgb(&rgb, Some(&colr), target)?;
845 let (w, h) = (width as usize, height as usize);
846 let heif_plane = |idx: usize| -> Result<Vec<u16>> {
847 let p = frame
848 .planes
849 .get(idx)
850 .ok_or_else(|| Error::invalid("avif encode: RGB conversion returned too few planes"))?;
851 plane_samples(
852 &AvifPlane::new(p.stride, p.data.clone()),
853 w,
854 h,
855 1,
856 "converted",
857 )
858 };
859 let y = heif_plane(0)?;
860 let (u, v) = match chroma {
861 StillChroma::Monochrome => (Vec::new(), Vec::new()),
862 StillChroma::Yuv444 => (heif_plane(1)?, heif_plane(2)?),
863 StillChroma::Yuv420 => (
864 box_filter(&heif_plane(1)?, w, h, 1, 1),
865 box_filter(&heif_plane(2)?, w, h, 1, 1),
866 ),
867 StillChroma::Yuv422 => (
868 box_filter(&heif_plane(1)?, w, h, 1, 0),
869 box_filter(&heif_plane(2)?, w, h, 1, 0),
870 ),
871 };
872 let mut still = StillImage::yuv(width, height, 8, chroma, y, u, v)?;
873 if channels == 4 {
874 let a: Vec<u16> = data.chunks_exact(4).map(|px| u16::from(px[3])).collect();
875 still = still.with_alpha(a)?;
876 }
877 Ok(still.with_colr(color.to_colr()))
878}
879
880fn box_filter(plane: &[u16], w: usize, h: usize, sx: u32, sy: u32) -> Vec<u16> {
883 let (cw, ch) = (w >> sx, h >> sy);
884 let count = 1u32 << (sx + sy);
885 let mut out = Vec::with_capacity(cw * ch);
886 for cy in 0..ch {
887 for cx in 0..cw {
888 let mut sum = 0u32;
889 for dy in 0..(1usize << sy) {
890 for dx in 0..(1usize << sx) {
891 sum += u32::from(plane[(cy * (1 << sy) + dy) * w + cx * (1 << sx) + dx]);
892 }
893 }
894 out.push(((sum + count / 2) / count) as u16);
895 }
896 }
897 out
898}
899
900pub fn encode_rgb8(width: u32, height: u32, rgb: &[u8], opts: &EncodeOptions) -> Result<Vec<u8>> {
908 let still = still_of_rgb(width, height, rgb, 3, opts)?;
909 encode_still_auto(&still, opts)
910}
911
912pub fn encode_rgba8(width: u32, height: u32, rgba: &[u8], opts: &EncodeOptions) -> Result<Vec<u8>> {
917 let still = still_of_rgb(width, height, rgba, 4, opts)?;
918 encode_still_auto(&still, opts)
919}
920
921pub fn encode_to<W: Write>(image: &AvifImage, opts: &EncodeOptions, mut w: W) -> Result<()> {
923 let bytes = encode(image, opts)?;
924 w.write_all(&bytes)?;
925 Ok(())
926}
927
928#[cfg(test)]
929mod tests {
930 use super::*;
931 use crate::api::{PixelFormat, Plane};
932 use crate::info;
933
934 fn fixture(name: &str) -> Vec<u8> {
935 std::fs::read(format!(
936 "{}/tests/fixtures/{name}",
937 env!("CARGO_MANIFEST_DIR")
938 ))
939 .unwrap()
940 }
941
942 #[test]
943 fn encode_all_mirrors_decode_all() {
944 use std::time::Duration;
945 let pic = |seed: u8, alpha: bool| -> AvifImage {
946 let n = 16 * 8;
947 if alpha {
948 let rgba: Vec<u8> = (0..n * 4)
949 .map(|i| ((i * 31 + seed as usize * 17) % 251) as u8)
950 .collect();
951 AvifImage::from_rgba8(16, 8, rgba).unwrap()
952 } else {
953 let rgb: Vec<u8> = (0..n * 3)
954 .map(|i| ((i * 29 + seed as usize * 13) % 251) as u8)
955 .collect();
956 AvifImage::from_rgb8(16, 8, rgb).unwrap()
957 }
958 };
959 let opts = EncodeOptions::default();
960
961 let one = pic(1, false);
963 assert_eq!(
964 encode_all(&[Frame::new(one.clone(), None)], &opts).unwrap(),
965 encode(&one, &opts).unwrap()
966 );
967
968 let burst = vec![
971 Frame::new(pic(1, true), None),
972 Frame::new(pic(2, true), None),
973 Frame::new(pic(3, true), None),
974 ];
975 let bytes = encode_all(&burst, &opts).unwrap();
976 assert_eq!(info(&bytes).unwrap().frames, 3);
977 assert_eq!(decode(&bytes).unwrap(), burst[0].image);
978 assert_eq!(decode_all(&bytes).unwrap(), burst);
979
980 let seq = vec![
982 Frame::new(pic(4, false), Some(Duration::from_millis(40))),
983 Frame::new(pic(5, false), Some(Duration::from_millis(1500))),
984 Frame::new(pic(6, false), Some(Duration::from_millis(1))),
985 ];
986 let bytes = encode_all(&seq, &opts).unwrap();
987 assert!(is_sequence_file(&bytes));
988 assert_eq!(info(&bytes).unwrap().frames, 3);
989 let back = decode_all(&bytes).unwrap();
990 assert_eq!(back.len(), 3);
991 for (i, (got, want)) in back.iter().zip(&seq).enumerate() {
992 assert_eq!(got.delay, want.delay, "sample {i} delay");
993 assert_eq!(got.image.planes, want.image.planes, "sample {i} planes");
994 assert_eq!(got.image.format, want.image.format, "sample {i} layout");
995 assert_eq!(got.image.color, want.image.color, "sample {i} colour");
996 }
997
998 assert!(matches!(encode_all(&[], &opts), Err(Error::InvalidData(_))));
1000 let mixed = [seq[0].clone(), burst[0].clone()];
1001 assert!(matches!(
1002 encode_all(&mixed, &opts),
1003 Err(Error::InvalidData(_))
1004 ));
1005 let timed_alpha = [
1006 Frame::new(pic(1, true), Some(Duration::from_millis(10))),
1007 Frame::new(pic(2, true), Some(Duration::from_millis(10))),
1008 ];
1009 assert!(matches!(
1010 encode_all(&timed_alpha, &opts),
1011 Err(Error::Unsupported(_))
1012 ));
1013 assert!(matches!(
1015 AvifImage::from_rgb8(2, 2, vec![0; 11]),
1016 Err(Error::InvalidData(_))
1017 ));
1018 assert!(matches!(
1019 AvifImage::from_rgba8(0, 2, vec![]),
1020 Err(Error::InvalidData(_))
1021 ));
1022 }
1023
1024 #[test]
1025 fn identity_rgb_fixture_decodes_to_the_pinned_bytes() {
1026 let bytes = fixture("identity_rgb_lossless.avif");
1027 let expect = fixture("identity_rgb_lossless.rgb");
1028 let img = decode(&bytes).unwrap();
1029 assert_eq!(img.format(), PixelFormat::Yuv444P);
1030 assert!(img.color.is_identity_matrix());
1031 assert_eq!(img.to_rgb8(), expect);
1032 let rgb = decode_rgb8(&bytes).unwrap();
1033 assert_eq!((rgb.width, rgb.height), (img.width, img.height));
1034 assert_eq!(rgb.data, expect);
1035 let rgba = decode_rgba8(&bytes).unwrap();
1036 assert_eq!(rgba.data.len(), expect.len() / 3 * 4);
1037 assert!(rgba.data.chunks_exact(4).all(|p| p[3] == 255));
1038 let from_reader = decode_from(std::io::Cursor::new(&bytes)).unwrap();
1039 assert_eq!(from_reader, img);
1040 }
1041
1042 #[test]
1043 fn lossless_round_trip_is_exact_for_planes_colour_and_metadata() {
1044 let rgba: Vec<u8> = (0..16 * 8 * 4).map(|i| (i * 31 % 251) as u8).collect();
1045 let img = AvifImage::from_rgba8(16, 8, rgba.clone())
1046 .unwrap()
1047 .with_metadata(Metadata::new(
1048 Some(vec![0x11; 8]),
1049 Some(b"MM\0\x2a\0\0\0\x08".to_vec()),
1050 Some(b"<x:xmpmeta/>".to_vec()),
1051 None,
1052 ));
1053 let bytes = encode(&img, &EncodeOptions::default()).unwrap();
1054 assert!(crate::probe(&bytes));
1055 let info = crate::info(&bytes).unwrap();
1056 assert!(info.has_alpha && info.has_exif && info.has_xmp && info.has_icc);
1057 assert_eq!(info.format, PixelFormat::Yuva444P);
1058 assert!(info.color.is_identity_matrix());
1059 let back = decode(&bytes).unwrap();
1060 assert_eq!(back.width, 16);
1061 assert_eq!(back.height, 8);
1062 assert_eq!(back.format, img.format);
1063 assert_eq!(back.planes, img.planes);
1064 assert_eq!(back.color, img.color);
1065 assert_eq!(back.metadata, img.metadata);
1066 assert_eq!(back.to_rgba8(), rgba);
1067 let bare = encode(
1069 &img,
1070 &EncodeOptions::default()
1071 .with_embed_exif(false)
1072 .with_embed_xmp(false)
1073 .with_embed_icc(false),
1074 )
1075 .unwrap();
1076 let info = crate::info(&bare).unwrap();
1077 assert!(!info.has_exif && !info.has_xmp && !info.has_icc);
1078 assert!(decode(&bare).unwrap().metadata.is_empty());
1079 let mut sink = Vec::new();
1080 encode_to(&img, &EncodeOptions::default(), &mut sink).unwrap();
1081 assert_eq!(sink, bytes);
1082 }
1083
1084 #[test]
1085 fn rgb_one_call_paths_code_420_by_default() {
1086 let rgb: Vec<u8> = (0..16 * 8 * 3).map(|i| (i * 7 % 256) as u8).collect();
1087 let bytes = encode_rgb8(16, 8, &rgb, &EncodeOptions::default()).unwrap();
1088 let info = crate::info(&bytes).unwrap();
1089 assert_eq!(info.format, PixelFormat::Yuv420P);
1090 assert_eq!(info.color, ColorInfo::miaf_default());
1091 let back = decode_rgb8(&bytes).unwrap();
1092 assert_eq!((back.width, back.height), (16, 8));
1093 let err: u32 = back
1095 .data
1096 .iter()
1097 .zip(&rgb)
1098 .map(|(a, b)| u32::from(a.abs_diff(*b)))
1099 .sum();
1100 assert!(err / (16 * 8 * 3) < 24, "mean error {}", err / (16 * 8 * 3));
1101 let odd: Vec<u8> = (0..5 * 3 * 3).map(|i| (i * 11 % 256) as u8).collect();
1104 let bytes = encode_rgb8(5, 3, &odd, &EncodeOptions::default()).unwrap();
1105 assert_eq!(crate::info(&bytes).unwrap().format, PixelFormat::Yuv444P);
1106 let back = decode_rgb8(&bytes).unwrap();
1107 assert!(back.data.iter().zip(&odd).all(|(a, b)| a.abs_diff(*b) <= 2));
1108 let rgba: Vec<u8> = (0..8 * 8 * 4).map(|i| (i * 13 % 256) as u8).collect();
1110 let bytes = encode_rgba8(8, 8, &rgba, &EncodeOptions::default()).unwrap();
1111 let info = crate::info(&bytes).unwrap();
1112 assert!(info.has_alpha);
1113 assert_eq!(info.format, PixelFormat::Yuva420P);
1114 let back = decode_rgba8(&bytes).unwrap();
1115 assert!(back
1116 .data
1117 .chunks_exact(4)
1118 .zip(rgba.chunks_exact(4))
1119 .all(|(a, b)| a[3] == b[3]));
1120 let lossy = encode_rgb8(16, 8, &rgb, &EncodeOptions::default().with_quality(50)).unwrap();
1122 assert_ne!(
1123 lossy,
1124 encode_rgb8(16, 8, &rgb, &EncodeOptions::default()).unwrap()
1125 );
1126 assert!(decode(&lossy).is_ok());
1127 assert!(matches!(
1129 encode_rgb8(4, 4, &[0; 10], &EncodeOptions::default()),
1130 Err(Error::InvalidData(_))
1131 ));
1132 }
1133
1134 #[test]
1135 fn encode_refuses_unsupported_layouts_without_converting() {
1136 let img = AvifImage::new(
1138 3,
1139 3,
1140 PixelFormat::Yuv420P,
1141 vec![
1142 Plane::new(3, vec![0; 9]),
1143 Plane::new(2, vec![0; 4]),
1144 Plane::new(2, vec![0; 4]),
1145 ],
1146 )
1147 .unwrap();
1148 assert!(matches!(
1149 encode(&img, &EncodeOptions::default()),
1150 Err(Error::Unsupported(_))
1151 ));
1152 let mut img =
1154 AvifImage::new(4, 4, PixelFormat::Gray8, vec![Plane::new(4, vec![0; 16])]).unwrap();
1155 img.format = PixelFormat::Yuv420P;
1156 assert!(matches!(
1157 encode(&img, &EncodeOptions::default()),
1158 Err(Error::InvalidData(_))
1159 ));
1160 }
1161
1162 #[test]
1163 fn deep_gray_and_packed_ya_round_trip() {
1164 let words: Vec<u16> = (0..8 * 4).map(|i| (i * 33 % 1024) as u16).collect();
1166 let alpha: Vec<u16> = (0..8 * 4).map(|i| (1023 - i * 29 % 1024) as u16).collect();
1167 let still = StillImage::yuv(
1168 8,
1169 4,
1170 10,
1171 StillChroma::Monochrome,
1172 words.clone(),
1173 vec![],
1174 vec![],
1175 )
1176 .unwrap()
1177 .with_alpha(alpha.clone())
1178 .unwrap();
1179 let bytes = crate::still::encode_still(&still, &EncodeOptions::default()).unwrap();
1180 let img = decode(&bytes).unwrap();
1181 assert_eq!(img.format, PixelFormat::Ya16Le);
1182 assert_eq!(img.bit_depth, 10);
1183 assert_eq!(crate::info(&bytes).unwrap().format, PixelFormat::Ya16Le);
1184 let again = encode(&img, &EncodeOptions::default()).unwrap();
1185 let back = decode(&again).unwrap();
1186 assert_eq!(back.planes, img.planes);
1187 assert_eq!(back.bit_depth, 10);
1188 let rgba = img.to_rgba8();
1190 assert_eq!(rgba.len(), 8 * 4 * 4);
1191 assert_eq!(rgba[0], ((u32::from(words[0]) * 255 + 511) / 1023) as u8);
1192 assert_eq!(rgba[3], ((u32::from(alpha[0]) * 255 + 511) / 1023) as u8);
1193 }
1194
1195 #[test]
1196 fn sequence_decodes_every_sample_with_delays() {
1197 let bytes = fixture("alpha_video.avif");
1198 let info = crate::info(&bytes).unwrap();
1199 assert!(info.is_sequence);
1200 let frames = decode_all(&bytes).unwrap();
1201 assert_eq!(frames.len(), info.frames as usize);
1202 assert!(frames
1203 .iter()
1204 .all(|f| f.delay.is_some_and(|d| d > Duration::ZERO)));
1205 assert!(frames
1206 .iter()
1207 .all(|f| f.image.width == frames[0].image.width));
1208 let first = decode(&bytes).unwrap();
1209 assert_eq!(first, frames[0].image);
1210 let rgb = first.to_rgb8();
1211 assert_eq!(rgb.len(), first.width as usize * first.height as usize * 3);
1212 }
1213
1214 #[test]
1215 fn stills_decode_all_as_one_frame() {
1216 let bytes = fixture("kimono_rotate90.avif");
1217 let frames = decode_all(&bytes).unwrap();
1218 assert_eq!(frames.len(), 1);
1219 assert_eq!(frames[0].delay, None);
1220 assert_eq!(frames[0].image, decode(&bytes).unwrap());
1221 }
1222
1223 #[test]
1224 fn limits_and_strictness_refuse_before_decoding() {
1225 let bytes = fixture("monochrome.avif");
1226 let info = crate::info(&bytes).unwrap();
1227 let too_small = DecodeOptions::default().with_max_width(Some(info.width - 1));
1228 assert!(matches!(
1229 decode_with(&bytes, &too_small),
1230 Err(Error::LimitExceeded(_))
1231 ));
1232 let few_pixels = DecodeOptions::default().with_max_pixels(Some(16));
1233 assert!(matches!(
1234 decode_with(&bytes, &few_pixels),
1235 Err(Error::LimitExceeded(_))
1236 ));
1237 let few_bytes = DecodeOptions::default().with_max_bytes(Some(bytes.len() as u64 - 1));
1238 assert!(matches!(
1239 decode_with(&bytes, &few_bytes),
1240 Err(Error::LimitExceeded(_))
1241 ));
1242 assert!(matches!(
1243 decode_all_with(&bytes, &few_bytes),
1244 Err(Error::LimitExceeded(_))
1245 ));
1246 let strict = decode_with(&bytes, &DecodeOptions::default().with_strict(true)).unwrap();
1248 assert_eq!(strict, decode(&bytes).unwrap());
1249 let mut structural = bytes.clone();
1252 let pos = structural
1253 .windows(4)
1254 .position(|w| w == b"avif")
1255 .expect("brand in ftyp");
1256 structural[pos..pos + 4].copy_from_slice(b"mif1");
1257 while let Some(p) = structural[..64].windows(4).position(|w| w == b"avif") {
1258 structural[p..p + 4].copy_from_slice(b"mif1");
1259 }
1260 assert!(decode(&structural).is_ok());
1261 assert!(matches!(
1262 decode_with(&structural, &DecodeOptions::default().with_strict(true)),
1263 Err(Error::InvalidData(_))
1264 ));
1265 }
1266
1267 #[test]
1268 fn framework_frame_conversions_round_trip() {
1269 let img = AvifImage::from_rgba8(4, 2, (0..32).collect())
1270 .unwrap()
1271 .with_bit_depth(8);
1272 let vf = VideoFrame::from(img.clone());
1273 assert_eq!(vf.image_plane_count(), 4);
1276 assert_eq!(
1277 vf.color_signal(),
1278 Some(color_signal_for(Some(&img.color.to_colr())))
1279 );
1280 let mut params = CodecParameters::video(CodecId::new(crate::CODEC_ID_STR));
1281 params.width = Some(4);
1282 params.height = Some(2);
1283 params.pixel_format = Some(oxideav_core::PixelFormat::Yuva444P);
1284 let back = AvifImage::from_video_frame(&vf, ¶ms).unwrap();
1285 assert_eq!(back, img);
1286 params.pixel_format = Some(oxideav_core::PixelFormat::Gbrap8);
1288 let back = AvifImage::try_from((&vf, ¶ms)).unwrap();
1289 assert_eq!(back, img);
1290 params.width = None;
1292 assert!(AvifImage::from_video_frame(&vf, ¶ms).is_err());
1293 let ya = AvifImage::new(2, 1, PixelFormat::Ya16Le, vec![Plane::new(8, vec![0; 8])])
1295 .unwrap()
1296 .with_bit_depth(12);
1297 let vf = VideoFrame::from(ya.clone());
1298 assert_eq!(vf.significant_bits(), Some(&[12u8][..]));
1299 let mut params = CodecParameters::video(CodecId::new(crate::CODEC_ID_STR));
1300 params.width = Some(2);
1301 params.height = Some(1);
1302 params.pixel_format = Some(oxideav_core::PixelFormat::Ya16Le);
1303 assert_eq!(
1304 AvifImage::from_video_frame(&vf, ¶ms).unwrap().bit_depth,
1305 12
1306 );
1307 }
1308}