Created
October 19, 2015 22:34
-
-
Save apsun/88880daac4348ea18fd8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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