Teknik Makale

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

/F1 1 Tf, sonra 12 0 0 12 72 700 Tm, sonra (Hello world) Tj gösteren bir içerik akışı son derece yasaldır. El yapımı bir renderer, on bir karakterin hepsini kabaca beş buçuk pointin içine çizer — 66 puntoluk bir kelimenin olması gereken yerde sol kenar boşluğunda koyu bir leke bırakır. O lekenin ardındaki aritmetik tek satırlıktır. ISO 32000-1 §9.4.4 her glyph ilerlemesini metin uzayında tanımlar; yer değiştirme sayfaya varmadan metin matrisinden geçmek zorundadır ve q ... Q içinde kurulan bir kırpma, çizgi genişliği kadarıyla grafik durumuna aittir

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

Çünkü eski ilerleme kodu, metin uzayı mesafesini Tm'nin ö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 1'e 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

Kısayol uzun süre hayatta kalır; çünkü insanların test ettiği üreticilerin çoğu /F1 12 Tf ve ölçeklenmemiş bir Tm yazar ve o durumda metin uzayı ile kullanıcı uzayı gerçekten aynı boyuttadır. Naif sürümün tamamı aşağıda; onu kıran içerik akışı üstteki yorum olarak yazılmıştır:

// Boyutu Tf yerine Tm'de kodlayan bir üreticiden içerik akışı:
//   BT
//   /F1 1 Tf
//   12 0 0 12 72 700 Tm
//   [(Hel) 30 (lo) -250 (world)] TJ
//   ET

// Naif ilerleme: kullanıcı uzayıymış gibi Tm.e'ye eklenen bir metin uzayı mesafesi
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

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

Tm.e satırı gürültülü hatadır; ötekiler daha sessizdir ve farklı dosyalarda ortaya çıkar. Kelime aralığı Tw çoktan ölçeklenmemiş metin uzayı birimlerindedir; onu FontSize / 1000 ile çarpmak, Tf 12 altındaki bir 3 Tw'yi 0.036 birime çevirir. Hizalanmış bir paragraf kelime arası boşluğunun neredeyse tamamını yitirir. Yatay ölçek Th, Tc ile Tw'ye ve TJ kerning sayısına da uygulanmak zorundadır. Code = 32 sınaması ayrıca iki baytlık bir kodun içindeki değeri 32 olan her baytta da ateşlenir ve CJK metnine rastgele boşluklar saçar. Metin durumu hataları bu tür nadiren gürültüyle başarısız olur: tek bir glyph çizilmeden Tc'yi, Tw'yi ve Tz'yi sıfırlayabilen operand indeksi ve kaynak adı tuzakları gibi, bunlar da bir üreticinin dosyasında inandırıcı sayfalar, öbürünün dosyasında bozuk sayfalar üretir

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 hesaplar ve onu metin matrisine bir öteleme matrisi olarak uygular; ölçekleme, rotasyon ve eğme Tm'den gelir, ilerlemenin kendisinden asla. Yatay yazım için tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th'tır; w0 binde birlik em cinsinden glyph genişliğidir, Tj TJ ayarıdır ve Th, Tz'nin 100'e bölümüdür. Yeni matris [1 0 0 1 tx 0] × Tm'dir. Açık yazıldığında o çarpım, e'ye a·tx'i ve f'ye b·tx'i ekler; metin uzayındaki bir ötelemenin, a 1 ve b 0 değilken e'ye sade bir ekleme olamayacağı buradan bellidir. §9.3.3 uyarınca Tw yalnızca tek baytlık 32 karakter koduna uygulanır

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

HotPDF Delphi Component o formülü tek bir yerde tutar. HPDFContentStream birimi, glyph başına ilerleme için HPDFTextGlyphDisplacement'i, bir TJ sayısı için HPDFTextAdjustmentDisplacement'i açığa eder; sayfa renderer'ı da metin ayıklama yorumlayıcısı da ikisini çağırır, böylece doğru render edilen bir sayfa gördüğünüzle örtüşen glyph konumları da bildirir. Kütüphanenin ayrıştırıcısı üzerine kendi içerik akışı tüketicinizi yazarsanız aynı iki rutini çağırıp matris adımını kendiniz sağlayabilirsiniz:

uses
  HPDFContentStream;  // HPDFTextGlyphDisplacement, HPDFTextAdjustmentDisplacement

type
  // Kendi metin matrisi kaydınız; HotPDF türü değil
  TTextMatrix = record
    a, b, c, d, e, f: Double;
  end;

// Kendi yardımcınız: [1 0 0 1 Tx Ty] x Tm, a, b, c ve d üzerinden ilerler
procedure TranslateInTextSpace(var M: TTextMatrix; Tx, Ty: Double);
begin
  M.e := M.e + M.a * Tx + M.c * Ty;
  M.f := M.f + M.b * Tx + M.d * Ty;
end;

// W0, 1/1000 em cinsinden glyph genişliğidir; Tz, Tz operandıdır (100 = ölçek yok)
procedure AdvanceAfterGlyph(var Tm: TTextMatrix; W0: Double; Code: Cardinal;
  TwoByteCodes: Boolean; Tfs, Tc, Tw, Tz: Double);
var
  Tx, Ty: Double;
begin
  HPDFTextGlyphDisplacement(W0 * Tfs / 1000, False,
    (Code = 32) and not TwoByteCodes,  // Tw yalnızca tek baytlık boşlukta
    Tc, Tw, Tz, Tx, Ty);
  TranslateInTextSpace(Tm, Tx, Ty);
  // Tf 1, Tm 12 0 0 12 72 700, W0 = 500: Tx = 0.5 ve Tm.e 6 büyür
end;

procedure ApplyTJNumber(var Tm: TTextMatrix; Adjustment, Tfs, Tz: Double);
var
  Tx, Ty: Double;
begin
  HPDFTextAdjustmentDisplacement(Adjustment, Tfs, Tz, False, Tx, Ty);
  TranslateInTextSpace(Tm, Tx, Ty);  // Th kerning'e de uygulanır
end;

Renderer'ın içinde aynı matris adımı, metin konumunu oynatan her operatörü sürer. Td, TD, T*, ' ve ", satır matrisini katsayıları üzerinden öteler; 12× metin matrisi altında 0 -1.2 Td, taban çizgisini 1.2 değil 14.4 point aşağı taşır. Hiç boyanmayan glyphler de özdeş yolu izler. Render modu 3 — OCR metin katmanlarının kullandığı görünmez mod — ve gizli optional content içindeki metin, boyanan metinle aynı biçimde Tm'yi ilerletir. Onlara aritmetiğin özel bir kopyasını vermek klasik hatadır; o zaman aynı BT bloğunda görünmez bir koşudan sonra gelen görünür metin hafifçe farklı bir konumdan başlar

Glyphin kendisini yerleştirmek

Glyph yerleşimi aynı mantığı izler. Gömülü kontur mevcut olmadığında renderer GDI TextOutW'ye düşer; tüm glyph matrisini CTM × Tm × rise × em ölçeğinden, Th dâhil, kurar ve matrisi bir SaveDC / RestoreDC çifti içinde GM_ADVANCED modunda SetWorldTransform ile geçirir. GDI fontu sabit 1000 birim yükseklikte oluşturulur ve boyutlandırmayı dünya dönüşümü üstlenir; döndürülmüş ve eğik metin, dönüştürülmüş bir başlangıç noktasında dik çizilmek yerine yönelimini korur. Konturlu render modları, 1000 birimlik tasarım ölçeğine göre ölçülen bir kalem genişliği neredeyse sıfıra büzüleceği için boyamadan önce dünya dönüşümünü sıfırlar. Dikey yazım bilinçli tek asimetridir: WMode 1 bir font, y ekseninde dikey metriği kadar aşağı ilerler; Tc ile Tw o eksen boyunca uygulanır, Th hiç 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. Bu açıkça görünür. GDI destekli bir renderer'da gözden kaçması kolaydır; çünkü CTM, renkler, çizgi parametreleri ve metin durumu renderer'ın kendi durum yığınında yaşarken kırpma, aygıt bağlamında — o yığının dışında durur. Sayısal durumun bir kopyası kırpma dışında her şeyi geri yükler. q ... Q bloğu içindeki bir W n sonra sayfadaki sonraki her işlemi kırpıp durur ve belirti, ona neden olan operatörden çok uzakta eksik içerik gibi görünür

Form XObject'leri aynı başarısızlığa ikinci bir yol açar. §8.10 bir forma içeriği çevresinde örtük bir kaydet-geri yükle verir ve onu /BBox'ına kırpar; gerçek dünya form içeriği, spesifikasyon çiftleşmelerini istese de, bazen kendi q ve Q'sunu dengesiz bırakır. HotPDF bir formu çalıştırmadan önce kırpmağı kaydeder ve aygıt bağlamını saklar; form bitince formun açık bıraktığı kırpma çerçevelerini atar ve aygıt bağlamını geri yükler, böylece her kaydedilmiş bölge tam olarak bir kez serbest kalır. Çağıranın durumunu patlatacak form içindeki başıboş bir Q, uygulanmak yerine reddedilir. RenderFallbackPolicy rfpReport iken o red tanılara düşer; özensiz bir üreticiyi render sorunundan ayırmayı kolaylaştıran da budur:

// Tanılar: THPDFRenderFallbackDiagnostics (HPDFPageRenderer içinde bildirilir)
Pdf.RenderFallbackPolicy := rfpReport;
Bmp := Pdf.RenderLoadedPageToBitmap(PageIndex, 150);
try
  Diags := Pdf.GetLastRenderFallbackDiagnostics;
  for J := 0 to High(Diags) do
    if (Diags[J].Kind = rfkInvalidObject) and (Diags[J].OperatorName = 'Q') then
      Log.Add(Format('page %d: unbalanced Q refused - %s',
        [PageIndex + 1, string(Diags[J].Reason)]));
finally
  Bmp.Free;  // Free nil üzerinde güvenlidir
end;

THPDFSavedClipState ile tembel clip yakalama

Düz düzeltme, GDI kırpma bölgesini her q'da okumak ve her Q'da geri koymaktır. Doğrudur. Ama tam olarak q'yu en çok kullanan sayfalarda pahalıya gelir; çünkü üreticiler her küçük çizimi salt başlangıç noktasını taşımak için q 1 0 0 1 x y cm ... Q'ya sarar. Her o çift bir GDI bölgesi oluşturur, kırpmağı içine kopyalar ve yine siler. Birkaç sayfa paralel render edildiğinde iş parçacıkları rasterleştirmek yerine GDI bölge nesneleri için yarışır ve paralel render hattının hızlanması buna göre küçülü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

HotPDF pahalı kısmı erteler. Bir q, yığın derinliğini ve ait olduğu yüzeyi kaydeder; frame dizisi 16'dan ikilerle büyüyerek sayfa boyunca yeniden kullanılır, böylece sayısal dönüşüm koşusu hiç GDI nesnesi ayırmaz. Kırpma, o frame'deki bir şey onu değiştirmek üzereyken ilk kez yakalanır: bekleyen bir W ya da W* ile yol boyama ya da n, bir desen doldurması, metin render modları 4-7'nin bıraktığı kırpma ya da bir forma giriş. Q'da yakalayan frame bölgesini geri koyar ve serbest bırakır; kırpmağı hiç değiştirmemiş frame yalnızca sayısal durumu patlatır. Sıradan sayfa böylece hiç bölge ayırmaz, kırpma yapan sayfa gerçek kırpma değişikliği başına tam bir bölge öder. Bir PDF yorumlayıcısı için kendi GDI aygıtınızı kurarsanız aynı biçim birkaç düzine satıra sığar:

// Kendi GDI destekli aygıtınız için bir taslak; bunların hiçbiri HotPDF API'si değildir
// uses bölümünde Windows, SysUtils ve Math gerekir
type
  TClipFrame = record
    Region: HRGN;
    Captured: Boolean;
  end;

procedure TMyDevice.PushState;            // q: hiç GDI çağrısı yok
begin
  if FCount = Length(FFrames) then
    SetLength(FFrames, Max(16, Length(FFrames) * 2));
  FFrames[FCount].Region := 0;
  FFrames[FCount].Captured := False;
  Inc(FCount);
end;

procedure TMyDevice.BeforeClipChange;     // W n, W f, metin kırpması, form BBox'tan önce çağır
var
  R: HRGN;
  Res: Integer;
begin
  if (FCount = 0) or FFrames[FCount - 1].Captured then Exit;
  R := CreateRectRgn(0, 0, 0, 0);
  if R = 0 then RaiseLastOSError;
  Res := GetClipRgn(FDC, R);              // 1 kopyalandı, 0 kırpma yok, -1 hata
  if Res <= 0 then
  begin
    DeleteObject(R);
    R := 0;
    if Res < 0 then RaiseLastOSError;
  end;
  FFrames[FCount - 1].Region := R;
  FFrames[FCount - 1].Captured := True;
end;

procedure TMyDevice.PopState;             // Q
begin
  if FCount = 0 then Exit;                // dengesiz Q: yok sayın
  Dec(FCount);
  if FFrames[FCount].Captured then
    SelectClipRgn(FDC, FFrames[FCount].Region);  // Region 0 kırpmağı kaldırır
  if FFrames[FCount].Region <> 0 then
    DeleteObject(FFrames[FCount].Region);
end;

O taslakta iki ayrıntı ağırın çoğunu taşır. GetClipRgn'in 0 döndürmesi hata değildir: aygıt bağlamının o anda kırpması yoktu demektir. Q'da SelectClipRgn'e sıfır bölge geçmek, eşleşen q'da var olmamış bir kırpmağı kaldıran şeyin ta kendisidir. 0'u başarısızlık saymak iç kırpmağı yerinde bırakır. Öbür ayrıntı Captured bayrağıdır; aynı frame'deki ikinci kırpma değişikliğinin kayıtlı bölgeyi çoktan daralmış biriyle ezmesini önler

Kaydedilen kırpma ile ilerleme kuralı nerede yetersiz kalır

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 frame'in aygıt bağlamını kaydetmesinin ve DevPopState'in DC değiştiğinde geri yüklemeyi atlamasının — örneğin bir transparency group kendi katman bitmapine render olurken — sebebi budur

Metin tarafında ilerleme kuralı her glyphi verilen genişlikten yerleştirir. Basit bir font /Widths dizisini atlıyorsa ve gömülü font programı mevcut değilse ilerleme, genişlik yedeği kadar iyidir ancak; §9.4.4 o genişliğin nereden geleceği hakkında bir şey söylemez. Bunun regresyon testleri, basit üretilmiş belgelerin neredeyse hiç içermediği dosyalar üzerinde yaşar: Tf 1 ile ölçeklenmiş bir Tm taşıyan bir dosya, hizalanmış metinde sıfırdan farklı Tz ve Tw taşıyan bir dosya ve q ... Q içinde bir W n kırpmasıyla ardından kırpılan alanın dışında çizilen içerik taşıyan bir dosya. Üçüncüsünün doğru renderı, dışarıdaki içeriği tam gösterir

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