1#[cfg(windows)]
4use std::sync::OnceLock;
5use std::{ffi::CStr, fs::File, io::Write, mem, path::Path, ptr, slice};
6
7use libwebp_sys as ffi;
8use rayon::prelude::*;
9
10use crate::{
11 error::{Error, Result},
12 renders::Renders,
13};
14
15pub const DEFAULT_WEBP_QUALITY: f32 = 95.0;
17
18pub const DEFAULT_WEBP_METHOD: usize = 4;
20
21const MAX_WEBP_DIMENSION: u32 = 16_383;
22const MAX_WEBP_FRAME_DURATION_MS: i64 = (1 << 24) - 1;
23const PARALLEL_ENCODER_MEMORY_BUDGET: usize = 256 * 1024 * 1024;
24const ESTIMATED_NATIVE_BYTES_PER_PIXEL: usize = 16;
25
26#[cfg(windows)]
30static LIBWEBP_INITIALIZED: OnceLock<std::result::Result<(), String>> = OnceLock::new();
31
32#[derive(Debug, Clone, Copy, PartialEq)]
34pub struct WebpOptions {
35 pub quality: f32,
37 pub lossless: bool,
39 pub method: usize,
41}
42
43impl Default for WebpOptions {
44 fn default() -> Self {
45 Self { quality: DEFAULT_WEBP_QUALITY, lossless: false, method: DEFAULT_WEBP_METHOD }
46 }
47}
48
49#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
51pub struct WebpAnimationOptions {
52 pub loop_count: u16,
54 pub minimize_size: bool,
56 pub kmin: i32,
58 pub kmax: i32,
60 pub allow_mixed: bool,
62}
63
64#[derive(Debug, Clone, Copy)]
66pub struct WebpFrame<'a> {
67 pub rgba: &'a [u8],
69 pub timestamp_ms: i32,
71}
72
73impl<'a> WebpFrame<'a> {
74 pub fn new(rgba: &'a [u8], timestamp_ms: i32) -> Self {
75 Self { rgba, timestamp_ms }
76 }
77}
78
79pub fn encode_webp_animation(
88 dimensions: (u32, u32),
89 frames: &[WebpFrame<'_>],
90 final_timestamp_ms: i32,
91 options: WebpOptions,
92 loop_count: u16,
93) -> Result<Vec<u8>> {
94 let expected_frame_bytes = frame_bytes(dimensions)?;
95 let options = normalize_options(options)?;
96 let durations = validate_frames(frames, final_timestamp_ms, expected_frame_bytes)?;
97 initialize_libwebp()?;
98 let (width, height) = (dimensions.0 as usize, dimensions.1 as usize);
99
100 let rectangles: Vec<_> = frames
101 .par_iter()
102 .enumerate()
103 .map(|(index, frame)| {
104 if index == 0 {
105 Some(FrameRect::full(width, height))
106 } else {
107 dirty_rect(frames[index - 1].rgba, frame.rgba, width)
108 }
109 })
110 .collect();
111
112 let mut plans: Vec<FramePlan<'_>> = Vec::with_capacity(frames.len());
113 for ((frame, rectangle), duration_ms) in
114 frames.iter().zip(rectangles).zip(durations.into_iter())
115 {
116 if let Some(rectangle) = rectangle {
117 plans.push(FramePlan { rgba: frame.rgba, rectangle, duration_ms });
118 } else {
119 let previous = plans.last_mut().expect("the first frame is always present");
120 if previous.duration_ms <= MAX_WEBP_FRAME_DURATION_MS - duration_ms {
121 previous.duration_ms += duration_ms;
122 } else {
123 plans.push(FramePlan {
124 rgba: frame.rgba,
125 rectangle: FrameRect::full(width, height),
126 duration_ms,
127 });
128 }
129 }
130 }
131
132 let mut encoded = Vec::with_capacity(plans.len());
133 let mut remaining = plans.as_slice();
134 while !remaining.is_empty() {
135 let chunk_len = parallel_chunk_len(remaining);
136 let (chunk, rest) = remaining.split_at(chunk_len);
137 let encoded_chunk: Result<Vec<_>> =
138 chunk.par_iter().map(|plan| encode_frame_rect(plan, width, options)).collect();
139 encoded.extend(encoded_chunk?);
140 remaining = rest;
141 }
142 mux_frames(
143 dimensions,
144 &plans,
145 &encoded,
146 WebpAnimationOptions { loop_count, ..Default::default() },
147 )
148}
149
150#[derive(Debug, Clone, Copy)]
151struct FrameRect {
152 x: usize,
153 y: usize,
154 width: usize,
155 height: usize,
156}
157
158impl FrameRect {
159 fn full(width: usize, height: usize) -> Self {
160 Self { x: 0, y: 0, width, height }
161 }
162}
163
164#[derive(Debug, Clone, Copy)]
165struct FramePlan<'a> {
166 rgba: &'a [u8],
167 rectangle: FrameRect,
168 duration_ms: i64,
169}
170
171fn parallel_chunk_len(plans: &[FramePlan<'_>]) -> usize {
172 parallel_chunk_len_for(plans, rayon::current_num_threads())
173}
174
175fn parallel_chunk_len_for(plans: &[FramePlan<'_>], available_threads: usize) -> usize {
176 let mut estimated_bytes = 0usize;
177 let mut chunk_len = 0;
178 for plan in plans.iter().take(available_threads.max(1)) {
179 let plan_bytes = plan
180 .rectangle
181 .width
182 .saturating_mul(plan.rectangle.height)
183 .saturating_mul(ESTIMATED_NATIVE_BYTES_PER_PIXEL)
184 .max(1);
185 if chunk_len > 0
186 && estimated_bytes.saturating_add(plan_bytes) > PARALLEL_ENCODER_MEMORY_BUDGET
187 {
188 break;
189 }
190 estimated_bytes = estimated_bytes.saturating_add(plan_bytes);
191 chunk_len += 1;
192 }
193 chunk_len.max(1)
194}
195
196fn validate_frames(
197 frames: &[WebpFrame<'_>],
198 final_timestamp_ms: i32,
199 expected_frame_bytes: usize,
200) -> Result<Vec<i64>> {
201 if frames.is_empty() {
202 return Err(webp_error("no frames were provided"));
203 }
204 for frame in frames {
205 if frame.rgba.len() != expected_frame_bytes {
206 return Err(Error::WebpEncode(format!(
207 "RGBA frame has {} bytes; expected {}",
208 frame.rgba.len(),
209 expected_frame_bytes
210 )));
211 }
212 }
213 for timestamps in frames.windows(2) {
214 if timestamps[1].timestamp_ms <= timestamps[0].timestamp_ms {
215 return Err(webp_error("frame timestamps must be strictly increasing"));
216 }
217 }
218 if final_timestamp_ms < frames.last().unwrap().timestamp_ms {
219 return Err(webp_error("final timestamp must not precede the last frame"));
220 }
221
222 frames
223 .iter()
224 .enumerate()
225 .map(|(index, frame)| {
226 let next_timestamp =
227 frames.get(index + 1).map_or(final_timestamp_ms, |next| next.timestamp_ms);
228 let duration = i64::from(next_timestamp) - i64::from(frame.timestamp_ms);
229 if duration > MAX_WEBP_FRAME_DURATION_MS {
230 Err(webp_error("frame duration exceeds WebP's limit"))
231 } else {
232 Ok(duration)
233 }
234 })
235 .collect()
236}
237
238fn dirty_rect(previous: &[u8], current: &[u8], width: usize) -> Option<FrameRect> {
239 let row_bytes = width * 4;
240 let mut rows = previous.chunks_exact(row_bytes).zip(current.chunks_exact(row_bytes));
241 let top = rows.clone().position(|(before, after)| before != after)?;
242 let bottom = rows.rposition(|(before, after)| before != after).unwrap();
243 let mut left = width;
244 let mut right = 0;
245
246 for y in top..=bottom {
247 let start = y * row_bytes;
248 let before = &previous[start..start + row_bytes];
249 let after = ¤t[start..start + row_bytes];
250 if before == after {
251 continue;
252 }
253 let first_byte = before.iter().zip(after).position(|(a, b)| a != b).unwrap();
254 let last_byte = before.iter().zip(after).rposition(|(a, b)| a != b).unwrap();
255 left = left.min(first_byte / 4);
256 right = right.max(last_byte / 4 + 1);
257 }
258
259 let x = left & !1;
260 let y = top & !1;
261 Some(FrameRect { x, y, width: right - x, height: bottom + 1 - y })
262}
263
264unsafe extern "C" fn write_webp_memory(
265 data: *const u8,
266 size: usize,
267 picture: *const ffi::WebPPicture,
268) -> i32 {
269 unsafe { ffi::WebPMemoryWrite(data, size, picture) }
272}
273
274fn encode_frame_rect(
275 plan: &FramePlan<'_>,
276 canvas_width: usize,
277 options: WebpOptions,
278) -> Result<Vec<u8>> {
279 let rect = plan.rectangle;
280 unsafe {
283 let mut config = mem::zeroed();
284 if ffi::WebPConfigInitInternal(
285 &mut config,
286 ffi::WebPPreset::WEBP_PRESET_DEFAULT,
287 75.0,
288 ffi::WEBP_ENCODER_ABI_VERSION as i32,
289 ) == 0
290 {
291 return Err(webp_error("could not initialize frame options"));
292 }
293 config.lossless = i32::from(options.lossless);
294 config.quality = options.quality;
295 config.method = options.method as i32;
296 config.exact = i32::from(options.lossless);
297 if ffi::WebPValidateConfig(&config) == 0 {
298 return Err(webp_error("libwebp rejected the frame options"));
299 }
300
301 let mut picture = mem::zeroed();
302 if ffi::WebPPictureInitInternal(&mut picture, ffi::WEBP_ENCODER_ABI_VERSION as i32) == 0 {
303 return Err(webp_error("could not initialize a frame"));
304 }
305 picture.width = rect.width as i32;
306 picture.height = rect.height as i32;
307 picture.use_argb = 1;
308 let offset = (rect.y * canvas_width + rect.x) * 4;
309 if ffi::WebPPictureImportRGBA(
310 &mut picture,
311 plan.rgba.as_ptr().add(offset),
312 (canvas_width * 4) as i32,
313 ) == 0
314 {
315 ffi::WebPPictureFree(&mut picture);
316 return Err(webp_error("could not import an RGBA frame"));
317 }
318
319 let mut writer = mem::zeroed();
320 ffi::WebPMemoryWriterInit(&mut writer);
321 picture.writer = Some(write_webp_memory);
322 picture.custom_ptr = (&mut writer as *mut ffi::WebPMemoryWriter).cast();
323 let encoded = ffi::WebPEncode(&config, &mut picture) != 0;
324 let output = if encoded && !writer.mem.is_null() {
325 slice::from_raw_parts(writer.mem, writer.size).to_vec()
326 } else {
327 Vec::new()
328 };
329 ffi::WebPMemoryWriterClear(&mut writer);
330 ffi::WebPPictureFree(&mut picture);
331
332 if !encoded || output.is_empty() {
333 Err(webp_error("could not encode a frame rectangle"))
334 } else {
335 Ok(output)
336 }
337 }
338}
339
340#[cfg(not(windows))]
341fn initialize_libwebp() -> Result<()> {
342 Ok(())
343}
344
345#[cfg(windows)]
346fn initialize_libwebp() -> Result<()> {
347 LIBWEBP_INITIALIZED
348 .get_or_init(|| initialize_libwebp_inner().map_err(|error| error.to_string()))
349 .as_ref()
350 .map_err(|message| Error::WebpEncode(message.clone()))
351 .copied()
352}
353
354#[cfg(windows)]
355fn initialize_libwebp_inner() -> Result<()> {
356 let rgba = [255, 0, 0, 255, 0, 255, 0, 128, 0, 0, 255, 0, 255, 255, 255, 255];
357 let plan = FramePlan { rgba: &rgba, rectangle: FrameRect::full(2, 2), duration_ms: 1 };
358
359 for lossless in [false, true] {
360 let encoded =
361 encode_frame_rect(&plan, 2, WebpOptions { lossless, ..WebpOptions::default() })?;
362 let mut width = 0;
363 let mut height = 0;
364 let decoded = unsafe {
367 ffi::WebPDecodeRGBA(encoded.as_ptr(), encoded.len(), &mut width, &mut height)
368 };
369 if decoded.is_null() {
370 return Err(webp_error("could not initialize libwebp's decoder"));
371 }
372 unsafe { ffi::WebPFree(decoded.cast()) };
373 }
374 Ok(())
375}
376
377fn mux_frames(
378 dimensions: (u32, u32),
379 plans: &[FramePlan<'_>],
380 encoded: &[Vec<u8>],
381 animation: WebpAnimationOptions,
382) -> Result<Vec<u8>> {
383 unsafe {
387 let mux = ffi::WebPMuxNew();
388 if mux.is_null() {
389 return Err(webp_error("could not create WebP muxer"));
390 }
391 let result = (|| {
392 let canvas_status =
393 ffi::WebPMuxSetCanvasSize(mux, dimensions.0 as i32, dimensions.1 as i32);
394 if canvas_status != ffi::WebPMuxError::WEBP_MUX_OK {
395 return Err(mux_error("could not set animation canvas", canvas_status));
396 }
397 let params = ffi::WebPMuxAnimParams {
398 bgcolor: 0xffff_ffff,
399 loop_count: i32::from(animation.loop_count),
400 };
401 let params_status = ffi::WebPMuxSetAnimationParams(mux, ¶ms);
402 if params_status != ffi::WebPMuxError::WEBP_MUX_OK {
403 return Err(mux_error("could not set animation options", params_status));
404 }
405
406 for (plan, bitstream) in plans.iter().zip(encoded) {
407 let frame = ffi::WebPMuxFrameInfo {
408 bitstream: ffi::WebPData { bytes: bitstream.as_ptr(), size: bitstream.len() },
409 x_offset: plan.rectangle.x as i32,
410 y_offset: plan.rectangle.y as i32,
411 duration: plan.duration_ms as i32,
412 id: ffi::WebPChunkId::WEBP_CHUNK_ANMF,
413 dispose_method: ffi::WebPMuxAnimDispose::WEBP_MUX_DISPOSE_NONE,
414 blend_method: ffi::WebPMuxAnimBlend::WEBP_MUX_NO_BLEND,
415 pad: [0],
416 };
417 let frame_status = ffi::WebPMuxPushFrame(mux, &frame, 0);
418 if frame_status != ffi::WebPMuxError::WEBP_MUX_OK {
419 return Err(mux_error("could not add animation frame", frame_status));
420 }
421 }
422
423 let mut data = mem::zeroed();
424 ffi::WebPDataInit(&mut data);
425 let assemble_status = ffi::WebPMuxAssemble(mux, &mut data);
426 let output =
427 if assemble_status == ffi::WebPMuxError::WEBP_MUX_OK && !data.bytes.is_null() {
428 slice::from_raw_parts(data.bytes, data.size).to_vec()
429 } else {
430 Vec::new()
431 };
432 ffi::WebPDataClear(&mut data);
433 if assemble_status != ffi::WebPMuxError::WEBP_MUX_OK {
434 Err(mux_error("could not assemble animation", assemble_status))
435 } else if output.is_empty() {
436 Err(webp_error("muxer returned an empty file"))
437 } else {
438 Ok(output)
439 }
440 })();
441 ffi::WebPMuxDelete(mux);
442 result
443 }
444}
445
446fn mux_error(message: &str, status: ffi::WebPMuxError) -> Error {
447 Error::WebpEncode(format!("{message} (libwebp mux status {status:?})"))
448}
449
450pub struct WebpEncoder {
456 raw: *mut ffi::WebPAnimEncoder,
457 picture: ffi::WebPPicture,
458 config: ffi::WebPConfig,
459 expected_frame_bytes: usize,
460 stride: i32,
461 previous_timestamp: Option<i32>,
462 failed: bool,
463}
464
465impl WebpEncoder {
466 pub fn new(dimensions: (u32, u32), options: WebpOptions) -> Result<Self> {
468 Self::new_with_animation_options(dimensions, options, WebpAnimationOptions::default())
469 }
470
471 pub fn new_with_animation_options(
473 dimensions: (u32, u32),
474 options: WebpOptions,
475 animation: WebpAnimationOptions,
476 ) -> Result<Self> {
477 let expected_frame_bytes = frame_bytes(dimensions)?;
478 let options = normalize_options(options)?;
479 validate_animation_options(animation)?;
480 initialize_libwebp()?;
481 let (width, height) = (dimensions.0 as i32, dimensions.1 as i32);
482
483 unsafe {
486 let mut animation_config = mem::zeroed();
487 if ffi::WebPAnimEncoderOptionsInitInternal(
488 &mut animation_config,
489 ffi::WEBP_MUX_ABI_VERSION as i32,
490 ) == 0
491 {
492 return Err(webp_error("could not initialize animation options"));
493 }
494 animation_config.anim_params.loop_count = i32::from(animation.loop_count);
495 animation_config.minimize_size = i32::from(animation.minimize_size);
496 animation_config.kmin = animation.kmin;
497 animation_config.kmax = animation.kmax;
498 animation_config.allow_mixed = i32::from(animation.allow_mixed);
499
500 let raw = ffi::WebPAnimEncoderNewInternal(
501 width,
502 height,
503 &animation_config,
504 ffi::WEBP_MUX_ABI_VERSION as i32,
505 );
506 if raw.is_null() {
507 return Err(webp_error("could not create animation encoder"));
508 }
509
510 let mut config = mem::zeroed();
511 if ffi::WebPConfigInitInternal(
512 &mut config,
513 ffi::WebPPreset::WEBP_PRESET_DEFAULT,
514 75.0,
515 ffi::WEBP_ENCODER_ABI_VERSION as i32,
516 ) == 0
517 {
518 ffi::WebPAnimEncoderDelete(raw);
519 return Err(webp_error("could not initialize frame options"));
520 }
521 config.lossless = i32::from(options.lossless);
522 config.quality = options.quality;
523 config.method = options.method as i32;
524 config.exact = i32::from(options.lossless);
525 if ffi::WebPValidateConfig(&config) == 0 {
526 ffi::WebPAnimEncoderDelete(raw);
527 return Err(webp_error("libwebp rejected the frame options"));
528 }
529
530 let mut picture = mem::zeroed();
531 if ffi::WebPPictureInitInternal(&mut picture, ffi::WEBP_ENCODER_ABI_VERSION as i32) == 0
532 {
533 ffi::WebPAnimEncoderDelete(raw);
534 return Err(webp_error("could not initialize a frame"));
535 }
536 picture.width = width;
537 picture.height = height;
538 picture.use_argb = 1;
539
540 Ok(Self {
541 raw,
542 picture,
543 config,
544 expected_frame_bytes,
545 stride: width * 4,
546 previous_timestamp: None,
547 failed: false,
548 })
549 }
550 }
551
552 pub fn add_frame(&mut self, rgba: &[u8], timestamp_ms: i32) -> Result<()> {
554 if self.failed {
555 return Err(webp_error("encoder cannot be reused after a native failure"));
556 }
557 if rgba.len() != self.expected_frame_bytes {
558 return Err(Error::WebpEncode(format!(
559 "RGBA frame has {} bytes; expected {}",
560 rgba.len(),
561 self.expected_frame_bytes
562 )));
563 }
564 if let Some(previous) = self.previous_timestamp {
565 if validate_frame_duration(previous, timestamp_ms)? == 0 {
566 return Err(webp_error("frame timestamps must be strictly increasing"));
567 }
568 }
569
570 if unsafe { ffi::WebPPictureImportRGBA(&mut self.picture, rgba.as_ptr(), self.stride) } == 0
574 {
575 self.failed = true;
576 return Err(webp_error("could not import an RGBA frame"));
577 }
578 if unsafe {
579 ffi::WebPAnimEncoderAdd(self.raw, &mut self.picture, timestamp_ms, &self.config)
580 } == 0
581 {
582 let error = self.encoder_error("could not encode frame");
583 self.failed = true;
584 return Err(Error::WebpEncode(error));
585 }
586 self.previous_timestamp = Some(timestamp_ms);
587 Ok(())
588 }
589
590 pub fn finish(self, final_timestamp_ms: i32) -> Result<Vec<u8>> {
592 if self.failed {
593 return Err(webp_error("encoder cannot be finalized after a native failure"));
594 }
595 let Some(previous_timestamp) = self.previous_timestamp else {
596 return Err(webp_error("no frames were added"));
597 };
598 validate_frame_duration(previous_timestamp, final_timestamp_ms)?;
599
600 if unsafe {
603 ffi::WebPAnimEncoderAdd(self.raw, ptr::null_mut(), final_timestamp_ms, ptr::null())
604 } == 0
605 {
606 return Err(Error::WebpEncode(self.encoder_error("could not finalize timeline")));
607 }
608
609 let mut data = unsafe {
612 let mut data = mem::zeroed();
613 ffi::WebPDataInit(&mut data);
614 data
615 };
616 if unsafe { ffi::WebPAnimEncoderAssemble(self.raw, &mut data) } == 0 {
617 let error = self.encoder_error("could not assemble animation");
618 unsafe { ffi::WebPDataClear(&mut data) };
619 return Err(Error::WebpEncode(error));
620 }
621 let output = if data.bytes.is_null() {
622 Vec::new()
623 } else {
624 unsafe { slice::from_raw_parts(data.bytes, data.size) }.to_vec()
625 };
626 unsafe { ffi::WebPDataClear(&mut data) };
627 if output.is_empty() {
628 return Err(webp_error("encoder returned an empty file"));
629 }
630
631 Ok(output)
632 }
633
634 fn encoder_error(&self, fallback: &str) -> String {
635 let message = unsafe { ffi::WebPAnimEncoderGetError(self.raw) };
636 if message.is_null() {
637 fallback.to_string()
638 } else {
639 unsafe { CStr::from_ptr(message) }.to_string_lossy().into_owned()
640 }
641 }
642}
643
644impl Drop for WebpEncoder {
645 fn drop(&mut self) {
646 unsafe {
649 ffi::WebPPictureFree(&mut self.picture);
650 ffi::WebPAnimEncoderDelete(self.raw);
651 }
652 }
653}
654
655pub struct WebpAnim {
656 renders: Renders,
657 period: f64,
658 hold: f64,
659 first_frame: i32,
660 options: WebpOptions,
661}
662
663impl WebpAnim {
664 pub fn new(renders: Renders) -> Self {
665 Self { renders, period: 0.1, hold: 5.0, first_frame: -1, options: WebpOptions::default() }
666 }
667
668 pub fn set_first_frame(&mut self, index: i32) {
669 self.first_frame = index;
670 }
671
672 pub fn set_timing(&mut self, period: f64, hold: f64) {
673 self.period = period;
674 self.hold = hold;
675 }
676
677 pub fn set_options(&mut self, options: WebpOptions) {
678 self.options = options;
679 }
680
681 pub fn encode_single(img: &image::RgbaImage, options: &WebpOptions) -> Result<Vec<u8>> {
683 let mut encoder = WebpEncoder::new(img.dimensions(), *options)?;
684 encoder.add_frame(img.as_raw(), 0)?;
685 encoder.finish(1)
686 }
687
688 pub fn encode(&mut self) -> Result<Vec<u8>> {
690 let frame_count = i32::try_from(self.renders.length())
691 .map_err(|_| webp_error("frame count exceeds WebP's timestamp range"))?;
692 if frame_count == 0 {
693 return Err(webp_error("no frames to encode"));
694 }
695 let (timeline, final_timestamp) =
696 animation_timeline(self.period, self.hold, frame_count, self.first_frame)?;
697
698 let dimensions = self.renders.get_render(0)?.get().dimensions();
699 let mut encoder = WebpEncoder::new_with_animation_options(
700 dimensions,
701 self.options,
702 WebpAnimationOptions { kmin: 3, kmax: 5, ..Default::default() },
703 )?;
704 for (index, timestamp) in timeline {
705 encoder.add_frame(self.renders.get_render(index)?.get().as_raw(), timestamp)?;
706 self.renders.remove_render(index);
707 }
708
709 encoder.finish(final_timestamp)
710 }
711
712 pub fn save<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
714 let data = self.encode()?;
715 let mut writer = std::io::BufWriter::new(File::create(path)?);
716 writer.write_all(&data)?;
717 Ok(())
718 }
719}
720
721fn validate_timing(period: f64, hold: f64, frame_count: i32) -> Result<()> {
722 if !period.is_finite() || !hold.is_finite() {
723 return Err(webp_error("animation timing must be finite"));
724 }
725 if period < 0.0 || hold < 0.0 {
726 return Err(webp_error("animation timing must not be negative"));
727 }
728
729 let total_ms = (period * f64::from(frame_count) + hold) * 1000.0;
730 let timestamp_upper_bound = total_ms.round() + f64::from(frame_count);
731 if !timestamp_upper_bound.is_finite() || timestamp_upper_bound > f64::from(i32::MAX) {
732 return Err(webp_error("animation timing exceeds WebP's timestamp range"));
733 }
734 Ok(())
735}
736
737fn animation_timeline(
738 period: f64,
739 hold: f64,
740 frame_count: i32,
741 first_frame: i32,
742) -> Result<(Vec<(i32, i32)>, i32)> {
743 validate_timing(period, hold, frame_count)?;
744 let offset = if first_frame >= 0 { first_frame % frame_count } else { 0 };
745 let mut timeline = Vec::with_capacity(frame_count as usize);
746 let mut current_ms = 0.0f64;
747 let mut previous_timestamp = -1;
748
749 for base in 0..frame_count {
750 let index = ((i64::from(base) + i64::from(offset)) % i64::from(frame_count)) as i32;
751 let timestamp = rounded_timestamp_ms(current_ms, previous_timestamp)?;
752 if previous_timestamp >= 0 {
753 validate_frame_duration(previous_timestamp, timestamp)?;
754 }
755 timeline.push((index, timestamp));
756 let step = if index == frame_count - 1 { period + hold } else { period };
757 previous_timestamp = timestamp;
758 current_ms += step * 1000.0;
759 }
760
761 let final_timestamp = rounded_timestamp_ms(current_ms, previous_timestamp)?;
762 validate_frame_duration(previous_timestamp, final_timestamp)?;
763 Ok((timeline, final_timestamp))
764}
765
766fn rounded_timestamp_ms(current_ms: f64, previous_timestamp: i32) -> Result<i32> {
767 let rounded = current_ms.round();
768 if !rounded.is_finite() || rounded < 0.0 || rounded > f64::from(i32::MAX) {
769 return Err(webp_error("animation timing exceeds WebP's timestamp range"));
770 }
771
772 let timestamp = rounded as i32;
773 if timestamp <= previous_timestamp {
774 previous_timestamp
775 .checked_add(1)
776 .ok_or_else(|| webp_error("animation timing exceeds WebP's timestamp range"))
777 } else {
778 Ok(timestamp)
779 }
780}
781
782fn validate_frame_duration(timestamp_ms: i32, next_timestamp_ms: i32) -> Result<i64> {
783 let duration = i64::from(next_timestamp_ms) - i64::from(timestamp_ms);
784 if duration < 0 {
785 Err(webp_error("frame timestamps must be non-decreasing"))
786 } else if duration > MAX_WEBP_FRAME_DURATION_MS {
787 Err(webp_error("frame duration exceeds WebP's limit"))
788 } else {
789 Ok(duration)
790 }
791}
792
793fn frame_bytes((width, height): (u32, u32)) -> Result<usize> {
794 if width == 0 || height == 0 {
795 return Err(webp_error("dimensions must be positive"));
796 }
797 if width > MAX_WEBP_DIMENSION || height > MAX_WEBP_DIMENSION {
798 return Err(Error::WebpEncode(format!(
799 "dimensions exceed WebP's {MAX_WEBP_DIMENSION}px limit"
800 )));
801 }
802 (width as usize)
803 .checked_mul(height as usize)
804 .and_then(|pixels| pixels.checked_mul(4))
805 .ok_or_else(|| webp_error("RGBA frame size overflowed"))
806}
807
808fn normalize_options(mut options: WebpOptions) -> Result<WebpOptions> {
809 if !options.quality.is_finite() {
810 return Err(webp_error("quality must be a finite number"));
811 }
812 options.quality = options.quality.clamp(0.0, 100.0);
813 options.method = options.method.min(6);
814 Ok(options)
815}
816
817fn validate_animation_options(options: WebpAnimationOptions) -> Result<()> {
818 let valid_keyframes = options.kmax <= 0
819 || options.kmax == 1
820 || (options.kmin > options.kmax / 2 && options.kmin < options.kmax);
821 if !valid_keyframes {
822 return Err(webp_error("invalid keyframe interval"));
823 }
824 Ok(())
825}
826
827fn webp_error(message: &str) -> Error {
828 Error::WebpEncode(message.to_string())
829}
830
831#[cfg(test)]
832mod tests {
833 #[cfg(not(miri))]
834 use super::WebpEncoder;
835 use super::{
836 FramePlan, FrameRect, WebpAnimationOptions, WebpFrame, WebpOptions, animation_timeline,
837 encode_webp_animation, frame_bytes, normalize_options, parallel_chunk_len_for,
838 rounded_timestamp_ms, validate_animation_options, validate_timing,
839 };
840
841 #[test]
842 fn validates_dimensions() {
843 assert!(frame_bytes((0, 1)).is_err());
844 assert!(frame_bytes((16_384, 1)).is_err());
845 }
846
847 #[test]
848 fn validates_batch_inputs_before_ffi() {
849 let options = WebpOptions::default();
850 assert!(encode_webp_animation((1, 1), &[], 0, options, 0).is_err());
851
852 let short = [0; 3];
853 let malformed = [WebpFrame::new(&short, 0)];
854 assert!(encode_webp_animation((1, 1), &malformed, 1, options, 0).is_err());
855
856 let pixel = [0; 4];
857 let duplicate_timestamps = [WebpFrame::new(&pixel, 0), WebpFrame::new(&pixel, 0)];
858 assert!(encode_webp_animation((1, 1), &duplicate_timestamps, 1, options, 0).is_err());
859 }
860
861 #[test]
862 fn limits_large_frame_parallelism() {
863 let rgba = [0; 4];
864 let small = FramePlan { rgba: &rgba, rectangle: FrameRect::full(640, 360), duration_ms: 1 };
865 let large =
866 FramePlan { rgba: &rgba, rectangle: FrameRect::full(8_192, 8_192), duration_ms: 1 };
867
868 assert_eq!(parallel_chunk_len_for(&[small; 32], 24), 24);
869 assert_eq!(parallel_chunk_len_for(&[large; 2], 24), 1);
870 let mut full_then_dirty = vec![large];
871 full_then_dirty.extend([small; 24]);
872 assert_eq!(parallel_chunk_len_for(&full_then_dirty, 24), 1);
873 assert_eq!(parallel_chunk_len_for(&full_then_dirty[1..], 24), 24);
874 }
875
876 #[cfg(not(miri))]
877 #[test]
878 fn validates_frame_lengths_before_import() {
879 let mut encoder = WebpEncoder::new((2, 2), WebpOptions::default()).unwrap();
880 assert!(encoder.add_frame(&[0; 15], 0).is_err());
881 }
882
883 #[cfg(not(miri))]
884 #[test]
885 fn validates_timestamps_without_entering_ffi() {
886 let frame = [0; 16];
887 let mut encoder = WebpEncoder::new((2, 2), WebpOptions::default()).unwrap();
888 encoder.add_frame(&frame, 0).unwrap();
889 assert!(encoder.add_frame(&frame, 0).is_err());
890 }
891
892 #[test]
893 fn normalizes_public_options() {
894 let options =
895 normalize_options(WebpOptions { quality: 120.0, lossless: false, method: 20 }).unwrap();
896 assert_eq!(options.quality, 100.0);
897 assert_eq!(options.method, 6);
898 assert!(
899 normalize_options(WebpOptions { quality: f32::NAN, ..Default::default() }).is_err()
900 );
901 }
902
903 #[test]
904 fn validates_animation_timing() {
905 assert!(validate_timing(0.1, 5.0, 10).is_ok());
906 assert!(validate_timing(f64::NAN, 0.0, 1).is_err());
907 assert!(validate_timing(0.1, -1.0, 1).is_err());
908 assert!(validate_timing(f64::MAX, 0.0, 1).is_err());
909 assert!(validate_timing(f64::from(i32::MAX) / 1000.0, 0.0, 1).is_err());
910 assert_eq!(rounded_timestamp_ms(0.1, 0).unwrap(), 1);
911 assert!(rounded_timestamp_ms(f64::from(i32::MAX), i32::MAX).is_err());
912
913 let (timeline, final_timestamp) =
914 animation_timeline(0.1, 0.0, 3, i32::MAX).expect("large rotation is normalized");
915 assert_eq!(timeline, vec![(1, 0), (2, 100), (0, 200)]);
916 assert_eq!(final_timestamp, 300);
917 assert!(
918 animation_timeline(0.1, (super::MAX_WEBP_FRAME_DURATION_MS + 1) as f64 / 1000.0, 2, 0)
919 .is_err()
920 );
921 }
922
923 #[test]
924 fn validates_keyframe_intervals() {
925 assert!(validate_animation_options(WebpAnimationOptions::default()).is_ok());
926 assert!(
927 validate_animation_options(WebpAnimationOptions {
928 kmin: 1,
929 kmax: 5,
930 ..Default::default()
931 })
932 .is_err()
933 );
934 }
935}