"""生成三类测试 PDF:文本版 / 影印版(扫描) / 混合版,用于验证解析效果。""" import os, sys import pymupdf as fitz from PIL import Image, ImageDraw, ImageFont HERE = os.path.dirname(os.path.abspath(__file__)) OUT = os.path.join(HERE, "..", "tests", "pdfs") os.makedirs(OUT, exist_ok=True) def font_path(): for p in [ "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", ]: if os.path.exists(p): return p return None def draw_chart(path, title="产品季度销量趋势"): """生成一张图表图片(折线图)。""" img = Image.new("RGB", (700, 380), "white") d = ImageDraw.Draw(img) f = None try: f = ImageFont.truetype(font_path(), 20) except Exception: f = ImageFont.load_default() d.text((250, 10), title, fill="black", font=f) d.rectangle([60, 50, 660, 320], outline="black", width=2) # 坐标轴刻度 xs = [80, 190, 300, 410, 520, 630] labels = ["Q1", "Q2", "Q3", "Q4", "Q5", "Q6"] vals = [80, 150, 120, 220, 190, 300] for x, lb in zip(xs, labels): d.text((x - 15, 325), lb, fill="black", font=f) d.line([x, 315, x, 55], fill="lightgray", width=1) pts = [] for x, v in zip(xs, vals): y = 315 - v d.ellipse([x - 4, y - 4, x + 4, y + 4], fill="blue") pts.append((x, y)) d.line(pts, fill="blue", width=3) for (x, y), v in zip(pts, vals): d.text((x - 10, y - 28), str(v), fill="black", font=f) img.save(path) def draw_photo(path): """生成一张风景照片风格的图(渐变+山形)。""" img = Image.new("RGB", (600, 350), (135, 206, 250)) d = ImageDraw.Draw(img) d.rectangle([0, 200, 600, 350], fill=(34, 139, 34)) # 草地 d.polygon([(150, 200), (300, 60), (450, 200)], fill=(105, 105, 105)) # 山 d.polygon([(350, 200), (520, 40), (600, 200)], fill=(139, 139, 139)) d.ellipse([80, 50, 180, 150], fill=(255, 255, 0)) # 太阳 try: f = ImageFont.truetype(font_path(), 22) d.text((180, 300), "山间风景图", fill="white", font=f) except Exception: pass img.save(path) def make_text_pdf(path): """文本版 PDF:标题/段落/列表/表格 + 内嵌图表。""" doc = fitz.open() page = doc.new_page(width=595, height=842) # A4 font = "china-s" y = 60 page.insert_text((72, y), "智能营销系统产品白皮书", fontsize=22, fontname=font) y += 40 page.insert_text((72, y), "版本 1.0 | 2025 年 8 月", fontsize=12, fontname=font) y += 40 page.insert_text((72, y), "一、系统概述", fontsize=16, fontname=font) y += 28 paras = [ "本白皮书介绍智能营销系统(SmartMarketing Pro)的整体架构、核心能力与商业价值。系统基于大数据与机器学习技术,为企业在获客、转化、留存三个环节提供端到端的自动化营销能力。", "系统当前已服务超过 1200 家企业客户,覆盖零售、金融、教育、医疗等多个行业。2025 年第二季度整体营收同比增长 32%,续费率达到 86%。", ] for p in paras: page.insert_textbox(fitz.Rect(72, y, 523, y + 60), p, fontsize=11, fontname=font, align=fitz.TEXT_ALIGN_LEFT) y += 65 page.insert_text((72, y), "二、核心功能模块", fontsize=16, fontname=font) y += 28 items = [ "1. 客户画像:基于行为数据构建 360 度用户画像,支持千人千面推荐;", "2. 智能触达:覆盖短信、邮件、站内信等多渠道,自动选择最优触达时间;", "3. 效果分析:实时 ROI 分析,支持 A/B 实验与归因模型;", "4. 自动化流程:可视化编排营销旅程,支持条件分支与延迟节点。", ] for it in items: page.insert_text((90, y), it, fontsize=11, fontname=font) y += 22 y += 10 page.insert_text((72, y), "三、产品版本对比", fontsize=16, fontname=font) y += 30 # 表格(用文本对齐绘制) cols = [(72, 200), (200, 90), (290, 90), (380, 90), (470, 60)] headers = ["版本", "用户数", "自动化", "AI 洞察", "价格"] rows = [ ["基础版", "≤500", "基础流程", "无", "¥9,999/年"], ["专业版", "≤5000", "全部流程", "基础", "¥39,999/年"], ["企业版", "不限", "全部流程", "高级", "¥99,999/年"], ] def draw_row(cy, cells, bold=False): for (x, w), c in zip(cols, cells): page.insert_text((x + 5, cy + 8), c, fontsize=10, fontname=font) return cy # 表头下划线 page.draw_line((72, y + 20), (530, y + 20), color=(0, 0, 0)) y += 24 y = draw_row(y, headers) page.draw_line((72, y + 8), (530, y + 8), color=(0, 0, 0)) for r in rows: y += 22 y = draw_row(y, r) page.draw_line((72, y + 8), (530, y + 8), color=(0.8, 0.8, 0.8)) y += 30 # 插入内嵌图表图片 chart = os.path.join(OUT, "_chart.png") draw_chart(chart) page.insert_image(fitz.Rect(120, y - 14, 475, y + 150), filename=chart) y += 178 page.insert_text((72, y), "图 1:产品季度销量趋势(2024Q1-2025Q2)", fontsize=10, fontname=font) y += 28 page.insert_text((72, y), "四、总结", fontsize=16, fontname=font) y += 28 page.insert_textbox(fitz.Rect(72, y, 523, y + 80), "智能营销系统通过数据驱动的方式显著提升了企业的营销效率。未来将持续引入大模型能力,提供更智能的营销策略生成与自然语言交互体验。", fontsize=11, fontname=font) doc.save(path) print("text pdf ->", path) def make_scanned_pdf(path, pages=3): """影印版 PDF:把排版好的页面渲染成图片,模拟扫描件(无文本层)。""" # 先用文本版生成一页,再渲染成图 tmp = os.path.join(OUT, "_tmp_text.pdf") make_text_pdf(tmp) src = fitz.open(tmp) page0 = src[0] pix = page0.get_pixmap(matrix=fitz.Matrix(2.0, 2.0)) img_path = os.path.join(OUT, "_scan_src.png") pix.save(img_path) doc = fitz.open() for i in range(pages): page = doc.new_page(width=595, height=842) if i == 0: img = img_path else: # 后续页用纯文字图片 img2 = os.path.join(OUT, f"_scan_text_{i}.png") im = Image.new("RGB", (1190, 1684), "white") d = ImageDraw.Draw(im) try: f = ImageFont.truetype(font_path(), 34) except Exception: f = ImageFont.load_default() lines = [ f"影印版第 {i} 页", "", "本页内容说明:影印版 PDF 没有可选择的文本层,", "所有内容都以图片形式存在。需要借助 OCR 或多模态大模型", "才能把内容准确还原为可编辑的文字。", "", "表格示例:", "科目 数量 单价 金额", "笔记本 100 30 3000", "显示器 20 1200 24000", "键盘 50 100 5000", ] yy = 80 for ln in lines: d.text((60, yy), ln, fill="black", font=f) yy += 60 im.save(img2) img = img2 page.insert_image(fitz.Rect(0, 0, 595, 842), filename=img) doc.save(path) print("scanned pdf ->", path) def make_mixed_pdf(path): """混合版 PDF:文本页 + 影印图片页 + 含内嵌图的文本页。""" doc = fitz.open() font = "china-s" # 第 1 页:纯文本 p = doc.new_page(width=595, height=842) p.insert_text((72, 60), "混合文档示例:目录页", fontsize=20, fontname=font) p.insert_text((72, 100), "本页为可复制文本。下一页为扫描图片页,再下一页为带图表页面。", fontsize=12, fontname=font) p.insert_text((72, 130), "1. 项目背景", fontsize=12, fontname=font) p.insert_text((72, 152), "2. 数据概览", fontsize=12, fontname=font) p.insert_text((72, 174), "3. 实施方案", fontsize=12, fontname=font) # 第 2 页:扫描图片页(照片 + 文字合成一张扫描图) p2 = doc.new_page(width=595, height=842) photo = os.path.join(OUT, "_photo.png") draw_photo(photo) # 合成:上部照片 + 下部说明文字 → 模拟真实扫描页 page_img = Image.new("RGB", (1190, 1684), "white") photo_im = Image.open(photo) photo_im = photo_im.resize((1190, 690)) page_img.paste(photo_im, (0, 0)) d = ImageDraw.Draw(page_img) try: f = ImageFont.truetype(font_path(), 34) except Exception: f = ImageFont.load_default() d.text((80, 750), "扫描页说明:这张页面是扫描件,文字和图片融为一体,", fill="black", font=f) d.text((80, 820), "只能通过图像识别还原内容。下方还有一段正文。", fill="black", font=f) lines = [ "正文段落:本扫描页展示了项目现场的照片。", "项目位于山区,风景优美,施工进度正常。", "更多细节请参见后续章节。", ] yy = 950 for ln in lines: d.text((80, yy), ln, fill="black", font=f) yy += 60 page_img_path = os.path.join(OUT, "_mix_scan.png") page_img.save(page_img_path) p2.insert_image(fitz.Rect(0, 0, 595, 842), filename=page_img_path) # 第 3 页:文本 + 内嵌表格图 + 段落 p3 = doc.new_page(width=595, height=842) p3.insert_text((72, 60), "混合文档示例:数据概览页", fontsize=18, fontname=font) p3.insert_text((72, 100), "下表展示了各区域销售业绩,表格以图片形式嵌入:", fontsize=11, fontname=font) table_img = os.path.join(OUT, "_table.png") timg = Image.new("RGB", (900, 300), "white") td = ImageDraw.Draw(timg) try: tf = ImageFont.truetype(font_path(), 26) except Exception: tf = ImageFont.load_default() td.text((20, 20), "区域 | 销售额(万) | 目标达成率", fill="black", font=tf) td.text((20, 80), "华东 | 5200 | 108%", fill="black", font=tf) td.text((20, 140), "华南 | 3800 | 96%", fill="black", font=tf) td.text((20, 200), "华北 | 4100 | 102%", fill="black", font=tf) timg.save(table_img) p3.insert_image(fitz.Rect(90, 130, 505, 230), filename=table_img) p3.insert_text((72, 280), "结论:华东区表现最佳,华南区需加强跟进。", fontsize=11, fontname=font) doc.save(path) print("mixed pdf ->", path) if __name__ == "__main__": make_text_pdf(os.path.join(OUT, "test_text.pdf")) make_scanned_pdf(os.path.join(OUT, "test_scanned.pdf"), pages=2) make_mixed_pdf(os.path.join(OUT, "test_mixed.pdf")) print("ALL DONE ->", OUT)