Teknik Makale

PDFlibPas JBIG2 halftone gridleri: HSKIP ve negatif ofsetler

PDFlibPas, native JBIG2 halftone bölge decoder'ındaki iki bağımsız hatayı düzeltti: v3.539.37'de HSKIP skip maskesi, ITU-T T.88 §6.6.5.1'in tanımladığı gibi HSKIP[ng, mg] olarak indeksleniyor; v3.539.38'de ise negatif HGX ya da HGY ya da rotasyon yoluyla negatif koordinatlara ulaşan grid'ler gerçek bir floor kaydırmasıyla yerleştiriliyor. O sürümlerden önce etkilenen halftone bölgeleri hiçbir hata fırlatmadan bozuk ya da yerinden oynmuş çıkıyordu. İki hata da tesadüfen simetrik ya da negatif olmayan test verisinin arkasına saklanmıştı ve ikincisi, JBIG2'nin çok ötesini ısıran bir Delphi ve Free Pascal özelliğine dayanır: işaretli bir integer üzerinde shr mantıksal kaydırmadır, standardın varsaydığı aritmetik >> değil

Halftone bölgeleri en nadir JBIG2 bölge tipidir; dolayısıyla bir decoder, biri olarak kodlanmış benekli bir fotoğrafa rastlamadan önce binlerce taranmış belge işleyebilir. Rastladığında başarısızlık çirkindir: dosya ayrışır, segment uzunlukları toplanır, sayfa doğru boyuttadır ve bölge çöptür

Bir JBIG2 halftone bölgesi aslında neyi çözer?

Bir JBIG2 halftone bölgesi, bir pattern dictionary'den seçilen küçük bitmap'lerden oluşan bir grid'dir ve decoder'ın asıl işi her grid hücresi için bir indeks ile o hücrenin düştüğü pixel konumunu hesaplamaktır. Pattern dictionary HPW × HPH pixel boyutunda HNUMPATS desen tutar. Halftone bölge segmenti sonra HGW sütun × HGH satırlık bir grid'i ve aynı boyutta, Gray-kodlu bitplane'ler olarak kodlanmış gri tonlamalı bir image'i tanımlar. Her bitplane, en anlamlı plane önce olacak biçimde HGW × HGH bitmap üzerinde generic region prosedürüyle çözülür ve plane'ler birlikte her hücreye pattern indeksini verir

Hücre yerleşimi 8 bitlik kesirli sabit noktalı aritmetik kullanır. Grid kökeni HGX, HGY 32 bitlik bir değer çiftidir ve grid vektörü HRX, HRY komşu hücreler arasındaki adımı tanımlar; bu da döndürülmüş bir grid'e izin verir. Grid satırı mg ve grid sütunu ng için T.88 §6.6.5 pixel konumunu şöyle hesaplar:

  • x = (HGX + mg × HRY + ng × HRX) >> 8
  • y = (HGY + mg × HRX − ng × HRY) >> 8

Skip maskesi isteğe bağlı HENABLESKIP bayrağıyla girer. Bayrak setliyken §6.6.5.1, HGW × HGH boyutlu bir HSKIP bitmap'i kurar ve deseni bölgenin tamamen dışında duran her hücre için HSKIP[ng, mg]'i 1 set eder: x + HPW <= 0, x >= HBW, y + HPH <= 0 ya da y >= HBH. Gri tonlamalı bitplane'ler sonra o maskeyle, generic region skip bitmap'i olarak çözülür; böylece aritmetik decoder atlanan bir hücre için ne context okur ne günceller. Decoder ile encoder HSKIP'in her bitinde hemfikir olmalıdır, yoksa iki aritmetik coder uyumdan düşer

Yer değiştirmiş bir HSKIP maskesi neden yalnızca kare olmayan grid'leri bozdu?

Skip maskesi koordinatları yer değiştirmiş yazılıyordu ve yalnızca kare olmayan bir grid onu ortaya çıkardı; kare bir grid her değişmiş koordinatı maske içinde tutuyor. PDFlibPas bitmap'leri bir (column, row) pixel erişicisiyle saklar ve maskesi kuran kod (mg, ng) geçiyordu, önce satır. Gri tonlamalı bitplane decoder'ı maskeyi (ng, mg) olarak doğru okur. Pattern yerleştirme döngüsü ise onu kurucunun değişmiş sırasında geri okuyordu; yani ikisi hemfikirdi ve yerleştirme mantığının tek başına bir gözden geçirmesi onu geçerdi. Bir adlandırma tuzağı işi daha da kötüleştirdi: yerleştirme döngüsünde col adlı değişken grid satırlarını, Row ise grid sütunlarını geziniyor

v3.539.37'nin regresyon olgusu olarak kullandığı 16 × 8 bölge üzerindeki 4 × 4 desenli 5 × 3 grid'i alın. HRX = 1024 ile HRY = 0 iken grid sütunu 4 x = 16'ya, grid satırı 2 y = 8'e düşer; ikisi de bölge dışıdır. Doğru maske yedi hücre işaretler: 4. sütunun tamamı ve 2. satırın tamamı. Değişmiş yazımlar, yalnızca üç satır yüksekliğindeki bir maskede 3 ve 4 satır indekslerine pixel set etmeye çalışıyordu ve bitmap setter'ı o menzil dışı yazımları sessizce yok sayıyordu. Sağ kalan 2. sütun, 0-2. satırlardı. Decoder bu yüzden encoder'ın kodladığı iki hücreyi atlıyor, encoder'ın attığı altı hücreyi çözüyordu

5'e 3 grid için PDFlibPas JBIG2 halftone skip maskeleri: doğru HSKIP[ng, mg], 4. sütun ile 2. satırı atlanmış işaretlerken üç satırlı maskenin 3 ve 4. satırlarını hedefleyen değişmiş yazımlar sessizce düşüyordu ve yalnızca 2. sütun hayatta kalıyordu; aritmetik coder'lar uyumdan düşüyordu
Yalnızca kare olmayan bir grid değişmiş bir maskeyi ifşa eder ve doğan coder uyumsuzluğu hata fırlatmak yerine bölgeyi bozar

Aritmetik decoder bu olduğunda başarısız olmaz. Sonraki hücrelere ait bitlerden fazla pixel çözüyor, context'leri yanlış komşular okuyor ve ilk uyuşmazlıktan sonraki her pattern indeks gürültüdür; belirti birkaç yanlış yerleştirilmiş hücre değil bozuk bir bölgeydi, çünkü. Kare bir grid'de aynı hata çoğu zaman görünmezdir: hiçbir değişmiş koordinat maskeden çıkmaz ve bölge dışı hücreler köşegene göre simetrikken — örneğin sağ ve alt kenardan aynı sayıda hücre taşan bir grid — değişmiş maske birebir doğru maskenin kendisidir. HENABLESKIP ayrıca isteğe bağlıdır, gri tonlamalı image MMR-kodlu iken 0 olmalıdır ve encoder'lar tarafından nadiren set edilir; hatanın yüzeye çıkma yolu çok azdı. v3.539.37'den beri kurucu HSKIP[ng, mg] yazıyor ve yerleştirme döngüsü aynı sırayı okuyor

Negatif halftone grid ofsetleri neden üç katmanda başarısız olur?

Bölgesinin solundan ya da üstünden başlayan bir halftone grid'i PDFlibPas'ı üç ayrı yerde kırdı ve her hata bir sonrakini sakladı. T.88 bu geometriye bilerek izin verir. Ekranını bölgeye değil sayfaya hizalayan ya da döndürülmüş grid kullanan bir encoder, bölgenin kırptığı negatif hücre köşelerini doğal olarak üretir. v3.539.38 üç katmanı birlikte düzeltti; çünkü herhangi birini tek başına düzeltmek yalnızca belirtiyi değiştiriyordu

Katman 1: işaretsiz okunan işaretli alan

T.88 §7.4.5.1.2, HGX ile HGY'yi işaretli 32 bitlik değerler olarak tanımlar ama decoder onları işaretsiz alanlar için kullandığı aynı 32 bitlik yardımcıyla okuyordu ve o yardımcı her negatif sonucu 0'a sabitliyordu. HGX = -900'de başlaması gereken bir grid sessizce bölge kökenine çekiliyordu. v3.539.38 regresyon olgusunda bütün resim iki satır fazla aşağı çıkıyordu. Sabitleme, diğer iki hatanın neden bu kadar uzun yaşadığını da açıklar: köken negatif olmaya zorlanınca negatif bir koordinat yalnızca HRY > 0 ile döndürülmüş bir grid üzerinden görünebilirdi; orada y = HGY + mg × HRX − ng × HRY sonraki grid sütunlarında sıfırın altına düşer

Katman 2: shr, >> 8 değildir

T.88 >> 8 yazar ve eksi sonsuza yuvarlayan bir aritmetik kaydırma kasteder. Decoder bunu shr 8'e çevirdi. Delphi ve Free Pascal'da işaretli bir integer üzerinde shr mantıksal kaydırmadır: işaret biti sıfır olarak içeri kaydırılır. -512 tutan bir Integer için shr 8, -2 yerine 16777214 verir. y = -2'de çizilip alt yarısına kırpması gereken bir desen 16 milyon satır aşağı gönderiliyor ve bölge dışı diye atılıyordu. Hiçbir şey çökmedi; halftone'un üst satırı sade bir şekilde kayboldu

Katman 3: pixel yerine sabit noktalı karşılaştırma

Skip testi sabit noktalı değerleri karşılaştırıyordu, pixel konumlarını değil; kesir sıfırdan farklı olduğu anda ikisi denk değildir. Özgün kod mantıksal kaydırmayı, kaydırılmamış değer üzerinde xx + HPW × 256 <= 0 sınamakla aşıyordu — T.88 testinin sözde eşdeğeri. HGX = -900 ve 4 pixellik bir desenle bu -900 + 1024 = 124 verir; pozitiftir, dolayısıyla hücre atlanmıyor. Standart önce kaydırır: floor(-900 / 256) = -4 ve -4 + 4 = 0, x + HPW <= 0'ı karşılar; hücre tamamen dışarıdadır ve atlanmalıdır. Encoder atlıyor, decoder çözüyor ve gri tonlamalı image, değişmiş maske olgusundaki gibi tam olarak sürükleniyordu

Negatif HGX'teki bir grid için PDFlibPas JBIG2 halftone hataları: işaretsiz yardımcıyla okunan işaretli alan sıfıra sabitleniyordu, T.88 sağa kaydırması mantıksal shr'ye çevrilip bir deseni 16 milyon satır aşağı gönderiyordu ve sabit noktalı değerler üzerindeki skip testi, encoder'ın attığı bir hücreyi tutuyordu
Her hata bir sonrakini sakladı; v3.539.38'in üç katmanı birlikte, maske kurucusu ile yerleştirme döngüsünün kullandığı tek paylaşımlı HalftoneGridPixel yardımcısında düzeltmesi bu yüzden

v3.539.38'in regresyon olgusu, 12 × 10 bölge üzerinde HGX = -900, HGY = -512, HRX = 1024 konumlarında 4 × 4 desenli 4 × 3 bir grid kullanır. Grid sütunları x = -4, 0, 4 ve 8'e düşer; 0. sütun tamamen dışarıdadır ve HSKIP'e aittir. Grid satırları y = -2, 2 ve 6'ya düşer; 0. satır atılmak yerine alt iki pixel satırına kırpmalıdır. Katmanları tek tek düzeltmek yığını yeniden üretir:

Düzeltilen hatalarÇözülen bölge
Hiçbiri (v3.539.38 öncesi)Grid kökene çekildi, bütün resim iki satır fazla aşağıda
Yalnızca işaretli HGX / HGY okumasıİlk grid satırı eksik, gerisi skip testi sürüklenmesiyle bozuk
İşaretli okuma, floor kaydırması ve pixel uzayı skip testiT.88 §6.6.5'ten hesaplanan sayfayla ve iki bağımsız referans decoder'ıyla pixel pixel aynı

Düzeltme tek bir yardımcıdır: skip-maskesi kurucusu ile yerleştirme döngüsünün paylaştığı HalftoneGridPixel. Koordinatı Int64'te biriktirir; böylece büyük bir mg × HRX çarpımı taşamaz, 256'ya eksi sonsuza yuvarlayarak böler ve ±MaxInt div 2'ye sabitler; böylece bozuk bir grid sonraki bitmap aritmetiğini taşıramaz. Skip testi artık o pixel değerlerini HPW, HPH, HBW ve HBH ile karşılaştırır, tam olarak §6.6.5.1'in söylediği gibi

Delphi'de aritmetik sağa kaydırma nasıl yazılır?

Delphi'nin aritmetik kaydırma operatörü yoktur; dolayısıyla doğru bir işaretli sağa kaydırma floor bölmesi olarak yazılmalıdır ve sade div o bölme değildir. div sıfıra doğru keser. Negatif olmayan değerler için kesme ile floor hemfikirdir ve bölenin tam katı olan negatif değerler için de hemfikirdir; -512 div 256 = -2'nin hızlı bir testte iyi görünmesi bu yüzden. Başka her yerde anlaşamazlar: -900 div 256 -3'tür, floor ise -4'tür; -1 div 256 0'dır, floor ise -1'dir. Sıfırdan farklı kesirli bir JBIG2 koordinatı, div'in yanlış pixeli verdiği tam olarak şu olgudur

Delphi Win32 ve Win64 derleyicilerinde -512 tutan bir Integer değişkeni 8 sağa kaydırıldığında 16777214, -512 tutan bir Int64 ise 72057594037927934 verir. Free Pascal da shr'yi mantıksal kaydırma olarak tanımlar ve aritmetik sürüm için System unitinde SarLongint ile SarInt64'ü kargolar; ama o fonksiyonlar Delphi'de yoktur, dolayısıyla iki derleyici arasında paylaşılan kodun kendi yardımcısına ihtiyacı vardır:

// Floor division: A ve B'nin her işareti için eksi sonsuza yuvarlar.
// B 0 olmamalı; FloorDiv(Low(Integer), -1) tıpkı div gibi taşar
function FloorDiv(A, B: Integer): Integer;
begin
  Result := A div B;
  if (A mod B <> 0) and ((A < 0) <> (B < 0)) then
    Dec(Result);
end;

// Aritmetik sağa kaydırma (C ve T.88'in işaretli değerlerdeki ">>"i).
// Negatif Value için not Value = -Value - 1 negatif değildir; mantıksal
// shr orada güvenlidir ve dıştaki not sonucu geri eşler
function SarInt32(Value: Integer; Shift: Integer): Integer;  // Shift 0..31
begin
  if Value >= 0 then
    Result := Value shr Shift
  else
    Result := not ((not Value) shr Shift);
end;

function SarInt64(Value: Int64; Shift: Integer): Int64;      // Shift 0..63
begin
  if Value >= 0 then
    Result := Value shr Shift
  else
    Result := not ((not Value) shr Shift);
end;

not hilesi asla negatif bir sayıyı kaydırmaz; derleyicinin işaret bitine nasıl davrandığına bağlı değildir ve Low(Integer) dahil hiç taşmaz. İki yardımcı da birkaç milyon değer üzerinde bir Int64 floor referansıyla, 0'dan 31'e her kaydırma ile ve Low(Integer) ile High(Integer) kenarlarında Delphi Win32, Delphi Win64 ve Free Pascal x86_64 üzerinde eşleşti. Koordinatlara dokunan her unit testte tutmaya değer bir sağlamlık sınaması:

var
  V: Integer;
begin
  V := -900;
  Writeln(V shr 8);           // 16777212  mantıksal kaydırma, eski hata
  Writeln(V div 256);         // -3        sıfıra doğru kesme
  Writeln(FloorDiv(V, 256));  // -4        T.88'in >> 8 ile kastettiği
  Writeln(SarInt32(V, 8));    // -4
end;
-900 koordinatının 8 sağa kaydırılması için PDFlibPas sayı doğrusu: shr 16777212 verir, div -3'e keser; FloorDiv ile SarInt32 ise ikisi de ITU-T T.88'in kaydırma ile kastettiği floor değeri olan -4'e düşer. Bu, yalnızca sabit noktalı kesir sıfırdan farklıyken önem taşır
Kesme ile floor yalnızca tam katlarda hemfikirdir; -512 div 256 hızlı bir testi geçer ve -900 div 256 yanlış pixeli seçer

Math.Floor(V / 256) da -4 döndürür ama Double üzerinden aldığı dolan yolu, 253 üzerindeki Int64 değerlerinde hassasiyet kaybeder; integer geometri integerlarda kalmalıdır

Hangi PDFlibPas çağrıları halftone decoder'ını çalıştırır?

JBIG2 halftone decoder'ı, PDFlibPas bir sayfayı gömülü renderer ile render ettiğinde çalışır; çünkü render pixel ister. RenderPageToFile ile RenderPageToStream ikisi de sayfanın JBIG2Decode image stream'leri üzerinden ona ulaşır; dolayısıyla bir halftone sayfasını yeniden render etmek, v3.539.38'in çıktınızı değiştirdiğini doğrulamanın doğrudan yoludur. Aynı decoder diğer JBIG2 bölge tiplerini de işler — saf Pascal decoder'ında JBIG2 custom Huffman tabloları ile Delphi'de random-access JBIG2 dosyalarının çözümünde kapsanır — ve render edilen bitmap, PDF sayfalarını 1-bit monochrome'a render etme gibi dönüşümleri besler

uses
  SysUtils, PDFlibrary;

var
  Lib: TPDFlib;
  Page: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('scanned-halftone.pdf', '') <> 1 then
      raise Exception.CreateFmt('Load failed, error %d', [Lib.LastErrorCode]);
    for Page := 1 to Lib.PageCount do
      // Rendering her JBIG2 bölgesini çözer, halftone'lar dahil
      if Lib.RenderPageToFile(150, Page, PDF_RENDER_PNG,
        Format('page-%.3d.png', [Page])) <> 1 then
        Writeln('Page ', Page, ' was not rendered');
  finally
    Lib.Free;
  end;
end.

Image extraction normalde başka bir yol izler. GetPageImageList JBIG2 image'lerini native biçimde döndürür; SaveImageListItemDataToFile ya da GetImageListItemDataToString ise stream baytlarından kurulmuş bağımsız bir JBIG2 dosyası verir: dosya başlığı, JBIG2Globals verisi ve sayfa verisinin etrafında bir end-of-file segmenti. GetImageListItemIntProperty'nin 400 propertysi böyle bir girdi için 6 bildirir. O yolda hiçbir şey çözülmez; dolayısıyla render edilen sayfa gürültü gösterirken çıkarılan .jb2'nin başka bir görüntüleyicide doğru görünmesi bu iki halftone hatasının tipik işaretiydi:

var
  ListID, I: Integer;
begin
  Lib.SelectPage(1);
  ListID := Lib.GetPageImageList(0);
  if ListID = 0 then
    Exit;
  try
    for I := 1 to Lib.GetImageListCount(ListID) do
      if Lib.GetImageListItemIntProperty(ListID, I, 400) = 6 then  // standalone JBIG2
        Lib.SaveImageListItemDataToFile(ListID, I, 0,
          Format('page1-image%d.jb2', [I]));
  finally
    Lib.ReleaseImageList(ListID);
  end;
end;

Maskeler ya da renk dönüşümü render edilmiş bir yedek yol zorladığında girdi çözülmüş bir bitmap olarak döner ve halftone decoder'ı gerçekten çalışır. Image list'ler hakkında daha fazlası Delphi PDF text, image ve font extractiondadır

Hızlı başvuru: JBIG2 halftone grid kuralları

  • Skip maskesini HSKIP[ng, mg] olarak indeksleyin, önce grid sütunu, ve hücrelerin yerleştirildiği her yerde aynı sırayla geri okuyun (T.88 §6.6.5.1, PDFlibPas v3.539.37'de düzeltildi)
  • Her halftone ya da grid kodunu kare olmayan bir grid ile bölge dışı hücrelerin asimetrik bir kümesiyle test edin; kare bir grid, değişmiş bir indeksi bütünüyle saklayabilir
  • HGX ile HGY'yi işaretli 32 bitlik değerler olarak okuyun (T.88 §7.4.5.1.2), negatifleri sabitleyen işaretsiz bir yardımcı üzerinden asla
  • Standardın >> 8'ini 256'ya floor bölmesi olarak çevirin; ne shr 8 ne de div 256 olarak
  • Skip testini kaydırılmış pixel konumları üzerinde koşun; sabit noktalı biçim kesir sıfırdan farklı olduğunda farklıdır, HGX = -900'in 4 pixellik desenle gösterdiği gibi
  • Grid koordinatlarını Int64'te biriktirin ve bitmap koduna vermeden önce sabitleyin; bozuk bir grid taşıramasın
  • Belgeleriniz HENABLESKIP'li, negatif grid kökenli ya da döndürülmüş grid'li halftone bölgeleri içeriyorsa v3.539.38 ve sonrasına yükseltin

PDFlibPas, halftone skip maskelerini, negatif grid kökenlerini ve döndürülmüş grid'leri artık T.88'in belirttiği gibi işleyen native Pascal bir JBIG2 decoder'ıyla Delphi ve C++Builder'dan PDF belgelerini render eder, çıkarır ve düzenler. Özellikler, sürümler ve deneme indirmesi için PDFlibPas Delphi PDF library'ye bakın