コード例 #1
0
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
コード例 #2
0
    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)
コード例 #3
0
    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
コード例 #4
0
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
コード例 #5
0
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
コード例 #6
0
    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
コード例 #7
0
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
コード例 #8
0
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
コード例 #9
0
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