background
Compared with JPEG image, PNG image is a lossless image storage format, and there is an additional transparency channel, so in general, PNG image is larger than JPEG image, and PNG image is often the big head of APK image resources, so optimizing the size of PNG image is more rewarding for reducing the volume of packet.
wiki about PNG:
Portable Network Graphics (PNG) is a kind of lossless compression Of bitmap Graphics format, support index Grayscale,RGB Three color schemes and Alpha Channel And other characteristics.
wiki about JPEG:
Joint Photographic Experts Group (JPEG) is widely used for photo image Lossy compression Standard method.
Common compression algorithms
There are many compression algorithms for PNG. Here we only talk about two more commonly used ones: Indexed_color and Color_quantization . These two are also recommended by Google on the Android Developer website. For details, see network-xfer.
Let's briefly talk about the general principles of these two algorithms. For more in-depth knowledge, please move to Google or Wiki.
Indexed_color
The literal meaning is index color. The file size can be reduced by converting the specific ARGB color storage to the table below the index. We know that in ARGB, each channel needs 8 bits, i.e. 1 byte. An ARGB needs 4 bytes, while the index only needs 1 byte. The color that the index points to is stored in an array called palette.
wiki definition:
In computing, index color is managed in a limited way digital image Color technology to save computers Memory and file space , while speeding up display refresh and file transfer. It is Vector quantization compressed A form.
Image from wiki:
This algorithm is very good, but it also has disadvantages. The size of color palette usually only supports 4,16256, that is to say, the maximum number of colors can not exceed 256, so the number of colors in PNG pictures that can apply this algorithm can not exceed 256.
Color_quantization
The literal meaning is color vectorization. By using similar colors to reduce the types of colors used in the image, combined with color palette, to reduce the size of the image file. This is a lossy compression algorithm.
Image from wiki:
This is an image using the standard 24 bit RGB color:
This is an image optimized to use only 16 colors:
The disadvantage of this algorithm is that the quality is lossy, so how to achieve a balance between the quality and the size is very important.
Talk about AAPT
AAPT is a resource packaging tool for Android. Our common R file is to use it to survive. In addition, it also has the function of compressing PNG pictures. AAPT now has AAPT and AAPT2. By default, we mean AAPT2.
For AAPT2's knowledge of PNG image compression, see Colt McAnlis's article, Smaller PNGs, and Android's AAPT tool
PS: the author is the bald head in the Android performance Dictionary...
AAPT2 for PNG image compression can be divided into three aspects:
- Can RGB be converted to grayscale
- Whether transparent channel can be deleted
- Is it only 256 colors at most
Next, let's start with the source code and just look at the above points.
The source code analysis uses Android 6.0, the version of marshmallow:
For PNG, the analysis code is located in the analyze_image() method. According to international practice, some code that does not affect the analysis is deleted.
static void analyze_image() { int w = imageInfo.width; int h = imageInfo.height; uint32_t colors[256], col; int num_colors = 0; bool isOpaque = true; bool isPalette = true; bool isGrayscale = true; // Scan the entire image and determine if: // 1. Every pixel has R == G == B (grayscale) // 2. Every pixel has A == 255 (opaque) // 3. There are no more than 256 distinct RGBA colors for (j = 0; j < h; j++) { const png_byte* row = imageInfo.rows[j]; png_bytep out = outRows[j]; for (i = 0; i < w; i++) { rr = *row++; gg = *row++; bb = *row++; aa = *row++; int odev = maxGrayDeviation; maxGrayDeviation = MAX(ABS(rr - gg), maxGrayDeviation); maxGrayDeviation = MAX(ABS(gg - bb), maxGrayDeviation); maxGrayDeviation = MAX(ABS(bb - rr), maxGrayDeviation); // Check if image is really grayscale if (isGrayscale) { if (rr != gg || rr != bb) { // ==>> Code 1 isGrayscale = false; } } // Check if image is really opaque if (isOpaque) { if (aa != 0xff) { // ==>> Code 2 isOpaque = false; } } // Check if image is really <= 256 colors if (isPalette) { col = (uint32_t) ((rr << 24) | (gg << 16) | (bb << 8) | aa); bool match = false; for (idx = 0; idx < num_colors; idx++) { if (colors[idx] == col) { match = true; break; } } if (!match) { if (num_colors == 256) { // ==>> Code 3 isPalette = false; } else { colors[num_colors++] = col; } } } } } *paletteEntries = 0; *hasTransparency = !isOpaque; int bpp = isOpaque ? 3 : 4; int paletteSize = w * h + bpp * num_colors; // Choose the best color type for the image. // 1. Opaque gray - use COLOR_TYPE_GRAY at 1 byte/pixel // 2. Gray + alpha - use COLOR_TYPE_PALETTE if the number of distinct combinations // is sufficiently small, otherwise use COLOR_TYPE_GRAY_ALPHA // 3. RGB(A) - use COLOR_TYPE_PALETTE if the number of distinct colors is sufficiently // small, otherwise use COLOR_TYPE_RGB{_ALPHA} if (isGrayscale) { if (isOpaque) { // ==>> Code 4 *colorType = PNG_COLOR_TYPE_GRAY; // 1 byte/pixel } else { // Use a simple heuristic to determine whether using a palette will // save space versus using gray + alpha for each pixel. // This doesn't take into account chunk overhead, filtering, LZ // compression, etc. if (isPalette && (paletteSize < 2 * w * h)) { // ==>> Code 5 *colorType = PNG_COLOR_TYPE_PALETTE; // 1 byte/pixel + 4 bytes/color } else { // ==>> Code 6 *colorType = PNG_COLOR_TYPE_GRAY_ALPHA; // 2 bytes per pixel } } } else if (isPalette && (paletteSize < bpp * w * h)) { // ==>> Code 7 *colorType = PNG_COLOR_TYPE_PALETTE; } else { if (maxGrayDeviation <= grayscaleTolerance) { // ==>> Code 8 *colorType = isOpaque ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_GRAY_ALPHA; } else { // ==>> Code 9 *colorType = isOpaque ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA; } } } //Copy code
Firstly, three variables are defined to represent: isOpaque (opaque), isparette (palette support), and isGrayscale (grayscale conversion).
First look at the code at Code 1:
if (rr != gg || rr != bb) { // ==>> Code 1 isGrayscale = false; } //Copy code
Only when RGB channels have the same color can they be converted to grayscale.
The code at Code 2 is to determine whether the transparent channel is 0, that is, whether it is opaque or not
if (aa != 0xff) { // ==>> Code 2 isOpaque = false; } //Copy code
Code 3 is to determine whether 256 color palette can be used:
if (!match) { if (num_colors == 256) { // ==>> Code 3 isPalette = false; } else { colors[num_colors++] = col; } } //Copy code
Colors is an array, which stores the colors that have appeared in the picture (not repeated). When the number of colors is greater than 256, it means that the palette mode is not supported.
Then, according to these conditions, we can determine which storage mode to use. The storage modes supported in AAPT are as follows:
-
PNG_COLOR_TYPE_PALETTE
Using palette mode, the final image size is 1 byte per pixel + 4 bytes per color in the palette
-
PNG_COLOR_TYPE_GRAY
Gray mode, which is the most economical mode, 1 byte per pixel
-
PNG_COLOR_TYPE_GRAY_ALPHA
Gray mode, with transparent channel, 2 bytes for one pixel
-
PNG_COLOR_TYPE_RGB
RGB mode, transparent channel removed, 3 bytes per pixel
-
PNG_COLOR_TYPE_RGB_ALPHA
ARGB mode, 4 bytes per pixel
PNG_COLOR_TYPE_PALETTE
To use this mode, you need to meet the following two conditions, Code 5 and Code 7:
Code 5
if (isGrayscale) { if (isOpaque) { } else { if (isPalette && (paletteSize < 2 * w * h)) { // ==>> Code 5 *colorType = PNG_COLOR_TYPE_PALETTE; // 1 byte/pixel + 4 bytes/color } } } //Copy code
On the premise of supporting gray-scale mode, there are transparent channels, supporting palette mode, and the length of palette is less than 2 * w * h.
Code 7
if (isGrayscale) { } else { if (isPalette && (paletteSize < bpp * w * h)) { // Code ==>> 7 *colorType = PNG_COLOR_TYPE_PALETTE; } } //Copy code
If grayscale mode is not supported, but palette is supported, and the length of palette is less than bpp * w * h, the size of bpp depends on whether it is opaque or not:
int bpp = isOpaque ? 3 : 4; Copy code
PNG_COLOR_TYPE_GRAY
To use this mode, it needs to support gray-scale mode and be opaque. Code at Code 4:
if (isGrayscale) { if (isOpaque) { // ==>> Code 4 *colorType = PNG_COLOR_TYPE_GRAY; // 1 byte/pixel } } //Copy code
###PNG_COLOR_TYPE_GRAY_ALPHA
Gray scale, and there is a mode of transparent channel. Codes in Code 6 and Code 8:
Code 6
if (isGrayscale) { if (isOpaque) { } else { if (isPalette && (paletteSize < 2 * w * h)) { } else { // ==>> Code 6 *colorType = PNG_COLOR_TYPE_GRAY_ALPHA; // 2 bytes per pixel } } } //Copy code
Code 8
if (isGrayscale) { } else if (isPalette && (paletteSize < bpp * w * h)) { } else { if (maxGrayDeviation <= grayscaleTolerance) { // ==>> Code 8 *colorType = isOpaque ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_GRAY_ALPHA; } else { } } //Copy code
maxGrayDeviation is the direct difference between RGB channels. If it is less than the threshold value of grayscale range, it can also be converted to grayscale.
PNG_COLOR_TYPE_RGB
The transparent channel can be deleted for opaque pictures. The code is in Code 9:
if (isGrayscale) { } else if (isPalette && (paletteSize < bpp * w * h)) { } else { if (maxGrayDeviation <= grayscaleTolerance) { } else { // ==>> Code 9 *colorType = isOpaque ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA; } } //Copy code
PNG_COLOR_TYPE_RGB_ALPHA
There's nothing to say about this. The last one is the bottom covering mode.
Summary
AAPT's optimization of PNG is mainly the Indexed_color algorithm, which is also a conservative choice, because this is lossless. If we want a higher compression rate, we can use some other compression tools to integrate into our compilation and packaging process.
PNG compression tool comparison
First of all, before we choose other PNG compression tools, we need to disable AAPT's default compression mode first, because for PNG compression, it is not 1 + 1 > 2, which can be turned off by the following code:
android { aaptOptions { cruncherEnabled = false } } Copy code
Now commonly used PNG compression tools are pngcrush,pngquant,zopfli ,tinypng Wait, we will not consider tinypng here first, because this only provides the form of HTTP interface, and there is a limit on the number of times.
Using Gradle integration
In order to integrate PNG compression tools into APK's build and compile process, we use Gradle to implement. The author currently uses Android Gradle plug-in version 3.5.0. In this version, we can get all the resource directories through applicationvariant.allrawandroid resources.
It should be noted that our Task needs to be executed before the MergeResources Task, so that we can overwrite the compressed same name and merge it into the APK file.
afterEvaluate { applicationVariants.all { ApplicationVariant variant -> if (variant.buildType.name != "release") { return } def compressPngTask = task('compressPng') compressPngTask.doLast { List<File> allResource = [] variant.allRawAndroidResources.files.forEach { dir -> if (!dir.exists()) { return } if (dir.isFile()) { if (dir.name.endsWith(".png")) { allResource.add(file) } } else { dir.traverse { file -> if (file.name.endsWith(".png")) { allResource.add(file) } } } } allResource.forEach { file -> // kb def oldSize = file.size() / 1024f println "path = ${file.path}" try { // TODO, this is the logic of compression } catch (Throwable ignore) { println "file: ${file.name} error: ${ignore.message}" } println "${file.name}: $oldSize KB ==> ${file.size() / 1024f} KB" } } Task mergeResourcesTask = variant.mergeResourcesProvider.get() mergeResourcesTask.dependsOn(compressPngTask) } } //Copy code
First, we create a Task named compressPng. The Task of this Task is to collect all PNG file paths. Here, we filter out the variants whose BuildType is not release. Finally, we make MergeResources Task rely on compressPng Task.
Remember to turn off AAPT's default PNG compression first.
Here we first record the APK file size after turning off AAPT default PNG compression and using default PNG compression:
What I am looking for here is a live project. There are more picture resource files in it. I have filtered JPEG and. 9 pictures. There are about 1000 PNG pictures. Only one compression thread is used.
Compression tool | APK size (MB) | time consuming |
---|---|---|
nothing | 81.9 | 29s |
AAPT | 78.9 | 1m 18s |
pngquant
First, test pngquant. We integrate pngquant into:
exec { ExecSpec spec -> spec.workingDir(project.file('.')) spec.commandLine("./pngquant", "--skip-if-larger", "--speed", "1", "--strip", "--nofs", "--force", "--output", file.path, "--", file.path) } //Copy code
Here we basically use the default configuration:
- --Skip if larger means that if the compressed image is larger, skip
- --speed 1 means using the slowest compression speed in exchange for better compression quality
- --strip delete some metadata of pictures
- --nofs disable Floyd–Steinberg dithering Image jitter processing
- --force overwrite source file
- --Path to output output file
Execute the Clean Task first, and then re execute the package. The final result is as follows:
Compression tool | APK size (MB) | time consuming |
---|---|---|
nothing | 81.9 | 29s |
AAPT | 78.9 | 32s |
pngquant | 73.7 | 1m 18s |
zopflipng
Integrated:
exec { ExecSpec spec -> spec.workingDir(new File('/Users/leo/Documents/projects/zopfli')) spec.commandLine("./zopflipng", "-y", "-m", file.path, file.path) } //Copy code
The basic configuration is also used:
- -y overwrite the original file without asking
- -m compress as much as possible, depending on the size of the file
Execute the Clean Task first, and then re execute the package. The final result is as follows:
Compression tool | APK size (MB) | time consuming |
---|---|---|
nothing | 81.9 | 29s |
AAPT | 78.9 | 32s |
pngquant | 73.7 | 1m 18s |
zopflipng | 78 | 36m 17s |
pngcursh
Integrated:
exec { ExecSpec spec -> spec.workingDir(new File('/Users/leo/Documents/projects/pngcrush-1.8.13')) spec.commandLine("./pngcrush", "-ow","-reduce", file.path) } //Copy code
Use basic configuration:
- -ow means overwrite the original file
- -reduce reduces the type and bit depth of lossless color values
- -brute tries 176 compression methods
Execute the Clean Task first, and then re execute the package. The final result is as follows:
Compression tool | APK size (MB) | time consuming |
---|---|---|
nothing | 81.9 | 29s |
AAPT | 78.9 | 32s |
pngquant | 73.7 | 1m 18s |
zopflipng | 78 | 36m 17s |
pngcursh | 78.7 | 13m 56s |
Summary
Although pngquant is the best choice from the result, because pngcursh only uses the default compression configuration, and pngcursh provides the most parameters, so which is better depends on parameter adjustment. As we all know, parameter adjustment is also a technical activity.
summary
It is recommended to use WebP and SVG instead of PNG and JPEG images, but some images in the tripartite library are beyond our control, so PNG compression tools can be used for optimization. As for the balance of compression rate and compression time, it is up to you to adjust the parameters.
Author: Leo struggling
Links: https://juejin.im/post/5de77f37e51d45583317d73a
Source: Nuggets
The copyright belongs to the author. For commercial reprint, please contact the author for authorization. For non-commercial reprint, please indicate the source.