Skip to content

Instantly share code, notes, and snippets.

@apsun
Created October 19, 2015 22:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save apsun/88880daac4348ea18fd8 to your computer and use it in GitHub Desktop.
Save apsun/88880daac4348ea18fd8 to your computer and use it in GitHub Desktop.
// Requires Magick.NET (https://magick.codeplex.com/)
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using ImageMagick;
namespace GifShrinker
{
public class GifShrinkerMain
{
private static string GetRelativePath(string dirPath, string filePath)
{
string absFromPath = Path.GetFullPath(dirPath + Path.DirectorySeparatorChar);
string absToPath = Path.GetFullPath(filePath);
Uri fromUri = new Uri(absFromPath, UriKind.Absolute);
Uri toUri = new Uri(absToPath, UriKind.Absolute);
Debug.Assert(fromUri.Scheme == toUri.Scheme);
Uri relativeUri = fromUri.MakeRelativeUri(toUri);
string relativePath = Uri.UnescapeDataString(relativeUri.ToString());
relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
return relativePath;
}
private static Dictionary<string, object> ParseArgs(string[] args)
{
var argDict = new Dictionary<string, object>();
argDict["SourceDirectory"] = "./gif_src";
argDict["DestinationDirectory"] = "./gif_dest";
argDict["MaximumSize"] = 512L * 1024;
argDict["CopyUnoptimized"] = true;
argDict["ScaleTolerance"] = 10.0;
argDict["RecursiveSearch"] = true;
argDict["OverwriteExisting"] = true;
return argDict;
}
private static string FormatFileSize(long byteSize)
{
string[] sizeUnits = { "B", "KB", "MB", "GB" };
for (int i = 0; i < sizeUnits.Length; ++i, byteSize /= 1024)
if (byteSize < 1024)
return byteSize + sizeUnits[i];
return byteSize + "TB";
}
private static MagickImageCollection ResizeImageCopy(MagickImageCollection gifImage, double scale)
{
MagickImage[] frames = new MagickImage[gifImage.Count];
gifImage.CopyTo(frames, 0);
foreach (MagickImage frame in frames)
frame.Sample(scale);
return new MagickImageCollection(frames);
}
private static MemoryStream GetOptimalResizedImage(MagickImageCollection gifImage, long optimalSize,
double tolerance = 10, double min = 0, double max = 100)
{
MemoryStream stream = new MemoryStream();
while (true)
{
double scaleFactor = (min + max) / 2;
Console.WriteLine("> Testing scale factor: {0}%", scaleFactor);
MagickImageCollection tmpImage = ResizeImageCopy(gifImage, scaleFactor);
tmpImage.Write(stream);
long byteSize = stream.Position;
int width = tmpImage[0].Width;
int height = tmpImage[0].Height;
Console.WriteLine(" >> Image dimensions: {0}x{1}", width, height);
if (byteSize > optimalSize)
{
Console.WriteLine(" >> Too large: {0}", FormatFileSize(byteSize));
max = scaleFactor;
}
else
{
if (optimalSize / (double)byteSize - 1 < tolerance / 100)
{
Console.WriteLine(" >> Found optimal size: {0}", FormatFileSize(byteSize));
Console.WriteLine(" >> Deviation from optimal: {0}", FormatFileSize(optimalSize - byteSize));
return stream;
}
Console.WriteLine(" >> Too small: {0}", FormatFileSize(byteSize));
min = scaleFactor;
}
stream.Seek(0, SeekOrigin.Begin);
stream.SetLength(0);
}
}
private static void ProcessImage(FileInfo file, Dictionary<string, object> args)
{
long maxSize = Convert.ToInt64(args["MaximumSize"]);
double tolerance = Convert.ToDouble(args["ScaleTolerance"]);
string srcDir = (string)args["SourceDirectory"];
string destDir = (string)args["DestinationDirectory"];
bool copyAll = (bool)args["CopyUnoptimized"];
bool overwriteExisting = (bool)args["OverwriteExisting"];
string fileName = file.FullName;
string relativePath = GetRelativePath(srcDir, fileName);
string destPath = Path.Combine(destDir, relativePath);
string destPathParent = Path.GetDirectoryName(destPath);
Console.WriteLine("Resizing <{0}>", relativePath);
if (File.Exists(destPath))
{
Console.WriteLine("Destination file already exists.");
if (!overwriteExisting)
return;
}
Debug.Assert(destPathParent != null);
if (!Directory.Exists(destPathParent))
Directory.CreateDirectory(destPathParent);
if (file.Length <= maxSize)
{
Console.WriteLine("File is already smaller than the limit ({0}).", FormatFileSize(file.Length));
if (copyAll)
file.CopyTo(destPath, true);
return;
}
MagickImageCollection gif = new MagickImageCollection(fileName);
MemoryStream optimalStream = GetOptimalResizedImage(gif, maxSize, tolerance);
using (FileStream fs = new FileStream(destPath, FileMode.Create))
optimalStream.WriteTo(fs);
}
public static void Main(string[] args)
{
var argDict = ParseArgs(args);
string srcDir = (string)argDict["SourceDirectory"];
bool recursiveSearch = (bool)argDict["RecursiveSearch"];
SearchOption searchOpts = SearchOption.AllDirectories;
if (recursiveSearch) searchOpts = SearchOption.TopDirectoryOnly;
List<FileInfo> failed = new List<FileInfo>();
DirectoryInfo srcDirInfo = new DirectoryInfo(srcDir);
foreach (FileInfo file in srcDirInfo.GetFiles("*", searchOpts))
{
try
{
ProcessImage(file, argDict);
}
catch (Exception ex)
{
failed.Add(file);
Console.WriteLine("Error: {0}", ex.Message);
}
Console.WriteLine(new string('-', 30));
}
if (failed.Count > 0)
{
Console.WriteLine("Done! {0} file(s) failed to be resized:", failed.Count);
foreach (FileInfo file in failed)
Console.WriteLine("> {0}", file.Name);
}
else
{
Console.WriteLine("Done! All files were resized successfully!");
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment