Teknik Makale

Delphi'de JBIG2 random-access dosyalarını çözmek

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

PDFlibPas'ta JBIG2 dosya düzenleri: setFileHeaderFlags'in okuduğu flags baytı iç içe header'lı sequential D.1'i, tüm header'ları veri bloğundan önce koyan random-access D.2'yi ya da sözlükleri JBIG2Globals'ta taşıyan JBIG2Decode akışının kullandığı headersız embedded D.3'ü seçer
Üç dizilim aynı segmentleri taşır ama yalnızca random access, okuyucunun sıkıştırılmış bir bayta dokunmadan önce her sayfa ve sözlük bağımlılığını görmesini sağlar; arşiv hatlarının istemesinin sebebi budur
// 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

PDFlibPas'ta IndexRandomHeaders ön taraması: her segment header ayrıştırılıp atılır, yalnızca bayt offset'i saklanır; segment numaraları kesin artmalı, 0xFFFFFFFF bilinmeyen uzunluk reddedilmeli, tarama tip 51 end-of-file header'ında durmalı ve bildirilen uzunluklar kalan baytlara tam eşit olmalı
Katılık bilinçli: header'ların verinin tek haritasını verdiği bir dizilimde tek başıbo bayt, sonraki her gövdenin kaymış olabileceği anlamına gelir; buna hoşgören bir decoder dolguyu yanlış hizadan ayırt edemez
// 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

PDFlibPas'ta random-access decode döngüsü: her yineleme mevcut header'ın HeaderOffsets'ine konumlanır, bayt ortası kuyrukları geri almak için bitPointer 7'ye sıfırlanır, gövde için NextBodyOffset'e atlanır ve bayt yerine header sayılır; böylece sıfır uzunluklu end-of-page segmentleri döngü bitmeden dağıtılır
End-of-page ve end-of-file segmentleri sıfır bayt veri taşıdığı için veri akışı header indeksinden önce tükenir; o son segmentlere sırasını yalnızca header sayan bir döngü verebilir
// 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