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]+);)', '&', text)
html_entities = {
' ': ' ', '—': '—', '–': '–',
'…': '…', '«': '«', '»': '»',
'“': '“', '”': '”', '‘': '‘',
'’': '’', '©': '©', '®': '®',
'™': '™', '°': '°', '±': '±',
'×': '×', '÷': '÷', '€': '€',
'£': '£', '¥': '¥', '§': '§',
'¶': '¶', '·': '·',
}
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()