Optimize package size - PNG section

Keywords: Front-end Android less Gradle network

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:

android.googlesource.com/platform/fr...

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.

Posted by mwmobley on Thu, 05 Dec 2019 12:08:02 -0800