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

Luồng và bộ lọc

Evidence: Standard-backed

Phần lớn byte của một PDF thực tế nằm bên trong các luồng: nội dung trang, phông chữ, hình ảnh, và cả chính luồng tham chiếu chéo. Gần như không byte nào trong số đó được lưu ở dạng thô; trước hết, chúng đi qua một hoặc nhiều bộ lọc. Trang này trình bày những bộ lọc bạn sẽ gặp, từng bộ lọc dùng để làm gì, chúng dễ gây rắc rối ở đâu, và vì sao NextPDF ghim chặt cách nén để cùng một đầu vào luôn tạo ra cùng những byte.

Một luồng và các bộ lọc của nó là một hợp đồng: “những byte này được nén deflate, rồi mã hóa base-85 — hãy giải mã theo đúng thứ tự đó để lấy dữ liệu thật.” Nếu mục /Filter không khớp với những byte thực tế, hoặc /Length sai, hoặc hai bộ lọc được liệt kê sai thứ tự, thì luồng không giải mã được và đối tượng mà nó mang theo bị mất. Trình đọc không suy đoán theo kinh nghiệm; nó làm đúng những gì từ điển chỉ định.

Còn một cái giá thứ hai, âm thầm hơn. Nếu bộ nén của một thư viện không xác định — bản dựng zlib khác nhau, mức nén khác nhau, ranh giới khối nội bộ khác nhau — thì hai lần chạy lẽ ra phải tạo ra một PDF giống hệt nhau lại tạo ra hai tệp khác nhau. Điều đó phá vỡ khả năng tái tạo ở mức byte. Khi khả năng tái tạo bị phá vỡ, các bài kiểm thử golden-file, việc xác minh bản dựng đã ký, và mọi quy trình so sánh khác biệt đầu ra cũng bị phá vỡ theo. Bộ lọc quyết định cả việc PDF có chính xác hay không lẫn việc PDF có giống hệt hay không.

  • Một đối tượng luồng là một từ điển cộng với một khối byte, được gói trong streamendstream, có một /Length và thường có một /Filter.
  • Mục /Filter nêu tên bộ lọc giải mã — hoặc một mảng bộ lọc được áp dụng lần lượt dưới dạng một pipeline.
  • Các bộ lọc chia thành hai họ: nén (FlateDecode, LZWDecode, RunLengthDecode, DCTDecode, JPXDecode, JBIG2Decode) và vận chuyển ASCII (ASCIIHexDecode, ASCII85Decode), cộng với bộ lọc Crypt đặc biệt dùng cho mã hóa.
  • Bộ lọc bạn gặp nhiều nhất là FlateDecode — zlib/deflate. Đây là mặc định cho nội dung, phông chữ, và luồng tham chiếu chéo.
  • NextPDF ghim đầu ra Flate của nó ở một mức và định dạng cố định để cùng những byte đầu vào luôn nén ra cùng những byte đầu ra.

NextPDF chủ đích phát ra các đối tượng luồng qua một trình trợ giúp bộ đệm duy nhất và nén qua một bộ nén được ghim duy nhất.

BinaryBuffer::writeStream() (src/Support/BinaryBuffer.php) gói nội dung luồng vào từ điển của nó, luôn ghi một /Length bằng đúng độ dài byte thực tế và hợp nhất mọi mục bổ sung mà bên gọi cung cấp, chẳng hạn như /Filter. Không có đường đi nào khiến độ dài đã khai báo lệch khỏi những byte được ghi, vì độ dài được lấy từ chính chuỗi nội dung.

Việc nén đi qua PinnedZlibCompressor (src/Writer/PinnedZlibCompressor.php). Lớp này tồn tại vì một lý do. gzcompress mà không có mức nén tường minh sẽ dùng mặc định lúc chạy của zlib, vốn trước nay vẫn khác nhau giữa các bản dựng. Phần đầu (header) 2 byte của zlib thậm chí còn mã hóa mức nén một cách gián tiếp, nên “mặc định” không phải là một đầu ra ổn định. Bộ nén ghim mức nén ở mức tối đa của RFC 1951 và luôn phát ra deflate được bọc zlib (header RFC 1950 + phần đuôi Adler-32), đúng như những gì /Filter /FlateDecode mong đợi. Lỗi nghiêm trọng từ zlib trở thành một ngoại lệ có kiểu, thay vì âm thầm quay về đầu ra không nén — một luồng không bao giờ được lặng lẽ phát ra ở dạng thô.

Bản thân luồng tham chiếu chéo là một ví dụ minh họa cho tất cả những điều này: CrossReferenceStream (src/Core/CrossReferenceStream.php) dựng một bảng nhị phân, nén bảng đó, rồi phát ra dưới dạng một đối tượng luồng với /Type /XRef, một mảng độ rộng trường /W, và /Filter /FlateDecode. Chỉ mục giúp trình đọc tìm thấy mọi đối tượng tự nó cũng là một luồng đã qua bộ lọc.

Bộ lọcHọDùng để làm gìChỗ gây trục trặc
FlateDecodeNénzlib/deflate; mặc định cho nội dung, phông chữ, luồng xrefMột bản dựng zlib không xác định khiến các PDF “giống hệt” lại khác nhau từng byte
LZWDecodeNénPhương pháp nén Lempel–Ziv–Welch cũ hơnCũ; đã được Flate thay thế, thỉnh thoảng vẫn còn thấy trong các tệp cũ
DCTDecodeNénHình ảnh colour/grayscale được mã hóa JPEGMất dữ liệu — mã hóa lại một hình ảnh vốn đã là DCT sẽ làm nó giảm chất lượng thêm lần nữa
JPXDecodeNénDữ liệu hình ảnh wavelet JPEG 2000Một số hồ sơ lưu trữ không cho phép; mức hỗ trợ rộng rãi không đồng đều
JBIG2DecodeNénNén hình ảnh hai mức (1 bit)Không được dùng với hình ảnh nội tuyến; các chế độ mất dữ liệu có thể làm thay đổi bản quét
RunLengthDecodeNénMã hóa độ dài chuỗi (run-length) theo byteChỉ giúp ích với dữ liệu có những chuỗi đơn byte lặp lại dài; có thể làm phình dữ liệu khác
ASCIIHexDecodeVận chuyểnDữ liệu nhị phân dưới dạng các chữ số hexLàm kích thước tăng gấp đôi; chỉ dùng cho kênh an toàn 7 bit, không bao giờ để giảm kích thước
ASCII85DecodeVận chuyểnDữ liệu nhị phân dưới dạng ASCII base-85Tốn thêm khoảng 25%; là tiện ích vận chuyển, không phải nén
CryptBảo mậtÁp dụng trình xử lý bảo mật của tài liệuMột luồng tham chiếu chéo không được dùng bộ lọc Crypt

Tập bộ lọc tiêu chuẩn của PDF, theo họ, kèm sự cố mà mỗi bộ lọc thường gắn liền. NextPDF ghi FlateDecode cho nội dung, phông chữ, và luồng tham chiếu chéo; các bộ lọc vận chuyển ASCII là dành cho kênh 7 bit, không bao giờ để giảm kích thước.

Cơ chế bộ lọc được định nghĩa bởi Spec: ISO 32000-2, §7.4 . Một từ điển luồng nêu tên các bộ lọc của nó qua /Filter. Khi mục này liệt kê nhiều hơn một bộ lọc, các bộ lọc đó tạo thành một pipeline giải mã và được áp dụng tuần tự. Trình ghi mã hóa một luồng để nén nó hoặc để làm cho nó an toàn 7 bit. Trình đọc gọi các bộ lọc giải mã tương ứng để khôi phục dữ liệu gốc. Evidence: Standard-backed

Bảng bộ lọc của tiêu chuẩn phân loại từng bộ lọc. FlateDecode giải nén dữ liệu zlib/deflate-encoded, tái tạo văn bản hoặc dữ liệu nhị phân ban đầu. DCTDecode tái tạo các mẫu hình ảnh xấp xỉ bản gốc thông qua JPEG — từ “xấp xỉ” chính là cách tiêu chuẩn cho biết nó gây mất dữ liệu. LZWDecode, RunLengthDecode, JBIG2Decode, JPXDecode, và bộ lọc Crypt cũng đều được định nghĩa ở đó, trong đó JBIG2 bị cấm rõ ràng đối với hình ảnh nội tuyến.

Luồng tham chiếu chéo áp dụng chính cơ chế của định dạng lên bản thân nó: nó là một đối tượng luồng (/Type /XRef, Spec: ISO 32000-2, §7.5.8 ) mà mảng /W của nó nêu rõ độ rộng theo byte của mỗi trường mục trong luồng đã giải mã. Tiêu chuẩn yêu cầu nó không được mã hóa và không dùng bộ lọc Crypt. CrossReferenceStream của NextPDF tuân thủ điều này một cách chính xác — FlateDecode, /W tường minh, không mã hóa.

Một luồng nội dung trang được nén bằng Flate. Đây là hình thức cực kỳ phổ biến: một từ điển có /Length/Filter, rồi đến những byte đã nén nằm giữa streamendstream.

<?php
declare(strict_types=1);
use NextPDF\Writer\PinnedZlibCompressor;
// The marking operators a page content stream carries, uncompressed.
$content = "BT /F1 12 Tf 72 712 Td (Hello) Tj ET\n";
// NextPDF compresses through the pinned compressor: fixed level,
// fixed zlib-wrapped format. The same $content always yields the
// same $compressed bytes, on any supported PHP/zlib build.
$compressed = PinnedZlibCompressor::compress($content);
// Emitted as a stream object. /Length is the real byte length of
// $compressed; /Filter names the decode the reader must apply.
// N 0 obj
// << /Length <strlen($compressed)> /Filter /FlateDecode >>
// stream
// <$compressed bytes>
// endstream
// endobj

Trình đọc làm điều ngược lại: đọc /Length byte, chạy chúng qua FlateDecode vì /Filter chỉ định như vậy, và lấy lại các toán tử ban đầu. Khi bộ nén được ghim, vòng lặp khứ hồi đó không chỉ chính xác. Nó còn giống hệt mỗi lần, chính là điều mà các bài kiểm tra golden-file và bản dựng đã ký dựa vào.

Cái bẫy là coi các bộ lọc ASCII như nén. ASCIIHexDecode và ASCII85Decode làm cho một luồng lớn hơn — lần lượt khoảng gấp đôi và khoảng 25%. Chúng tồn tại để chuyển dữ liệu nhị phân qua một kênh chỉ an toàn cho văn bản 7 bit, không phải để tiết kiệm dung lượng. Chọn ASCII85 để “thu nhỏ” một PDF sẽ cho kết quả ngược lại. Nửa sau của cùng hiểu lầm đó là tin rằng FlateDecode giúp hình ảnh không mất dữ liệu “miễn phí”. Flate đúng là không mất dữ liệu, nhưng nếu hình ảnh vốn đã được mã hóa DCT (JPEG), thì việc bọc nó lại lần nữa hoặc chuyển mã nó qua một bộ lọc gây mất dữ liệu sẽ làm nó giảm chất lượng, bất kể Flate làm gì xung quanh nó. Pipeline bộ lọc giữ nguyên đúng những gì bạn đưa vào — bao gồm cả hiện vật do nén lại mà bạn vô tình đưa vào.

Trang này trình bày cách các bộ lọc được khai báo và áp dụng, không đi vào thuật toán mức bit bên trong từng bộ lọc. Cam kết về tính xác định chỉ áp dụng cụ thể cho đầu ra Flate của NextPDF đối với các luồng mà nó ghi. Cam kết đó đúng qua các phiên bản phụ của PHP và các bản dựng zlib tuân thủ tiêu chuẩn, nhưng tiêu chuẩn cho phép rõ ràng rằng bộ mã hóa deflate có thể chọn các ranh giới khối nội bộ khác nhau. Vì vậy, đầu ra giống hệt từng byte qua các bản triển khai zlib thực sự khác nhau (chẳng hạn zlib thông thường so với zlib-ng) là điều không được cam kết. Môi trường bản dựng được ghim chặt cũng vì lý do đó.

NextPDF chọn FlateDecode và các bộ lọc vận chuyển ASCII cho dữ liệu mà nó phát ra. Nó không phải là một bộ chuyển mã hình ảnh. Nó không hứa hẹn đóng gói lại một luồng JPEG2000 hay JBIG2 tùy ý đến từ bên ngoài, và các đánh đổi mất dữ liệu của hình ảnh là thuộc tính của dữ liệu nguồn, chứ không phải điều mà một trình ghi có thể hoàn tác.

Vì sao FlateDecode có ở khắp nơi? Vì nó không gây mất dữ liệu, đa dụng, được hỗ trợ tốt, và rất phù hợp với nội dung gồm văn bản và toán tử của phần lớn PDF. Đây là mặc định an toàn cho các luồng nội dung, phông chữ nhúng, và luồng tham chiếu chéo.

Tôi có thể tắt nén không? Bạn có thể bỏ qua /Filter và lưu byte ở dạng thô, và trình đọc sẽ chấp nhận điều đó. Tệp sẽ lớn hơn nhưng không cải thiện điều gì khác; hiếm khi có lý do để làm vậy ngoài việc gỡ lỗi.

Tại sao lại phải ghim mức nén làm gì? Để đầu ra có thể tái tạo được. Một mức nén không được ghim (hoặc một bản dựng zlib không được ghim) có thể làm thay đổi các byte đã nén mà không làm thay đổi nội dung sau giải nén — vẫn đúng, nhưng không giống hệt, và điều đó làm hỏng việc xác minh ở mức byte.

  • Đối tượng luồng — một từ điển cộng với một khối byte nằm giữa streamendstream, mang theo một /Length và thường có một /Filter.
  • Bộ lọc — một phép biến đổi giải mã có tên mà trình đọc áp dụng lên các byte của một luồng (ví dụ FlateDecode).
  • Pipeline bộ lọc — một mảng các bộ lọc được áp dụng tuần tự; thứ tự của mảng chính là thứ tự giải mã.
  • FlateDecode — bộ lọc zlib/deflate; cách nén mặc định cho nội dung, phông chữ, và các luồng tham chiếu chéo.
  • DCTDecode — bộ lọc hình ảnh JPEG; gây mất dữ liệu, nên mã hóa lại sẽ làm hình ảnh giảm chất lượng thêm lần nữa.
  • Bộ lọc vận chuyển ASCII — ASCIIHexDecode / ASCII85Decode; làm cho dữ liệu an toàn 7 bit với cái giá phải trả là kích thước — không phải nén.
  • Nén xác định — tạo ra đầu ra đã nén giống hệt từng byte với cùng một đầu vào, đạt được bằng cách ghim mức nén và định dạng của bộ nén.