Loading...
Error

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

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

 | 

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

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 пока не все так гладко, поэтому пока отложил в сторону.

kd2600

12.04.2025 Добавлен функционал обработки обложек извлеченных из .djvu файлов.

Для расширения функционала использован пакет DjVuLibre (стандартный пакет утилит обработки .djvu файлов).
Для того чтобы программа заработала нужно скачать и установить пакет DjVuLibre, а так же добавить путь куда установлено данное приложение в системную переменную %PATH%

Общий порядок действий для этого: в Windows 10 в левом нижнем углу на том месте где когда-то была кнопка Пуск, а сейчас "квадратики логотипа Windows 10" вызывающе меню, нажать правой клавишей мышки и на появившемся меню нажать Система, далее в открывшемся окне справа сверху выбрать Допольные параметры системы, далее на вкладке Дополнительно нажать на Переменные среды, в открывшемся меню в окне Системные переменные выбрать переменную Path и нажать кнопку Изменить, нажать кнопку Создать и внести туда (для x64 системы):
C:\Program Files (x86)\DjVuLibre
Сохранить и всё закрыть.

Всё, данная библиотека с утилитами прописана в системные пути и может быть вызвана из любого места вашей операционной системы.

Саму обновленную утилиту модно использовать так же как и прежде - просто запускать в папке с выложенными файлами подлежащими обработке (извлечению обложек). Данная версия обрабатывает .fb2, .epub, .pdf и .djvu файлов.

Для пользователей linux-подобных операционных систем: если вы осилили работу в данных системах - более чем уверен, что такое подробное разжевывание как пользоваться данной утилиткой думаю, что будет слишком излишним.

Код:

import os
import sys
import zipfile
import subprocess
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 = root.find('.//{http://www.gribuser.ru/xml/fictionbook/2.0}coverpage')
        if coverpage is None:
            print(f"Обложка не найдена в файле {fb2_file}")
            return

        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 = 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:
        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('\\', '/')

            try:
                cover_data = z.read(cover_path)
                with open(output_file, 'wb') as img_file:
                    img_file.write(cover_data)
                print(f"Обложка сохранена как {output_file}")
            except KeyError:
                print(f"Обложка не найдена по пути {cover_path} в файле {epub_file}")

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

def extract_pdf_cover(pdf_file, output_file):
    try:
        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 extract_djvu_cover(djvu_file, output_file):
    try:
        # Создаем временный TIFF файл
        temp_tiff = output_file + '.tiff'
       
        # Используем ddjvu для извлечения первой страницы
        subprocess.run(['ddjvu', '--format=tiff', '--page=1', djvu_file, temp_tiff], check=True)
       
        # Конвертируем TIFF в JPG
        from PIL import Image
        with Image.open(temp_tiff) as img:
            img.save(output_file, 'JPEG')
       
        # Удаляем временный TIFF файл
        os.remove(temp_tiff)
       
        print(f"Обложка сохранена как {output_file}")
    except subprocess.CalledProcessError as e:
        print(f"Ошибка при обработке DJVU файла {djvu_file}: {e}")
    except Exception as e:
        print(f"Ошибка при конвертации обложки {djvu_file}: {e}")
    finally:
        if os.path.exists(temp_tiff):
            try:
                os.remove(temp_tiff)
            except:
                pass

def main():
    if len(sys.argv) == 1:
        # Поиск всех поддерживаемых файлов в текущей папке
        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')]
        djvu_files = [f for f in os.listdir() if f.endswith('.djvu')]
        all_files = fb2_files + epub_files + pdf_files + djvu_files

        if not all_files:
            print("Не найдено файлов поддерживаемых форматов (.fb2, .epub, .pdf, .djvu)")
            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 file.endswith('.djvu'):
                extract_djvu_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)
        elif input_file.endswith('.djvu'):
            extract_djvu_cover(input_file, output_file)
        else:
            print("Поддерживаются только файлы .fb2, .epub, .pdf и .djvu")
            return

    else:
        print("fb2cover - извлечение обложек из файлов поддерживаемых форматов")
        print("\nИспользование:")
        print("fb2cover - извлечение обложек из всех файлов в текущей папке")
        print("fb2cover <input_file> <output_file> - извлечение обложки из указанного файла")

if __name__ == "__main__":
    main()
Всем красивых и информативных обложек для ваших раздач ) ну и с Днём космонавтики! )))

kd2600

22.04.2026 В функцию поиска файлов в папке добавлен поиск .PDF .DJV файлов. В связи с расширением перечня обрабатываемых форматов, сама программа переименована в Extractorcovers или просто excover

Код:

import os
import sys
import zipfile
import subprocess
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 = root.find('.//{http://www.gribuser.ru/xml/fictionbook/2.0}coverpage')
        if coverpage is None:
            print(f"Обложка не найдена в файле {fb2_file}")
            return

        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 = 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:
        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('\\', '/')

            try:
                cover_data = z.read(cover_path)
                with open(output_file, 'wb') as img_file:
                    img_file.write(cover_data)
                print(f"Обложка сохранена как {output_file}")
            except KeyError:
                print(f"Обложка не найдена по пути {cover_path} в файле {epub_file}")

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

def extract_pdf_cover(pdf_file, output_file):
    try:
        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 extract_djvu_cover(djvu_file, output_file):
    try:
        # Создаем временный TIFF файл
        temp_tiff = output_file + '.tiff'
       
        # Используем ddjvu для извлечения первой страницы
        subprocess.run(['ddjvu', '--format=tiff', '--page=1', djvu_file, temp_tiff], check=True)
       
        # Конвертируем TIFF в JPG
        from PIL import Image
        with Image.open(temp_tiff) as img:
            img.save(output_file, 'JPEG')
       
        # Удаляем временный TIFF файл
        os.remove(temp_tiff)
       
        print(f"Обложка сохранена как {output_file}")
    except subprocess.CalledProcessError as e:
        print(f"Ошибка при обработке DJVU файла {djvu_file}: {e}")
    except Exception as e:
        print(f"Ошибка при конвертации обложки {djvu_file}: {e}")
    finally:
        if os.path.exists(temp_tiff):
            try:
                os.remove(temp_tiff)
            except:
                pass

def main():
    if len(sys.argv) == 1:
        # Поиск всех поддерживаемых файлов в текущей папке
        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')]
        pdf_files2 = [f for f in os.listdir() if f.endswith('.PDF')]
        djvu_files = [f for f in os.listdir() if f.endswith('.djvu')]
        djvu_files2 = [f for f in os.listdir() if f.endswith('.djv')]
        all_files = fb2_files + epub_files + pdf_files + djvu_files + pdf_files2 + djvu_files2

        if not all_files:
            print("Не найдено файлов поддерживаемых форматов (.fb2, .epub, .pdf, .djvu)")
            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 file.endswith('.PDF'):
                extract_pdf_cover(file, output_file)
            elif file.endswith('.djvu'):
                extract_djvu_cover(file, output_file)
            elif file.endswith('.djv'):
                extract_djvu_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)
        elif input_file.endswith('.PDF'):
            extract_pdf_cover(input_file, output_file)
        elif input_file.endswith('.djvu'):
            extract_djvu_cover(input_file, output_file)
        elif input_file.endswith('.djv'):
            extract_djvu_cover(input_file, output_file)
        else:
            print("Поддерживаются только файлы .fb2, .epub, .pdf и .djvu")
            return

    else:
        print("fb2cover - извлечение обложек из файлов поддерживаемых форматов")
        print("\nИспользование:")
        print("fb2cover - извлечение обложек из всех файлов в текущей папке")
        print("fb2cover <input_file> <output_file> - извлечение обложки из указанного файла")

if __name__ == "__main__":
    main()

plutofertile

Wow, this is such a handy resource for extracting covers from e-books! I’m really curious about the Python scripts you mentioned. How user-friendly are they for someone new to coding? Any tips for getting started? granny 1

kd2600

26.08.2026 В связи с тем, что некоторые файлы .fb2 не соответствовали стандарту (ну или какой-то новый стандарт, я не разбирался в глубину) в функцию поиска обложек внесены правки. Теперь извлекает корректно.

kd2600

17.09.2026 Августовская история повторилась. Найден ряд .fb2 и .epub файлов, которые не соответствовали стандарту, пришлось вносить корректировки в обработку

Перед использованием необходимо подключить библиотеку lxml Для этого в командной строке ввести:

Код:

pip install lxml

Код:

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

def _localname(tag):
    """Возвращает локальное имя тега без namespace."""
    if isinstance(tag, str) and '}' in tag:
        return tag.split('}', 1)[1]
    return tag

def _iter_by_localname(root, name):
    """Итерирует все элементы с заданным локальным именем."""
    for elem in root.iter():
        if _localname(elem.tag) == name:
            yield elem

def _get_href(elem):
    """Возвращает значение атрибута href (xlink или обычного)."""
    for attr, val in elem.attrib.items():
        if _localname(attr) == 'href':
            return val
    return None

def _parse_fb2_tree(fb2_file):
    """Пытается распарсить FB2 разными способами. Возвращает root или None."""
    # Способ 1: обычный ET.parse
    try:
        tree = ET.parse(fb2_file)
        return tree.getroot()
    except ET.ParseError as e:
        print(f"ET.parse не смог разобрать {fb2_file}: {e}")
    except Exception as e:
        print(f"Ошибка чтения {fb2_file}: {e}")

    # Способ 2: читаем как байты, чистим типичные проблемы и парсим из строки
    try:
        with open(fb2_file, 'rb') as f:
            raw = f.read()
    except Exception as e:
        print(f"Не удалось прочитать файл {fb2_file}: {e}")
        return None

    encoding = 'utf-8'
    m = re.match(rb'<\?xml[^>]*encoding\s*=\s*["\']([^"\']+)["\']', raw[:200], re.IGNORECASE)
    if m:
        try:
            encoding = m.group(1).decode('ascii')
        except Exception:
            encoding = 'utf-8'

    try:
        text = raw.decode(encoding, errors='replace')
    except Exception:
        text = raw.decode('utf-8', errors='replace')

    text = text.lstrip('\ufeff')
    text = re.sub(r'&(?!(?:[a-zA-Z][a-zA-Z0-9]*|#\d+|#x[0-9a-fA-F]+);)', '&amp;', text)

    html_entities = {
        '&nbsp;': ' ', '&mdash;': '—', '&ndash;': '–',
        '&hellip;': '…', '&laquo;': '«', '&raquo;': '»',
        '&ldquo;': '“', '&rdquo;': '”', '&lsquo;': '‘',
        '&rsquo;': '’', '&copy;': '©', '&reg;': '®',
        '&trade;': '™', '&deg;': '°', '&plusmn;': '±',
        '&times;': '×', '&divide;': '÷', '&euro;': '€',
        '&pound;': '£', '&yen;': '¥', '&sect;': '§',
        '&para;': '¶', '&middot;': '·',
    }
    for k, v in html_entities.items():
        text = text.replace(k, v)

    try:
        return ET.fromstring(text)
    except ET.ParseError as e:
        print(f"ET.fromstring (после чистки) не смог разобрать {fb2_file}: {e}")
    except Exception as e:
        print(f"Ошибка парсинга {fb2_file}: {e}")

    # Способ 3: lxml с recover=True (если установлен)
    try:
        from lxml import etree as LET
        parser = LET.XMLParser(recover=True, huge_tree=True)
        lroot = LET.fromstring(raw, parser=parser)
        return ET.fromstring(LET.tostring(lroot, encoding='utf-8'))
    except ImportError:
        print("lxml не установлен, не могу использовать recover-парсер")
    except Exception as e:
        print(f"lxml recover тоже не справился с {fb2_file}: {e}")

    return None

def extract_fb2_cover(fb2_file, output_file):
    try:
        root = _parse_fb2_tree(fb2_file)
        if root is None:
            print(f"Не удалось разобрать FB2-файл {fb2_file}")
            return

        image_href = None

        # Способ 1: coverpage -> image
        for coverpage in _iter_by_localname(root, 'coverpage'):
            for image in _iter_by_localname(coverpage, 'image'):
                href = _get_href(image)
                if href and href.startswith('#'):
                    image_href = href
                    break
            if image_href:
                break

        # Способ 2: первая section внутри body, где есть image
        if image_href is None:
            for body in _iter_by_localname(root, 'body'):
                for section in _iter_by_localname(body, 'section'):
                    for image in _iter_by_localname(section, 'image'):
                        href = _get_href(image)
                        if href and href.startswith('#'):
                            image_href = href
                            break
                    if image_href:
                        break
                if image_href:
                    break

        # Способ 3: любой image с href, начинающимся с #
        if image_href is None:
            for image in _iter_by_localname(root, 'image'):
                href = _get_href(image)
                if href and href.startswith('#'):
                    image_href = href
                    break

        if image_href is None:
            print(f"Обложка не найдена в файле {fb2_file}")
            return

        binary_id = image_href[1:]

        binary = None
        for elem in _iter_by_localname(root, 'binary'):
            if elem.attrib.get('id') == binary_id:
                binary = elem
                break

        if binary is None:
            all_ids = [e.attrib.get('id') for e in _iter_by_localname(root, 'binary')]
            print(f"Binary с id='{binary_id}' не найден в {fb2_file}")
            print(f"Доступные id binary: {all_ids[:10]}{'...' if len(all_ids) > 10 else ''}")
            return

        image_data = binary.text
        if image_data is None:
            image_data = ''.join(binary.itertext())
        if not image_data:
            print(f"Нет данных изображения в файле {fb2_file}")
            return

        import base64
        image_data_clean = re.sub(r'\s+', '', image_data)
        try:
            image_bytes = base64.b64decode(image_data_clean)
        except Exception as e:
            print(f"Не удалось декодировать base64 в {fb2_file}: {e}")
            return

        if len(image_bytes) < 100:
            print(f"Подозрительно маленький размер обложки ({len(image_bytes)} байт) в {fb2_file}")
            return

        with open(output_file, 'wb') as img_file:
            img_file.write(image_bytes)
        print(f"Обложка сохранена как {output_file} ({len(image_bytes)} байт)")

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

def _find_title_page_html(items, content_root, ns):
    for item_id, info in items.items():
        if info['media-type'] not in ('application/xhtml+xml', 'text/html'):
            continue
        low = (item_id + ' ' + info['href']).lower()
        if ('title-page' in low or 'title_page' in low or 'titlepage' in low
                or 'cover-page' in low or 'cover_page' in low):
            return info['href']

    spine = content_root.find('.//opf:spine', ns)
    if spine is not None:
        for itemref in spine.findall('.//opf:itemref', ns):
            idref = itemref.attrib.get('idref')
            if idref and idref in items:
                info = items[idref]
                if info['media-type'] in ('application/xhtml+xml', 'text/html'):
                    return info['href']

    for item_id, info in items.items():
        if info['media-type'] in ('application/xhtml+xml', 'text/html'):
            return info['href']

    return None

def _extract_img_from_title_section(html_text):
    markers = [
        r'class\s*=\s*["\'][^"\']*epub__cover-page__wrapper[^"\']*["\']',
        r'id\s*=\s*["\']title-page["\']',
        r'class\s*=\s*["\'][^"\']*epub__title-page__header[^"\']*["\']',
        r'class\s*=\s*["\'][^"\']*cover[^"\']*["\']',
    ]

    for marker in markers:
        for m in re.finditer(marker, html_text, re.IGNORECASE):
            start = m.end()
            tail = html_text[start:start + 4000]

            img_m = re.search(r'<img[^>]*\bsrc\s*=\s*["\']([^"\']+)["\']', tail, re.IGNORECASE)
            if img_m:
                return img_m.group(1)

            svg_m = re.search(r'<image[^>]*\b(?:xlink:)?href\s*=\s*["\']([^"\']+)["\']', tail, re.IGNORECASE)
            if svg_m:
                return svg_m.group(1)

    for m in re.finditer(r'<img\b[^>]*>', html_text, re.IGNORECASE):
        tag = m.group(0)
        src_m = re.search(r'\bsrc\s*=\s*["\']([^"\']+)["\']', tag, re.IGNORECASE)
        if not src_m:
            continue
        src = src_m.group(1)
        if re.search(r'\.(jpe?g|png)$', src, re.IGNORECASE):
            return src

    return None

def _read_from_zip(z, path):
    path = path.replace('\\', '/')
    try:
        return z.read(path)
    except KeyError:
        target_name = os.path.basename(path)
        for name in z.namelist():
            if os.path.basename(name) == target_name:
                return z.read(name)
    return None

def extract_epub_cover(epub_file, output_file):
    try:
        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)

            ns = {
                'opf': 'http://www.idpf.org/2007/opf',
                'dc': 'http://purl.org/dc/elements/1.1/'
            }

            manifest = content_root.find('.//opf:manifest', ns)
            if manifest is None:
                print(f"Манифест не найден в файле {epub_file}")
                return

            items = {}
            for item in manifest.findall('.//opf:item', ns):
                item_id = item.attrib.get('id')
                items[item_id] = {
                    'href': item.attrib.get('href', ''),
                    'media-type': item.attrib.get('media-type', ''),
                    'properties': item.attrib.get('properties', ''),
                }

            # === Шаг 1: находим HTML титульной/обложечной страницы ===
            html_href = _find_title_page_html(items, content_root, ns)

            if html_href:
                html_href = html_href.replace('\\', '/')
                base_dir = os.path.dirname(content_path)
                if base_dir:
                    html_path = os.path.normpath(os.path.join(base_dir, html_href)).replace('\\', '/')
                else:
                    html_path = os.path.normpath(html_href).replace('\\', '/')

                html_data = _read_from_zip(z, html_path)

                if html_data is not None:
                    try:
                        html_text = html_data.decode('utf-8', errors='replace')
                    except Exception:
                        html_text = html_data.decode('latin-1', errors='replace')

                    img_src = _extract_img_from_title_section(html_text)

                    if img_src:
                        img_src = img_src.split('#')[0].split('?')[0]
                        img_src = img_src.replace('\\', '/')

                        if os.path.isabs(img_src) or img_src.startswith('/'):
                            img_path = img_src.lstrip('/')
                        else:
                            img_path = os.path.normpath(
                                os.path.join(os.path.dirname(html_path), img_src)
                            ).replace('\\', '/')

                        img_data = _read_from_zip(z, img_path)

                        if img_data is not None and len(img_data) >= 100:
                            with open(output_file, 'wb') as img_file:
                                img_file.write(img_data)
                            print(f"Обложка сохранена как {output_file} ({len(img_data)} байт)")
                            return
                        else:
                            print(f"Картинка из HTML найдена, но данные пусты/малы ({img_path})")

            # === Шаг 2: fallback — стандартный поиск обложки как изображения ===
            print(f"Ищем обложку стандартным способом в {epub_file}")
            cover_href = None

            meta_cover = content_root.find('.//opf:meta[@name="cover"]', ns)
            if meta_cover is not None:
                cover_id = meta_cover.attrib.get('content')
                if cover_id and cover_id in items:
                    cover_href = items[cover_id]['href']

            if cover_href is None:
                for item_id, info in items.items():
                    if 'cover-image' in info['properties'].split():
                        cover_href = info['href']
                        break

            if cover_href is None:
                for item_id, info in items.items():
                    if info['media-type'].startswith('image/'):
                        cover_href = info['href']
                        break

            if cover_href:
                cover_href = cover_href.replace('\\', '/')
                base_dir = os.path.dirname(content_path)
                if base_dir:
                    cover_path = os.path.normpath(os.path.join(base_dir, cover_href)).replace('\\', '/')
                else:
                    cover_path = os.path.normpath(cover_href).replace('\\', '/')

                cover_data = _read_from_zip(z, cover_path)

                if cover_data is not None and len(cover_data) >= 100:
                    with open(output_file, 'wb') as img_file:
                        img_file.write(cover_data)
                    print(f"Обложка сохранена как {output_file} ({len(cover_data)} байт)")
                    return

            print(f"Обложка не найдена в файле {epub_file}")

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

def extract_pdf_cover(pdf_file, output_file):
    try:
        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 extract_djvu_cover(djvu_file, output_file):
    temp_tiff = output_file + '.tiff'
    try:
        subprocess.run(['ddjvu', '--format=tiff', '--page=1', djvu_file, temp_tiff], check=True)

        from PIL import Image
        with Image.open(temp_tiff) as img:
            img.save(output_file, 'JPEG')

        print(f"Обложка сохранена как {output_file}")
    except subprocess.CalledProcessError as e:
        print(f"Ошибка при обработке DJVU файла {djvu_file}: {e}")
    except Exception as e:
        print(f"Ошибка при конвертации обложки {djvu_file}: {e}")
    finally:
        if os.path.exists(temp_tiff):
            try:
                os.remove(temp_tiff)
            except Exception:
                pass

def main():
    if len(sys.argv) == 1:
        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')]
        pdf_files2 = [f for f in os.listdir() if f.endswith('.PDF')]
        djvu_files = [f for f in os.listdir() if f.endswith('.djvu')]
        djvu_files2 = [f for f in os.listdir() if f.endswith('.djv')]
        all_files = fb2_files + epub_files + pdf_files + pdf_files2 + djvu_files + djvu_files2

        if not all_files:
            print("Не найдено файлов поддерживаемых форматов (.fb2, .epub, .pdf, .djvu)")
            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') or file.endswith('.PDF'):
                extract_pdf_cover(file, output_file)
            elif file.endswith('.djvu') or file.endswith('.djv'):
                extract_djvu_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') or input_file.endswith('.PDF'):
            extract_pdf_cover(input_file, output_file)
        elif input_file.endswith('.djvu') or input_file.endswith('.djv'):
            extract_djvu_cover(input_file, output_file)
        else:
            print("Поддерживаются только файлы .fb2, .epub, .pdf и .djvu")
            return

    else:
        print("fb2cover - извлечение обложек из файлов поддерживаемых форматов")
        print("\nИспользование:")
        print("fb2cover - извлечение обложек из всех файлов в текущей папке")
        print("fb2cover <input_file> <output_file> - извлечение обложки из указанного файла")

if __name__ == "__main__":
    main()
Показать сообщения:    
Ответить на тему