Skip to content

Commit

Permalink
Add new srcset syntax per #629
Browse files Browse the repository at this point in the history
  • Loading branch information
lilith committed Apr 3, 2023
1 parent b911878 commit 6a92bd2
Show file tree
Hide file tree
Showing 4 changed files with 314 additions and 2 deletions.
42 changes: 42 additions & 0 deletions docs/src/querystring/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,49 @@ maintaining aspect ratio.
then minimally cropped to meet the aspect ratio. `scale=both` ensures the image is upscaled if smaller so the result
is always 300x300.

## srcset syntax

Our new, more compact comma-delimited syntax. It lets you use the familiar srcset width and density descriptors such as `80w`, `70h`, `2.5x`.

### Notes:

* srcset commands are comma delimited, and use `-` to separate command parameters. Ex `srcset=jpeg-100,sharp-20`
* You can also combine them, so you don't have to do math. Ex. `&srcset=100w,2x` translates to `&w=100&zoom=2` which translates to 200px wide.
* Since CSS pixels map to multiple device pixels, this reduces an error-prone task.
* The default mode is `max`, so you don't need to specify it when using both width and height.
* Quality values do not translate across encoders, a fact that is lost on many people. In this syntax, we combine the format and quality value.
* `srcset` commands expand, internally, to `&w=100&h=100&mode=max&format=[value]&zoom=[density]&quality=[x]&webp.quality=[y]&png.quality=[z]&scale=[both|down]&f.sharpen=[pct]` etc


### List of srcset values and what they affect

* `jpeg-100` - JPEG, 100% quality
* `jpeg` - JPEG, default quality configured in the server
* `png-100` - PNG, 100% quality
* `webp-100` - WebP, 100% quality
* `webp` - WebP, default quality configured in the server
* `webp-lossless` or `webp-l` - WebP, lossless
* `png-lossless` or `png-l` or `png` - PNG, lossless
* `gif` - GIF
* `2.5x` - 2.5x density/multiplier applied to width and height
* `100w` - 100px wide (times the density multiplier, if specified)
* `100h` - 100px tall
* `fit-max` - (default) don't change the image's intrinsic aspect ratio, just constrain it within the width/height box if both are specified/
* `fit-crop` - crop to meet aspect ratio
* `fit-pad` - pad to meet aspect ratio
* `fit-distort` - distort to meet aspect ratio
* `upscale` - by default, images are only downscaled. This can cause unexpected results when using `crop` or `pad`. This option allows upscaling when needed.
* `crop-10-20-80-90` - crop to rectangle 10%,20%,80%,90% of the source image
* `crop-10-20-80-90,100w,100h,webp-90` - crop to rectangle 10%,20%,80%,90% of the source image, scale to 100px wide, 100px tall, WebP, 90% quality
* `sharpen-20` - sharpen by 20%
* `sharp-20` - sharpen by 20%

#### Examples

* `&srcset=webp-70,sharp-15,100w` - WebP, 70% quality, 15% sharpening, 100px wide
* `&srcset=jpeg-80,2x,100w,sharpen-20` - JPEG, 80% quality, 2x density, 200px wide, 20% sharpening
* `&srcset=png-90,2.5x,100w,100h,crop` - PNG, 90% quality, 250px wide, 250px tall, cropped to aspect ratio
* `&srcset=png-lossless` - PNG, lossless
* `&srcset=gif,crop-20-30-90-100,2.5x,100w,100h` - GIF, cropped to rectangle 20%,30%,90%,100%, 250px wide, 250px tall
* `&srcset=webp-l,2.5x,100w,100h,crop` - WebP, lossless, cropped to aspect ratio, resized to 250px wide, 250px tall
* `&srcset=webp-lossless,2.5x,100w,100h,upscale` - WebP, lossless, 250x250px, upscale to width & height if original image is smaller.
1 change: 1 addition & 0 deletions imageflow_riapi/src/ir4/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use imageflow_types as s;

pub mod parsing;
mod layout;
mod srcset;

use crate::sizing;
use crate::sizing::prelude::*;
Expand Down
57 changes: 55 additions & 2 deletions imageflow_riapi/src/ir4/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use imageflow_helpers::colors::*;
use imageflow_types::Filter;
use imageflow_helpers::preludes::from_std::fmt::Formatter;

use super::srcset::apply_srcset_string;

macro_attr! {


Expand Down Expand Up @@ -214,7 +216,7 @@ pub enum ScalingColorspace {

}

pub static IR4_KEYS: [&'static str;80] = ["mode", "anchor", "flip", "sflip", "scale", "cache", "process",
pub static IR4_KEYS: [&'static str;81] = ["mode", "anchor", "flip", "sflip", "scale", "cache", "process",
"quality", "jpeg.quality", "zoom", "crop", "cropxunits", "cropyunits",
"w", "h", "width", "height", "maxwidth", "maxheight", "format", "thumbnail",
"autorotate", "srotate", "rotate", "ignoreicc", "ignore_icc_errors", //really? : "precise_scaling_ratio",
Expand All @@ -227,7 +229,7 @@ pub static IR4_KEYS: [&'static str;80] = ["mode", "anchor", "flip", "sflip", "sc
"jpeg.turbo", "encoder", "decoder", "builder", "s.roundcorners", "paddingwidth",
"paddingheight", "margin", "borderwidth", "decoder.min_precise_scaling_ratio",
"png.quality","png.min_quality", "png.quantization_speed", "png.libpng", "png.max_deflate",
"png.lossless", "up.filter", "down.filter", "dpr", "up.colorspace"];
"png.lossless", "up.filter", "down.filter", "dpr", "up.colorspace", "srcset"];


#[derive(PartialEq,Debug, Clone)]
Expand Down Expand Up @@ -469,6 +471,10 @@ impl Instructions{
i.jpeg_turbo = p.parse_bool("jpeg.turbo");

i.watermark_red_dot = p.parse_bool("watermark_red_dot");


p.apply_srcset(&mut i);

i
}

Expand Down Expand Up @@ -507,6 +513,22 @@ struct Parser<'a>{
}
impl<'a> Parser<'a>{

fn remove(&mut self, key: &str) -> Option<String>{
self.m.remove(key).map(|v| v.trim().to_owned())
}

fn apply_srcset(&mut self, i: &mut Instructions){
if let Some(srcset) = self.remove("srcset"){
if self.w.is_some(){
apply_srcset_string(i, &srcset, self.w.as_mut().unwrap());
}else{
let mut w = Vec::new();
apply_srcset_string(i, &srcset, &mut w);
}
}

}

fn warn(&mut self, warning: ParseWarning){
if self.w.is_some() {
self.w.as_mut().unwrap().push(warning);
Expand Down Expand Up @@ -1118,13 +1140,44 @@ fn test_url_parsing() {
t("anchor=bottomleft", Instructions{anchor: Some((Anchor1D::Near, Anchor1D::Far)), ..Default::default()}, vec![]);
t("watermark_red_dot=true", Instructions{watermark_red_dot: Some(true), ..Default::default()}, vec![]);

let srcset_default = Instructions{mode: Some(FitMode::Max), ..Default::default()};
t("srcset=100w", Instructions{w: Some(100), ..srcset_default.to_owned()}, vec![]);
t("srcset=100h", Instructions{h: Some(100), ..srcset_default.to_owned()}, vec![]);
t("srcset=2x", Instructions{zoom: Some(2.0), ..srcset_default.to_owned()}, vec![]);
t("srcset=webp-90", Instructions{format: Some(OutputFormat::Webp), webp_quality: Some(90.0), ..srcset_default.to_owned()}, vec![]);
t("srcset=png-90", Instructions{format: Some(OutputFormat::Png), png_quality: Some(90), ..srcset_default.to_owned()}, vec![]);
t("srcset=jpeg-90", Instructions{format: Some(OutputFormat::Jpeg), quality: Some(90), ..srcset_default.to_owned()}, vec![]);
t("srcset=crop-10-20-80-90", Instructions{cropxunits: Some(100.0), cropyunits: Some(100.0),crop: Some([10.0,20.0,80.0,90.0]), ..srcset_default.to_owned()}, vec![]);
t("srcset=upscale", Instructions{scale: Some(ScaleMode::Both), ..srcset_default.to_owned()}, vec![]);
t("srcset=fit-crop", Instructions{mode: Some(FitMode::Crop), ..srcset_default.to_owned()}, vec![]);
t("srcset=fit-pad", Instructions{mode: Some(FitMode::Pad), ..srcset_default.to_owned()}, vec![]);
t("srcset=fit-distort", Instructions{mode: Some(FitMode::Stretch), ..srcset_default.to_owned()}, vec![]);
t("srcset=", Instructions{..Default::default()}, vec![]);
t("srcset=sharp-20", Instructions{f_sharpen: Some(20.0), ..srcset_default.to_owned()}, vec![]);
t("srcset=sharpen-20", Instructions{f_sharpen: Some(20.0), ..srcset_default.to_owned()}, vec![]);
t("srcset=sharp-20", Instructions{f_sharpen: Some(20.0), ..srcset_default.to_owned()}, vec![]);
t("srcset=gif", Instructions{format: Some(OutputFormat::Gif), ..srcset_default.to_owned()}, vec![]);
t("srcset=png", Instructions{format: Some(OutputFormat::Png), png_lossless: Some(true), ..srcset_default.to_owned()}, vec![]);
t("srcset=png-l", Instructions{format: Some(OutputFormat::Png), png_lossless: Some(true), ..srcset_default.to_owned()}, vec![]);
t("srcset=png-lossless", Instructions{format: Some(OutputFormat::Png), png_lossless: Some(true), ..srcset_default.to_owned()}, vec![]);
t("srcset=webp-l", Instructions{format: Some(OutputFormat::Webp), webp_lossless: Some(true), ..srcset_default.to_owned()}, vec![]);
t("srcset=webp-lossless", Instructions{format: Some(OutputFormat::Webp), webp_lossless: Some(true), ..srcset_default.to_owned()}, vec![]);
t("srcset=webp", Instructions{format: Some(OutputFormat::Webp), ..srcset_default.to_owned()}, vec![]);
t("srcset=webp&webp.quality=5", Instructions{format: Some(OutputFormat::Webp), webp_quality: Some(5.0), ..srcset_default.to_owned()}, vec![]);
t("srcset=webp-76,100w, 100h,2x ,sharp-20 ,fit-crop", Instructions{format: Some(OutputFormat::Webp), webp_quality: Some(76.0), w: Some(100), h: Some(100), zoom: Some(2.0), f_sharpen: Some(20.0), mode: Some(FitMode::Crop), ..srcset_default.to_owned()}, vec![]);


expect_warning("a.balancewhite","gimp", Instructions{a_balance_white: Some(HistogramThresholdAlgorithm::Gimp), ..Default::default()});
expect_warning("a.balancewhite","simple", Instructions{a_balance_white: Some(HistogramThresholdAlgorithm::Simple), ..Default::default()});
expect_warning("crop","(0,3,80, 90)", Instructions { crop: Some([0f64,3f64,80f64,90f64]), ..Default::default() });
expect_warning("crop","(0,3,happy, 90)", Instructions { crop: Some([0f64,3f64,0f64,90f64]), ..Default::default() });
expect_warning("crop","( a0, 3, happy, 90)", Instructions { crop: Some([0f64,3f64,0f64,90f64]), ..Default::default() });

// expect_warning("srcset","crop,pad", Instructions{mode: Some(FitMode::Pad), ..srcset_default.to_owned()});
// expect_warning("srcset","pad,crop", Instructions{mode: Some(FitMode::Crop), ..srcset_default.to_owned()});
// expect_warning("srcset","png,gif", Instructions{format: Some(OutputFormat::Gif), ..srcset_default.to_owned()});


}

#[test]
Expand Down
Loading

0 comments on commit 6a92bd2

Please sign in to comment.