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.
fire
img2pdf
$ 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