From 356b9ddb221a48e6c629b69fcfa4970502f16523 Mon Sep 17 00:00:00 2001 From: chesirecatt Date: Mon, 24 Aug 2026 19:42:14 +0300 Subject: [PATCH] =?UTF-8?q?OCR-=D1=81=D0=BB=D0=BE=D0=B9:=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=BD=D0=BE-=D0=BF=D1=80=D0=B8=D0=B7=D0=BD=D0=B0=D0=BA=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=BF=D0=BE=20=D1=84=D0=BB=D0=B0=D0=B3=D1=83,?= =?UTF-8?q?=20=D0=BA=D0=B5=D0=B3=D0=BB=D1=8C=20=D0=BE=D0=BA=D1=80=D1=83?= =?UTF-8?q?=D0=B3=D0=BB=D1=8F=D0=B5=D1=82=D1=81=D1=8F,=20=D0=BB=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BD=D0=B3=D0=B8=20=D1=81=D0=BA=D0=BB=D0=B5?= =?UTF-8?q?=D0=B8=D0=B2=D0=B0=D1=8E=D1=82=D1=81=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Невидимый слой tesseract набран GlyphLessFont с выставленным моно-битом, и is_mono() опознавала книгу как один сплошной листинг: измерено на Ansible после OCR — pre 10498, p 0. Кегль в таком слое подгоняется под рамку слова и гуляет внутри абзаца (9.0, 9.2, 9.4), из-за чего гистограмма размазывалась и основной кегль книги определялся неверно; в OCR-слое он округляется до целого. Каждая строка кода приходит отдельным блоком — соседние pre склеиваются переводом строки, иначе листинг рассыпается на десяток однострочных. Признак OCR-слоя считается по выборке страниц, обычные PDF идут прежним путём. Co-Authored-By: Claude Opus 5 --- scripts/pdf2html.py | 49 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/scripts/pdf2html.py b/scripts/pdf2html.py index 254ee45..b140e73 100644 --- a/scripts/pdf2html.py +++ b/scripts/pdf2html.py @@ -40,6 +40,7 @@ MAX_IMG_SHARE = 0.6 # доля площади страницы: больше # кегль мельче основного текста плюс пунктуация, которой в прозе не бывает. CODEY = re.compile(r"[{}();\[\]<>=*/#]|::|->") SOFT_HYPHEN = "\u00ad" +OCR = False def style(font, flags=0): @@ -51,10 +52,40 @@ def style(font, flags=0): def is_mono(s): """Моноширинный шрифт = листинг кода. Признак из флагов, а не из имени: - имена у каждого издательства свои, флаг одинаковый.""" + имена у каждого издательства свои, флаг одинаковый. + + Исключение — невидимый слой Tesseract: он весь набран GlyphLessFont с + выставленным моно-битом, и без этой проверки книга целиком опознаётся как + один сплошной листинг (измерено на Ansible после OCR: pre 10498, p 0). + В таком слое листинги ловятся кеглем и пунктуацией, см. block_kind(). + """ + if "glyphless" in s["font"].lower(): + return False return bool(s["flags"] & MONO) +def ocr_layer(doc, step=23): + """Текст сделан OCR-ом: у всего слоя один невидимый шрифт. Кегль в таком + слое подгоняется под рамку слова и гуляет (9.0, 9.2, 9.4 в одном абзаце), + поэтому дальше он округляется до целого — иначе гистограмма размазывается + и основной кегль книги определяется неверно.""" + glyphless = total = 0 + for pno in range(1, doc.page_count, step): + for b in doc[pno].get_text("dict")["blocks"]: + for l in b.get("lines", []): + for s in l["spans"]: + n = len(s["text"]) + total += n + if "glyphless" in s["font"].lower(): + glyphless += n + return total > 0 and glyphless / total > 0.9 + + +def size_of(span): + """Кегль спана. В OCR-слое округляется до целого: см. ocr_layer().""" + return round(span["size"]) if OCR else round(span["size"], 1) + + def profile(doc, step=7): """(кегль текста, x0 красной строки, кегль глав) по выборке страниц. @@ -74,9 +105,9 @@ def profile(doc, step=7): for l in b["lines"]: x0[round(l["bbox"][0])] += 1 for sp in spans: - size_chars[round(sp["size"], 1)] += len(sp["text"]) + size_chars[size_of(sp)] += len(sp["text"]) top = max(spans, key=lambda sp: len(sp["text"])) - head_blocks[round(top["size"], 1)] += 1 + head_blocks[size_of(top)] += 1 if not size_chars: sys.exit("в PDF нет извлекаемого текста — нужен OCR, этот скрипт не поможет") body = size_chars.most_common(1)[0][0] @@ -118,7 +149,7 @@ def block_kind(b, body, h1_size): if not spans: return None top = max(spans, key=lambda s: len(s["text"])) - sz = top["size"] + sz = size_of(top) if all(is_mono(sp) for sp in spans): return ("pre", None) if sz <= body * 0.93 and looks_like_code(spans): @@ -169,7 +200,10 @@ def block_text(b, pre=False): doc = pymupdf.open(SRC) +OCR = ocr_layer(doc) BODY, INDENT_X, H1_SIZE = profile(doc) +if OCR: + print("слой распознан OCR-ом: моно-признак отключён, кегль округляется") print("кегль текста %.1f, глав %.1f, красная строка от x0=%.0f" % (BODY, H1_SIZE, INDENT_X)) parts = [] @@ -224,6 +258,13 @@ for pno, page in enumerate(doc): join = "" if open_para[2].endswith("-") else " " open_para = (tag, cls, open_para[2] + join + txt, open_para[3]) continue + # Соседние листинги склеиваются в один блок: OCR-слой отдаёт каждую + # строку кода отдельным блоком, и без склейки листинг рассыпается на + # десяток однострочных
 подряд. Строки внутри блока значимы,
+        # поэтому соединяются переводом строки, а не пробелом.
+        if OCR and tag == "pre" and open_para and open_para[0] == "pre":
+            open_para = (tag, cls, open_para[2] + "\n" + txt, open_para[3])
+            continue
         if open_para:
             parts.append(open_para)
         open_para = (tag, cls, txt, at_top)