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);
Related
I have the following image (have put a screen-grab of the image as its size is more than 2 MB - the original can be downloaded from https://drive.google.com/file/d/1rC2QQBzMhZ8AG5Lp5PyrpkOxwlyP9QaE/view?usp=sharing
I'm reading the image using the BitmapDecoder class and saving it using a JPEG Encoder.
This results in the following image which is off color and faded.
var frame = BitmapDecoder.Create(new Uri(inputFilePath, UriKind.RelativeOrAbsolute),BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None).Frames[0];
var encoder = new JpegBitmapEncoder();
encoder.Frames.Add(frame);
using (var stream = File.OpenWrite(outputFilePath))
{
encoder.Save(stream);
}
The image is using PhotoShop RGB Color scheme.I tried setting the color profile using the following code,but this results in this error The designated BitmapEncoder does not support ColorContexts
encoder.ColorContexts = frame.ColorContexts;
Update:
Cloning the image seems to fix the issue. But when i resize the image using the following code for transformation the color profile is not preserved
Transform transform = new ScaleTransform(width / frame.Width * 96 / frame.DpiX, height / frame.Height * 96 / frame.DpiY, 0, 0);
var copy = BitmapFrame.Create(frame);
var resized = BitmapFrame.Create(new
TransformedBitmap(copy, transform));
encoder.Frames.Add(resized);
using (var stream = File.OpenWrite(outputFilePath))
{
encoder.Save(stream);
}
The image bits are identical. This is a metadata issue. This image file contains lots of metadata (Xmp, Adobe, unknown, etc.) and this metadata contains two color profiles/spaces/contexts:
ProPhoto RG (usually found in C:\WINDOWS\system32\spool\drivers\color\ProPhoto.icm)
sRGB IEC61966-2.1 (usually found in WINDOWS\system32\spool\drivers\color\sRGB Color Space Profile.icm)
The problem occurs because the two contexts order may differ in the target file for some reason. An image viewer can either use no color profile (Paint, Pain3D, Paint.NET, IrfanView, etc.), or use (from my experience) the last color profile in the file (Windows Photo Viewer, Photoshop, etc.).
You can fix your issue if you clone the frame, ie:
var frame = BitmapDecoder.Create(new Uri(inputFilePath, UriKind.RelativeOrAbsolute),BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None).Frames[0];
var encoder = new JpegBitmapEncoder();
var copy = BitmapFrame.Create(frame);
encoder.Frames.Add(copy);
using (var stream = File.OpenWrite(outputFilePath))
{
encoder.Save(stream);
}
As in this case, the order is preserved as is.
If you recreate the frame or transform it in any way, you can copy the metadata and color contexts like this:
var ctxs = new List<ColorContext>();
ctxs.Add(frame.ColorContexts[1]); // or just remove this
ctxs.Add(frame.ColorContexts[0]); // first color profile is ProPhoto RG, make sure it's last
var resized = BitmapFrame.Create(new TransformedBitmap(frame, transform), frame.Thumbnail, (BitmapMetadata)frame.Metadata, ctxs.AsReadOnly());
var encoder = new JpegBitmapEncoder();
encoder.Frames.Add(resized);
using (var stream = File.OpenWrite("resized.jpg"))
{
encoder.Save(stream);
}
Note an image that has more than one color context is painful (and IMHO should not be created / saved). Color profiles are for there to ensure a correct display, so two or more (rather conflicting! sRGB vs ProPhoto) profiles associated with one image means it can be displayed ... in two or more ways.
You'll have to determine what's you're preferred color profile in this strange case.
This is the code I'm using to convert the TIFF to PNG.
var image = Image.FromFile(#"Test.tiff");
var encoders = ImageCodecInfo.GetImageEncoders();
var imageCodecInfo = encoders.FirstOrDefault(encoder => encoder.MimeType == "image/tiff");
if (imageCodecInfo == null)
{
return;
}
var imageEncoderParams = new EncoderParameters(1);
imageEncoderParams.Param[0] = new EncoderParameter(Encoder.Quality, 100L);
image.Save(#"Test.png", imageCodecInfo, imageEncoderParams);
The TIFF file size is 46.8 MB (49,161,628 bytes) the PNG that is made using this code is 46.8 MB (49,081,870 bytes) but if I use MS paint the PNG file size is 6.69 MB (7,021,160 bytes).
So what do I change in the code to get the same compress I get by using MS Paint?
Without a good Minimal, Complete, and Verifiable code example, it's impossible to know for sure. But…
The code you posted appears to be getting a TIFF encoder, not a PNG encoder. Just because you name the file with a ".png" extension does not mean that you will get a PNG file. It's the encoder that determines the actual file format.
And it makes perfect sense that if you use the TIFF encoder, you're going to get a file that's exactly the same size as the TIFF file you started with.
Instead, try:
var imageCodecInfo = encoders.FirstOrDefault(encoder => encoder.MimeType == "image/png");
Note that this may or may not get you exactly the same compression used by Paint. PNG has a wide variety of compression "knobs" to adjust the exact way it compresses, and you don't get access to most of those through the .NET API. Paint may or may not be using the same values as your .NET program. But you should at least get a similar level of compression.
OK, after a lot of trial and error I came up with this.
var image = Image.FromFile(#"Test.tiff");
Bitmap bm = null;
PictureBox pb = null;
pb = new PictureBox();
pb.Size = new Size(image.Width, image.Height);
pb.Image = image;
bm = new Bitmap(image.Width, image.Height);
ImageCodecInfo png = GetEncoder(ImageFormat.Png);
EncoderParameters imageEncoderParams = new EncoderParameters(1);
imageEncoderParams.Param[0] = new EncoderParameter(Encoder.Quality, 100L);
pb.DrawToBitmap(bm, pb.ClientRectangle);
bm.Save(#"Test.png", png, encodePars);
pb.Dispose();
And add this to my code.
private ImageCodecInfo GetEncoder(ImageFormat format)
{
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
foreach (ImageCodecInfo codec in codecs)
if (codec.FormatID == format.Guid)
return codec;
return null;
}
By loading the TIFF in a PictureBox then saving it as a PNG the output PNG file size is 7.64 MB (8,012,608 bytes). Witch is a little larger then Paint But that is fine.
I wrote a desktop app which converts an 8bit TIFF to a 1bit but the output file cannot be opened in Photoshop (or other graphics software).
What the application does is
it iterates every 8 bytes (1 byte per pixel) of the original image
then converts each value to bool (so either 0 or 1)
saves every 8 pixels in a byte - bits in the byte are in the same order as the pixels in the original image
The TIFF tags I set: MINISBLACK, compression is NONE, fill order is MSB2LSB, planar config is contiguous. I'm using BitMiracle's LibTiff.NET for reading and writing the files.
What am I doing wrong that the output cannot be opened by popular software?
Input image: http://www.filedropper.com/input
Output image: http://www.filedropper.com/output
From your description of the byte manipulation part, it appears you are converting the image data from 8-bit to 1-bit correctly.
If that's the case, and you don't have specific reasons to do it from scratch using your own code, you can simplify the task of creating valid TIFF files by using System.Drawing.Bitmap and System.Drawing.Imaging.ImageCodecInfo. This allows you to save either uncompressed 1-bit TIFF or compressed files with different types of compression. The code is as follows:
// first convert from byte[] to pointer
IntPtr pData = Marshal.AllocHGlobal(imgData.Length);
Marshal.Copy(imgData, 0, pData, imgData.Length);
int bytesPerLine = (imgWidth + 31) / 32 * 4; //stride must be a multiple of 4. Make sure the byte array already has enough padding for each scan line if needed
System.Drawing.Bitmap img = new Bitmap(imgWidth, imgHeight, bytesPerLine, PixelFormat.Format1bppIndexed, pData);
ImageCodecInfo TiffCodec = null;
foreach (ImageCodecInfo codec in ImageCodecInfo.GetImageEncoders())
if (codec.MimeType == "image/tiff")
{
TiffCodec = codec;
break;
}
EncoderParameters parameters = new EncoderParameters(2);
parameters.Param[0] = new EncoderParameter(Encoder.Compression, (long)EncoderValue.CompressionLZW);
parameters.Param[1] = new EncoderParameter(Encoder.ColorDepth, (long)1);
img.Save("OnebitLzw.tif", TiffCodec, parameters);
parameters.Param[0] = new EncoderParameter(Encoder.Compression, (long)EncoderValue.CompressionCCITT4);
img.Save("OnebitFaxGroup4.tif", TiffCodec, parameters);
parameters.Param[0] = new EncoderParameter(Encoder.Compression, (long)EncoderValue.CompressionNone);
img.Save("OnebitUncompressed.tif", TiffCodec, parameters);
img.Dispose();
Marshal.FreeHGlobal(pData); //important to not get memory leaks
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.
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;
}
///////////////////////////////////
}