Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce custom srcsets #28

Merged
merged 8 commits into from
Jun 4, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
302 changes: 262 additions & 40 deletions src/Imgix/UrlBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,19 @@ public class UrlBuilder
public String SignKey { set { _signKey = value; } }
private String Domain;

private static readonly List<int> SRCSET_TARGET_WIDTHS = GenerateTargetWidths();
private static readonly int[] SrcSetTargetWidths = {
100, 116, 135, 156, 181, 210, 244, 283,
328, 380, 441, 512, 594, 689, 799, 927,
1075, 1247, 1446, 1678, 1946, 2257, 2619,
3038, 3524, 4087, 4741, 5500, 6380, 7401, 8192 };

private static readonly int[] DprRatios = { 1, 2, 3, 4, 5 };
private static readonly int[] DprQualities = { 75, 50, 35, 23, 20 };


const int MaxWidth = 8192;
const int MinWidth = 100;
const int SrcSetWidthTolerance = 8;

public UrlBuilder(String domain,
String signKey = null,
Expand Down Expand Up @@ -61,66 +73,194 @@ public String BuildUrl(String path, Dictionary<String, String> parameters)
return GenerateUrl(Domain, path, parameters);
}

private String GenerateUrl(String domain, String path, Dictionary<String, String> parameters)
{
String scheme = UseHttps ? "https" : "http";
path = path.TrimEnd('/').TrimStart('/');

var qs = GenerateUrlStringFromDict(parameters);
var localParams = new Dictionary<String, String>(parameters);

if (!String.IsNullOrEmpty(_signKey))
{
var hashString = String.Format("{0}/{1}{2}", _signKey, path, localParams.Any() ? "?" + qs : String.Empty);
localParams.Add("s", HashString(hashString));
}

return String.Format("{0}://{1}/{2}{3}", scheme, domain, path, localParams.Any() ? "?" + GenerateUrlStringFromDict(localParams) : String.Empty);
}

public String BuildSrcSet(String path)
{
return BuildSrcSet(path, new Dictionary<string, string>());
}

public String BuildSrcSet(String path, Dictionary<String, String> parameters)
{
String srcset;
parameters.TryGetValue("w", out String width);
parameters.TryGetValue("h", out String height);
parameters.TryGetValue("ar", out String aspectRatio);
return BuildSrcSet(path, parameters, MinWidth, MaxWidth, SrcSetWidthTolerance);
}

if ((width != null) || (height != null && aspectRatio != null))
{
srcset = GenerateSrcSetDPR(Domain, path, parameters);
}
else
{
srcset = GenerateSrcSetPairs(Domain, path, parameters);
}
/// <summary>
/// Create a srcset attribute by disabling variable output quality.
///
/// Variable output quality for dpr-based srcset attributes
/// is _on by default_. Setting `disableVariableQuality` to
/// `true` disables this.
///
/// </summary>
/// <param name="path">path to the image, i.e. "image/file.png"</param>
/// <param name="parameters">dictionary of query parameters</param>
/// <param name="disableVariableQuality">toggle variable quality output on
/// (default/false) or off (true).</param>
/// <returns></returns>
public String BuildSrcSet(
String path,
Dictionary<String, String> parameters,
Boolean disableVariableQuality = false)
{
// Pass off to 6-param overload.
return BuildSrcSet(
path,
parameters,
MinWidth,
MaxWidth,
SrcSetWidthTolerance,
disableVariableQuality);
}

return srcset;
/// <summary>
/// Create a srcset attribute by specifying `tol`erance.
/// </summary>
/// <param name="path">path to the image, i.e. "image/file.png"</param>
/// <param name="parameters">dictionary of query parameters</param>
/// <param name="tol">tolerable amount of width value variation, 1-100.</param>
/// <returns>srcset attribute string</returns>
public String BuildSrcSet(
String path,
Dictionary<String, String> parameters,
int tol = SrcSetWidthTolerance)
{
return BuildSrcSet(path, parameters, MinWidth, MaxWidth, tol);
}

private String GenerateUrl(String domain, String path, Dictionary<String, String> parameters)
/// <summary>
/// Create a a srcset attribute by specifying `begin` and `end`.
///
/// This method creates a srcset attribute string whose image width
/// values range between `begin` and `end`.
///
/// </summary>
/// <param name="begin">beginning (min) image width value</param>
/// <param name="end">ending (max) image width value</param>
/// <returns>srcset attribute string</returns>
public String BuildSrcSet(
String path,
Dictionary<String, String> parameters,
int begin = MinWidth,
int end = MaxWidth)
{
String scheme = UseHttps ? "https" : "http";
path = path.TrimEnd('/').TrimStart('/');
return BuildSrcSet(path, parameters, begin, end, SrcSetWidthTolerance);
}

var qs = GenerateUrlStringFromDict(parameters);
var localParams = new Dictionary<String, String>(parameters);
/// <summary>
/// Create a a srcset attribute by specifying `begin`, `end`, and `tol`.
///
/// This method creates a srcset attribute string whose image width
/// values range between `begin` and `end` with the specified amount
/// `tol`erance between each.
///
/// </summary>
/// <param name="begin">beginning (min) image width value</param>
/// <param name="end">ending (max) image width value</param>
/// <param name="tol">tolerable amount of width value variation, 1-100.</param>
/// <returns>srcset attribute string</returns>
public String BuildSrcSet(
String path,
Dictionary<String, String> parameters,
int begin = MinWidth,
int end = MaxWidth,
int tol = SrcSetWidthTolerance)
{
return BuildSrcSet(path, parameters, begin, end, tol, false);
}

if (!String.IsNullOrEmpty(_signKey))
/// <summary>
/// Create a srcset attribute.
///
/// If the `parameters` indicate the srcset attribute should be
/// dpr-based, then a dpr-based srcset is created.
///
/// Otherwise, a viewport based srcset attribute is created with
/// with the target widths that are generated by using `begin`,
/// `end`, and `tol`.
/// </summary>
/// <param name="path">path to the image, i.e. "image/file.png"</param>
/// <param name="parameters">dictionary of query parameters</param>
/// <param name="begin">beginning (min) image width value</param>
/// <param name="end">ending (max) image width value</param>
/// <param name="tol">tolerable amount of width value variation, 1-100.</param>
/// <param name="disableVariableQuality">toggle variable quality output on
/// (default/false) or off (true).</param>
/// <returns></returns>
public String BuildSrcSet(
String path,
Dictionary<String, String> parameters,
int begin = MinWidth,
int end = MaxWidth,
int tol = SrcSetWidthTolerance,
Boolean disableVariableQuality = false)
{

if (IsDpr(parameters))
{
var hashString = String.Format("{0}/{1}{2}", _signKey, path, localParams.Any() ? "?" + qs : String.Empty);
localParams.Add("s", HashString(hashString));
return GenerateSrcSetDPR(path, parameters, disableVariableQuality);
}
else
{
List<int> targets = GenerateTargetWidths(begin: begin, end: end, tol: tol);
return GenerateSrcSetPairs(path, parameters, targets: targets);
}

return String.Format("{0}://{1}/{2}{3}", scheme, domain, path, localParams.Any() ? "?" + GenerateUrlStringFromDict(localParams) : String.Empty);
}

private String GenerateSrcSetDPR(String domain, String path, Dictionary<String, String> parameters)
private String
GenerateSrcSetDPR(
String path,
Dictionary<String, String> parameters,
Boolean disableVariableQuality)
{
String srcset = "";
int[] targetRatios = { 1, 2, 3, 4, 5 };
var srcSetParams = new Dictionary<String, String>(parameters);

// If a "q" is present, it will be applied whether or not
// variable quality has been disabled.
Boolean hasQuality = parameters.TryGetValue("q", out String q);

foreach(int ratio in targetRatios)
foreach(int ratio in DprRatios)
{
parameters["dpr"] = ratio.ToString();
srcset += BuildUrl(path, parameters) + " " + ratio.ToString()+ "x,\n";
if (!disableVariableQuality && !hasQuality)
{
srcSetParams["q"] = DprQualities[ratio - 1].ToString();
}
srcSetParams["dpr"] = ratio.ToString();
srcset += BuildUrl(path, srcSetParams) + " " + ratio.ToString()+ "x,\n";
}

return srcset.Substring(0, srcset.Length - 2);
}

private String GenerateSrcSetPairs(String domain, String path, Dictionary<String, String> parameters)
private String GenerateSrcSetPairs(
String path,
Dictionary<String, String> parameters,
List<int> targets = null)
{
if (targets == null)
{
targets = SrcSetTargetWidths.ToList();
}

String srcset = "";

foreach(int width in SRCSET_TARGET_WIDTHS)
foreach(int width in targets)
{
parameters["w"] = width.ToString();
srcset += BuildUrl(path, parameters) + " " + width + "w,\n";
Expand All @@ -129,22 +269,104 @@ private String GenerateSrcSetPairs(String domain, String path, Dictionary<String
return srcset.Substring(0, srcset.Length - 2);
}

private static List<int> GenerateTargetWidths()
/// <summary>
/// Create a `List` of integer target widths.
///
/// If `begin`, `end`, and `tol` are the default values, then the
/// targets are not custom, in which case the default widths are
/// returned.
///
/// A target width list of length one is valid: if `begin` == `end`
/// then the list begins where it ends.
///
/// When the `while` loop terminates, `begin` is greater than `end`
/// (where `end` less than or equal to `MAX_WIDTH`). This means that
/// the most recently appended value may, or may not, be the `end`
/// value.
///
/// To be inclusive of the ending value, we check for this case and the
/// value is added if necessary.
/// </summary>
/// <param name="begin">beginning (min) image width value</param>
/// <param name="end">ending (max) image width value</param>
/// <param name="tol">tolerable amount of width value variation, 1-100.</param>
/// <returns>list of image width values</returns>
public static List<int>
GenerateTargetWidths(
int begin = MinWidth,
int end = MaxWidth,
int tol = SrcSetWidthTolerance)
{
return ComputeTargetWidths(begin, end, tol);
}

public static List<int> GenerateEvenTargetWidths()
{
return SrcSetTargetWidths.ToList();
}

/**
* Create an `ArrayList` of integer target widths.
*
* This function is the implementation details of `targetWidths`.
* This function exists to provide a consistent interface for
* callers of `targetWidths`.
*
* This function implements the syntax that fulfills the semantics
* of `targetWidths`. Meaning, `begin`, `end`, and `tol` are
* to be whole integers, but computation requires `double`s. This
* function hides this detail from callers.
*/
private static List<int> ComputeTargetWidths(double begin, double end, double tol)
{
if(NotCustom(begin, end, tol))
{
// If not custom, return the default target widths.
return SrcSetTargetWidths.ToList();
}

if (begin == end)
{
return new List<int> {(int) Math.Round(begin) };
}

List<int> resolutions = new List<int>();
int MAX_SIZE = 8192, roundedPrev;
double SRCSET_PERCENT_INCREMENT = 8;
double prev = 100;
while (begin < end && begin < MaxWidth)
{
resolutions.Add((int) Math.Round(begin));
begin *= 1 + (tol / 100) * 2;
}

while (prev < MAX_SIZE)
if (resolutions.Last() < end)
{
roundedPrev = (int)(2 * Math.Round(prev / 2));
resolutions.Add(roundedPrev);
prev *= 1 + (SRCSET_PERCENT_INCREMENT / 100) * 2;
resolutions.Add((int) Math.Round(end));
}
resolutions.Add(MAX_SIZE);

return resolutions;

}

private static Boolean IsDpr(Dictionary<String, String> parameters)
{
Boolean hasWidth = parameters.TryGetValue("w", out String width);
Boolean hasHeight = parameters.TryGetValue("h", out String height);
Boolean hasAspectRatio = parameters.TryGetValue("ar", out String aspectRatio);

// If `params` have a width param or _both_ height and aspect
// ratio parameters then the srcset to be constructed with
// these params _is dpr based_
return hasWidth || (hasHeight && hasAspectRatio);
}

private static Boolean NotCustom(double begin, double end, double tol)
{
Boolean defaultBegin = (begin == MinWidth);
Boolean defaultEnd = (end == MaxWidth);
Boolean defaultTol = (tol == SrcSetWidthTolerance);

// A list of target widths is _NOT_ custom if `begin`, `end`,
// and `tol` are equal to their default values.
return defaultBegin && defaultEnd && defaultTol;
}

private String HashString(String input)
Expand Down
Loading