Pro 版本
Webview
Webview 通过 HTTP 投递一份线性化(Fast Web View)PDF,使客户端能从一小段开头前缀开始渲染第 1 页,而文件其余部分仍在传输途中。它把原始字节包装成一个 LinearizedDocument,通过一个 PSR-7 ByteRangeResponder 以 RFC 9110 部分内容响应来应答 Range 请求,并能(通过 FirstPageProber)证明第一页在该前缀中是自包含的。
可用性与许可
标题为“可用性与许可”的章节此功能随 NextPDF Pro(nextpdf/pro)提供,并通过 Pro 层级的许可证信封激活。没有该授权的部署不会加载此功能的类。比较版本并获取许可证。
没有单独的单项功能许可标志。responder 在运行时被接线到你自己的 PSR-17 工厂——一个 ResponseFactoryInterface 和一个 StreamFactoryInterface——媒体类型默认为 application/pdf,且作为一个构造函数参数,而非许可开关。
composer require nextpdf/pro代码位于 NextPDF\Pro\Webview 命名空间下。
概念概览
标题为“概念概览”的章节一份线性化 PDF 的布局方式是:让文档的第一页——线性化参数字典、主提示流(primary hint stream),以及第 1 页的对象——位于一个在 /E 偏移量处结束的开头区段。Webview 把那种布局转化为渐进式投递。
LinearizedDocument::fromBytes() 通过 Core 读取侧的 LinearizationView 解析字节(Pro 绝不重新实现线性化解析),并拒绝任何不是可用线性化文档的内容:完全未线性化、声明的 /L 长度与真实字节长度不匹配,或 /E 第一页结束偏移量不是文件内的一个正偏移量。因此构造是全函数式(total)的——一旦你持有一个 LinearizedDocument,它暴露的每一个偏移量都是可信的。
随后 ByteRangeResponder 应答一个 HTTP 请求。它仅依据 PSR-7 / PSR-17 实现,不与任何框架耦合。它始终通告 Accept-Ranges: bytes 和一个强、确定性的 SHA-256 ETag,依据 RFC 9110 §14 解析客户端的 Range 头,并返回一个完整的 200 OK、一个 206 Partial Content 单范围、一个用于多个范围的 206 multipart/byteranges 响应,或一个 416 Range Not Satisfiable。
FirstPageProber 是结构性证明侧:它量化第一页前缀、该前缀占整份文件的比例,以及主提示流是否完整地位于其内——正是后者这个属性,让一个阅读器仅凭该前缀就能定位第 1 页的对象。
为什么这样设计
标题为“为什么这样设计”的章节Webview 从不自己重新解析线性化。它借用 Core 读取侧的 LinearizationView,因此投递层继承的是一个经过审计的解析器,而非第二份会逐渐漂移的副本。构造刻意做成全函数式的。LinearizedDocument::fromBytes() 在一开始就拒绝畸形布局,因此一个 Range 响应所信任的每一个偏移量都已先被校验。responder 只讲 PSR-7 和 PSR-17,所以同一份代码可从任何 HTTP 栈提供一份线性化 PDF。正是这种纪律,使渐进式范围投递能安全地大规模暴露给不可信客户端。
设计背景:大规模文档生成。
渐进式字节范围服务的工作原理
标题为“渐进式字节范围服务的工作原理”的章节- 从渲染后的 PDF 字节构建一个
LinearizedDocument。无效输入会在一开始就引发UnsupportedDocumentException。 - 把该文档与入站的 PSR-7
ServerRequestInterface交给ByteRangeResponder::respond()。responder 读取Range(以及可选的If-Range前置条件),并产出正确的 PSR-7ResponseInterface。 - 客户端先请求开头前缀(或者你用
firstPageResponse()推送它),渲染第 1 页,然后随着用户滚动再请求其余范围。
字节范围模型依据 RFC 9110 §14.1.2 使用包含式偏移量:一个 ByteRange 是在一个 contentLength 的表示上的 firstByte–lastByte,其 Content-Range 字段是 bytes first-last/length。
行为契约
标题为“行为契约”的章节LinearizedDocument::fromBytes()是全函数式的:一份非线性化文档、一处/L不匹配,或一个非正/越出文件的/E偏移量,都会引发UnsupportedDocumentException,而不会产出一份不安全的文档。ETag是对确切字节的一个强 SHA-256 实体标签,在构造时记忆化一次。相同的渲染输入产出相同的字节,因而产出相同的ETag,所以缓存与If-Range的行为是可预期的。- 一个没有适用
Range的请求返回带完整正文的200 OK。一个与当前强ETag不匹配的If-Range会导致Range被忽略并返回一个完整的200(RFC 9110 §13.1.5)。仅If-Range的强实体标签形式被采纳;一个 HTTP-date 形式的If-Range被视为不匹配。 - 一个无法识别的范围单位或一个语法上无效的
Range会被忽略,并返回一个完整的200(RFC 9110 §14.2)。 - 一个可满足的范围返回带
Content-Range的206 Partial Content;多个可满足的范围返回206multipart/byteranges。有效字节范围但无一可满足时返回带Content-Range: bytes */length的416(RFC 9110 §15.3.7)。 firstPageResponse()发出一个恰好携带第一页字节范围[0, /E - 1]的206——即“整份下载完成前先给第一页”的服务器推送形式。
代码示例 —— 快速上手
标题为“代码示例 —— 快速上手”的章节以下反映已记录的公开 API。该仓库不为本模块提供可运行示例。
use NextPDF\Pro\Webview\LinearizedDocument;use NextPDF\Pro\Webview\ByteRangeResponder;
$document = LinearizedDocument::fromBytes($pdfBytes);$responder = new ByteRangeResponder($responseFactory, $streamFactory);
$response = $responder->respond($document, $request);代码示例 —— 第一页推送与探测
标题为“代码示例 —— 第一页推送与探测”的章节use NextPDF\Pro\Webview\LinearizedDocument;use NextPDF\Pro\Webview\ByteRangeResponder;use NextPDF\Pro\Webview\FirstPageProber;use NextPDF\Pro\Webview\Exception\UnsupportedDocumentException;
try { $document = LinearizedDocument::fromBytes($pdfBytes);} catch (UnsupportedDocumentException $e) { // Not a usable linearized document — fall back to plain full delivery. // ... return;}
$prober = new FirstPageProber($document);if ($prober->isFirstPageSelfContained()) { // Push exactly the first page's bytes for an instant render. $response = (new ByteRangeResponder($responseFactory, $streamFactory)) ->firstPageResponse($document);}边界情况与注意事项
标题为“边界情况与注意事项”的章节- Webview 需要一份真正线性化的 PDF。如果渲染后的文档未线性化,请在渲染时启用线性化,或用普通完整投递来提供它——当你只需要范围支持、而不需要第一页语义时,
respondToBytes()仍可在任意(非线性化)字节上提供范围。 - 增量更新事关重大:一份被追加到超出其声明
/L的文档会被作为长度不匹配而拒绝,因为字节范围偏移量将不再可信。 - responder 对每个请求所采纳的不同范围数量设有上限。一个请求要求的合并范围数多于该上限,或要求的总字节数多于整份表示时,其
Range会被忽略,并被提供一个完整的200。
第一页前缀是被钳制到文件长度的 /E 第一页结束偏移量,因此 FirstPageProber::prefixFraction() 报告相对于整份文件而言初始抓取有多小——对于一份多页文档,这正是 Fast Web View 的全部意义所在。响应构建会切取内存中的字节串;成本与所选字节成正比。ETag 每份文档计算一次。请用有代表性的文档测量。
安全说明
标题为“安全说明”的章节请把输入视为不可信。LinearizedDocument::fromBytes() 在任何偏移量被使用之前校验线性化不变量。responder 拒绝含有控制字符的 contentType 以防止头注入,派生一个保证不会出现在正文内部的 multipart 边界,并合并重叠范围、限定其数量与总大小,以防御 multipart 范围放大这一类拒绝服务(Apache HTTPD CVE-2011-3192)。本模块不记录任何文档内容。
一致性
标题为“一致性”的章节字节范围投递遵循 RFC 9110(HTTP Semantics)——§14 用于范围请求、§13.1.5 用于 If-Range,以及 §15.3.7 用于 416。线性化文档模型是 ISO 32000-2 Annex F 所描述的 Fast Web View 布局。本模块除其测试所验证的行为之外,不主张任何进一步的外部条款标识。
Enterprise 边界说明
标题为“Enterprise 边界说明”的章节Enterprise 不改变 Webview 行为。Enterprise 增加更高层级的合规与归档功能,另行记录;它们并非通过字节范围提供一份线性化 PDF 所必需。
发布边界
标题为“发布边界”的章节本页仅记录外部可观察的行为以及受支持的公开 API 表面。内部命名空间路径、辅助类、机制表格、runbook 文件名和工单前缀均不在范围内。