preface
All the contents and algorithms described in this article do not use any external libraries, and have been used in the open source compression software PicSizer
PicSizer is a batch image compression software independently written by me. Its main function is to realize the compression of web page images. Therefore, all algorithms give priority to web page display. If you are interested in image compression, you can go to Gitee View the source code. The software is completely open source, with a size of less than 1 MB. It can be used safely, and there will be no residue after deletion.
https://gitee.com/dearxuan/pic-sizer/releases
Thread management
Namespace required in this section:
using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading;
Multithreading is a way to make full use of CPU, but if the number of threads exceeds the number of logical processors of CPU, it will be counterproductive. Moreover, a large number of graphic calculation and IO operations will also cause the program to get stuck. Therefore, in PicSizer, I chose 2 threads by default and 10 threads at most
When using the C# built-in ThreadPool, I found that even if one thread is started, there will be serious jams, so I use my own thread pool
Thread pool
The specific idea of implementing thread pool is to create a specified number of threads, and then continuously read pictures from an array through an endless loop for compression until the end.
The process is very simple, and the code is given below
//Start compression for (int i = 0; i < 10; i++) { //Create a high priority thread and execute it immediately Thread thread = new Thread(() => { //Compressed picture code }) { Priority = ThreadPriority.Highest }; //Thread start thread.Start(); } //Compression complete //Other codes
When the compression is finished, we should do some "aftermath" work. The actual situation is that the function ends as soon as 10 threads are created. In order to enable the function to wait for these 10 compression threads, we can use WaitHandle, which avoids simultaneous access by creating exclusive resources. Here, we can use its "busy wait" feature to monopolize a resource in the sub threads, Release these resources after completion, and the main thread will wait because the resources are occupied by other threads until all sub threads are finished
private static List<WaitHandle> waitHandles = new List<WaitHandle>(); public static void StartThreadsPool() { //Empty all exclusive resources waitHandles.Clear(); //Create 10 child threads for (int i = 0; i < 10; i++) { //Create an exclusive resource ManualResetEvent manual = new ManualResetEvent(false); //Add to array waitHandles.Add(manual); //Create a new thread Thread thread = new Thread(() => { //Pass exclusive resources to a child thread DoInThread(manual); }) { Priority = ThreadPriority.Normal }; thread.Start(); } //Wait for all resources in the array to be released before continuing execution WaitHandle.WaitAll(waitHandles.ToArray()); //dealing with the aftermath //...... } public static void DoInThread(ManualResetEvent manualResetEvent) { int index; //Get the sequence number of the next station picture. If it is - 1, it means there is no picture while ((index = GetNext()) != -1) { //Compressed picture } //The cycle ends and resources are released manualResetEvent.Set(); return; }
Thread synchronization
When two threads "write" to the same resource, thread synchronization needs to be considered. In this article, we want 10 threads to share a function to obtain the subscript of the next image in the array. Obviously, the "write" operation is used here, so thread synchronization is required, that is, only one thread is allowed to access at a time
The implementation of C # is very simple. You only need to add a sentence to the function
[MethodImpl(MethodImplOptions.Synchronized)] public static int GetIndex() { //Get subscript }
Picture reading and writing
Namespace required in this section:
using System; using System.Drawing; using System.Drawing.Imaging; using System.IO;
Read from file
Bitmap bitmap = new Bitmap("File path");
Write to hard disk
bitmap.Save("export path", imageFormat);
imageFormat is the output format. Note that this format is not the same as the suffix. A "*. PNG" file is not necessarily a PNG image
imageFormat has many options. If you want to export BMP images, you can write it like this
bitmap.Save(path, ImageFormat.Bmp);
Memory stream read / write
If you want to get the file size after output, you can directly save the Bitmap to disk and read it. However, in the next algorithm, a large number of output files are required, and these files are one-time. Frequent reading and writing to the hard disk will reduce the service life of the hard disk, and the efficiency is also very low. We can simulate the output file in memory and then read the file size in memory.
//Create a memory stream MemoryStream memoryStream = new MemoryStream(); //Write Bitmap to memory bitmap.Save(memoryStream, imageFormat); //Destroy memory stream memoryStream.Dispose();
Now we can define a function to calculate the size of Bitmap output to memory in the specified format
public static long LengthOfBitmapInMemory(Bitmap bitmap, ImageFormat imageFormat) { MemoryStream memoryStream = null; try { memoryStream = new MemoryStream(); bitmap.Save(memoryStream, imageFormat); return memoryStream.Length >> 10;//The displacement here is only used for unit conversion and can be removed } finally { //Destroy the memory stream in time memoryStream?.Dispose(); } }
ICON file structure
For the detailed physical structure of ICON, you can go to Microsoft Document view
ICON files are mainly divided into header, data segment and pixel segment
The header saves the basic information of the file, such as the file type and the number of icons contained (multiple icons can be saved in ICON)
Each data segment corresponds to an icon, which stores icon related information, such as size, color gamut and pixel offset
The pixel segment holds the specific pixel value of each icon
C # built-in Icon class cannot be saved to the hard disk. We need to write it by bit. The code saved as Ico is given below
private static void SaveAsIcon(Bitmap bitmap, string path, byte size) { Image image = null; FileStream fileStream = null; BinaryWriter writer = null; try { image = new Bitmap(bitmap, size, size); fileStream = new FileStream(path, FileMode.Create); writer = new BinaryWriter(fileStream); //ICON file header (0x0) writer.Write((short)0);//Reserved bit, must be 0 writer.Write((short)1);//Resource type (1 for ICON) writer.Write((short)1);//There are several resources in this document //ICON file data segment (0x6) writer.Write((byte)size);//Width, offset 0x6 writer.Write((byte)size);//Height, offset 0x7 writer.Write((byte)0);//Number of pixels (0 means > = 8bpp) writer.Write((byte)0);//Reserved bit, must be 0 writer.Write((short)0);//Color palette (I don't know what to use) writer.Write((short)32);//Bit depth, 32-bit color writer.Write((int)0);//Pixel segment length. The specific length is not known yet. Replace it with 0 first writer.Write((int)0x16);//The pixel segment offset corresponding to the data segment must be 0x16 because there is one picture in total //ICON file pixel segment (offset 0x16) image.Save(fileStream, ImageFormat.Png); //Now that the length of the pixel segment is known, control the pointer to move back and write again writer.Seek(0xE, SeekOrigin.Begin); //The length of pixel segment is the length of the current entire file stream minus the length of header and data segment, i.e. Length-22 writer.Write((int)fileStream.Length - 22); } finally { writer?.Dispose(); fileStream?.Dispose(); image?.Dispose(); } }
Considering that most of the written data are fixed, I save the file header and data segment as a byte array. Next time, I just need to write this array first, and then modify the data of relevant fields through offset
//Header and segment array private static readonly byte[] _ICON_HEADER = new byte[] { 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 22, 0, 0, 0 }; private static void SaveAsIcon(Bitmap bitmap, string path, byte size) { Image image = null; FileStream fileStream = null; BinaryWriter writer = null; try { image = new Bitmap(bitmap, size, size); fileStream = new FileStream(path, FileMode.CreateNew); writer = new BinaryWriter(fileStream); //Write header byte array writer.Write(_ICON_HEADER); //Write pixel segment image.Save(fileStream, ImageFormat.Png); //The offset 0x6 is the width of the picture writer.Seek(0x6, SeekOrigin.Begin); writer.Write(size); //The offset 0x7 is the height of the picture writer.Seek(0x7, SeekOrigin.Begin); writer.Write(size); //The offset 0xE is the length of the main part of the picture writer.Seek(0xE, SeekOrigin.Begin); writer.Write((int)fileStream.Length - 22); } finally { writer?.Dispose(); fileStream?.Dispose(); image?.Dispose(); } }
Image preprocessing
Namespace required in this section:
using System; using System.Drawing; using System.Drawing.Imaging; using System.IO;
zoom
There are two ways to scale Bitmap. The simplest method requires only one line of code
Bitmap bitmap = new Bitmap(oldBitmap, width, height);
Scaling itself is not difficult, but in practice, we usually don't want the picture to be too large or too small, because the browser will automatically enlarge the picture with smaller size, resulting in blur. Therefore, we can set a benchmark size. If the picture is larger than it, it will be reduced to the same size as it. Otherwise, it will not be scaled
int LimitWidth = 1920; int LimitHeight = 1080; public static Bitmap Scale(Bitmap bitmap) { int width = bitmap.Width; int height = bitmap.Height; //Calculate the ratio float widthByMin = (float)width / LimitWidth; float heightByMin = (float)height / LimitHeight; //Find the smaller one float min = Math.Min(widthByMin, heightByMin); //If the smaller one is greater than 1, the picture size exceeds the limit if(min > 1) { //Zoom in and out according to the smaller one, so as to ensure that one of the length and width is exactly the limit value and the other is slightly larger than the limit value width = (int)(width / min); height = (int)(height / min); return new Bitmap(bitmap, width, height); } //The picture is not zoomed. Return to the original picture return bitmap; }
Center crop
Suppose the original size of the picture is 500 × 600, we want to cut it into 1000 × If the size is 1000, the first step is to get the size of the clipping area of the picture, i.e. 500 × 500, then cut the picture to 500 × 500, and finally zoom in to 1000 × one thousand
First of all, we should calculate the ratio that the size of the restriction needs to be scaled. This ratio is actually the min in the previous code block, which will not be repeated here
The second part is to transfer the Bitmap and ratio to a function for clipping
private static Bitmap CenterCutBitmap(Bitmap bitmap, float scale) { //Multiply the limiting size by the ratio to get the clipping area size of Bitmap //Width and height are the width and height of the area to be cropped on the bitmap int final_width = (int)(LimitWidth * scale); int final_height = (int)(LimitHeight * scale); //The upper left corner of the clipping region of the bitmap int left = (bitmap.Width - final_width) / 2; int top = (bitmap.Height - final_height) / 2; //Create a new Bitmap to save the cropped image Bitmap newBitmap = new Bitmap(LimitWidth, LimitHeight, PixelFormat.Format24bppRgb); //Draw on a new Bitmap Graphics g = Graphics.FromImage(newBitmap); //Use the highest brush quality g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; g.DrawImage(bitmap, //This parameter is the size of drawing on the new Bitmap and should fill the whole newBitmap new Rectangle(0, 0, LimitWidth, LimitHeight), //This parameter is the size of the color taken on the old Bitmap, and only the middle part should be intercepted new Rectangle(left, top, final_width, final_height), GraphicsUnit.Pixel); g.Dispose(); bitmap.Dispose(); return newBitmap; }
Compression method
Namespace required in this section:
using System; using System.Drawing; using System.Drawing.Imaging; using System.IO;
Image quality compression
For JPEG images, we can adjust its image quality. Lower image quality means smaller volume
First, get the coding parameters
//Get JPEG codec public static ImageCodecInfo _Info_JPEG = Encoder.GetEncoderInfo("image/jpeg"); public static System.Drawing.Imaging.Encoder encoder = System.Drawing.Imaging.Encoder.Quality; public static EncoderParameter[] parameterList = new EncoderParameter[101]; //This method returns an array of encoding information according to the specified image quality. This array needs to be used when compressing JPEG public static EncoderParameters GetEncoderParameters(long value) { EncoderParameters encoderParameters = new EncoderParameters(1); encoderParameters.Param[0] = GetParameter(value); return encoderParameters; } //This method returns the encoding information containing the specified image quality according to the parameters. The range of value is: [0100] public static EncoderParameter GetParameter(long value) { int v = (int)value; //In order to improve performance, you can save the used coding information and retrieve it only when there is no in the array if (parameterList[v] == null) { parameterList[v] = new EncoderParameter(encoder, value); } return parameterList[v]; } //Get image codec public static ImageCodecInfo GetEncoderInfo(string type) { int j; ImageCodecInfo[] encoders; encoders = ImageCodecInfo.GetImageEncoders(); for (j = 0; j < encoders.Length; ++j) { if (encoders[j].MimeType == type) { return encoders[j]; } } return null; }
Now we can use this encoding information to compress JPEG images
public static void CompressionByValue(string file) { Bitmap bitmap = null; try { bitmap = new Bitmap(file); //Create an array of encoding information and pass it in as a parameter EncoderParameters encoderParameters = new EncoderParameters(1); //Obtain the coding information when the picture quality is 50 encoderParameters.Param[0] = GetParameter(50L); //Save to hard disk bitmap.Save("Save path", _Info_JPEG, encoderParameters); } finally { bitmap?.Dispose(); } }
Bit depth compression
For non JPEG images, because they do not provide modifiable parameters, they cannot reduce the volume through image quality. At this time, we can reduce the color gamut
The class representing pixel format in C# is PixelFormat. Here are four common pixel formats
public static PixelFormat[] pixelFormats = new PixelFormat[] { PixelFormat.Format8bppIndexed, PixelFormat.Format16bppArgb1555, PixelFormat.Format32bppArgb, PixelFormat.Format64bppArgb };
The lower the bit depth, the smaller the number of bytes required to store a pixel and the smaller the file size. However, if there are fewer bytes to store pixels, the color range that a pixel can represent becomes less, which may cause some color display abnormalities. Modifying the bit depth is very simple, and only one line of code is required
//Copies the Bitmap with the specified bit depth Bitmap newBitmap = oldBitmap.Clone( new Rectangle(oldBitmap.Width, oldBitmap.Height), pixelFormat);
This method is effective for all pictures
Zoom compression
In the browser, we can modify the html tag appropriately to make the picture display to the specified size. If the picture is small or large, the browser will automatically zoom for us. Therefore, we can reduce the size of the picture to reduce the volume without considering its actual display effect
The only disadvantage of this method is that the enlarged image will become blurred, but compared with the color anomaly caused by bit depth compression, this loss is acceptable
Compress to specified size
Strictly speaking, it is almost impossible to compress to the specified size. What we can do is to compress to the best condition that it does not exceed the specified size. For image quality compression, bit depth compression and scaling compression, we can make it by adjusting parameters
Taking image quality compression as an example, the image quality can be divided into 101 levels (0 ~ 100). First, create an array to store the file size under each image quality
long[] sizeList = new long[101];
Common sense shows that the file size is directly proportional to the image quality, so we can quickly find the highest image quality that does not exceed a given size through binary search
//The maximum volume is 1024KB long LimitSize = 1024; //Use binary search to obtain the maximum image quality that does not exceed the given value private static bool Compress(string file) { using (Bitmap bitmap = new Bitmap(file)) { long left = 0L, right = 100L, mid = 0L; long[] sizeList = new long[101]; //Enter binary search while (left < right - 1) { //Calculate intermediate value mid = (left + right) / 2; //Find the file volume corresponding to mid sizeList[mid] = GetBitmapSize(bitmap, mid); //Even if the current volume meets the requirements, continue to search, because the goal is to find the highest image quality that meets the requirements if (sizeList[mid] <= LimitSize) { left = mid; } else { right = mid; } } //At this time, left is the highest image quality that can be selected if (sizeList[left] == 0) { sizeList[left] = GetBitmapSize(bitmap, left); } //The file volume corresponding to left may still exceed the limit, so we need to add a judgment if (sizeList[left] <= LimitSize) { bitmap.Save("Save path"); return true; } else { return false; } } }
Here is only an example of image quality compression. In fact, it is also applicable to the other two compression methods. For bit depth compression, different pixel formats can be listed as an array for searching; For scaling compression, you can adjust the scaling ratio to 0.01 ~ 1.00 to find