Image uploading / Setting DPI but ensuring file-size <=200k - c#

I have a form (using MVC2) which has an image-upload script, but the rules for the final image stored on the server are pretty strict. I can force the file to the dimensions I want but it always ends up exceeding the file-size required... so I can allow a sub-200k image but once my code has processed it ends up slightly bigger.
These are the rules I have to adhere to:
Photographs should be in colour
The permitted image types for the
photograph are .JPG or .GIF
The maximum size of the image is 200kb
The dimensions of the photograph on the badge will be 274 pixels
(wide) x 354 pixels (high) # 200dpi (depth of pixels per inch)
This is what I have currently:
[HttpPost]
public ActionResult ImageUpload(HttpPostedFileBase fileBase)
{
ImageService imageService = new ImageService();
if (fileBase != null && fileBase.ContentLength > 0 && fileBase.ContentLength < 204800 && fileBase.ContentType.Contains("image/"))
{
string profileUploadPath = "~/Resources/images";
Path.GetExtension(fileBase.ContentType);
var newGuid = Guid.NewGuid();
var extension = Path.GetExtension(fileBase.FileName);
if (extension.ToLower() != ".jpg" && extension.ToLower() != ".gif") // only allow these types
{
return View("WrongFileType", extension);
}
EncoderParameters encodingParameters = new EncoderParameters(1);
encodingParameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 70L); // Set the JPG Quality percentage
ImageCodecInfo jpgEncoder = imageService.GetEncoderInfo("image/jpeg");
var uploadedimage = Image.FromStream(fileBase.InputStream, true, true);
Bitmap originalImage = new Bitmap(uploadedimage);
Bitmap newImage = new Bitmap(originalImage, 274, 354);
Graphics g = Graphics.FromImage(newImage);
g.InterpolationMode = InterpolationMode.HighQualityBilinear;
g.DrawImage(originalImage, 0, 0, newImage.Width, newImage.Height);
var streamLarge = new MemoryStream();
newImage.Save(streamLarge, jpgEncoder, encodingParameters);
var fileExtension = Path.GetExtension(extension);
var ImageName = newGuid + fileExtension;
newImage.Save(Server.MapPath(profileUploadPath) + ImageName);
//newImage.WriteAllBytes(Server.MapPath(profileUploadPath) + ImageName, streamLarge.ToArray());
originalImage.Dispose();
newImage.Dispose();
streamLarge.Dispose();
return View("Success");
}
return View("InvalidImage");
}
Just to add:
The images are going off to print on a card so the DPI is important. But I realise that 200k is not a lot for a printed image.. none of these are my business rules! As it stands with this code an image uploaded that is pretty much 200k, ends up costing 238k(ish)

It's very difficult to calculate the size of a jpeg in advance. Having said that, you don't need to compress it much.
Let's just look at some metrics:
274 * 354 = 96996 pixels. If you have 8 bits per pixel and 3 colour
channels (i.e. 24bit colour) then you have:
274* 354 * 8 * 3 = 2,327,904 bits = 290988 bytes = 284.17 kb.
200 / 284.17 ~ 0.70.
You only need to reduce it to 70% of its original size.
Sadly, it's at this point we get to the limit of my knowledge in this area! But I reckon that by saving as a jpeg it will be in the right size range anyway, even if saving at the highest quality setting.
I would guess at setting the quality to 70 and see what happens.
EDIT: DPI settings
Apparently you only need to change the EXIF data. See this answer: https://stackoverflow.com/a/4427411/234415

You should experiment with the JPEG quality setting. You currently have it set to 90, 80 might be sufficient and will result in a smaller file.

I see some problems with the code:
You are using GetThumbnailImage to create a thumbnail, but that is not intended for such large thumbnails. It works up to about 120x120 pixels. If the image has an embedded thumbnail, that will be used instead of scaling down the full image, so you will be scaling up a smaller image, with obvious quality problems.
You are saving the thumbnail to a memory stream, which you then just throw away.
You are saving the thumbnail to file without specifying the encoder, which means that it will either be saved as a low compressed JPEG image or a PNG image, that's why you get a larger file size.
You never dispose the uploadedImage object.
Note: The resolution (PPI/DPI) has no relevance when you display images on the web.

Related

Way to decode an image into a thumbnail, skipping loading the image completely

I am currently trying to create thumbnails the moment I am decoding them to avoid loading the entire image into memory and then scaling it. Secondly I want to get rid from my other thumbnail code which is using ShellObjects and what the OS file explorer cached as thumbnails. The problem with the later is that its depending on if there is anything cached.
The following code is my attempt to create an image the moment its decoded which fails with a Format Unknown error. I am pretty close to have the solution so I am coming here because I have not found an answer. Every "solution" I found loaded the entire file, creating two images, scaling the original which creates more overhead than what I believe is needed to accomplish this. Pretty resource intensive for an image manager with a thousand image files being loaded asynchronously.)
public static async Task<Texture2D> GetThumbnail(string filePath)
{
// Decode the image directly in the given DecodePixelHeight (or width), maintaining aspect ratio.
var thumbnail = new BitmapImage();
thumbnail.BeginInit();
thumbnail.UriSource = new Uri(filePath, UriKind.Absolute);
thumbnail.DecodePixelHeight = 144; //Fit to this height.
thumbnail.EndInit();
// Here I am trying reset a format. I am aiming to not need this step.
// Format the bitmap image into a known format.
var formatted = new FormatConvertedBitmap();
formatted.BeginInit();
formatted.Source = thumbnail;
formatted.DestinationFormat = System.Windows.Media.PixelFormats.Default;
formatted.EndInit();
using var stream = new MemoryStream();
var bytesPerPixel = (formatted.DestinationFormat.BitsPerPixel + 7) / 8;
var stride = 4 * ((formatted.PixelWidth * bytesPerPixel + 3) / 4);
var buffer = new byte[formatted.PixelHeight * stride];
formatted.CopyPixels(buffer, stride, 0);
await stream.WriteAsync(buffer, 0, buffer.Length);
return Texture2D.FromStream(___.GraphicsDevice, stream);
}

ImageProcessor.ImageFactory compression not reducing image file size

I am attempting to implement an image compression function to be used on images uploaded to my website. I want to take the original image and save 3 different sizes/quality levels. For this I am using ImageProcessor.ImageFactory. The three levels:
ISupportedImageFormat sm_format = new JpegFormat { Quality = 40 };
Size sm_size = new Size(150, 0);
ISupportedImageFormat md_format = new JpegFormat { Quality = 60 };
Size md_size = new Size(280, 0);
ISupportedImageFormat lg_format = new JpegFormat { Quality = 100 };
Size lg_size = new Size(1000, 0);
imageFactory.Load(or_directoryPath + "/" + fileName)
.Resize(sm_size)
.Format(sm_format)
.BackgroundColor(Color.Transparent)
.Save(Path.Combine(sm_directory, fileName));
// same for md and lg images
What's happening is that the medium and small images do not have the expected smaller filesize.
An example:
Original image is a .jpg 3000x3000 that is 3.7MB large.
The large image size is 2.96MB
The medium image size is 2.63MB
The small image size is 2.62MB
I tried the following on the small image to further compress it to 10% quality:
// Encoder parameter for image quality
EncoderParameter qualityParam = new EncoderParameter(Encoder.Quality, 10);
// JPEG image codec
ImageCodecInfo jpegCodec = GetEncoderInfo("image/jpeg");
EncoderParameters encoderParams = new EncoderParameters(1);
encoderParams.Param[0] = qualityParam;
img.Save(path, jpegCodec, encoderParams);
The end result is significantly lower quality, but the image file size is still 2.62MB
Edit: uploaded original images shared on postimg
The medium image:
The small image:
The small image compressed:
The original image:
Upon further inspection of the image, the EXIF data really is the problem with that file. It contains a section with a custom color profile, and has data stored in it with a size of about 2.64 megabytes. This can be checked by uploading the image to http://regex.info/exif.cgi and clicking the "Show ICC profile data".
Stripping that weird profile data gets rid of the extreme overhead and brings down the filesize to 348 KB at 1000x1000 px.
As you already found out yourself, you must set the preserveExifData parameter in the constructor of the ImageFactory object to false to make it strip the data. Or call the default constructor as
ImageFactory imageFactory = new ImageFactory();
When using ImageProcessor it is possible to set an option in the processing.config
<processing preserveExifMetaData="true" fixGamma="false" interceptAllRequests="false" allowCacheBuster="true">
Setting preserveExifMetaData="false" to removes the EXIF data aswell.
You can also set the option in the ImageFactory constructor:
var imageFactory = new ImageFactory(preserveExifData:true);

Losing image quality in c# using Image class (reduces amount of colors)

I have a c# program that opens a .tif image and later offers the option to save it. However, there is always a drop in quality when saving the image.
(EDIT:I passed some parameters while saving the image so that the quality is at 100 % and there is no compression, but the number of actual unique colors go from 254 to 16, even though the image properties show 8bpp)
(EDIT2: The image in question is a grayscale image at 8 bits per pixel - 256 colors/shades of gray - This doesn't happen with a 24 bits per pixel color image that I tested where all the colors are retained. I am starting to think that the image class may only support 16 shades of gray)
How do I avoid this?
Here's the code for opening the image:
public Image imageImport()
{
Stream myStream = null;
OpenFileDialog openTifDialog = new OpenFileDialog();
openTifDialog.Title = "Open Desired Image";
openTifDialog.InitialDirectory = #"c:\";
openTifDialog.Filter = "Tiff only (*.tif)|*.tif";
openTifDialog.FilterIndex = 1;
openTifDialog.RestoreDirectory = true;
if (openTifDialog.ShowDialog() == DialogResult.OK)
{
try
{
if ((myStream = openTifDialog.OpenFile()) != null)
{
using (myStream)
{
String tifFileName= openTifDialog.FileName;
imgLocation = tifFileName;
Bitmap tifFile = new Bitmap(tifFileName);
return tifFile;
}
}
}
catch (Exception ex)
{
MessageBox.Show("Error: Could not read file from disk. Original error: " + ex.Message);
}
}
return null;
}
This is the way I save the image:
private void saveImage(Image img)
{
SaveFileDialog sf = new SaveFileDialog();
sf.Title = "Select File Location";
sf.Filter = " bmp (*.bmp)|*.bmp|jpeg (*.jpg)|*.jpg|tiff (*.tif)|*.tif";
sf.FilterIndex = 4;
sf.RestoreDirectory = true;
sf.ShowDialog();
// If the file name is not an empty string open it for saving.
if (sf.FileName != "")
{
// Saves the Image via a FileStream created by the OpenFile method.
System.IO.FileStream fs =
(System.IO.FileStream)sf.OpenFile();
// Saves the Image in the appropriate ImageFormat based upon the
// File type selected in the dialog box.
// NOTE that the FilterIndex property is one-based.
switch (sf.FilterIndex)
{
case 1:
img.Save(fs,
System.Drawing.Imaging.ImageFormat.Bmp);
break;
case 2:
img.Save(fs,
System.Drawing.Imaging.ImageFormat.Jpeg);
break;
case 3://EDITED -STILL DOES NOT RESOLVE THE ISSUE
ImageCodecInfo codecInfo = ImageClass.GetEncoderInfo(ImageFormat.Tiff);
EncoderParameters parameters = new EncoderParameters(2);
parameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality,100L);
parameters.Param[1] = new EncoderParameter(System.Drawing.Imaging.Encoder.Compression, (long)EncoderValue.CompressionNone);
img.Save(fs,codecInfo, parameters);
break;
}
fs.Close();
}
}
Even if I don't resize or change the image in any ways, I experience a loss in quality. any advice?
System.Drawing has poor support for 8-bit images. When converting from 24 or 32-bit images to 8-bit; it'll always use a fixed default color palette. That default color palette only contains 16 shades of grey, the other entries are various colors.
Do you have the same problem when saving as '.bmp'? If yes, then the conversion to the 8-bit format already happened earlier, you'll have to figure out where your program does that and fix the issue there.
If it's only the tiff encoder that converts to 8-bit, you'll have to do the 8-bit conversion in a separate step first. Create an 8-bit image, fill Image.Palette with a gray-scale palette, and then copy the bitmap data over.
But System.Drawing has poor support for 8-bit images, and several methods (e.g. SetPixel) will just throw InvalidOperationException when dealing with such images. You will probably have to use unsafe code (with LockBits etc.) to copy the bitmap data. If I were you, I'd look if there are alternative graphics libraries you could use.
I had issues with using the .NET libraries to find good balances of image quality and size. I gave up rolling my own and tried out a few imaging libraries. I found http://imageresizing.net/ to produce consistently good results, much better than I was able to do.
Just throwing that out there as a plan B in case the roll your own method doesn't wind up working well on a consistent basis for you.
Image.Save by default uses a quality setting of 75%. You could try using one of the other overloads of the method that allows you to specify quality setting parameters. See this question.
Only one suggestion really....when loading the Image you use new Bitmap(fileName)... Rather than using Bitmap have you considered using
Image tiffImage = Image.FromFile(tiffFileName, true);
The true tells it to use "embedded color management", and using Image instead of Bitmap avoids any image casting that might be occurring behind the scenes.

Change Size of Jpeg File in C#

I need to transfer some images through Network, I saved images with Jpeg and 40% quality as following:
public void SaveJpeg(string path, Image image, int quality) {
if((quality < 0) || (quality > 100)) {
string error = string.Format("Jpeg image quality must be
between 0 and 100, with 100 being the highest quality. A value of {0} was
specified.", quality);
throw new ArgumentOutOfRangeException(error);
}
EncoderParameter qualityParam = new
EncoderParameter(System.Drawing.Imaging.Encoder.Quality, quality);
ImageCodecInfo jpegCodec = GetEncoderInfo("image/jpeg");
EncoderParameters encoderParams = new EncoderParameters(1);
encoderParams.Param[0] = qualityParam;
image.Save(path, jpegCodec, encoderParams);
}
But with this way the size of Jpeg files not enough small, Also I change the quality but that's not good appearance. Is there any way to save pictures with smaller file size and proper appearance? I don't know but is there any way to use System.Drawing.Graphics object, also I don't need to zip files, or change dimension of images, at now just the size of picture file is important.
With image compression, there's a fine line between creating a small file and creating a poor quality image. JPEG is a lossy compression format which means that data is removed when compressed, which is why constantly re-encoding a JPEG file will continually decrease its quality.
On the other hand, PNG files are lossless but may still result in bigger files. You could try encoding the file as a PNG using PngBitmapEncoder. This will ensure the quality remains high, but the size may or may not decrease enough for your program (it depends on the image).
If you're performing this on a local machine and don't need to do it too often (e.g. for many concurrent users), you could invoke an external program to do it for you. PNG Monster is very good at compressing PNG files without decreasing the quality. You could call this from your program and send the resulting PNG file. (You may want to check the licensing terms to ensure that it's compatible with your program).
There aren't many ways where you can maintain a high quality and perform a high compression at the same time, without manipulating the image (e.g. changing dimension).
I have a method for creating and saving the thumbnail of an uploaded picture, I think NewImageSize method might help you.It also handles the quality issue.
public Size NewImageSize(int OriginalHeight, int OriginalWidth, double FormatSize)
{
Size NewSize;
double tempval;
if (OriginalHeight > FormatSize && OriginalWidth > FormatSize)
{
if (OriginalHeight > OriginalWidth)
tempval = FormatSize / Convert.ToDouble(OriginalHeight);
else
tempval = FormatSize / Convert.ToDouble(OriginalWidth);
NewSize = new Size(Convert.ToInt32(tempval * OriginalWidth), Convert.ToInt32(tempval * OriginalHeight));
}
else
NewSize = new Size(OriginalWidth, OriginalHeight); return NewSize;
}
private bool save_image_with_thumb(string image_name, string path)
{
ResimFileUpload1.SaveAs(path + image_name + ".jpg"); //normal resim kaydet
///////Thumbnail yarat ve kaydet//////////////
try
{
Bitmap myBitmap;
myBitmap = new Bitmap(path + image_name + ".jpg");
Size thumbsize = NewImageSize(myBitmap.Height, myBitmap.Width, 100);
System.Drawing.Image.GetThumbnailImageAbort myCallBack = new System.Drawing.Image.GetThumbnailImageAbort(ThumbnailCallback);
// If jpg file is a jpeg, create a thumbnail filename that is unique.
string sThumbFile = path + image_name + "_t.jpg";
// Save thumbnail and output it onto the webpage
System.Drawing.Image myThumbnail = myBitmap.GetThumbnailImage(thumbsize.Width, thumbsize.Height, myCallBack, IntPtr.Zero);
myThumbnail.Save(sThumbFile);
// Destroy objects
myThumbnail.Dispose();
myBitmap.Dispose();
return true;
}
catch //yaratamazsa normal ve thumb iptal
{
return false;
}
///////////////////////////////////
}

How can I get better results when shrinking an image

I'm scaling images down in c#, and I've compared my methods with the best method in Photoshop cs5 and cannot replicate it.
In PS i'm using bicubic sharper, which looks really good. However, when trying to do the same in c# I don't get as high quality results. I've tried bicubic interpolation as well as HQ bicubic, smoothing mode HQ/None/AA. Composition modes, I've tried about 50 different variations and each one comes out pretty close to the image on the right.
You'll notice the pixelation on her back and around the title, as well as the authors name not coming out too well.
(Left is PS, right is c#.)
It seems that c# bicubic does too much smoothing even with smoothing set to none. I've been playing around with many variations of the following code:
g.CompositingQuality = CompositingQuality.HighQuality;
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.PixelOffsetMode = PixelOffsetMode.None;
g.SmoothingMode = SmoothingMode.None;
Edit: As requested here is the starting image (1mb).
Perhaps I am missing something, but I have typically used the following code below to resize/compress JPEG Images. Personally, I think the result turned out pretty well based on your source image. The code doesn't handle a few edge cases concerning input parameters, but overall gets the job done (I have additional extension methods for Cropping, and Combining image transformations if interested).
Image Scaled to 25% original size and using 90% Compression. (~30KB output file)
Image Scaling Extension Methods:
public static Image Resize(this Image image, Single scale)
{
if (image == null)
return null;
scale = Math.Max(0.0F, scale);
Int32 scaledWidth = Convert.ToInt32(image.Width * scale);
Int32 scaledHeight = Convert.ToInt32(image.Height * scale);
return image.Resize(new Size(scaledWidth, scaledHeight));
}
public static Image Resize(this Image image, Size size)
{
if (image == null || size.IsEmpty)
return null;
var resizedImage = new Bitmap(size.Width, size.Height, image.PixelFormat);
resizedImage.SetResolution(image.HorizontalResolution, image.VerticalResolution);
using (var g = Graphics.FromImage(resizedImage))
{
var location = new Point(0, 0);
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.DrawImage(image, new Rectangle(location, size), new Rectangle(location, image.Size), GraphicsUnit.Pixel);
}
return resizedImage;
}
Compression Extension Method:
public static Image Compress(this Image image, Int32 quality)
{
if (image == null)
return null;
quality = Math.Max(0, Math.Min(100, quality));
using (var encoderParameters = new EncoderParameters(1))
{
var imageCodecInfo = ImageCodecInfo.GetImageEncoders().First(encoder => String.Compare(encoder.MimeType, "image/jpeg", StringComparison.OrdinalIgnoreCase) == 0);
var memoryStream = new MemoryStream();
encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, Convert.ToInt64(quality));
image.Save(memoryStream, imageCodecInfo, encoderParameters);
return Image.FromStream(memoryStream);
}
}
Usage:
using(var source = Image.FromFile(#"C:\~\Source.jpg"))
using(var resized = source.Resize(0.25F))
using(var compressed = resized.Compress(90))
compressed.Save(#"C:\~\Output.jpg");
NOTE:
For anyone who may comment, you cannot dispose the MemoryStream created in the Compress method until after the image is disposed. If you reflect in to the implementation of Dispose on MemoryStream, it is actually save to not explicitly call dispose. The only alternative would be to wrap the image/memory stream in a custom implementation of a class that implements Image/IDisposable.
Looking at the amount of JPEG artifacts, especially at the top of the image, I think you set the jpg compression to high. That results in a smaller (filesize) file, but reduces image quality and seems to add more blur.
Can you try saving it in a higher quality? I assume the line containing CompositingQuality.HighQuality does this already, but maybe you can find an even higher quality mode. What are the differences in file size between Photoshop and C#? And how does the Photoshop image look after you saved it and reopened it? Just resizing in Photoshop doesn't introduce any jpg data loss. You will only notice that after you've saved the image as jpg and then closed and reopened it.
I stumbled upon this question.
I used this code to use no compression of the jpeg and it comes out like the PS version:
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders();
ImageCodecInfo ici = null;
foreach (ImageCodecInfo codec in codecs)
{
if (codec.MimeType == "image/jpeg")
ici = codec;
}
EncoderParameters ep = new EncoderParameters();
ep.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, (long)100);

Categories