Teknik Makale

Saf Pascal PDF Çözücüsünde JBIG2 Özel Huffman Tabloları

PDFlibPas 3.539.22 sürümü JBIG2 özel Huffman tablolarını yerel olarak çözer: PDFlibJBIG2.pas içindeki saf Pascal çözücü Tables segmentini (tip 53) ayrıştırır, ITU-T T.88 Ek B.3'ün gerektirdiği gibi tablo satırı sırasına göre kurallı önek kodları atar, sembol sözlükleri ve metin bölgeleri için özel tablo başvurularını seçici sırasında tüketir ve her okumayı, arkadan gelen baytlar ne olursa olsun, bildirilen segment uzunluğuyla sınırlar

Bu işi zorunlu kılan dosya yüzeyde sıradan görünüyordu. Tarayıcıdan geçmiş bir sözleşme; JBIG2 ile, hem de çok daha yaygın olan aritmetik kodlama yerine Huffman sembol kodlamasıyla sıkıştırılmış ve kodlayıcı standart B.1 - B.15 tabloları yerine kendi kod tablolarını göndermiş. İki bağımsız çözücü dosyanın iyileştirme pikselleri üzerinde anlaşamadı ve o dönemdeki PDFlibPas çözücüsü, sanki bir öğütücüden geçmiş gibi görünen metin üretti: birkaç piksel kaymış glif parçaları, her karakterin eksik bir sütunu. Hiçbir şey hata vermedi. Yıllarca ayakta kalan hata tam olarak bu biçimdedir, çünkü bir dosyayı reddeden çözücü bir destek kaydı alır, onu azıcık yanlış render eden çözücü ise taramanın bozuk olduğunu varsayan bir müşteri

Bir JBIG2 Tables segmenti tam olarak ne içerir?

Bir Tables segmenti tek bir Huffman tablosunun derli toplu tarifidir: bir bayt bayrak, iki işaretli 32 bit sınır ve ardından sınırlar arasındaki aralığı bölümleyen (önek uzunluğu, aralık uzunluğu) ikililerinden oluşan bir dizi; düzenlemesi T.88 §7.4.13 ve Ek B.2'de verilir. Bayrak baytının 0. biti HTOOB'dur ve tablonun bant dışı bir kodu olup olmadığını söyler. 1-3. bitler artı bir HTPS'i, yani her önek uzunluğunu yazmak için kullanılan bit sayısını verir; 4-6. bitler artı bir ise HTRS'i, her aralık uzunluğu alanının genişliğini verir. 7. bit ayrılmıştır ve PDFlibPas, gelecekteki bir revizyonun onunla ne demek istediğini tahmin etmek yerine set edilmişse segmenti reddeder. HTLOW ve HTHIGH işaretli 32 bit tamsayı olarak gelir ve bir çözücünün yanlış yapabileceği ilk yer burasıdır: onları işaretsiz okumak, alt sınırı negatif olan bir tabloyu, ki bu delta kodlanmış sembol genişlikleri için tamamen normaldir, dört milyardan başlıyormuş gibi gösterir. Her alan, isteği segment verisinin bittiği bit konumuna karşı denetleyen yerel bir ReadField yardımcısından geçer, çünkü kendi segmentinin ötesini okuyan bir tablo, sıradaki segment başlığını önek uzunluğu sanarak tüketir

PDFlibPas'te JBIG2 özel Huffman çözmenin arkasındaki Tables segment düzeni: HTOOB, HTPS ve HTRS taşıyan ve reddedilen ayrılmış bir bit içeren bir bayrak baytı, işaretli HTLOW ve HTHIGH sınırları, önek ve aralık uzunluğu ikililerinden oluşan bir dizi ve jbig2HuffmanLOW kaçış satırı, sabit 32 bitlik bir üst satır ile isteğe bağlı jbig2HuffmanOOB
Segmentin her alanı sınır denetimli bir yardımcıdan okunur, çünkü bildirilen sonunun ötesini okuyan bir tablo sıradaki segment başlığını önek uzunluğu sanarak tüketir; sentinel kaçış satırları da yerleşik standart tablolarla aynıdır
// TCodeTableSegment.readSegment, PDFlibJBIG2.pas
EndBit := (Int64(decoder.reader.bytePointer) +
  segmentHeader.getSegmentDataLength) * 8;
Flags := ReadField(8);
if (Flags and $80) <> 0 then
  raise EJBIG2DecodeError.Create('reserved custom Huffman table flag');
PrefixBits := ((Flags shr 1) and 7) + 1;   // HTPS
RangeBits  := ((Flags shr 4) and 7) + 1;   // HTRS
LowValue   := Integer(ReadField(32));      // signed HTLOW
HighValue  := Integer(ReadField(32));      // signed HTHIGH
if LowValue >= HighValue then
  raise EJBIG2DecodeError.Create('invalid custom Huffman range bounds');
CurrentValue := LowValue;
while CurrentValue < HighValue do
begin
  PrefixLength := ReadField(PrefixBits);
  RangeLength  := ReadField(RangeBits);
  if RangeLength > 32 then
    raise EJBIG2DecodeError.Create('invalid custom Huffman range length');
  AddLine(CurrentValue, PrefixLength, RangeLength);
  Inc(CurrentValue, Int64(1) shl RangeLength);
end;
AddLine(LowValue - 1, ReadField(PrefixBits), jbig2HuffmanLOW);
AddLine(HighValue,   ReadField(PrefixBits), 32);
if (Flags and 1) <> 0 then
  AddLine(0, ReadField(PrefixBits), jbig2HuffmanOOB);

Döngüden sonra eklenen iki satır Ek B.2'deki kaçış satırlarıdır: alt aralık satırı HTLOW eksi birden başlayıp aşağı doğru sayar, üst aralık satırı HTHIGH'dan sabit 32 bitlik bir aralıkla başlar ve isteğe bağlı OOB satırının hiç değeri yoktur. PDFlibPas bu satırları jbig2HuffmanLOW ($FFFFFFFD) ve jbig2HuffmanOOB ($FFFFFFFE) sentinel aralık uzunluklarıyla işaretler; on beş yerleşik standart tablonun da kullandığı bu kural sayesinde çözme döngüsü, tablonun spesifikasyondan mı dosyadan mı geldiğini umursamaz

Önek kodları neden tablo satırı sırasına göre atanmalıdır?

Çünkü bit desenlerini hiçbir zaman kodlayıcı yazmaz. Bir JBIG2 Tables segmenti yalnızca önek uzunluklarını taşır ve iki taraf da gerçek bit desenlerini Ek B.3'teki kurallı yordamla yeniden kurar: her uzunluktan kaç satır olduğunu say, önce bir bit uzunluğundaki kodları dağıt, sonra sola kaydırıp devam et ve aynı uzunluk içinde kodları satırların görünme sırasına göre ver. Bu sıradan her sapma sessizce farklı bir tablo üretir. Çözücü bunu fark etmez, çünkü ürettiği her bit deseni hâlâ geçerli bir önek kodudur; yalnızca kodlayıcının kullandığı değildir ve çıktı, yanlış sembollerden bir araya getirilmiş akla yatkın görünen bir bitmap'tir

PDFlibPas JBIG2 çözücüsünde kurallı önek kodu ataması: segmente yalnızca önek uzunlukları gelir, Counts, Starts ve Positions üzerinden kararlı bir sayma sıralaması her uzunluk içinde bildirim sırasını korur, bir bitlik kodlar önce dağıtılır ve kod her uzunlukta sola kaydırılır, aşırı yükleme ise Kraft denetimiyle reddedilir
Bit desenlerini hiçbir zaman kodlayıcı yazmaz, bu yüzden tablo satırı sırasından her sapma sessizce farklı ama geçerli bir önek kodu kurar ve çıktı akla yatkın görünür; sıfır uzunluklu satırlar kullanılmadan düşer ve 32 bitten uzun önekler reddedilir
// THuffmanDecoder.buildTable, PDFlibJBIG2.pas
FillChar(Counts, SizeOf(Counts), 0);
for I := 0 to length - 1 do
begin
  if table[I].prefixLen > 32 then
    raise EJBIG2DecodeError.Create(
      'Huffman prefixes longer than 32 bits are not supported');
  Inc(Counts[table[I].prefixLen]);
end;
Active := 0;
Code := 0;
for Bits := 1 to 32 do
begin
  Starts[Bits]    := Active;
  Positions[Bits] := Active;
  Inc(Active, Counts[Bits]);
  if Code + UInt64(Counts[Bits]) > (UInt64(1) shl Bits) then
    raise EJBIG2DecodeError.Create('oversubscribed Huffman prefix codes');
  Code := (Code + UInt64(Counts[Bits])) shl 1;
end;
SetLength(Result, Active + 1);
for I := 0 to length - 1 do            // kararlı: özgün sıra korunur
  if table[I].prefixLen > 0 then       // her önek uzunluğunun kendi içinde
  begin
    Result[Positions[table[I].prefixLen]] := table[I];
    Inc(Positions[table[I].prefixLen]);
  end;
Code := 0;
for Bits := 1 to 32 do
begin
  for I := Starts[Bits] to Positions[Bits] - 1 do
  begin
    Result[I].prefix := Cardinal(Code);
    Inc(Code);
  end;
  Code := Code shl 1;
end;
Result[Active].rangeLen := jbig2HuffmanEOT;

THuffmanDecoder.buildTable bir karşılaştırma sıralaması değil sayma sıralamasıdır ve bunun tek bir sebebi var: Counts, Starts ve Positions üzerinden tek bir sayma geçişi yapısal olarak kararlıdır, dolayısıyla aynı önek uzunluğundaki satırlar sonuca bildirildikleri sırayla düşer ve Ek B.3'ün kodları atadığı sıra tam olarak budur. Önek uzunluğu sıfır olan satırlar kod atamasından önce düşer, çünkü B.3 onları bir bitlik kod değil kullanılmayan satırlar olarak tanımlar. Aynı döngüde iki koruma oturur. Aşırı yükleme denetimi, uzunlukların o derinlikte bir önek kodunun tutabileceğinden fazla kod iddia ettiği bir tabloyu yakalar ki bu, Kraft eşitsizliğinin tamsayı karşılaştırması olarak yazılmış hâlidir; o olmasa düşmanca bir tablo iki satırla eşleşen bir kod üretir ve çözücü hangisini önce taradıysa onu seçer. 32 bitlik tavan ise prefix alanının bir Cardinal olması ve decodeInt içindeki eşleştiricinin bitleri tek bir yerde biriktirmesi yüzünden vardır. T.88 kâğıt üzerinde daha uzun öneklere izin verir, PDFlibPas onları adıyla reddeder ve bugüne kadar böyle bir şey üreten gerçek bir kodlayıcı görülmemiştir. Değer aritmetiği de kod aritmetiği kadar özen ister: THuffmanTable.val bir Int64'tür ve alt aralık satırı val - readBits(32) olarak, yani HTLOW eksi birden çıkarılan 32 bitlik işaretsiz bir ofset olarak çözülür. Integer ara değerleriyle bu çıkarma taşar ve taşan değer ardından sembol genişliği olarak kabul edilir. 64 bitlik yol gerçek değeri hesaplar, işaretli 32 bit aralığına karşı denetler ve sığmıyorsa hata verir; böylece sessiz bir bozulma yerine açık bir ret olur

Özel tablolar 3.539.22'den önce neden hiç devreye girmedi?

İki kusur birbirini sakladı. Birincisi tek satırlık bir setter hatasıydı: TTextRegionHuffmanFlags.setFlags argümanını, içine yazdığı alanla aynı adla alıyordu, dolayısıyla Self.flagsAsInt := flagsAsInt başlatılmamış alanı kendine atıyor ve her seçici sıfır okunuyordu; bu da özel tablo isteyen metin bölgelerini standart F, H ve K tablolarından geçiriyordu. İkinci kusur ise birincisi tek başına düzeltilse bile bozuk semboller üretilmesine yol açardı. Bir Huffman sembol sözlüğü sembollerini sıkıştırılmamış kolektif bir bitmap olarak sakladığında her satırın son baytı kısmi olur ve eski kopyalama döngüsü, geçerli bit sayısını tutan padding değerini en alttaki geçerli bitin konumu sanıyordu; 63 piksel genişliğinde bir satır son baytından yedi bit yerine bir bit kopyalıyordu. Düzeltilmiş döngü for bitPointer := 7 downto ((8 - padding) and 7) biçiminde çalışır ve 7 bit ile 9 bit genişliklerindeki sentetik fikstürler bayt sınırının iki yakasını da sabitler. Seçiciler doğru okunduğunda tablolar spesifikasyonun listelediği sırayla dağıtılır; T.88 §7.4.3.1.2 bunu metin bölgeleri için FS, DS, DT, RDW, RDH, RDX, RDY ve RSIZE, §7.4.2.1.1 ise sembol sözlükleri için DH, DW, BMSIZE ve AGGINST olarak sabitler. Her iki bitlik seçici standart tablo 0 veya 1 anlamına gelir; yalnızca iki standart tablo bulunan alanlarda 2 ayrılmıştır, 3 ise özeldir ve her özel seçim, başvurulan segmentler arasından sıradaki Tables segmentini başvuru sırasında tüketir. NextCustomHuffmanTable tam olarak bu yürüyüşü yapar ve bir bölge seçicilerinin istediğinden daha az tabloya başvuruyorsa missing custom Huffman table reference hatasını verir. Aynı düzeltmeye bir satır daha aittir: girdi ve yeni sembolleri toplamı bire eşit olan bir Huffman sembol sözlüğü, log2 formülünden sıfır sembol kodu uzunluğu hesaplar; oysa biçimin Huffman varyantı her sembol kimliğini en az bir bit ile yazar, dolayısıyla TSymbolDictionarySegment içindeki if sdHuffman and (symbolCodeLength = 0) then symbolCodeLength := 1 satırı iyileştirme ve toplu yolu, sembol kimliği başına sıfır bit okumaktan alıkoyar

Segment sınırı neyi garanti eder?

PDFlibPas her başlıktaki segment veri uzunluğunu, iki tarafın da uyması gereken bir sözleşme sayar: bir segment bildirilen sonunun ötesini okuyamaz ve eksik bitirip sıradaki başlığı öngörülemeyen bir ofsette bırakamaz. Bu sözleşmeden çıkan kuralların her biri tek başına küçüktür. 31. biti set edilmiş bir veri uzunluğu, T.88 §7.2.7'nin bilinmeyen uzunluk işaretidir ve handleSegmentDataLength onu, ileriye dönük bir sonlandırıcı aramak yerine readSegments'in doğrudan reddettiği negatif bir değere eşler. Başvurulan her segment numarası geçerli segment numarasından küçük olmalı ve zaten var olmalıdır; böylece ileriye dönük ya da askıda kalan bir başvuru, herhangi bir bölge onu çözmeye kalkışmadan önce başarısız olur. END_OF_PAGE ve END_OF_FILE sıfır bayt veri bildirmelidir. Bir Profiles segmenti (tip 52), ardından o kadar 32 bitlik tanımlayıcı gelen 32 bitlik bir sayım taşır ve hiç piksel içermez; bu yüzden 4 artı sayım çarpı 4 olarak bildirilen uzunluğa karşı denetlenir, atlanır ve segment listesinde yalnızca sonraki segmentler ona numarayla başvurabilsin diye tutulur. Bilinmeyen bir profil tanımlayıcısı bilinmeyen bir kodlama değildir ve onu öyle saymak, gayet iyi çözülen dosyaları reddeder

// TJBIG2StreamDecoder.readSegments, PDFlibJBIG2.pas
DataLength := segmentHeader.getSegmentDataLength;
if DataLength < 0 then
  raise EJBIG2DecodeError.Create(Context +
    'unknown or oversized segment length is not supported');
if DataLength > Length(reader.Data) - reader.bytePointer then
  raise EJBIG2DecodeError.Create(Context + 'truncated segment data');
DataEnd := reader.bytePointer + DataLength;
for I := 0 to noOfReferredToSegments - 1 do
  if (referredToSegments[I] >= segmentHeader.getSegmentNumber) or
     (findSegment(referredToSegments[I]) = nil) then
    raise EJBIG2DecodeError.Create(Context + 'invalid segment reference');
// ... bu tip için segment nesnesini oluştur ...
reader.SegmentEnd := DataEnd;
segment.readSegment;
if reader.bytePointer > DataEnd then
  raise EJBIG2DecodeError.Create(Context +
    'decoded data exceeds declared segment length');
if reader.bytePointer < DataEnd then
begin
  reader.bytePointer := DataEnd;   // MMR, EOFB'yi okumadan bırakabilir
  reader.bitPointer := 7;
end;

Bu döngünün kuyruğu, çözücünün önceki bir sürümünün MMR kodlu bölgelerde yanlış gitmesinin yeridir. Bir MMR çözücü, son satırın son pikseli üretildiğinde işinin bittiğini bilir; bu ise T.88 §6.2.5.7'nin verinin sonuna koyduğu EOFB sonlandırıcısını tüketmeden gerçekleşebilir. Eski kod okuyucunun sıradaki başlıkta durduğunu varsayıyordu; dolayısıyla artakalan sonlandırıcı baytları bir segment numarası olarak ayrıştırıldı ve akış birkaç bayt sonra yanıltıcı bir hatayla düştü. Artık bildirilen son kazanır: ötesini okumak hatadır, erken durmak normaldir ve okuyucu, sıradaki başlık dosyanın olacağını söylediği yerden okunsun diye bit göstericisi sıfırlanarak DataEnd konumuna taşınır. Aynı disiplin, PDFlibPas'ın güvenilmeyen PDF yapılarını ayrıştırdığı her yerde görünür: sınır bildirilen uzunluktur ve çözücü daha dostça bir sınır aramaya çıkmaz

Huffman iyileştirmesi bitmap boyutunu nereden okur?

Aritmetik çözücü başlamadan önce ve yalnızca Huffman kipinde var olan bir alandan. Bir metin bölgesi örneği iyileştirme taşıdığında (RI sıfırdan farklıdır) ve SBHUFF set edildiğinde, T.88 §6.4.11 çözücüye RDW, RDH, RDX ve RDY'yi seçili tablolarıyla, ardından BMSIZE'ı RSIZE tablosuyla okumasını, sonra bir bayt sınırına hizalanmasını ve ancak ondan sonra tam olarak BMSIZE bayt üzerinde genel iyileştirme çözmesini yürütmesini söyler. Aritmetik kipli metin bölgelerinde böyle bir alan yoktur; iki kip için tek bir kod yolu paylaşan çözücü bu alanı atlar, aritmetik çözücüyü iki ya da daha fazla bayt erken başlatır ve her sembolü çöp veriye karşı iyileştirir. §6.5.8.2.2'de anlatılan, REFAGG ve tek bir iyileştirme örneği içeren sembol sözlüğü yolu da aynı BMSIZE alanını aynı sonuçlarla taşır. PDFlibPas'te bu boyutun üst sınırı, tüm akışın değil, readSegments tarafından ayarlanan geçerli segment sonunun, yani TStreamReader.SegmentEnd değeridir; çünkü ancak sonraki segmentten bayt ödünç alarak karşılanabilen bir BMSIZE bozuktur ve onu akış uzunluğuna göre doğrulamak, aritmetik çözücünün sıradaki başlığa kadar okumasına izin verirdi. İki baytlık alt sınır, aritmetik çözücünün her zaman tükettiği başlangıç bayt çiftini yansıtır ve iyileştirmeden sonra okuyucu, aritmetik çözücünün ne kadar ileri okuduğundan bağımsız olarak RefinementEnd konumuna atlar, çünkü onun son konumu sıradaki Huffman kodlu alanın konumu değildir

PDFlibPas JBIG2 çözücüsünde Huffman kipli iyileştirme sınırları: RDW, RDH, RDX ve RDY kendi tablolarından çözülür, BMSIZE RSIZE tablosundan çözülüp bayt hizalanır, ardından aritmetik çözücü RefinementEnd ile SegmentEnd arasına hapsedilmiş tam olarak BMSIZE baytı iyileştirir; iki baytın altındaki ya da segment sınırını aşan boyutlar reddedilir
İki baytlık alt sınır, aritmetik çözücünün her zaman tükettiği başlangıç çiftini yansıtır; üst sınır tüm akış değil geçerli segmenttir ve iyileştirmeden sonra okuyucu, ne kadar ileri okunmuş olursa olsun RefinementEnd konumuna atlar
// TJBIG2Bitmap metin bölgesi çözme, Huffman iyileştirme yolu
RefinementSize := huffmanDecoder.decodeInt(huffmanRSizeTable).intResult;
huffmanDecoder.consumeRemainingBits;
if (RefinementSize < 2) or
   (RefinementSize > huffmanDecoder.reader.SegmentEnd -
                     huffmanDecoder.reader.bytePointer) then
  raise EJBIG2DecodeError.Create('invalid refinement bitmap size');
RefinementEnd := huffmanDecoder.reader.bytePointer + RefinementSize;
arithmeticDecoder.start;
// ... readGenericRefinementRegion ...
if huffmanDecoder.reader.bytePointer > RefinementEnd then
  raise EJBIG2DecodeError.Create('refinement data exceeds declared size');
huffmanDecoder.reader.bytePointer := RefinementEnd;
huffmanDecoder.reader.bitPointer := 7;

Neler doğrulandı, neler hâlâ reddediliyor

Bütün bunları başlatan örnek, özel tablolar ve Huffman iyileştirmesi içeren 500'e 473 piksel bir JBIG2 görüntüsü, artık bağımsız bir çözücüye karşı sıfır farklı pikselle bir bitmap'e çözülüyor ve sentetik 7 bit ile 9 bit kolektif bitmap fikstürleri ikisinde de beklenen satırları üretiyor. Özgün örnekte anlaşamayan iki bağımsız çözücü hâlâ birbirleriyle anlaşamıyor; PDFlibPas onlardan biriyle eşleşiyor ve dürüst ifade, yerel çıktının bir bağımsız uygulamayla ve okunduğu biçimiyle spesifikasyonla uyuştuğudur; dünyadaki her çözücünün anlaştığı değil. Paketin bozuk girdi tarafı şunları kapsıyor:

  • ayrılmış bir bayrak biti ya da ayrılmış bir seçici değeri
  • bir satırın ortasında kesilmiş bir tablo
  • aşırı yüklenmiş önek uzunlukları ve 32 bitten uzun önekler
  • seçicileri başvurduğundan daha fazla özel tablo isteyen bir bölge
  • başarısız bir çözmeden sonra eski çıktının, çağıranın onu sonuç sanması için yerinde bırakılmak yerine temizlendiğinin doğrulanması

Üç sınır bilinçli olarak korunuyor. Tüm segment başlıklarının tüm segment verisinden önce geldiği rastgele erişimli akış düzeni, dosya başlığı bayrakları okunur okunmaz JBIG2 random-access organisation is not supported hatasını verir; çünkü ona karşı doğrulayacak temsili bir örnek yoktur ve yarım uygulanmış bir yol, adı konmuş bir retten kötüdür. Özel tablolar 65.536 satır ve 32 bit önekle sınırlıdır. Ve genel çözme girişi TPLJBIG2Decoder.LoadFromByteArray, sayfa ilişkilendirmesi sıfır olanı aramak yerine akış sırasındaki ilk sayfa bitmap'ini, yani karşılaşılan ilk sayfa bilgisi segmentini getPageAsJBIG2Bitmap(0) üzerinden döndürür; gömülü PDF akışları tek sayfalarını rutin olarak 1 diye numaralar ve ilişkilendirmeyle 0. sayfayı istemek hiçbir şey bulmaz. Hata metni, hatanın segment numarasını, tipini ve bayt ofsetini taşıyan iç çözücü tanılaması TPLJBIG2Decoder.LastError içine düşer ve kütüphane düzeyindeki TPDFlib.LastErrorCode ile aynı şey değildir. Bunların hiçbiri kodlama tarafına dokunmaz; o taraf JBIG2 kodlayıcı arka uçları ve nasıl bağlandıkları notlarında ele alınır. Okuma yolu, başkasının kodlayıcısının üretmeye karar verdiği her şeyi kabul etmek zorundadır ve kurallarını görüntü yığınının geri kalanıyla, yerleşik TIFF çözücü ile BigTIFF ve döşemeli düzen retleri dahil, paylaşır: adıyla reddet, bildirilen bir sınırın ötesinden asla bayt ödünç alma ve ara değerin taşması geçerli bir yanıt sanılmasın diye aritmetiği yeterince geniş tut. Delphi ya da C++Builder için yerel bir JBIG2 okuma yolu değerlendiriyorsanız, çözücü ve görüntü işlemenin geri kalanı PDF Library for Delphi sayfasında belgelenmiştir