Teknik Makale

Delphi renderer'da PDF metin ilerlemesi ve q/Q clip restore

HotPDF Delphi Component sayfa rendererı artık metni, her glyph yer değiştirmesini ISO 32000-1 §9.4.4'ün tanımladığı gibi metin uzayında — tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th — hesaplayarak ilerletir ve sonra metin matrisini doğrusal kısmı üzerinden HPDFTranslateTextMatrix ile taşır. Kırpma her q frameinde kaydedilir ve Qda geri yüklenir ama bir GDI bölgesi yalnızca o frame kırpmağı gerçekten değiştirdiğinde yakalanır. İki düzeltme de HotPDF 2.754.0da çıktı ve ikisi de kelimeleri çökmüş ya da clip bölgeleri Qlarını sızarak render edilen gerçek dünya sayfalarından geldi. İlk hata, bir üretici font boyutunu matrise yazana kadar doğru görünen aritmetiktir. İkincisi, neredeyse paralel render hızlanmamıza mal olan bir doğruluk düzeltmesidir; hızı geri kazanma yöntemimiz, GDI tabanlı herhangi bir PDF aygıtı yazıyorsanız bilinmeye değer

Bir PDF Tf 1 kullandığında metin neden topaklanır?

Çünkü eski ilerleme kodu, metin uzayı mesafesini Tmnin öteleme bileşenine doğrudan ekliyordu; sanki metin uzayı ile kullanıcı uzayı her zaman aynı ölçekteymiş gibi. Gerçek dünyadaki pek çok üretici font boyutunu Tf ile 1e set eder ve gerçek boyutu metin matrisinde taşır. /F1 1 Tf ile 12 0 0 12 72 700 Tm birlikte, 500 birim genişlikte bir glyph metin uzayında 0.5 ilerler; Tm onu ölçeklediğinde sayfada bu 6 puntodur. Eski renderer Tm.e := Tm.e + Adv çalıştırıyor ve kalemi 0.5 punto taşıyordu. Her glyph bir öncekinin bir karakter on ikisi kadar gerisine düşüyordu; gövde metni sol kenar boşluğunda koyu bir leke olarak render ediliyordu, oysa aynı dosya diğer her görüntüleyicide kusursuz görünüyordu

HotPDF rendererında metnin Tf 1 altında neden çöktüğü: 12 0 0 12 72 700 Tm ile 500 birimlik bir glyph 0.5 metin uzayı birimi ilerlemeli; Tm bunu 6 puntoya ölçekler, eski kod ise 0.5i doğrudan Tm.e'ye ekleyip gövde metnini glyph başına bir karakter on ikisi leke olarak render etti
Font boyutunu metin matrisine kodlayan üreticiler her glyphi bir öncekinin bir karakter on ikisi kadar gerisine düşürüyordu; kütüphanenin kendi çıktısında görünmez bir kusurdu
// Boyutu Tf yerine Tmde kodlayan bir üreticiden içerik akışı:
//   BT
//   /F1 1 Tf
//   12 0 0 12 72 700 Tm
//   [(Hel) 30 (lo) -250 (world)] TJ
//   ET

// Eski ilerleme (sadeleştirilmiş): mesafe, sanki kullanıcı uzayıymış gibi Tm.e'ye ekleniyor
Adv := W * FontSize / 1000;                  // 500 birimlik glyph için 0.5
if (HorizScale <> 0) and (HorizScale <> 100) then
  Adv := Adv * HorizScale / 100;             // Th yalnızca genişlikte
Adv := Adv + CharSpace;                      // Tc, Th ile ölçeklenmiyor
if Code = 32 then
  Adv := Adv + WordSpace * FontSize / 1000;  // Tw yanlışlıkla Tfs ile ölçekleniyor
Tm.e := Tm.e + Adv;                          // Tm.a, Tm.b, Tm.c, Tm.d'yi yok sayar

// Eski TJ ayarı: Th yok ve yine yalnızca Tm.e
Tm.e := Tm.e - NumValue * FontSize / 1000;

Tm.e kısayolu o bloktaki tek kusur değildi. Kelime aralığı Tw, ölçeklenmemiş metin uzayı birimlerinde ifade edilir; eski kod onu FontSize / 1000 ile çarpıyordu, dolayısıyla Tf 12 altında hizalanmış bir satır kelime arası boşluğunun neredeyse tamamını kaybediyordu. Yatay ölçek Th glyph genişliğine uygulanıyordu ama Tc ya da Twye değil ve TJ kerning ayarı onu tümüyle atlıyordu. Render modu 3 görünmez metni — OCR metin katmanlarının kullandığı tür — ilerleyen boyamayan yol ve gizli optional content içindeki metin aynı aritmetiğin özel bir kopyasını taşıyordu; görünmez bir koşudan sonra çizilen her şey yanlış konumdan başlıyordu. Rendererda metin durumu hataları nadiren gürültüyle başarısız olur: bir zamanlar Tc, Tw ve Tzyi tek bir hatasız sıfırlayan operand indeksi ve kaynak adı hataları gibi, bunlar kütüphanenin kendi çıktısında inandırıcı sayfalar üretti ve yalnızca başka üreticilerin dosyalarında bozuldu

ISO 32000-1 §9.4.4 glyph ilerlemesini nasıl tanımlar?

ISO 32000-1 §9.4.4 ilerlemeyi tümüyle metin uzayında tanımlar ve onu metin matrisine bir öteleme matrisi olarak uygular; cevap, önce txi hesaplamak ve ölçeklemeyi, rotasyonu ve eğmeyi Tmye yaptırmaktır. Yatay yazım için tx, ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th değerine eşittir; w0, binde birlik em cinsinden glyph genişliğidir, Tj TJ ayarıdır ve Th, Tznin 100e bölümüdür. Yeni Tm, [1 0 0 1 tx 0] × Tmdir; HotPDF'de bu, HPDFTranslateTextMatrix yardımcısıdır: e ile fye doğrudan yazmak yerine X ile Yyi matris katsayıları a, b, c ve d üzerinden ekler. §9.3.3 uyarınca Tw yalnızca tek baytlık 32 karakter koduna uygulanır; çok baytlı CID kodları yatay yolda kelime aralığını asla almaz. Aynı yardımcı artık Tdyi, TDyi, T*yi, ' ile " operatörlerini, TJ ayarlarını ve gizli metin yolunu sürer; kuralın sahibi tek bir fonksiyon demektir

HotPDF rendererında ISO 32000-1 9.4.4 glyph ilerlemesi: tx, w0, Tj, Tfs, Tc, Tw ve Thden metin uzayında hesaplanır, sonra HPDFTranslateTextMatrix üzerinden uygulanır; yer değiştirme a, b, c ve d matris katsayılarından geçer ve Td, TD, TJ ile gizli metin yolu tek kuralı paylaşır
İlerlemeyi doğrudan Tm.eye eklemek yalnızca metin uzayı kullanıcı uzayına eşitken işe yarar; onu matris katsayılarından geçirmek ölçeklenmiş, döndürülmüş ve eğik metni doğru tutar
procedure HPDFTranslateTextMatrix(var Matrix: THPDFAffineMatrix; X, Y: Double);
begin
  Matrix.e := Matrix.e + Matrix.a * X + Matrix.c * Y;
  Matrix.f := Matrix.f + Matrix.b * X + Matrix.d * Y;
end;

// Yatay glyph ilerlemesi, ISO 32000-1 9.4.4
W   := HPDFFontCharWidth(F, Code);
Adv := W * State.Text.FontSize / 1000 + State.Text.CharSpace;
if (Code = 32) and not F.CID2Byte then
  Adv := Adv + State.Text.WordSpace;         // Tw metin uzayında, ölçeklenmemiş
Adv := Adv * State.Text.HorizScale / 100;    // Th tüm toplama uygulanır
HPDFTranslateTextMatrix(Tm, Adv, 0);

// TJ sayı elemanı: aynı uzay, aynı Th
Adjustment := -Items[I].NumValue * State.Text.FontSize / 1000;
HPDFTranslateTextMatrix(State.Text.Tm, Adjustment * State.Text.HorizScale / 100, 0);

Glyph yerleşimi aynı mantığı izlemek zorundaydı. Gömülü kontur mevcut olmadığında renderer GDI TextOutWye düşer; artık tüm glyph matrisini CTM × Tm × rise × em ölçeğinden, Th dahil, kuruyor ve bir SaveDC / RestoreDC çifti içinde GM_ADVANCED modunda SetWorldTransform ile kuruyor. GDI fontu sabit 1000 birim yükseklikte oluşturulur ve boyutlandırmayı dönüşüm yapar; döndürülmüş ve eğik metin, dönüştürülmüş bir başlangıç noktasında dik çizilmek yerine yönelimini korur. Dikey yazım modu bilinçli tek asimetridir: WMode 1 bir font y ekseninde dikey metriği kadar aşağı ilerler ve yatay ölçek o eksene uygulanmaz

q/Q bir PDF grafik durumunda gerçekte neyi kaydeder?

ISO 32000-1 §8.4.2, geçerli kırpma yolunu grafik durumunun parçası sayar; Q, kırpmağı eşleşen q anındaki hâliyle geri yüklemek zorundadır, yalnızca sayısal parametreleri değil. HotPDF zaten CTM, renkler, çizgi parametreleri ve metin durumu taşıyan bir grafik durum yığını tutuyordu ama GDI kırpmağı aygıt bağlamında, o yığının dışında tutar. Sayısal durumun bir kopyası bu yüzden kırpma hariç her şeyi geri yüklüyordu ve q ... Q bloğu içinde W n ile kurulmuş bir kırpma, sayfadaki sonraki her işlemi kırpıp duruyordu. Form XObjectleri aynı başarısızlığa ikinci bir yol ekledi; §8.10 bir forma içeriği çevresinde örtük bir kaydet-geri yükle verir ve gerçek dünya form içeriği bazen kendi q operatörlerini dengesiz bırakır, spesifikasyon onların çiftleşmesini istese de. Renderer artık bir formu çalıştırmadan önce CaptureClipBeforeChange ile SaveDC çağırır, form bitince giriş derinliğinden daha derin kaydedilmiş bölgeleri atar ve RestoreDC çağırır; böylece her kaydedilmiş HRGNnin tam olarak tek bir serbest bırakma yolu olur

THPDFSavedClipState ile tembel clip yakalama

Gönderilen düzeltme, q başına bir THPDFSavedClipState kaydı tutar ama pahalı kısmı frame kırpmağı ilk değiştirene kadar erteler. Kayıt, bölge tutamacını, ait olduğu yığın derinliğini, alındığı aygıt bağlamını ve bir Captured bayrağını tutar. DevPushState yalnızca derinlik ile DCyi doldurur ve frame dizisini 16dan başlayarak ikilerle büyütür; q 1 0 0 1 x y cm ... Q ile dolu bir içerik akışı hiçbir GDI nesnesi ayırmaz. Kırpmağı değiştirmek üzere olan operatörler — bekleyen bir W ya da W* ile yol boyama, n operatörü, desen doldurmaları ve form girişi — önce CaptureClipBeforeChange çağırır

HotPDF rendererında tembel GDI clip yakalama: DevPushState q başına yalnızca derinlik ve DC kaydeder, CaptureClipBeforeChange bölgeyi W, n ya da form girişi kırpmağı değiştirmeden hemen önce okur, DevPopState Qda geri yükleyip siler ve her qda yakalayan istekli sürüm paralel verimi yaklaşık 1,15 kat tek iş parçacıklıya düşürdü
Her qda bir GDI bölgesi oluşturmak render iş parçacıklarını aç bıraktı; yakalama artık yalnızca bir operatör kırpmağı değiştirmek üzereyken olur ve 1,5 kat hızlanma kapısı yeniden geçilir
procedure THPDFPageRenderer.CaptureClipBeforeChange;
var
  Index, ClipResult: Integer;
  Region: HRGN;
begin
  ClearSavedClipRegions(FGSStack.Count);
  Index := FSavedClipCount - 1;
  if (Index < 0) or FSavedClips[Index].Captured or
     (FSavedClips[Index].StackDepth <> FGSStack.Count) or
     (FSavedClips[Index].DC <> FDC) then Exit;   // zaten kaydedilmiş ya da bizim değil
  Region := CreateRectRgn(0, 0, 0, 0);
  if Region = 0 then RaiseLastOSError;
  ClipResult := GetClipRgn(FDC, Region);         // 0, hiç kırpma yok demektir
  if ClipResult <= 0 then
  begin
    DeleteObject(Region);
    Region := 0;
    if ClipResult < 0 then RaiseLastOSError;
  end;
  FSavedClips[Index].Region := Region;
  FSavedClips[Index].Captured := True;
end;

procedure THPDFPageRenderer.DevPopState;
var
  Index: Integer;
begin
  ClearSavedClipRegions(FGSStack.Count);
  Index := FSavedClipCount - 1;
  if (Index >= 0) and (FSavedClips[Index].StackDepth = FGSStack.Count) then
  begin
    if FSavedClips[Index].Captured and (FSavedClips[Index].DC = FDC) then
      SelectClipRgn(FDC, FSavedClips[Index].Region);  // Bölge 0 kırpmağı kaldırır
    if FSavedClips[Index].Region <> 0 then
      DeleteObject(FSavedClips[Index].Region);
    Dec(FSavedClipCount);
  end;
  FGSStack.Pop;
end;

İstekli sürümün ölçülen maliyeti, bu tasarımın var olma sebebidir. İlk doğru uygulama her qda bir GDI bölgesi oluşturuyor ve okuyordu; çoğunlukla sayısal dönüşümlerden kurulu sayfalarda renderer iş parçacıkları, rasterleştirmek yerine GDI bölge nesneleri için yarışarak vakit geçiriyordu. Paralel render hattı beklenen kazancından yaklaşık 1,13 ile 1,20 kat tek iş parçacıklı verime düştü ve benchmark takımındaki 1,5 kat hızlanma kapısını geçemedi. Tembel yakalama ve yeniden kullanılan frame kapasitesiyle aynı benchmark, özgün 1,5 kat kapısını yeniden geçiyor. Küçük TrueType glyph antialiasing aynı sürümde geldi ve bariz şüpheliydi; ama gerileme bölge ayırmaya kadar izlendi — en yeni özelliği suçlamadan önce ölçmek için iyi bir hatırlatma

Bu yaklaşımın sınırları nerede?

Kaydedilen kırpma, aygıt piksellerinde bir GDI bölgesidir; render edilen bitmap için kesindir ve başka her hedef için anlamsızdır. Her framein aygıt bağlamını kaydetmesinin ve DC değiştiğinde DevPopStatein geri yüklemeyi atlamasının — örneğin bir transparency group kendi katman bitmapine render olurken — sebebi budur. GetClipRgnin sıfır döndürmesi kırpma yok demek olan meşru bir sonuçtur ve onu SelectClipRgn(FDC, 0) ile geri yüklemek, eşleşen qda var olmamış bir kırpmağı doğru biçimde kaldıran şeydir. Metin tarafında düzeltme her glyphin nereye gittiğini doğruyor ama genişlikleri icat etmez: bir font /Widths dizisini atlıyorsa ve gömülü program mevcut değilse, ilerleme hâlâ yalnızca genişlik yedeği kadar iyidir. Bu alanı regresyon testine sokarken Tf 1 ile ölçeklenmiş bir Tm taşıyan en az bir fikstür, sıfır olmayan Tz ile Tw taşıyan bir tane ve q ... Q içinde bir kırpma ile ardından dışındaki içerik taşıyan bir tane tutun; çünkü bunların hiçbiri kütüphanenin kendisinin ürettiği belgelerde görünmez

Rendererı uygulama kodundan sürüyorsanız bir PDF sayfasını bitmape render etmek yazısında anlatılan çağırma deseninde hiçbir şey değişmez; daha önce lekeleşmiş satırlar ya da kırpılmış içerik gösteren sayfalar 2.754.0 ve sonrasında düzgün render edilmeli. Component, desteklenen Delphi ve C++Builder sürümleri ve lisanslama ayrıntıları HotPDF Delphi PDF Component ürün sayfasındadır