I am upgrading legacy software (.NET Framework 4.8) to integrate with an API which provides pictures for all users in a base64 encoded string. I am able to take this string, extract it into a byte array, and use create an image which will be used for a thumbnail. The problem arises when I attempt to scale and compress the image to make sure it does not exceed 10kb in size.
Scaling is done by estimating a scaling factor based upon the size of the image and the maximumAllowedBytes of 10,000. This is not a perfect scaling function, but it is an adequate estimation. Getting the size of the file before saving the image is difficult as the size of the memory stream does not equal the size of the bitmap when using the bmp.Save() method. If I take that same byte array and save it using File.SaveAllBytes() however, the size of the resulting jpg perfectly matches the size of the source byte array. Using File.SaveAllBytes method has the downside of removing the ability to control the level of compression. Therefore, this horrible looping solution seems to be the only way to guarantee the file is under 10000 bytes. The loop saves the temporary file with varying degrees of compression until the size of the temporary file is under 10000 bytes.
With this check of the file size I still get some files that are circa 10,200 bytes large despite the temporary file indicating that it should fall under that. If anyone has any tips I'd love to see how I could improve this code without having to slap some additional "padding" to the 10000 byte limit.
The problem arises, however, arises when I attempt to constrain the file to 10kb.
Here is my current solution.
using (MemoryStream ms = new MemoryStream(imageByteArray))
{
Bitmap bmp = new Bitmap(ms);
if (bmp != null)
{
long quality = 82;
//SaveImageToFile(bmp, filePath, quality);
long size = ObtainFileSize(bmp, imageName, quality);
double scaleFactor =
Math.Sqrt((double)allowedFileSizeInByte / (double)ms.Length);
int newWidth = (int)(bmp.Width * scaleFactor);
int newHeight = (int)(bmp.Height * scaleFactor);
bmp = new Bitmap(bmp, new Size(newWidth, newHeight));
while (size > allowedFileSizeInByte)
{
quality = quality - 5;
size = ObtainFileSize(bmp, imageName, quality);
}
SaveImageToFile(bmp, filePath, quality);
bmp.Dispose();
//result.Dispose();
}
}
In addition I have two helper methods used for the actual saving.
private void SaveImageToFile(Bitmap bmp, string imagePath, long quality)
{
try
{
//All images will be encoded as ajpg with varyig quality
ImageCodecInfo jgpEncoder = GetEncoder(ImageFormat.Jpeg);
System.Drawing.Imaging.Encoder imageEncoder =
System.Drawing.Imaging.Encoder.Quality;
EncoderParameters myEncoderParameters = new EncoderParameters(1);
myEncoderParameters.Param[0] = new EncoderParameter(imageEncoder, quality);
bmp.Save(imagePath, jgpEncoder, myEncoderParameters);
}
catch
{
throw;
}
}
private long ObtainFileSize(Bitmap bmp, string imageName, long quality)
{
using (Bitmap newBmp = new Bitmap(bmp))
{
// Get the temporary folder path
string tempPath = Path.GetTempPath();
// Combine the path with your file name
string tempFile = Path.Combine(tempPath, imageName + ".jpeg");
// Save the image to the temporary file
SaveImageToFile(newBmp, tempFile, quality);
long size = new FileInfo(tempFile).Length;
File.Delete(tempFile);
return size;
}
}
// for now we only use JPG but this can change...
private ImageCodecInfo GetEncoder(ImageFormat format)
{
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
foreach (ImageCodecInfo codec in codecs)
{
if (codec.FormatID == format.Guid)
{
return codec;
}
}
return null;
}
}
Following advice from JonasH I developed my solution a bit more to get the following.
public void ProcessImages(List<WorkerPicture> workerPictures, int allowedFileSizeInByte, string imagePath)
{
if (string.IsNullOrEmpty(_imagePath))
{
throw new Exception("Image File Path cannot be empty!");
}
foreach (var workerPicture in workerPictures)
{
try
{
// Decode the base64 string into a byte array
var imageByteArray = workerPicture.ImageBinary;
var imageName = workerPicture.EmpId;
var filePath = imagePath + "\\" + imageName + ".jpeg";
using (MemoryStream ms = new MemoryStream(imageByteArray))
{
using (System.Drawing.Image sourceImage = System.Drawing.Image.FromStream(ms))
{
double scaleFactor = Math.Sqrt((double)allowedFileSizeInByte / (double)ms.Length);
int targetWidth = (int)(sourceImage.Width * scaleFactor);
int targetHeight = (int)(sourceImage.Height * scaleFactor);
using (Bitmap targetImage = new Bitmap(targetWidth, targetHeight))
{
using (Graphics g = Graphics.FromImage(targetImage))
{
g.DrawImage(sourceImage, 0, 0, targetWidth, targetHeight);
}
ImageCodecInfo jpgEncoder = GetEncoder(ImageFormat.Jpeg);
System.Drawing.Imaging.Encoder myEncoder = System.Drawing.Imaging.Encoder.Quality;
EncoderParameters myEncoderParameters = new EncoderParameters(1);
long quality = 100;
while (quality >= 10)
{
myEncoderParameters.Param[0] = new EncoderParameter(myEncoder, quality);
using (MemoryStream ms2 = new MemoryStream())
{
targetImage.Save(ms2, jpgEncoder, myEncoderParameters);
if (ms2.Length <= allowedFileSizeInByte)
{
File.WriteAllBytes(filePath, ms2.ToArray());
break;
}
quality -= 10;
}
}
}
}
}
}
catch
{
throw;
}
}
}
// for now we only use JPG but this can change...
private ImageCodecInfo GetEncoder(ImageFormat format)
{
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
foreach (ImageCodecInfo codec in codecs)
{
if (codec.FormatID == format.Guid)
{
return codec;
}
}
return null;
}