Một stream đánh dấu /Predictor 12 không có nghĩa mọi dòng đều dùng bộ lọc PNG số 2. HotPDF, thành phần VCL PDF gốc cho Delphi và C++Builder, coi các giá trị predictor từ 10 đến 15 là một họ duy nhất: tag bộ lọc thật, từ 0 đến 4, là byte đầu tiên của mỗi dòng đã mã hóa, và HPDFDecodePredictor đọc và xác thực tag đó theo từng dòng. Sự phân biệt này là hình dạng của gần như mọi lỗi trong góc khuất này của PDF, vì không có gì báo lỗi khi bạn làm sai. Chuỗi bộ lọc vẫn chạy, ảnh raster vẫn đúng kích thước bạn mong đợi, và hình ảnh ra thành nhiễu chéo hoặc một dải gradient trôi dạt ngày càng xa qua từng dòng quét. Năm con số trong /DecodeParms (ISO 32000-1 §7.4.4) phần lớn thay đổi ý nghĩa của các byte chứ không phải độ dài của chúng, nên một giá trị sai tạo ra rác trông có vẻ hợp lý thay vì một lỗi
Vì sao /Predictor 12 không có nghĩa là bộ lọc PNG số 2 trên mọi dòng?
Vì con số predictor chỉ nói "đang dùng dự đoán kiểu PNG", không nói cụ thể bộ lọc nào. Bộ mã hóa PNG chọn một bộ lọc cho mỗi dòng quét và bộ lọc PDF thừa hưởng điều đó, nên các giá trị predictor 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) và 15 (Optimum) đều giải mã giống hệt nhau: byte tag dẫn đầu của mỗi dòng mới là thứ bộ giải mã phải tuân theo. Hệ quả về bố cục cũng quan trọng không kém ý nghĩa. Mỗi dòng đã mã hóa dài 1 + RowBytes byte, đầu vào do đó vượt quá đầu ra đúng bằng số dòng, và một stream có độ dài không phải bội số nguyên của RowBytes + 1 theo định nghĩa là bị cắt cụt. HotPDF kiểm tra ranh giới đó trước khi chạm vào bất kỳ byte nào, từ chối mọi tag lớn hơn 4 với thông báo Invalid PNG predictor row tag, và đọc dòng trước đó trực tiếp từ chính buffer đầu ra duy nhất thay vì tạo ra một mảng dòng hai chiều. Bộ lọc 1 và 3 nhìn ngược lại BytesPerPixel trong dòng hiện tại, bộ lọc 2 đọc thẳng lên trên, bộ lọc 4 chạy lựa chọn Paeth trên trái, trên và trên-trái — và cả bốn đều hoạt động trên đầu ra đã được tái tạo, đó là lý do dòng-trên phải là dòng đã giải mã chứ không bao giờ là đầu vào đã lọc
uses
HPDFPredictor;
var
Filtered, Raster: AnsiString;
ErrorText: string;
begin
// /DecodeParms << /Predictor 12 /Colors 3 /BitsPerComponent 8 /Columns 1024 >>
if HPDFTryDecodePredictor(Filtered, 12, 3, 8, 1024,
Int64(1024) * 3 * 8192, Raster, ErrorText) then
ConsumeRaster(Raster)
else
LogStreamDefect('predictor', ErrorText); // no exception, no partial raster
end;
Tham số MaxOutputBytes không phải để trang trí. Một bước predictor thực chất là một bước giải nén được ngụy trang, và một giá trị /Columns thù địch hoặc chỉ đơn thuần bị hỏng có thể biến vài kilobyte đầu vào thành một yêu cầu cấp phát nhiều gigabyte. HotPDF tính số bit mỗi dòng, số byte mỗi dòng và tổng kích thước raster trước tiên bằng Int64, từ chối mọi hình học gây tràn, và tuân theo giới hạn trên do bên gọi cung cấp. Truyền vào một giới hạn thực suy ra từ dictionary hình ảnh, và chế độ thất bại trở thành một thông điệp được ghi log thay vì một hộp thoại hết bộ nhớ trên máy khách hàng
Vì sao TIFF Predictor 2 làm hỏng ảnh 4-bit?
Vì Predictor 2 là sai phân ngang theo từng mẫu (sample), không phải theo từng byte, và ở 1, 2 hay 4 bit mỗi thành phần thì nhiều mẫu chia sẻ chung một byte. Cách cài đặt phổ biến cộng byte N-Colors vào byte N, điều này tình cờ đúng ở 8 bit mỗi thành phần và âm thầm sai ở mọi nơi khác. Một lượt quét RGB 8-bit giải mã hoàn hảo, rồi cũng đúng đoạn mã đó phá hỏng một ảnh chỉ mục 4-bit ngay lần đầu tiên nó xuất hiện trong sản xuất
Phép toán đúng hoạt động bên trong trường bit. HotPDF duyệt các mẫu từ chỉ số Colors tới Colors * Columns - 1, trích xuất mẫu và láng giềng bên trái cùng thành phần của nó với một mask (1 shl BitsPerComponent) - 1 ở độ dịch thích hợp, cộng chúng theo modulo mask đó, và ghi kết quả trở lại mà không làm xáo trộn các mẫu khác đóng gói trong cùng byte. Phần đuôi cũng quan trọng: một dòng được đệm tới ranh giới byte, nên các bit đệm sau mẫu cuối cùng phải giữ nguyên không bị tính vào phép toán. Ở 16 bit mỗi thành phần, mỗi mẫu là một cặp byte big-endian và phép cộng cuộn vòng tại $FFFF trên cả cặp thay vì mang qua từng byte một cách độc lập; ở 8 bit thì phép truy hồi byte đơn giản là đúng, bước nhảy theo Colors để đỏ tích lũy với đỏ và alpha tích lũy với alpha. Trong mọi biến thể, pixel đầu tiên của một dòng là một giá trị literal, không bao giờ là một sai phân, và phép truy hồi khởi động lại tại mỗi ranh giới dòng — dự đoán kiểu TIFF không bao giờ đọc dòng phía trên, đó chính là toàn bộ điểm khác biệt giữa nó và họ PNG
EarlyChange thực sự điều khiển điều gì trong LZWDecode?
Nó điều khiển thời điểm bộ đọc mở rộng kích thước mã thêm một bit, và chỉ cần lệch một mã là mọi thứ theo sau đều hỏng. HotPDF diễn đạt quy tắc này thành một bất biến duy nhất: sau khi thêm một mục dictionary, lần đọc tiếp theo mở rộng khi NextCode đạt tới (1 shl CodeSize) - Ord(EarlyChange). Với /EarlyChange 1, giá trị mặc định theo ISO 32000-1 §7.4.4, việc chuyển đổi xảy ra sớm một mã; với /EarlyChange 0 nó xảy ra đúng tại ranh giới. Cả hai đều xuất hiện trong các tệp thực tế và không có gì trong luồng bit cho bạn biết bộ mã hóa đã dùng cái nào. Phần còn lại của máy trạng thái phải di chuyển đồng bộ: một mã clear đặt lại kích thước mã, mask bit, mã trống tiếp theo và kho lưu cụm từ (phrase) cùng lúc, và mã kết-thúc-thông-tin được đọc ở bất kỳ độ rộng nào đang hiện hành tại thời điểm đó, không phải ở 9 bit ban đầu. HotPDF bắt đầu tại InitialCodeSize 9, giới hạn kích thước mã ở 12 và dictionary ở 4096 mục, và mặc định FillOrder là foTop vì PDF đóng gói mã theo bit bậc cao trước — foBottom tồn tại cho các stream kiểu TIFF không làm vậy
uses
HPDFLZW;
var
Decoder: TPDFLZWDecompressor;
Parms: TPDFLZWParms;
Plain: AnsiString;
begin
Decoder := TPDFLZWDecompressor.Create;
try
Decoder.EarlyChange := True; // /EarlyChange 1 is the PDF default
Decoder.FillOrder := foTop; // high-order bit first
Decoder.MaxOutputBytes := 256 * 1024 * 1024;
Decoder.RequireInitialClear := False;
Decoder.RequireEndOfInformation := False;
Parms.Predictor := 12;
Parms.Colors := 3;
Parms.BitsPerComponent := 8;
Parms.Columns := 1024;
Parms.ExpandedTo8Bit := False;
Parms.ColorSpace := 'DeviceRGB';
if Decoder.TryDecompress(RawStreamBytes, Parms, Plain) then
LogDecodeStats(Decoder.PeakCodeSize, Decoder.DictionaryAdds,
Decoder.KwKwKExpansions, Decoder.OutputBytes)
else
LogStreamDefect('lzw', Decoder.LastError);
finally
Decoder.Free;
end;
end;
Các số liệu thống kê này tồn tại để phân loại lỗi (triage), không phải cho hào nhoáng. Khi một tệp giải mã ra đúng độ dài nhưng sai pixel, PeakCodeSize và DictionaryAdds cho bạn biết ngay liệu bộ đọc có từng mở rộng ở đúng nơi bộ ghi đã mở rộng hay không. Đảo EarlyChange, giải mã lại, so sánh hai lần: nếu các con số thay đổi, bạn có câu trả lời trong một lượt chạy thay vì phải dò từng bit
Nhánh KwKwK, và khi nào một stream nên đơn giản là thất bại
Trường hợp hợp lệ duy nhất trông có vẻ bất hợp lệ là Code = NextCode, và HotPDF xử lý nó bằng cách xây dựng mục đó trước khi phát ra nó. Một bộ mã hóa có thể phát ra mã cho một cụm từ mà nó đang định nghĩa ngay trong cùng bước đó, điều này xảy ra bất cứ khi nào đầu vào chứa một mẫu dạng K w K w K; bộ giải mã không thể tra cứu mã đó vì nó chưa tồn tại, nên nó phải xây Previous + First(Previous), thêm nó như một mục mới, và phát ra chính mục nó vừa tạo. HotPDF đếm những trường hợp đó trong KwKwKExpansions và đối chiếu chéo rằng mã nó thêm vào chính là mã nó được yêu cầu. Mọi thứ vượt trên NextCode là hỏng dữ liệu, và ở đó một bộ giải mã nên dừng lại thay vì ứng biến: HotPDF báo lỗi khi gặp một mã tương lai, một tiền tố dictionary trỏ ra ngoài vùng lưu cụm từ, một dictionary đầy, và một mã đầu tiên không phải literal. Hai công tắc nghiêm ngặt cố tình tắt theo mặc định, RequireInitialClear và RequireEndOfInformation, vì rất nhiều PDF sản xuất thực tế bỏ qua mã clear dẫn đầu hoặc hết dữ liệu mà không có ký hiệu kết thúc. Hãy bật chúng lên khi xác thực đầu ra của chính bạn, để tắt khi tiêu thụ các tệp từ thế giới thực
Nơi /DecodeParms thực sự được đọc ở phía tài liệu đã tải
HotPDF giải quyết /DecodeParms hay dạng viết tắt /DP của nó trên dictionary stream hình ảnh, chấp nhận cả một dictionary lẫn một mảng và lấy phần tử cuối cùng khi đó là một mảng, rồi mang Predictor, Colors, BitsPerComponent, Columns và EarlyChange vào đường xử lý raster. Trường hợp mảng là điều mọi người hay quên: một stream được lọc bởi [/ASCII85Decode /FlateDecode] mang một mảng tham số song song, và các thiết lập predictor thuộc về bộ lọc cuối cùng, không phải bộ lọc đầu tiên
var
Pdf: THotPDF;
Info: THPDFLoadedImageInfo;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('scanned.pdf', '') > 0 then
for I := 0 to Pdf.GetLoadedImageCount - 1 do
if Pdf.GetLoadedImageInfo(I, Info) then
begin
Bmp := Pdf.ExtractLoadedImage(I); // nil when the raster is unusable
if Bmp <> nil then
try
Bmp.SaveToFile(Format('image-%d.bmp', [I]));
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Một khiếm khuyết lịch sử trên đường đó đáng được nêu tên, vì loại lỗi này lặp lại. Hàm Flate-có-tham-số cũ tạo ra một stream giải nén rồi sao chép từ đầu vào đã nén gốc, nên bước predictor nhận các byte đã nén và trung thành "phản-dự-đoán" chúng: luôn luôn sai, không bao giờ báo lỗi. Đoạn mã hiện tại chỉ đọc từ bộ giải mã trước khi giao kết quả cho predictor dùng chung, và nó từ chối một raster ngắn hơn kích thước đã tính thay vì quay lại dùng các byte vẫn còn nén — một cách quay lui từng biến một lỗi giải mã thành một bitmap hỏng. Cùng cách cài đặt predictor đó giờ cũng phục vụ cả cross-reference stream, đây là một sự nhất quán hữu ích nếu bạn cũng làm việc với object stream và incremental update, và cơ chế trích xuất xung quanh được trình bày trong bài viết đồng hành về trích xuất hình ảnh đã tải và các bộ lọc giải mã của chúng. Các hình ảnh tới dưới dạng DCTDecode hay JPXDecode không bao giờ tới predictor cả; chúng mang mô hình pixel nén riêng của chúng
Thông lượng: một vùng lưu cụm từ liên tục so với chuỗi theo từng mục
Thay dictionary chuỗi theo từng mục bằng một vùng lưu cụm từ liên tục đo được nhanh hơn khoảng 1,61 lần trên một đầu vào bệnh lý: 1558 MiB/s so với 969 MiB/s trên một benchmark mà cụm từ dài nhất của nó đạt 7.370.880 byte. Hình dạng của đầu vào đó giải thích khoảng cách này, vì các cách cài đặt cổ điển chọn một trong hai đánh đổi tồi. Một dictionary giá trị AnsiString cấp phát và sao chép một chuỗi mới cho mỗi mục trong số tối đa 4096 mục, mỗi mục mới sao chép toàn bộ mục cha của nó; một ngăn xếp tiền tố/hậu tố tránh hoàn toàn việc tốn bộ nhớ đó nhưng tái tạo mỗi cụm từ bằng cách duyệt ngược chuỗi từng byte một rồi đảo ngược nó, điều này ổn với văn bản thông thường và đau đớn khi một cụm từ chạy tới cả megabyte. HotPDF nối thêm mỗi cụm từ liên tục vào một vùng lưu tăng trưởng theo cấp số nhân, lập chỉ mục các mục theo offset và độ dài, và phát ra một cụm từ bằng một lệnh Move duy nhất vào buffer đầu ra. Cái giá trung thực là bộ nhớ: một vùng lưu chứa mọi cụm từ đầy đủ bị giới hạn bởi tổng độ dài của mọi cụm từ chứ không phải bởi số lượng mục, đó chính xác là lý do MaxOutputBytes tồn tại trên cả bộ giải nén lẫn predictor. Suy ra giới hạn đó từ những gì dictionary hình ảnh tuyên bố raster nên có, và một stream nói dối sẽ thất bại nhanh chóng
Bộ giải nén LZW, predictor dùng chung và đường trích xuất hình ảnh đã tải được trình bày ở đây là một phần của HotPDF Component tiêu chuẩn cho Delphi và C++Builder, với tài liệu tham khảo đầy đủ về bộ lọc và DecodeParms trên trang sản phẩm