今是昨非

今是昨非

日出江花红胜火,春来江水绿如蓝

Batch Image Compression & Replacement

Batch Image Compression & Replacement#

Background#

Recently, the product team raised a requirement to compress the package, and since the project is a mix of OC & Swift, with RN added this time, the package cannot increase in size. It's quite a headache... But since the requirement is out, it still needs to be done. So I thought of the following methods:

  1. First, use LSUnusedResources to analyze the unused images and classes in the project and delete them;
  2. Then compress and replace the images in the project;
  3. Next, analyze the linkMap file to identify large files for optimization.
  4. An iOS package size reduction solution based on clang plugins

Implementation#

This article is about the second step. There are about 1600 images in the project. In previous compressions, I sorted them by size and uploaded images larger than 10kb one by one to tinypng for compression, then downloaded and replaced them. Tinypng web supports a maximum of 20 images at a time, so I had to upload, compress, and wait. It was quite annoying...

Batch Image Compression#

So, this time I finally couldn't stand it anymore. I wanted to find a way to compress in bulk, and I actually found a Batch Image Compression Script (Python). The usage is clearly documented on GitHub. You can compress 500 images in bulk per month, and there is an output folder:

When using this script, be aware of the following:

  1. Install Python
  2. Install click and tinify
  3. Apply for an API key here: https://tinypng.com/developers. One key allows you to compress 500 images for free each month, and you can apply for multiple keys.
    pip install click // Install click library
    pip install --upgrade tinify // Install tinify library

Then use the script. The print function in the script from that GitHub user hasn't been updated, so here’s my updated version:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-

import os
import sys
import os.path
import click
import tinify

tinify.key = "Your API Key"		# API KEY
version = "1.0.1"				# Version

# Core compression function
def compress_core(inputFile, outputFile, img_width):
	source = tinify.from_file(inputFile)
	if img_width is not -1:
		resized = source.resize(method = "scale", width  = img_width)
		resized.to_file(outputFile)
	else:
		source.to_file(outputFile)

# Compress images in a folder
def compress_path(path, width):
	print ("compress_path-------------------------------------")
	if not os.path.isdir(path):
		print ("This is not a folder, please enter the correct path!")
		return
	else:
		fromFilePath = path 			# Source path
		toFilePath = path+"/tiny" 		# Output path
		print ("fromFilePath=%s" %fromFilePath)
		print ("toFilePath=%s" %toFilePath)

		for root, dirs, files in os.walk(fromFilePath):
			print ("root = %s" %root)
			print ("dirs = %s" %dirs)
			print ("files= %s" %files)
			for name in files:
				fileName, fileSuffix = os.path.splitext(name)
				if fileSuffix == '.png' or fileSuffix == '.jpg' or fileSuffix == '.jpeg':
					toFullPath = toFilePath + root[len(fromFilePath):]
					toFullName = toFullPath + '/' + name
					if os.path.isdir(toFullPath):
						pass
					else:
						os.mkdir(toFullPath)
					compress_core(root + '/' + name, toFullName, width)
			break									# Only traverse the current directory

# Compress a specified file
def compress_file(inputFile, width):
	print ("compress_file-------------------------------------")
	if not os.path.isfile(inputFile):
		print ("This is not a file, please enter the correct path!")
		return
	print ("file = %s" %inputFile)
	dirname  = os.path.dirname(inputFile)
	basename = os.path.basename(inputFile)
	fileName, fileSuffix = os.path.splitext(basename)
	if fileSuffix == '.png' or fileSuffix == '.jpg' or fileSuffix == '.jpeg':
		compress_core(inputFile, dirname+"/tiny_"+basename, width)
	else:
		print ("Unsupported file type!")

@click.command()
@click.option('-f', "--file",  type=str,  default=None,  help="Compress a single file")
@click.option('-d', "--dir",   type=str,  default=None,  help="Folder to be compressed")
@click.option('-w', "--width", type=int,  default=-1,    help="Image width, default unchanged")
def run(file, dir, width):
	print ("GcsSloop TinyPng V%s" %(version))
	if file is not None:
		compress_file(file, width)				# Compress a single file
		pass
	elif dir is not None:
		compress_path(dir, width)				# Compress files in the specified directory
		pass
	else:
		compress_path(os.getcwd(), width)		# Compress files in the current directory
	print ("Finished!")

if __name__ == "__main__":
    run()

Batch Image Replacement#

Yeah, after using this script, images can be compressed in bulk, but the compressed images are generated in a separate folder. I need to replace them in bulk, but I don't know the exact directory of my images... oh no.

So, you pushed me to think. I wondered if I could directly replace the images after reading the compressed ones; or write a separate batch replacement script because the large directory is fixed, and the names of the images before and after compression do not change. This should work, so I just did it.

When using it, change TargetPath in the Python file to the main directory where you want to replace, and SourcePath to the directory of the compressed images after running the previous script, then run it, bingo, done.

The principle:

  1. Read all files in the specified directory & subdirectories
  2. Check if it's an image; if so, store it in an array
  3. Read the Target and Source directories, then traverse and split by '/', taking the last part to check for equality; if equal, write it in.

import os
import shutil

# Check if it's an image
def is_img(ext):
    ext = ext.lower()
    if ext in ['.jpg', '.png', '.jpeg', '.bmp']:
        return True
    else:
        return False

TargetPath = 'Your Target Path' # Directory to copy to
SourcePath = 'Your Source Path' # Directory to copy from

# Get all images in the specified directory
def get_img_files(dir):
    py_files = []
    for root, dirs, files in os.walk(dir):
        for file in files:
            pre, suf = os.path.splitext(file)
            if is_img(suf):
                py_files.append(os.path.join(root, file))
    return py_files

TargetFiles = get_img_files(TargetPath)
SourceFiles = get_img_files(SourcePath)

for target in TargetFiles:
    for source in SourceFiles:
        targetName = target.split('/')[-1]
        sourceName = source.split('/')[-1]
        if targetName == sourceName:
            shutil.copyfile(source, target)

Batch image compression & replacement, two-in-one
With these two scripts, batch compression and replacement can be achieved, but I have to run two scripts, which is quite troublesome. Can I combine them into one? Just asking if you can?
 
Of course, this can't stump my cleverness.

The output directory of the compression script is the source directory of the replacement script, and the source directory of the compression script is the output directory of the replacement script. 
So I modified the implementation of the compression script to read a fixed directory, and then changed the output directory of the compression script, making sure it's not a subdirectory of the read directory to avoid issues. 
Then, after the compression script executes successfully, run the batch replacement script, done.

```python
#!/usr/bin/env python
# -*- coding: UTF-8 -*-

import os
import sys
import os.path
import click
import tinify
import shutil

tinify.key = "Your API KEY"		# API KEY
version = "1.0.1"				# Version

TargetPath = 'Your Target Path' # Directory to copy to
SourcePath = 'Your Source Path' # Directory to copy from

# Core compression function
def compress_core(inputFile, outputFile, img_width):
	source = tinify.from_file(inputFile)
	if img_width is not -1:
		resized = source.resize(method = "scale", width  = img_width)
		resized.to_file(outputFile)
	else:
		source.to_file(outputFile)

# Compress images in a folder
def compress_path(path, width):
	print ("compress_path-------------------------------------")
	if not os.path.isdir(path):
		print ("This is not a folder, please enter the correct path!")
		return
	else:
		fromFilePath = path 			# Source path
		toFilePath = SourcePath 		# Output path
		print ("fromFilePath=%s" %fromFilePath)
		print ("toFilePath=%s" %toFilePath)

		for root, dirs, files in os.walk(fromFilePath):
			print ("root = %s" %root)
			print ("dirs = %s" %dirs)
			print ("files= %s" %files)
			for name in files:
				fileName, fileSuffix = os.path.splitext(name)
				if fileSuffix == '.png' or fileSuffix == '.jpg' or fileSuffix == '.jpeg':
					toFullPath = toFilePath + root[len(fromFilePath):]
					toFullName = toFullPath + '/' + name
					if os.path.isdir(toFullPath):
						pass
					else:
						os.mkdir(toFullPath)
					compress_core(root + '/' + name, toFullName, width)
			# break									# Only traverse the current directory

# Compress a specified file
def compress_file(inputFile, width):
	print ("compress_file-------------------------------------")
	if not os.path.isfile(inputFile):
		print ("This is not a file, please enter the correct path!")
		return
	print ("file = %s" %inputFile)
	dirname  = os.path.dirname(inputFile)
	basename = os.path.basename(inputFile)
	fileName, fileSuffix = os.path.splitext(basename)
	if fileSuffix == '.png' or fileSuffix == '.jpg' or fileSuffix == '.jpeg':
		compress_core(inputFile, dirname+"/tiny_"+basename, width)
	else:
		print ("Unsupported file type!")

# Check if it's an image
def is_img(ext):
    ext = ext.lower()
    if ext in ['.jpg', '.png', '.jpeg', '.bmp']:
        return True
    else:
        return False

# Get all images in the specified directory
def get_img_files(dir):
    py_files = []
    for root, dirs, files in os.walk(dir):
        for file in files:
            pre, suf = os.path.splitext(file)
            if is_img(suf):
                py_files.append(os.path.join(root, file))
    return py_files

# Batch replacement
def batch_replace_img():
    TargetFiles = get_img_files(TargetPath)
    SourceFiles = get_img_files(SourcePath)

    for target in TargetFiles:
        for source in SourceFiles:
            targetName = target.split('/')[-1]
            sourceName = source.split('/')[-1]
            if targetName == sourceName:
                shutil.copyfile(source, target)

@click.command()
@click.option('-f', "--file",  type=str,  default=None,  help="Compress a single file")
@click.option('-d', "--dir",   type=str,  default=None,  help="Folder to be compressed")
@click.option('-w', "--width", type=int,  default=-1,    help="Image width, default unchanged")
def run(file, dir, width):
	print ("GcsSloop TinyPng V%s" %(version))
	if file is not None:
		compress_file(file, width)				# Compress a single file
		pass
	elif dir is not None:
		compress_path(dir, width)				# Compress files in the specified directory
		pass
	else:
		compress_path(os.getcwd(), width)		# Compress files in the current directory
	print ("Compression finished!")

if __name__ == "__main__":
    # run(None, TargetPath, None)
    compress_path(TargetPath, -1)
    batch_replace_img()
    print("Replacement finished")

To Be Continued#

But, because my project directory has over 1600 images, and the batch compression script can only execute 500 images per month, and I don't have a fixed way to read the images, I can't compress them all at once. So, I need to think about whether I can achieve this in one go.

Is there an API for batch compression without a limit on the number?
If not, how can I ensure the continuity of executing this script multiple times, i.e., after executing once, how to continue with a different key?
... Just thinking about it gives me a headache.
....to be continued

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.