PDFlibPas bản 3.539.23 giải mã được các tệp JBIG2 độc lập dùng tổ chức random-access theo ITU-T T.88 Phụ lục D.2, nơi mọi segment header đứng trước và dữ liệu segment theo sau đúng thứ tự đó. Bộ giải mã Pascal thuần trong PDFlibJBIG2.pas đánh chỉ mục các offset header cho tới header end-of-file bắt buộc, kiểm tra số segment phải tăng dần và tổng độ dài dữ liệu khai báo phải khớp chính xác số byte còn lại, rồi giải mã từng phần thân theo thứ tự header mà không copy hay sắp xếp lại dữ liệu nén. Trước bản phát hành này, cùng một tệp như vậy chỉ nhặt được một lỗi sáo rỗng "random-access organisation is not supported" ngay khi các cờ header được đọc
Tệp JBIG2 random-access hiếm gặp, và chính vì thế mà chúng gây đau đầu mỗi khi xuất hiện. Chúng chui ra từ các pipeline lưu trữ và hệ thống document-imaging vốn muốn trình đọc nhìn thấy mọi segment header, qua đó là mọi phụ thuộc trang và dictionary, trước khi chạm vào một byte nén nào. Một ứng dụng Delphi chuyển hàng loạt kho scan thành PDF thường gặp một tệp như thế ngay giữa chừng công việc, sau khi hàng trăm tệp sequential đi qua ngon lành, và một bộ giải mã chết đứng trước một tệp dạng chuẩn cũng chỉ hơn một bộ render ra rác một chút xíu. Cùng dòng phát hành đó vừa mới dạy xong cho bộ giải mã JBIG2 custom Huffman table và canonical prefix code, nên random access là khoảng trống tổ chức cuối cùng còn lại trong các giới hạn khả năng đã ghi chép của bộ giải mã
Tổ chức random-access của JBIG2 là gì?
Tổ chức random-access là một trong ba cách T.88 Phụ lục D cho phép dàn xếp cùng một bộ segment: sequential (D.1) cài mỗi header xen giữa dữ liệu của nó, random-access (D.2) đặt toàn bộ header lên trước và toàn bộ dữ liệu xuống sau, còn embedded (D.3) là dạng không header dùng bên trong các container khác như PDF. Một tệp .jb2 độc lập mở đầu bằng identifier tám byte 97 4A 42 32 0D 0A 1A 0A, theo sau là một byte cờ và, khi số trang đã biết, một số trang bốn byte. Bit 0 của byte cờ chọn tổ chức, với 1 nghĩa là sequential và 0 nghĩa là random-access; bit 1 bật nghĩa là số trang chưa biết và trường bốn byte vắng mặt. PDFlibPas đọc các giá trị này trong checkHeader và setFileHeaderFlags, còn các bit dự phòng 2 tới 7 được khoan dung thay vì bị từ chối
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = random-access (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = bỏ qua số trang
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// PDF stream: không có file header, tổ chức embedded, một trang
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
PDF đích thân chưa bao giờ mang layout này. Một image stream JBIG2Decode, mô tả trong ISO 32000-1 §7.4.7, chỉ giữ các segment trang ở tổ chức embedded, với các symbol dictionary dùng chung được dời sang stream JBIG2Globals riêng và không có file header, segment end-of-page hay end-of-file. Khi decodeJBIG2 không tìm thấy identifier tám byte, nó suy ra đúng tình huống đó và ép buộc giải mã sequential, một trang. Xuất ảnh JBIG2 thuần đi theo hướng ngược lại, gói các segment PDF vào một tệp độc lập có byte cờ là $03, sequential với số trang chưa biết, rồi chèn thêm một header end-of-file ở cuối. Nên phần việc random-access chỉ chạm tới một đường duy nhất: các tệp độc lập đưa thẳng cho TPLJBIG2Decoder, thường là trước khi chúng được chuyển đổi hay nén lại cho PDF, công việc mà các backend encoder JBIG2 trong PDFlibPas đảm nhận ở phía output
Vì sao một tệp random-access không thể đọc theo thứ tự tệp?
Một tệp random-access không thể đọc theo thứ tự tệp vì chẳng có gì trong dòng byte đánh dấu nơi khối header dừng và khối dữ liệu bắt đầu, ngoài chính header segment end-of-file. Header segment JBIG2 có độ dài thay đổi: số segment referred-to có thể là dạng ngắn ba bit hay dạng dài kèm retention bitmap, số segment referred-to chiếm một, hai hoặc bốn byte tùy theo số của chính segment đó, và trường page association là một hay bốn byte. Một bộ đọc sequential ngây thơ sẽ parse header đầu tiên, đọc độ dài dữ liệu của nó, rồi coi những byte đầu tiên của header thứ hai là dữ liệu của segment đầu. Bộ giải mã không thể biết mình đã đi sai cho tới khi đi được khá xa, và đó là lý do code cũ từ chối thẳng tổ chức này thay vì mò thử
PDFlibPas đánh chỉ mục segment header random-access thế nào?
PDFlibPas đánh chỉ mục header random-access trong một lần pre-scan duy nhất, IndexRandomHeaders, parse từng header, chỉ ghi lại byte offset của nó, và dừng ở header end-of-file đầu tiên (segment type 51). Mỗi header được parse trọn vẹn rồi vứt đi, nên chỉ mục là một mảng số nguyên thay vì một danh sách object, và pre-scan cộng dồn các độ dài dữ liệu khai báo trên đường đi. Khi scan kết thúc, đầu đọc đứng ngay trên byte đầu tiên của dữ liệu segment đầu, và vị trí đó trở thành NextBodyOffset
// IndexRandomHeaders, cục bộ trong TJBIG2StreamDecoder.readSegments
while not reader.isFinished do
begin
Offset := reader.bytePointer;
Header := TSegmentHeader.Create;
try
readSegmentHeader(Header);
if reader.BufferOverrun then
raise EJBIG2DecodeError.CreateFmt(
'JBIG2 truncated random-access header at byte %d', [Offset]);
if (HeaderCount > 0) and
(Cardinal(Header.getSegmentNumber) <= Cardinal(PreviousNumber)) then
raise EJBIG2DecodeError.Create('JBIG2 random-access segment numbers must increase');
PreviousNumber := Header.getSegmentNumber;
Count := Header.getSegmentDataLength;
if Count < 0 then
raise EJBIG2DecodeError.Create('JBIG2 unknown or oversized segment length is not supported');
Inc(TotalLength, Count); // biến cộng dồn Int64
HeaderOffsets[HeaderCount] := Offset; // tăng theo từng khối
Inc(HeaderCount);
if Header.getSegmentType = JBIG2_END_OF_FILE then
begin
if Count <> 0 then
raise EJBIG2DecodeError.Create('JBIG2 invalid end segment length');
FoundEnd := True;
Break;
end;
finally
Header.Free;
end;
end;
if not FoundEnd then
raise EJBIG2DecodeError.Create('JBIG2 random-access file is missing its end-of-file header');
if TotalLength > Length(reader.Data) - reader.bytePointer then
raise EJBIG2DecodeError.Create('JBIG2 truncated random-access segment data');
if TotalLength < Length(reader.Data) - reader.bytePointer then
raise EJBIG2DecodeError.Create('JBIG2 trailing random-access data');
NextBodyOffset := reader.bytePointer;
Mọi phép kiểm tra trong vòng lặp đó đều tồn tại vì một tệp random-access có độ dư thừa thấp hơn tệp sequential. Số segment phải tăng nghiêm ngặt, so sánh dưới dạng unsigned, vì hai header đòi cùng một số sẽ khiến không rõ danh sách referred-to của một region về sau ám chỉ phần thân nào. Trường data-length được handleSegmentDataLength đọc, hàm này ánh xạ mọi giá trị có bit cao bật, kể cả marker "unknown length" 0xFFFFFFFF, về -1; trong layout random-access chẳng còn cách nào khác để tìm nơi phần thân kế tiếp bắt đầu, nên PDFlibPas từ chối ngay độ dài đó thay vì scan tìm marker kết thúc. Tổng phải khớp chính xác số byte còn lại theo cả hai hướng, và một byte thừa duy nhất sau phần thân cuối cũng thất bại với "trailing random-access data". Sự nghiêm ngặt đó là có chủ đích: trong layout này một độ dài lệch nghĩa là mọi phần thân sau điểm lỗi đều bị dịch chuyển, và một bộ giải mã phẩy tay với một byte lạc sẽ không cách nào biết nó là padding vô hại hay triệu chứng đầu tiên của dữ liệu lệch trục
Vì sao segment end-of-page cuối cùng biến mất?
Segment end-of-page cuối cùng biến mất vì phiên bản đầu tiên của vòng lặp giải mã giữ nguyên điều kiện dừng kiểu sequential, while not reader.isFinished, mà trong layout random-access dòng dữ liệu cạn trước chỉ mục header. Segment end-of-page (type 49) và end-of-file mang 0 byte dữ liệu, và chúng thường là những header cuối cùng trong tệp. Sau khi phần thân region cuối được tiêu thụ, đầu đọc đứng đúng ở cuối buffer, nên vòng lặp thoát và những segment zero-length đó không bao giờ được điều phối, để lại trang dở dang. Bản sửa cho vòng lặp random-access đếm header thay vì đếm byte. Mỗi bước lặp nhảy đầu đọc tới header được chỉ mục kế tiếp, reset bitPointer về 7 vì phần thân trước đó có thể kết thúc giữa byte, parse lại header đó, rồi dời bytePointer tới NextBodyOffset và đẩy nó qua phần thân. Các segment handler sẵn có, phép kiểm tra segment referred-to và diagnostics Context chạy nguyên xi, và thông báo lỗi vẫn báo byte offset gốc của header chứ không phải vị trí phần thân
// TJBIG2StreamDecoder.readSegments, vòng lặp chính
if randomAccessOrganisation then
IndexRandomHeaders;
while (randomAccessOrganisation and (HeaderIndex < HeaderCount)) or
((not randomAccessOrganisation) and (not reader.isFinished)) do
begin
if randomAccessOrganisation then
begin
reader.bytePointer := HeaderOffsets[HeaderIndex];
reader.bitPointer := 7; // căn lại sau một byte rời rạc
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // dùng trong ngữ cảnh lỗi
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // nhảy tới dữ liệu của segment này
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... điều phối cho segment handler sẵn có, rồi nhảy tới DataEnd
end;
Phần xác thực random-access thực ra chứng minh điều gì?
Phần xác thực chứng minh rằng các byte được tái tổ chức giải mã ra cùng pixel như bản sequential gốc của chúng, và chứng minh rằng input random-access dạng lỗi thất bại sạch sẽ; nó không chứng minh độ bao phủ với tệp random-access từ các encoder tùy ý. Bộ hồi quy Pascal dùng chung dùng một tệp tổng hợp 235 byte dựng trên fixture custom-table phải giải mã ra một hàng 7 nhân 1 pixel đen, cả khi có số trang đã biết lẫn khi bỏ trường số trang, rồi nạp cho bộ giải mã mọi tiền tố bị cắt cụt của tệp đó, một số segment trùng lặp, một byte đuôi thừa và một độ dài dữ liệu không xác định, khẳng định mỗi lần rằng LoadFromByteArray trả về False và giữ Width cùng Height bằng 0. Ca ảnh thật là một ảnh refinement custom-table cỡ 500 nhân 473 với các segment được tái tổ chức vào layout random-access nhưng giữ nguyên mọi header gốc và byte nén; SHA-256 của nó khớp tuyệt đối với baseline sequential đã được soát. Tệp đó là một dẫn xuất do phép biến đổi tổ chức tạo ra, chứ không phải một tài liệu random-access tự nhiên tìm thấy ngoài thực tế, và chẳng có mẫu tự nhiên nào sẵn có. Các bộ test đạt với 1.598 test cho Delphi Win32, 42 cho bộ ảnh Delphi Win64, 48 cho FPC Win32 và 46 cho FPC Win64, cùng với ba ca pixel sequential có sẵn
Nạp một tệp .jb2 random-access và các giới hạn của nó
Code ứng dụng không đổi: TPLJBIG2Decoder.LoadFromByteArray tự phát hiện file header và tổ chức, trả về False với bất kỳ input bị từ chối kèm lý do trong LastError, và phơi bày trang đã giải mã qua Width, Height và GetScanline, thứ trả về một byte mỗi pixel
uses
SysUtils, Classes, PDFlibJBIG2;
function ReadJb2(const FileName: string): TJBIG2ByteArray;
var
FS: TFileStream;
begin
FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
SetLength(Result, FS.Size);
if Length(Result) > 0 then
FS.ReadBuffer(Result[0], Length(Result));
finally
FS.Free;
end;
end;
function CountBlackPixels(const FileName: string): Integer;
var
Decoder: TPLJBIG2Decoder;
Row: TJBIG2ByteArray;
X, Y: Integer;
begin
Result := 0;
Decoder := TPLJBIG2Decoder.Create;
try
// Tệp độc lập sequential lẫn random-access đi qua cùng một lệnh gọi
if not Decoder.LoadFromByteArray(ReadJb2(FileName)) then
raise Exception.Create('JBIG2 rejected: ' + Decoder.LastError);
for Y := 0 to Decoder.Height - 1 do
if Decoder.GetScanline(Y, Row) then
for X := 0 to Decoder.Width - 1 do
if Row[X] = 1 then
Inc(Result);
finally
Decoder.Free;
end;
end;
Những ranh giới này đáng được nói thẳng. Hỗ trợ random-access là một tính năng tổ chức tệp, không phải một API trang ngẫu nhiên: TPLJBIG2Decoder vẫn trả về bitmap của trang đầu tiên, và không có lệnh gọi nào để chọn trang 7 trong một tệp 40 trang hay giải mã trang kiểu lazy. Các segment có độ dài dữ liệu không xác định bị từ chối trong tệp random-access, và các giới hạn sẵn có về độ dài prefix custom Huffman lẫn số mục bảng không đổi. Những giới hạn đó hẹp vừa đủ để một ứng dụng Delphi có thể định tuyến các ca bị từ chối đi chỗ khác qua LastError, và phần còn lại của pipeline ảnh, từ trích xuất ảnh PDF tới mã hóa JBIG2, được trình bày trên trang sản phẩm PDFlibPas Delphi PDF library