def _split_apng(apng_path: Path, out_dir: Path, name: str, criteria: SplitCriteria) -> List[Path]: """Extracts all of the frames of an animated PNG into a folder Args: apng_path (Path): Path to the APNG. out_dir (Path): Path to the output directory. name (str): New prefix name of the sequence. criteria (SplitCriteria): Splitting criteria to follow. Returns: List[Path]: List of paths for every split image. """ frame_paths = [] apng = APNG.open(apng_path) apng_frames = InternalImageAPI.get_apng_frames(apng, criteria.is_unoptimized) # frames = _fragment_apng_frames(apng_path, criteria) pad_count = criteria.pad_count shout_nums = imageutils.shout_indices(len(apng.frames), 5) save_name = criteria.new_name or name for index, (fr, control) in enumerate(apng_frames): if shout_nums.get(index): stdio.message(f"Saving split frames... ({shout_nums.get(index)})") save_path = out_dir.joinpath( f"{save_name}_{str.zfill(str(index), pad_count)}.png") fr.save(save_path, format="PNG") frame_paths.append(save_path) if criteria.extract_delay_info: stdio.message("Generating delay information file...") imageutils.generate_delay_file(apng_path, "PNG", out_dir) return frame_paths
def quantize_png_images( cls, apngopt_criteria: APNGOptimizationCriteria, image_paths: List[Path], out_dir: Path = "", ) -> List[Path]: """Use pngquant to perform an array of modifications on a sequence of PNG images. Args: pq_args (List): pngquant arguments. image_paths (List[Path]): Path to each image optional_out_path (Path, optional): Optional path to save the quantized PNGs to. Defaults to "". Returns: List[Path]: [description] """ if not out_dir: out_dir = filehandler.mk_cache_dir(prefix_name="quant_temp") pq_args = cls._pngquant_args_builder(apngopt_criteria) quantized_frames = [] stdio.debug("WILL QUANTIZE") # quantization_args = " ".join([arg[0] for arg in pq_args]) # descriptions = " ".join([arg[1] for arg in pq_args]) # quant_dir = mk_cache_dir(prefix_name="quant_dir") shout_nums = imageutils.shout_indices(len(image_paths), 1)
def extract_gif_frames(cls, gif_path: Path, name: str, criteria: SplitCriteria, out_dir: Path) -> List[Path]: """Extract all frames of a GIF image and return a list of paths of each frame Args: gif_path (Path): Path to gif. name (str): Filename of sequence, before appending sequence numbers (zero-padded). criteria (SplitCriteria): Criteria to follow. out_dir (Optional[Path]): Optional output directory of the split frames, else use default fragment_dir Returns: List[Path]: List of paths of each extracted gif frame. """ fr_paths = [] # indexed_ratios = _get_aimg_delay_ratios(unop_gif_path, "GIF", criteria.is_duration_sensitive) with Image.open(gif_path) as gif: total_frames = gif.n_frames gifsicle_path = cls.gifsicle_path shout_nums = imageutils.shout_indices(total_frames, 1) for n in range(0, total_frames): if shout_nums.get(n): stdio.message(f"Extracting frames ({n}/{total_frames})") split_gif_path: Path = out_dir.joinpath( f"{name}_{str.zfill(str(n), criteria.pad_count)}.gif") args = [ str(gifsicle_path), str(gif_path), f'#{n}', "--output", str(split_gif_path) ] cmd = " ".join(args) # logger.debug(cmd) process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) index = 0 while process.poll() is None: output = process.stdout.readline() # if process.poll() is not None: # break # if output: # output = output.decode("utf-8") # logger.message(output.capitalize()) fr_paths.append(split_gif_path) return fr_paths
def inspect_sequence(image_paths: List[Path]) -> Dict: """Gets information of a selected sequence of static images Args: image_paths (List[Path]): List of resolved image sequence paths Returns: Dict: Information of the image sequence as a whole """ abs_image_paths = image_paths sequence_info = [] shout_nums = imageutils.shout_indices(len(abs_image_paths), 1) for index, path in enumerate(abs_image_paths): if shout_nums.get(index): stdio.message(f"Loading images... ({shout_nums.get(index)})") info = inspect_general(path, filter_on="static", skip=True) if info: gen_info = info.format_info()["general_info"] sequence_info.append(gen_info) # logger.message(sequence_info) if not sequence_info: stdio.error( "No images selected. Make sure the path to them are correct and they are not animated images!" ) return {} static_img_paths = [si["absolute_url"]["value"] for si in sequence_info] # print("imgs count", len(static_img_paths)) # first_img_name = os.path.splitext(os.path.basename(static_img_paths[0]))[0] # filename = first_img_name.split('_')[0] if '_' in first_img_name else first_img_name sequence_count = len(static_img_paths) sequence_filesize = filehandler.read_filesize( sum((si["fsize"]["value"] for si in sequence_info))) # im = Image.open(static_img_paths[0]) # width, height = im.size # im.close() image_info = { "name": sequence_info[0]["base_filename"]["value"], "total": sequence_count, "sequence": static_img_paths, "sequence_info": sequence_info, "total_size": sequence_filesize, "width": sequence_info[0]["width"]["value"], "height": sequence_info[0]["height"]["value"], } return image_info
def create_animated_gif(image_paths: List, out_full_path: Path, crbundle: CriteriaBundle) -> Path: """Generate an animated GIF image by first applying transformations and lossy compression on them (if specified) and then converting them into singular static GIF images to a temporary directory, before compiled by Gifsicle. Args: image_paths (List): List of path to each image in a sequence crbundle (CriteriaBundle): Bundle of animated image creation criteria to adhere to. out_full_path (Path): Complete output path with the target name of the GIF. Returns: Path: Path of the created GIF. """ criteria = crbundle.create_aimg_criteria stdio.debug({"_build_gif crbundle": crbundle}) black_bg_rgba = Image.new("RGBA", size=criteria.size, color=(0, 0, 0, 255)) target_dir = filehandler.mk_cache_dir(prefix_name="tmp_gifrags") fcount = len(image_paths) if criteria.start_frame: image_paths = imageutils.shift_image_sequence(image_paths, criteria.start_frame) shout_nums = imageutils.shout_indices(fcount, 1) for index, ipath in enumerate(image_paths): if shout_nums.get(index): stdio.message(f"Processing frames... ({shout_nums.get(index)})") with Image.open(ipath) as im: im = transform_image(im, crbundle.create_aimg_criteria) im = gif_encode(im, crbundle, black_bg_rgba) fragment_name = str(ipath.name) if criteria.reverse: reverse_index = len(image_paths) - (index + 1) fragment_name = f"rev_{str.zfill(str(reverse_index), 6)}_{fragment_name}" else: fragment_name = f"{str.zfill(str(index), 6)}_{fragment_name}" save_path = target_dir.joinpath(f"{fragment_name}.gif") im.save(save_path) out_full_path = GifsicleAPI.combine_gif_images(target_dir, out_full_path, crbundle) shutil.rmtree(target_dir) # logger.control("CRT_FINISH") return out_full_path
def split_apng(cls, target_path: Path, seq_rename: str = "", out_dir: Path = "") -> Iterator[Path]: """Split an APNG image into its individual frames using apngdis Args: target_path (Path): Path of the APNG. seq_rename (str, optional): New prefix name of the sequence. Defaults to "". out_dir (Path, optional): Output directory. Defaults to "". Returns: Iterator[Path]: Iterator of paths to each split image frames of the APNG. """ split_dir = filehandler.mk_cache_dir(prefix_name="apngdis_dir") filename = target_path.name target_path = shutil.copyfile(target_path, split_dir.joinpath(filename)) cwd = os.getcwd() # target_rel_path = os.path.relpath(target_path, cwd) uuid_name = f"{uuid.uuid4()}_" stdio.debug(f"UUID: {uuid_name}") args = [str(cls.dis_exec_path), str(target_path), uuid_name] if seq_rename: args.append(seq_rename) cmd = " ".join(args) # logger.message(f"APNGDIS ARGS: {cmd}") fcount = len(APNG.open(target_path).frames) stdio.debug(f"fcount: {fcount}") shout_nums = imageutils.shout_indices(fcount, 5) stdio.debug(f"split_apng cmd -> {cmd}") stdio.debug(args) process = subprocess.Popen( args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1, universal_newlines=True, ) # process = subprocess.Popen(cmd, stdout=sys.stdout, stderr=sys.stdout, bufsize=1, universal_newlines=True) # unbuffered_Popen(cmd) # for line in unbuffered_process(process): # logger.message(line) index = 0 while True: stdio.message(index) output = process.stdout.readline() stdio.message(output) err = process.stderr.readline() if process.poll() is not None: break if output and shout_nums.get(index): stdio.message( f"Extracting frames... ({shout_nums.get(index)})") # if err: # yield {"apngdis stderr": err.decode('utf-8')} index += 1 # while True: # # output = process.stdout.readline() # # logger.message(output) # # err = process.stderr.readline() # if process.poll() is not None: # break # # if output and shout_nums.get(index): # # logger.message(f'Extracting frames... ({shout_nums.get(index)})') # # if err: # # yield {"apngdis stderr": err.decode('utf-8')} # index += 1 # for line in iter(process.stdout.readline(), b''): # yield {"msg": line.decode('utf-8')} stdio.message("Getting splitdir...") fragment_paths = (split_dir.joinpath(f) for f in split_dir.glob("*") if f != filename and f.name.startswith(uuid_name) and f.suffixes[-1] == ".png") return fragment_paths
def _create_gifragments(image_paths: List[Path], criteria: CreationCriteria, gif_opt_criteria: GIFOptimizationCriteria): """Generate a sequence of static GIF images created from the input sequence with the specified criteria. Args: image_paths (List[Path]): List of image paths to be converted into GIF images. criteria (CreationCriteria): Creation criteria. """ # disposal = 0 # if criteria.reverse: # image_paths.reverse() # temp_gifs = [] out_dir = filehandler.mk_cache_dir(prefix_name="tmp_gifrags") fcount = len(image_paths) if criteria.start_frame: image_paths = imageutils.shift_image_sequence(image_paths, criteria.start_frame) shout_nums = imageutils.shout_indices(fcount, 1) black_bg = Image.new("RGBA", size=criteria.size) for index, ipath in enumerate(image_paths): if shout_nums.get(index): stdio.message(f"Processing frames... ({shout_nums.get(index)})") with Image.open(ipath) as im: im: Image.Image transparency = im.info.get("transparency", False) orig_width, orig_height = im.size alpha = None # if im.mode == "RGBA" and criteria.preserve_alpha: # pass # im = InternalImageAPI.dither_alpha(im) if criteria.flip_x: im = im.transpose(Image.FLIP_LEFT_RIGHT) if criteria.flip_y: im = im.transpose(Image.FLIP_TOP_BOTTOM) if criteria.must_resize(width=orig_width, height=orig_height): resize_method_enum = getattr(Image, criteria.resize_method) # yield {"resize_method_enum": resize_method_enum} im = im.resize( (round(criteria.width), round(criteria.height)), resample=resize_method_enum, ) # if criteria.must_rotate(): # im = im.rotate(criteria.rotation, expand=True) fragment_name = str(ipath.name) if criteria.reverse: reverse_index = len(image_paths) - (index + 1) fragment_name = f"rev_{str.zfill(str(reverse_index), 6)}_{fragment_name}" else: fragment_name = f"{str.zfill(str(index), 6)}_{fragment_name}" save_path = out_dir.joinpath(f"{fragment_name}.gif") if im.mode == "RGBA": if gif_opt_criteria.is_dither_alpha: stdio.debug(gif_opt_criteria.dither_alpha_threshold_value) stdio.debug(gif_opt_criteria.dither_alpha_method_enum) im = InternalImageAPI.dither_alpha(im, method=gif_opt_criteria.dither_alpha_method_enum, threshold=gif_opt_criteria.dither_alpha_threshold_value) if criteria.preserve_alpha: alpha = im.getchannel("A") im = im.convert("RGB").convert("P", palette=Image.ADAPTIVE, colors=255) mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0) im.paste(255, mask) im.info["transparency"] = 255 else: bg_image = black_bg.copy() bg_image.alpha_composite(im) # im.show() im = bg_image # black_bg.show() im = im.convert("P", palette=Image.ADAPTIVE) im.save(save_path) elif im.mode == "RGB": im = im.convert("RGB").convert("P", palette=Image.ADAPTIVE) im.save(save_path) elif im.mode == "P": if transparency: if type(transparency) is int: im.save(save_path, transparency=transparency) else: im = im.convert("RGBA") alpha = im.getchannel("A") im = im.convert("RGB").convert("P", palette=Image.ADAPTIVE, colors=255) mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0) im.paste(255, mask) im.info["transparency"] = 255 im.save(save_path) else: im.save(save_path) # yield {"msg": f"Save path: {save_path}"} # if absolute_paths: # temp_gifs.append(save_path) # else: # temp_gifs.append(os.path.relpath(save_path, os.getcwd())) return out_dir
def _build_apng(image_paths: List[Path], out_full_path: Path, crbundle: CriteriaBundle) -> Path: criteria = crbundle.create_aimg_criteria aopt_criteria = crbundle.apng_opt_criteria # temp_dirs = [] # if aopt_criteria.is_lossy: # qtemp_dir = mk_cache_dir(prefix_name="quant_temp") # temp_dirs.append(qtemp_dir) # image_paths = PNGQuantAPI.quantize_png_images(aopt_criteria, image_paths) # qsequence_info = {} # for index, ip in enumerate(image_paths): # with Image.open(ip) as im: # palette = im.getpalette() # if palette: # qimg_info = { # 'colors': np.array(im.getcolors()), # 'palette': imageutils.reshape_palette(palette), # 'transparency': im.info.get('transparency') # } # qsequence_info[index] = qimg_info # logger.debug(qimg_info) # im.resize((256, 256), Image.NEAREST).show() if criteria.reverse: image_paths.reverse() apng = APNG() img_sizes = set(Image.open(i).size for i in image_paths) stdio.message(str([f"({i[0]}, {i[1]})" for i in img_sizes])) uneven_sizes = len(img_sizes) > 1 or (criteria.width, criteria.height) not in img_sizes shout_nums = imageutils.shout_indices(len(image_paths), 1) # if criteria.flip_x or criteria.flip_y or uneven_sizes or aopt_criteria.is_lossy\ # or aopt_criteria.convert_color_mode: out_dir = filehandler.mk_cache_dir(prefix_name="tmp_apngfrags") preprocessed_paths = [] # logger.debug(crbundle.create_aimg_criteria.__dict__) for index, ipath in enumerate(image_paths): fragment_name = str(ipath.name) if criteria.reverse: reverse_index = len(image_paths) - (index + 1) fragment_name = f"rev_{str.zfill(str(reverse_index), 6)}_{fragment_name}" else: fragment_name = f"{str.zfill(str(index), 6)}_{fragment_name}" save_path = out_dir.joinpath(f"{fragment_name}.png") if shout_nums.get(index): stdio.message(f"Processing frames... ({shout_nums.get(index)})") # with io.BytesIO() as bytebox: with Image.open(ipath) as im: im: Image.Image orig_width, orig_height = im.size has_transparency = im.info.get("transparency") is not None or im.mode == "RGBA" stdio.debug(f"Color mode im: {im.mode}") if criteria.must_resize(width=orig_width, height=orig_height): resize_method_enum = getattr(Image, criteria.resize_method) # yield {"resize_method_enum": resize_method_enum} im = im.resize( (round(criteria.width), round(criteria.height)), resize_method_enum, ) im = im.transpose(Image.FLIP_LEFT_RIGHT) if criteria.flip_x else im im = im.transpose(Image.FLIP_TOP_BOTTOM) if criteria.flip_y else im if im.mode == "P": if has_transparency: im = im.convert("RGBA") else: im = im.convert("RGB") # if criteria.rotation: # im = im.rotate(criteria.rotation, expand=True) # logger.debug(f"Modes comparison: {im.mode}, {aopt_criteria.new_color_mode}") quant_method = Image.FASTOCTREE if has_transparency else Image.MEDIANCUT if aopt_criteria.is_reduced_color: stdio.debug(f"Frame #{index}, has transparency: {has_transparency}, transparency: " f"{im.info.get('transparency')}, quantization method: {quant_method}") im = im.quantize(aopt_criteria.color_count, method=quant_method).convert("RGBA") if aopt_criteria.convert_color_mode: im = im.convert(aopt_criteria.new_color_mode) # logger.debug(f"SAVE PATH IS: {save_path}") im.save(save_path, "PNG") if aopt_criteria.quantization_enabled: save_path = PNGQuantAPI.quantize_png_image(aopt_criteria, save_path) preprocessed_paths.append(save_path) # apng.append(PNG.from_bytes(bytebox.getvalue()), delay=int(criteria.delay * 1000)) stdio.message("Saving APNG....") if criteria.start_frame: preprocessed_paths = imageutils.shift_image_sequence(preprocessed_paths, criteria.start_frame) delay_fraction = Fraction(1/criteria.fps).limit_denominator() apng = APNG.from_files(preprocessed_paths, delay=delay_fraction.numerator, delay_den=delay_fraction.denominator) apng.num_plays = criteria.loop_count apng.save(out_full_path) # else: # logger.message("Saving APNG....") # apng = APNG.from_files(image_paths, delay=int(criteria.delay * 1000)) # apng.num_plays = criteria.loop_count # apng.save(out_full_path) if aopt_criteria.is_optimized: out_full_path = APNGOptAPI.optimize_apng(out_full_path, out_full_path, aopt_criteria) # for td in temp_dirs: # shutil.rmtree(td) stdio.preview_path(out_full_path) # logger.control("CRT_FINISH") shutil.rmtree(out_dir) return out_full_path
def _split_gif(gif_path: Path, out_dir: Path, criteria: SplitCriteria) -> List[Path]: """Unoptimizes GIF, and then splits the frames into separate images Args: gif_path (Path): Path to the GIF image out_dir (Path): Output directory of the image sequence criteria (SplitCriteria): Image splitting criteria Raises: Exception: [description] Returns: List[Path]: Paths to each split images """ frame_paths = [] name = criteria.new_name or gif_path.stem # unop_dir = filehandler.mk_cache_dir(prefix_name="unop_gif") # unop_gif_path = unop_dir.joinpath(gif_path.name) # color_space = criteria.color_space target_path = Path(gif_path) stdio.message(str(target_path)) # if color_space: # if color_space < 2 or color_space > 256: # raise Exception("Color space must be between 2 and 256!") # else: # logger.message(f"Reducing colors to {color_space}...") # target_path = GifsicleAPI.reduce_gif_color(gif_path, unop_gif_path, color=color_space) # ===== Start test splitting code ===== # gif: GifImageFile = GifImageFile(gif_path) # for index in range(0, gif.n_frames): # gif.seek(index) # gif.show() # new = gif.convert("RGBA") # new.show() # with io.BytesIO() as bytebox: # gif.save(bytebox, "GIF") # yield {"bytebox": bytebox.getvalue()} # yield {"GIFINFO": [f"{d} {getattr(gif, d, '')}" for d in gif.__dir__()]} # ===== End test splitting code ===== if criteria.is_unoptimized: stdio.message("Unoptimizing and splitting GIF...") # ImageMagick is used to unoptimized rather than Gifsicle's unoptimizer because Gifsicle doesn't support # unoptimization of GIFs with local color table # target_path = ImageMagickAPI.unoptimize_gif(gif_path, unop_gif_path) frame_paths = ImageMagickAPI.extract_unoptimized_gif_frames( gif_path, name, criteria, out_dir) else: stdio.message("Splitting GIF...") # frames = _fragment_gif_frames(target_path, name, criteria) frame_paths = GifsicleAPI.extract_gif_frames(target_path, name, criteria, out_dir) # gif = Image.open(gif_path) if criteria.convert_to_rgba: shout_nums = imageutils.shout_indices(len(frame_paths), 5) for index, fpath in enumerate(frame_paths): # gif.seek(index) if shout_nums.get(index): stdio.message( f"Converting frames into RGBA color mode... ({shout_nums.get(index)})" ) # save_path = out_dir.joinpath(f"{save_name}_{str.zfill(str(index), criteria.pad_count)}.png") with Image.open(fpath).convert("RGBA") as im: # alpha = im.getchannel("A") # im = im.convert("RGB").convert("P", palette=Image.ADAPTIVE, colors=255) # mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0) # im.paste(255, mask) # im.info["transparency"] = 255 im.save(fpath, "PNG") # else: # with Image.open(fpath, formats=["GIF"]) as im: # if index in range(0, 4): # im.show() # logger.debug(im.info) # im.save(fpath, "GIF") if criteria.extract_delay_info: stdio.message("Generating delay information file...") imageutils.generate_delay_file(gif_path, "GIF", out_dir) # shutil.rmtree(unop_dir) stdio.debug({"frame_paths": frame_paths}) return frame_paths