Teknik Makale

PDF Renderer Hiçbir Şey Çizmiyor: Dört Sessiz Delphi Hatası

Hiçbir şey çizmeyen bir PDF renderer'ının genellikle çizim kodunda hiç hata yoktur. Delphi ve C++Builder için HotPDF Component'te, her günlük satırı temiz kalırken sayfaların boş görüntülenmesine dört ayrı kusur neden oldu: başında eğik çizgi taşıyan isim operandları, ters çevrilmiş bir cm birleştirmesi ve sıfır okuyan bir token indeksi. Hiçbiri exception fırlatmadı. Hiçbiri log yazmadı. İçerik akışı doğru tokenize edildi, operatör dispatcher her operatörü tanıdı, görüntü XObject'i geçerli bir bitmap'e çözüldü ve sonra sayfa boş çıktı. Bu kombinasyon — her aşamada başarı bildiren ve görünür hiçbir şey üretmeyen bir pipeline — sessizce başarısız olan bir arama ya da indeksin imzasıdır. Bu, böyle bir hata ailesinin ve bunun 38 sürüm boyunca hayatta kalmasına izin veren test disiplininin bir post-mortem'idir

Bir PDF renderer neden hiçbir şey çizmez?

Çünkü bir PDF renderer'da başarısız bir kaynak araması, boş bir sayfadan ayırt edilemez. İçerik akışı isim operandları ve kaynak sözlüğü anahtarları iki farklı string uzayıdır ve HotPDF, normalizasyon yapmadan bunları birbiriyle karşılaştırıyordu. Tokenizer, /Im0'ı okur ve eğik çizgiyi korur, çünkü token budur; yüklenen /Resources /XObject sözlüğü ise anahtarı Im0 olarak saklar, çünkü ayrıştırıcı sözlük anahtarlarını oluştururken sınırlayıcıyı kaldırır. Bir operand ismine karşı yapılan her FindValue bu yüzden -1 döndürüyordu. Etki alanı görüntülerden çok daha genişti. ISO 32000-1 §8.9 Do'yu, §8.4 gs'i ve /ExtGState aramasını, §8.6 cs ve CS'i, §8.7.4.3 ise sh'yi kapsar. Beş operatörün hepsi kaynak alt sözlüklerini ham operandla anahtarlıyordu, dolayısıyla hepsi kaçırıyordu. İsimlendirilmiş renk uzayları DeviceGray'e düşüyordu, bu da 1 scn'yi beyaz sayfa üzerinde beyaz mürekkebe çeviriyordu. Görüntü XObject'leri hiç boyanmıyordu — bitmap görüntü yolu pratikte yayına girdiği günden beri hiç çalışmamıştı. Düzeltme, operand-anahtarlı her aramada uygulanan birim seviyeli bir yardımcı fonksiyondur; kuralın tekrar kaymasını önlemenin tek yolu budur

// Page content stream, the ordinary image-placement idiom:
//   q
//   /GS0 gs
//   200 0 0 120 60 400 cm
//   /Im0 Do
//   Q
// The operand token is '/Im0'. The resource dictionary key is 'Im0'.

function HPDFStripNameSlash(const N: AnsiString): AnsiString;
begin
  Result := N;
  if (Result <> '') and (Result[1] = '/') then
    Delete(Result, 1, 1);
end;

// Every resource lookup keyed by an operand name goes through the helper.
Name := HPDFStripNameSlash(Name);
XObjIdx := FPageResources.FindValue('XObject');
if XObjIdx < 0 then
  Exit;
// The /XObject sub-dictionary may itself be an indirect reference.
XObjDict := FAccess.ResolveDictionary(FAccess.Context,
  FPageResources.GetIndexedItem(XObjIdx));
if XObjDict = nil then
  Exit;

İkinci, ilgili bir kaçırma bir katman altında yatıyordu. Renderer yalnızca akışlar ve sözlükler için tipli çözücülere sahipti, bu yüzden en üst seviye bir dizi nesnesini işaret eden dolaylı bir referans — diğer ucunda [/Separation ...] bulunan yaygın /CS0 5 0 R — her ikisi üzerinden de nil'e çözülüyor ve çözülmemiş bağlantıya düşüyordu. Genel bir nesne çözücü eklemek, isimlendirilmiş renk uzaylarını ve fonksiyon dizilerini tek hamlede düzeltti. Gölgelendirme sözlükleri kuruyorsanız, aynı çözümleme disiplini eksenel ve dairesel gölgelendirme yoluna da uygulanır; burada /Function girdisi çok sık dolaylıdır

cm operatörü ve ters yazılmış bir birleştirme

İkinci kusur, görüntüleri sayfanın kabaca yüz bin piksel dışına yerleştiriyordu, ki bu da onları hiç çizmemekle tamamen aynı görünür. ISO 32000-1 §8.3.4, PDF dönüşümlerini satır vektörleriyle tanımlar ve cm operatörü, operand matrisi M'yi geçerli dönüşüm matrisine M × CTM olarak birleştirir — önce M etkili olur, sonra mevcut CTM. HotPDF, matrisleri önce B'yi uygulayan HPDFMatMul(A, B) aracılığıyla birleştirir. Doğru çağrı bu yüzden eski CTM'yi A olarak geçirmelidir. Yayınlanan kod ise operand matrisini A olarak geçiriyor, CTM × M üretiyordu

Ters çevrilmiş sıra, tek bir cm için zararsızdır ve standart iki adımlı deyim için felakettir. Bir görüntüyü 1 0 0 1 x y cm ile ardından w 0 0 h 0 0 cm ile yerleştirin; doğru basamak, birim kareyi (w, h) ile ölçekler ve sonra (x, y) ile öteler. Ters çevrilmiş basamak altında ise öteleme önce girer ve ölçek onu çarpar, böylece nominal olarak (60, 400)'de olup 200'e 120'ye ölçeklenen bir görüntü (12000, 48000)'e yerleşir. Blit'in tepesindeki kırpma testi bunu reddeder, blit atlanır ve hiçbir yerde bir sorun bildirilmez

// HPDFMatMul(A, B) applies B first, then A.
// ISO 32000-1 cm semantics: new CTM = M x CTM, so M must be B.

// Wrong, and shipped for 38 versions:
GS.CTM := HPDFMatMul(HPDFMatFromOps(NumAt(6), NumAt(5), NumAt(4),
                                    NumAt(3), NumAt(2), NumAt(1)), GS.CTM);

// Correct:
GS.CTM := HPDFMatMul(GS.CTM, HPDFMatFromOps(NumAt(6), NumAt(5), NumAt(4),
                                            NumAt(3), NumAt(2), NumAt(1)));

Bunu öğretici kılan şey, aynı kaynak dosyanın zaten doğru sırayı içermesiydi. Bir Form XObject'in /Matrix girdisi aynı ters çevrilmiş birleşime sahipti, ama Type 3 glif yolu ile gömülü glif taslak yolu baştan itibaren doğru yapıyordu, çünkü glif yerleştirme, tersine çevrildiğinde görünür biçimde başlangıç noktasına çöker ve birileri daha önce bunu düzeltmek zorunda kalmıştı. Bir birimde otuz iki sürüm boyunca iki kural bir arada var oldu, her biri kendi fonksiyonunda doğruydu ve hiçbir gözden geçiren fark etmedi çünkü ne çağrı noktası tek başına yanlış görünüyordu

Bir token indeksi bir eksik olduğunda ne olur?

Sözdizimsel olarak işlenen ama anlamsal olarak ölü on iki operatör elde edersiniz. Renderer'daki operand erişimcisi, Tokens[OpIndex - Back]'i okuyan NumAt(Back)'tir ve OpIndex, operatör token'ının kendi indeksidir. Tek operandlı bir operatör bu yüzden sayısını back 1'de bulur. On iki tanesi NumAt(0) olarak yazılmıştı; bu, operatör token'ını okur, ctOperandNumber tür kontrolünde başarısız olur ve sıfır varsayılanını döndürür. Liste, ISO 32000-1 §9.3'ün metin durumu operatörlerinden Tc, Tw, Tz, TL, Ts ve Tr ile §8.4.3'ün grafik durumu operatörlerinden w, J, j, M, ri ve i'dir. Karakter ve kelime aralığı no-op oldu, yatay ölçekleme hiç uygulanmadı, satır aralığı sıfırda kaldı bu yüzden T* hiçbir zaman satır ilerletmedi, metin yükselmesi hiçbir şey yapmadı, render modu her zaman dolgu oldu ve her belgedeki her vuruş, beyan edilen çizgi genişliğinden bağımsız olarak 1 piksellik ince bir çizgi olarak çıktı. m, rg ve Tm gibi çok operandlı operatörler NumAt(1..6) kullanıyordu ve hepsi doğruydu, bu yüzden fonksiyonu tarayan bir gözden geçiren, içine gömülü on iki yanlış girdiyle makul indeks aritmetiğinden oluşan bir duvar gördü

function NumAt(Back: Integer): Double;
begin
  Result := 0;
  if (OpIndex - Back >= 0)
    and (Tokens[OpIndex - Back].Kind = ctOperandNumber) then
    Result := Tokens[OpIndex - Back].NumValue;
end;

// OpIndex addresses the operator token, so a lone operand sits at back 1.
else if Op = 'Tc' then GS.Text.CharSpace := NumAt(1)   // previously NumAt(0)
else if Op = 'TL' then GS.Text.Leading   := NumAt(1)   // previously NumAt(0)
else if Op = 'Tr' then GS.Text.RenderMode := Round(NumAt(1))
else if Op = 'w'  then GS.LineWidth      := NumAt(1)   // previously NumAt(0)

Test paketi neden 38 sürüm boyunca yeşil kaldı?

Çünkü assertion'lar, render edilmiş bir sayfayı kısmen render edilmiş bir sayfadan ayırt edecek kadar güçlü değildi. Rendering smoke testleri, çıktı bitmap'inin tamamen siyah olmadığı, sayfanın boş olmadığı ya da görüntü digest'inin sıfır olmadığı gibi şeyler iddia ediyordu. Metin render edildiğinde ve görüntüler edilmediğinde bunların her biri geçerlidir. Metin sorunsuz çizildi, dolayısıyla frame buffer hiçbir zaman tekdüze olmadı, digest hiçbir zaman sıfır olmadı ve tüm görüntü pipeline'ı pratikte ölü kod olmasına rağmen paket başarı bildirdi. Zayıf assertion'lar grafikler için tam da bu yüzden baştan çıkarıcıdır. Kimse anti-aliasing kenarının bir piksel kayması durumunda kırılan bir test istemez, bu yüzden doğal geri çekilme, hiçbir makul değişikliğin ihlal edemeyeceği bir şeyi iddia etmektir — ve bu geri çekilme sizi, hiçbir makul olmayan değişikliğin de ihlal edemeyeceği yüklemlere götürür. Bir ayrım renk-uzayı testi, çıktının siyahtan ayırt edilebilir olduğunu iddia ediyordu; beyaz üzerine gri bunu geçti, beyaz üzerine beyaz da geçti. Test, doğru rengin boyanıp boyanmadığını ölçmüyordu. Tuvalde herhangi bir şeyin olup olmadığını ölçüyordu

Gerçekten başarısız olan bir rendering assertion'ı nasıl yazılır?

Beklenen renkteki pikselleri, beklenen miktarda sayın ve konum ile boyutun sayımdan çıkmasına izin verin. Yerine geçen disiplin, elle oluşturulmuş minimal bir PDF, dosya başına bir görsel gerçek ve belirli bir RGB üçlüsüne tolerans dahilinde kaç pikselin düştüğüne dair bir assertion'dır. Bilinen bir ofsete yerleştirilmiş 200'e 120'lik saf kırmızı bir görüntü, kabaca 24000 kırmızı piksel üretmelidir. Kaynak araması kaçırırsa sayım 0'dır. cm basamağı ters çevrilmişse sayım 0'dır. Görüntü yanlış renk uzayında render edilirse sayım 0'dır. Tek bir sayı üçünü de yakalar ve tolerans bandı, insanların ilk başta tam karşılaştırmadan çekinmesine neden olan anti-aliasing gürültüsünü emer

function CountPixelsNear(Bmp: TBitmap; R, G, B, Tol: Integer): Integer;
var
  X, Y: Integer;
  C: TColor;
begin
  Result := 0;
  for Y := 0 to Bmp.Height - 1 do
    for X := 0 to Bmp.Width - 1 do
    begin
      C := Bmp.Canvas.Pixels[X, Y];
      if (Abs(GetRValue(C) - R) <= Tol)
        and (Abs(GetGValue(C) - G) <= Tol)
        and (Abs(GetBValue(C) - B) <= Tol) then
        Inc(Result);
    end;
end;

// A 200x120 red image placed at 60,400 must paint about 24000 red pixels.
Check(CountPixelsNear(Bmp, 255, 0, 0, 12) > 20000,
  'image XObject was never drawn');

Dört smoke testi bu şekilde yeniden yazıldı — bir Type 4 tint dönüşümü, bir görüntü Do yerleştirmesi, isteğe bağlı içerik görünürlük durumu ve bir Tr vuruş modu — ve bunlar birlikte tüm aileyi ortaya çıkardı. Asıl ders budur ve bu kod tabanının çok ötesine genellenir: bir rendering pipeline'ında assertion, rengi adlandırmak zorundadır. Daha yumuşak olan her şey, renderer'ın çalıştığını kontrol eder, çizdiğini değil. Kendi sayfa-bitmap harness'inizi kuruyorsanız, sayfa rasterleştirme anlatımı, ilk regresyonunuza bir piksel-sayım yardımcısı eklemek için doğal bir yerdir

Dürüst sınırlar

İki sınırın açıkça belirtilmesi gerekir. 4'ten 7'ye kadar olan metin kırpma render modları, temel dolgu ya da vuruş modu olarak çizilir, çünkü renderer glif taslaklarından biriken kırpma yollarını modellemez; metin şeklinde kırpmaya dayanan dokümanlar, altındaki kırpılmış çizim yerine metni render eder. Ve burada anlatılan piksel-sayım disiplini bir smoke-test tekniğidir, bir uygunluk paketi değildir — çıktının bir referans rasterleştiriciyle eşleştiğini kanıtlamaktan çok daha düşük bir çıtayı temsil eden, belirli bir görsel gerçeğin frame buffer'a ulaştığını kanıtlar. Bununla birlikte, bu dört hatanın üç yıllık sürüm boyunca geçemediği çıta tam olarak budur

Burada tartışılan renderer, Delphi ve C++Builder için standart HotPDF Component'in bir parçası olarak sunulur; ürün sayfası, bitmap önbelleği ve arka plan ön getirme giriş noktaları dahil tam sayfa render API referansını içerir