diff --git a/README.md b/README.md index 0ede5dd..9c2a9f8 100644 --- a/README.md +++ b/README.md @@ -4,29 +4,46 @@ Create Android bootanimation from .gif or .png images +## Fast bash commands + +```bash +python create_bootanimation.py ../input/1 1440 3120 50 ../input/output/1 --steps 200 --colors 16 --tolerance 1 +python create_bootanimation.py ../input/2 1440 3120 50 ../input/output/2 --steps 200 --colors 16 --tolerance 1 + +zip -0qry -i \*.txt \*.png \*.wav @ ../bootanimation.zip *.txt part* +``` + ## Usage - $python3 create_bootanimation.py SOURCE_FOLDER WIDTH HEIGHT FPS SAVE_TO_FOLDER -ZIP +usage: `create_bootanimation.py [-h] [--zip] [--tolerance TOLERANCE] [--colors COLORS] source width height fps save_to` + +Create Android bootanimation.zip from .gif or bunch of images + +positional arguments: + +- `source` - Absolute path to the GIF file or folder with images. Expected image name format: xxxx-001.png Where: xxx - + some image name; 001 - image number. +- `width` - Width of result images in pixels. You should use width of the device screen +- `height` - Height of result images in pixels. You should use height of the device screen +- `fps` - FPS (Frames Per Second) for animation +- `save_to` - path to the folder where result images should be saved + +optional arguments: -* SOURCE_FOLDER - absolute path to the folder with .gif image or .png -images. If you specify .gif image, it will be unpacked to .png images. -* WIDTH - width of the device screen -* HEIGHT - height of the device screen -* FPS - speed at which images will be displayed -* SAVE_TO_FOLDER - folder where result files will be saved -* -ZIP - (optional) create bootanimation.zip with result files +- `-h, --help` - show this help message and exit +- `--zip` - create bootanimation.zip with result images +- `--tolerance TOLERANCE` - set tolerance for detecting background color. For background detection (0, 0) pixel used +- `--colors COLORS` - set colors count for resulted images ## Examples -Create bootanimation.zip in '/path/to/result_folder' folder from images, that -was unpacked from example.gif, for device with HD screen resolution (1280x720) -and set FPS to 24: +Create bootanimation.zip in '/path/to/result_folder' folder from images, that was unpacked from example.gif, for device +with HD screen resolution (1280x720) and set FPS to 24: $ python3 create_bootanimation.py /path/to/example.gif 720 1280 24 /path/to/result_folder -zip -Create bootanimation.zip in '/path/to/result_folder' folder with images from -folder 'folder_with_images' for device with HD screen resolution (1920x1080) -and set FPS to 60: +Create bootanimation.zip in '/path/to/result_folder' folder with images from folder 'folder_with_images' for device with +HD screen resolution (1920x1080) and set FPS to 60: $ python3 create_bootanimation.py /path/to/folder_with_images 1080 1920 60 /path/to/result_folder @@ -36,4 +53,4 @@ Show help: ## Additional Info -* https://forum.xda-developers.com/showthread.php?t=2756198 +- https://forum.xda-developers.com/showthread.php?t=2756198 diff --git a/create_bootanimation.py b/create_bootanimation.py index 98dcf5c..76647a7 100644 --- a/create_bootanimation.py +++ b/create_bootanimation.py @@ -1,10 +1,17 @@ import argparse +import logging import os import tempfile +import time import zipfile + from PIL import Image + import gifextract +logging.basicConfig(level=logging.INFO) +_log = logging.getLogger(__name__) + def parse_arguments(): parser = argparse.ArgumentParser( @@ -12,11 +19,11 @@ def parse_arguments(): "of images") parser.add_argument("source", type=str, default="", - help="Absolute path to the GIF file or folder with images." - "Expected image name format: xxxx-001.png" - "Where:" - "xxx - some image name;" - "001 - image number.") + help="Absolute path to the GIF file or folder with images. " + "Expected image name format: xxxx-001.png " + "Where: " + "xxx - some image name; " + "001 - image number.") parser.add_argument("width", type=int, default=720, help="Width of result images in pixels. " @@ -33,62 +40,93 @@ def parse_arguments(): help="path to the folder where result images should " "be saved") - parser.add_argument("-zip", action='store_true', + parser.add_argument("--zip", action='store_true', help="create bootanimation.zip with result images") + parser.add_argument("--tolerance", type=int, default=10, + help="set tolerance for detecting background color. " + "For background detection (0, 0) pixel used") + + parser.add_argument("--colors", type=int, default=256, + help="set colors count for resulted images") + + parser.add_argument("--steps", type=int, default=100, + help="set steps count for scanning image") + args = parser.parse_args() - return args.source, args.width, args.height, args.fps, args.save_to, args.zip + return args.source, args.width, args.height, args.fps, \ + args.save_to, args.zip, args.tolerance, args.colors, \ + args.steps -def check_args(t_source, t_width, t_height, t_fps, t_save_to, t_zip): +def check_args(t_source, t_width, t_height, t_fps, t_save_to, + t_zip, t_tolerance, t_colors, t_steps): result = True if len(t_source) <= 0: - print("Error: source path is empty") + _log.error("source path is empty") result = False if os.path.exists(t_source) is False: - print("Error: path '{}' do not exist".format(t_source)) + _log.error("path '{}' do not exist".format(t_source)) result = False if t_width <= 0: - print("Error: width is too small: " + str(t_width)) + _log.error("width is too small: " + str(t_width)) result = False if t_height <= 0: - print("Error: height is too small: " + str(t_height)) + _log.error("height is too small: " + str(t_height)) result = False if t_fps <= 0: - print("Error: fps is too small: " + str(t_fps)) + _log.error("fps is too small: " + str(t_fps)) result = False if t_fps <= 0: - print("Error: fps is too small: " + str(t_fps)) + _log.error("fps is too small: " + str(t_fps)) result = False if len(t_save_to) <= 0: - print("Error: save_to path is empty") + _log.error("save_to path is empty") + result = False + + if t_tolerance <= 0: + _log.error(f'background color tolerance is too small: {t_tolerance}') + result = False + + if t_colors <= 0: + _log.error(f'colors count is too small: {t_colors}') + result = False + + if t_steps <= 0: + _log.error(f'steps count is too small: {t_steps}') result = False return result -def main(t_source, t_width, t_height, t_fps, t_save_to, t_zip): +def main(t_source, t_width, t_height, t_fps, t_save_to, t_zip, t_tolerance, t_colors, t_steps): + start = time.time() + + _log.info('start creating bootimage') + source_dir = "" temp_dir = None if os.path.isdir(t_source): source_dir = t_source elif os.path.isfile(t_source) and get_extension(t_source) == "gif": temp_dir = tempfile.TemporaryDirectory() + _log.info(f'extracting {t_source} to {temp_dir.name}') gifextract.processImage(t_source, temp_dir.name) source_dir = temp_dir.name + _log.debug(f'gif extracted to {source_dir}') else: - print("Error: invalid source path: " + t_source) + _log.error("invalid source path: " + t_source) return images = get_images_paths(source_dir) if len(images) <= 0: - print("Error: no images to process") + _log.error("no images to process") return if not os.path.exists(t_save_to): @@ -100,9 +138,10 @@ def main(t_source, t_width, t_height, t_fps, t_save_to, t_zip): if not os.path.exists(dir_for_images): os.makedirs(dir_for_images) - count = 0 - for img in images: - count = transform_images(img, count, t_width, t_height, dir_for_images) + _log.info(f'{len(images)} images are ready to process') + for idx, img in enumerate(images): + transform_images(img, idx, t_width, t_height, dir_for_images, + t_tolerance, t_colors, t_steps) with open(path_to_desc_file, "a") as f: print("p 1 0 part0", file=f) @@ -117,7 +156,8 @@ def main(t_source, t_width, t_height, t_fps, t_save_to, t_zip): zip_dir(dir_for_images, zip_file) zip_file.close() - print("Done") + end = time.time() + _log.info(f'done in {end - start} seconds') def get_extension(t_path): @@ -151,7 +191,73 @@ def create_desc_file(t_folder, t_width, t_height, t_fps): return file_name -def transform_images(t_img_path, t_count, t_width, t_height, t_save_to_path): +def compare_colors(color_a, color_b, tolerance=10): + return abs(color_a[0] - color_b[0]) < tolerance and \ + abs(color_a[1] - color_b[1]) < tolerance and \ + abs(color_a[2] - color_b[2]) < tolerance + + +def crop_image(image, tolerance, steps=100): + """Crop image""" + + image_pixels = image.load() + + # Get background color + background_color = image_pixels[0, 0] + + _log.debug(f'using background color {background_color}') + + # Get image crop coords + crop_start_x = image.width + crop_start_y = image.height + crop_end_x = 0 + crop_end_y = 0 + + for x in range(0, image.width - 1, int(image.width / steps)): + for y in range(0, image.height - 1, int(image.height / steps)): + if not compare_colors(image_pixels[x, y], background_color, tolerance): + if x < crop_start_x: + crop_start_x = x + if y < crop_start_y: + crop_start_y = y + if x > crop_end_x: + crop_end_x = x + if y > crop_end_y: + crop_end_y = y + + crop_start_x = max(crop_start_x - int(image.width / steps), 0) + crop_start_y = max(crop_start_y - int(image.height / steps), 0) + crop_end_x = min(crop_end_x + int(image.width / steps), image.width - 1) + crop_end_y = min(crop_end_y + int(image.height / steps), image.height - 1) + + # Get only 1 pixel if start > end + if crop_start_x > crop_end_x: + crop_start_x = 0 + crop_end_x = 1 + if crop_start_y > crop_end_y: + crop_start_y = 0 + crop_end_y = 1 + + _log.debug(f'crop coords: ({crop_start_x},{crop_start_y}) - ' + f'({crop_end_x}, {crop_end_y})') + + # Crop image + cropped_image = image.crop((crop_start_x, crop_start_y, + crop_end_x, crop_end_y)) + + _log.debug(f'trim: {cropped_image.width}x{cropped_image.height}+{crop_start_x}+{crop_start_y}') + + return { + 'image': cropped_image, + 'pos_x': crop_start_x, + 'pos_y': crop_start_y + } + + +def transform_images(t_img_path, t_count, t_width, t_height, t_save_to_path, + t_tolerance, t_colors, t_steps): + _log.info(f'processing image {t_count}: {t_img_path}') + original_img = Image.open(t_img_path) # Scale image @@ -159,17 +265,28 @@ def transform_images(t_img_path, t_count, t_width, t_height, t_save_to_path): height_size = int((float(original_img.height) * float(width_percent))) original_img = original_img.resize((t_width, height_size), Image.LANCZOS) - result_image = Image.new("RGB", (t_width, t_height), "white") + result_image = Image.new("RGB", (t_width, t_height), original_img.getpixel((0, 0))) width_pos = 0 height_pos = int(t_height / 2 - original_img.height / 2) result_image.paste(original_img, (width_pos, height_pos)) + # Crop image + _log.debug(f'size before crop: {result_image.width}x{result_image.height}') + crop_result = crop_image(result_image, tolerance=t_tolerance, steps=t_steps) + result_image = crop_result['image'] + _log.debug(f'size after crop: {result_image.width}x{result_image.height}') + + with open(t_save_to_path + '/' + 'trim.txt', mode="a+") as trim_file: + trim_file.write(f'{result_image.width}x{result_image.height}' + f'+{crop_result["pos_x"]}+{crop_result["pos_y"]}\n') + + # Convert image to adaptive palette colors + result_image = result_image.convert('P', palette=Image.ADAPTIVE, colors=t_colors) + result_img_name = "{0:0{width}}.png".format(t_count, width=5) result_img_path = t_save_to_path + "/" + result_img_name result_image.save(result_img_path) - t_count += 1 - return t_count def zip_dir(t_path, t_zip_file):