Восстановление структуры EPUB по CSS для дампов из PDF

Дамп, собранный конвертером из PDF, теряет всю семантику: ни заголовков, ни
листингов, только абзацы с обфусцированными классами. Такие книги уезжали в
перевод одним куском, а код — вместе с текстом.

Теперь при нулевом числе заголовков или листингов разметка восстанавливается из
таблицы стилей книги: три самых крупных кегля становятся уровнями заголовков,
моноширинная гарнитура — листингом, соседние строки листинга склеиваются в один
блок. Книга со своей разметкой в этот путь не попадает.

Проверено на двух настоящих книгах: дамп APoSD — 1833 плоских абзаца стали 29
главами и 58 листингами; Страуструп с собственной разметкой не изменился.
This commit is contained in:
chesirecatt
2026-08-20 19:11:51 +03:00
parent db9fe5e0fa
commit 1bacd38292
3 changed files with 181 additions and 18 deletions
+73 -2
View File
@@ -113,7 +113,78 @@ def test_convert(tmp: Path):
assert text.count("<h1>") == 3 and "<h2>" not in text
# Дамп, собранный конвертером из PDF: ни одного заголовка, ни одного <pre>,
# только <p> с обфусцированными классами и таблица стилей.
FLAT_CSS = """
.cls_head { font-size: 1.66667em; font-family: Reg; }
.cls_body { font-size: 1em; font-family: Reg; }
.cls_code { font-size: 0.9em; font-family: lucidasanstypewriter; }
"""
FLAT_OPF = """<?xml version="1.0"?>
<package xmlns="http://www.idpf.org/2007/opf" version="2.0">
<metadata/>
<manifest>
<item id="s" href="style.css" media-type="text/css"/>
<item id="a" href="p1.xhtml" media-type="application/xhtml+xml"/>
<item id="b" href="p2.xhtml" media-type="application/xhtml+xml"/>
<item id="c" href="p3.xhtml" media-type="application/xhtml+xml"/>
</manifest>
<spine><itemref idref="a"/><itemref idref="b"/><itemref idref="c"/></spine>
</package>"""
def flat_doc(n):
return """<?xml version="1.0" encoding="utf-8"?>
<html xmlns="http://www.w3.org/1999/xhtml"><body>
<p class="cls_head">Chapter %d</p>
<p class="cls_head">Title Of Chapter %d</p>
<p class="cls_body">Body text of the chapter, long enough to be a paragraph.</p>
<p class="cls_code">def f(x):</p>
<p class="cls_code"> return x + 1</p>
<p class="cls_body">Closing paragraph of the chapter goes here.</p>
</body></html>""" % (n, n)
def test_flat_dump_recovered(tmp: Path):
src = tmp / "flat.epub"
with zipfile.ZipFile(src, "w") as z:
z.writestr("mimetype", "application/epub+zip")
z.writestr("META-INF/container.xml", CONTAINER)
z.writestr("OEBPS/content.opf", FLAT_OPF)
z.writestr("OEBPS/style.css", FLAT_CSS)
for i in (1, 2, 3):
z.writestr("OEBPS/p%d.xhtml" % i, flat_doc(i))
out = tmp / "flat.html"
epub2html.convert(src, out)
text = out.read_text(encoding="utf-8")
# заголовки восстановлены по кеглю и склеены с номером главы
assert "<h1>Chapter 2. Title Of Chapter 2</h1>" in text, text
assert text.count("<h1>") == 3
# листинг восстановлен по гарнитуре, соседние строки склеены в один блок
assert "<pre>def f(x):\n return x + 1</pre>" in text, text
assert text.count("<pre>") == 3
# обычный текст остался абзацем
assert "<p>Body text of the chapter" in text
def test_real_markup_wins_over_css(tmp: Path):
"""Книга со своей разметкой не должна уезжать в запасной путь."""
src = tmp / "book.epub"
build_epub(src)
out = tmp / "book.html"
epub2html.convert(src, out)
text = out.read_text(encoding="utf-8")
assert text.count("<h1>") == 3, "заголовки книги должны остаться её собственными"
assert "def main():" in text
if __name__ == "__main__":
with tempfile.TemporaryDirectory() as d:
test_convert(Path(d))
for case in (test_convert, test_flat_dump_recovered,
test_real_markup_wins_over_css):
with tempfile.TemporaryDirectory() as d:
case(Path(d))
print("OK")