Github
:octocat: https://github.com/ikota3/image_utilities
In order to make self-catering comfortable, we have created a tool to convert the images stored in each directory to PDF. Also, subdirectories are created recursively.
The operation was confirmed on Windows 10.
fireimg2pdf$ git clone https://github.com/ikota3/image_utilities
$ cd image_utilities
$ pip install -r requirements.txt
$ python src/images_to_pdf.py convert -i "path/to/input" -o "path/to/output" -e "jpg,jpeg,png"
This time, I thought it would be difficult to make everything from scratch to make a CLI tool, so I used the library fire that was talked about before. ..
In fact, it was very easy to use.
First, create a skeleton so that you can hit the command and receive the input value. This time, create a class and call it with fire. In addition to classes, fire can call functions, modules, objects, and much more. See the official documentation for details. https://github.com/google/python-fire/blob/master/docs/guide.md
images_to_pdf.py
import fire
class PDFConverter(object):
    """Class for convert images to pdf."""
    def __init__(
            self,
            input_dir: str = "",
            output_dir: str = "",
            extensions: Union[str, Tuple[str]] = None,
            force_write: bool = False,
            yes: bool = False
    ):
        """Initialize
        Args:
            input_dir (str): Input directory. Defaults to "".
            output_dir (str): Output directory. Defaults to "".
            extensions (Union[str, Tuple[str]]): Extensions. Defaults to None.
            force_write (bool): Flag for overwrite the converted pdf. Defaults to False.
            yes (bool): Flag for asking to execute or not. Defaults to False.
        """
        self.input_dir: str = input_dir
        self.output_dir: str = output_dir
        if not extensions:
            extensions = ('jpg', 'png')
        self.extensions: Tuple[str] = extensions
        self.force_write: bool = force_write
        self.yes: bool = yes
	def convert(self):
		print("Hello World!")
if __name__ == '__main__':
    fire.Fire(PDFConverter)
For the time being, the skeleton is completed.
If you type the command in this state, it should output Hello World!.
$ python src/images_to_pdf.py convert
Hello World!
Also, other parameters such as ʻinput_dir =" "` have default values, but if you do not pass a value on the command side without setting this, an error on the fire side will occur.
To pass the value, just add a hyphen before the prefix of the argument set in __init__ and then write the value you want to pass.
The method of passing the following commands is the same, although there are differences in the writing method.
$ # self.input_dir example
$ python src/images_to_pdf.py convert -i "path/to/input"
$ python src/images_to_pdf.py convert -i="path/to/input"
$ python src/images_to_pdf.py convert --input_dir "path/to/input"
$ python src/images_to_pdf.py convert --input_dir="path/to/input"
Also, I was confused when I tried to pass the list as a stumbling block.
$ # self.Examples of extensions
$ python src/images_to_pdf.py convert -e jpg,png # OK
$ python src/images_to_pdf.py convert -e "jpg,png" # OK
$ python src/images_to_pdf.py convert -e "jpg, png" # OK
$ python src/images_to_pdf.py convert -e jpg, png # NG
Before performing PDF conversion processing, type check by ʻis instance ()` and check whether the specified path exists, etc. are performed.
images_to_pdf.py
def _input_is_valid(self) -> bool:
    """Validator for input.
    Returns:
        bool: True if is valid, False otherwise.
    """
    is_valid = True
    # Check input_dir
    if not isinstance(self.input_dir, str) or \
            not os.path.isdir(self.input_dir):
        print('[ERROR] You must type a valid directory for input directory.')
        is_valid = False
    # Check output_dir
    if not isinstance(self.output_dir, str) or \
            not os.path.isdir(self.output_dir):
        print('[ERROR] You must type a valid directory for output directory.')
        is_valid = False
    # Check extensions
    if not isinstance(self.extensions, tuple) and \
            not isinstance(self.extensions, str):
        print('[ERROR] You must type at least one extension.')
        is_valid = False
    # Check force_write
    if not isinstance(self.force_write, bool):
        print('[ERROR] You must just type -f flag. No need to type a parameter.')
        is_valid = False
    # Check yes
    if not isinstance(self.yes, bool):
        print('[ERROR] You must just type -y flag. No need to type a parameter.')
        is_valid = False
    return is_valid
I am using something called ʻos.walk () to scan the directory from the received ʻinput_dir path.
https://docs.python.org/ja/3/library/os.html?highlight=os walk#os.walk
The directory is scanned as shown below, images are collected, and PDF is created.
images_to_pdf.py
def convert(self):
    #To the prefix of the extension.Add
    extensions: Union[str | Tuple[str]] = None
    if isinstance(self.extensions, tuple):
        extensions = []
        for extension in self.extensions:
            extensions.append(f'.{extension}')
        extensions = tuple(extensions)
    elif isinstance(self.extensions, str):
        extensions = tuple([f'.{self.extensions}'])
	#Scan directories and convert images in each directory to PDF
    for current_dir, dirs, files in os.walk(self.input_dir):
        print(f'[INFO] Watching {current_dir}.')
        #A list that stores the path where the target image is located
        images = []
        #files is current_List of files in dir
        #Sorting will be out of order if the number of digits is different(https://github.com/ikota3/image_utilities#note)
        #Therefore, I prepared a function to make it as expected.(See below)
        for filename in sorted(files, key=natural_keys):
            if filename.endswith(extensions):
                path = os.path.join(current_dir, filename)
                images.append(path)
		#When there is no image as a result of scanning
        if not images:
            print(
                f'[INFO] There are no {", ".join(self.extensions).upper()} files at {current_dir}.'
            )
            continue
        pdf_filename = os.path.join(
            self.output_dir, f'{os.path.basename(current_dir)}.pdf'
        )
		# -If there is an f parameter, forcibly overwrite even if there is a file
        if self.force_write:
            with open(pdf_filename, 'wb') as f:
                f.write(img2pdf.convert(images))
            print(f'[INFO] Created {pdf_filename}!')
        else:
            if os.path.exists(pdf_filename):
                print(f'[ERROR] {pdf_filename} already exist!')
                continue
            with open(pdf_filename, 'wb') as f:
                f.write(img2pdf.convert(images))
            print(f'[INFO] Created {pdf_filename}!')
When collecting the images in the directory, there were times when the sort was wrong and the order was not what I expected, so I prepared a function. Also, I made it by referring to the following link. https://stackoverflow.com/questions/5967500/how-to-correctly-sort-a-string-with-a-number-inside
sort_key.py
import re
from typing import Union, List
def atoi(text: str) -> Union[int, str]:
    """Convert ascii to integer.
    Args:
        text (str): string.
    Returns:
        Union[int, str]: integer if number, string otherwise.
    """
    return int(text) if text.isdigit() else text
def natural_keys(text: str) -> Union[List[int], List[str]]:
    """Key for natural sorting
    Args:
        text (str): string
    Returns:
        Union[List[int], List[str]]: A list of mixed integer and strings.
    """
    return [atoi(c) for c in re.split(r'(\d+)', text)]
I used to make CLI tools without using a library, but it was easier to implement than fire!
Would you like to make a CLI tool?
Recommended Posts