# -*- coding: utf-8 -*- """Build 分册 + 合订版 Word/PDF for 大洋 MES manuals 9.19.""" import os, re, html as htmlmod from pathlib import Path import fitz from docx import Document from docx.shared import Pt, Cm, RGBColor, Emu from docx.oxml.ns import qn from docx.enum.table import WD_TABLE_ALIGNMENT from docx.enum.text import WD_ALIGN_PARAGRAPH ROOT = Path(r"D:\产线\大洋\大洋操作手册汇总9.19") FONT_FILE = r"C:\Windows\Fonts\simhei.ttf" FONT_FILE_REG = r"C:\Windows\Fonts\msyh.ttc" DATE = "2026年9月19日" A4 = fitz.paper_rect("a4") FASCICLES = [ ("1、系统管理操作手册.md", "系统管理操作手册", "分册一"), ("2、设备管理操作手册.md", "设备管理操作手册", "分册二"), ("3、物料管理操作手册.md", "物料管理操作手册", "分册三"), ("4、质量管理操作手册.md", "质量管理操作手册", "分册四"), ("5、生产管理操作手册.md", "生产管理操作手册", "分册五"), ("6、ERP对接操作手册.md", "ERP对接操作手册", "分册六"), ("7、生产线管理操作手册.md", "生产线管理操作手册", "分册七"), ("8、PDA操作说明操作手册.md", "PDA操作说明操作手册", "分册八"), ("9、MES系统客户端员工扫码操作手册.md", "MES系统客户端员工扫码操作手册", "分册九"), ] def esc(s): return htmlmod.escape(s, quote=True) def md_inline_html(text): text = esc(text) text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text) text = re.sub(r"`([^`]+)`", r"\1", text) return text def is_sep_row(cells): return all(re.match(r"^:?-+:?$", c.replace(" ", "")) for c in cells if c != "") def md_to_html_body(md_text, skip_first_h1=False): lines = md_text.splitlines() out = [] i = 0 skipped = False while i < len(lines): line = lines[i].rstrip() if not line: i += 1 continue if line.strip() == "---": i += 1 continue if line.startswith("# "): title = line[2:].strip() if skip_first_h1 and not skipped: skipped = True i += 1 continue out.append("

%s

" % md_inline_html(title)) i += 1 continue m = re.match(r"^(#{2,6})\s+(.*)$", line) if m: lv = len(m.group(1)) out.append("%s" % (lv, md_inline_html(m.group(2).strip()), lv)) i += 1 continue if line.startswith("|"): rows = [] while i < len(lines) and lines[i].startswith("|"): cells = [c.strip() for c in lines[i].strip().strip("|").split("|")] if not is_sep_row(cells): rows.append(cells) i += 1 if rows: out.append("") for ri, row in enumerate(rows): out.append("") tag = "th" if ri == 0 else "td" for c in row: out.append("<%s>%s" % (tag, md_inline_html(c), tag)) out.append("") out.append("
") continue if line.startswith("- "): out.append("") continue if re.match(r"^\d+\.\s", line): out.append("
    ") while i < len(lines) and re.match(r"^\d+\.\s", lines[i]): out.append("
  1. %s
  2. " % md_inline_html(re.sub(r"^\d+\.\s+", "", lines[i]))) i += 1 out.append("
") continue img = re.match(r"^!\[(.*?)\]\((.*?)\)$", line.strip()) if img: cap, src = img.group(1), img.group(2).replace("\\", "/") rel = src.split("images/")[-1] if "images/" in src else src.split("/")[-1] member = rel.replace("/", "__") out.append('

%s

' % (esc(member), esc(cap))) out.append('

%s

' % md_inline_html(cap)) i += 1 continue if line.strip().startswith("*(界面截图") and "为准" in line: i += 1 continue out.append("

%s

" % md_inline_html(line)) i += 1 return "\n".join(out) CSS = r""" @font-face { font-family: bodyfont; src: url("simhei.ttf"); } body { font-family: bodyfont, sans-serif; font-size: 10.5pt; line-height: 1.5; color: #222; } h1 { font-size: 18pt; color: #1F4E79; margin: 18pt 0 8pt 0; page-break-before: auto; } h2 { font-size: 14pt; color: #2E75B6; margin: 14pt 0 6pt 0; } h3 { font-size: 12.5pt; color: #2E75B6; margin: 12pt 0 5pt 0; } h4 { font-size: 11.5pt; color: #1F4E79; margin: 10pt 0 4pt 0; } h5, h6 { font-size: 11pt; margin: 8pt 0 4pt 0; } p { margin: 0 0 6pt 0; } ul, ol { margin: 0 0 8pt 18pt; padding: 0; } li { margin: 0 0 3pt 0; } table { border-collapse: collapse; width: 100%; margin: 6pt 0 10pt 0; } th { background: #1F4E79; color: #fff; font-weight: bold; } th, td { border: 1px solid #8aa; padding: 4pt 6pt; font-size: 9.5pt; vertical-align: top; } code { font-size: 9.5pt; } p.shot { text-align: center; margin: 8pt 0 2pt 0; } p.shot img { max-width: 100%; max-height: 380pt; } p.imgcap { text-align: center; font-size: 9pt; color: #555; margin: 0 0 10pt 0; } """ def make_cover(title, subtitle, extra=""): doc = fitz.open() page = doc.new_page(width=A4.width, height=A4.height) page.insert_font(fontname="hei", fontfile=FONT_FILE) cx = A4.width / 2 page.draw_rect(fitz.Rect(40, 40, A4.width - 40, A4.height - 40), color=(0.12, 0.31, 0.47), width=1.5) page.draw_rect(fitz.Rect(48, 48, A4.width - 48, A4.height - 48), color=(0.18, 0.46, 0.71), width=0.6) page.insert_text((cx - 120, 180), "安徽大洋 · MES", fontname="hei", fontsize=14, fill=(0.18, 0.46, 0.71)) # wrap title y = 250 for line in title.split("\n"): w = fitz.get_text_length(line, fontname="hei", fontsize=26, fontfile=FONT_FILE) if False else None tw = fitz.Font(fontfile=FONT_FILE).text_length(line, 26) page.insert_text((cx - tw / 2, y), line, fontname="hei", fontsize=26, fill=(0.12, 0.31, 0.47)) y += 40 if subtitle: tw = fitz.Font(fontfile=FONT_FILE).text_length(subtitle, 16) page.insert_text((cx - tw / 2, y + 10), subtitle, fontname="hei", fontsize=16, fill=(0.3, 0.3, 0.3)) y += 40 if extra: tw = fitz.Font(fontfile=FONT_FILE).text_length(extra, 13) page.insert_text((cx - tw / 2, y + 20), extra, fontname="hei", fontsize=13, fill=(0.35, 0.35, 0.35)) tw = fitz.Font(fontfile=FONT_FILE).text_length(DATE, 12) page.insert_text((cx - tw / 2, A4.height - 120), DATE, fontname="hei", fontsize=12, fill=(0.2, 0.2, 0.2)) return doc def html_to_pdf_pages(html_body, header_text): css = CSS html = "%s" % html_body import tempfile, shutil img_tmp = tempfile.mkdtemp(prefix="mesimg_") img_root = str(ROOT / "images") if os.path.isdir(img_root): for dirpath, _, files in os.walk(img_root): for fn in files: if fn.lower().endswith((".png", ".jpg", ".jpeg")): full = os.path.join(dirpath, fn) rel = os.path.relpath(full, img_root).replace("\\", "/") member = rel.replace("/", "__") shutil.copy2(full, os.path.join(img_tmp, member)) arch = fitz.Archive(os.path.dirname(FONT_FILE)) arch.add(img_tmp) story = fitz.Story(html=html, user_css=css, archive=arch) where = fitz.Rect(48, 52, A4.width - 48, A4.height - 48) import tempfile tmp = tempfile.mktemp(suffix=".pdf") writer = fitz.DocumentWriter(tmp) more = 1 while more: dev = writer.begin_page(A4) more, _ = story.place(where) story.draw(dev) writer.end_page() writer.close() try: shutil.rmtree(img_tmp, ignore_errors=True) except Exception: pass body = fitz.open(tmp) try: os.remove(tmp) except Exception: pass font = fitz.Font(fontfile=FONT_FILE) for i, page in enumerate(body): page.insert_font(fontname="hei", fontfile=FONT_FILE) page.insert_text((48, 32), header_text, fontname="hei", fontsize=8, fill=(0.4, 0.4, 0.4)) pn = str(i + 1) tw = font.text_length(pn, 9) page.insert_text((A4.width - 48 - tw, A4.height - 32), pn, fontname="hei", fontsize=9, fill=(0.3, 0.3, 0.3)) page.draw_line(fitz.Point(48, 40), fitz.Point(A4.width - 48, 40), color=(0.75, 0.75, 0.75), width=0.4) page.draw_line(fitz.Point(48, A4.height - 40), fitz.Point(A4.width - 48, A4.height - 40), color=(0.75, 0.75, 0.75), width=0.4) return body def stamp_page_numbers(doc, header_text, start=1): font = fitz.Font(fontfile=FONT_FILE) for i, page in enumerate(doc): # skip cover (first page) header already different page.insert_font(fontname="hei", fontfile=FONT_FILE) if i == 0: continue page.insert_text((48, 32), header_text, fontname="hei", fontsize=8, fill=(0.4, 0.4, 0.4)) pn = str(start + i - 1) tw = font.text_length(pn, 9) page.insert_text((A4.width - 48 - tw, A4.height - 32), pn, fontname="hei", fontsize=9, fill=(0.3, 0.3, 0.3)) page.draw_line(fitz.Point(48, 40), fitz.Point(A4.width - 48, 40), color=(0.75, 0.75, 0.75), width=0.4) page.draw_line(fitz.Point(48, A4.height - 40), fitz.Point(A4.width - 48, A4.height - 40), color=(0.75, 0.75, 0.75), width=0.4) def build_pdf(cover_title, cover_sub, extra, html_body, header, out_path): cover = make_cover(cover_title, cover_sub, extra) body = html_to_pdf_pages(html_body, header) final = fitz.open() final.insert_pdf(cover) final.insert_pdf(body) # restamp body pages only font = fitz.Font(fontfile=FONT_FILE) for i, page in enumerate(final): if i == 0: continue page.insert_font(fontname="hei", fontfile=FONT_FILE) # html_to_pdf_pages already stamped; cover inserted shifts numbers. final.save(out_path, deflate=True, garbage=4) cover.close() body.close() final.close() print("PDF", out_path.name, "pages ok") # ---- docx (reuse simplified converter) ---- from importlib.machinery import SourceFileLoader # inline converter to avoid path issues def set_run_font(run, size=11, bold=False, color=None): run.bold = bold run.font.size = Pt(size) run.font.name = "微软雅黑" rPr = run._element.get_or_add_rPr() rFonts = rPr.find(qn("w:rFonts")) if rFonts is None: rFonts = rPr.makeelement(qn("w:rFonts"), {}) rPr.append(rFonts) rFonts.set(qn("w:eastAsia"), "微软雅黑") rFonts.set(qn("w:ascii"), "微软雅黑") rFonts.set(qn("w:hAnsi"), "微软雅黑") if color: run.font.color.rgb = RGBColor(*color) def add_runs(p, text, size=11, bold=False): parts = re.split(r"(\*\*[^*]+\*\*)", text) for part in parts: if part.startswith("**") and part.endswith("**"): run = p.add_run(part[2:-2]) set_run_font(run, size=size, bold=True) else: run = p.add_run(part) set_run_font(run, size=size, bold=bold) def md_to_docx(md_text, docx_path, heading_shift=0): doc = Document() section = doc.sections[0] section.top_margin = Cm(2) section.bottom_margin = Cm(2) section.left_margin = Cm(2.2) section.right_margin = Cm(2.2) style = doc.styles["Normal"] style.font.name = "微软雅黑" style.font.size = Pt(11) style.element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑") lines = md_text.splitlines() i = 0 while i < len(lines): line = lines[i].rstrip() if not line or line.strip() == "---": i += 1 continue if line.startswith("#"): hashes = len(line) - len(line.lstrip("#")) title = line[hashes:].strip() level = min(max(hashes - 1 + heading_shift, 0), 4) p = doc.add_heading("", level=level) p.clear() run = p.add_run(title) sizes = {0: 18, 1: 16, 2: 14, 3: 12, 4: 12} set_run_font(run, size=sizes.get(level, 12), bold=True, color=(0x1F, 0x4E, 0x79)) i += 1 continue if line.startswith("|"): rows = [] while i < len(lines) and lines[i].startswith("|"): cells = [c.strip() for c in lines[i].strip().strip("|").split("|")] if not is_sep_row(cells): rows.append(cells) i += 1 if rows: cols = max(len(r) for r in rows) table = doc.add_table(rows=len(rows), cols=cols) table.style = "Table Grid" table.alignment = WD_TABLE_ALIGNMENT.CENTER for r_idx, row in enumerate(rows): for c_idx in range(cols): cell = table.cell(r_idx, c_idx) cell.text = "" p = cell.paragraphs[0] val = row[c_idx] if c_idx < len(row) else "" add_runs(p, val, size=10, bold=(r_idx == 0)) continue if line.startswith("- "): p = doc.add_paragraph(style="List Bullet") p.clear() add_runs(p, line[2:].strip()) i += 1 continue if re.match(r"^\d+\.\s", line): p = doc.add_paragraph(style="List Number") p.clear() add_runs(p, re.sub(r"^\d+\.\s+", "", line)) i += 1 continue img = re.match(r"^!\[(.*?)\]\((.*?)\)$", line.strip()) if img: cap, src = img.group(1), img.group(2).replace("/", os.sep) img_path = src if os.path.isabs(src) else str(ROOT / src) p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER if os.path.exists(img_path): run = p.add_run() try: run.add_picture(img_path, width=Cm(14.5)) except Exception: add_runs(p, "[图片无法插入] " + cap) cap_p = doc.add_paragraph() cap_p.alignment = WD_ALIGN_PARAGRAPH.CENTER add_runs(cap_p, cap, size=9) i += 1 continue if line.strip().startswith("*(界面截图"): i += 1 continue p = doc.add_paragraph() add_runs(p, line) i += 1 try: doc.save(str(docx_path)) except PermissionError: alt = str(docx_path).replace(".docx", "_新.docx") doc.save(alt) print("docx locked, wrote", alt) def strip_toc(md): # drop ## 目录 ... until next ## return re.sub(r"\n## 目录\n[\s\S]*?(?=\n## )", "\n", md, count=1) def remap_headings(md, delta): def repl(m): hashes = m.group(1) rest = m.group(2) n = min(len(hashes) + delta, 6) return "#" * n + " " + rest return re.sub(r"^(#{1,6})\s+(.*)$", repl, md, flags=re.M) def build_combined_md(): intro = (ROOT / "_合订版_导言.md").read_text(encoding="utf-8") parts = [intro, ""] mapping = [ (FASCICLES[0][0], "#### 3.1.3 系统管理", 3), (FASCICLES[1][0], "#### 3.1.4 设备管理", 3), (FASCICLES[2][0], "#### 3.1.5 物料管理", 3), (FASCICLES[3][0], "#### 3.1.6 质量管理", 3), (FASCICLES[4][0], "#### 3.1.7 生产管理", 3), (FASCICLES[5][0], "#### 3.1.8 ERP 对接", 3), (FASCICLES[6][0], "#### 3.1.9 生产线管理", 3), (FASCICLES[7][0], "### 3.2 PDA 软件操作说明", 2), (FASCICLES[8][0], "### 3.3 工控机软件操作说明", 2), ] for fname, section_title, delta in mapping: raw = (ROOT / fname).read_text(encoding="utf-8") raw = strip_toc(raw) # drop first h1 raw = re.sub(r"^# .*\n+", "", raw) raw = re.sub(r"^20\d{2} 年.*\n+", "", raw) raw = remap_headings(raw, delta) parts.append(section_title) parts.append("") parts.append(raw.strip()) parts.append("") changelog = (ROOT / "00-修订对照说明.md").read_text(encoding="utf-8") changelog = remap_headings(re.sub(r"^# .*\n+", "## 附录 本次修订对照说明\n\n", changelog, count=1), 0) parts.append(changelog) text = "\n".join(parts) out = ROOT / "大洋交付-操作手册合订版.md" out.write_text(text, encoding="utf-8") return text def fix_toc_numbers(md): """Avoid '4. 常见问题' in TOC being parsed as a new ordered list.""" def repl(m): block = m.group(0) block = re.sub(r"^(\d+)\.\s+", r"\1、", block, flags=re.M) return block return re.sub(r"(## 目录\n)([\s\S]*?)(?=\n## )", lambda m: m.group(1) + re.sub(r"^(\d+)\.\s+", r"\1、", m.group(2), flags=re.M), md, count=1) def main(): import shutil # 分册 PDF + 刷新 docx for fname, short, extra in FASCICLES: md_path = ROOT / fname md = fix_toc_numbers(md_path.read_text(encoding="utf-8")) md_path.write_text(md, encoding="utf-8") stem = fname[:-3] html_body = md_to_html_body(md, skip_first_h1=False) build_pdf( "大洋MES 生产管理系统", "——" + short + "——", extra, html_body, "大洋MES · " + short + " · 分册", ROOT / (stem + ".pdf"), ) md_to_docx(md, ROOT / (stem + ".docx")) # 对照说明 ch = (ROOT / "00-修订对照说明.md").read_text(encoding="utf-8") build_pdf("大洋MES 操作手册", "修订对照说明", "相对 8.28 版本", md_to_html_body(ch), "大洋MES · 修订对照说明", ROOT / "00-修订对照说明.pdf") md_to_docx(ch, ROOT / "00-修订对照说明.docx") combined = build_combined_md() build_pdf( "大洋MES 生产管理系统\n操作手册合订版", "——(MES 后台 + PDA + 工控机)——", "分册合订 · 现行菜单与 PDA 1.0.6", md_to_html_body(combined, skip_first_h1=True), "大洋MES 生产管理系统 · 操作手册合订版", ROOT / "大洋交付-操作手册合订版.pdf", ) md_to_docx(combined, ROOT / "大洋交付-操作手册合订版.docx") fen = ROOT / "分册" he = ROOT / "合订版" fen.mkdir(exist_ok=True) he.mkdir(exist_ok=True) for fname, short, extra in FASCICLES: stem = fname[:-3] for ext in (".pdf", ".docx", ".md"): src = ROOT / (stem + ext) if src.exists(): shutil.copy2(src, fen / src.name) for name in ("大洋交付-操作手册合订版.pdf", "大洋交付-操作手册合订版.docx", "大洋交付-操作手册合订版.md"): src = ROOT / name if src.exists(): shutil.copy2(src, he / name) for name in ("00-修订对照说明.pdf", "00-修订对照说明.docx", "00-修订对照说明.md"): src = ROOT / name if src.exists(): shutil.copy2(src, fen / name) shutil.copy2(src, he / name) chk = ROOT / "_pdf_check.txt" if chk.exists(): chk.unlink() print("done") if __name__ == "__main__": main()