Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Dave McDermid committed Dec 16, 2011
0 parents commit e67f9ed
Show file tree
Hide file tree
Showing 5 changed files with 298 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
bin
obj
*.suo
183 changes: 183 additions & 0 deletions AdaptiveImageHandler.cs
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);
}
}
}
56 changes: 56 additions & 0 deletions AdaptiveImages.csproj
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>
20 changes: 20 additions & 0 deletions AdaptiveImages.sln
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
36 changes: 36 additions & 0 deletions Properties/AssemblyInfo.cs
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")]

0 comments on commit e67f9ed

Please sign in to comment.