private ImageSettings GetSettings(string settingFileLocation) { ImageSettings settings = new ImageSettings(); if (settingFileLocation != null) { XmlTextReader settingsData; // Open the settings file. If it fails here, we throw an exception since we expect the file to be there and readable. using (settingsData = new XmlTextReader(settingFileLocation)) { while (settingsData.Read()) { if (settingsData.NodeType == XmlNodeType.Element) { string nodeName = settingsData.Name; if (nodeName.Equals("FileFormat", StringComparison.OrdinalIgnoreCase)) { settings.Format = settingsData.ReadElementContentAsString().Trim('.'); } else if (nodeName.Equals("Quality", StringComparison.OrdinalIgnoreCase)) { settings.Quality = settingsData.ReadElementContentAsInt(); } else if (nodeName.Equals("MaxSize", StringComparison.OrdinalIgnoreCase)) { settings.MaxSize = settingsData.ReadElementContentAsInt(); } else if (nodeName.Equals("BackgroundColor", StringComparison.OrdinalIgnoreCase)) { string output = settingsData.ReadElementContentAsString(); int temp = Int32.Parse(output, System.Globalization.NumberStyles.HexNumber, CultureInfo.InvariantCulture); settings.BackgroundColor = Color.FromArgb(temp); } else if (nodeName.Equals("Base64Encoding", StringComparison.OrdinalIgnoreCase)) { settings.Base64 = settingsData.ReadElementContentAsBoolean(); } else if (nodeName.Equals("TileInYAxis", StringComparison.OrdinalIgnoreCase)) { settings.TileInYAxis = settingsData.ReadElementContentAsBoolean(); } } } } return(settings); } return(settings); }
private void PerformOptimizations(string path, ImageSettings settings, TextWriter cssHighCompatOutput, TextWriter cssLowCompatOutput, List <string> imageLocations, ref DateTime mostRecentCreationTimeUtc, ref DateTime mostRecentLastWriteTimeUtc) { // Create a list containing each image (in Bitmap format), and calculate the total size (in pixels) of final image int x = 0; int y = 0; int imageIndex = 0; long size = 0; int spriteNumber = 0; List <Bitmap> images = new List <Bitmap>(); try { foreach (string imagePath in imageLocations) { var imageFile = new FileInfo(imagePath); // If the image is growing above the specified max file size, make the sprite with the existing images // and add the new image to the next sprite list if ((imageIndex > 0) && IsSpriteOversized(settings.MaxSize, size, imagePath)) { GenerateSprite(path, settings, x, y, spriteNumber, images, cssHighCompatOutput, cssLowCompatOutput); // Clear the existing images foreach (Bitmap image in images) { image.Dispose(); } // Reset variables to initial values, and increment the spriteNumber images.Clear(); x = 0; y = 0; imageIndex = 0; size = 0; spriteNumber++; } // Add the current image to the list of images that are to be processed images.Add(new Bitmap(imagePath)); // Use the image tag to store its name images[imageIndex].Tag = MakeCssSelector(imagePath, SpriteDirectoryRelativePath); // Find the total pixel size of the sprite based on the tiling direction if (settings.TileInYAxis) { y += images[imageIndex].Height; if (x < images[imageIndex].Width) { x = images[imageIndex].Width; } } else { x += images[imageIndex].Width; if (y < images[imageIndex].Height) { y = images[imageIndex].Height; } } // Update the filesize size of the bitmap list size += imageFile.Length; // ensure the create and modify date time stamps are the most recent if (mostRecentCreationTimeUtc < imageFile.CreationTimeUtc) { mostRecentCreationTimeUtc = imageFile.CreationTimeUtc; } if (mostRecentLastWriteTimeUtc < imageFile.LastWriteTimeUtc) { mostRecentLastWriteTimeUtc = imageFile.LastWriteTimeUtc; } imageIndex++; } // Merge the final list of bitmaps into a sprite if (imageIndex != 0) { GenerateSprite(path, settings, x, y, spriteNumber, images, cssHighCompatOutput, cssLowCompatOutput); } } finally { // Close the CSS file and clear the images list foreach (Bitmap image in images) { image.Dispose(); } } }
private void GenerateSprite(string path, ImageSettings settings, int x, int y, int spriteNumber, List <Bitmap> images, TextWriter cssHighCompatOutput, TextWriter cssLowCompatOutput) { // Create a drawing surface and add the images to it // Since we'll be padding each image by 1px later on, we need to increase the sprites's size correspondingly. if (settings.TileInYAxis) { y += images.Count; } else { x += images.Count; } using (Bitmap sprite = new Bitmap(x, y)) { using (Graphics drawingSurface = Graphics.FromImage(sprite)) { // Set the background to the specs from the settings file drawingSurface.Clear(settings.BackgroundColor); // Make the final sprite and save it int xOffset = 0; int yOffset = 0; var cssImages = new List <CssImageInfo>(); foreach (Bitmap image in images) { drawingSurface.DrawImage(image, new Rectangle(xOffset, yOffset, image.Width, image.Height)); cssImages.Add(new CssImageInfo() { XOffset = xOffset, YOffset = yOffset, Image = image, }); if (settings.TileInYAxis) { // pad each image in the sprite with a 1px margin yOffset += image.Height + 1; } else { // pad each image in the sprite with a 1px margin xOffset += image.Width + 1; } } // Set the encoder parameters and make the image string spriteMD5; using (var spriteMemoryStream = new System.IO.MemoryStream()) { string spriteFileExt; try { using (EncoderParameters spriteEncoderParameters = new EncoderParameters(1)) { spriteEncoderParameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, settings.Quality); // Attempt to save the image to disk with the specified encoder spriteFileExt = settings.Format; sprite.Save(spriteMemoryStream, GetEncoderInfo(settings.Format), spriteEncoderParameters); } } catch (Exception) { // If errors occur, get the CLI to auto-choose an encoder. Unfortunately this means that the quality settings will be not used. try { spriteFileExt = settings.Format; sprite.Save(spriteMemoryStream, GetImageFormat(settings.Format)); } catch (Exception) { // If errors occur again, try to save as a png spriteFileExt = "png"; sprite.Save(spriteMemoryStream, ImageFormat.Png); } } // create MD5 of the memory stream // reset the stream to the beginning spriteMemoryStream.Position = 0; // create MD5 of the memory stream // Use HttpServerUtility.UrlTokenEncode so it's url safe spriteMD5 = HttpServerUtility.UrlTokenEncode(System.Security.Cryptography.MD5.Create().ComputeHash(spriteMemoryStream)); // reset the stream to the beginning spriteMemoryStream.Position = 0; // save to final file var spriteFile = Path.Combine(path, GenerateSpriteFileName(spriteNumber, spriteMD5, settings.Format)); File.WriteAllBytes(spriteFile, spriteMemoryStream.ToArray()); if (IsLoggingEnabled) { LogMessage($"Saved sprite image \"{spriteFile}\"."); } } foreach (var cssImage in cssImages) { // Add the CSS data GenerateCss(cssImage.XOffset, cssImage.YOffset, spriteNumber, spriteMD5, settings.Format, settings.Base64, cssImage.Image, cssHighCompatOutput); GenerateCss(cssImage.XOffset, cssImage.YOffset, spriteNumber, spriteMD5, settings.Format, false, cssImage.Image, cssLowCompatOutput); } } } }
/// <summary> /// Executes the image optimizer on a specific subdirectory of the root image directory (non-recursive) /// </summary> /// <param name="path">The path to the directory to be rebuilt</param> /// <param name="checkIfFilesWereModified">Indicate whether the directory should only be rebuilt if files were modified</param> /// <returns>A list of the files which are dependencies of the cache.</returns> public List <string> ProcessDirectory(string spriteDirectory) { // Check if directory was deleted if (!Directory.Exists(spriteDirectory)) { return(new List <string>(0)); } var dependencies = new List <string>(); // add the current directory as a dependency so if any files are added the directory will get reprocessed dependencies.Add(spriteDirectory); var hashedFiles = new List <string>(); // get the settings file so we can add it to the hashed files string settingsFile = LocateSettingFile(spriteDirectory); if (File.Exists(settingsFile)) { hashedFiles.Add(settingsFile); if (IsLoggingEnabled) { LogMessage($"Loaded sprite settings from \"{settingsFile}\" for directory \"{spriteDirectory}\"."); } } var allFiles = Directory.GetFiles(spriteDirectory); var generatedFiles = new List <string>(); var imageFiles = new List <string>(); foreach (var file in allFiles) { if (IsGeneratedFile(file)) { generatedFiles.Add(file); hashedFiles.Add(file); } else if (IsImageFile(file)) { imageFiles.Add(file); hashedFiles.Add(file); if (IsLoggingEnabled) { LogMessage($"Found image \"{file}\" to sprite."); } } } // Make sure to not include the hash file string md5HashFile = GetMD5HashFile(spriteDirectory); string currentDirectoryMD5 = ComputeHash(hashedFiles); string savedDirectoryMD5 = GetDirectoryMD5(spriteDirectory); if (savedDirectoryMD5 == currentDirectoryMD5) { // add all the hashed files to the dependencies so if any change we can reload dependencies.AddRange(hashedFiles); return(dependencies); } // Import settings from settings file ImageSettings settings = GetSettings(settingsFile); // delete the generated files generatedFiles.ForEach(f => File.Delete(f)); // Create pointer to the CSS output file lock (_lockObj) { string newCssMD5 = ComputeHash(imageFiles); string highCompCssFile = Path.Combine(spriteDirectory, CreateHighCompatibilityCssFileName(newCssMD5)); string lowCompCssFile = Path.Combine(spriteDirectory, CreateLowCompatibilityCssFileName(newCssMD5)); DateTime mostRecentCreationTimeUtc = DateTime.MinValue; DateTime mostRecentLastWriteTimeUtc = DateTime.MinValue; using (TextWriter cssHighCompatOutput = new StreamWriter(highCompCssFile, append: false), cssLowCompatOutput = new StreamWriter(lowCompCssFile, append: false)) { PerformOptimizations(spriteDirectory, settings, cssHighCompatOutput, cssLowCompatOutput, imageFiles, ref mostRecentCreationTimeUtc, ref mostRecentLastWriteTimeUtc); // Merge with a user's existing CSS file(s) MergeExternalCss(spriteDirectory, cssHighCompatOutput, cssLowCompatOutput, ref mostRecentCreationTimeUtc, ref mostRecentLastWriteTimeUtc); } if (IsLoggingEnabled) { LogMessage($"Saved sprite high compatiability CSS to \"{highCompCssFile}\"."); } if (IsLoggingEnabled) { LogMessage($"Saved sprite low compatiability CSS to \"{lowCompCssFile}\"."); } // look at the directory and get all the files we're going to make the new hash from // since the directory now contains all the image files and generated files hashedFiles = Directory.GetFiles(spriteDirectory).Where(f => IsGeneratedFile(f) || IsImageFile(f)).ToList(); // add the settings file to the hashed files if (File.Exists(settingsFile)) { hashedFiles.Add(settingsFile); } // re-compute the hash now that we've generated all the files string newDirectoryMD5 = ComputeHash(hashedFiles); // add the hashedFiles to the dependencies since we've not update it properly dependencies.AddRange(hashedFiles); // write the md5s to the hash file File.WriteAllLines(md5HashFile, new string[] { newCssMD5, newDirectoryMD5 }); if (IsLoggingEnabled) { LogMessage($"Saved CSS MD5 hash \"{newCssMD5}\" and directory MD5 has \"{newDirectoryMD5}\" to \"{md5HashFile}\"."); } // ensure the modify date is after or equal to the create date if (mostRecentCreationTimeUtc > mostRecentLastWriteTimeUtc) { mostRecentLastWriteTimeUtc = mostRecentCreationTimeUtc; } // set the output files to the correct time // this ensures cached items relying on these times // stay in sync and only change which the underlying // is modified File.SetCreationTimeUtc(md5HashFile, mostRecentCreationTimeUtc); File.SetLastWriteTimeUtc(md5HashFile, mostRecentLastWriteTimeUtc); hashedFiles.ForEach(f => { File.SetCreationTimeUtc(f, mostRecentCreationTimeUtc); File.SetLastWriteTimeUtc(f, mostRecentLastWriteTimeUtc); }); } return(dependencies); }