自建 PDF 工具箱:Stirling-PDF 部署与中文 OCR 配置

admin 开源软件 2026-10-11 7 0

有个朋友上个月把一份带身份证复印件的租房合同传到了某个免费在线 PDF 站,转完才发现要登录才能下载,登录还得绑手机号。文件传上去之后躺在谁的硬盘上、存多久、有没有人打开看过,他一个都答不上来。这类需求本身简单到不能再简单:合并两个文件、拆出一页、压小一点、加个打开密码。可每一次操作的前提都是先把文件交出去。另一条路是买正版订阅,一年几百块,而你一年大概只用四五次。

自己搭一个 PDF 工具箱就卡在这两者中间。Stirling-PDF 跑在你自己的机器上,浏览器打开就是界面,转换全部在本地完成,文件不出门。

它能替你做的事

界面左边是一列功能,右边是操作区,基本都是「拖文件进来、选参数、点按钮」的流程。日常能想到的 PDF 操作,八成都能在这里找到入口:

项目内容
合并与拆分多份合成一份,或按页范围拆成多个文件
压缩降低内嵌图片分辨率并重新编码,体积常能砍掉一半以上
水印文字或图片水印,可调位置、透明度、字号、是否平铺
密码给文件加打开密码,或去掉已知密码
格式转换PDF 与 Word、表格、图片之间互转
图片转 PDF多张图排成一个文件,可调顺序、页边距与方向
OCR把扫描件里的文字识别成可搜索的 PDF 或纯文本
页面操作旋转、重排、删除页、插入空白页、裁剪、叠加
签名手写签名或图片签名盖到指定位置
元数据查看并清除作者、创建工具、修改时间等隐藏信息

冷门一点的也在,比如提取内嵌图片、拆分书签、比较两个版本的差异。用得最多的是合并、压缩和加密码这三样,它们恰好也是各类在线站最爱让你登录、最爱给你限速的功能。

用 Docker 把它跑起来

最短的一条命令

mkdir -p /opt/stirling/configs /opt/stirling/tessdata /opt/stirling/output

docker run -d \
  --name stirling-pdf \
  --restart unless-stopped \
  -p 8080:8080 \
  -v /opt/stirling/configs:/configs \
  -v /opt/stirling/tessdata:/tessdata \
  -v /opt/stirling/output:/output \
  -e TZ=Asia/Shanghai \
  -e DOCKER_ENABLE_SECURITY=true \
  -e SECURITY_INITIALLOGIN_USERNAME=admin \
  -e SECURITY_INITIALLOGIN_PASSWORD=换成一串足够长的随机密码 \
  -e SYSTEM_MAXFILESIZEINMB=200 \
  -e SYSTEM_MAXREQUESTFILESIZEINMB=200 \
  -e JAVA_TOOL_OPTIONS=-Xmx2g \
  --memory=4g \
  stirlingtools/stirling-pdf

三个挂载点各有分工,别图省事只挂一个:

  • /configs 保存你在界面里改过的设置、自定义模板、保存过的水印图片,不挂的话容器重建一次就全丢
  • /tessdata OCR 语言包目录,后面单独讲
  • /output 处理结果的落盘目录。界面里可以直接点下载,但大文件走浏览器下载容易断,落盘更稳

环境变量怎么理解

项目内容
TZ容器时区,影响日志时间和输出文件的命名
DOCKER_ENABLE_SECURITY设为 true 才启用登录与用户体系,默认是敞开的
SECURITY_INITIALLOGIN_USERNAME / PASSWORD首次启动时创建的管理员账号,之后可在界面里改
SYSTEM_MAXFILESIZEINMB允许上传的单个文件上限,单位 MB
SYSTEM_MAXREQUESTFILESIZEINMB整个请求体的上限,批量上传时它更容易先被撞到
JAVA_TOOL_OPTIONS给运行时传参,主要用来显式指定最大堆内存

写成 compose 更好维护

services:
  stirling-pdf:
    image: stirlingtools/stirling-pdf
    container_name: stirling-pdf
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - ./configs:/configs
      - ./tessdata:/tessdata
      - ./output:/output
    environment:
      TZ: Asia/Shanghai
      DOCKER_ENABLE_SECURITY: "true"
      SECURITY_INITIALLOGIN_USERNAME: admin
      SECURITY_INITIALLOGIN_PASSWORD: 换成一串足够长的随机密码
      SYSTEM_MAXFILESIZEINMB: "200"
      SYSTEM_MAXREQUESTFILESIZEINMB: "200"
      JAVA_TOOL_OPTIONS: "-Xmx2g"
    mem_limit: 4g

中文 OCR 不是开箱即用

OCR 引擎默认只带英文的语言数据。你直接把一张中文扫描件扔进去识别,结果常常是满屏乱码,或者干脆给你一份空白的文本,很多人到这一步就以为功能坏了,其实是缺语言包。

需要的文件有三个:chi_sim.traineddata 是简体中文,chi_tra.traineddata 是繁体中文,osd.traineddata 负责方向检测,扫描件摆歪了靠它摆正。它们来自 OCR 引擎的公开语言数据目录,简体包几十兆,下载完丢进挂载出来的 tessdata 目录:

ls -lh /opt/stirling/tessdata
docker restart stirling-pdf

放完必须重启容器,它只在启动时读这个目录。识别时记得在界面上把语言选成中文,或者直接写 chi_sim;中英混排的文件写 chi_sim+eng。

另外说句实话:OCR 准确率和原图质量强相关。三百 dpi 的平整扫描件能到九成五以上,手机斜着拍、有阴影、有折痕的照片就难说了,识别完自己通读校对一遍是常态,别指望一键全对。

给它加一道门

默认情况下这个服务是敞开的,谁扫到 8080 端口谁就能用。它内部有些功能能读服务器上的文件、能发起网络请求,直接挂在公网上不合适。把 DOCKER_ENABLE_SECURITY 设为 true 会启用用户体系,首次启动时用初始登录变量建出来的账号就是管理员。登进去之后可以在设置里建普通用户、改密码、关掉注册。

只在局域网里自己用,也可以不开登录,但一定要保证它没有被暴露到公网:别在路由器上给 8080 做端口映射,云服务器的安全组里只放行必要的来源 IP。

大文件、内存和运行时参数

PDF 处理是内存活。压一本两百页的图册、把整份扫描件转成图片,中间态在内存里能涨到几个 G。这里有两个限制必须一起调:

  • 容器内存上限。--memory 或者 compose 里的 mem_limit,别让它把宿主机吃干
  • 运行时堆上限。用 JAVA_TOOL_OPTIONS=-Xmx2g 这种参数显式指定,别让它自己猜

两条要配套:容器给 4G,堆给 2G 到 3G,剩下的留给堆外内存和系统缓存。只设堆不设容器上限,内存一涨宿主就跟着抖;只设容器上限不设堆,运行时按宿主机总内存估算堆大小,然后被内核 OOM 直接杀掉,日志里连个异常都没有,只有退出码 137。

反代之后上传失败,多半是 413

把 8080 藏到 Nginx 后面是标准做法,但 Nginx 默认只允许 1M 的请求体,PDF 一传就 413。转换类接口本身也慢,默认 60 秒的读超时会把长任务掐断。

server {
    listen 443 ssl;
    server_name pdf.example.com;

    ssl_certificate     /etc/nginx/certs/fullchain.pem;
    ssl_certificate_key /etc/nginx/certs/privkey.pem;

    client_max_body_size 200m;
    proxy_read_timeout   600s;
    proxy_send_timeout   600s;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

client_max_body_size 要和容器里的文件大小上限对齐。前端放 200M、后端只认 50M,用户看到的还是失败。改完先 nginx -t 再 reload,别把配置写错了还不知道。

几个真会踩到的坑

  1. 中文识别成乱码。九成是没放简体语言包,或者放了没重启容器,或者界面上的语言那栏还留着英文。
  2. 扫描件识别不出文字。扫描件本质上就是一页图片,里面的文字对任何软件都不存在,必须先过 OCR。反过来,如果你只是想在 PDF 里搜索,先确认这份文件是电子版生成的。
  3. 大文件转换卡到超时。反代的读超时和容器内的处理时间两边都得放宽,同时把输出目录挂出来,别依赖浏览器一直挂着下载。
  4. 容器莫名其妙退出。看退出码,137 基本就是内存不够被内核杀掉,回去调堆参数和容器上限。
  5. 页面能开、一上传就报错。先看反代那层的请求体上限,再看容器里的文件大小上限,最后才怀疑文件本身有问题。
  6. 处理完的文件在哪。界面里点下载最省事;如果你去输出目录里找,注意文件名是按任务随机生成的,不是原始文件名。

适合你,不适合你

适合:经常要处理合同、发票、课件、扫描件的个人和小团队;对文件流向敏感,不愿意把身份证、病历、合同传给别人的服务器;手里有一台常年开着的 NAS 或小主机。

不适合:一天要批量处理几千份文件的流水线场景,它是交互式工具不是批处理系统;也不想为这点事维护一台机器的人,买个订阅或者用系统自带功能更省心。

最后把隐私这件事说准确一点:自建不等于绝对安全。它的意义是文件只在你自己的机器上被处理,风险从「别人怎么管」变成了「你怎么管」。你自己有没有做快照、输出目录多久清一次、端口有没有漏在公网上,这些就是你接手过来的责任。