Skip to content
This repository has been archived by the owner on Jan 8, 2019. It is now read-only.

Commit

Permalink
Add lower bound on image dimensions, code cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
apsun committed May 2, 2017
1 parent 6864181 commit 059429b
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 64 deletions.
4 changes: 2 additions & 2 deletions GoogleImageShell.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25420.1
# Visual Studio 15
VisualStudioVersion = 15.0.26403.7
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoogleImageShell", "GoogleImageShell\GoogleImageShell.csproj", "{AAF5D166-15DE-4633-8414-2B1A478E1F8E}"
EndProject
Expand Down
2 changes: 1 addition & 1 deletion GoogleImageShell/ConfigForm.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion GoogleImageShell/GoogleImageShell.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<IsWebBootstrapper>false</IsWebBootstrapper>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
Expand All @@ -24,7 +25,6 @@
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<IsWebBootstrapper>false</IsWebBootstrapper>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
</PropertyGroup>
Expand All @@ -37,6 +37,7 @@
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<LangVersion>default</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
Expand Down
164 changes: 110 additions & 54 deletions GoogleImageShell/GoogleImages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,61 +13,126 @@ namespace GoogleImageShell
{
public static class GoogleImages
{
private const int _maxImageSize = 800;

public static async Task<string> Search(string imagePath, bool includeFileName, bool resizeOnUpload, CancellationToken cancelToken)
private const int MinImageDimension = 200;
private const int MaxImageDimension = 800;

/// <summary>
/// Determines whether the input image should be resized,
/// and if so, the optimal dimensions after resizing.
/// </summary>
/// <param name="originalSize">Original size of the image</param>
/// <param name="newSize">Dimensions after resizing</param>
/// <returns>true if the image should be resized; false otherwise</returns>
private static bool ShouldResize(Size originalSize, out Size newSize)
{
var handler = new HttpClientHandler();
handler.AllowAutoRedirect = false;

StringContent content = null;
// Compute resize ratio (at LEAST ratioMin, at MOST ratioMax).
// ratioMin is used to prevent the image from getting too small.
// Note that ratioMax is calculated on the LARGER image dimension,
// whereas ratioMin is calculated on the SMALLER image dimension.
int origW = originalSize.Width;
int origH = originalSize.Height;
double ratioMax = Math.Min(MaxImageDimension / (double)origW, MaxImageDimension / (double)origH);
double ratioMin = Math.Max(MinImageDimension / (double)origW, MinImageDimension / (double)origH);
double ratio = Math.Max(ratioMax, ratioMin);

// If resizing it would make it bigger, then don't bother
if (ratio >= 1)
{
newSize = originalSize;
return false;
}

int newW = (int)(origW * ratio);
int newH = (int)(origH * ratio);
newSize = new Size(newW, newH);
return true;
}

if (resizeOnUpload)
/// <summary>
/// Loads an image from disk into a byte array.
/// </summary>
/// <param name="imagePath">Path to the image file</param>
/// <param name="resize">Whether to allow resizing</param>
/// <returns>The loaded image, represented as a byte array</returns>
private static byte[] LoadImageData(string imagePath, bool resize)
{
// Resize the image if user enabled the option
// and the image is reasonably large
if (resize)
{
try
{

}
catch (Exception)
{

}
using (Bitmap bmp = new Bitmap(imagePath))
{
if (bmp.Width > _maxImageSize || bmp.Height > _maxImageSize)
using (Bitmap bmp = new Bitmap(imagePath))
{
var newSize = ResizeKeepAspect(bmp.Size, _maxImageSize, _maxImageSize);

using (var newBmp = new Bitmap(newSize.Width, newSize.Height))
if (ShouldResize(bmp.Size, out Size newSize))
{
using (Graphics g = Graphics.FromImage(newBmp))
using (var newBmp = new Bitmap(newSize.Width, newSize.Height))
{
g.DrawImage(bmp, new Rectangle(0, 0, newSize.Width, newSize.Height));
}

using (var ms = new MemoryStream())
{
newBmp.Save(ms, ImageFormat.Jpeg);

content = new StringContent(BinaryToBase64Compat(ms.ToArray()));
using (Graphics g = Graphics.FromImage(newBmp))
{
g.DrawImage(bmp, new Rectangle(0, 0, newSize.Width, newSize.Height));
}

// Save as JPEG (format doesn't have to match file extension,
// Google will take care of figuring out the correct format)
using (var ms = new MemoryStream())
{
newBmp.Save(ms, ImageFormat.Jpeg);
return ms.ToArray();
}
}
}
}
}
catch (Exception)
{
// Ignore exceptions (out of memory, invalid format, etc)
// and fall back to just reading the raw file bytes
}
}

// No resizing required or image is too small,
// just load the bytes from disk directly
return File.ReadAllBytes(imagePath);
}

/// <summary>
/// Converts a byte array into base-64 format, using
/// a format compatible with Google Images.
/// </summary>
/// <param name="content">Raw bytes to encode</param>
/// <returns>Base-64 encoded string</returns>
private static string BinaryToBase64Compat(byte[] content)
{
// Uploaded image needs to be encoded in base-64,
// with `+` replaced by `-` and `/` replaced by `_`
string base64 = Convert.ToBase64String(content).Replace('+', '-').Replace('/', '_');
return base64;
}

if (content == null)
{
content = new StringContent(FileToBase64Compat(imagePath));
}
/// <summary>
/// Asynchronously uploads the specified image to Google Images,
/// and returns the URL of the results page.
/// </summary>
/// <param name="imagePath">Path to the image file</param>
/// <param name="includeFileName">Whether to send the image file name to Google</param>
/// <param name="resizeOnUpload">Whether to resize large images</param>
/// <param name="cancelToken">Allows for cancellation of the upload</param>
/// <returns>String containing the URL of the results page</returns>
public static async Task<string> Search(string imagePath, bool includeFileName, bool resizeOnUpload, CancellationToken cancelToken)
{
// Load the image, resizing it if necessary
byte[] data = LoadImageData(imagePath, resizeOnUpload);

// Prevent auto redirect (we want to open the
// redirect destination directly in the browser)
var handler = new HttpClientHandler();
handler.AllowAutoRedirect = false;

using (var client = new HttpClient(handler))
{
var form = new MultipartFormDataContentCompat();
form.Add(content, "image_content");
form.Add(new StringContent(BinaryToBase64Compat(data)), "image_content");
if (includeFileName)
{
form.Add(new StringContent(Path.GetFileName(imagePath)), "filename");
Expand All @@ -82,26 +147,17 @@ public static async Task<string> Search(string imagePath, bool includeFileName,
}
}

private static string BinaryToBase64Compat(byte[] content)
{
string base64 = Convert.ToBase64String(content).Replace('+', '-').Replace('/', '_');
return base64;
}


private static string FileToBase64Compat(string imagePath)
{
byte[] content = File.ReadAllBytes(imagePath);
string base64 = BinaryToBase64Compat(content);
return base64;
}

public static Size ResizeKeepAspect(Size src, int maxWidth, int maxHeight)
{
decimal rnd = Math.Min(maxWidth / (decimal)src.Width, maxHeight / (decimal)src.Height);
return new Size((int)Math.Round(src.Width * rnd), (int)Math.Round(src.Height * rnd));
}

/// <summary>
/// Google Images has some oddities in the way it requires
/// forms data to be uploaded. The main three that I could
/// find are:
///
/// 1. Content-Disposition name parameters must be quoted
/// 2. Content-Type boundary parameter must NOT be quoted
/// 3. Image base-64 encoding replaces `+` -> `-`, `/` -> `_`
///
/// This class transparently handles the first two quirks.
/// </summary>
private class MultipartFormDataContentCompat : MultipartContent
{
public MultipartFormDataContentCompat() : base("form-data")
Expand Down
4 changes: 2 additions & 2 deletions GoogleImageShell/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@
// 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.1.0.0")]
[assembly: AssemblyFileVersion("1.1.0.0")]
[assembly: AssemblyVersion("1.2.0.0")]
[assembly: AssemblyFileVersion("1.2.0.0")]
28 changes: 27 additions & 1 deletion GoogleImageShell/ShortcutMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,16 @@ public static class ShortcutMenu
{ImageFileType.BMP, new[] {".bmp"}}
};

/// <summary>
/// Creates a shell command to run this program.
/// </summary>
/// <param name="includeFileName">Whether to include the image file name when uploading</param>
/// <param name="resizeOnUpload">Whether to resize large images when uploading</param>
/// <returns>The shell command string</returns>
private static string CreateProgramCommand(bool includeFileName, bool resizeOnUpload)
{
string exePath = new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath;
string command = $"\"{exePath}\" search \"%1\"";
string command = exePath + " search \"%1\"";
if (includeFileName)
{
command += " -n";
Expand All @@ -33,6 +39,13 @@ private static string CreateProgramCommand(bool includeFileName, bool resizeOnUp
return command;
}

/// <summary>
/// Opens the shell key corresponding to this program,
/// with read/write permissions.
/// </summary>
/// <param name="allUsers">true if installing for all users, false if for current user</param>
/// <param name="fileType">File extension (".jpg", ".png", etc)</param>
/// <returns>Registry key object for the specified user/file type</returns>
private static RegistryKey GetShellKey(bool allUsers, string fileType)
{
RegistryKey hiveKey = allUsers ? Registry.LocalMachine : Registry.CurrentUser;
Expand All @@ -41,6 +54,14 @@ private static RegistryKey GetShellKey(bool allUsers, string fileType)
return shellKey;
}

/// <summary>
/// Adds the program to the Windows Explorer context menu.
/// </summary>
/// <param name="menuText">The text to display on the context menu</param>
/// <param name="includeFileName">Whether to include the image file name when uploading</param>
/// <param name="allUsers">Whether to install for all users</param>
/// <param name="resizeOnUpload">Whether to resize large images when uploading</param>
/// <param name="types">Image file types to install the handler for</param>
public static void InstallHandler(string menuText, bool includeFileName, bool allUsers, bool resizeOnUpload, ImageFileType[] types)
{
string command = CreateProgramCommand(includeFileName, resizeOnUpload);
Expand All @@ -59,6 +80,11 @@ public static void InstallHandler(string menuText, bool includeFileName, bool al
}
}

/// <summary>
/// Removes the program from the Windows Explorer context menu.
/// </summary>
/// <param name="allUsers">Whether to uninstall for all users</param>
/// <param name="types">Image file types to uninstall the handler for</param>
public static void UninstallHandler(bool allUsers, ImageFileType[] types)
{
foreach (ImageFileType fileType in types)
Expand Down
15 changes: 12 additions & 3 deletions GoogleImageShell/UploadForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,18 @@ public UploadForm(string[] args)
for (int i = 1; i < args.Length; ++i)
{
string arg = args[i];
if (arg == "-n") _includeFileName = true;
else if (arg == "-r") _resizeOnUpload = true;
else _imagePath = arg;
switch (arg)
{
case "-n":
_includeFileName = true;
break;
case "-r":
_resizeOnUpload = true;
break;
default:
_imagePath = arg;
break;
}
}
}

Expand Down

0 comments on commit 059429b

Please sign in to comment.