In my travels working with Wally McClure and David Silverlight on the Silverlight
3 UI client for Wally's TimedTweet Azure service, I discovered that when we bring
back a Twitter Timeline, all the users' profile images point to Amazon S3 storage.
There is no crossdomain.xml or clientaccesspolicy.xml file there, so all the
image url requests will be denied by Silverlight. Obviously, for better or worse,
Silverlight has been trained to know that any image that doesn't come from the
same domain it was served from is a baddie, unless there is an enabling policy
file there. Now of course all this extra checking means extra HTTP Requests over
the wire, but hey - that's progress.
Tim Heuer and others have some fancy workarounds involving DNS server settings, but
I think the easiest solution for me is the simplest one. Just make a "ClientAccess.ashx"
handler on the same server / domain that that Silverlight app is served from
, and convert all the Amazon image urls to point to ClientAccess.ashx?url=amazonimageurl
- with the original Amazon image url on the querystring. Your handler will then
read the "url" item from the Request.QueryString collection, make the
request for the image, and Response.BinaryWrite the image bytes out to the requesting
Silverlight client. Since Silverlight sees this as a same-domain request, there
are no security issues and your images will come back just fine.
There is an additional benefit of using this technique: Silverlight - even Silverlight
3 -- cannot display gif images natively. Some (but not all) of the Twitter user
Profile images are indeed gifs. There is some code out in the wild that will
decode gif images in Silverlight, but since I am already serving my images from
my server with the ASHX handler, it just makes sense to convert gifs to jpegs
there too. So all we need to do is have the ASHX handler check to see if ".gif"
is in the url it's been given, and if so, we can create a .NET Bitmap object
from the image bytes, convert it to a jpeg image, and rewrite out the new jpg
byte array and serve it out. Silverlight will now happily consume the converted
jpeg image and we've solved all our problems in one fell swoop. This approach
to handling the delivery of off-domain images will work equally well in Silverlight
2.
Here is an example of how this all works; for this "demo" I'm just doing
it with a single gif image from Amazon:
public MainPage()
{
InitializeComponent();
// test Amazon s3 url to a Twitter User's Profile Image that is a gif:
string amazonImageUrl =
"http://s3.amazonaws.com/twitter_production/profile_images/53709078/me2_bigger.gif";
string imgUrl =Application.Current.Host.Source.Scheme + "://" + Application.Current.Host.Source.Host + ":" +
Application.Current.Host.Source.Port + "/ClientAccess.ashx?url=" + amazonImageUrl;
image1.Source = new BitmapImage(new Uri(imgUrl, UriKind.RelativeOrAbsolute));
}
Here is the code for my ASHX handler:
using System;
using System.Collections;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml.Linq;
using System.Net;
namespace SLImageHandler.Web
{
public class ClientAccess : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
string imageUrl = context.Request["url"].ToString();
WebClient client = new WebClient();
byte[] bytes = client.DownloadData(new Uri(imageUrl));
// convert gif to jpg here
if(imageUrl.Contains(".gif"))
{
Bitmap bmp = (Bitmap)Bitmap.FromStream(new MemoryStream(bytes));
bytes = GifConverter.ConvertGif(bmp);
}
context.Response.BinaryWrite(bytes);
}
public bool IsReusable
{
get
{
return false;
}
}
}
And finally, here is the code for my little "GifConverter" utility:
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Web;
namespace SLImageHandler.Web
{
public class GifConverter
{
public static byte[] ConvertGif(Bitmap Image)
{
MemoryStream objStream = new MemoryStream();
ImageCodecInfo objImageCodecInfo = GetEncoderInfo("image/jpeg");
EncoderParameters objEncoderParameters;
try
{
if (Image == null)
throw new Exception("ImageObject is not initialized.");
objEncoderParameters = new EncoderParameters(3);
objEncoderParameters.Param[0] = new EncoderParameter(Encoder.Compression,
(long)EncoderValue.CompressionLZW);
objEncoderParameters.Param[1] = new EncoderParameter(Encoder.Quality, 100L);
objEncoderParameters.Param[2] = new EncoderParameter(Encoder.ColorDepth, 24L);
Image.Save(objStream, objImageCodecInfo, objEncoderParameters);
}
catch
{
throw;
}
return objStream.ToArray();
}
private static ImageCodecInfo GetEncoderInfo(String mimeType)
{
int j;
ImageCodecInfo[] encoders;
encoders = ImageCodecInfo.GetImageEncoders();
for (j = 0; j < encoders.Length; ++j)
{
if (encoders[j].MimeType == mimeType)
return encoders[j];
}
return null;
}
}
}
You can download the Visual Studio 2008 Silverlight 3 Beta solution here.