using System;
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using Mapper;
using System.Drawing;
using System.IO;
using System.Web;
namespace CssSpriteGenerator
{
/// <summary>
/// Represents an individual image as used on the page or in the css
/// (that is, before it gets replaced by a sprite).
/// </summary>
public class ImageInfo : IImageInfo, IComparable<ImageInfo>
{
private class Sizes
{
// _reportedWidth and _reportedHeight holds the width and height of the image as reported to the outside world.
// It is based not only on the bitmap, but also on _resizeSize, _resizeSizeOverride.
// Calculating this may not involve the bitmap at all, if the width and height can be found from those 2 "resize..." variables.
// If a value is not known, it is set to -1.
private Size _reportedSize = new Size(-1, -1);
// An image may get resized for a couple of reasons:
// * the image tag contains a width and/or height property and the physical dimensions of the image are different.
// This can be disabled with the group property DisableAutoResize.
// For restrictions, see the DisableAutoResize property of classGroup.
// * the image is addded to a group with ResizeHeight or ResizeWidth.
//
// This Size can have these values:
// -2 means "not set". If one is -2, the other should be -2 as well. -1 means "use physical size.
// * If both are -2 or -1, use the physical size of the image
// * If one is -1 and the other is > -1, the aspect ratio will be used to deduce the one with -1.
private Size _resizeSize = new Size(-2, -2);
// The overrides are used to override the image's "normal" size.
// They get set based on the group's ResizeWidth and ResizeHeight.
// -1 means "not set" (so that's different from _resizeSize)
private Size _resizeSizeOverride = new Size(-1, -1);
public Size ReportedSize
{
// When setting resizeSize, also discard the reported size
set { _reportedSize = value; }
get { return _reportedSize; }
}
public Size ResizeSize
{
// When setting resizeSize, also discard the reported size
set { _resizeSize = value; _reportedSize = new Size(-1, -1); }
get { return _resizeSize; }
}
public Size ResizeSizeOverride
{
// When setting resizeSizeOverride, also discard the reported size
set { _resizeSizeOverride = value; _reportedSize = new Size(-1, -1); }
get { return _resizeSizeOverride; }
}
public bool ReportedSizeSet()
{
return ((_reportedSize.Width != -1) && (_reportedSize.Height != -1));
}
// Note that if Width and Height are -1, than they have been set.
public bool ResizeSizeSet()
{
return ((_resizeSize.Width != -2) && (_resizeSize.Height != -2));
}
// If both width and height of the override are -1, then there is no override
public bool ResizeSizeOverrideSet()
{
return ((_resizeSizeOverride.Width != -1) || (_resizeSizeOverride.Height != -1));
}
}
// ------------------
private StoredImage _storedImage = new StoredImage();
private Sizes _sizes = new Sizes();
private bool _isCombinable = true;
private bool _hasPageBasedReferences = false;
List<IImageReference> _imageReferences = new List<IImageReference>();
/// <summary>
/// Most restrictive combine restriction of all image references.
/// </summary>
private CombineRestriction _combineRestriction = CombineRestriction.None;
public Size ResizeSizeOverride { set { _sizes.ResizeSizeOverride = value; } }
/// <summary>
/// True if it is known that the OriginalImageFilePath is broken
/// (that is, doesn't point to a file, or the file cannot be read into a bitmap).
/// </summary>
/// <param name="url"></param>
public bool FilePathBroken
{
get { return _storedImage.FilePathBroken; }
}
/// <summary>
/// Gets the full image file path of the image, such as
/// c:\website\images\smallicon.png
/// </summary>
/// <returns></returns>
public string OriginalImageFilePath
{
get { return _storedImage.OriginalImageFilePath; }
}
/// <summary>
/// Gets the image type based on the extension of the full image file path of the image
/// TODO: performance: could store this in a private field.
/// </summary>
/// <returns></returns>
public ImageType ImageType
{
get { return UrlUtils.ImageTypeUrl(OriginalImageFilePath); }
}
/// <summary>
/// Width in px of the image.
/// </summary>
public int Width
{
get
{
return ReportedSize.Width;
}
}
/// <summary>
/// Height in px of the image
/// </summary>
public int Height
{
get
{
return ReportedSize.Height;
}
}
/// <summary>
/// Size of the image after it has been optionally resized (eg. because of group properties).
/// </summary>
public Size ReportedSize
{
get { return GetReportedSize(); }
}
/// <summary>
/// Size of the original image on disk
/// </summary>
public Size OriginalSize
{
get { return _storedImage.OriginalImageSize(); }
}
/// <summary>
/// Size in bytes of the image file. This is its actual size, not the size on disk.
/// </summary>
public long ImageFileSize
{
get
{
return _storedImage.GetImageFileSize();
}
}
/// <summary>
/// Bitmap representing the image
/// </summary>
public Bitmap ImageBitmap
{
get
{
return _storedImage.ImageBitmap(GetReportedSize());
}
}
/// <summary>
/// File path of the image on disk.
/// </summary>
public string ImageFilePath
{
get
{
return _storedImage.OriginalImageFilePath;
}
}
/// <summary>
/// List of all the image references that use this image.
/// </summary>
public List<IImageReference> ImageReferences
{
get { return _imageReferences; }
}
/// <summary>
/// If this returns false, than this ImageInfo is not actually combinable with anything else.
/// This could be because the image references that share the same image url have
/// conflicting combine restrictions.
/// </summary>
public bool IsCombinable
{
get { return _isCombinable; }
}
/// <summary>
/// True if this ImageInfo has references to images used on the page.
/// False if all references in this ImageInfo are either folder references (which were read from
/// a disk folder) or from the CSS images collection.
/// </summary>
public bool HasPageBasedReferences
{
get { return _hasPageBasedReferences; }
}
/// <summary>
/// Strictest combine restriction of all image references in this ImageInfo.
/// </summary>
public CombineRestriction CombineRestriction
{
get { return _combineRestriction; }
}
/// <summary>
/// Used by a IMapper. When it has processed this ImageInfo, it sets this flag to true.
/// </summary>
public bool Processed { get; set; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="firstImageReference">
/// First image reference to be added to the ImageInfo.
/// Note that there is no point in having an ImageInfo without image references.
///
/// It is up to the caller to make sure that firstImageReference.OriginalImageFilePath
/// is an image that lives on the web server (not external, not broken).
/// </param>
public ImageInfo(IImageReference firstImageReference)
{
_storedImage.OriginalImageFilePath = firstImageReference.OriginalImageFilePath;
_sizes.ResizeSize = firstImageReference.SizeFromProperties;
AddImageReference(firstImageReference);
Processed = false;
}
/// <summary>
/// Disposes the bitmap related to the image.
/// Does nothing if there is no bitmap.
///
/// You can still use the ImageInfo object after calling this.
/// If you access a property that requires the bitmap, it will simply load it again.
/// </summary>
public void DisposeBitmap()
{
_storedImage.DisposeBitmap();
}
/// <summary>
/// Call this to stop the ImageInfo from automatically resizing the image
/// based on width and height properties. Does not affect the dimensions set
/// with ResizeWidthOverride and ResizeHeightOverride.
/// </summary>
public void DisableAutoResize()
{
_sizes.ResizeSize = new Size(-1, -1);
}
/// <summary>
/// Returns true if the ImageInfo was instructed to resize the image,
/// via auto resize (based on width or height properties of img tag)
/// or via ResizeSizeOverride property.
/// </summary>
public bool MadeToResize()
{
bool madeToResize =
(!SizeUtils.IsEmptySize(_sizes.ResizeSizeOverride)) ||
(!SizeUtils.IsEmptySize(_sizes.ResizeSize));
return madeToResize;
}
/// <summary>
/// Returns true if the image is animated.
///
/// We're assuming here that only .gif images can be animated.
/// </summary>
/// <returns></returns>
public bool IsAnimated()
{
bool? isBitmapAnimated = CacheUtils.FileBasedEntry<bool?>(CacheUtils.CacheEntryId.IsAnimated, OriginalImageFilePath);
if (isBitmapAnimated == null)
{
// If the image is not a .gif, assume it isn't animated.
ImageType imageType = UrlUtils.ImageTypeUrl(OriginalImageFilePath);
if (imageType != ImageType.Gif)
{
isBitmapAnimated = false;
}
else
{
// Check the bitmap itself.
isBitmapAnimated = ImageUtils.IsAnimated(ImageBitmap);
}
CacheUtils.InsertFileBasedEntry<bool?>(CacheUtils.CacheEntryId.IsAnimated, OriginalImageFilePath, isBitmapAnimated);
}
return (bool)isBitmapAnimated;
}
/// <summary>
/// Merges another image info into this image info.
///
/// IMPORTANT:
/// Only use this if you know that the other image info has the same reportedSize as this one
/// (for example, if both this image info and the other one are part of a group with ResizeWidth and/or ResizeHeight set).
///
/// Also, this doesn't take Processed into account.
/// </summary>
/// <param name="otherImageInfo"></param>
public void Merge(ImageInfo otherImageInfo)
{
// In the end, an ImageInfo is defined by its image references. So add those of the other image info.
foreach (IImageReference imageReference in otherImageInfo.ImageReferences)
{
AddImageReference(imageReference);
}
}
/// <summary>
/// Returns a string that uniquely identifies this ImageInfo
/// </summary>
/// <returns></returns>
public string UniqueId()
{
Size size = GetReportedSize();
//TODO: ImageFilePath contains the root dir of the web site itself.
// You could remove that and save some space in the string.
// When UniqueId is used to create a cache key based on lots of ImageInfos,
// this redundancy makes for a very long cache key.
string result = ImageFilePath + "|" + size.Width.ToString() + "|" + size.Height.ToString();
return result;
}
/// <summary>
/// Implementation of CompareTo, to implement IComparable interface
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public int CompareTo(ImageInfo other)
{
// If other is not a valid object reference, this instance is greater.
if (other == null) return 1;
return string.Compare(UniqueId(), other.UniqueId());
}
/// <summary>
/// Override the ToString method, to get a better readout when looking at lists of ImageInfos.
/// Note that calling this is likely to load the bitmap.
/// </summary>
/// <returns></returns>
public override string ToString()
{
string result = string.Format(
"ImageInfo: {0} | {1} x {2}{3}{4}{5}{6}",
ImageFilePath,
Width, Height,
(IsCombinable ? "" : " | Not Combinable"),
(HasPageBasedReferences ? "" : " | No Page Based References"),
((CombineRestriction == CombineRestriction.HorizontalOnly) ||
(CombineRestriction == CombineRestriction.VerticalOnly) ? " | " + CombineRestriction.ToString() : ""),
(Processed ? " | Processed" : ""));
return result;
}
/// <summary>
/// Retrieves the reported size of the image from _reportedSize.
///
/// Recalculates the reported size of the image if the reported size is not available.
/// This is based on _resizeSize and _resizeSizeOverride.
///
/// This may involve loading the bitmap if not enough width and height info is available
/// through the _storedBitmap object.
/// </summary>
/// <returns>
/// The reported size.
/// </returns>
private Size GetReportedSize()
{
// TODO: This method is called often, eg. to find the unique id of this ImageInfo, which in turn is used to sort the images
// in GroupInfo.GenerateSprite to find the cache key of the sprite (every time a sprite is generated).
// Make this a bit faster by storing the reported size in ImageInfo, wiping the stored info if any of the input sizes are set.
// (use set accessor).
if (_sizes.ReportedSizeSet()) { return _sizes.ReportedSize; }
Size size = _sizes.ResizeSize;
if (_sizes.ResizeSizeOverrideSet()) { size = _sizes.ResizeSizeOverride; }
_sizes.ReportedSize = CompletedSize(size);
return _sizes.ReportedSize;
}
/// <summary>
/// Takes a size, and makes it complete (so both width and height are set).
/// Returns the completed size.
///
/// If both width and height are set, the size is returned as is.
/// If neither are set, the original dimensions of the image in this ImageInfo are used.
/// If only one is set, the other is set based on the aspect ratio of the original dimensions.
/// </summary>
/// <param name="size"></param>
private Size CompletedSize(Size size)
{
Size newSize;
if (SizeUtils.IsCompleteSize(size))
{
// We have both the width and height. No need to do anything.
newSize = size;
}
else if (SizeUtils.IsEmptySize(size))
{
// If both width and height are empty, use the physical dimensions of the image.
newSize = _storedImage.OriginalImageSize();
}
else
{
newSize = SizeUtils.CompletedSize(size, _storedImage.OriginalAspectRatio());
}
return newSize;
}
/// <summary>
/// Replaces all referred images (in the ImageReferences collection)
/// with the given sprite.
/// </summary>
/// <param name="spriteUrl">
/// Url of the sprite. The sprite is an image in which one or more images have
/// been combined, including the original image.
/// </param>
/// <param name="xOffset">
/// X offset within the sprite where the original image starts.
/// </param>
/// <param name="yOffset">
/// Y offset within the sprite where the original image starts.
/// </param>
/// <param name="additionalCss">
/// If replacing the original image means that additional CSS needs to be sent to the browser
/// (after the original CSS has been sent), add the additional CSS to this Stylesheet.
///
/// The caller of this method
/// is responsible for putting the CSS in a stylesheet and having it linked to from the page.
/// </param>
/// <param name="nbrImagesInSprite">
/// Number of images in the sprite identified by spriteUrl.
/// </param>
public void ReplaceAllReferredImagesWithSprite(
string spriteUrl, int xOffset, int yOffset,
Stylesheet additionalCss, int nbrImagesInSprite)
{
// No point in replacing anything if the image file path is broken.
// In that case, you want to keep the original img tag, to make it easier for the user
// to find the broken src.
if (FilePathBroken) { return; }
foreach (IImageReference imageReference in ImageReferences)
{
imageReference.ReplaceWithSprite(
spriteUrl, xOffset, yOffset,
Width, Height,
MadeToResize(),
additionalCss, nbrImagesInSprite);
}
}
/// <summary>
/// Returns true if the given imageRefence can be added to this ImageInfo.
///
/// This checks both the image path and the dimension that need to be used when showing the image on the
/// page. For example
/// img src="abc.png" width="100" height="100
/// and
/// img src="abc.png" width="200" height="200
/// are not compatible.
///
/// Dimensions are an issue, because when images are combined into a sprite, they will be shown as
/// background images, and background images are not resizable in CSS. So when width/height are different
/// from the physical size and the image is to go into a sprite, the image must be auto resized!
/// </summary>
/// <param name="imageReference"></param>
/// <returns></returns>
public bool MatchesImageReference(IImageReference imageReference)
{
if (OriginalImageFilePath != imageReference.OriginalImageFilePath) { return false; }
// If the file path of this image info is broken, than that of the image reference is broken as well.
// To prevent any further processing, return true.
// This broken image info (along with its image references) won't be processed further into a sprite, etc.
if (FilePathBroken) { return true; }
// If ResizeSizeOverride has been set, than this ImageInfo is evidently part of a group
// that sets ResizeSize to give all images in its group a uniform size.
// In that case, it is ok to add any image reference with the same file path.
if (_sizes.ResizeSizeOverrideSet()) { return true; }
// At this point, ResizeSizeOverride has not been set, so the size of the image will be
// determined as far as we know now by any width and height properties on the original img
// (causing auto resize) and the physical image size. This is all captured in
// _sizes.ReportedSize.
// If ResizeSize and SizeFromProperties are the same, than return true.
// This is a common situation before any attempt has been made to match the ImageInfo with a
// group (so GetReportedSize has never been called), and the user has either given
// all img tags a width and height property, or has given none of them a width and height
// property.
//
// However, if you read image from disk folder, than those image references will have
// no SizeFromProperties, so than if img tags have width and height, you need to read the image
// from disk to see if those width and height are the same as the physical width and height.
if (_sizes.ResizeSize == imageReference.SizeFromProperties) { return true; }
// ResizeSize and SizeFromProperties may have been different because
// ResizeSize has been set to physical size of the image, while SizeFromProperties
// is only partially or not filled in - in which case they are really the same.
Size completedSizeFromProperties = CompletedSize(imageReference.SizeFromProperties);
Size reportedSize = GetReportedSize();
return (completedSizeFromProperties == reportedSize);
}
/// <summary>
/// Adds an image reference.
///
/// Do not use ImageReferences.Add to do this.
/// </summary>
/// <param name="imageReference"></param>
public void AddImageReference(IImageReference imageReference)
{
if (imageReference.OriginalImageFilePath != _storedImage.OriginalImageFilePath)
{
throw new Exception(
string.Format(
"Attempting to add an image reference with url={0} to an ImageInfo with url={1}",
imageReference.OriginalImageFilePath, _storedImage.OriginalImageFilePath));
}
// If you've already found that this ImageInfo is not combinable, no need to check further.
if (_isCombinable && (imageReference is IImageReferenceWithCombineTypeRestriction))
{
IImageReferenceWithCombineTypeRestriction imageReferenceWithCombineTypeRestriction =
imageReference as IImageReferenceWithCombineTypeRestriction;
// Check if this image reference is not compatible with the other image references. If not, than this ImageInfo cannot be
// combined with others into a sprite.
if (!CombineRestrictionUtils.Combinable(_combineRestriction, imageReferenceWithCombineTypeRestriction.CombineRestriction))
{
_isCombinable = false;
}
else
{
// Update the overall combine restriction.
// _combineRestriction contains the strictest of all image references.
_combineRestriction = CombineRestrictionUtils.MostRestrictive(
_combineRestriction, imageReferenceWithCombineTypeRestriction.CombineRestriction);
}
}
if ((imageReference is PageBase_ImageReference) || (imageReference is Css_ImageReference))
{
_hasPageBasedReferences = true;
}
_imageReferences.Add(imageReference);
}
/// <summary>
/// Returns true if this ImageInfo matches the given group.
/// </summary>
/// <param name="group"></param>
/// <returns></returns>
public bool MatchesGroup(Group group)
{
// Put the most expensive property accesses last.
// Accessing _storedImage.OriginalImageFilePath is very cheap.
// ImageSize involves reading file size from disk (and can throw an exception if file not found).
// Width and Height involve reading the bitmap from disk.
//
// It is at this point that animated images get excluded from all groups
if (FilePathBroken) { return false; }
bool result = false;
bool pathMatch = (group.FilePathMatchRegex == null) || group.FilePathMatchRegex.IsMatch(OriginalImageFilePath);
if (pathMatch)
{
// Make sure that this group can be used on this page
// Do that here instead of in ConfigSection.Groups(), because Groups() doesn't get called for every page
// (ASP.NET seems to do some caching there).
//
// Check ImageFileSize against MaxSpriteSize (below). It is very easy for users to set a MaxSpriteSize
// but than forget to restrict the group to images that are actually smaller than this.
string currentUrl = HttpContext.Current.Request.Url.ToString();
bool pageUrlMatches = (group.PageUrlMatchRegex == null) || group.PageUrlMatchRegex.IsMatch(currentUrl);
result =
(pageUrlMatches &&
((group.MaxSize == Int32.MaxValue) || (ImageFileSize <= group.MaxSize)) &&
((group.MaxSpriteSize == Int32.MaxValue) || (ImageFileSize <= group.MaxSpriteSize)) &&
((group.MaxWidth == Int32.MaxValue) || (Width <= group.MaxWidth)) &&
((group.MaxHeight == Int32.MaxValue) || (Height <= group.MaxHeight)) &&
((group.MaxPixelFormat == PixelFormat.Format64bppPArgb) ||
(!ImageBitmap.PixelFormat.IsHigherThan(group.MaxPixelFormat))) &&
(!IsAnimated()));
}
// We need to make sure that at some stage we can get the UniqueId of the ImageInfo, because this is used in
// GroupInfo.GenerateSprite to sort the ImageInfos to create the cache key for the sprite.
// If the ImageInfo has a broken image and getting the UniqueId needs to load the image to get the dimensions of the
// image (part of the unique id), then that would lead to an exception. This point here is by far the best place to
// deal with that exception.
//
// Essentially we're not allowing ImageInfos with broken image links to join a group,
// rather than letting it join and creating problems later.
// Call UniqueId but throw away its return value
if (result) { UniqueId(); }
return result;
}
/// <summary>
/// Returns true if this ImageInfo matches the given GroupInfo.
/// </summary>
/// <param name="groupInfo">
/// GroupInfo to check for match.
/// </param>
/// <param name="group">
/// Group that this ImageInfo belongs to
/// </param>
/// <returns>
/// true: The GroupInfo is a match.
/// false: it isn't.
/// </returns>
public bool MatchesGroupInfo(GroupInfo groupInfo, Group group)
{
if (!IsCombinable) { return false; }
// If the image is part of a group that has GiveOwnSprite set to true, than simply return false here.
// That will cause a new GroupInfo to be created just for this image.
// No other image will be added to this new and exclusive GroupInfo, because all images that could be added
// here are part of the same "don't combine" group.
if (group.GiveOwnSprite) { return false; }
if (group != groupInfo.Group) { return false; }
if (!CombineRestrictionUtils.Combinable(groupInfo.CombineRestriction, CombineRestriction)) { return false; }
if (CombineRestrictionUtils.Compare(groupInfo.CombineRestriction, CombineRestriction) < 0)
{
throw new Exception(
string.Format(
"Matching an ImageInfo against a GroupInfo, where the GroupInfo has less restrictive combine restrictions" +
" than the ImageInfo." +
" ImageInfo.CombineRestriction={0}, ImageInfo.OriginalImageFilePath={1}, GroupInfo.CombineRestriction={2}, GroupInfo.Group.GroupName={3}",
CombineRestriction, OriginalImageFilePath, groupInfo.CombineRestriction, groupInfo.Group.GroupName));
}
// At this point, we know that the GroupInfo has combine restrictions that are at least as restrictive as those of the ImageInfo.
// And that they are combinable.
// So you only need to look at the GroupInfo's combine restrictions when checking dimensions.
// Note that we're not looking at MaxSpriteSize here, because at this stage we don't know which images will be
// combined together into sprites by the mapper algo. It is that algo (in MapperUtils.Mapping) that takes MaxSpriteSize into account.
if (groupInfo.CombineRestriction == CombineRestriction.HorizontalOnly)
{
if (Height != groupInfo.InitialImageHeight) { return false; }
}
else if (groupInfo.CombineRestriction == CombineRestriction.VerticalOnly)
{
if (Width != groupInfo.InitialImageWidth) { return false; }
}
// -----------
if (group.SameImageType)
{
if (ImageType != groupInfo.InitialImageType) { return false; }
}
// If SamePixelFormat is true, the image must have the same pixel format as the images in the group,
// and if it is a jpeg the rest of the group has to be jpeg as well and vice versa.
if (group.SamePixelFormat)
{
if ((ImageBitmap.PixelFormat != groupInfo.InitialPixelFormat) ||
((ImageType == ImageType.Jpg) != (groupInfo.InitialImageType == ImageType.Jpg)) ) { return false; }
}
return true;
}
}
}