| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496 |
- # -*- 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"<b>\1</b>", text)
- text = re.sub(r"`([^`]+)`", r"<code>\1</code>", 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("<h1>%s</h1>" % md_inline_html(title))
- i += 1
- continue
- m = re.match(r"^(#{2,6})\s+(.*)$", line)
- if m:
- lv = len(m.group(1))
- out.append("<h%d>%s</h%d>" % (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("<table>")
- for ri, row in enumerate(rows):
- out.append("<tr>")
- tag = "th" if ri == 0 else "td"
- for c in row:
- out.append("<%s>%s</%s>" % (tag, md_inline_html(c), tag))
- out.append("</tr>")
- out.append("</table>")
- continue
- if line.startswith("- "):
- out.append("<ul>")
- while i < len(lines) and lines[i].startswith("- "):
- out.append("<li>%s</li>" % md_inline_html(lines[i][2:].strip()))
- i += 1
- out.append("</ul>")
- continue
- if re.match(r"^\d+\.\s", line):
- out.append("<ol>")
- while i < len(lines) and re.match(r"^\d+\.\s", lines[i]):
- out.append("<li>%s</li>" % md_inline_html(re.sub(r"^\d+\.\s+", "", lines[i])))
- i += 1
- out.append("</ol>")
- 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('<p class="shot"><img src="%s" alt="%s"/></p>' % (esc(member), esc(cap)))
- out.append('<p class="imgcap">%s</p>' % md_inline_html(cap))
- i += 1
- continue
- if line.strip().startswith("*(界面截图") and "为准" in line:
- i += 1
- continue
- out.append("<p>%s</p>" % 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 = "<html><body>%s</body></html>" % 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()
|