Skip to content
Browse files

Initial commit

  • Loading branch information...
0 parents commit e67f9edc98569089fa29f7e4128d0cf9f0637cfb Dave McDermid committed Dec 16, 2011
Showing with 298 additions and 0 deletions.
  1. +3 −0 .gitignore
  2. +183 −0 AdaptiveImageHandler.cs
  3. +56 −0 AdaptiveImages.csproj
  4. +20 −0 AdaptiveImages.sln
  5. +36 −0 Properties/AssemblyInfo.cs
3 .gitignore
@@ -0,0 +1,3 @@
+bin
+obj
+*.suo
183 AdaptiveImageHandler.cs
@@ -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 AdaptiveImages.csproj
@@ -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 AdaptiveImages.sln
@@ -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 Properties/AssemblyInfo.cs
@@ -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.
Something went wrong with that request. Please try again.