Custom mediaRequest in Sitecore 8

For a new project in Sitecore 8, we needed an Image cropper. However, the images needed to be cropped dynamically. We started off by implementing the image cropper made by Anders Laub (found here). After adding the custom image processor, we could use a few querystring parameters to crop the image. the important ones are:

  • cw : crop width
  • ch : crop height
  • c : indicate that this cropper is to be used

However, when we got this working we noticed that the image was cached by sitecore, but the new querystring parameters were not. When an image is being processed, a cache is created in the app_data/mediacache folder. In a subfolder the transformed image is stored along with some info in a .ini file. The contents of the file will look something like this:

1
2
3
4
5
6
7
8
9
10
11
[key]
?as=False&bc=0&h=0&iar=False&mh=0&mw=0&sc=0&thn=False&w=0

[extension]
png

[headers]
Content-Type: image/png

[dataFile]
3f427bbc14fa4e4aa399266c96221b51.png

The key obviously contains the querystring parameters used in the url to link the image. Notice how all the default parameters are included, but none of the custom ones for the image cropper. To force sitecore to do that you need to write a custom mediarequest class that inherits from the MediaRequest class, found in the Sitecore.Resources.Media namespace. In your new class, you need to override the GetOptions() and Clone() methods of the MediaRequest class. I came up with a super original name for my new class, the CustomMediaRequest class.

1
2
3
4
5
6
7
public override MediaRequest Clone()
{

Assert.IsTrue((bool)(base.GetType() == typeof(CustomMediaRequest)),
"The Clone() method must be overridden to support prototyping.");
return new CustomMediaRequest { innerRequest = this.innerRequest, mediaUri = this.mediaUri,
options = this.options, mediaQueryString = this.mediaQueryString };
}

Since I didn’t need any fancy things done here I basically copied the existing code and changed the MediaRequest to CustomMediaRequest. The GetOptions() method is also mostly a copy of the original, but here I added some code to include the new querystring parameters. The parameters both custom and sitecore ones (KnownOptions) are stored in MediaOptions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
this.ProcessCustomParameters(options);                          

if (!options.CustomOptions.ContainsKey("c"))
{
options.CustomOptions.Add("c", queryString.Get("c"));
}
else
{
options.CustomOptions["c"] = queryString.Get("c");
}

if (!options.CustomOptions.ContainsKey("cw"))
{
options.CustomOptions.Add("cw", queryString.Get("cw"));
}
else
{
options.CustomOptions["cw"] = queryString.Get("cw");
}

if (!options.CustomOptions.ContainsKey("ch"))
{
options.CustomOptions.Add("ch", queryString.Get("ch"));
}
else
{
options.CustomOptions["ch"] = queryString.Get("ch");
}

Finally, we need to add an extra config file in the app_config/include folder. I named mine Sitecore.Media.RequestParser.config since we are replacing the default requestparser with our own.

1
2
<requestParser type="Company.SC.Requests.CustomMediaRequest, Company.SC" 
patch:instead="processor[@type='Sitecore.Resources.Media.MediaRequest, Sitecore.Kernel']" />

Notice how we added the patch:instead. Patch:After won’t work as only one RequestParser is used. you also need to patch the Sitecore.Media.RequestProtection.config file, so that sitecore knows about these parameters.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<protectedMediaQueryParameters>
...
<parameter description="crop query key" name="c"/>
<parameter description="crop width query key" name="cw"/>
<parameter description="crop height query key" name="ch"/>
<parameter description="jpeg compression" name="jq"/>
</protectedMediaQueryParameters>

<customMediaQueryParameters>
<parameter description="image encoding" name="enc"/>
</customMediaQueryParameters>
You need to patch this before the renderWebEditing pipeline.

<pipelines>
<renderField>
<processor patch:before="processor[@type='Sitecore.Pipelines.RenderField.RenderWebEditing, Sitecore.Kernel']"
type="Sitecore.Pipelines.RenderField.ProtectedImageLinkRenderer, Sitecore.Kernel" />
</renderField>
</pipelines>

When this is done, the CustomOptions will be added to the querystring and the image cache key in the ini .file will look like this:

1
2
[key]
?as=False&bc=0&h=0&iar=False&mh=0&mw=0&sc=0&thn=False&w=0&c=1&ch=550&cw=180

Now, every time you render the image with different cropping (querystring) settings, a new image is stored in the mediacache (sub)folder and a new key is added to the .ini file. I hope this short tutorial can help someone looking to extend the image caching.

The complete CustomMediaRequest class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
using System.Collections.Specialized;
using System.Web;
using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Resources.Media;

namespace YourProject.SC.Requests
{
public class CustomMediaRequest : MediaRequest
{
private HttpRequest innerRequest;
private bool isRawUrlSafe;
private bool isRawUrlSafeInitialized;
private MediaUrlOptions mediaQueryString;
private Sitecore.Resources.Media.MediaUri mediaUri;
private MediaOptions options;

protected override MediaOptions GetOptions()
{
NameValueCollection queryString = this.InnerRequest.QueryString;
if ((queryString == null) || queryString.HasKeys() )
{
options = new MediaOptions();
}
else
{
MediaUrlOptions mediaQueryString = this.GetMediaQueryString();
options = new MediaOptions
{
AllowStretch = mediaQueryString.AllowStretch,
BackgroundColor = mediaQueryString.BackgroundColor,
IgnoreAspectRatio = mediaQueryString.IgnoreAspectRatio,
Scale = mediaQueryString.Scale,
Width = mediaQueryString.Width,
Height = mediaQueryString.Height,
MaxWidth = mediaQueryString.MaxWidth,
MaxHeight = mediaQueryString.MaxHeight,
Thumbnail = mediaQueryString.Thumbnail
};
if (mediaQueryString.DisableMediaCache)
{
options.UseMediaCache = false;
}
string[] strArray = queryString.AllKeys;
for (int i = 0; i < strArray.Length; i = (int)(i + 1))
{
string str = strArray[i];
if ((str != null) && (queryString.Get(str) != null))
{
options.CustomOptions[str] = queryString.Get(str);
}
}
}
if (!this.IsRawUrlSafe)
{
if (Settings.Media.RequestProtection.LoggingEnabled)
{
string urlReferrer = this.GetUrlReferrer();
Log.SingleError(string.Format("MediaRequestProtection: An invalid/missing hash value was encountered.
The expected hash value: {0}. Media URL: {1}, Referring URL: {2}"
, HashingUtils.GetAssetUrlHash(this.InnerRequest.Path), this.InnerRequest.Path, string.IsNullOrEmpty(urlReferrer)
? ((object)"(empty)") : ((object)urlReferrer)), this);
}
options = new MediaOptions();
}

this.ProcessCustomParameters(options);

if (!options.CustomOptions.ContainsKey("c"))
{
options.CustomOptions.Add("c", queryString.Get("c"));
}
else
{
options.CustomOptions["c"] = queryString.Get("c");
}

if (!options.CustomOptions.ContainsKey("cw"))
{
options.CustomOptions.Add("cw", queryString.Get("cw"));
}
else
{
options.CustomOptions["cw"] = queryString.Get("cw");
}

if (!options.CustomOptions.ContainsKey("ch"))
{
options.CustomOptions.Add("ch", queryString.Get("ch"));
}
else
{
options.CustomOptions["ch"] = queryString.Get("ch");
}

return options;
}

public override MediaRequest Clone()
{
Assert.IsTrue((bool)(base.GetType() == typeof(CustomMediaRequest)),
"The Clone() method must be overridden to support prototyping.");
return new CustomMediaRequest { innerRequest = this.innerRequest,
mediaUri = this.mediaUri, options = this.options, mediaQueryString = this.mediaQueryString };
}
}
}

The complete Sitecore.Media.RequestParser.config file:

1
2
3
4
5
6
7
8
9
<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<mediaLibrary>
<requestParser type="Company.SC.Requests.CustomMediaRequest, Company.SC"
patch:instead="processor[@type='Sitecore.Resources.Media.MediaRequest, Sitecore.Kernel']" />
</mediaLibrary>
</sitecore>
</configuration>

Finally I’d like to thank Anders Laub, for his excellent Sitecore blog and image cropper code.