-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Dave McDermid
committed
Dec 16, 2011
0 parents
commit e67f9ed
Showing
5 changed files
with
298 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
bin | ||
obj | ||
*.suo |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Web; | ||
using System.IO; | ||
using System.Drawing; | ||
using System.Drawing.Drawing2D; | ||
using System.Drawing.Text; | ||
using System.Drawing.Imaging; | ||
|
||
namespace AdaptiveImages | ||
{ | ||
class AdaptiveImageHandler : IHttpHandler | ||
{ | ||
private static int[] resolutions = { 1382, 992, 768, 480 }; // the resolution break-points to use (screen widths, in pixels) | ||
private static string cache_path = "ai-cache"; // where to store the generated re-sized images. This folder must be writable. | ||
private static long jpg_quality = 80L; // the quality of any generated JPGs on a scale of 0 to 100 | ||
private static bool watch_cache = true; // check that the responsive image isn't stale (ensures updated source images are re-cached) | ||
private static int browser_cache = 60 * 60 * 24 * 7; // How long the BROWSER cache should last (seconds, minutes, hours, days. 7days by default) | ||
private static bool mobile_first = true; // If there's no cookie FALSE sends the largest var resolutions version (TRUE sends smallest) | ||
private static string cookie_name = "resolution"; // the name of the cookie containing the resolution value | ||
|
||
private static string[] desktop_oss = { "macintosh", "x11", "windows nt" }; | ||
private static string[] image_exts = { ".png", ".gif", ".jpeg" }; | ||
|
||
public bool IsReusable | ||
{ | ||
get { return false; } | ||
} | ||
|
||
public void ProcessRequest(HttpContext context) | ||
{ | ||
|
||
var requested_file = context.Request.RawUrl; | ||
var source_file = context.Server.MapPath(requested_file); | ||
int resolution = 0; | ||
|
||
//check source file exists | ||
if (!File.Exists(source_file)) | ||
SendErrorImage(context, "Image not found"); | ||
//look for cookie identifying resolution | ||
if (context.Request.Cookies[cookie_name] != null) { | ||
int client_width = 0; | ||
if (int.TryParse(context.Request.Cookies[cookie_name].Value, out client_width)) { | ||
resolution = resolutions.OrderBy(i => i).FirstOrDefault(break_point => client_width <= break_point); | ||
} else { | ||
//delete the mangled cookie | ||
context.Response.Cookies[cookie_name].Value = string.Empty; | ||
context.Response.Cookies[cookie_name].Expires = DateTime.Now; | ||
} | ||
} | ||
//if no resolution set, use default | ||
if (resolution == 0) { | ||
resolution = mobile_first && !BrowserDetect(context) ? resolutions.Min() : resolutions.Max(); | ||
} | ||
//map path to cached file | ||
string cache_file = context.Server.MapPath(string.Format("/{0}/{1}/{2}", cache_path, resolution, requested_file)); | ||
//send image | ||
try { | ||
if (File.Exists(cache_file)) { // it exists cached at that size | ||
if (watch_cache) { // if cache watching is enabled, compare cache and source modified dates to ensure the cache isn't stale | ||
cache_file = RefreshCache(source_file, cache_file, resolution); | ||
} | ||
//send cached image | ||
SendImage(context, cache_file, browser_cache); | ||
} else { | ||
string file = GenerateImage(source_file, cache_file, resolution); | ||
SendImage(context, file, browser_cache); | ||
} | ||
} catch (Exception ex) { // send exception message as image | ||
SendErrorImage(context, ex.Message); | ||
} | ||
} | ||
|
||
//Switch off mobile-first if browser is identifying as desktop | ||
private bool BrowserDetect(HttpContext context) | ||
{ | ||
string userAgent = context.Request.UserAgent.ToLower(); | ||
|
||
// Identify the OS platform. Match only desktop OSs | ||
return desktop_oss.Any(os => userAgent.Contains(os)); | ||
} | ||
|
||
/// <summary>Sends an image to the client with caching enabled</summary> | ||
private void SendImage(HttpContext context, string filename, int browser_cache) | ||
{ | ||
string extension = Path.GetExtension(filename).ToLower(); | ||
context.Response.ContentType = "image/" + (image_exts.Contains(extension) ? extension.TrimStart('.') : "jpeg"); | ||
context.Response.Cache.SetCacheability(HttpCacheability.Private); | ||
context.Response.Cache.SetMaxAge(TimeSpan.FromSeconds(browser_cache)); | ||
context.Response.ExpiresAbsolute = DateTime.UtcNow.AddSeconds(browser_cache); | ||
context.Response.TransmitFile(filename); | ||
} | ||
|
||
/// <summary>Sends an imaeg to the client with the specified message</summary> | ||
private void SendErrorImage(HttpContext context, string message) | ||
{ | ||
Bitmap objBmpImage = new Bitmap(800, 200); | ||
Font objFont = new Font("Arial", 20, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Pixel); | ||
Graphics objGraphics = Graphics.FromImage(objBmpImage); | ||
objGraphics = Graphics.FromImage(objBmpImage); | ||
objGraphics.Clear(Color.White); | ||
objGraphics.SmoothingMode = SmoothingMode.AntiAlias; | ||
objGraphics.TextRenderingHint = TextRenderingHint.AntiAlias; | ||
objGraphics.DrawString(message, objFont, new SolidBrush(Color.FromArgb(102, 102, 102)), 0, 0); | ||
objGraphics.Flush(); | ||
|
||
context.Response.ContentType = "image/jpeg"; | ||
context.Response.Cache.SetCacheability(HttpCacheability.NoCache); | ||
context.Response.Expires = -1; | ||
objBmpImage.Save(context.Response.OutputStream, ImageFormat.Jpeg); | ||
} | ||
|
||
/// <summary>Deletes the cache_file if older than source_file</summary> | ||
private string RefreshCache(string source_file, string cache_file, int resolution) | ||
{ | ||
if (File.Exists(cache_file)) { | ||
// not modified | ||
if (File.GetLastWriteTime(cache_file) >= File.GetLastWriteTime(source_file)) { | ||
return cache_file; | ||
} | ||
// modified, clear it | ||
File.Delete(cache_file); | ||
} | ||
return GenerateImage(source_file, cache_file, resolution); | ||
} | ||
|
||
/// <summary>Generates a resized image at a specified resolution from the source_file and saves to the cache_file</summary> | ||
private string GenerateImage(string source_file, string cache_file, int resolution) | ||
{ | ||
string extension = Path.GetExtension(source_file).ToLower(); | ||
using (Image source_image = Image.FromFile(source_file)) { | ||
// Check the image dimensions | ||
int width = source_image.Size.Width; | ||
int height = source_image.Size.Height; | ||
|
||
// Do we need to downscale the image? | ||
if (width <= resolution) { // no, because the width of the source image is already less than the client width | ||
return source_file; | ||
} | ||
|
||
// We need to resize the source image to the width of the resolution breakpoint we're working with | ||
float ratio = (float)height / width; | ||
int new_width = resolution; | ||
int new_height = (int)Math.Ceiling(new_width * ratio); | ||
|
||
using (Image scaled_image = new Bitmap(new_width, new_height)) { | ||
Graphics graphic = Graphics.FromImage(scaled_image); | ||
//Set interpolation mode, otherwise it looks rubbish. | ||
graphic.InterpolationMode = InterpolationMode.HighQualityBicubic; | ||
graphic.SmoothingMode = SmoothingMode.HighQuality; | ||
graphic.PixelOffsetMode = PixelOffsetMode.HighQuality; | ||
graphic.CompositingQuality = CompositingQuality.HighQuality; | ||
//Draw the original image as a scaled image | ||
graphic.DrawImage(source_image, new Rectangle(0, 0, new_width, new_height), new RectangleF(0, 0, width, height), GraphicsUnit.Pixel); | ||
//create cache directory if it doesn't exist | ||
if (!Directory.Exists(Path.GetDirectoryName(cache_file))) | ||
Directory.CreateDirectory(Path.GetDirectoryName(cache_file)); | ||
//save image in appropriate format | ||
switch (extension) { | ||
case ".png": | ||
scaled_image.Save(cache_file, ImageFormat.Png); | ||
break; | ||
case ".gif": | ||
scaled_image.Save(cache_file, ImageFormat.Gif); | ||
break; | ||
default: | ||
EncoderParameters ep = new EncoderParameters(); | ||
ep.Param[0] = new EncoderParameter(Encoder.Quality, jpg_quality); | ||
scaled_image.Save(cache_file, GetEncoderForMimeType("image/jpeg"), ep); | ||
break; | ||
} | ||
} | ||
} | ||
return cache_file; | ||
} | ||
|
||
private ImageCodecInfo GetEncoderForMimeType(string mimeType) | ||
{ | ||
return ImageCodecInfo.GetImageEncoders().FirstOrDefault(e => string.Compare(e.MimeType, mimeType, true) == 0); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | ||
<PropertyGroup> | ||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> | ||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> | ||
<ProductVersion>8.0.30703</ProductVersion> | ||
<SchemaVersion>2.0</SchemaVersion> | ||
<ProjectGuid>{E9A94104-2FFF-429D-A969-142E511AA2B7}</ProjectGuid> | ||
<OutputType>Library</OutputType> | ||
<AppDesignerFolder>Properties</AppDesignerFolder> | ||
<RootNamespace>AdaptiveImages</RootNamespace> | ||
<AssemblyName>AdaptiveImages</AssemblyName> | ||
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion> | ||
<FileAlignment>512</FileAlignment> | ||
</PropertyGroup> | ||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> | ||
<DebugSymbols>true</DebugSymbols> | ||
<DebugType>full</DebugType> | ||
<Optimize>false</Optimize> | ||
<OutputPath>bin\Debug\</OutputPath> | ||
<DefineConstants>DEBUG;TRACE</DefineConstants> | ||
<ErrorReport>prompt</ErrorReport> | ||
<WarningLevel>4</WarningLevel> | ||
</PropertyGroup> | ||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> | ||
<DebugType>pdbonly</DebugType> | ||
<Optimize>true</Optimize> | ||
<OutputPath>bin\Release\</OutputPath> | ||
<DefineConstants>TRACE</DefineConstants> | ||
<ErrorReport>prompt</ErrorReport> | ||
<WarningLevel>4</WarningLevel> | ||
</PropertyGroup> | ||
<ItemGroup> | ||
<Reference Include="System" /> | ||
<Reference Include="System.Core" /> | ||
<Reference Include="System.Drawing" /> | ||
<Reference Include="System.Web" /> | ||
<Reference Include="System.Xml.Linq" /> | ||
<Reference Include="System.Data.DataSetExtensions" /> | ||
<Reference Include="Microsoft.CSharp" /> | ||
<Reference Include="System.Data" /> | ||
<Reference Include="System.Xml" /> | ||
</ItemGroup> | ||
<ItemGroup> | ||
<Compile Include="AdaptiveImageHandler.cs" /> | ||
<Compile Include="Properties\AssemblyInfo.cs" /> | ||
</ItemGroup> | ||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> | ||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. | ||
Other similar extension points exist, see Microsoft.Common.targets. | ||
<Target Name="BeforeBuild"> | ||
</Target> | ||
<Target Name="AfterBuild"> | ||
</Target> | ||
--> | ||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
|
||
Microsoft Visual Studio Solution File, Format Version 11.00 | ||
# Visual Studio 2010 | ||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveImages", "AdaptiveImages.csproj", "{E9A94104-2FFF-429D-A969-142E511AA2B7}" | ||
EndProject | ||
Global | ||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||
Debug|Any CPU = Debug|Any CPU | ||
Release|Any CPU = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||
{E9A94104-2FFF-429D-A969-142E511AA2B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
{E9A94104-2FFF-429D-A969-142E511AA2B7}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
{E9A94104-2FFF-429D-A969-142E511AA2B7}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
{E9A94104-2FFF-429D-A969-142E511AA2B7}.Release|Any CPU.Build.0 = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(SolutionProperties) = preSolution | ||
HideSolutionNode = FALSE | ||
EndGlobalSection | ||
EndGlobal |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
using System.Reflection; | ||
using System.Runtime.CompilerServices; | ||
using System.Runtime.InteropServices; | ||
|
||
// General Information about an assembly is controlled through the following | ||
// set of attributes. Change these attribute values to modify the information | ||
// associated with an assembly. | ||
[assembly: AssemblyTitle("AdaptiveImages")] | ||
[assembly: AssemblyDescription("A C# port of adaptive-images.com")] | ||
[assembly: AssemblyConfiguration("")] | ||
[assembly: AssemblyCompany("Dave McDermid")] | ||
[assembly: AssemblyProduct("AdaptiveImages")] | ||
[assembly: AssemblyCopyright("")] | ||
[assembly: AssemblyTrademark("")] | ||
[assembly: AssemblyCulture("")] | ||
|
||
// Setting ComVisible to false makes the types in this assembly not visible | ||
// to COM components. If you need to access a type in this assembly from | ||
// COM, set the ComVisible attribute to true on that type. | ||
[assembly: ComVisible(false)] | ||
|
||
// The following GUID is for the ID of the typelib if this project is exposed to COM | ||
[assembly: Guid("88fc29ee-10a0-424f-b64e-388d36c5d0d9")] | ||
|
||
// Version information for an assembly consists of the following four values: | ||
// | ||
// Major Version | ||
// Minor Version | ||
// Build Number | ||
// Revision | ||
// | ||
// You can specify all the values or you can default the Build and Revision Numbers | ||
// by using the '*' as shown below: | ||
// [assembly: AssemblyVersion("1.0.*")] | ||
[assembly: AssemblyVersion("1.0.0.0")] | ||
[assembly: AssemblyFileVersion("1.0.0.0")] |