EPUB как второй вход в конвейер; листинги кода не переводятся и не ломают приёмку
- epub2html.py: EPUB -> тот же XHTML, что pdf2html.py, только стандартная библиотека. Уровень глав определяется по книге, а не берётся из <h1>: в EPUB там обычно название и части. - pdf2html.py: листинг узнаётся по флагу моноширинного шрифта PyMuPDF, переносы и отступы внутри <pre> сохраняются, де-дефисация к коду не применяется. - translate.py: <pre> исключён из проверок языка (английский код утягивал долю кириллицы ниже порога приёмки), рабочий каталог привязан к книге, устаревшие главы прошлого прогона чистятся, инлайновый <code> переживает переводчика. - bookhtml.py: общий каркас документа и CSS для обоих входов. - Тесты: test_epub2html.py на собранном в памяти EPUB, четыре новых случая в test_translate.py.
This commit is contained in:
+34
-32
@@ -7,6 +7,11 @@ from pathlib import Path
|
||||
|
||||
import pymupdf
|
||||
|
||||
import bookhtml
|
||||
|
||||
if len(sys.argv) < 3 or sys.argv[1] in ("-h", "--help"):
|
||||
sys.exit("usage: pdf2html.py book.pdf out.html | pdf2html.py --fonts book.pdf")
|
||||
|
||||
if sys.argv[1] == "--fonts": # разведка: какие шрифты/кегли в PDF
|
||||
import collections
|
||||
|
||||
@@ -29,6 +34,7 @@ IMGDIR.mkdir(parents=True, exist_ok=True)
|
||||
# Пороги подобраны под вёрстку 15pt/letter. Для другой книги сначала
|
||||
# посмотреть реальные шрифты и кегли: pdf2html.py --fonts file.pdf
|
||||
INDENT_X = 88 # x0 первой строки: больше — абзац с красной строки
|
||||
MONO = 8 # бит моноширинного шрифта в span["flags"] (PyMuPDF)
|
||||
|
||||
|
||||
def style(font):
|
||||
@@ -36,10 +42,20 @@ def style(font):
|
||||
return ("bold" in f or "semibold" in f, "-it" in f or "italic" in f)
|
||||
|
||||
|
||||
def span_html(s):
|
||||
def is_mono(s):
|
||||
"""Моноширинный шрифт = листинг кода. Признак берётся из флагов PyMuPDF, а
|
||||
не из имени шрифта: имена у каждого издательства свои, флаг одинаковый."""
|
||||
return bool(s["flags"] & MONO)
|
||||
|
||||
|
||||
def span_html(s, plain=False):
|
||||
t = html.escape(s["text"])
|
||||
if not t:
|
||||
return ""
|
||||
if plain: # внутри <pre> курсив и полужирный только мешают
|
||||
return t
|
||||
if is_mono(s):
|
||||
return "<code>%s</code>" % t
|
||||
bold, ital = style(s["font"])
|
||||
if ital:
|
||||
t = "<i>%s</i>" % t
|
||||
@@ -59,6 +75,8 @@ def block_kind(b):
|
||||
return ("h1", "title")
|
||||
if sz >= 24:
|
||||
return ("h1", None)
|
||||
if all(is_mono(sp) for sp in spans):
|
||||
return ("pre", None)
|
||||
if sz >= 17:
|
||||
return ("h2", None)
|
||||
if f.startswith("MyriadPro"):
|
||||
@@ -66,7 +84,13 @@ def block_kind(b):
|
||||
return ("p", None)
|
||||
|
||||
|
||||
def block_text(b):
|
||||
def block_text(b, pre=False):
|
||||
if pre:
|
||||
# Перенос строки в коде значим, висящий дефис — это минус, а не перенос
|
||||
# слова. Ни склейки строк, ни де-дефисации здесь быть не должно.
|
||||
rows = ["".join(span_html(s, plain=True) for s in l["spans"])
|
||||
for l in b["lines"]]
|
||||
return "\n".join(rows).rstrip()
|
||||
out = []
|
||||
for i, l in enumerate(b["lines"]):
|
||||
line = "".join(span_html(s) for s in l["spans"])
|
||||
@@ -82,6 +106,7 @@ def block_text(b):
|
||||
txt = "".join(out)
|
||||
txt = re.sub(r"</i>(\s*)<i>", r"\1", txt)
|
||||
txt = re.sub(r"</b>(\s*)<b>", r"\1", txt)
|
||||
txt = re.sub(r"(?<=\S) {2,}(?=\S)", " ", txt)
|
||||
return txt.strip()
|
||||
|
||||
|
||||
@@ -112,7 +137,7 @@ for pno, page in enumerate(doc):
|
||||
if not kind:
|
||||
continue
|
||||
tag, cls = kind
|
||||
txt = block_text(b)
|
||||
txt = block_text(b, pre=(tag == "pre"))
|
||||
if not txt:
|
||||
continue
|
||||
indented = b["lines"][0]["bbox"][0] >= INDENT_X
|
||||
@@ -134,36 +159,13 @@ for pno, page in enumerate(doc):
|
||||
if open_para:
|
||||
parts.append(open_para)
|
||||
|
||||
CSS = """
|
||||
body { margin: 0 1em; }
|
||||
h1 { text-align: center; margin: 2em 0 0.2em; page-break-before: always; }
|
||||
h1.title { page-break-before: avoid; }
|
||||
h2 { text-align: center; font-style: italic; font-weight: normal;
|
||||
font-size: 1.1em; margin: 0.2em 0 1.5em; }
|
||||
p { text-indent: 1.2em; margin: 0; text-align: justify; }
|
||||
p.note { text-indent: 0; margin: 1em 2em; font-size: 0.9em;
|
||||
font-family: sans-serif; }
|
||||
p.figure { text-indent: 0; text-align: center; margin: 1em 0; }
|
||||
img { max-width: 100%; }
|
||||
"""
|
||||
|
||||
buf = ['<?xml version="1.0" encoding="utf-8"?>',
|
||||
'<html xmlns="http://www.w3.org/1999/xhtml"><head>',
|
||||
"<title>%s</title>" % html.escape(doc.metadata.get("title") or SRC.stem),
|
||||
"<style>%s</style></head><body>" % CSS]
|
||||
for tag, cls, txt in parts:
|
||||
if tag == "figure":
|
||||
buf.append('<p class="figure">%s</p>' % txt)
|
||||
else:
|
||||
c = ' class="%s"' % cls if cls else ""
|
||||
buf.append("<%s%s>%s</%s>" % (tag, c, txt, tag))
|
||||
buf.append("</body></html>")
|
||||
doc_html = "\n".join(buf)
|
||||
# merge tag runs split at page/line boundaries, collapse doubled spaces
|
||||
doc_html = re.sub(r"</(i|b)>(\s*)<\1>", r"\2", doc_html)
|
||||
doc_html = re.sub(r"(?<=\S) {2,}(?=\S)", " ", doc_html)
|
||||
doc_html = bookhtml.document(doc.metadata.get("title") or SRC.stem, parts)
|
||||
# merge tag runs split at page/line boundaries. Пробелы здесь уже не трогаем:
|
||||
# внутри <pre> они значимы, а в прозе схлопнуты в block_text.
|
||||
doc_html = re.sub(r"</(i|b|code)>(\s*)<\1>", r"\2", doc_html)
|
||||
OUT.write_text(doc_html, encoding="utf-8")
|
||||
|
||||
print("blocks:", len(parts), "images:", img_n)
|
||||
print("h1:", sum(1 for p in parts if p[0] == "h1"),
|
||||
"p:", sum(1 for p in parts if p[0] == "p"))
|
||||
"p:", sum(1 for p in parts if p[0] == "p"),
|
||||
"pre:", sum(1 for p in parts if p[0] == "pre"))
|
||||
|
||||
Reference in New Issue
Block a user