예제 #1
0
    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
예제 #2
0
    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
예제 #3
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
예제 #4
0
    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
예제 #5
0
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
예제 #6
0
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
예제 #7
0
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
예제 #8
0
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)
예제 #9
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
예제 #10
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
예제 #11
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
예제 #12
0
    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
예제 #13
0
    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
예제 #14
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
예제 #15
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
예제 #16
0
    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
예제 #17
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
예제 #18
0
 def ping_c_interface(self):
     pass
     msg = c_ping()
     stdio.message(msg)
예제 #19
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