281 lines
9.0 KiB
Python
281 lines
9.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""生成软著程序鉴别材料PDF
|
|
|
|
从项目 src 目录收集源代码,生成前30页 + 后30页的程序鉴别材料PDF。
|
|
自动适配 Windows / macOS / Linux 的中文字体。
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import glob
|
|
import platform
|
|
from reportlab.lib.pagesizes import A4
|
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
from reportlab.lib.units import cm
|
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
|
|
from reportlab.pdfbase import pdfmetrics
|
|
from reportlab.pdfbase.ttfonts import TTFont
|
|
from reportlab.lib.enums import TA_CENTER
|
|
|
|
# ============================================================
|
|
# 配置
|
|
# ============================================================
|
|
|
|
# 项目根目录(脚本所在目录的上级目录)
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
PROJECT_DIR = os.path.dirname(SCRIPT_DIR)
|
|
OUTPUT_PDF = os.path.join(SCRIPT_DIR, "程序鉴别材料_源代码.pdf")
|
|
|
|
# 软件信息
|
|
SOFTWARE_NAME = "数据可视化大屏展示系统V1.0"
|
|
SOFTWARE_VERSION = "V1.0"
|
|
COPYRIGHT_OWNER = "[著作权人姓名]"
|
|
|
|
# 分页参数
|
|
LINES_PER_PAGE = 55 # 每页约55行代码
|
|
PAGES_FRONT = 30 # 前30页
|
|
PAGES_BACK = 30 # 后30页
|
|
|
|
|
|
# ============================================================
|
|
# 字体注册(自动适配多平台)
|
|
# ============================================================
|
|
|
|
def find_chinese_fonts():
|
|
"""在当前操作系统中查找可用的中文字体"""
|
|
system = platform.system()
|
|
|
|
if system == "Windows":
|
|
candidates = [
|
|
("SimSun", "C:/Windows/Fonts/simsun.ttc"),
|
|
("SimHei", "C:/Windows/Fonts/simhei.ttf"),
|
|
("MSYH", "C:/Windows/Fonts/msyh.ttc"),
|
|
]
|
|
elif system == "Darwin": # macOS
|
|
candidates = [
|
|
("PingFang", "/System/Library/Fonts/PingFang.ttc"),
|
|
("STHeiti", "/System/Library/Fonts/STHeiti Medium.ttc"),
|
|
("STSong", "/Library/Fonts/Songti.ttc"),
|
|
]
|
|
else: # Linux
|
|
candidates = [
|
|
("NotoSansCJK", "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"),
|
|
("NotoSansCJK", "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc"),
|
|
("NotoSansCJK-OTF", "/usr/share/fonts/opentype/noto/NotoSansCJKsc-Regular.otf"),
|
|
("WenQuanYi", "/usr/share/fonts/wenquanyi/wqy-zenhei/wqy-zenhei.ttc"),
|
|
("WenQuanYi-Micro", "/usr/share/fonts/wenquanyi/wqy-microhei/wqy-microhei.ttc"),
|
|
("DroidSans", "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf"),
|
|
]
|
|
|
|
# 也可从环境变量覆盖
|
|
env_font = os.environ.get("SOFTCOPY_FONT_PATH")
|
|
env_font_name = os.environ.get("SOFTCOPY_FONT_NAME", "CustomFont")
|
|
if env_font and os.path.exists(env_font):
|
|
candidates.insert(0, (env_font_name, env_font))
|
|
|
|
found = []
|
|
for name, path in candidates:
|
|
if os.path.exists(path):
|
|
found.append((name, path))
|
|
|
|
return found
|
|
|
|
|
|
def register_fonts():
|
|
"""注册中文字体,返回 (body_font, heading_font)"""
|
|
fonts = find_chinese_fonts()
|
|
|
|
if not fonts:
|
|
print("错误: 未找到可用的中文字体!")
|
|
print("请安装中文字体或设置环境变量 SOFTCOPY_FONT_PATH")
|
|
if platform.system() == "Linux":
|
|
print(" Ubuntu/Debian: sudo apt install fonts-noto-cjk")
|
|
sys.exit(1)
|
|
|
|
# 注册第一个找到的字体作为正文字体
|
|
body_name, body_path = fonts[0]
|
|
pdfmetrics.registerFont(TTFont(body_name, body_path))
|
|
print(f"正文字体: {body_name} ({body_path})")
|
|
|
|
# 尝试找一个不同的字体作为标题字体
|
|
heading_name = body_name
|
|
for name, path in fonts[1:]:
|
|
if name != body_name:
|
|
pdfmetrics.registerFont(TTFont(name, path))
|
|
heading_name = name
|
|
print(f"标题字体: {heading_name} ({path})")
|
|
break
|
|
else:
|
|
# 如果只有一个字体,标题也用它
|
|
heading_name = body_name
|
|
|
|
return body_name, heading_name
|
|
|
|
|
|
# ============================================================
|
|
# 源代码收集
|
|
# ============================================================
|
|
|
|
def collect_source_files():
|
|
"""收集项目 src 目录下的所有源代码文件"""
|
|
extensions = ["*.vue", "*.ts", "*.js", "*.css", "*.scss", "*.less"]
|
|
source_files = []
|
|
src_dir = os.path.join(PROJECT_DIR, "src")
|
|
|
|
for ext in extensions:
|
|
pattern = os.path.join(src_dir, "**", ext)
|
|
files = glob.glob(pattern, recursive=True)
|
|
# 排除 node_modules
|
|
files = [f for f in files if "node_modules" not in f]
|
|
source_files.extend(sorted(files))
|
|
|
|
return source_files
|
|
|
|
|
|
def read_all_code(files):
|
|
"""读取所有代码内容,返回行列表"""
|
|
all_lines = []
|
|
for filepath in files:
|
|
try:
|
|
with open(filepath, "r", encoding="utf-8") as f:
|
|
lines = f.readlines()
|
|
rel_path = os.path.relpath(filepath, PROJECT_DIR)
|
|
all_lines.append(f"\n{'=' * 70}")
|
|
all_lines.append(f"文件: {rel_path}")
|
|
all_lines.append(f"{'=' * 70}\n")
|
|
for line in lines:
|
|
all_lines.append(line.rstrip("\n"))
|
|
except Exception as e:
|
|
print(f"警告: 无法读取文件 {filepath}: {e}")
|
|
return all_lines
|
|
|
|
|
|
# ============================================================
|
|
# PDF 生成
|
|
# ============================================================
|
|
|
|
def generate_pdf(lines_front, lines_back, body_font, heading_font):
|
|
"""生成程序鉴别材料PDF"""
|
|
doc = SimpleDocTemplate(
|
|
OUTPUT_PDF,
|
|
pagesize=A4,
|
|
rightMargin=2 * cm,
|
|
leftMargin=2 * cm,
|
|
topMargin=2 * cm,
|
|
bottomMargin=2 * cm,
|
|
)
|
|
|
|
styles = getSampleStyleSheet()
|
|
|
|
# 代码样式
|
|
code_style = ParagraphStyle(
|
|
"CodeStyle",
|
|
parent=styles["Normal"],
|
|
fontName=body_font,
|
|
fontSize=8,
|
|
leading=12,
|
|
leftIndent=0,
|
|
spaceAfter=0,
|
|
wordWrap="CJK",
|
|
)
|
|
|
|
# 标题样式
|
|
title_style = ParagraphStyle(
|
|
"TitleStyle",
|
|
parent=styles["Title"],
|
|
fontName=heading_font,
|
|
fontSize=16,
|
|
alignment=TA_CENTER,
|
|
spaceAfter=20,
|
|
)
|
|
|
|
story = []
|
|
|
|
# 封面页
|
|
story.append(Paragraph("程序鉴别材料", title_style))
|
|
story.append(Paragraph("(源代码)", title_style))
|
|
story.append(Spacer(1, 30))
|
|
story.append(Paragraph(f"软件全称:{SOFTWARE_NAME}", code_style))
|
|
story.append(Paragraph(f"版本号:{SOFTWARE_VERSION}", code_style))
|
|
story.append(Paragraph(f"著作权人:{COPYRIGHT_OWNER}", code_style))
|
|
story.append(PageBreak())
|
|
|
|
# 前30页
|
|
story.append(Paragraph("—— 源代码前30页 ——", title_style))
|
|
story.append(Spacer(1, 10))
|
|
|
|
for i, line in enumerate(lines_front):
|
|
if i > 0 and i % LINES_PER_PAGE == 0:
|
|
story.append(PageBreak())
|
|
|
|
# 转义HTML特殊字符
|
|
safe_line = line.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
# 处理空格
|
|
safe_line = safe_line.replace(" ", " ")
|
|
|
|
p = Paragraph(safe_line, code_style)
|
|
story.append(p)
|
|
|
|
story.append(PageBreak())
|
|
|
|
# 后30页
|
|
story.append(Paragraph("—— 源代码后30页 ——", title_style))
|
|
story.append(Spacer(1, 10))
|
|
|
|
for i, line in enumerate(lines_back):
|
|
if i > 0 and i % LINES_PER_PAGE == 0:
|
|
story.append(PageBreak())
|
|
|
|
safe_line = line.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
safe_line = safe_line.replace(" ", " ")
|
|
|
|
p = Paragraph(safe_line, code_style)
|
|
story.append(p)
|
|
|
|
doc.build(story)
|
|
print(f"PDF已生成: {OUTPUT_PDF}")
|
|
|
|
|
|
# ============================================================
|
|
# 主流程
|
|
# ============================================================
|
|
|
|
if __name__ == "__main__":
|
|
print("=" * 60)
|
|
print("程序鉴别材料PDF生成工具")
|
|
print(f"项目目录: {PROJECT_DIR}")
|
|
print(f"输出文件: {OUTPUT_PDF}")
|
|
print("=" * 60)
|
|
|
|
# 注册字体
|
|
body_font, heading_font = register_fonts()
|
|
|
|
# 收集源代码文件
|
|
print("\n正在收集源代码文件...")
|
|
files = collect_source_files()
|
|
print(f"找到 {len(files)} 个源代码文件")
|
|
|
|
# 读取代码内容
|
|
print("正在读取代码内容...")
|
|
all_lines = read_all_code(files)
|
|
print(f"总代码行数: {len(all_lines)}")
|
|
|
|
# 计算前后30页
|
|
total_lines_needed = (PAGES_FRONT + PAGES_BACK) * LINES_PER_PAGE
|
|
|
|
if len(all_lines) <= total_lines_needed:
|
|
# 代码不足60页,全部提交
|
|
lines_front = all_lines[: len(all_lines) // 2]
|
|
lines_back = all_lines[len(all_lines) // 2 :]
|
|
else:
|
|
lines_front = all_lines[: PAGES_FRONT * LINES_PER_PAGE]
|
|
lines_back = all_lines[-PAGES_BACK * LINES_PER_PAGE :]
|
|
|
|
print(f"前30页: {len(lines_front)} 行")
|
|
print(f"后30页: {len(lines_back)} 行")
|
|
|
|
# 生成PDF
|
|
print("\n正在生成PDF...")
|
|
generate_pdf(lines_front, lines_back, body_font, heading_font)
|
|
print("完成!")
|