跳转到内容

使用 Gotenberg 将 Office 文档转换为 PDF

Gotenberg 桥接会将一份 Office 文档转换为 PDF。它会通过 HTTPS 将文档发送到 Gotenberg 微服务,并返回 PDF 字节。先使用不可变的 GotenbergConfig 描述服务,将一个 PSR-18 客户端与 PSR-17 工厂接入 GotenbergBridge,通过健康检查探测服务,然后转换磁盘上的文件或内存中的字节。本指南涵盖基于扩展名的格式检测、健康探测、类型化失败合约,以及交接给 NextPDF 后处理的流程。

先讲清楚的前置条件:

  • 已安装 NextPDF 核心与 nextpdf/gotenberg
  • 有一个可通过 HTTPS 连接的 Gotenberg 服务。在任何请求离开进程之前,桥接就会拒绝单纯的 http:// URL。
  • 已安装一个 PSR-18 客户端,以及 PSR-17 的 request 与 stream 工厂。若要做 DNS 与 TLS 固定,你还需要提供一个 PSR-17 response 工厂。
  • 输入是六种已识别的 Office 格式之一:.docx.xlsx.pptx.odt.ods.odp。桥接会以 ValueError 拒绝任何其他扩展名。

这是一份操作指南。如需一个可直接运行的完整程序,请阅读 Gotenberg 快速入门。

安装桥接、一个 PSR-18 客户端,以及 PSR-17 工厂。

Terminal window
composer require nextpdf/gotenberg guzzlehttp/guzzle

运行一个可通过 HTTPS 连接的 Gotenberg 服务,并从密钥管理器或注入的环境值中获取任何 bearer token。桥接从不读取环境变量,也不会自行构建 HTTP 客户端;这两者都由你提供。

GotenbergBridge::convertFile() 接受一个磁盘路径。它会规范化路径以阻挡路径穿越,将扩展名映射到支持的格式,筛查大小与文件名,再将一个 multipart 请求发送到 <apiUrl>/forms/libreoffice/convertconvertString() 会对你已有的字节执行同样的操作;它会使用原始文件名来检测扩展名。

格式检测基于扩展名完成。桥接会将 .docx.xlsx.pptx.odt.ods.odp 映射到各自的格式,并在任何网络流量发生之前就以 ValueError 拒绝其他任何格式。结果对象会通过枚举值公开检测到的来源格式。

桥接本质上是一次带验证的同步 HTTP 往返。它不会重试、排队、缓存或限流;这些都属于桥接外围的应用程序职责。请将每次转换都视为一次发往外部服务的远程调用:该服务由你运营,但无法在进程内掌控,因此需要围绕它的延迟与失败进行设计。

桥接会通过类型化异常暴露失败,绝不返回部分结果或未经验证的结果:

  • 200 状态、Content-Type(其值不含 application/pdf),或开头不是 %PDF 的内容,都会分别引发 GotenbergConvertException。只有当这三项检查全部通过时,桥接才会返回结果。
  • PSR-18 客户端失败(包括网络失败或超时)会被包装为 GotenbergConvertException,并以原始异常作为成因。
  • 验证失败(非 HTTPS URL、私有或保留地址、输入过大、不安全的文件名)会在任何网络流量发生之前就引发 RuntimeException
  • 无法识别的文件扩展名会在任何网络流量发生之前就引发 ValueError
// Configuration (final readonly):
new GotenbergConfig(
string $apiUrl, // required, must be HTTPS
int $timeout = 30, // hard transfer timeout, seconds
int $maxFileSize = 52_428_800, // 50 MiB
string $apiKey = '', // #[SensitiveParameter]; Bearer when non-empty
list<string> $pinnedPublicKeys = [], // sha256/<base64>
list<string> $backupPublicKeys = [],
)
GotenbergConfig::fromArray(array $config): self
GotenbergConfig::isValid(): bool
// The bridge:
new GotenbergBridge(
GotenbergConfig $config,
ClientInterface $httpClient, // PSR-18
RequestFactoryInterface $requestFactory, // PSR-17
StreamFactoryInterface $streamFactory, // PSR-17
?LoggerInterface $logger = null, // PSR-3
?HtmlSecurityPolicyInterface $htmlSecurityPolicy = null,
?ResponseFactoryInterface $responseFactory = null, // enables pinned transport
)
GotenbergBridge::isAvailable(): bool
GotenbergBridge::convertFile(string $path): GotenbergConvertResult
GotenbergBridge::convertString(string $bytes, string $originalFilename): GotenbergConvertResult

结果对象会公开 pdfDatasourceFormat 枚举、isValid()(当内容非空且开头为 %PDF 时为 true),以及 size()。完整的字段参考、fromArray() 键映射,以及传输选择规则,请见“另请参阅”中链接的 Gotenberg 配置页面。

描述服务、接入桥接、探测服务,然后转换一个文件。

convert-quickstart.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Gotenberg\GotenbergBridge;
use NextPDF\Gotenberg\GotenbergConfig;
use NextPDF\Gotenberg\GotenbergConvertException;
$config = new GotenbergConfig(
apiUrl: 'https://gotenberg.example.com',
timeout: 60,
apiKey: getenv('GOTENBERG_TOKEN') ?: '',
);
$bridge = new GotenbergBridge(
config: $config,
httpClient: $httpClient, // your PSR-18 client
requestFactory: $requestFactory, // your PSR-17 factory
streamFactory: $streamFactory, // your PSR-17 factory
responseFactory: $responseFactory, // enables the pinned transport
);
// Probe before converting. The probe validates the URL with no network
// traffic, then sends a HEAD to <apiUrl>/health.
if (!$bridge->isAvailable()) {
throw new RuntimeException('Gotenberg is not reachable.');
}
try {
$result = $bridge->convertFile('/path/to/report.docx');
} catch (GotenbergConvertException $exception) {
// Bad config, HTTP failure, non-200, wrong Content-Type, or non-PDF body.
throw $exception;
}
if (!$result->isValid()) {
throw new RuntimeException('Result is not a valid PDF.');
}
file_put_contents('/path/to/report.pdf', $result->pdfData);

这个类是 NextPDF\Gotenberg\GotenbergConfig(上面那行用的正是你的代码必须导入的命名空间)。isAvailable() 对于空的、非 HTTPS 或私有地址的 URL,以及任何网络错误,都会返回 false,从不抛出异常;只要返回低于 500 的状态(来自 /health),即表示可用。

生产环境的转换应分别捕获每一种失败类型,只在正确条件下重试,并在调用端一侧限制并发量。下面的 catch 顺序是完整穷举的。

OfficeConverter.php
<?php
declare(strict_types=1);
use NextPDF\Gotenberg\GotenbergBridge;
use NextPDF\Gotenberg\GotenbergConvertException;
use Psr\Log\LoggerInterface;
use RuntimeException;
use ValueError;
final readonly class OfficeConverter
{
public function __construct(
private GotenbergBridge $bridge,
private LoggerInterface $logger,
) {}
public function convert(string $path): string
{
try {
$result = $this->bridge->convertFile($path);
} catch (GotenbergConvertException $exception) {
// Transport, non-200, wrong Content-Type, or non-PDF body.
// Retry only on transport-level or 502/503/504 causes, with
// bounded exponential backoff and jitter — never blind retries.
$this->logger->error('gotenberg.convert.failed', [
'path' => basename($path),
'exception' => $exception::class,
]);
throw $exception;
} catch (ValueError $exception) {
// Extension is not one of the six recognized Office formats.
$this->logger->warning('gotenberg.convert.unsupported_format', [
'path' => basename($path),
]);
throw $exception;
} catch (RuntimeException $exception) {
// Non-HTTPS URL, private address, oversized input, or unsafe name.
$this->logger->error('gotenberg.convert.rejected', [
'path' => basename($path),
'exception' => $exception::class,
]);
throw $exception;
}
if (!$result->isValid()) {
throw new RuntimeException('Gotenberg returned an invalid PDF body.');
}
return $result->pdfData;
}
}

只在传输层的 GotenbergConvertException(一个被包装的 PSR-18 客户端异常),以及幂等的服务器错误(502503504)时重试。400 级别的响应通常表示输入有误,因此重试也会以同样方式失败。限制总尝试次数与总实际耗时。将同时进行的转换数量限制在你的 Gotenberg 部署可承受的容量内。桥接本身是无状态的,可安全地从多个 worker 使用,但服务的转换容量是有限的。

  • 格式检测基于扩展名判断。 一个 .docx 被改名为 .txt 会被以 ValueError 拒绝;而一个 .txt 被改名为 .docx 则会被发送到 Gotenberg,并在那里失败。接受上传时,要信任真正的格式,而不是文件名。
  • fromArray() 在设计上是宽容的。 它会对格式错误的输入静默代入默认值。在你的启动流程中验证来源数组,让缺少的 URL 尽早以配置错误的形式浮现,而不是变成每次转换时才抛出的异常。
  • 大小上限是在进程内强制执行的。 maxFileSize(默认 50 MiB)会在请求发送之前就被检查,因此过大的文件永远不会消耗服务容量。将上限降到刚好满足你的文档所需;较小的上限是成本更低的拒绝服务防护。
  • 探测不是免费的。 请从就绪或健康检查端点调用 isAvailable(),而不是在每次转换前都调用。每次转换都跑一遍探测,会让你对服务的请求率加倍,却毫无收益。
  • 进程内不做缓存。 如果同一份文档会被反复转换,请在你的应用程序中以输入内容哈希为键,缓存产出的 PDF。
  • renderTimeMs 由你自己设定。 除非你的集成已经测量并设定它,否则结果的计时字段会是 0.0。如果你需要这个数字,请自行给这次调用计时。

在请求期间,一次转换会在 Gotenberg 侧占用一条连接与一个 LibreOffice worker,而 Office 转换并非瞬间完成。请根据你的真实文档测得的转换延迟来设置 timeout,并留出余量。将它设得低于任何上游网关或 PHP max_execution_time,这样桥接会先超时,你会得到一个类型化异常,而不是一个被强制终止的进程。使用队列、信号量,或规模与服务容量匹配的 worker 池来限制并发量。进程内没有缓存;如果你会反复转换同一份输入,请在你的应用程序中加上一层缓存。

  • 发送前先做 HTTPS 与地址筛查。 在任何请求离开进程之前,桥接就会拒绝非 HTTPS URL,以及会 resolve(解析)到私有或保留地址空间的目的地。每次重试的调用都会重新执行这套验证,因此重试无法绕过 SSRF 防护。
  • 按需采用固定传输。 当你提供 response 工厂与固定(或已有一组解析后的 IP)时,桥接会将连接绑定到解析后的地址、强制执行 SPKI 固定、验证对端与主机、应用超时,并停用跟随重定向。证书轮替之前,请先设定好一个备用固定。
  • 不要信任上传文件所声明的内容类型。 接受用户上传时,请自行验证真正的文件类型;扩展名映射到格式只是一个路由决策,不是真伪性检查。
  • 密钥会被遮蔽且不可变。 apiKey 带有 #[SensitiveParameter],而配置是 final readonly。请从密钥管理器获取 token;绝不要把它提交进版本库。记录下来的转换条目会带有 URL、文件名、格式与内容长度 —— 绝不会包含文件内容或 token。
  • 绝不要写空的 catch 块。 每个示例都会捕获特定类型,并带上下文记录下来。

完整的安全与部署模型,请见 Gotenberg 安全与运维页面。PSR-18 传输合约,以及“不要信任内容类型”的指引,都固定到上游生产使用页面上各自的条款。

本指南本身不提出任何规范性的标准主张。桥接的 PSR-18 传输行为(客户端只在无法发送请求或无法解析响应时才引发异常;4xx/5xx 是正常的返回值)、文件上传验证指引,以及 TLS 固定模型,都固定到上游 Gotenberg 生产使用与配置页面上的 PSR-18、OWASP 与 RFC 7469。这份 cookbook 页面只重述用法,并将这些引用交由那些页面处理。桥接会产出 PDF 字节,然后就停下来。签名、PDF/A 配置文件与水印,属于 NextPDF 后处理的范畴,也是商业版能力,并非这个桥接的一部分。