use super::params::OutputFormat;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct AcceptResult {
pub supports_avif: bool,
pub supports_webp: bool,
pub supports_jpeg: bool,
pub supports_png: bool,
pub supports_gif: bool,
pub wildcard: bool,
}
impl AcceptResult {
pub fn supports(&self, fmt: OutputFormat) -> bool {
match fmt {
OutputFormat::Avif => self.supports_avif || self.wildcard,
OutputFormat::Webp => self.supports_webp || self.wildcard,
OutputFormat::Jpeg => self.supports_jpeg || self.wildcard,
OutputFormat::Png => self.supports_png || self.wildcard,
OutputFormat::Gif => self.supports_gif || self.wildcard,
OutputFormat::Auto => true,
}
}
}
pub fn parse_accept_header(accept: &str) -> AcceptResult {
let mut result = AcceptResult::default();
if accept.is_empty() {
return result;
}
for raw_entry in accept.split(',') {
let entry = raw_entry.trim();
if entry.is_empty() {
continue;
}
let mut parts = entry.split(';');
let media_type = match parts.next() {
Some(mt) => mt.trim(),
None => continue,
};
let mut q_value: f32 = 1.0;
for param_raw in parts {
let param = param_raw.trim();
if let Some(rest) = param
.strip_prefix("q=")
.or_else(|| param.strip_prefix("Q="))
{
q_value = rest.parse::<f32>().unwrap_or(1.0);
}
}
if q_value == 0.0 {
continue;
}
match media_type {
"*/*" | "image/*" => result.wildcard = true,
"image/avif" => result.supports_avif = true,
"image/webp" => result.supports_webp = true,
"image/jpeg" | "image/jpg" => result.supports_jpeg = true,
"image/png" => result.supports_png = true,
"image/gif" => result.supports_gif = true,
_ => {}
}
}
result
}
pub fn negotiate_format(
accept_header: Option<&str>,
source_has_alpha: bool,
requested_format: Option<OutputFormat>,
) -> OutputFormat {
if let Some(fmt) = requested_format
&& fmt != OutputFormat::Auto
{
return fmt;
}
let accept = accept_header.map(parse_accept_header).unwrap_or_default();
if source_has_alpha {
if accept.supports(OutputFormat::Avif) {
return OutputFormat::Avif;
}
if accept.supports(OutputFormat::Webp) {
return OutputFormat::Webp;
}
if accept.supports(OutputFormat::Png) {
return OutputFormat::Png;
}
OutputFormat::Jpeg
} else {
if accept.supports(OutputFormat::Avif) {
return OutputFormat::Avif;
}
if accept.supports(OutputFormat::Webp) {
return OutputFormat::Webp;
}
if accept.supports(OutputFormat::Jpeg) {
return OutputFormat::Jpeg;
}
if accept.supports(OutputFormat::Png) {
return OutputFormat::Png;
}
OutputFormat::Jpeg
}
}
pub fn negotiate_animated_format(
accept_header: Option<&str>,
requested_format: Option<OutputFormat>,
) -> Option<OutputFormat> {
if let Some(fmt) = requested_format
&& fmt != OutputFormat::Auto
{
return if fmt.supports_animation() {
Some(fmt)
} else {
None
};
}
let accept = accept_header.map(parse_accept_header).unwrap_or_default();
if accept.supports(OutputFormat::Webp) {
return Some(OutputFormat::Webp);
}
if accept.supports(OutputFormat::Gif) {
return Some(OutputFormat::Gif);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_accept_header_single_format_webp() {
let r = parse_accept_header("image/webp");
assert!(r.supports_webp);
assert!(!r.supports_avif);
assert!(!r.supports_jpeg);
assert!(!r.supports_png);
assert!(!r.wildcard);
}
#[test]
fn parse_accept_header_multiple_formats_with_image_wildcard() {
let r = parse_accept_header("image/avif,image/webp,image/*");
assert!(r.supports_avif);
assert!(r.supports_webp);
assert!(r.wildcard);
assert!(r.supports(OutputFormat::Jpeg));
assert!(r.supports(OutputFormat::Png));
}
#[test]
fn parse_accept_header_star_star_wildcard() {
let r = parse_accept_header("*/*");
assert!(r.wildcard);
assert!(r.supports(OutputFormat::Avif));
assert!(r.supports(OutputFormat::Webp));
assert!(r.supports(OutputFormat::Jpeg));
assert!(r.supports(OutputFormat::Png));
}
#[test]
fn parse_accept_header_image_wildcard() {
let r = parse_accept_header("image/*");
assert!(r.wildcard);
assert!(r.supports(OutputFormat::Avif));
assert!(r.supports(OutputFormat::Webp));
assert!(r.supports(OutputFormat::Jpeg));
assert!(r.supports(OutputFormat::Png));
}
#[test]
fn parse_accept_header_empty_string() {
let r = parse_accept_header("");
assert!(!r.supports_avif);
assert!(!r.supports_webp);
assert!(!r.supports_jpeg);
assert!(!r.supports_png);
assert!(!r.wildcard);
}
#[test]
fn parse_accept_header_q_values_parsed_q0_disables_format() {
let r = parse_accept_header("image/webp;q=0,image/avif;q=1.0");
assert!(r.supports_avif);
assert!(!r.supports_webp);
}
#[test]
fn parse_accept_header_q_values_nonzero_are_accepted() {
let r = parse_accept_header("image/webp;q=0.9,image/avif;q=1.0");
assert!(r.supports_avif);
assert!(r.supports_webp);
}
#[test]
fn parse_accept_header_spaces_around_entries_are_trimmed() {
let r = parse_accept_header(" image/avif , image/webp ; q=0.8 , image/png ");
assert!(r.supports_avif);
assert!(r.supports_webp);
assert!(r.supports_png);
assert!(!r.supports_jpeg);
}
#[test]
fn parse_accept_header_jpeg_and_jpg_aliases() {
assert!(parse_accept_header("image/jpeg").supports_jpeg);
assert!(parse_accept_header("image/jpg").supports_jpeg);
}
#[test]
fn parse_accept_header_unknown_media_types_are_ignored() {
let r = parse_accept_header("image/tiff,application/json,text/html");
assert!(!r.supports_avif);
assert!(!r.supports_webp);
assert!(!r.supports_jpeg);
assert!(!r.supports_png);
assert!(!r.wildcard);
}
#[test]
fn parse_accept_header_malformed_entries_are_gracefully_ignored() {
assert!(!parse_accept_header(";;;").wildcard);
assert!(!parse_accept_header(",,,").wildcard);
assert!(parse_accept_header("image/webp;q=notanumber").supports_webp);
assert!(parse_accept_header("image/webp;q=").supports_webp);
}
#[test]
fn parse_accept_header_realistic_browser_accept_header() {
let chrome = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8";
let r = parse_accept_header(chrome);
assert!(r.supports_avif);
assert!(r.supports_webp);
assert!(r.wildcard);
}
#[test]
fn negotiate_format_explicit_format_overrides_everything() {
assert_eq!(
negotiate_format(Some("image/webp"), false, Some(OutputFormat::Png)),
OutputFormat::Png
);
assert_eq!(
negotiate_format(Some("image/avif"), true, Some(OutputFormat::Jpeg)),
OutputFormat::Jpeg
);
assert_eq!(
negotiate_format(None, false, Some(OutputFormat::Avif)),
OutputFormat::Avif
);
}
#[test]
fn negotiate_format_auto_triggers_negotiation() {
assert_eq!(
negotiate_format(Some("image/webp"), false, Some(OutputFormat::Auto)),
OutputFormat::Webp
);
}
#[test]
fn negotiate_format_null_requested_format_triggers_negotiation() {
assert_eq!(
negotiate_format(Some("image/webp"), false, None),
OutputFormat::Webp
);
}
#[test]
fn negotiate_format_null_accept_header_defaults_to_jpeg() {
assert_eq!(negotiate_format(None, false, None), OutputFormat::Jpeg);
}
#[test]
fn negotiate_format_empty_accept_header_defaults_to_jpeg() {
assert_eq!(negotiate_format(Some(""), false, None), OutputFormat::Jpeg);
}
#[test]
fn negotiate_format_avif_preferred_over_webp() {
assert_eq!(
negotiate_format(Some("image/avif,image/webp"), false, None),
OutputFormat::Avif
);
}
#[test]
fn negotiate_format_webp_preferred_over_jpeg() {
assert_eq!(
negotiate_format(Some("image/webp,image/jpeg"), false, None),
OutputFormat::Webp
);
}
#[test]
fn negotiate_format_jpeg_preferred_over_png_no_alpha() {
assert_eq!(
negotiate_format(Some("image/jpeg,image/png"), false, None),
OutputFormat::Jpeg
);
}
#[test]
fn negotiate_format_alpha_source_webp_jpeg_webp_preferred() {
assert_eq!(
negotiate_format(Some("image/webp,image/jpeg"), true, None),
OutputFormat::Webp
);
}
#[test]
fn negotiate_format_alpha_source_jpeg_only_still_jpeg() {
assert_eq!(
negotiate_format(Some("image/jpeg"), true, None),
OutputFormat::Jpeg
);
}
#[test]
fn negotiate_format_alpha_source_png_jpeg_png_preferred() {
assert_eq!(
negotiate_format(Some("image/png,image/jpeg"), true, None),
OutputFormat::Png
);
}
#[test]
fn negotiate_format_alpha_source_avif_webp_jpeg_avif_wins() {
assert_eq!(
negotiate_format(Some("image/avif,image/webp,image/jpeg"), true, None),
OutputFormat::Avif
);
}
#[test]
fn negotiate_format_wildcard_accept_avif_highest_priority() {
assert_eq!(
negotiate_format(Some("*/*"), false, None),
OutputFormat::Avif
);
}
#[test]
fn negotiate_format_wildcard_accept_alpha_avif() {
assert_eq!(
negotiate_format(Some("image/*"), true, None),
OutputFormat::Avif
);
}
#[test]
fn negotiate_format_only_png_accepted_no_alpha() {
assert_eq!(
negotiate_format(Some("image/png"), false, None),
OutputFormat::Png
);
}
#[test]
fn negotiate_format_malformed_accept_fallback_to_jpeg() {
assert_eq!(
negotiate_format(Some("garbage/nonsense"), false, None),
OutputFormat::Jpeg
);
}
#[test]
fn negotiate_format_all_formats_disabled_by_q0_fallback_to_jpeg() {
assert_eq!(
negotiate_format(
Some("image/avif;q=0,image/webp;q=0,image/png;q=0"),
false,
None
),
OutputFormat::Jpeg
);
}
#[test]
fn negotiate_animated_format_accept_webp() {
assert_eq!(
negotiate_animated_format(Some("image/webp"), None),
Some(OutputFormat::Webp)
);
}
#[test]
fn negotiate_animated_format_accept_gif() {
assert_eq!(
negotiate_animated_format(Some("image/gif"), None),
Some(OutputFormat::Gif)
);
}
#[test]
fn negotiate_animated_format_accept_webp_gif_webp_preferred() {
assert_eq!(
negotiate_animated_format(Some("image/webp,image/gif"), None),
Some(OutputFormat::Webp)
);
}
#[test]
fn negotiate_animated_format_accept_jpeg_only_static_fallback() {
assert_eq!(negotiate_animated_format(Some("image/jpeg"), None), None);
}
#[test]
fn negotiate_animated_format_explicit_gif_format() {
assert_eq!(
negotiate_animated_format(Some("image/jpeg"), Some(OutputFormat::Gif)),
Some(OutputFormat::Gif)
);
}
#[test]
fn negotiate_animated_format_explicit_webp_format() {
assert_eq!(
negotiate_animated_format(Some("image/jpeg"), Some(OutputFormat::Webp)),
Some(OutputFormat::Webp)
);
}
#[test]
fn negotiate_animated_format_explicit_jpeg_not_animated() {
assert_eq!(
negotiate_animated_format(Some("image/webp"), Some(OutputFormat::Jpeg)),
None
);
}
#[test]
fn negotiate_animated_format_wildcard_accept_webp() {
assert_eq!(
negotiate_animated_format(Some("*/*"), None),
Some(OutputFormat::Webp)
);
}
#[test]
fn parse_accept_header_gif_support() {
let r = parse_accept_header("image/gif");
assert!(r.supports_gif);
assert!(!r.supports_webp);
}
#[test]
fn accept_result_supports_gif_through_wildcard() {
assert!(parse_accept_header("*/*").supports(OutputFormat::Gif));
}
}