PDFlibPas giải mã TIFF bằng một bộ phân tích Object Pascal viết tay thay vì binding libtiff, và phiên bản 3.534.1 đã siết chặt đúng những điểm mà bộ phân tích này từ chối dữ liệu vào. BigTIFF magic 43 giờ bị từ chối bằng tên gọi cụ thể, TileOffsets và TileByteCounts bị chặn ngay khi phân tích tag, còn mọi bộ đệm đều được tính kích thước bằng số học Int64 dưới trần giải mã 256 MiB
Lỗi mà bản này vá lại không bao giờ xuất hiện trong phòng thí nghiệm. Nó xuất hiện dưới dạng một cổng quét đã chạy êm ru ba năm trời, cho đến khi khách hàng đưa một kho lưu trữ địa lý hoặc một ảnh bệnh học toàn slide đi qua nó. Tệp có header TIFF hợp lệ. Nó phân tích được. Kết quả trả về là một trang nhiễu sọc, hoặc một lần cấp phát nhiều gigabyte kéo sập dịch vụ, và dọc đường đi không bước nào tuyên bố dữ liệu vào không hợp lệ. Đó là kiểu hỏng hóc xứng đáng để phòng chống: không phải crash, mà là một câu trả lời sai được đưa ra đầy tự tin
Vì sao II hay MM không chứng minh bạn đang có TIFF cổ điển?
Vì marker thứ tự byte được dùng chung bởi cả hai biến thể. TIFF cổ điển lẫn BigTIFF đều mở đầu bằng II hoặc MM, còn trường thực sự phân biệt chúng là magic 16 bit ngay sau đó: 42 cho TIFF cổ điển theo định nghĩa trong đặc tả TIFF 6.0, 43 cho BigTIFF với offset 64 bit. Một loader viết kiểu FValidTIFF := PopWord = 42 không sai với TIFF cổ điển, nhưng nó gộp hai dạng từ chối rất khác nhau vào một boolean im lặng, khiến BigTIFF không còn phân biệt được với một JPEG bị cắt cụt mà ai đó đã đổi đuôi. PDFlibPas giờ tách riêng các trường hợp và ghi từng trường hợp vào TPDFTIFF.LastError: header ngắn hơn bốn byte, marker thứ tự byte không hợp lệ, magic 43, và mọi giá trị magic khác đều cho ra thông điệp riêng. Thư viện vẫn không giải mã BigTIFF, và nói thẳng điều đó chính là ý nghĩa của việc này. Caller nhận được sự khác biệt giữa "đây không phải TIFF" và "đây là TIFF có bố cục offset 64 bit mà bộ giải mã dựng sẵn chưa hỗ trợ", và đó cũng là khác biệt giữa một ticket hỗ trợ bạn có thể trả lời trong một email và một ticket kéo thành cả tuần đoán mò
var
Tiff: TPDFTIFF;
Page: Integer;
begin
Tiff := TPDFTIFF.Create;
try
Tiff.LoadFromFile('inbox\scan-0417.tif');
if not Tiff.ValidTIFF then
raise Exception.Create('TIFF rejected: ' + Tiff.LastError);
if Tiff.PageCount < 1 then
raise Exception.Create('TIFF carries no decodable page');
for Page := 1 to Tiff.PageCount do
Writeln(Format('page %d: %dx%d, %d spp',
[Page,
Tiff.PageInfo[Page].Width,
Tiff.PageInfo[Page].Height,
Tiff.PageInfo[Page].SamplesPerPixel]));
finally
Tiff.Free;
end;
end;
Tile là hình học khác, không phải mảng offset thứ hai
PDFlibPas từ chối TIFF xếp ô ngay trong lúc phân tích tag, trước khi chạm vào bất kỳ dữ liệu pixel nào. Con tắt mời gọi lỗi thì dễ thấy: tag 324 (TileOffsets) và tag 325 (TileByteCounts) là các mảng offset tệp và số byte, về cấu trúc giống hệt các mảng strip, nên trỏ các trường strip sẵn có vào chúng chỉ tốn hai dòng code và biên dịch sạch sẽ. Nhưng điều đó cũng sai. Tile tạo thành lưới hai chiều với các khối mép được đệm, có stride hàng riêng trong từng tile, và hoàn toàn không có ngữ nghĩa RowsPerStrip, đúng như phần ảnh xếp ô của TIFF 6.0 mô tả. Vì thế, đưa payload tile vào một bộ giải mã strip sẽ không hỏng ầm ĩ. SimpleExtract và CompDecode đi qua dữ liệu với stride sai và xuất ra ảnh đúng kích thước nhưng sai pixel. Code cũ còn nhân đôi vấn đề khi giữ StripsAreTiles, ColumnsPerTile và RowsPerTile trong TTIFFPage: hình học tile được ghi lại bởi một bộ giải mã không hề có bộ lắp ráp tile phía sau. Ở 3.534.1, các trình xử lý tag 324 và 325 raise lỗi tile và bỏ IFD ngay lập tức, nên lời từ chối mang đúng chữ "tiled" thay vì nhiều tuần sau mới nổi lên dưới dạng một phiếu phàn nàn về hiển thị
Giới hạn từng chiều không phải là ngân sách bộ nhớ
Chặn width và height mỗi chiều ở 65.535 là cần thiết nhưng xa mới đủ, vì đại lượng quyết định việc cấp phát là một tích. RowsPerStrip * Width * SamplesPerPixel có thể tràn số học 32 bit từ rất sớm trước khi bất kỳ thừa số nào chạm giới hạn của riêng nó, và ngay cả khi không tràn, nó vẫn có thể đặt tên một lần cấp phát mà không dịch vụ nào nên thử. PDFlibPas tính số byte mỗi hàng bằng Int64 và áp ba trần cùng lúc: 65.535 mỗi chiều, 32 thành phần màu, và 256 MiB byte đã giải mã
const
PDFLIB_TIFF_MAX_IMAGE_DIM = 65535;
PDFLIB_TIFF_MAX_COLOR_COMPONENTS = 32;
PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES = 256 * 1024 * 1024;
// bên trong TPDFTIFF.ValidatePageForDecode
BitsPerPixel := Int64(P.BitsPerSample) * P.SamplesPerPixel;
RowBytes := (Int64(P.Width) * BitsPerPixel + 7) div 8;
if (RowBytes < 1) or
(RowBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
(Int64(P.Height) > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES div RowBytes) then
Exit(False);
DecodedBytes := RowBytes * P.RowsPerStrip;
if (DecodedBytes < 1) or
(DecodedBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
(DecodedBytes > MaxInt) then
Exit(False);
Ba chi tiết ở đó quan trọng hơn chính các hằng số. Phép kiểm tra chiều cao được viết thành phép chia thay vì phép nhân, để tích quá khổ không bao giờ được hình thành. RowsPerStrip nhỏ hơn 1 hoặc lớn hơn chiều cao ảnh sẽ được chuẩn hóa về chiều cao trước, đây chính là cách đọc single-strip mà TIFF 6.0 đã ngầm định và nó ngăn một tag độc hại phình to bộ đệm strip. Và thủ tục này được dùng chung: ValidatePageForDecode chạy ở cuối khâu phân tích tag rồi chạy lại tại đầu vào của cả SimpleExtract lẫn CompDecode, nên code nào đi thẳng đến bộ giải mã cũng không thể vòng qua ngân sách. Đó cũng là quy tắc PDFlibPas áp dụng khi phân tích đồ thị đối tượng PDF không đáng tin, vì một giới hạn chỉ canh ở một trong ba cánh cửa thì không phải là giới hạn
Caller cần kiểm tra gì trước khi đọc PageInfo?
Kiểm tra ValidTIFF trước, rồi đến PageCount, và chỉ sau đó mới đánh chỉ số vào PageInfo. Một tệp bị từ chối có thể để PageCount ở mức 0, còn GetPageInfo trả lời chỉ số ngoài phạm vi bằng một bản ghi TTIFFPage chưa khởi tạo, nên đường xử lý lỗi mà đọc resolution hay số mẫu trên đường báo thất bại sẽ kết thúc bằng việc đọc nhiễu. Phiên bản 3.534.1 sửa cả hai caller bên trong thư viện: đường nhập ảnh chỉ đọc XRes và YRes trong nhánh hợp lệ, còn TPDFlib.GetImagePageCount yêu cầu ValidTIFF thay vì tự tin vào một số trang khác 0. Phía hạ nguồn, đối số Options của AddImageFromFile là số trang tính từ 1 của TIFF nhiều trang, nên GetImagePageCount phải đáng tin trước khi vòng lặp bắt đầu chứ không phải sau đó. Số trang bằng 0 giờ là một câu trả lời thật, nghĩa là "ở đây không có gì giải mã được", chứ không phải tai nạn của một lệnh return sớm, điều quan trọng nhất khi bạn đang sắp collate và chen luân các lô quét duplex và một tờ bị giải mã sai trong im lặng sẽ rơi vào vị trí sai
var
Pdf: TPDFlib;
Pages, I, ImageID: Integer;
begin
Pdf := TPDFlib.Create;
try
Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
if Pages < 1 then
Exit; // header lỗi, BigTIFF, bố cục xếp ô hoặc vượt ngân sách
Pdf.NewDocument;
for I := 1 to Pages do
begin
Pdf.NewPage;
ImageID := Pdf.AddImageFromFile('inbox\scan-0417.tif', I);
if ImageID > 0 then
begin
Pdf.SelectImage(ImageID);
Pdf.DrawImage(0, 0, 595, 842);
end;
end;
Pdf.SaveToFile('scan-0417.pdf');
finally
Pdf.Free;
end;
end;
Tự xây bộ giải mã hay liên kết libtiff?
PDFlibPas giữ bộ giải mã dựng sẵn, và yếu tố quyết định là độ phủ nền tảng chứ không phải ai là tác giả. Khoảng 1.873 dòng Object Pascal biên dịch ở bất cứ nơi nào trình biên dịch tới được: Win32, Win64, macOS, iOS, Android, và FPC trên Linux. libtiff 4.7.1 là khoảng 30.000 dòng C trải trên 34 đơn vị dịch tif_*.c, và các tệp object dựng sẵn hiện có chỉ phủ Windows. Nhận libtiff đồng nghĩa đánh đổi khả năng phủ TIFF trọn vẹn lấy một danh sách nền tảng được hỗ trợ thu hẹp về đúng những máy chạy được chuỗi công cụ C, cộng thêm một lượt liên kết chưa ai từng rà
Cái giá đó xứng đáng được nói thẳng không tô vẽ. Bộ giải mã dựng sẵn xử lý được những gì công việc tài liệu quét thực sự tạo ra: CCITT Group 3 một chiều và hai chiều, Group 4, LZW, Deflate, PackBits và JPEG-in-TIFF, trên các photometric WhiteIsZero, BlackIsZero, RGB, palette và CMYK với Predictor 1 và 2. Những payload này khớp với các bộ lọc PDF trong ISO 32000-1 §7.4.4 và §7.4.6, đó là lý do front-end TIFF mang sức nặng lớn trong một pipeline quét tài liệu. Những thứ nó không xử lý là BigTIFF, tile, Predictor 3 số thực, PixarLog và SGILog, JPEG nén kiểu cũ số 6, và các kim tự tháp sub-IFD. Kể từ 3.534.1, mỗi mục trong số đó đều là một lời từ chối có tên riêng thay vì một bức ảnh sai, và thư viện giữ một danh sách điều kiện kích hoạt đã ghi thành văn để mở lại quyết định libtiff:
- khách hàng báo một tệp BigTIFF và cần hỗ trợ gốc thay vì một bước chuyển đổi
- khách hàng báo TIFF xếp ô từ nguồn y tế, GIS hoặc công nghiệp và cần giải mã ngay tại chỗ
- khách hàng báo TIFF Predictor 3 số thực
- một lỗ hổng được công bố rơi đúng vào đường giải mã CCITT hoặc LZW dựng sẵn
- lập luận đa nền tảng không còn đúng, hoặc vì hỗ trợ macOS, iOS và Android bị bỏ, hoặc vì một tích hợp libtiff tái sử dụng được đã phủ macOS và Linux
Chính lộ trình di chuyển đã được khoanh vùng chứ không phải suy đoán: một conditional USE_LIBTIFF sẽ giữ nguyên mặt công khai của TPDFTIFF, dẫn LoadFromStream qua TIFFClientOpen với các stream callback, và để bộ phân tích Pascal làm phương án dự phòng cho môi trường không phải Windows. Cho đến khi một trong những điều kiện kích hoạt đó thực sự xảy ra, việc duy trì hai bộ giải mã và ma trận kiểm thử gấp đôi không mua được điều gì mà khách hàng cảm nhận được. Trì hoãn một chi phí khi đã ghi sẵn lối thoát là chuyện khác với phớt lờ nó
Điều này đặt pipeline quét tài liệu ở đâu
Hãy coi TPDFTIFF là một cổng chặn chứ không phải một bộ chuyển đổi. Nạp tệp, đọc ValidTIFF, và log LastError nguyên văn mỗi khi nó false, vì chuỗi đó giờ là con đường ngắn nhất từ một báo cáo hiện trường đến một chẩn đoán. Các tệp trượt cổng vẫn cứu được bằng cách chuyển đổi ở thượng nguồn, đó là câu trả lời thực dụng cho nguồn BigTIFF và tile ngày nay. Với dữ liệu vào nằm ngoài hẳn TIFF, PDFlibPas đi một nhánh riêng qua đường nhập ảnh AVIF, HEIF và JPEG XL, để câu hỏi bộ giải mã nào sở hữu định dạng nào luôn được nêu rõ thay vì tự nhen nhóm
Toàn bộ những điều trên nằm sau API ảnh thông thường, nên một pipeline tài liệu có thêm ranh giới chặt hơn mà không phải đổi một dòng code gọi hàm nào, ngoài việc kiểm tra số trang mà lẽ ra đã phải kiểm tra. Nếu bạn đang cân nhắc một lộ trình TIFF sang PDF gốc cho Delphi hoặc C++Builder, toàn bộ component và phần xử lý ảnh của nó được tài liệu hóa tại trang PDF Library for Delphi