#!/usr/bin/env python3 """PDF (calibre-generated) -> semantic XHTML, preserving italic/bold/headings.""" import html import re import sys from pathlib import Path import pymupdf if sys.argv[1] == "--fonts": # разведка: какие шрифты/кегли в PDF import collections c = collections.Counter() d = pymupdf.open(sys.argv[2]) for p in d: for b in p.get_text("dict")["blocks"]: for l in b.get("lines", []): for s in l["spans"]: c[(s["font"], round(s["size"], 1))] += len(s["text"]) for (f, sz), n in c.most_common(25): print("%-32s %5.1f %8d симв." % (f, sz, n)) sys.exit() SRC = Path(sys.argv[1]) OUT = Path(sys.argv[2]) IMGDIR = OUT.parent / "images" IMGDIR.mkdir(parents=True, exist_ok=True) # Пороги подобраны под вёрстку 15pt/letter. Для другой книги сначала # посмотреть реальные шрифты и кегли: pdf2html.py --fonts file.pdf INDENT_X = 88 # x0 первой строки: больше — абзац с красной строки def style(font): f = font.lower() return ("bold" in f or "semibold" in f, "-it" in f or "italic" in f) def span_html(s): t = html.escape(s["text"]) if not t: return "" bold, ital = style(s["font"]) if ital: t = "%s" % t if bold: t = "%s" % t return t def block_kind(b): """(tag, css class) from dominant span font/size.""" spans = [s for l in b["lines"] for s in l["spans"] if s["text"].strip()] if not spans: return None top = max(spans, key=lambda s: len(s["text"])) f, sz = top["font"], top["size"] if sz >= 28 or f.startswith("Arvo") or f.startswith("CloisterBlack"): return ("h1", "title") if sz >= 24: return ("h1", None) if sz >= 17: return ("h2", None) if f.startswith("MyriadPro"): return ("p", "note") # Maxine's journal / chat / slides return ("p", None) def block_text(b): out = [] for i, l in enumerate(b["lines"]): line = "".join(span_html(s) for s in l["spans"]) if not line: continue if out: prev = out[-1] if prev.endswith("-") and not prev.endswith("--"): out[-1] = prev[:-1] # de-hyphenate else: out.append(" ") out.append(line) txt = "".join(out) txt = re.sub(r"(\s*)", r"\1", txt) txt = re.sub(r"(\s*)", r"\1", txt) return txt.strip() doc = pymupdf.open(SRC) parts = [] open_para = None # (tag, cls, text) still collecting # cover pix = doc[0].get_pixmap(dpi=150) pix.save(IMGDIR / "cover.jpg") img_n = 0 for pno, page in enumerate(doc): if pno == 0: continue blocks = sorted(page.get_text("dict")["blocks"], key=lambda b: b["bbox"][1]) for bi, b in enumerate(blocks): if b["type"] == 1: img_n += 1 name = "img%02d.png" % img_n (IMGDIR / name).write_bytes(b["image"]) if open_para: parts.append(open_para) open_para = None parts.append(("figure", None, '' % name)) continue kind = block_kind(b) if not kind: continue tag, cls = kind txt = block_text(b) if not txt: continue indented = b["lines"][0]["bbox"][0] >= INDENT_X cont = ( open_para and tag == "p" and open_para[0] == "p" and open_para[1] == cls and bi == 0 and not indented ) if cont: join = "" if open_para[2].endswith("-") else " " open_para = (tag, cls, open_para[2] + join + txt) continue if open_para: parts.append(open_para) open_para = (tag, cls, txt) 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 = ['', '', "%s" % html.escape(doc.metadata.get("title") or SRC.stem), "" % CSS] for tag, cls, txt in parts: if tag == "figure": buf.append('

%s

' % txt) else: c = ' class="%s"' % cls if cls else "" buf.append("<%s%s>%s" % (tag, c, txt, tag)) buf.append("") doc_html = "\n".join(buf) # merge tag runs split at page/line boundaries, collapse doubled spaces doc_html = re.sub(r"(\s*)<\1>", r"\2", doc_html) doc_html = re.sub(r"(?<=\S) {2,}(?=\S)", " ", 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"))