Loading...
Error

Программное обеспечение (скрипты на Python) для извлечения обложек из электронных книг .fb и .epub .pdf форматов

Ответить на тему

 | 

 
Автор Сообщение

kd2600

Здравствуйте, решил поделиться своими наработками, которые помогают мне в обработке .pdf .fb2 .epub файлов, а именно автоматического извлечению обложек, а так же созданию постеров для раздач, может кому будет полезно.

Использую Python 3.8 (как я понимаю, можно использовать почти всю линейку версии 3.0, не 2.0) установленный по-умолчанию (с прописью интерпритатора в переменную PATH) в систему Win10x64. Написано с помощью AI помощника, подправлено мной. У меня, на встреченных мною файлах, работает великолепно, если у кого-то встретится другой "хитрый файл", то скормите в тот же DeepSeek с уточнением проблемы - подправит.

ПОДГОТОВКА
Установка в систему интерпритатора Python:
Для Windows: скачать с официального сайта, далее установить следуя инструкциям (обязятельно поставить галочку в поле прописи интепритатора в переменную PATH)
Для Linux/Mac: по-умолчанию установлено в системе, если нет, то уже следуйте инструкциям вашего дистрибутива.

Установка в интерпритатор Python всех используемых в данной программах библиотек:
Вводить в терминале (коммандной строке) Win+R, в окне набрать cmd и нажать Enter

Код:

pip install pymupdf pillow


САМИ ПРОГРАММЫ:
fb2cover - используется для извлечения обложек (слаживает по-порядку в ту же папку где лежат файлы)
Создать файл в любом текстовом редакторе в кодировке UTF-8, сохранить с расширением .py , сам .py файл (к примеру fb2cover.py) положить в любую папку, которая присутствует в системной переменной PATH, запустить просто набрав имя в терминале (командной строке) - будут обработаны все найденные в текущей папке из которой запущен скрипт файлы, извлеченные обложки будут помещены в ту же папку откуда был запущен скрипт.

Код:

import os
import sys
import zipfile
from xml.etree import ElementTree as ET
import fitz  # PyMuPDF

def extract_fb2_cover(fb2_file, output_file):
    try:
        tree = ET.parse(fb2_file)
        root = tree.getroot()

        # Находим тег <coverpage>
        coverpage = root.find('.//{http://www.gribuser.ru/xml/fictionbook/2.0}coverpage')
        if coverpage is None:
            print(f"Обложка не найдена в файле {fb2_file}")
            return

        # Находим тег <image> внутри <coverpage>
        image = coverpage.find('.//{http://www.gribuser.ru/xml/fictionbook/2.0}image')
        if image is None:
            print(f"Изображение обложки не найдено в файле {fb2_file}")
            return

        # Извлекаем данные изображения
        image_href = image.attrib.get('{http://www.w3.org/1999/xlink}href', '')
        if not image_href.startswith('#'):
            print(f"Некорректная ссылка на изображение в файле {fb2_file}")
            return

        # Находим элемент <binary> с соответствующим id
        binary = root.find(f'.//{{http://www.gribuser.ru/xml/fictionbook/2.0}}binary[@id="{image_href[1:]}"]')
        if binary is None:
            print(f"Бинарные данные обложки не найдены в файле {fb2_file}")
            return

        # Декодируем и сохраняем изображение
        image_data = binary.text
        if image_data is None:
            print(f"Нет данных изображения в файле {fb2_file}")
            return

        import base64
        image_bytes = base64.b64decode(image_data)
        with open(output_file, 'wb') as img_file:
            img_file.write(image_bytes)
        print(f"Обложка сохранена как {output_file}")

    except Exception as e:
        print(f"Ошибка при обработке файла {fb2_file}: {e}")

def extract_epub_cover(epub_file, output_file):
    try:
        # Открываем EPUB как ZIP-архив
        with zipfile.ZipFile(epub_file, 'r') as z:
            # Ищем файл с метаданными
            container = z.read('META-INF/container.xml')
            root = ET.fromstring(container)
            rootfile = root.find('.//{urn:oasis:names:tc:opendocument:xmlns:container}rootfile')
            if rootfile is None:
                print(f"Файл с метаданными не найден в {epub_file}")
                return

            # Получаем путь к основному файлу с содержимым
            content_path = rootfile.attrib['full-path']
            content = z.read(content_path)
            content_root = ET.fromstring(content)

            # Ищем обложку в метаданных
            meta_cover = content_root.find('.//{http://www.idpf.org/2007/opf}meta[@name="cover"]')
            if meta_cover is None:
                print(f"Обложка не найдена в метаданных файла {epub_file}")
                return

            cover_id = meta_cover.attrib['content']
            manifest = content_root.find('.//{http://www.idpf.org/2007/opf}manifest')
            if manifest is None:
                print(f"Манифест не найден в файле {epub_file}")
                return

            # Ищем элемент с обложкой в манифесте
            cover_item = manifest.find(f'.//{{http://www.idpf.org/2007/opf}}item[@id="{cover_id}"]')
            if cover_item is None:
                print(f"Элемент обложки не найден в манифесте файла {epub_file}")
                return

            cover_href = cover_item.attrib['href']
            # Нормализуем путь к обложке
            cover_href = cover_href.replace('\\', '/')  # Заменяем обратные слэши на прямые
            cover_path = os.path.normpath(os.path.join(os.path.dirname(content_path), cover_href))
            cover_path = cover_path.replace('\\', '/')  # Нормализуем путь для ZIP-архива

            # Пытаемся прочитать обложку
            try:
                cover_data = z.read(cover_path)
            except KeyError:
                print(f"Обложка не найдена по пути {cover_path} в файле {epub_file}")
                return

            # Сохраняем обложку
            with open(output_file, 'wb') as img_file:
                img_file.write(cover_data)
            print(f"Обложка сохранена как {output_file}")

    except Exception as e:
        print(f"Ошибка при обработке файла {epub_file}: {e}")

def extract_pdf_cover(pdf_file, output_file):
    try:
        # Открываем PDF-файл
        doc = fitz.open(pdf_file)
        # Получаем первую страницу (обычно это обложка)
        page = doc.load_page(0)
        # Получаем изображения со страницы
        pix = page.get_pixmap()
        # Сохраняем изображение
        pix.save(output_file)
        print(f"Обложка сохранена как {output_file}")
    except Exception as e:
        print(f"Ошибка при обработке файла {pdf_file}: {e}")

def main():
    if len(sys.argv) == 1:
        # Поиск всех .fb2, .epub и .pdf файлов в текущей папке
        fb2_files = [f for f in os.listdir() if f.endswith('.fb2')]
        epub_files = [f for f in os.listdir() if f.endswith('.epub')]
        pdf_files = [f for f in os.listdir() if f.endswith('.pdf')]
        all_files = fb2_files + epub_files + pdf_files

        if not all_files:
            print("Не найдено .fb2, .epub или .pdf файлов в текущей папке.")
            return

        # Извлечение обложек
        for i, file in enumerate(all_files, start=1):
            output_file = f"cover_{i:02d}.jpg"
            if file.endswith('.fb2'):
                extract_fb2_cover(file, output_file)
            elif file.endswith('.epub'):
                extract_epub_cover(file, output_file)
            elif file.endswith('.pdf'):
                extract_pdf_cover(file, output_file)

    elif len(sys.argv) == 3:
        # Извлечение обложки из указанного файла с заданным именем
        input_file = sys.argv[1]
        output_file = sys.argv[2]
        if input_file.endswith('.fb2'):
            extract_fb2_cover(input_file, output_file)
        elif input_file.endswith('.epub'):
            extract_epub_cover(input_file, output_file)
        elif input_file.endswith('.pdf'):
            extract_pdf_cover(input_file, output_file)
        else:
            print("Первый аргумент должен быть .fb2, .epub или .pdf файлом.")
            return

    else:
        print("fb2cover - извлечение обложек из всех .fb2, .epub и .pdf файлов в текущей папке")
        print("\nИспользование:")
        print("fb2cover <имя_файла.fb2|имя_файла.epub|имя_файла.pdf> <имя_обложки.jpg> - извлечение обложки из указанного файла")

if __name__ == "__main__":
    main()
merger - объединение всех найденных в папке файлов обложек в один постер в формате мозаики.
Создать файл в любом текстовом редакторе в кодировке UTF-8, сохранить с расширением .py , сам .py файл (к примеру fb2cover.py) положить в любую папку, которая присутствует в системной переменной PATH, запустить просто набрав имя в терминале (командной строке) - будут обработаны все найденные в текущей папке из которой запущен скрипт файлы, извлеченные обложки будут помещены в ту же папку откуда был запущен скрипт.

Использование чуть по-сложнее, но и задача с которой придётся столкнуться более гибкая, нежели извлечение обложек из .pdf , .fb2 и .epub файлов.
запускать из командной строки с параметрами"

Код:

merger.py --images_per_row 2 --spacing 5 --border 10
Где --images_per_row 2 (сколько изображений сложить в строке) , --spacing 5 (задать отступ вокруг каждого изображения 5 пикселей), --border 10 (задать рамку вокруг созданного постера в 10 пикселей)
Как и первый скрипт: в любом текстовом редакторе, который умеет сохранять в UTF-8 кодировке создать текстовывй файл с исходником срипта, сохранить с расширением (к примеру merger.py), сохраненный файл скрипта положить в папку, присутствующей в любой папке из переменной PATH.
запустить в терминале (командной строке) в той папке, где находятся файлы подлежащие в склейку в постер, на выходе получите собранный из найденных изображений постер, который уже можно использовать для прикрепления в раздачу.

Код:

from PIL import Image, ImageOps
import os
import argparse
import sys

def combine_images(image_paths, output_path, images_per_row=3, spacing=5, border=5):
    # Проверка на минимальное количество файлов
    if len(image_paths) < 2:
        print("Ошибка: Для объединения необходимо не менее 2 файлов.")
        return
   
    # Загружаем изображения
    images = [Image.open(image_path) for image_path in image_paths]
   
    # Определяем максимальные ширину и высоту среди всех изображений
    max_width = max(img.width for img in images)
    max_height = max(img.height for img in images)
   
    # Приводим все изображения к максимальному размеру
    resized_images = [ImageOps.fit(img, (max_width, max_height)) for img in images]
   
    # Определяем количество строк и столбцов
    num_images = len(resized_images)
    num_rows = (num_images + images_per_row - 1) // images_per_row
   
    # Рассчитываем размеры итогового изображения с учетом отступов и рамки
    combined_width = max_width * images_per_row + spacing * (images_per_row - 1) + 2 * border
    combined_height = max_height * num_rows + spacing * (num_rows - 1) + 2 * border
    combined_image = Image.new('RGB', (combined_width, combined_height), color=(255, 255, 255))  # Белый фон
   
    # Вставляем изображения в новое полотно с учетом отступов и рамки
    for i, img in enumerate(resized_images):
        row = i // images_per_row
        col = i % images_per_row
        x = border + col * (max_width + spacing)
        y = border + row * (max_height + spacing)
        combined_image.paste(img, (x, y))
   
    # Сохраняем результат
    combined_image.save(output_path)
    print(f"Изображения объединены и сохранены в {output_image_path}")

# Парсинг аргументов командной строки
parser = argparse.ArgumentParser(description="Объединение изображений в мозаику.", add_help=False)
parser.add_argument('--images_per_row', type=int, default=3, help="Количество изображений в одном ряду (по умолчанию: 3).")
parser.add_argument('--spacing', type=int, default=5, help="Отступ между изображениями в пикселях (по умолчанию: 5).")
parser.add_argument('--border', type=int, default=5, help="Рамка вокруг мозаики в пикселях (по умолчанию: 5).")
parser.add_argument('--h', '-h', action='store_true', help="Показать справку по использованию программы.")

# Обработка ключа --h или -h
if '--h' in sys.argv or '-h' in sys.argv:
    parser.print_help()
    sys.exit(0)

args = parser.parse_args()

# Получаем текущую папку, в которой запущена программа
image_folder = os.getcwd()

# Собираем все изображения с расширением .jpg, .png, .jpeg из текущей папки
image_files = [os.path.join(image_folder, f) for f in os.listdir(image_folder) if f.lower().endswith(('.jpg', '.png', '.jpeg'))]

# Путь для сохранения итогового изображения
output_image_path = os.path.join(image_folder, 'combined_image.jpg')

# Объединяем изображения с учетом параметров командной строки
combine_images(image_files, output_image_path, images_per_row=args.images_per_row, spacing=args.spacing, border=args.border)
К сожалению прикрепить архив с откомпилированными в исполняемые файлами мне не дало, поэтому прикрепляю просто исходники на Python.

КОМПИЛЯЦИЯ В ИСПОЛНЯЕМЫЙ ФАЙЛ (опционально)
Компиляция в исполняемый файл, если кому хочется получить независимый от интерпретатора запускаемый файл:
1. убеждаемся, что скрипт запускается и отрабатывает корректно!
2. аналогично тому как это сделана выше, устанавливаем в интерпритатор Python систему компиляции Pyinstaller

Код:

pip install pyinstaller
3. в командной строке собираем программы в исполняемый файл

Код:

pyinstaller -F <имя скрипта.py>
пробегутся строчки сборки и на выходе в появившейся папке dist получите исполняемый файл, не зависящий от установленного или нет в системе компилятора Python, большой и тормозной, но для таких мелких задач, вполне себе пойдёт.

К сожалению три дня ковыряния по "переписыванию" данного скрипта с языка Python, на более "низкоуровневый" Си (C99) или Pascal (Freepascal) привел с "хождению по-кругу" в ошибках, логике и вообще.
У меня нет столько времени для разбора косяков ИИ ассистента (самый лучший результат выдал DeepSeek, остальные просто позатыкались на второй итерации парсинга ошибок), а потому оставлю эту задачу на потом, а лучше кому-нибудь другому. Остальным, надеюсь, будет полезными эти две утилитки, мне очень помогает.

28.02.2025 - добавлена обработка .pdf файлов, с .djvu пока не все так гладко, поэтому пока отложил в сторону.
Показать сообщения:    
Ответить на тему