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

if __name__ == "__main__":
    main()
ps: 04102026 внесены косметические правки в вывод программы, сам алгоритм работы остался не тронутым.

regidrer

ramikaseo писал(а):

Escorts Greater Kailash is elegant in public and wildly passionate in private. She focuses completely on your satisfaction, combining soft romance with raw heat. Every movement feels intentional, every moment addictive. Discreet, sophisticated, and dangerously good for a night that stays in your mind long after.
СПАМ
Показать сообщения:    
Ответить на тему