OCR-слой: моно-признак не по флагу, кегль округляется, листинги склеиваются
Невидимый слой 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 <noreply@anthropic.com>
This commit is contained in:
+45
-4
@@ -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-слой отдаёт каждую
|
||||
# строку кода отдельным блоком, и без склейки листинг рассыпается на
|
||||
# десяток однострочных <pre> подряд. Строки внутри блока значимы,
|
||||
# поэтому соединяются переводом строки, а не пробелом.
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user