diff --git a/README.md b/README.md index 832bdd2..c7f2154 100644 --- a/README.md +++ b/README.md @@ -68,5 +68,51 @@ using Imgix; var builder = new UrlBuilder("domain.imgix.net", includeLibraryParam: false); ``` +Srcset Generation +----------- + +The imgix-csharp library allows for generation of custom `srcset` attributes, which can be invoked through `BuildSrcSet()`. By default, the `srcset` generated will allow for responsive size switching by building a list of image-width mappings. + +```csharp +var builder = new UrlBuilder("domain.imgix.net", "my-token", false, true); +String srcset = builder.BuildSrcSet("bridge.png"); +Debug.Print(srcset); +``` + +Will produce the following attribute value, which can then be served to the client: + +```html +https://domain.imgix.net/bridge.png?w=100&s=494158d968e94ac8e83772ada9a83ad1 100w, +https://domain.imgix.net/bridge.png?w=116&s=6a22236e189b6a9548b531330647ffa7 116w, +https://domain.imgix.net/bridge.png?w=134&s=cbf91f556dd67c0b9e26cb9784a83794 134w, + ... +https://domain.imgix.net/bridge.png?w=7400&s=503e3ba04588f1c301863c9a5d84fe91 7400w, +https://domain.imgix.net/bridge.png?w=8192&s=152551ce4ec155f7a03f60f762a1ca33 8192w +``` + +In cases where enough information is provided about an image's dimensions, `BuildSrcSet()` will instead build a `srcset` that will allow for an image to be served at different resolutions. The parameters taken into consideration when determining if an image is fixed-width are `w` (width), `h` (height), and `ar` (aspect ratio). By invoking `BuildSrcSet()` with either a width **or** the height and aspect ratio (along with `fit=crop`, typically) provided, a different `srcset` will be generated for a fixed-size image instead. + +```csharp +var builder = new UrlBuilder("domain.imgix.net", "my-token", false, true); +var parameters = new Dictionary(); +parameters["h"] = "200"; +parameters["ar"] = "3:2"; +parameters["fit"] = "crop"; +String srcset = builder.BuildSrcSet("bridge.png", parameters); +Console.WriteLine(srcset); +``` + +Will produce the following attribute value: + +```html +https://domain.imgix.net/bridge.png?h=200&ar=3%3A2&fit=crop&dpr=1&s=f39a78a6a2f245a70ba6aac910088435 1x, +https://domain.imgix.net/bridge.png?h=200&ar=3%3A2&fit=crop&dpr=2&s=d5dfd75bd777283d82975ab18a3091ff 2x, +https://domain.imgix.net/bridge.png?h=200&ar=3%3A2&fit=crop&dpr=3&s=8f25811130e3573530754c52f86a851d 3x, +https://domain.imgix.net/bridge.png?h=200&ar=3%3A2&fit=crop&dpr=4&s=ec348479a843a688c2ef9be487ea9be8 4x, +https://domain.imgix.net/bridge.png?h=200&ar=3%3A2&fit=crop&dpr=5&s=ce70bbfd682e683497f1afa6118ae2e3 5x +``` + +For more information to better understand `srcset`, we highly recommend [Eric Portis' "Srcset and sizes" article](https://ericportis.com/posts/2014/srcset-sizes/) which goes into depth about the subject. + ## Code of Conduct Users contributing to or participating in the development of this project are subject to the terms of imgix's [Code of Conduct](https://github.com/imgix/code-of-conduct). diff --git a/src/Imgix/UrlBuilder.cs b/src/Imgix/UrlBuilder.cs index a5c0458..f1fdab6 100644 --- a/src/Imgix/UrlBuilder.cs +++ b/src/Imgix/UrlBuilder.cs @@ -14,9 +14,10 @@ public class UrlBuilder private String _signKey; public String SignKey { set { _signKey = value; } } - private String Domain; + private static readonly List SRCSET_TARGET_WIDTHS = GenerateTargetWidths(); + public UrlBuilder(String domain, String signKey = null, Boolean includeLibraryParam = true, @@ -59,6 +60,29 @@ public String BuildUrl(String path, Dictionary parameters) return GenerateUrl(Domain, path, parameters); } + public String BuildSrcSet(String path) + { + return BuildSrcSet(path, new Dictionary()); + } + + public String BuildSrcSet(String path, Dictionary parameters) + { + String srcset; + parameters.TryGetValue("w", out String width); + parameters.TryGetValue("h", out String height); + parameters.TryGetValue("ar", out String aspectRatio); + + if ((width != null) || (height != null && aspectRatio != null)) + { + srcset = GenerateSrcSetDPR(Domain, path, parameters); + } + else + { + srcset = GenerateSrcSetPairs(Domain, path, parameters); + } + + return srcset; + } private String GenerateUrl(String domain, String path, Dictionary parameters) { @@ -77,6 +101,52 @@ private String GenerateUrl(String domain, String path, Dictionary parameters) + { + String srcset = ""; + int[] targetRatios = { 1, 2, 3, 4, 5 }; + + foreach(int ratio in targetRatios) + { + parameters["dpr"] = ratio.ToString(); + parameters.Remove("ixlib"); + srcset += BuildUrl(path, parameters) + " " + ratio.ToString()+ "x,\n"; + } + + return srcset.Substring(0, srcset.Length - 2); + } + + private String GenerateSrcSetPairs(String domain, String path, Dictionary parameters) + { + String srcset = ""; + + foreach(int width in SRCSET_TARGET_WIDTHS) + { + parameters["w"] = width.ToString(); + parameters.Remove("ixlib"); + srcset += BuildUrl(path, parameters) + " " + width + "w,\n"; + } + + return srcset.Substring(0, srcset.Length - 2); + } + + private static List GenerateTargetWidths() + { + List resolutions = new List(); + int MAX_SIZE = 8192, roundedPrev; + double SRCSET_PERCENT_INCREMENT = 8; + double prev = 100; + + while (prev < MAX_SIZE) + { + roundedPrev = (int)(2 * Math.Round(prev / 2)); + resolutions.Add(roundedPrev); + prev *= 1 + (SRCSET_PERCENT_INCREMENT / 100) * 2; + } + resolutions.Add(MAX_SIZE); + + return resolutions; + } private String HashString(String input) { diff --git a/tests/Imgix.Tests/SrcSetTest.cs b/tests/Imgix.Tests/SrcSetTest.cs new file mode 100644 index 0000000..70032e8 --- /dev/null +++ b/tests/Imgix.Tests/SrcSetTest.cs @@ -0,0 +1,563 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using Imgix; +using System.Security.Cryptography; +using System.Linq; + +namespace Imgix.Tests +{ + [TestFixture] + public class SrcSetTest + { + private static Dictionary parameters; + private static String[] srcsetSplit; + private static String[] srcsetWidthSplit; + private static String[] srcsetHeightSplit; + private static String[] srcsetAspectRatioSplit; + private static String[] srcsetWidthAndHeightSplit; + private static String[] srcsetWidthAndAspectRatioSplit; + private static String[] srcsetHeightAndAspectRatioSplit; + + [TestFixtureSetUp] + public void Init() + { + String srcset, srcsetWidth, srcsetHeight, srcsetAspectRatio, srcsetWidthAndHeight, srcsetWidthAndAspectRatio, srcsetHeightAndAspectRatio; + + UrlBuilder ub = new UrlBuilder("test.imgix.net", "MYT0KEN", false, true); + parameters = new Dictionary(); + + srcset = ub.BuildSrcSet("image.jpg"); + srcsetSplit = srcset.Split(','); + + parameters.Add("w", "300"); + srcsetWidth = ub.BuildSrcSet("image.jpg", parameters); + srcsetWidthSplit = srcsetWidth.Split(','); + parameters.Clear(); + + parameters.Add("h", "300"); + srcsetHeight = ub.BuildSrcSet("image.jpg", parameters); + srcsetHeightSplit = srcsetHeight.Split(','); + parameters.Clear(); + + parameters.Add("ar", "3:2"); + srcsetAspectRatio = ub.BuildSrcSet("image.jpg", parameters); + srcsetAspectRatioSplit = srcsetAspectRatio.Split(','); + parameters.Clear(); + + parameters.Add("w", "300"); + parameters.Add("h", "400"); + srcsetWidthAndHeight = ub.BuildSrcSet("image.jpg", parameters); + srcsetWidthAndHeightSplit = srcsetWidthAndHeight.Split(','); + parameters.Clear(); + + parameters.Add("w", "300"); + parameters.Add("ar", "3:2"); + srcsetWidthAndAspectRatio = ub.BuildSrcSet("image.jpg", parameters); + srcsetWidthAndAspectRatioSplit = srcsetWidthAndAspectRatio.Split(','); + parameters.Clear(); + + parameters.Add("h", "300"); + parameters.Add("ar", "3:2"); + srcsetHeightAndAspectRatio = ub.BuildSrcSet("image.jpg", parameters); + srcsetHeightAndAspectRatioSplit = srcsetHeightAndAspectRatio.Split(','); + parameters.Clear(); + } + + [Test] + public void NoParametersGeneratesCorrectWidths() + { + int[] targetWidths = {100, 116, 134, 156, 182, 210, 244, 282, + 328, 380, 442, 512, 594, 688, 798, 926, + 1074, 1246, 1446, 1678, 1946, 2258, 2618, + 3038, 3524, 4088, 4742, 5500, 6380, 7400, 8192}; + + String generatedWidth; + int index = 0; + int widthInt; + + foreach (String src in srcsetSplit) + { + generatedWidth = src.Split(' ')[1]; + widthInt = int.Parse(generatedWidth.Substring(0, generatedWidth.Length - 1)); + Assert.AreEqual(targetWidths[index], widthInt); + index++; + } + } + + [Test] + public void NoParametersReturnsExpectedNumberOfPairs() + { + int expectedPairs = 31; + Assert.AreEqual(expectedPairs, srcsetSplit.Length); + } + + [Test] + public void NoParametersDoesNotExceedBounds() + { + String minWidth = srcsetSplit[0].Split(' ')[1]; + String maxWidth = srcsetSplit[srcsetSplit.Length - 1].Split(' ')[1]; + + int minWidthInt = int.Parse(minWidth.Substring(0, minWidth.Length - 1)); + int maxWidthInt = int.Parse(maxWidth.Substring(0, maxWidth.Length - 1)); + + Assert.True(minWidthInt >= 100); + Assert.True(maxWidthInt <= 8192); + } + + // a 17% testing threshold is used to account for rounding + [Test] + public void NoParametersDoesNotIncreaseMoreThan17Percent() + { + const double INCREMENT_ALLOWED = .17; + String width; + int widthInt, prev; + + // convert and store first width (typically: 100) + width = srcsetSplit[0].Split(' ')[1]; + prev = int.Parse(width.Substring(0, width.Length - 1)); + + foreach (String src in srcsetSplit) + { + width = src.Split(' ')[1]; + widthInt = int.Parse(width.Substring(0, width.Length - 1)); + + Assert.True((widthInt / prev) < (1 + INCREMENT_ALLOWED)); + prev = widthInt; + } + } + + [Test] + public void NoParametersSignsUrls() + { + String src, parameters, generatedSignature, signatureBase, expectedSignature; + + foreach (String srcLine in srcsetSplit) + { + + src = srcLine.Split(' ')[0]; + Assert.True(src.Contains("s=")); + generatedSignature = src.Substring(src.IndexOf("s=") + 2); + + // calculate the number of chars between ? and s= + var parameterAll = src.Substring(src.IndexOf("?")).Length; + var parameterSignKey = src.Substring(src.IndexOf("s=")).Length; + + parameters = src.Substring(src.IndexOf("?"), parameterAll - parameterSignKey - 1); + signatureBase = "MYT0KEN" + "/image.jpg" + parameters; + var hashString = String.Format("{0}/{1}{2}", "MYT0KEN", "image.jpg", parameters); + expectedSignature = BitConverter.ToString(MD5.Create().ComputeHash(signatureBase.Select(Convert.ToByte).ToArray())).Replace("-", "").ToLower(); + + Assert.AreEqual(expectedSignature, generatedSignature); + } + } + + [Test] + public void WidthInDPRForm() + { + String generatedRatio; + int expectedRatio = 1; + Assert.True(srcsetWidthSplit.Length == 5); + + foreach (String src in srcsetWidthSplit) + { + generatedRatio = src.Split(' ')[1]; + Assert.AreEqual(expectedRatio + "x", generatedRatio); + expectedRatio++; + } + } + + [Test] + public void WidthSignsUrls() + { + String src, parameters, generatedSignature, signatureBase, expectedSignature; + + foreach (String srcLine in srcsetWidthSplit) + { + + src = srcLine.Split(' ')[0]; + Assert.True(src.Contains("s=")); + generatedSignature = src.Substring(src.IndexOf("s=") + 2); + + // calculate the number of chars between ? and s= + var parameterAll = src.Substring(src.IndexOf("?")).Length; + var parameterSignKey = src.Substring(src.IndexOf("s=")).Length; + + parameters = src.Substring(src.IndexOf("?"), parameterAll - parameterSignKey - 1); + signatureBase = "MYT0KEN" + "/image.jpg" + parameters; + var hashString = String.Format("{0}/{1}{2}", "MYT0KEN", "image.jpg", parameters); + expectedSignature = BitConverter.ToString(MD5.Create().ComputeHash(signatureBase.Select(Convert.ToByte).ToArray())).Replace("-", "").ToLower(); + + Assert.AreEqual(expectedSignature, generatedSignature); + } + } + + [Test] + public void WidthIncludesDPRParam() + { + String src; + + for (int i = 0; i < srcsetWidthSplit.Length; i++) + { + src = srcsetWidthSplit[i].Split(' ')[0]; + Assert.True(src.Contains(String.Format("dpr={0}", i + 1))); + } + } + + [Test] + public void HeightGeneratesCorrectWidths() + { + int[] targetWidths = {100, 116, 134, 156, 182, 210, 244, 282, + 328, 380, 442, 512, 594, 688, 798, 926, + 1074, 1246, 1446, 1678, 1946, 2258, 2618, + 3038, 3524, 4088, 4742, 5500, 6380, 7400, 8192}; + + String generatedWidth; + int index = 0; + int widthInt; + + foreach (String src in srcsetHeightSplit) + { + generatedWidth = src.Split(' ')[1]; + widthInt = int.Parse(generatedWidth.Substring(0, generatedWidth.Length - 1)); + Assert.AreEqual(targetWidths[index], widthInt); + index++; + } + } + + [Test] + public void HeightContainsHeightParameter() + { + String url; + + foreach (String src in srcsetHeightSplit) + { + url = src.Split(' ')[0]; + Assert.True(url.Contains("h=")); + } + } + + [Test] + public void HeightReturnsExpectedNumberOfPairs() + { + int expectedPairs = 31; + Assert.AreEqual(expectedPairs, srcsetHeightSplit.Length); + } + + [Test] + public void HeightDoesNotExceedBounds() + { + String minWidth = srcsetHeightSplit[0].Split(' ')[1]; + String maxWidth = srcsetHeightSplit[srcsetHeightSplit.Length - 1].Split(' ')[1]; + + int minWidthInt = int.Parse(minWidth.Substring(0, minWidth.Length - 1)); + int maxWidthInt = int.Parse(maxWidth.Substring(0, maxWidth.Length - 1)); + + Assert.True(minWidthInt >= 100); + Assert.True(maxWidthInt <= 8192); + } + + // a 17% testing threshold is used to account for rounding + [Test] + public void testHeightDoesNotIncreaseMoreThan17Percent() + { + const double INCREMENT_ALLOWED = .17; + String width; + int widthInt, prev; + + // convert and store first width (typically: 100) + width = srcsetHeightSplit[0].Split(' ')[1]; + prev = int.Parse(width.Substring(0, width.Length - 1)); + + foreach (String src in srcsetHeightSplit) + { + width = src.Split(' ')[1]; + widthInt = int.Parse(width.Substring(0, width.Length - 1)); + + Assert.True((widthInt / prev) < (1 + INCREMENT_ALLOWED)); + prev = widthInt; + } + } + + [Test] + public void testHeightSignsUrls() + { + String src, parameters, generatedSignature, signatureBase, expectedSignature; + + foreach (String srcLine in srcsetHeightSplit) + { + + src = srcLine.Split(' ')[0]; + Assert.True(src.Contains("s=")); + generatedSignature = src.Substring(src.IndexOf("s=") + 2); + + // calculate the number of chars between ? and s= + var parameterAll = src.Substring(src.IndexOf("?")).Length; + var parameterSignKey = src.Substring(src.IndexOf("s=")).Length; + + parameters = src.Substring(src.IndexOf("?"), parameterAll - parameterSignKey - 1); + signatureBase = "MYT0KEN" + "/image.jpg" + parameters; + var hashString = String.Format("{0}/{1}{2}", "MYT0KEN", "image.jpg", parameters); + expectedSignature = BitConverter.ToString(MD5.Create().ComputeHash(signatureBase.Select(Convert.ToByte).ToArray())).Replace("-", "").ToLower(); + + Assert.AreEqual(expectedSignature, generatedSignature); + } + } + + [Test] + public void WidthAndHeightInDPRForm() + { + String generatedRatio; + int expectedRatio = 1; + Assert.True(srcsetWidthAndHeightSplit.Length == 5); + + foreach (String src in srcsetWidthAndHeightSplit) + { + generatedRatio = src.Split(' ')[1]; + Assert.AreEqual(expectedRatio + "x", generatedRatio); + expectedRatio++; + } + } + + [Test] + public void WidthAndHeightSignsUrls() + { + String src, parameters, generatedSignature, signatureBase, expectedSignature; + + foreach (String srcLine in srcsetWidthAndHeightSplit) + { + + src = srcLine.Split(' ')[0]; + Assert.True(src.Contains("s=")); + generatedSignature = src.Substring(src.IndexOf("s=") + 2); + + // calculate the number of chars between ? and s= + var parameterAll = src.Substring(src.IndexOf("?")).Length; + var parameterSignKey = src.Substring(src.IndexOf("s=")).Length; + + parameters = src.Substring(src.IndexOf("?"), parameterAll - parameterSignKey - 1); + signatureBase = "MYT0KEN" + "/image.jpg" + parameters; + var hashString = String.Format("{0}/{1}{2}", "MYT0KEN", "image.jpg", parameters); + expectedSignature = BitConverter.ToString(MD5.Create().ComputeHash(signatureBase.Select(Convert.ToByte).ToArray())).Replace("-", "").ToLower(); + + Assert.AreEqual(expectedSignature, generatedSignature); + } + } + + [Test] + public void WidthAndHeightIncludesDPRParam() + { + String src; + + for (int i = 0; i < srcsetWidthAndHeightSplit.Length; i++) + { + src = srcsetWidthAndHeightSplit[i].Split(' ')[0]; + Assert.True(src.Contains(String.Format("dpr={0}", i + 1))); + } + } + + [Test] + public void AspectRatioGeneratesCorrectWidths() + { + int[] targetWidths = {100, 116, 134, 156, 182, 210, 244, 282, + 328, 380, 442, 512, 594, 688, 798, 926, + 1074, 1246, 1446, 1678, 1946, 2258, 2618, + 3038, 3524, 4088, 4742, 5500, 6380, 7400, 8192}; + + String generatedWidth; + int index = 0; + int widthInt; + + foreach (String src in srcsetAspectRatioSplit) + { + generatedWidth = src.Split(' ')[1]; + widthInt = int.Parse(generatedWidth.Substring(0, generatedWidth.Length - 1)); + Assert.AreEqual(targetWidths[index], widthInt); + index++; + } + } + + [Test] + public void AspectRatioContainsARParameter() + { + String url; + + foreach (String src in srcsetAspectRatioSplit) + { + url = src.Split(' ')[0]; + Assert.True(url.Contains("ar=")); + } + } + + [Test] + public void AspectRatioReturnsExpectedNumberOfPairs() + { + int expectedPairs = 31; + Assert.AreEqual(expectedPairs, srcsetAspectRatioSplit.Length); + } + + [Test] + public void AspectRatioDoesNotExceedBounds() + { + String minWidth = srcsetAspectRatioSplit[0].Split(' ')[1]; + String maxWidth = srcsetAspectRatioSplit[srcsetAspectRatioSplit.Length - 1].Split(' ')[1]; + + int minWidthInt = int.Parse(minWidth.Substring(0, minWidth.Length - 1)); + int maxWidthInt = int.Parse(maxWidth.Substring(0, maxWidth.Length - 1)); + + Assert.True(minWidthInt >= 100); + Assert.True(maxWidthInt <= 8192); + } + + // a 17% testing threshold is used to account for rounding + [Test] + public void AspectRatioDoesNotIncreaseMoreThan17Percent() + { + const double INCREMENT_ALLOWED = .17; + String width; + int widthInt, prev; + + // convert and store first width (typically: 100) + width = srcsetAspectRatioSplit[0].Split(' ')[1]; + prev = int.Parse(width.Substring(0, width.Length - 1)); + + foreach (String src in srcsetAspectRatioSplit) + { + width = src.Split(' ')[1]; + widthInt = int.Parse(width.Substring(0, width.Length - 1)); + + Assert.True((widthInt / prev) < (1 + INCREMENT_ALLOWED)); + prev = widthInt; + } + } + + [Test] + public void AspectRatioSignsUrls() + { + String src, parameters, generatedSignature, signatureBase, expectedSignature; + + foreach (String srcLine in srcsetAspectRatioSplit) + { + + src = srcLine.Split(' ')[0]; + Assert.True(src.Contains("s=")); + generatedSignature = src.Substring(src.IndexOf("s=") + 2); + + // calculate the number of chars between ? and s= + var parameterAll = src.Substring(src.IndexOf("?")).Length; + var parameterSignKey = src.Substring(src.IndexOf("s=")).Length; + + parameters = src.Substring(src.IndexOf("?"), parameterAll - parameterSignKey - 1); + signatureBase = "MYT0KEN" + "/image.jpg" + parameters; + var hashString = String.Format("{0}/{1}{2}", "MYT0KEN", "image.jpg", parameters); + expectedSignature = BitConverter.ToString(MD5.Create().ComputeHash(signatureBase.Select(Convert.ToByte).ToArray())).Replace("-", "").ToLower(); + + Assert.AreEqual(expectedSignature, generatedSignature); + } + } + + [Test] + public void WidthAndAspectRatioInDPRForm() + { + String generatedRatio; + int expectedRatio = 1; + Assert.True(srcsetWidthAndAspectRatioSplit.Length == 5); + + foreach (String src in srcsetWidthAndAspectRatioSplit) + { + generatedRatio = src.Split(' ')[1]; + Assert.AreEqual(expectedRatio + "x", generatedRatio); + expectedRatio++; + } + } + + [Test] + public void WidthAndAspectRatioSignsUrls() + { + String src, parameters, generatedSignature, signatureBase, expectedSignature; + + foreach (String srcLine in srcsetWidthAndAspectRatioSplit) + { + + src = srcLine.Split(' ')[0]; + Assert.True(src.Contains("s=")); + generatedSignature = src.Substring(src.IndexOf("s=") + 2); + + // calculate the number of chars between ? and s= + var parameterAll = src.Substring(src.IndexOf("?")).Length; + var parameterSignKey = src.Substring(src.IndexOf("s=")).Length; + + parameters = src.Substring(src.IndexOf("?"), parameterAll - parameterSignKey - 1); + signatureBase = "MYT0KEN" + "/image.jpg" + parameters; + var hashString = String.Format("{0}/{1}{2}", "MYT0KEN", "image.jpg", parameters); + expectedSignature = BitConverter.ToString(MD5.Create().ComputeHash(signatureBase.Select(Convert.ToByte).ToArray())).Replace("-", "").ToLower(); + + Assert.AreEqual(expectedSignature, generatedSignature); + } + } + + [Test] + public void WidthAndAspectRatioIncludesDPRParam() + { + String src; + + for (int i = 0; i < srcsetWidthAndAspectRatioSplit.Length; i++) + { + src = srcsetWidthAndAspectRatioSplit[i].Split(' ')[0]; + Assert.True(src.Contains(String.Format("dpr={0}", i + 1))); + } + } + + [Test] + public void HeightAndAspectRatioInDPRForm() + { + String generatedRatio; + int expectedRatio = 1; + Assert.True(srcsetHeightAndAspectRatioSplit.Length == 5); + + foreach (String src in srcsetHeightAndAspectRatioSplit) + { + generatedRatio = src.Split(' ')[1]; + Assert.AreEqual(String.Format("{0}", expectedRatio + "x"), generatedRatio); + expectedRatio++; + } + } + + [Test] + public void HeightAndAspectRatioSignsUrls() + { + String src, parameters, generatedSignature, signatureBase, expectedSignature; + + foreach (String srcLine in srcsetHeightAndAspectRatioSplit) + { + + src = srcLine.Split(' ')[0]; + Assert.True(src.Contains("s=")); + generatedSignature = src.Substring(src.IndexOf("s=") + 2); + + // calculate the number of chars between ? and s= + var parameterAll = src.Substring(src.IndexOf("?")).Length; + var parameterSignKey = src.Substring(src.IndexOf("s=")).Length; + + parameters = src.Substring(src.IndexOf("?"), parameterAll - parameterSignKey - 1); + signatureBase = "MYT0KEN" + "/image.jpg" + parameters; + var hashString = String.Format("{0}/{1}{2}", "MYT0KEN", "image.jpg", parameters); + expectedSignature = BitConverter.ToString(MD5.Create().ComputeHash(signatureBase.Select(Convert.ToByte).ToArray())).Replace("-", "").ToLower(); + + Assert.AreEqual(expectedSignature, generatedSignature); + } + } + + [Test] + public void HeightAndAspectRatioIncludesDPRParam() + { + String src; + + for (int i = 0; i < srcsetHeightAndAspectRatioSplit.Length; i++) + { + src = srcsetHeightAndAspectRatioSplit[i].Split(' ')[0]; + Assert.True(src.Contains(String.Format("dpr={0}", i + 1))); + } + } + } +}