def reduce_gif_color(cls, gif_path: Path, out_path: Path, color: int = 256) -> Path: """Reduce the color of a GIF image. Args: gif_path (Path): Path to the GIF. out_path (Path): Output path to save the color-reduced GIF as. color (int, optional): Amount of color to reduce to. Defaults to 256. Returns: Path: Absolute path of the color-reduced GIF. """ stdio.message("Performing color reduction...") # redux_gif_path = out_dir.joinpath(gif_path.name) args = [ str(cls.gifsicle_path), f"--colors={color}", str(gif_path), "--output", str(out_path), ] cmd = " ".join(args) subprocess.run(args) return out_path
def imagemagick_render(cls, target_path: Path, out_full_path: Path, crbundle: CriteriaBundle) -> Path: """Use imagemagick to perform an array of modifications to an existing animated image. Args: target_path (Path): Target path of the animated image. out_full_path (Path): Output full path to save the animated image to. Returns: Path: Path of the new modified animated image. """ magick_args = cls._mod_args_builder(crbundle.gif_opt_criteria) # yield {"magick_args": magick_args} for index, (arg, description) in enumerate(magick_args, start=1): stdio.message( f"index {index}, arg {arg}, description: {description}") cmdlist = [ str(cls.imagemagick_path), arg, f'"{target_path}"', "--output", f'"{out_full_path}"', ] cmd = " ".join(cmdlist) # logger.message(f"[{shift_index + index}/{total_ops}] {description}") # yield {"cmd": cmd} subprocess.run(cmd) if target_path != out_full_path: target_path = out_full_path return target_path
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 unoptimize_gif(cls, gif_path: Path, out_path: Path) -> Path: """Perform GIF unoptimization using Gifsicle/ImageMagick, in order to obtain the true singular frames for Splitting purposes. Returns the path of the unoptimized GIF. Args: gif_path (Path): Path to GIF image out_path (Path): Output path of unoptimized GIF Returns: Path: Path of unoptimized GIF """ # raise Exception(gif_path, out_dir) supressed_error_txts = ["binary operator expected"] args = [ str(cls.imagemagick_path), "convert", "-coalesce", str(gif_path), str(out_path) ] cmd = " ".join(args) stdio.debug(cmd) process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) while process.poll() is None: if process.stdout: stdout_res = process.stdout.readline().decode("utf-8") if stdout_res: stdio.message(stdout_res) if process.stderr: stderr_res = process.stderr.readline().decode("utf-8") if stderr_res and not any(s in stderr_res for s in supressed_error_txts): stdio.error(stderr_res) return out_path
def _modify_gif(gif_path: Path, out_path: Path, metadata: AnimatedImageMetadata, crbundle: CriteriaBundle) -> Path: if crbundle.gif_opt_criteria.is_unoptimized: stdio.message("Unoptimizing GIF...") # ImageMagick is used to unoptimized rather than Gifsicle's unoptimizer because Gifsicle doesn't support # unoptimization of GIFs with local color table gif_path = ImageMagickAPI.unoptimize_gif(gif_path, out_path) final_path = GifsicleAPI.modify_gif_image(gif_path, out_path, metadata, crbundle) return final_path
def _modify_apng(apng_path: Path, out_path: Path, metadata: AnimatedImageMetadata, crbundle: CriteriaBundle) -> Path: stdio.message("Modifying APNG...") mod_criteria = crbundle.modify_aimg_criteria aopt_criteria = crbundle.apng_opt_criteria apng_im: APNG = APNG.open(apng_path) stdio.debug({"crbundle": crbundle}) # Reiterate through the frames if matching certain conditions, or per-frame lossy compression is required if mod_criteria.apng_must_reiterate(metadata) or aopt_criteria.is_reduced_color or aopt_criteria.is_unoptimized: stdio.debug(f"REITERATE APNG") new_apng: APNG = APNG() orig_width, orig_height = metadata.width["value"], metadata.height["value"] alpha_base = Image.new("RGBA", size=(orig_width, orig_height)) unoptimized_apng_frames = InternalImageAPI.get_apng_frames(apng_im, unoptimize=True) if mod_criteria.reverse: unoptimized_apng_frames = reversed(list(unoptimized_apng_frames)) for index, (im, control) in enumerate(unoptimized_apng_frames): # logger.debug(png.chunks) delay_fraction = Fraction(1/mod_criteria.fps).limit_denominator() # delay = int(mod_criteria.delay * 1000) control.delay = delay_fraction.numerator control.delay_den = delay_fraction.denominator stdio.debug({"fr_control": control}) if mod_criteria.must_transform(metadata) or aopt_criteria.is_reduced_color or aopt_criteria.convert_color_mode\ or aopt_criteria.is_unoptimized: # with io.BytesIO() as img_buf: # png.save(img_buf) # with Image.open(img_buf) as im: # im.show() stdio.debug({"fr_orig_size": im.size}) has_transparency = im.info.get("transparency") is not None or im.mode == "RGBA" im = im.resize(mod_criteria.size, resample=getattr(Image, mod_criteria.resize_method)) im = im.transpose(Image.FLIP_LEFT_RIGHT) if mod_criteria.flip_x else im im = im.transpose(Image.FLIP_TOP_BOTTOM) if mod_criteria.flip_y else im if im.mode == "P": if has_transparency: im = im.convert("RGBA") else: im = im.convert("RGB") quant_method = Image.FASTOCTREE if has_transparency else Image.MEDIANCUT if aopt_criteria.is_reduced_color: im = im.quantize( aopt_criteria.color_count, method=quant_method, dither=1).convert("RGBA") if aopt_criteria.convert_color_mode: im = im.convert(aopt_criteria.new_color_mode) with io.BytesIO() as new_buf: im.save(new_buf, "PNG") new_apng.append(PNG.from_bytes(new_buf.getvalue()), delay=delay_fraction.numerator, delay_den=delay_fraction.denominator) stdio.debug(f"NEW FRAMES COUNT: {len(new_apng.frames)}") if len(new_apng.frames) > 0: apng_im = new_apng apng_im.num_plays = mod_criteria.loop_count apng_im.save(out_path) if aopt_criteria.is_optimized: stdio.message(f"Optimizing APNG...") APNGOptAPI.optimize_apng(out_path, out_path, aopt_criteria) return out_path
def shift_image_sequence(image_paths: List[Path], start_frame: int) -> List[Path]: """Shift an image sequence based on the indicated start frame. Args: image_paths (List[Path]): List of paths of each image in a sequence. start_frame (int): The frame number to start the sequence at. Returns: List[Path]: List of image sequence which ordering has been shifted. """ shift_items = deque(image_paths) shift = -start_frame stdio.message(f"SHIFT {shift}") shift_items.rotate(shift) image_paths = list(shift_items) return image_paths
def modify_aimg(img_path: Path, out_path: Path, crbundle: CriteriaBundle) -> Path: orig_attribute = inspect_general(img_path, filter_on="animated") if orig_attribute is None: raise Exception("Error: cannot load image") criteria = crbundle.modify_aimg_criteria change_format = criteria.change_format(orig_attribute) if change_format or criteria.gif_must_rebuild(): stdio.message("Rebuilding im...") return rebuild_aimg(img_path, out_path, orig_attribute, crbundle) else: stdio.message("Modifying im...") if criteria.format == "GIF": return _modify_gif(img_path, out_path, orig_attribute, crbundle) elif criteria.format == "PNG": return _modify_apng(img_path, out_path, orig_attribute, crbundle)
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 modify_gif_image(cls, target_path: Path, out_full_path: Path, original_metadata: AnimatedImageMetadata, crbundle: CriteriaBundle) -> Path: """Use gifsicle to perform an array of modifications on an existing GIF image, by looping through the supplied arguments. Args: target_path (Path): Target path of the existing GIF image. out_full_path (Path): Output full path to save the GIF to. original_metadata (AnimatedImageMetadata): Original GIF metadata crbundle (CriteriaBundle): Criteria bundle object Returns: Path: Resulting path of the new modified GIF image. """ gifsicle_options = cls._mod_options_builder( original_metadata, crbundle.modify_aimg_criteria, crbundle.gif_opt_criteria) supressed_error_txts = [ "warning: too many colors, using local colormaps", "You may want to try" ] # yield {"sicle_args": sicle_args} if os_platform() not in (OS.WINDOWS, OS.LINUX): raise UnsupportedPlatformException(platform) for index, (option, description) in enumerate(gifsicle_options, start=1): # yield {"msg": f"index {index}, arg {arg}, description: {description}"} stdio.message(description) args = [ shlex.quote(str(cls.gifsicle_path)) if os_platform() == OS.LINUX else str(cls.gifsicle_path), option, shlex.quote(str(target_path)) if os_platform() == OS.LINUX else str(target_path), "--output", shlex.quote(str(out_full_path)) if os_platform() == OS.LINUX else str(out_full_path) ] # cmd = " ".join(cmdlist) # yield {"msg": f"[{index}/{total_ops}] {description}"} # yield {"cmd": cmd} cmd = " ".join(args) if ";" in cmd: raise MalformedCommandException("gifsicle") stdio.debug(f"modify_gif_image cmd -> {cmd}") result = subprocess.Popen( args if os_platform() == OS.WINDOWS else cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=os_platform() == OS.LINUX) while result.poll() is None: if result.stdout: stdout_res = result.stdout.readline().decode("utf-8") if stdout_res and not any(s in stdout_res for s in supressed_error_txts): stdio.message(stdout_res) if result.stderr: stderr_res = result.stderr.readline().decode("utf-8") if stderr_res and not any(s in stderr_res for s in supressed_error_txts): stdio.error(stderr_res) if target_path != out_full_path: target_path = out_full_path return target_path
def combine_gif_images(cls, gifragment_dir: Path, out_full_path: Path, crbundle: CriteriaBundle) -> Path: """Combine a list of static GIF images in a directory into one animated GIF image. Args: gifragment_dir (List[Path]): Path to the directory containing static GIF images. out_full_path (Path): Full path including the name of the new animated GIF image. crbundle (CriteriaBundle): Bundle of image creation criteria to adhere to. Returns: Path: Path of the created GIF. """ ROOT_PATH = str(os.getcwd()) if os.getcwd() != gifragment_dir: stdio.message( f"Changing directory from {os.getcwd()} to {gifragment_dir}") os.chdir(gifragment_dir) stdio.message("Combining frames...") supressed_error_txts = [ "warning: too many colors, using local colormaps", "You may want to try", "input images have conflicting background colors", "This means some animation frames may appear incorrect." ] # result = subprocess.run(cmd, shell=True, capture_output=True) # stdout_res = result.stdout.decode("utf-8") # stderr_res = result.stderr.decode("utf-8") # logger.message(stdout_res) # if "gifsicle.exe: warning: too many colors, using local colormaps" not in stderr_res: # logger.error(stderr_res) if os_platform() == OS.WINDOWS: args = cls._combine_cmd_builder(out_full_path, crbundle, quotes=False) stdio.debug(args) result = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) elif os_platform() == OS.LINUX: args = cls._combine_cmd_builder(out_full_path, crbundle, quotes=True) cmd = " ".join(args) stdio.debug(f"linux cmd -> {cmd}") result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) else: raise UnsupportedPlatformException(platform) while result.poll() is None: if result.stdout: stdout_res = result.stdout.readline().decode("utf-8") if stdout_res and not any(s in stdout_res for s in supressed_error_txts): stdio.message(stdout_res) if result.stderr: stderr_res = result.stderr.readline().decode("utf-8") if stderr_res and not any(s in stderr_res for s in supressed_error_txts): stdio.error(stderr_res) os.chdir(ROOT_PATH) 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 _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
def optimize_apng( cls, target_path: Path, out_full_path: Path, apngopt_criteria: APNGOptimizationCriteria, ) -> Path: """Use apngopt to optimize an APNG. Returns the output path Args: aopt_args (List[Tuple[str, str]]): apngopt arguments target_path (Path): Target path of the animated image to be optimized out_full_path (Path): Destination output path to save the optimized image to total_ops (int, optional): UNUSED. Defaults to 0. shift_index (int, optional): UNUSED. Defaults to 0. Returns: Path: [description] """ aopt_args = cls._opt_args_builder(apngopt_criteria) aopt_dir = filehandler.mk_cache_dir(prefix_name="apngopt_dir") filename = target_path.name aopt_temp_path = aopt_dir.joinpath(filename) # logger.message(f"COPY FROM {target_path} TO {aopt_temp_path}") target_path = shutil.copy(target_path, aopt_temp_path, follow_symlinks=False) cwd = os.getcwd() # common_path = os.path.commonpath([opt_exec_path, target_path]) target_rel_path = Path(os.path.relpath(target_path, cwd)) # out_rel_path = Path(os.path.relpath(out_full_path, cwd)) newline_check = ["\r\n", "\n"] for index, (arg, description) in enumerate(aopt_args, start=1): stdio.message( f"index {index}, arg {arg}, description: {description}") cmdlist = [ str(cls.opt_exec_path), arg, str(target_rel_path), str(target_rel_path), ] # raise Exception(cmdlist, out_full_path) cmd = " ".join(cmdlist) # result = subprocess.check_output(cmd, shell=True) stdio.message("Performing optimization...") stdio.debug(cmd) stdio.debug(cmdlist) process = subprocess.Popen(cmdlist, stdout=subprocess.PIPE, stderr=subprocess.PIPE) index = 0 while process.poll() is None: # if process.poll() is not None: # break # print('process-polling...') if process.stdout: # print(process.stdout) stdout_res = process.stdout.readline().decode("utf-8") # print('1') stdio.debug(stdout_res.capitalize()) # print('2') if stdout_res and stdout_res not in newline_check and "saving" in stdout_res: # print('3') out_words = " ".join( stdout_res.translate({ ord("\r"): None, ord("\n"): None }).capitalize().split(" ")[3:])[:-1] out_msg = f"Optimizing frame {out_words}..." stdio.message(out_msg) index += 1 # print('4') # print('5') elif process.stderr: # print('6') stderr_res = process.stderr.readline().decode("utf-8") # print('7') # if stderr_res and not any(s in stderr_res for s in supressed_error_txts): if stderr_res: # print('8') stdio.error(stderr_res) # print('9') # process.communicate('\n') # if target_path != out_full_path: # target_path = out_full_path out_full_path = shutil.move(target_path, out_full_path) shutil.rmtree(aopt_dir) return out_full_path
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 ping_c_interface(self): pass msg = c_ping() stdio.message(msg)
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