Этот пост носит малоприкладной характер и будет интересен исключительно в виде заметки о том, как ещё можно обойти работу в прикладном ПО, потратив час на автоматизацию очередной рутинной задачи.
Помнится, где-то здесь я уже ковырялся в дебрях автоматизации распечатки увесистой пачки jpg. Но тогда я разрабатывал скрипт на PowerShell (тег). Здесь же всю работу я осуществил на Python.
К сабжу привела ситуация, когда в сетевой папке накопилась целая куча сканированных и импортированных из разных приложений чеков. Часть из них представляла собой парный набор, которые было бы правильнее объединить в один документ (желательно) формата А4 для более упорядоченного хранения и последующей распечатки. В контексте этого поста конечный документ я назвал чек-листом (не путать с контрольным листом).
Спустя пару часов файлового аудита, обработки графической составляющей изображений для повышения читаемости и приведения в порядок имён файлов (об этом чуть позже) — я приступил к написанию основного скрипта.
Логика творения такова: скрипту в качестве аргументов должны быть переданы два изображения формата jpg и имя выходного файла в формате pdf. Под капотом должна срабатывать магия с формированием листа А4, разделённого на две колонки. В каждой колонке должно располагаться одно изображение. Важным моментом здесь является масштабирование изображений, чтобы избежать последующего наложения. Завершается весь этот праздник сохранением итогового документа.
Для работы с аргументами я обратился к модулю argparse. Про рутину написания параметров можно забыть, просто указав всё, что нужно в соответствующих директивах:
parser = argparse.ArgumentParser(description=f"{title} | {version}")
parser.add_argument("-i", "--images", required=True, nargs='+', help="Список путей к файлам изображений, которые нужно объединить.")
parser.add_argument("-o", "--output", required=True, help="Имя выходного файла для сохранения страницы (с расширением .jpg или .pdf).")
args = parser.parse_args()
# Вызов функции создания страницы #
create_page(args.images, args.output)
Из этого фрагмента видно, что я объявил для скрипта два параметра:
i/--images и -o/--output
Они оба являются обязательными, оба имеют описание для вывода помощи. Параметр images принимает множественные значения.
Параметр help (-h) argparse формирует сам, основываясь на описании добавленных параметров. Вывод помощи выглядит так:
usage: createchecklist.py [-h] -i IMAGES [IMAGES ...] -o OUTPUT
Скрипт для формирования чек-листов | 1.0.1.2
options:
-h, --help show this help message and exit
-i IMAGES [IMAGES ...], --images IMAGES [IMAGES ...]
Список путей к файлам изображений, которые нужно объединить.
-o OUTPUT, --output OUTPUT
Имя выходного файла для сохранения страницы (с расширением .jpg или .pdf).
Если вызвать скрипт без параметров — в терминал упадёт краткая справка и соответствующая ошибка:
usage: createchecklist.py [-h] -i IMAGES [IMAGES ...] -o OUTPUT createchecklist.py: error: the following arguments are required: -i/--images, -o/--output
Ну и, наконец, сам скрипт createchecklist.py:
from PIL import Image
import argparse
import os
import sys
# Заголовок скрипта #
title = "Скрипт для формирования чек-листов"
# Версия скрипта #
version = "1.0.1.2"
# Размеры страницы A4 в пикселях (при 300 DPI) #
PAGE_WIDTH = 2480 # 21 см
PAGE_HEIGHT = 3508 # 29.7 см
MARGIN = 118 # 1 см в пикселях
# Ширина колонки с учетом отступов #
COLUMN_WIDTH = (PAGE_WIDTH - 3 * MARGIN) // 2
# Максимальная высота для изображения #
MAX_HEIGHT = PAGE_HEIGHT - 2 * MARGIN
# Функция создания страницы #
def create_page(image_files, output_file):
# Проверка существования файлов изображений #
for file in image_files:
if not os.path.isfile(file):
print(f"Файл не найден: {file}")
sys.exit(1)
# Создаем новое изображение для страницы #
page = Image.new("RGB", (PAGE_WIDTH, PAGE_HEIGHT), "white")
images = []
for file in image_files:
image = Image.open(file)
images.append(image)
# Переменные для размещения изображений #
y_offset = MARGIN
max_col_height = 0
for index, image in enumerate(images):
image.thumbnail((COLUMN_WIDTH, MAX_HEIGHT))
# Определяем позицию для размещения изображения #
# Левая колонка #
if index % 2 == 0:
x_offset = MARGIN
# Правая колонка #
# Просчет положения правой колонки #
else:
x_offset = PAGE_WIDTH - MARGIN - image.width
# Размещение изображения на странице
page.paste(image, (x_offset, y_offset))
# Обновление максимальной высоты для колонки
# Если это левое изображение #
if index % 2 == 0:
max_col_height = max(max_col_height, image.height)
else:
# Если это правое изображение, переходим на новую строку #
y_offset += max_col_height
max_col_height = 0
# Если количество изображений нечётное, нам нужно обработать последнее изображение #
if len(images) % 2 != 0:
y_offset += max_col_height
# Сохраняем итоговое изображение в указанном формате #
file_extension = os.path.splitext(output_file)[-1].lower()
if file_extension in ('.jpg', '.jpeg'):
page.save(output_file, "JPEG")
elif file_extension == '.pdf':
page.save(output_file, "PDF")
else:
print(f"Неподдерживаемый формат файла: {file_extension}. Используйте .jpg или .pdf.")
sys.exit(1)
# Основная функция #
def main():
# Парсинг аргументов #
parser = argparse.ArgumentParser(description=f"{title} | {version}")
parser.add_argument("-i", "--images", required=True, nargs='+', help="Список путей к файлам изображений, которые нужно объединить.")
parser.add_argument("-o", "--output", required=True, help="Имя выходного файла для сохранения страницы (с расширением .jpg или .pdf).")
args = parser.parse_args()
# Вызов функции создания страницы #
create_page(args.images, args.output)
# Точка входа #
if __name__ == "__main__":
main()
В основной функции скрипта create_page заложен принцип формирования чек-листа с учётом размещения не более двух изображений на листе, учитывая масштабирование и дальнейшее расположение.
Дальше хотелось бы упомянуть принцип именования файлов и автоматизацию запуска. Для автоматизации я написал ещё один скрипт и назвал его batch.py.
Принцип именования изображений был организован так:
Общее название - Номер чека (максимальное значение 2) Например, "Закупка от 2024.12.01 - 01"
За разделитель я взял символ «-». Теперь оставалось разместить в папке пачку изображений и запустить скрипт batch.py:
import os
import subprocess
from collections import defaultdict
# Функция поиска jpg файлов #
def find_jpg_files():
return [f for f in os.listdir('.') if f.lower().endswith('.jpg')]
# Группировка файлов #
def group_files_by_name(files):
groups = defaultdict(list)
for file in files:
# Имя до символа "-" #
base_name = file.split('-')[0].strip()
groups[base_name].append(file)
# Оставить только группы с 2 и более файлами #
return {k: v for k, v in groups.items() if len(v) > 1}
# Основная функция обработки файлов #
def process_files():
jpg_files = find_jpg_files()
grouped_files = group_files_by_name(jpg_files)
# Если есть группы файлов >=2, обрабатываем их #
if grouped_files:
for base_name, files in grouped_files.items():
output_file = f"{base_name}.pdf"
# Берем только первые два файла #
input_files = " ".join(files[:2])
# Вызов скрипта для создания страницы #
subprocess.run(['python', 'createchecklist.py', '-i', *files[:2], '-o', output_file])
# Если групп нет, но есть хотя бы один jpg файл, обрабатываем его #
elif jpg_files:
# Берем первый файл в списке #
single_file = jpg_files[0]
# Имя выходного файла #
output_file = f"{single_file.split('.')[0]}.pdf"
# Вызов скрипта для создания страницы #
subprocess.run(['python', 'createchecklist.py', '-i', single_file, '-o', output_file])
else:
print("Не найдено файлов .jpg в текущей директории.")
# Точка входа #
if __name__ == "__main__":
process_files()
В итоговом варианте получаются файлы pdf именованные по первой части имени парных файлов до символа «-». Всё, что расположено в имени после «-» — отбрасывается. Основываясь на примере, приведённом выше, имя файла будет таковым: «Закупка от 2024.12.01.pdf». В самом же файле будут содержаться два чека, расположенные на одном листе в две колонки.