Bỏ qua để đến nội dung

Contracts / Streaming

Phạm vi streaming gồm hai interface experimental: StreamingWriterInterface để xuất PDF theo từng phần và CursorInterface để soạn nội dung ở cấp trang. Core đi kèm một engine final đã qua kiểm thử và triển khai cả hai interface này. Các class của engine là internal, vì vậy bạn dùng contract experimental công khai thay vì tự triển khai engine. Vì cấp độ này là experimental, contract có thể thay đổi trong một bản phát hành minor kèm theo thông báo deprecation trước đó. Hãy ghim phiên bản thật chặt hoặc bọc nó sau adapter riêng trước khi phụ thuộc vào nó trong môi trường production.

Terminal window
composer require nextpdf/core:^3

Một streaming writer tuần tự hóa từng trang ngay khi bạn soạn và có thể đẩy trang đó ra đầu ra trước khi bắt đầu trang tiếp theo. Hãy dùng nó khi tài liệu có thể vượt quá ngân sách bộ nhớ hiện có. Writer trong bộ nhớ giữ toàn bộ tài liệu; streaming writer thì không. StreamingWriterInterface định nghĩa một state machine nghiêm ngặt. Một instance mới ở trạng thái CLOSED. open() chuyển nó sang OPEN và ghi header PDF vào luồng do bên gọi cung cấp. newPage() chuyển nó sang PAGING và trả về một cursor. close() ghi cấu trúc cross-reference và trailer, rồi chuyển nó sang trạng thái kết thúc CLOSED. Một cross-reference stream ánh xạ mỗi số đối tượng tới byte offset của nó, như được trình bày trong ISO 32000-2 §7. Mỗi instance chỉ chạy một phiên. Sau close(), instance đã hết tác dụng. Bên gọi sở hữu tài nguyên luồng. Writer ghi vào luồng nhưng không bao giờ đóng luồng đó.

CursorInterface là bề mặt ghi ở cấp trang. Bạn lấy một cursor từ StreamingWriterInterface::newPage(), và cursor đó vẫn hợp lệ cho đến khi bạn hoàn tất nó, cho đến khi newPage() tiếp theo tự động hoàn tất nó, hoặc cho đến khi close() làm nó mất hiệu lực. Việc mất hiệu lực là vĩnh viễn. Không thể kích hoạt lại một cursor. Mọi method trên một cursor đã mất hiệu lực đều ném LogicException. Cursor ghi các toán tử content-stream thô, đặt phông chữ đang dùng và ghi văn bản theo vị trí. Một content stream mã hóa nội dung trang thành một chuỗi các toán tử đồ họa, như được trình bày trong ISO 32000-2 §8. Cursor là một bề mặt cấp thấp: nó không thực hiện text shaping, sắp xếp lại theo hai chiều, ngắt dòng hay bất kỳ bố cục nào. Những việc đó vẫn thuộc phạm vi cấp Document. Bất biến một-cursor luôn được giữ: tại bất kỳ thời điểm nào cũng có nhiều nhất một cursor hợp lệ.

Cả hai interface đều là experimental, và Core đi kèm một engine hoạt động phía sau chúng: một bản triển khai StreamingWriterInterface final, cursor trang của nó và một discard sink dùng để đo lường bộ nhớ. Các class engine này là internal và không thuộc bề mặt công khai. Để dùng streaming, hãy phụ thuộc vào contract experimental và để Core cung cấp bản triển khai. PHPDoc trên mỗi kiểu đều trỏ tới ADR streaming-writer để biết state machine vòng đời và lý do về phạm vi. Vì cấp độ này là experimental, chữ ký của contract vẫn có thể thay đổi trong một bản phát hành minor kèm theo thông báo deprecation trước đó. Hãy ghim phiên bản thật chặt hoặc bọc nó sau adapter riêng trước khi phụ thuộc vào nó trong môi trường production.

KiểuLoạiThành viên chínhĐộ ổn địnhTừ phiên bản
StreamingWriterInterfaceinterfaceopen(resource, Config), newPage(?PageSize): CursorInterface, close()experimental (engine đã phát hành)3.1.0
CursorInterfaceinterfacewriteContent(string), setFont(string, string, float), writeText(float, float, string), finalizePage()experimental (engine đã phát hành)3.1.0

open() ném InvalidArgumentException đối với một luồng không thể ghi và LogicException nếu writer đã được mở. close() không có tính lũy đẳng. Gọi nó hai lần sẽ ném ngoại lệ.

examples/contracts/streaming-quickstart.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\StreamingWriterInterface;
use NextPDF\Core\Config;
/**
* Drive a streaming writer through one page.
*
* The parameter is the experimental contract; Core supplies the
* implementation. Type-hint the interface and let the engine satisfy it.
*
* @param StreamingWriterInterface $writer A Core-supplied streaming writer.
* @param resource $stream A writable, caller-owned stream.
*/
function writeOnePage(StreamingWriterInterface $writer, $stream): void
{
$writer->open($stream, new Config());
$cursor = $writer->newPage();
$cursor->setFont('helvetica', '', 12.0);
$cursor->writeText(72.0, 720.0, 'Streamed page.');
$cursor->finalizePage();
$writer->close();
// The caller closes $stream after close() returns.
}

Hàm này nhắm tới interface experimental, nên vẫn tách rời khỏi class của engine. Core tiêm một bản triển khai hoạt động tại nơi gọi.

examples/contracts/streaming-production.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\StreamingWriterInterface;
use NextPDF\Core\Config;
use NextPDF\ValueObjects\PageSize;
use Psr\Log\LoggerInterface;
final readonly class LargeReportStreamer
{
public function __construct(
private StreamingWriterInterface $writer,
private LoggerInterface $logger,
) {}
/**
* Stream a multi-page report to a caller-owned file handle.
*
* @param resource $stream Writable file handle owned by the caller.
* @param list<list<string>> $pages One list of text lines per page.
*/
public function stream($stream, array $pages): void
{
$this->writer->open($stream, new Config());
try {
foreach ($pages as $lines) {
$cursor = $this->writer->newPage(PageSize::A4());
$cursor->setFont('helvetica', '', 11.0);
$y = 760.0;
foreach ($lines as $line) {
$cursor->writeText(72.0, $y, $line);
$y -= 14.0;
}
$cursor->finalizePage();
}
} finally {
$this->writer->close();
}
}
}

Khối finally bảo đảm writer được đóng và trailer được ghi, ngay cả khi vòng lặp trang ném ngoại lệ. Bên gọi vẫn sở hữu và đóng luồng.

Trường hợp đặc biệt & điều cần lưu ý

Phần tiêu đề “Trường hợp đặc biệt & điều cần lưu ý”
  • Hãy phụ thuộc vào interface, không phải class của engine. Engine triển khai cả hai contract là internal và không thuộc bề mặt công khai. Đừng new nó hoặc tham chiếu nó theo tên. Hãy type-hint StreamingWriterInterface và để Core cung cấp bản triển khai.
  • Contract này là experimental. Chữ ký của nó có thể thay đổi trong một bản phát hành minor, kèm theo thông báo deprecation trước đó. Hãy ghim phiên bản thật chặt hoặc bọc nó sau adapter riêng trước khi phụ thuộc vào nó trong môi trường production.
  • Một cursor trở nên không hợp lệ ngay khi lệnh gọi newPage() hoặc close() tiếp theo diễn ra. Giữ một cursor đã cũ rồi gọi một method trên nó sẽ ném LogicException. Hãy hoàn tất tường minh để dễ hiểu.
  • close() không có tính lũy đẳng. Gọi nó hai lần là lỗi của bên gọi, không phải một điều kiện có thể khắc phục. Contract sẽ ném ngoại lệ.
  • Writer không bao giờ đóng luồng. Nếu bạn quên đóng một handle do bên gọi sở hữu sau khi close() trả về, bạn sẽ rò rỉ một file descriptor.
  • Engine đẩy từng trang đã hoàn tất ra ngoài để bộ nhớ thường trú không tăng theo số lượng trang. Hồ sơ bộ nhớ chính xác là một thuộc tính cấp experimental và có thể thay đổi giữa các bản phát hành minor. Đừng hard-code giả định từ một lần đo lường.

Thiết kế streaming giới hạn bộ nhớ đỉnh. Engine đã phát hành đẩy từng trang hoàn chỉnh ra ngoài và giải phóng buffer của nó, nên tập thường trú không tăng theo số lượng trang, khác với writer trong bộ nhớ. Engine ghi tạm dữ liệu quản lý cross-reference và page-tree của nó vào các luồng tạm lưu trên đĩa để giữ mức tiêu thụ của tiến trình gần như không đổi. Các con số cụ thể về bộ nhớ và thời gian thực thi là thuộc tính cấp experimental và có thể thay đổi giữa các bản phát hành minor, nên trang này không khẳng định bất kỳ con số cố định nào. performance_budget 1500 ms thời gian thực thi và 64 MB đỉnh là giới hạn khung, không phải một bảo đảm theo hợp đồng. Khả năng tái lập là bitwise: cùng một nội dung và cấu hình tạo ra đầu ra giống hệt từng byte, điều mà các bài kiểm thử golden-baseline của engine ghim chặt.

Method writeContent() của cursor là một lối thoát cấp thấp. Nó nối nguyên văn các byte được cung cấp vào content stream của trang và không xác thực cú pháp hay ngữ nghĩa của toán tử. Đầu vào không đáng tin được truyền vào writeContent() sẽ tạo ra một PDF hỏng hoặc độc hại. Hãy coi method đó là bề mặt chỉ dành cho đầu vào đáng tin, và ưu tiên writeText() cho mọi văn bản chịu ảnh hưởng của bên gọi. Cursor đã phát hành escape văn bản được truyền vào writeText() theo ngữ pháp literal-string của PDF, nhưng không làm sạch các toán tử thô. Mô hình luồng-do-bên-gọi-sở-hữu cũng là một thuộc tính bảo mật. Engine ghi vào luồng nhưng không bao giờ đóng hoặc mở lại luồng đó, nên nó không thể chuyển hướng đầu ra. Bề mặt tấn công khi chạy là có thật vì engine được phát hành kèm theo. Bên gọi tuyệt đối không được đưa các byte không đáng tin vào writeContent(), và engine phải tôn trọng các bất biến của contract.

Khẳng địnhTiêu chuẩnĐiều khoảnBằng chứng
Một content stream mã hóa nội dung trang thành một chuỗi các toán tử đồ họa, do cursor nối thêm vào.ISO 32000-2§8
Writer phát ra một cấu trúc cross-reference ánh xạ mỗi số đối tượng tới byte offset của nó khi đóng.ISO 32000-2§7

Cả hai điều khoản đều được ghim theo glossary và diễn giải lại. NextPDF không sao chép bất kỳ văn bản quy phạm nào. ADR streaming-writer được PHPDoc của contract tham chiếu chứa lý do về vòng đời và phạm vi.

Một engine streaming đã qua kiểm thử được cung cấp trong Core mã nguồn mở phía sau các contract experimental này. Các class engine là internal, vì vậy bạn dùng streaming thông qua contract công khai thay vì một tên class cụ thể. NextPDF Pro và NextPDF Enterprise tuân theo cùng một contract, nên code được viết dựa trên StreamingWriterInterface trong Core vẫn hợp lệ với một bản triển khai Premium của cùng contract đó. Điều cần lưu ý là cấp độ experimental, chứ không phải phiên bản phát hành hay tình trạng sẵn có. Chữ ký có thể thay đổi trong một bản phát hành minor kèm theo thông báo deprecation trước đó.