在日常写文档、生成报告或导出说明书时,我们经常会遇到一个看似简单但实际并不稳定的需求:在 PDF 文件的左上角添加 Logo、项目标识或印章。
一开始,我尝试使用 Typora 的 PDF 导出功能解决这个问题。Typora 支持在导出 PDF 时向生成的 HTML 中追加自定义内容,也就是在 PDF 偏好设置面板中的 “添加额外内容 / Append Extra Content” 功能。理论上,我们可以在这里插入 HTML、CSS 或 JavaScript,从而在导出的 PDF 中添加自定义封面、页脚、Logo 等元素。
但是在实际使用过程中,我发现通过 Typora 的 HTML 附加内容来添加 Logo 并不稳定。最终,我改用 Python 对已经导出的 PDF 进行二次处理,直接在 PDF 页面上添加 Logo。这个方案更加清晰、稳定,也不会受到 Typora 主题样式、页眉页脚或正文排版的影响。
本文记录这个问题的解决过程,并给出最终可用的完整代码。
一、问题背景
我的需求很简单:在使用 Typora 导出 PDF 后,希望在 PDF 的左上角添加一个 Logo。
理想效果是:
- Logo 位于 PDF 页面左上角;
- 可以控制 Logo 的位置和大小;
- 可以选择只在第一页添加,也可以添加到所有页面;
- 不受 Typora 主题样式影响;
- 不出现图片阴影、白底、圆角等额外样式;
- 导出的 PDF 仍然保持原有正文内容不变。
最开始,我尝试使用 Typora 的 Append Extra Content 功能,通过 HTML 插入图片。例如使用 <img> 标签或者将图片转为 Base64 后嵌入页面中。
但是很快就遇到了几个问题。
二、Typora 自定义 HTML 方案的问题
Typora 的 PDF 导出本质上是先将 Markdown 渲染为 HTML,再将 HTML 输出为 PDF。因此,它提供的“添加额外内容”功能,本质上是将自定义 HTML 内容追加到页面的 <body> 中。
这意味着插入的 Logo 并不是真正添加到了 PDF 页眉或页面画布中,而是作为 HTML 正文的一部分参与渲染。
实际测试中遇到的问题主要有三个。
首先,Logo 会出现在正文最后面,而不是页面左上角。这是因为追加的 <img> 标签默认会被当作普通正文图片处理。
其次,Logo 会继承 Typora 当前主题中对图片的默认样式。例如某些主题会给图片添加阴影、白色背景、圆角、边框或默认外边距。这样一来,Logo 看起来就像普通插图,而不是一个干净的页面标识。
再次,即使使用 position: fixed 或 position: absolute 强制定位,Logo 也可能只能出现在正文渲染区域内,无法稳定进入 PDF 真正的页眉或页边距区域。尤其当 PDF 中设置了页眉、页脚或较大的页边距时,Logo 往上移动还可能被裁切或覆盖。
后来我尝试改用 div + background-image 的方式,避免 <img> 被主题样式影响。这个方法可以解决图片阴影的问题,但仍然无法完全解决 PDF 顶部区域被 Typora 渲染规则限制的问题。
因此,对于“给 PDF 添加 Logo / 印章”这种需求,更稳定的做法不是在 Typora 导出时修改 HTML,而是:先正常导出 PDF,再使用 Python 对 PDF 文件进行后处理。
三、为什么选择 Python 处理 PDF
使用 Python 后处理 PDF 有几个明显优势。
第一,Logo 是直接添加到 PDF 页面上的,不再受 Typora 主题 CSS 影响。
第二,可以精确控制 Logo 的位置、宽度和高度。
第三,可以灵活选择添加范围:只添加第一页,或者添加到全部页面。
第四,处理逻辑清晰,后续也方便扩展。例如可以进一步添加水印、页眉、页脚、二维码、签名图片等。
这里使用的库是 PyMuPDF,在代码中通过 import fitz 引入。它可以读取 PDF 页面,并向指定坐标区域插入图片。
安装依赖:
pip install pymupdf
四、最终实现代码
下面是最终整理好的完整代码。它支持两种模式:
mode = "first":只在第一页添加 Logo;mode = "all":在所有页面添加 Logo。
代码中 Logo 的位置和大小使用毫米作为单位,更符合日常排版习惯。
import fitz
from pathlib import Path
# =========================
# 基础配置
# =========================
input_pdf = "input.pdf" # 原始 PDF
logo_path = "logo.png" # Logo 图片
output_pdf = "output_with_logo.pdf" # 输出 PDF
# 添加模式:
# "first" = 只添加到第一页
# "all" = 添加到全部页面
mode = "all"
# Logo 位置和大小,单位:mm
left_mm = 8
top_mm = 8
width_mm = 28
height_mm = 12
# 是否覆盖在正文上方
# True = Logo 显示在最上层
# False = Logo 显示在底层
overlay = True
# =========================
# 工具函数
# =========================
def mm_to_pt(mm: float) -> float:
"""
PDF 坐标单位是 pt。
1 mm ≈ 2.83465 pt
"""
return mm * 2.83465
def add_logo_to_page(page: fitz.Page, logo_file: str):
"""
向单页 PDF 添加 Logo。
"""
left = mm_to_pt(left_mm)
top = mm_to_pt(top_mm)
width = mm_to_pt(width_mm)
height = mm_to_pt(height_mm)
rect = fitz.Rect(
left,
top,
left + width,
top + height
)
page.insert_image(
rect,
filename=logo_file,
keep_proportion=True,
overlay=overlay
)
# =========================
# 主程序
# =========================
def main():
input_path = Path(input_pdf)
logo_file = Path(logo_path)
if not input_path.exists():
raise FileNotFoundError(f"找不到输入 PDF:{input_path}")
if not logo_file.exists():
raise FileNotFoundError(f"找不到 Logo 图片:{logo_file}")
if mode not in ["first", "all"]:
raise ValueError('mode 只能设置为 "first" 或 "all"')
doc = fitz.open(input_path)
if len(doc) == 0:
raise ValueError("PDF 页数为 0,无法处理")
if mode == "first":
# 只添加第一页
add_logo_to_page(doc[0], str(logo_file))
elif mode == "all":
# 添加到全部页面
for page in doc:
add_logo_to_page(page, str(logo_file))
doc.save(
output_pdf,
garbage=4,
deflate=True
)
doc.close()
print(f"处理完成:{output_pdf}")
if __name__ == "__main__":
main()
五、代码使用说明
使用前,需要准备三个文件或配置:
input_pdf = "input.pdf"
logo_path = "logo.png"
output_pdf = "output_with_logo.pdf"
其中:
input.pdf是原始 PDF 文件;logo.png是需要添加的 Logo 图片;output_with_logo.pdf是处理后的输出文件。
建议将 Python 脚本、原始 PDF 和 Logo 图片放在同一个文件夹下,这样可以直接使用相对路径,减少路径错误。
运行脚本:
python add_logo_to_pdf.py
运行完成后,会生成一个新的 PDF 文件:output_with_logo.pdf
六、添加到第一页或全部页面
代码中通过 mode 控制 Logo 的添加范围。
只添加到第一页:
mode = "first"
添加到全部页面:
mode = "all"
这个设计比写两个脚本更方便。日常使用时,只需要改一个参数即可。
例如,封面页需要公司 Logo,但正文页不需要,可以使用first。
如果是正式报告、合同、说明文档,希望每一页都带有机构标识,可以使用all。
七、调整 Logo 的位置和大小
Logo 的位置由下面两个参数控制:
left_mm = 8
top_mm = 8
它们表示 Logo 距离页面左边和上边的距离,单位是毫米。
Logo 的尺寸由下面两个参数控制:
width_mm = 28
height_mm = 12
其中:
width_mm控制 Logo 显示宽度;height_mm控制 Logo 显示高度。
代码中使用了:
keep_proportion=True
因此图片会尽量保持原始比例,不会被强制拉伸变形。
例如,想让 Logo 更靠近左上角,可以改为:
left_mm = 5
top_mm = 5
想让 Logo 更小,可以改为:
width_mm = 22
height_mm = 10
八、关于 overlay 参数
代码中还有一个参数:
overlay = True
它表示 Logo 是否覆盖在原 PDF 内容上方。
当设置为:
overlay = True
Logo 会显示在最上层,适合添加印章、Logo、水印等。
当设置为:
overlay = False
Logo 会被添加到底层。如果 PDF 原本的页面内容覆盖了这个区域,Logo 可能会被遮住。
一般情况下,添加 Logo 推荐使用:
overlay = True
九、这个方案适合哪些场景
这个脚本适合以下场景:
- 给 Typora 导出的 PDF 添加 Logo;
- 给报告、说明书、合同、论文草稿添加机构标识;
- 给 PDF 首页添加项目封面标识;
- 给所有页面统一添加公司 Logo;
- 批量处理 PDF 前的基础脚本改造;
- 替代不稳定的 HTML / CSS PDF 导出方案。
相比在 Typora 里强行插入 HTML,这种方式更接近“PDF 后期盖章”。它不依赖 Markdown 编辑器,也不依赖主题样式。只要 PDF 已经生成,就可以直接处理。
十、总结
这次问题的解决过程也说明了一个很实用的思路:当 Markdown 编辑器或导出工具无法稳定控制 PDF 细节时,不一定要继续在导出配置里反复调试。对于 Logo、印章、水印这类固定页面元素,直接对 PDF 文件进行后处理,往往更加稳定。
Typora 的自定义 HTML 功能适合做一些简单的导出增强,比如添加封面、自定义说明文字或调整部分样式。但对于“精确控制 PDF 页面坐标”的需求,Python 的 PDF 处理库会更合适。
最终,这个脚本实现了三个核心目标:
- 可以精确控制 Logo 的位置和大小;
- 可以选择只添加第一页或添加全部页面;
- 不受 Typora 主题、页眉页脚和正文样式影响。
对于日常写作、文档发布和报告生成来说,这个方案足够轻量,也足够稳定。
