Files
pdf2epub/scripts/test_pack_epub.py
T
chesirecatt 33845bd47a Пять правок по итогам четырёх книг из to_read
Извлечение (pdf2html.py), каждая правка вызвана конкретной книгой:

* колонтитул опознаётся по повтору текста в верхнем поле на многих полосах, а
  не по одной позиции. Позиционный признак рубит и настоящие заголовки: у
  Вернона («DDD Distilled») глава начинается ровно с верха полосы и короче 80
  знаков, так пропали 14 заголовков из 15. Это ровно тот второй проход, о
  котором говорил ponytail-комментарий на прежнем фильтре;
* блок режется, если шапка набрана крупнее следующего за ней текста. У Вернона
  85 подзаголовков лежали в одном блоке с первым абзацем раздела: оглавление
  теряло второй уровень, а абзац начинался с заголовка без точки;
* порог раздела снижен с 1.15 до 1.10 кегля текста. У Вернона подзаголовок
  17.2 при тексте 15.0 — ровно на пять сотых ниже прежнего порога, и все 85
  разделов уезжали в прозу. Тот же множитель, что у разреза блока, иначе
  разрез и классификация расходятся;
* кандидатом в главы не может быть кегль, у которого в блоках нет букв. У
  Нейгарда («Release it!») номер главы набран кеглем 100 отдельным блоком
  («1», «2», …), и главой объявлялся он, а настоящие заголовки в 30 пунктов
  уезжали в разделы — оглавление выходило пустым. Отбор по наличию букв, а не
  по длине: «Preface» — семь знаков, порог по длине отсекал бы и его.

Упаковка (pack_epub.py): XML 1.0 запрещает управляющие символы, а из PDF они
приезжают регулярно. У Бейера («Site Reliability Engineering») 33 главы из 45
не разбирались как XML из-за одного такого знака в начале абзаца, и читалка
спотыкается на таком файле молча. Чистим на упаковке, а не надеемся на
извлечение. Тест на это добавлен и проверен: с обезвреженной правкой он падает.

SKILL.md приведён в соответствие с репозиторием. Шаг упаковки с 25.08 делает
pack_epub.py, а текст всё ещё требовал calibre на PATH — из-за этого проверка
инструментов давала ложный отказ. Замерено 05.09.2026: ebook-convert
отсутствует на всех трёх здешних машинах, и все четыре книги собрались без
него. За calibre остался только .azw3 для Kindle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XCiPWRi3Jqy4saD2LwU1Dp
2026-09-05 18:23:05 +03:00

118 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Самопроверка упаковщика без сети: python3 test_pack_epub.py"""
import re
import tempfile
import zipfile
from pathlib import Path
import pack_epub
BOOK = """<?xml version="1.0" encoding="utf-8"?>
<html xmlns="http://www.w3.org/1999/xhtml"><head><title>T</title>
<style>p { margin: 0; }</style></head><body>
<p>Предисловие до первой главы.</p>
<h1>Глава 1. Начало</h1>
<p>Текст первой главы.</p>
<pre>def f():
return 1</pre>
<p class="figure"><img src="images/fig.png"/></p>
<h1>Глава 2. Продолжение</h1>
<p>Текст второй главы.</p>
</body></html>"""
def test_pack(tmp: Path):
src = tmp / "book.html"
src.write_text(BOOK, encoding="utf-8")
(tmp / "images").mkdir()
(tmp / "images" / "fig.png").write_bytes(b"\x89PNG\r\n\x1a\n")
(tmp / "images" / "cover.jpg").write_bytes(b"\xff\xd8\xff")
out = tmp / "book.epub"
chapters, imgs = pack_epub.build(src, out, {
"title": "Книга & книга", "author": "Автор", "lang": "ru",
"cover": "cover.jpg", "id": "urn:uuid:test"})
# преамбула до первой главы не теряется, поэтому глав три, а не две
assert (chapters, imgs) == (3, 1), (chapters, imgs)
z = zipfile.ZipFile(out)
names = z.namelist()
# без этого часть читалок не опознаёт файл как EPUB
assert names[0] == "mimetype"
assert z.getinfo("mimetype").compress_type == zipfile.ZIP_STORED
assert "OEBPS/images/fig.png" in names and "OEBPS/images/cover.jpg" in names
opf = z.read("OEBPS/content.opf").decode("utf-8")
assert "<dc:title>Книга &amp; книга</dc:title>" in opf, "разметку надо экранировать"
assert opf.count("<itemref") == 3
assert 'properties="cover-image"' in opf
ncx = z.read("OEBPS/toc.ncx").decode("utf-8")
assert re.findall(r"<text>([^<]*)</text>", ncx)[1:] == [
"Начало", "Глава 1. Начало", "Глава 2. Продолжение"]
ch1 = z.read("OEBPS/ch001.xhtml").decode("utf-8")
assert "<pre>def f():\n return 1</pre>" in ch1, "переносы в листинге значимы"
assert 'href="style.css"' in ch1
assert "Текст второй главы" not in ch1, "главы не должны склеиваться"
def test_cover_used_in_text_is_not_duplicated(tmp: Path):
"""Обложка, встречающаяся и в тексте, не должна попадать в архив дважды."""
src = tmp / "book.html"
src.write_text("<h1>Глава</h1>\n"
'<p class="figure"><img src="images/cover.jpg"/></p>\n'
"<p>Текст главы.</p>", encoding="utf-8")
(tmp / "images").mkdir()
(tmp / "images" / "cover.jpg").write_bytes(b"\xff\xd8\xff")
out = tmp / "book.epub"
pack_epub.build(src, out, {"title": "t", "author": "a", "lang": "ru",
"cover": "cover.jpg", "id": "i"})
names = zipfile.ZipFile(out).namelist()
assert names.count("OEBPS/images/cover.jpg") == 1, names
opf = zipfile.ZipFile(out).read("OEBPS/content.opf").decode()
assert opf.count('href="images/cover.jpg"') == 1, opf
def test_control_chars_stripped(tmp: Path):
"""Управляющий знак из PDF делает главу неразбираемой как XML.
У Бейера («Site Reliability Engineering») так падали 33 главы из 45:
один \x02 в начале абзаца, и читалка молча спотыкается на файле.
"""
import xml.etree.ElementTree as ET
src = tmp / "book.html"
src.write_text(BOOK.replace("<p>Текст первой главы.</p>",
"<p>\x02Текст первой главы.</p>"),
encoding="utf-8")
(tmp / "images").mkdir()
(tmp / "images" / "fig.png").write_bytes(b"\x89PNG\r\n\x1a\n")
(tmp / "images" / "cover.jpg").write_bytes(b"\xff\xd8\xff")
out = tmp / "book.epub"
pack_epub.build(src, out, {"title": "T", "author": "A", "lang": "ru",
"cover": "cover.jpg", "id": "urn:uuid:test"})
z = zipfile.ZipFile(out)
for name in z.namelist():
if name.endswith((".xhtml", ".opf", ".ncx")):
ET.fromstring(z.read(name)) # падает, если знак остался
def test_no_content(tmp: Path):
src = tmp / "empty.html"
src.write_text("<html><body>\n</body></html>", encoding="utf-8")
try:
pack_epub.build(src, tmp / "x.epub", {"title": "t", "author": "", "lang": "ru",
"cover": "", "id": "i"})
except SystemExit:
return
raise AssertionError("пустой источник должен останавливать упаковку")
if __name__ == "__main__":
for case in (test_pack, test_cover_used_in_text_is_not_duplicated,
test_no_content):
with tempfile.TemporaryDirectory() as d:
case(Path(d))
print("OK")