How to resize an image an image in C# to a certain hard-disk size, like 2MiB? Is there a better way than trial and error (even if it's approximate, of course).
Any particular keywords to search for when trying to find the solution on the web?
How to resize an image an image in C# to a certain hard-disk size, like 2MiB? Is there a better way than trial and error (even if it's approximate, of course).
Any particular keywords to search for when trying to find the solution on the web?
You can calculate an approximate information level for the image by taking the original image size divided by the number of pixels:
info = fileSize / (width * height);
I have an image that is 369636 bytes and 1200x800 pixels, so it uses ~0.385 bytes per pixel.
I have a smaller version that is 101111 bytes and 600x400 pixels, so it uses ~0.4213 bytes per pixel.
When you shrink an image you will see that it generally will contain slightly more information per pixel, in this case about 9% more. Depending on your type of images and how much you shrink them, you should be able to calculate an average for how much the information/pixel ration increases (c), so that you can calculate an approximate file size:
newFileSize = (fileSize / (width * height)) * (newWidth * newHeight) * c
From this you can extract a formula for how large you have to make an image to reach a specific file size:
newWidth * newHeight = (newFileSize / fileSize) * (width * height) / c
This will get you pretty close to the desired file size. If you want to get closer you can resize the image to the calculated size, compress it and calculate a new bytes per pixel value from the file size that you got.
I achieved this by reducing the quality until I reached my desired size.
NB: Requires you to add the System.Drawing reference.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
namespace PhotoShrinker
{
class Program
{
/// <summary>
/// Max photo size in bytes
/// </summary>
const long MAX_PHOTO_SIZE = 409600;
static void Main(string[] args)
{
var photos = Directory.EnumerateFiles(Directory.GetCurrentDirectory(), "*.jpg");
foreach (var photo in photos)
{
var photoName = Path.GetFileNameWithoutExtension(photo);
var fi = new FileInfo(photo);
Console.WriteLine("Photo: " + photo);
Console.WriteLine(fi.Length);
if (fi.Length > MAX_PHOTO_SIZE)
{
using (var image = Image.FromFile(photo))
{
using (var stream = DownscaleImage(image))
{
using (var file = File.Create(photoName + "-smaller.jpg"))
{
stream.CopyTo(file);
}
}
}
Console.WriteLine("File resized.");
}
Console.WriteLine("Done.")
Console.ReadLine();
}
}
private static MemoryStream DownscaleImage(Image photo)
{
MemoryStream resizedPhotoStream = new MemoryStream();
long resizedSize = 0;
var quality = 93;
//long lastSizeDifference = 0;
do
{
resizedPhotoStream.SetLength(0);
EncoderParameters eps = new EncoderParameters(1);
eps.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, (long)quality);
ImageCodecInfo ici = GetEncoderInfo("image/jpeg");
photo.Save(resizedPhotoStream, ici, eps);
resizedSize = resizedPhotoStream.Length;
//long sizeDifference = resizedSize - MAX_PHOTO_SIZE;
//Console.WriteLine(resizedSize + "(" + sizeDifference + " " + (lastSizeDifference - sizeDifference) + ")");
//lastSizeDifference = sizeDifference;
quality--;
} while (resizedSize > MAX_PHOTO_SIZE);
resizedPhotoStream.Seek(0, SeekOrigin.Begin);
return resizedPhotoStream;
}
private static ImageCodecInfo GetEncoderInfo(String mimeType)
{
int j;
ImageCodecInfo[] encoders;
encoders = ImageCodecInfo.GetImageEncoders();
for (j = 0; j < encoders.Length; ++j)
{
if (encoders[j].MimeType == mimeType)
return encoders[j];
}
return null;
}
}
}
If it's a 24bit BMP i think you would need to do something like this:
//initial size = WxH
long bitsperpixel = 24; //for 24 bit BMP
double ratio;
long size = 2 * 1 << 20;//2MB = 2 * 2^20
size -= 0x35;//subtract the BMP header size from it
long newH, newW, left, right, middle,BMProwsize;
left = 1;
right = size;//binary search for new width and height
while (left < right)
{
middle = (left + right + 1) / 2;
newW = middle;
ratio = Convert.ToDouble(newW) / Convert.ToDouble(W);
newH = Convert.ToInt64(ratio * Convert.ToDouble(H));
BMProwsize = 4 * ((newW * bitsperpixel + 31) / 32);
//row size must be multiple of 4
if (BMProwsize * newH <= size)
left = middle;
else
right = middle-1;
}
newW = left;
ratio = Convert.ToDouble(newW) / Convert.ToDouble(W);
newH = Convert.ToInt64(ratio * Convert.ToDouble(H));
//resize image to newW x newH and it should fit in <= 2 MB
If it is a different BMP type like 8 bit BMP also in the header section there will be more data specifying the actual color of each value from 0 to 255 so you will need to subtract more from the total file size before the binary search.
Convert, Reduce (Iterative, In Memory) & Download (MVC)
public ActionResult ReduceFileSize(string ImageURL, long MAX_PHOTO_SIZE) //KB
{
var photo = Server.MapPath("~/" + ImageURL); //Files/somefiles/2018/DOC_82401583cb534b95a10252d29a1eb4ee_1.jpg
var photoName = Path.GetFileNameWithoutExtension(photo);
var fi = new FileInfo(photo);
//const long MAX_PHOTO_SIZE = 100; //KB //109600;
var MAX_PHOTO_SIZE_BYTES = (MAX_PHOTO_SIZE * 1000);
if (fi.Length > MAX_PHOTO_SIZE_BYTES)
{
using (var image = Image.FromFile(photo))
{
using (var mstream = DownscaleImage(image, MAX_PHOTO_SIZE_BYTES))
{
//Convert the memorystream to an array of bytes.
byte[] byteArray = mstream.ToArray();
//Clean up the memory stream
mstream.Flush();
mstream.Close();
// Clear all content output from the buffer stream
Response.Clear();
// Add a HTTP header to the output stream that specifies the default filename
// for the browser's download dialog
Response.AddHeader("Content-Disposition", "attachment; filename=" + fi.Name);
// Add a HTTP header to the output stream that contains the
// content length(File Size). This lets the browser know how much data is being transfered
Response.AddHeader("Content-Length", byteArray.Length.ToString());
// Set the HTTP MIME type of the output stream
Response.ContentType = "application/octet-stream";
// Write the data out to the client.
Response.BinaryWrite(byteArray);
}
}
}
else
{
return null;
}
return null;
}
private static MemoryStream DownscaleImage(Image photo, long MAX_PHOTO_SIZE_BYTES)
{
MemoryStream resizedPhotoStream = new MemoryStream();
long resizedSize = 0;
var quality = 93;
//long lastSizeDifference = 0;
do
{
resizedPhotoStream.SetLength(0);
EncoderParameters eps = new EncoderParameters(1);
eps.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, (long)quality);
ImageCodecInfo ici = GetEncoderInfo("image/jpeg");
photo.Save(resizedPhotoStream, ici, eps);
resizedSize = resizedPhotoStream.Length;
//long sizeDifference = resizedSize - MAX_PHOTO_SIZE;
//Console.WriteLine(resizedSize + "(" + sizeDifference + " " + (lastSizeDifference - sizeDifference) + ")");
//lastSizeDifference = sizeDifference;
quality--;
} while (resizedSize > MAX_PHOTO_SIZE_BYTES);
resizedPhotoStream.Seek(0, SeekOrigin.Begin);
return resizedPhotoStream;
}
private static ImageCodecInfo GetEncoderInfo(String mimeType)
{
int j;
ImageCodecInfo[] encoders;
encoders = ImageCodecInfo.GetImageEncoders();
for (j = 0; j < encoders.Length; ++j)
{
if (encoders[j].MimeType == mimeType)
return encoders[j];
}
return null;
}
It depends on what you are willing to change
It's hard to guess what the final disk size will be, but if you know a starting point you can get a pretty good estimate. Reducing the size will probably be proportional, reducing the bits per pixel will also likely be proportional.
If you change the format, compression or quality, it's really just a guess -- depends highly on the image content. You could probably get a good range by trying it on a corpus of images that matches what you think you'll be seeing.