Teknik Makale

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

Elle yazılmış bir JBIG2 okuyucusunun alışılmadık ilk dosyada yaptığı hata şudur: dosya başlığını ayrıştırır, ilk segment başlığını ayrıştırır, dört baytlık veri uzunluğunu okur ve o kadar baytı segment gövdesi sayar. Çoğu .jb2 dosyasında bu tamamen doğrudur. Bayrak baytının 0. biti temizlenmiş bir dosyada ise o "gövde" baytları aslında ikinci, üçüncü ve dördüncü segment başlıklarıdır ve aritmetik çözücü onları gürültü dolu bir bit eşlemeye çevirir ya da tamponun sonuna atlar. Dosya bozuk değildir. ITU-T T.88 Annex D.2'deki random-access düzenini kullanır: her segment başlığı önce gelir, bütün segment verileri sonra, aynı sırayla

Random-access dosyalar nadirdir. Arşiv ve belge görüntüleme sistemlerinden çıkarlar; okuyucu sıkıştırılmış bir bayta dokunmadan önce her sayfayı ve sözlük bağımlılığını görsün isterler. Bir dönüştürme işi böyle bir kaynaktan gelen dosyaları sıradan taramalarla karıştırırsa, bir random-access dosya öbür yönüyle tamamen sıralı bir grubun ortasına düşebilir. Onları okuyan çözücü, JBIG2 özel Huffman tabloları ve kanonik önek kodları işini gören aynı yerel Pascal çözücüdür; kodlama tarafı değişmez, değişen yalnızca segmentlerin bulunma sırasıdır

Tek bir bayrak baytının ardındaki üç dosya düzeni

PDF Library for Delphi, bağımsız JBIG2 dosyalarını PDFlibJBIG2 birimindeki TPLJBIG2Decoder üzerinden okur ve çözücü düzeni dosya başlığından kendisi çıkarır. T.88 Annex D aynı segmentlerin üç türlü dizilişine izin verir: sequential (D.1) her başlığı doğrudan verisinin önüne koyar, random-access (D.2) bütün başlıkları öne, bütün verileri ardına alır, embedded (D.3) başka kapsayıcıların içinde kullanılan başlıksız biçimdir. Bağımsız bir dosya sekiz baytlık 97 4A 42 32 0D 0A 1A 0A tanıtıcısıyla başlar, ardından bir bayrak baytı, sonra — bayraklar sayının bilinmediğini söylemedikçe — dört baytlık sayfa sayısı gelir

Bayrak baytının 0. biti düzeni seçer: 1 sequential, 0 random-access demektir. 1. bit kuruluysa sayfa sayısı bilinmiyordur ve dört baytlık sayı yoktur; ilk segment başlığı bayt 13 yerine bayt 9'da başlar. 2'den 7'ye bitler rezervdir; çözücü, kurulu rezerv bitlerin segmentlerin kendisinin okunabilirliği hakkında hiçbir şey söylemediğini düşünerek onları kurmuş dosyaları reddetmek yerine kabul eder

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

PDF'in kendisi random-access düzenini hiç taşımaz. ISO 32000-1 §7.4.7'de anlatılan bir JBIG2Decode görüntü akışı yalnızca sayfa segmentlerini embedded biçimde tutar; paylaşılan sembol sözlükleri ayrı bir JBIG2Globals akışına taşınır ve dosya başlığı, end-of-page ya da end-of-file segmenti yoktur. Çözücü sekiz baytlık tanıtıcıyı bulamadığında girdiyi tam olarak o türden bir akış sayar: sequential, tek sayfa. Ters yönü de öyledir: PDF Library for Delphi bir JBIG2Decode görüntüsünü yerli bir .jb2 dosyasına aktardığında bayrak baytı $03 yazar (sequential, sayfa sayısı bilinmiyor) ve bir end-of-file segment başlığı ekler. Yani random access yalnızca dışarıdan gelir; TPLJBIG2Decoder'a doğrudan verilen, genellikle PDFlibPas'taki JBIG2 kodlayıcı arka uçlarıyla PDF'e dönüştürülmek ya da yeniden sıkıştırılmak üzere gelen bağımsız bir dosya olarak

Bir random-access dosyayı dosya sırasında neden okuyamazsınız?

Çünkü bayt akışında başlık bloğunun nerede bitip veri bloğunun nerede başladığını işaretleyen tek şey, end-of-file segment başlığının (tür 51) kendisidir. JBIG2 segment başlıklarının sabit boyutu yoktur. Referans verilen segment sayısı ya üç bitlik kısa biçimdir ya da bir saklama bit eşlemesinin izlediği uzun biçim; referans verilen her segment numarası, o anki segmentin kendi numarasının büyüklüğüne göre bir, iki ya da dört bayt tutar; sayfa ilişkilendirme alanı bir ya da dört bayttır. Bir okuyucu, sonraki başlığın nerede başladığını öğrenmek için bir başlığın her alanını ayrıştırmak zorundadır

Dosyayı sırayla gezen bir sequential okuyucu ilk başlığı doğru alır, ardından gelen her şeyi yanlış alır. Daha kötüsü, bir süre fark etmeyebilir: genel bir bölge çözücüsüne verilen başlık baytları, çok sonra bir uzunluk ya da referans denetimi sonunda tetiklenene dek hata değil piksel üretir. Bizim görüşümüz, başlıkları önce indekslemeyen bir çözücünün random-access dosyaları düpedüz reddetmesi. Net bir red, gruptaki tek dosyaya mal olur; makul görünen ama yanlış bir bit eşleme bir arşive girip orada kalabilir

İlk gövdeden önce her başlığı indekslemek

Bayrak baytı random-access dediğinde PDF Library for Delphi, hiçbir şey çözmeden önce başlık bloğu üzerinde tek bir geçiş yapar. Her başlık eksiksiz ayrıştırılır ve sonra atılır; saklanan tek şey dosyadaki bayt uzaklığıdır. Sıkıştırılmış veri hiçbir zaman yeniden sıralanmış ikinci bir tampona kopyalanmaz; büyük arşiv taramalarında veri bloğu dosyanın çoğunu tutabildiği için bu önemlidir. Geçiş ilk end-of-file başlığında durur; o anda okuma konumu ilk segmentin verisinin ilk baytının üzerinde durur

Random-access dosyanın sıralı olandan daha az artığı vardır; bu yüzden ön tarama katıdır. Şu durumlarda dosyayı reddeder:

  • bir başlık tamponun sonunda kesilmişse; JBIG2 truncated random-access header at byte N olarak, o başlığın başladığı uzaklıkla birlikte bildirilir
  • segment numaraları kesin artmıyorsa (işaretsiz değerler olarak karşılaştırılır); çünkü aynı numarayı isteyen iki başlık, sonraki bir bölgenin referans listesini belirsiz kılar
  • bir veri uzunluğunun en üst biti kuruluysa; 0xFFFFFFFF "bilinmeyen uzunluk" işareti de buna dâhildir. Bu dizilimde sonraki gövdenin nerede başladığını bulmanın başka yolu yoktur; çözücü bir uç işareti için ileri taramak yerine hemen reddeder
  • end-of-file başlığı veri bildiriyorsa ya da hiç görünmüyorsa (JBIG2 random-access file is missing its end-of-file header)
  • bildirilen uzunlukların toplamı kalan baytlardan fazlaysa ya da azsa
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

İnsanların takıldığı kural sondur. Son gövdeden sonraki tek bir fazladan bayt, JBIG2 trailing random-access data ile başarısız olur. Sequential yola kıyasla sert görünür; orada çözüm end-of-file segmentinde durur ve sonrasındaki her şey yok sayılır. Asimetrinin bir nedeni vardır. Sequential dizilimde her başlık kendi verisinin hemen yanında durur; sondaki çöp hiçbir şeyi kaydıramaz. Random-access dizilimde başlıklar veri bloğunun tek haritasıdır ve uzunluklar tutmuyorsa çözücü, zararsız dolguyu çok daha önce kaybolmuş ya da eklenmiş ve ardındaki her gövdeyi kaydırmış bir bayttan ayırt edemez

Sondaki end-of-page segmenti neden kayboldu?

Bayt kaldıkça koşan bir döngü random-access dosyada erken biter; çünkü veri, başlık indeksinden önce tükenir. End-of-page (tür 49) ve end-of-file segmentleri sıfır bayt veri taşır ve normalde sonda gelir. Son bölge gövdesi tüketildiğinde okuma konumu tam olarak tamponun sonundadır; bayt sayan döngü oradan çıkar ve sıfır uzunluklu end-of-page segmenti hiç dağıtılmaz, sayfa yarım kalır

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

Bu yüzden random-access dosyalarda çözücü bayt değil başlık sayar. Her adım bir sonraki indeksli başlığa geri atlar, bayt sınırına hizalanır (aritmetik kodlu bir gövde baytın ortasında bitebilir), başlığı yeniden ayrıştırır, sonra sonraki gövdenin başladığı yere atlar ve bildirilen uzunluk kadar onu geçer. Çözücüsü erken biten gövde bildirilen sonuna atlanır; onu aşan okunan gövde reddedilir. Oradan itibaren segment, sequential bir dosyadakiyle aynı işleyicilere ve referans denetimlerine girer. Gövde çözümü sırasında fırlayan hatalar başlığın özgün konumunu taşır: JBIG2 segment N (type T) at byte B: ... biçiminde; böylece LastError'daki uzaklık hex düzenleyicide bulabileceğiniz bir yeri gösterir

Delphi'de bir random-access .jb2 dosyası yüklemek

Uygulama kodu düzenle birlikte değişmez. TPLJBIG2Decoder.LoadFromByteArray dosya başlığını ve düzeni saptar, reddedilen her girdide False döndürür ve nedeni LastError'a yazar; çözülen sayfayı, siyah için 1 yazan, piksel başına bir bayt dolduran Width, Height ve GetScanline üzerinden açığa eder

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;

Bir dosyanın hangi düzeni kullandığını bilmek isterseniz — günlük tutmak ya da bir dönüştürme koşusu öncesi bir korpusu ayıklamak için — bayrak baytını kendiniz okuyun. Kütüphane saptadığı düzeni açığa etmez ve dokuz bayt yeter. Aşağıdaki yardımcı kendi kodunuza ekleyeceğiniz türdendir; yalnızca T.88 başlık kurallarını yeniden söyler:

type
  TJb2Layout = (jlNoFileHeader, jlSequential, jlRandomAccess);

// Kendi yardımcınız, PDFlibJBIG2'nin parçası değil: bir tamponu T.88 dosya başlığına göre sınıflandırır
function DetectJb2Layout(const Data: TJBIG2ByteArray;
  out PageCountKnown: Boolean): TJb2Layout;
const
  Signature: array[0..7] of Byte = ($97, $4A, $42, $32, $0D, $0A, $1A, $0A);
var
  I: Integer;
begin
  Result := jlNoFileHeader;          // örneğin bir PDF'ten çıkarılmış gömülü akış
  PageCountKnown := False;
  if Length(Data) < 9 then
    Exit;
  for I := 0 to 7 do
    if Data[I] <> Signature[I] then
      Exit;
  PageCountKnown := (Data[8] and 2) = 0;   // bit 1 kurulu: dört baytlık sayfa sayısı yok
  if (Data[8] and 1) = 0 then
    Result := jlRandomAccess
  else
    Result := jlSequential;
end;

Toplu işler: dolgulu tamponlar ve reddedilen dosyalar

Sonda-veri kuralı, toplu iş kodunun ters gitmeye en yatkın olduğu yerdir; başka yerde açılan bir dosya bu yüzden reddedilirse dosyadan önce tampona bakın. Veritabanı BLOB sütunundan çekilen bir .jb2, sabit boyutlu bir ağ okuması ya da hizalama dolgusu taşıyan bir kapsayıcı biçimi, gerçek sonun ardından birkaç sıfır baytla gelebilir. Sequential dosya bunu silker. Aynı dolguyu taşıyan random-access dosya JBIG2 trailing random-access data ile başarısız olur; birkaç bayt kısa kesilmiş tampon da kesilmiş olarak başarısız olur. Tam olarak dosyanın baytlarını geçin, boyutu kaynağın gerçek uzunluğundan alın; asla parça boyutundan ya da kapasiteden değil

Reddedilen dosya geride hiçbir şey de bırakmaz. LoadFromByteArray, çözmeye başlamadan önce Width, Height ve scanline'ları sıfırlar; başarısız çağrı önceki dosyanın sayfasını değil sıfır boyutları bildirir ve aynı çözücü örneği doğrudan sıradaki dosyaya geçebilir:

procedure DecodeBatch(const Files, Log, Quarantine: TStrings);
var
  Decoder: TPLJBIG2Decoder;
  I: Integer;
begin
  Decoder := TPLJBIG2Decoder.Create;
  try
    for I := 0 to Files.Count - 1 do
      if Decoder.LoadFromByteArray(ReadJb2(Files[I])) then
        Log.Add(Format('%s: %d x %d', [Files[I], Decoder.Width, Decoder.Height]))
      else
      begin
        // Burada Width ve Height 0'dır; önceki dosyadan hiçbir şey kalmaz
        Log.Add(Files[I] + ': ' + Decoder.LastError);
        if Pos('trailing random-access data', Decoder.LastError) > 0 then
          Quarantine.Add(Files[I]);   // dosyayı suçlamadan önce baytların nasıl saklandığını denetleyin
      end;
  finally
    Decoder.Free;
  end;
end;

Bunu kendi hattınızda denetlemek nadir örnek dosyalar gerektirmez. Sequential bir .jb2'yi random-access düzene örgütleyin; her başlığı ve her sıkıştırılmış baytı koruyarak, end-of-page ve end-of-file başlıklarını başlık bloğunun sonuna taşıyın ve çözülen pikseller sequential özgünle birebir eşleşmeli — sayfa sayısı alanıyla da onsuz da. Sonra ona her kesilmiş öneki, çiftlenmiş bir segment numarası, tek bir fazladan sondaki baytı ve 0xFFFFFFFF uzunluğunu verin; her biri False döndürmeli, Width ile Height sıfırda kalmalı

Çözücünün döndürdüğü ve beklediği

Buradaki random access, dosya düzeninin özelliğidir. TPLJBIG2Decoder, dosya hangi düzeni kullanırsa kullansın ilk sayfanın bit eşlemesini döndürür ve Width ile Height o sayfayı anlatır. Her segment veri uzunluğunu bildirmek zorundadır; T.88, hemen bir generic region'ın 0xFFFFFFFF işaretini kullanmasına izin verir ve çözücü o işareti her iki düzende de unknown or oversized segment length is not supported ile reddeder. Özel Huffman önek uzunlukları ile tablo girdi sayıları, sequential dosyalardakiyle aynı biçimde sınırlandırılır

JBIG2 çözmek, PDF görüntü ayıklamayı, yeniden sıkıştırmayı ve JBIG2 kodlamayı da kapsayan daha uzun görüntü yolunun bir aşamasıdır; PDF Library for Delphi ürün sayfası hattın geri kalanını ve desteklenen görüntü biçimlerini anlatır