PDFlibPas 3.539.23, ITU-T T.88 Annex D.2'deki random-access düzenini kullanan bağımsız JBIG2 dosyalarını çözer; bu düzende her segment header önce gelir ve segment verisi aynı sırayı izler. PDFlibJBIG2.pas içindeki yerli Pascal decoder, zorunlu end-of-file header'ına kadar header offset'lerini indeksler, segment numaralarının arttığını ve bildirilen veri uzunluklarının tam olarak kalan baytlara eşit toplandığını kontrol eder, sonra her gövdeyi header sırasında, sıkıştırılmış veriyi kopyalamadan ya da yeniden sıralamadan çözer. Bu sürümden önce aynı dosya, header bayrakları okunduğu an dümdüz bir "random-access organisation is not supported" hatası fırlatıyordu
Random-access JBIG2 dosyaları nadirdir; tam da bu yüzden ortaya çıktıklarında can yakarlar. Bunlar, bir okuyucunun tek bir sıkıştırılmış bayta dokunmadan önce her segment header'ı — dolayısıyla her sayfa ve sözlük bağımlılığını — görmesini isteyen arşiv hatlarından ve belge görüntüleme sistemlerinden çıkar. Taranan arşivleri PDF'e toplu dönüştüren bir Delphi uygulaması bunlardan biriyle genelde işin ortasında, yüzlerce sıralı dosya sorunsuz geçtikten sonra karşılaşır ve iyi biçimli bir dosyada aniden duran bir decoder, çöp render eden bir decoder'dan ancak biraz daha iyidir. Aynı sürüm hattı decoder'a daha yeni JBIG2 özel Huffman tabloları ve kanonik önek kodlarını öğretmişti; random access, decoder'ın belgelenmiş yetenek sınırlarında kalan son düzen boşluğuydu
JBIG2 random-access düzeni nedir?
Random-access düzeni, T.88 Annex D'nin aynı segmentleri dizmeye izin verdiği üç yoldan biridir: sequential (D.1) her header'ı verisiyle iç içe dizer, random-access (D.2) tüm header'ları başa tüm veriyi sona koyar, embedded (D.3) ise PDF gibi başka kapsayıcıların içinde kullanılan headersız biçimdir. Bağımsız bir .jb2 dosyası sekiz baytlık 97 4A 42 32 0D 0A 1A 0A tanımlayıcısıyla başlar, ardından bir baytlık flags ve sayfa sayısı biliniyorsa dört baytlık sayfa sayısı gelir. Flags baytının 0. biti düzeni seçer: 1 sequential, 0 random-access demektir; 1. bitin set olması sayfa sayısının bilinmediğini ve dört baytlık sayının bulunmadığını gösterir. PDFlibPas bunları checkHeader ve setFileHeaderFlags içinde okur ve rezerve 2 ile 7 arası bitler reddedilmek yerine hoşgörülür
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = random-access (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = sayfa sayısı yok
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// PDF akışı: dosya header'ı yok, embedded düzen, tek sayfa
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
PDF'in kendisi bu dizilimi asla taşımaz. ISO 32000-1 §7.4.7'de tanımlanan bir JBIG2Decode görüntü akışı, embedded düzende yalnızca sayfa segmentlerini tutar; paylaşılan sembol sözlükleri ayrı bir JBIG2Globals akışına taşınmıştır ve dosya header'ı, end-of-page ya da end-of-file segmenti yoktur. decodeJBIG2 sekiz baytlık tanımlayıcıyı bulamadığında tam olarak bunu varsayar ve sıralı, tek sayfalık çözmeye zorlar. Yerli JBIG2 görüntü dışa aktarımı ters yönde gider ve PDF segmentlerini flags baytı $03 olan, bilinmeyen sayfa sayılı sıralı ve sonuna eklenmiş bir end-of-file header'ı taşıyan bağımsız bir dosyaya sarar. Dolayısıyla random-access çalışması yalnızca tek yola dokunur: doğrudan TPLJBIG2Decoder'a verilen bağımsız dosyalar; genelde PDF için dönüştürülmeye ya da yeniden sıkıştırılmaya girmeden önceki aşamadır, çıktı tarafını ise PDFlibPas'taki JBIG2 encoder backend'leri yönetir
Bir random-access dosyası dosya sırasında neden okunamaz?
Bir random-access dosyası dosya sırasında okunamaz, çünkü bayt akışında header bloğunun nerede durup veri bloğunun nerede başladığını işaretleyen tek şey, end-of-file segment header'ının kendisidir. JBIG2 segment header'ları değişken uzunluktadır: referred-to segment sayısı üç bitlik kısa biçim ya da retention bitmap'li uzun biçim olabilir, referred-to segment numaraları segmentin kendi numarasına göre bir, iki ya da dört bayt tutar ve sayfa ilişki alanı bir ya da dört bayttır. Naif bir sıralı okuyucu ilk header'ı ayrıştırır, veri uzunluğunu okur ve sonra ikinci header'ın ilk baytlarını o segmentin verisi sayar. Decoder çok daha geçene kadar yanıldığını anlayamaz; eski kodun düzeni denemek yerine tümüyle reddetmesinin sebebi budur
PDFlibPas random-access segment header'larını nasıl indeksler?
PDFlibPas random-access header'larını tek bir ön taramada indeksler; IndexRandomHeaders her header'ı ayrıştırır, yalnızca bayt offset'ini kaydeder ve ilk end-of-file header'ında (segment tipi 51) durur. Her header tamamen ayrıştırılır ve atılır, böylece indeks nesne listesi değil bir tamsayı dizisidir ve ön tarama ilerledikçe bildirilen veri uzunluklarını biriktirir. Tarama bittiğinde okuyucu ilk segmentin verisinin ilk baytının üzerinde durur ve o konum NextBodyOffset olur
// IndexRandomHeaders, TJBIG2StreamDecoder.readSegments içinde yerel
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); // Int64 biriktirici
HeaderOffsets[HeaderCount] := Offset; // parçalarla büyütülür
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;
O döngüdeki her kontrol, bir random-access dosyasının sıralı olandan daha az artıklık taşımasının sonucudur. Segment numaraları işaretsiz değerler olarak karşılaştırılarak kesin artmalıdır, çünkü aynı numarayı talep eden iki header, sonraki bir bölgenin referred-to listesinin hangi gövdeyi kastettiğini belirsizleştirir. Veri uzunluğu alanı handleSegmentDataLength tarafından okunur; en yüksek biti set olan herhangi bir değeri, 0xFFFFFFFF "unknown length" işareti dahil, -1'e eşler; random-access dizilimde bir sonraki gövdenin nerede başladığını bulmanın başka yolu yoktur, bu yüzden PDFlibPas end işareti aramak yerine o uzunluğu hemen reddeder. Toplam her iki yönde de kalan baytlara tam eşit olmalı ve son gövdeden sonraki tek bir artık bayt "trailing random-access data" ile başarısız olur. Bu katılık bilinçlidir: bu dizilimde uzunluk uyumsuzluğu, hata noktasından sonraki her gövdenin kaydığı anlamına gelir ve tek başıbo baytı omuz silkerek geçen bir decoder, onun zararsız dolgu mu yoksa yanlış hizalanmış verinin ilk belirtisi mi olduğunu bilemez
Sondaki end-of-page segmenti neden kayboldu?
Sondaki end-of-page segmenti, decode döngüsünün ilk sürümü sıralı bitiş testini koruduğu için kayboldu; while not reader.isFinished, oysa random-access dizilimde veri akışı header indeksinden önce tükenir. End-of-page (tip 49) ve end-of-file segmentleri sıfır bayt veri taşır ve normalde dosyanın son header'larıdırlar. Son bölge gövdesi tüketildikten sonra okuyucu tam olarak tamponun sonunda durur; döngü çıkar ve o sıfır uzunluklu segmentler hiç dağıtılmaz, sayfa yarım kalır. Düzeltme, random-access döngüsünün bayt yerine header saymasını sağlar. Her yineleme okuyucuyu sıradaki indeksli header'a atlar, önceki gövde bayt ortasında bitmiş olabileceği için bitPointer'ı 7'ye sıfırlar, o header'ı yeniden ayrıştırır, sonra bytePointer'ı NextBodyOffset'a taşır ve gövdenin ötesine ilerletir. Mevcut segment işleyicileri, referred-to segment kontrolleri ve Context tanılamaları değişmeden çalışır ve bir hata iletisi hâlâ gövde konumunu değil header'ın özgün bayt offset'ini bildirir
// TJBIG2StreamDecoder.readSegments, ana döngü
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; // kısmi bayttan sonra yeniden hizala
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // hata bağlamında kullanılır
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // bu segmentin verisine atla
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... mevcut segment işleyicisine dağıt, sonra DataEnd'e konumlan
end;
Random-access doğrulaması gerçekte neyi kanıtlar?
Doğrulama, yeniden düzenlenen baytların sıralı özgünleriyle aynı piksellere çözüldüğünü ve bozuk random-access girdinin temizce başarısız olduğunu kanıtlar; rastgele encoder'lardan gelen random-access dosyalarının kapsandığını kanıtlamaz. Paylaşılan Pascal regresyonu, bilinen sayfa sayısıyla ve sayı alanı çıkarılmış hâliyle, 7'ye 1'lik siyah piksel satırına çözülmesi gereken özel tablolu bir fikstür üzerine kurulu 235 baytlık sentetik bir dosya kullanır; sonra decoder'a o dosyanın her kırpılmış önekini, yinelenmiş bir segment numarasını, tek bir artık baytı ve bilinmeyen bir veri uzunluğunu verir ve her seferinde LoadFromByteArray'in False döndürdüğünü, Width ile Height'in sıfırda kaldığını savunur. Gerçek görüntü vakası, segmentleri her özgün header'ı ve sıkıştırılmış baytı korunarak random-access dizilime yeniden düzenlenmiş 500'e 473'lük özel tablolu bir refinement görüntüsüdür; SHA-256'sı incelenmiş sıralı baseline ile tam eşleşir. O dosya bir düzen dönüşümünün ürettiği türetilmiş bir dosyadır, doğada bulunmuş gerçek bir random-access belge değildir ve böyle bir doğal örnek mevcut değildi. Takımlar, mevcut üç sıralı piksel vakasıyla birlikte, Delphi Win32'de 1.598 testte, Delphi Win64 görüntü takımında 42'de, FPC Win32'de 48'de ve FPC Win64'te 46'da geçti
Random-access bir .jb2 dosyasını yüklemek ve sınırları
Uygulama kodu değişmez: TPLJBIG2Decoder.LoadFromByteArray dosya header'ını ve düzeni kendi kendine saptar, reddedilen her girdide gerekçeyi LastError'a yazarak False döndürür ve çözülen sayfayı piksel başına bir bayt döndüren Width, Height ve GetScanline üzerinden ortaya koyar
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
// Sıralı ve random-access bağımsız dosyalar aynı çağrıyı alır
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;
Sınırları açıkça söylemek yerinde olur. Random-access desteği bir dosya düzeni özelliğidir, rastgele sayfa API'si değil: TPLJBIG2Decoder hâlâ ilk sayfanın bitmap'ini döndürür ve 40 sayfalık bir dosyadan 7. sayfayı seçecek ya da sayfaları tembel çözecek bir çağrı yoktur. Bilinmeyen veri uzunluklu segmentler random-access dosyalarında reddedilir ve özel Huffman önek uzunlukları ile tablo girdisi sayılarındaki mevcut sınırlar değişmedir. Bu sınırlar, bir Delphi uygulamasının reddedilen vakaları LastError üzerinden başka yere yönlendirebileceği kadar dardır ve görüntü hattının gerisi, PDF görüntü çıkarımından JBIG2 kodlamasına, PDFlibPas Delphi PDF kütüphanesi ürün sayfasında kapsanır