Teknik Makale

PDF Grafikleri Nasıl Çalışır: İçerik Akışları ve Operatörler

Bir PDF sayfası pikselleri saklamaz ve SVG'nin yaptığı gibi bir şekil nesneleri ağacını da saklamaz. O bir program saklar. Sayfadaki her satır, eğri, dolgu ve yerleştirilmiş görüntü, çalışan bir grafik durumuna (graphics state) karşı yukarıdan aşağıya doğru bir içerik akışındaki bir dizi operatörün yürütülmesinin sonucudur. Bu tek gerçeği anladığınızda, formatın davranışının çoğu şaşırtıcı olmaktan çıkar: yol oluşturulduktan sonra bir dolgunun neden ayrı bir boyama operatörüne ihtiyaç duyduğu, onları parantez içine almadığınız (bracket) sürece renklerin ve çizgi genişliklerinin neden bir şekilden diğerine sızdığı, aynı çizim kodunun tek bir koordinat dönüşümünden sonra neden tamamen farklı yerlere inebileceği. Bu, ISO 32000'de tanımlandığı şekliyle bu yürütme modelinin bir turudur: bir içerik akışını açtığınızda karşılaştığınız operatörler ve sayfada neyin görüneceğine karar veren kurallar

İçerik akışı postfix bayt kodudur

Bir içerik akışı, işlenenlerin (operands) ve ardından operatörlerin geldiği düz bir bayt dizisidir. Önce işlenenler gelir, onları tüketen operatör en son gelir; bu, bir işlev (function) çağrısının tersidir ve bir yığın makinesiyle (stack machine) aynıdır: sayıları itin (push), ardından fiili verin. İç içe geçme, ifade sözdizimi, değişken yoktur. Bir üçgen taslağı bunun beş satırıdır:

100 100 m    % moveto: (100, 100) konumunda yeni bir alt yol başlat
200 200 l    % lineto: add a segment to (200, 200)
300 100 l    % lineto: add a segment to (300, 100)
h            % closepath: başlangıca geri bağlan
S            % stroke: yolun dış hattını boya

Operatörler bilerek kısa ve özdür. Gerçek bir sayfa bunlardan binlercesidir ve genellikle FlateDecode ile sıkıştırılır. Bu kompaktlığın bedeli, akışın sorgulayabileceğiniz hiçbir yapı taşımamasıdır: bir görüntüleyici "bu sayfadaki başlık nerede" diye soramaz, sadece programı çalıştırabilir ve hangi mürekkebin nereye indiğini görebilir. Rastgele PDF'lerden metin çıkarmanın zor olmasının temel nedeni budur

Orijin sol alttadır ve Y yukarı doğru büyür

Herhangi bir koordinatın anlamlı olması için (0, 0) noktasının nerede olduğunu bilmeniz gerekir. PDF, orijini sayfanın sol alt köşesine koyar; X sağa doğru artar ve Y yukarı doğru artar; inç başına 72 nokta olacak şekilde noktalarla (points) ölçülür (ISO 32000-2 §8.3.2). Bir US Letter sayfasında üst kenar y = 0'da değil y = 792'de oturur. Orijinin sol üstte olduğu ve Y'nin aşağı doğru büyüdüğü ekran grafiklerinden gelen herkes bunu ilk denemede ters anlar ve ilk çizgiyi sayfanın altından dışarı çizer. Birim (unit) ayrıca ortamdan da bağımsızdır: sayfa ister bir telefon ekranında ister bir film kaydedicide (imagesetter) oluşturulsun, 72 birim bir inçtir

PDF koordinat sisteminin PDF şeması: başlangıç sol-altta ve Y yukarı doğru büyür; özdeş koordinatları sayfanın altına yakın çizen sol-üst ekran kuralıyla karşılaştırılır
PDF, Y yukarı artacak biçimde sol alttan ölçer; ekran alışkanlıklarının ilk çiziminizi sayfanın dibine park ettirmesinin sebebi budur

Çoğu sayfa çizim kütüphanesi bu kuralı doğrudan devralır. Örneğin HotPDF'te, TextOut ve yol çağrılarının hepsi sol alttan nokta (point) cinsinden ölçüm yapar, bu nedenle sayfa yüksekliğine yakın bir değer içeriği en üste koyar:

// HotPDF, Delphi: y alt kenardan yukarı doğru, puan cinsinden ölçülür
Pdf.CurrentPage.SetLineWidth(2.0);
Pdf.CurrentPage.MoveTo(100, 700);   // sayfanın üstüne yakın
Pdf.CurrentPage.LineTo(300, 700);
Pdf.CurrentPage.Stroke;             // moveto/lineto/stroke operatörlerini üretir

Bu çağrı dizisi tam olarak yukarıdaki m, l ve S operatörlerine derlenir (compile). Kütüphane, içerik akışı için bir daktilocudur, daha fazlası değil ve ne yaydığını (emit) bilmek, bir şekil beklemediğiniz bir yere indiğinde çıktı hakkında mantık yürütmenizi sağlayan şeydir

Yolu oluşturun, ardından boyayın

PDF, yol (path) oluşturmayı yol boyamadan (painting) ayırır ve bu ayrım bir ukalalık değildir. İlk olarak hiçbir görünür şey eklemeyen oluşturma operatörleriyle bir şekil tanımlarsınız, ardından birikmiş yolla ne yapacağına karar veren tek bir boyama operatörü verirsiniz. Aynı üçgen, yalnızca sonlandırdığınız fiile bağlı olarak bir anahat (outline), düz bir dolgu veya her ikisi de olabilir

Oluşturma operatörleri azdır. m bir noktada yeni bir alt yol (subpath) başlatır. l düz bir bölüm ekler. c altı işlenenden, iki kontrol noktasından ve bir uç noktadan oluşan bir kübik Bezier eğrisi ekler. re bir x, y, genişlik, yükseklik dörtlüsünden tam bir dikdörtgen ekleyen bir kısayoldur. h geçerli alt yolu başlangıcına geri kapatır. Hiçbiri sayfaya mürekkep sürmez; sadece geometriyi biriktirirler

200 250 m                    % alt yolu başlat
300 350 400 450 500 250 c    % kübik Bezier: iki kontrol noktası, ardından bitiş noktası
150 200 re                   % kendi alt yolu olarak eklenen 150 x 200 dikdörtgen
h                            % close

Orijinal örnek, eğri (curve) operatörünün artık kullanılmayan y varyantını (variant) kullanıyordu; üç açık noktaya sahip olan c pratikte göreceğiniz ve ulaşmanız gereken biçimdir. Yol var olduğunda, bir boyama operatörü onu bitirir. Kelime dağarcığı küçüktür ve ezberlemeye değerdir, çünkü her sayfadaki her şekil bunlardan biriyle biter:

PDF yol modelinin PDF şeması: inşa operatörleri m, l, c, re ve h görünmez geometri biriktirir ve kapatma operatörlerinden biri olan S, f, f*, B veya n; kontur, dolgu, dolgu artı kontur veya kırpma kararını verir
Yol operatörleri yalnızca geometri toplar ve yolu kapatan tek fiil, bir kontur mu, bir dolgu mu, ikisi mi yoksa bir kırpma mı alacağınızı belirler
  • S mevcut çizgi genişliğini ve çizgi rengini kullanarak yolun anahattını çizer (stroke)
  • f mevcut dolgu (fill) rengini ve sıfır olmayan (nonzero) sarma kuralını kullanarak içini doldurur
  • f* kendi kendine kesişen (self-intersecting) şekiller ve delikli şekiller için önemli olan çift-tek (even-odd) kuralını kullanarak doldurur
  • B tek bir işlemde doldurur ve ardından çizer; b önce yolu kapatır
  • n hiçbir şey boyamaz, bir yolun görünür bir iz bırakmadan nasıl bir kırpma (clip) bölgesi haline geldiği budur

Sarma kuralı insanların yanıldığı kısımdır. Sıfır olmayan (f, B) test noktasından bir ışının işaretli kesişimlerini sayar ve sayımın sıfır olmadığı her yeri doldurur, bu nedenle bir delik yalnızca alt yolu dıştakine zıt yönde sarılırsa boş kalır. Çift-tek (f*, B*) yönden bağımsız olarak her kesişimde geçiş yapar (toggle). Eğer bir "çörek" (donut) şekli katı (solid) çıkarsa, iç daire dış daire ile aynı şekilde sarılmıştır ve siz onu ya tersine çevirirsiniz ya da çift-tek'e geçersiniz

Renk bir parametre değil, bir moddur

İçerik akışında renk yapışkandır (sticky). Bir renk ayarlarsınız ve siz başka bir renk ayarlayana veya daha önceki bir durumu (state) geri yükleyene kadar ayarlanmış olarak kalır, bu yüzden parantez içine alınmamış bir renk değişikliği kendisinden sonra çizilen her şeyi sessizce renklendirir. PDF ayrıca dolgu rengini (lowercase) ve çizgi rengini (uppercase) dolgu için küçük harfli operatörler ve çizgi için büyük harfli operatörlerle iki bağımsız ayar olarak tutar. Cihaz renk uzaylarının (device color spaces) her birinin kendi kısaltması vardır:

0.5 g                % DeviceGray fill, mid gray (0 = black, 1 = white)
0.2 0.6 0.8 rg       % DeviceRGB fill
0.8 0.2 0.1 RG       % DeviceRGB stroke (uppercase = stroke)
0.2 0.8 0.0 0.1 k    % DeviceCMYK fill

DeviceRGB ekran çıktısına uyar, DeviceCMYK baskı üretiminin beklediği şeydir ve DeviceGray monokrom içerik için en küçük seçimdir. Cihaz uzayları kullanışlıdır ancak kalibre edilmemiştir: aynı RGB üçlüsü iki monitörde farklı şekilde oluşturulabilir, ICC tabanlı renk uzaylarının ve PDF/A çıktı amaçlarının (output intents) çözmek için var olduğu sorun budur. Renk açısından kritik işler için cs ve CS ile kalibre edilmiş bir uzay seçer ve bileşenleri sc ve scn ile ayarlarsınız, ancak sıradan belgeler için cihaz kısaltmaları yükü taşır. Bir kütüphane bunları türlendirilmiş çağrılarla (typed calls) sarar. Örneğin HotPDF, tek bir TColor alır ve eşleşen operatörleri yayar:

Pdf.CurrentPage.SetRGBFillColor(clRed);
Pdf.CurrentPage.Rectangle(100, 100, 200, 150);  // x, y, width, height
Pdf.CurrentPage.Fill;

Pdf.CurrentPage.SetRGBFillColor(RGB(0, 255, 0));
Pdf.CurrentPage.Circle(150, 400, 50);           // x, y, radius
Pdf.CurrentPage.Fill;

Grafik durumu ve q/Q yığını

Yolun (path) kendisi olmayan her şey grafik durumunda yaşar: mevcut dönüşüm matrisi (transformation matrix), dolgu ve çizgi renkleri, çizgi genişliği, kesikli çizgi deseni (dash pattern), kırpma (clip) bölgesi, alfa (alpha). Durum küreseldir ve değiştirilebilirdir, bu nedenle yerel bir değişiklik yapmanın tek güvenli yolu her şeyi kaydetmek, değiştirmek, çizmek ve geri almaktır. q ve Q'nun yaptığı şey budur. q mevcut durumun bir kopyasını bir yığına (stack) iter; Q eşleşen q'dan bu yana yapılan her değişikliği atarak onu çeker (pop)

q                    % tüm grafik durumunu kaydet
2 0 0 2 100 100 cm   % concatenate a transform: scale 2x, translate to (100,100)
0.8 g                % bu blokla sınırlı gri dolgu
% ... draw scaled, gray content ...
Q                    % restore: dönüşüm ve renk geri alınır

Dengesiz q ve Q, elle oluşturulmuş veya dikilmiş (stitched) bir içerik akışının yanlış gitmesinin yaygın bir yoludur. Eşleşen bir Q'su olmayan başıboş bir q, sayfa bittiğinde yığını derin bırakır; fazladan bir Q onu eksik akıtır (underflow). Her iki durumda da bir görüntüleyici eski bir kırpmayı veya dönüşümü yürürlükte tutabilir ve içerik kaybolur veya yanlış yere iner. Grafikler, yolun açıklayamayacağı bir nedenden dolayı ortadan kaybolduğunda, önce durum yığınını denetleyin

CTM her koordinatı dönüştürür

Mevcut dönüşüm matrisi (current transformation matrix - CTM), operatörlerinizdeki sayılar ile gerçek sayfa arasında yer alır. Herhangi bir şey çizilmeden önce her koordinat CTM ile çarpılır, bu nedenle matrisi değiştirmek, tek bir yol koordinatına dokunmadan sonraki tüm çizimlerin nerede ve nasıl görüneceğini değiştirir. cm operatörü, afin (affine) matrisine [a b c d e f] eşlenen altı işleneni alarak mevcut matrisin üzerine yeni bir matris bitiştirir (concatenate):

1 0 0 1 100 50 cm        % (100, 50) kadar taşı: ofseti e ve f taşır
2 0 0 1.5 0 0 cm         % x'i 2, y'yi 1.5 ile ölçekle: a ve d ölçek katsayılarıdır
0.707 0.707 -0.707 0.707 0 0 cm   % rotate 45 degrees (cos/sin in a, b, c, d)

İki şey insanları yanıltır. Birincisi, cm değiştirmek yerine birleştirir, bu nedenle dönüşümler birikir ve sıra önemlidir: önce ölçekleyip (scale) sonra ötelemek (translate), önce öteleyip sonra ölçeklemekle aynı şey değildir. İkincisi, döndürme (rotation) ve ölçekleme şeklinizin merkezi etrafında değil, mevcut orijin etrafında döner (pivot), bu nedenle bir şeyi yerinde döndürmek için onu orijine ötelersiniz, döndürürsünüz, ardından hepsi q/Q içine sarılmış olarak geri ötelersiniz. Aynı matris görüntüleri yerleştiren şeydir, görülmeye değer son parçadır

Görüntüler ve yeniden kullanılabilir içerik XObject'lerdir

Raster görüntüler içerik akışında satır içi (inline) olarak yaşamazlar. Genişlik, yükseklik, bit derinliği, renk uzayı ve sıkıştırma filtresini açıklayan kendi sözlüklerine sahip dış nesneler (external objects) olan görüntü XObject'leri olarak depolanırlar ve içerik akışı yalnızca bunlara başvurur. JPEG destekli bir fotoğraf kendini şu şekilde bildirir:

/Photo <<
  /Type /XObject
  /Subtype /Image
  /Width 640
  /Height 480
  /BitsPerComponent 8
  /ColorSpace /DeviceRGB
  /Filter /DCTDecode        % görüntü verisi bir JPEG akışıdır
>>

Bir görüntü XObject'i birim kareye (unit square) çizilir: kullanıcı uzayında her zaman (0, 0)'dan (1, 1)'e kadar olan bölgeyi kaplar. Ona bir konum veya boyut aktarmazsınız. Bunun yerine, birim karenin istediğiniz dikdörtgene eşlenmesi için CTM'yi ayarlarsınız, ardından onu Do ile çağırırsınız (invoke). Bu nedenle, bir görüntüyü yerleştirmek her zaman bir dönüşüm (transform) ve ardından bir çağrıdır ve ölçeğin (scale) bir sonraki işleme sızmaması (bleed) için bir kaydet/geri yükle (save/restore) işlemine sarılır:

PDF görsel XObject yerleşiminin PDF şeması: sözlük pikselleri içerik akışının dışında tanımlar, Do birim kareye boyar ve cm, q ile Q içine sarılı şekilde onu hedef dikdörtgene eşler
Akış hiçbir piksel saklamaz — bir XObject'i birim kareye çağırır ve cm o kareyi hedef dikdörtgene eşler
q
640 0 0 480 50 300 cm    % birim kareyi (50, 300) konumundaki 640x480 kutuya eşle
/Photo Do                % görüntü XObject'ini boya
Q

Aynı Do mekanizması, sınırlayıcı bir kutuya (bounding box) sahip kendi içerik akışları olarak, yeniden kullanılabilir bir grafik yığınını, bir logoyu veya tekrarlanan bir damgayı tutan form XObject'lerini yönlendirir. Onu bir kez tanımlayın, farklı bir CTM ile birçok kez çağırın ve baytlar dosyada yalnızca bir kez görünür. Çoğu kütüphane bunu tek bir yerleştirme çağrısının arkasına gizler: HotPDF bir bit eşlemi (bitmap) AddImage ile kaydeder ve onu matrisi elle (by hand) oluşturmanızı istemek yerine açık bir x, y, genişlik ve yükseklik alarak ShowImage ile yerleştirir:

var
  Bmp: TBitmap;
  ImgIndex: Integer;
begin
  Bmp := TBitmap.Create;
  try
    Bmp.LoadFromFile('logo.bmp');
    ImgIndex := Pdf.AddImage(Bmp, icFlate);
    // x, y (bottom-left), width, height, rotation angle
    Pdf.CurrentPage.ShowImage(ImgIndex, 50, 300, 200, 150, 0);
  finally
    Bmp.Free;
  end;
end;

Bu tek satırın altında, kütüphane görüntü XObject sözlüğünü yazar, CTM'yi birim kareyi boyutlandıracak ve konumlandıracak şekilde ayarlar ve Do yayar. Alttaki model bilinmeye değer olanıdır, çünkü her garip sonucu açıklar: gerilmiş (stretched) bir görüntü, eşleşmeyen ölçek faktörlerine sahip bir CTM'dir, kırk sayfada aynı olan bir logo, kırk kez çağrılan tek bir form XObject'idir ve baş aşağı (upside down) oluşturulan (render) bir görüntü, bozuk bir dosya değil, matriste bir işaret dönüşüdür (sign flip)

Bunun varacağı yer

Şeklini bir kez gördüğünüzde grafik modeli küçüktür. Bir içerik akışı, değiştirilebilir bir duruma (mutable state) karşı çalışan postfix bayt kodudur; koordinatlar sol alttan başlar ve CTM'den geçer; yollar sessizce oluşturulur ve kasıtlı bir operatörle boyanır; renk ve çizgi ayarları siz onları q/Q ile parantez içine alana kadar (bracket) devam eder; görüntüler ve yeniden kullanılabilir grafikler bir birim karenin dönüştürülmesiyle (transforming) yerleştirilen XObject'lerdir. Kafa karıştırıcı hemen hemen her oluşturma (rendering) sonucu bu beş kuraldan birine indirgenir. Bu grafik operatörlerinin daha büyük nesne modeli, onlara işaret eden sayfa sözlükleri ve çapraz başvuru tablosu (cross-reference table) içinde nasıl oturduğunu görmek istiyorsanız, PDF dosya yapısına teknik genel bakış bu katmanı kapsar ve sıfırdan basit bir PDF oluşturma baytları baştan sona dolaşır. Metin çizimi, kendi operatör ailesinde yaşar ve PDF metin ve yazı tipi işleme (handling) hakkındaki tamamlayıcı makalede ele alınan kendi tuzaklarına (pitfalls) sahiptir

Burada gösterilen MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage ve ShowImage gibi Delphi çizim çağrıları, bu içerik akışı (content-stream) operatörlerini sizin için yayan (emit) Delphi ve C++Builder için HotPDF Bileşeninin (Component) bir parçasıdır