Teknik Makale

PDF Nasıl Çalışır: Açılıştan Piksele Dosya Anatomisi

Aynı fatura PDF'ini Windows'ta da telefonda da açın; satır sonları, fontlar ve logo özdeş noktalara düşer. Bir PDF dosyası, numaralı nesneler kümesi artı bir bayt-ofset indeksidir ve PDFium Component'in içindeki PDFium motoru gibi bir okuyucu onu sondan açar: startxref, xref tablosu, katalog, sayfa ağacı, içerik akışları, sonra pikseller

Peki bir PDF dosyası nedir ve neden her yerde aynı görünür? Word ve RTF belgeleri metni ve stilleri saklar, yerleşimi onları açan makineye bırakır; eksik bir font ya da farklı bir yazıcı sürücüsü sayfaları yeniden akıtır. PDF bunun yerine bitmiş sonucu saklar. Her glyph ve satır, pointlerle ölçülen bir koordinat uzayında mutlak bir konuma sahiptir; font programları dosyanın içinde yol alır ve görseller kendi renk verisini ve sıkıştırma filtrelerini taşır. Görüntüleme zamanında hiçbir şey dizilmez; bu yüzden herhangi bir işletim sisteminde standarda uygun herhangi bir renderer aynı sayfayı boyar. Biçim, PostScript görüntüleme modeli üzerine kurulu olarak 1993'te Acrobat ile PDF 1.0 olarak başladı ve her revizyon aşağıda anlatılan makinelerin bir parçasını ekledi:

  • PDF 1.1 ile 1.4 şifrelemeyi, etkileşimli formları, mantıksal yapıyla dijital imzaları ve saydamlığı getirdi
  • PDF 1.5 object stream'leri ve çapraz başvuru akışlarını ekledi; PDF 1.6 AES şifrelemesini ekledi
  • PDF 1.7, ISO 32000-1:2008 oldu; PDF 2.0, 2020'de gözden geçirilen ISO 32000-2:2017 oldu
  • PDF/A (ISO 19005), PDF/X (ISO 15930) ve PDF/UA (ISO 14289) o tabanı arşivleme, baskı üretimi ve erişilebilirlik için kısıtlar

Bir okuyucu bir PDF dosyasını açtığında ne olur?

Bir PDF'i açmak, dosyanın son baytlarında başlayan ve bir bitmap ile biten bir arama zinciridir. O zincirdeki her halka neredeyse, ISO 32000-1 §7.3'ün temel nesne türlerinden biri olan bir sözlüktür ve sözlükler, 7 0 R (nesne 7, üretim 0) gibi dolaylı referanslarla birbirine işaret eder. Dosya bu yüzden bir sayfa dizisi değil, bir nesne grafiği gibi davranır. Başlık, gövde, çapraz başvuru tablosu ve trailer, PDF dosya yapısı genel bakışında düzenlenir ve satır satır açıklamalı minimal bir PDF, obj, endobj ve stream'in bir metin düzenleyicide neye benzediğini gösterir. Her okuyucunun izlediği sıra şudur

  1. Dosyanın sonuna gidin ve startxref anahtar kelimesinden sonra yazılı bayt ofsetini okuyun
  2. O ofsetteki çapraz başvuru verisini yükleyin; o, her nesne numarasını dosyadaki bir konuma eşler
  3. Trailer'ı okuyun ve /Root'u izleyerek belge kataloğuna gidin
  4. Katalogun /Pages girişini izleyerek sayfa ağacından inin ve istenen sayfa nesnesine ulaşın
  5. Sayfanın içerik akışını çözün ve operatörlerini sırayla yorumlayın
  6. O operatörlerin /Resources üzerinden adlandırdığı fontları, görselleri ve form XObject'leri çözün
  7. Yolları, glyph dış hatlarını ve görselleri cihaz çözünürlüğünde piksellere rasterleştirin

Birinci adım: startxref ve çapraz başvuru tablosu

Bir okuyucu bir PDF'i asla tepeden taramaz. İyi biçimli bir dosya, startxref anahtar kelimesi, 48211 gibi bir ondalık sayı ve %%EOF ile biter ve o sayı, çapraz başvuru verisinin bayt ofsetidir. xref için naif bir geriye doğru arama, startxrefin içinde durur; o yüzden tam anahtar kelimeyi arayın. Klasik tablo sabit genişlikli girişler kullanır (§7.5.4); bir okuyucu, öteki girişleri ayrıştırmadan herhangi bir nesne numarasının girişine atlayabilir. Her girişin bayt düzeni yapı genel bakışında kapsanır. Tabloyu izleyen trailer /Size, /Root ve isteğe bağlı /Encrypt, /ID ve /Prev'i adlandırır

PDF nasıl çalışır: bir PDF dosyasının başlık ve nesne gövdesinden çapraz başvuru tablosu boyunca trailer'a dikey anatomisi
Çapraz başvuru tablosu, nesne numaralarını bayt ofsetlerine çevirir; böylece bir okuyucu, kalanını taramadan doğrudan herhangi bir sayfaya konumlanır

Her giriş mutlak bir bayt konumudur; dolayısıyla baytları kaydıran her şey — satır sonlarını yeniden yazan bir düzenleyici gibi — indeksi yanlış yerlere işaret eden durumda bırakır. Onarım geçişi olan okuyucular, obj anahtar kelimelerini tarayarak indeksi yeniden kurar; olmayanlar dosyayı reddeder, hasarlı bir PDF'in bir görüntüleyicide açılıp öbüründe başarısız olmasının nedeni budur. startxref ofsetinde ne durduğu, hangi durumda olduğunuzu söyler: klasik bir tablo için xref anahtar kelimesi, PDF 1.5 çapraz başvuru akışı için bir N 0 obj başlığı ve bayat bir ofset için başka her şey. Aşağıdaki yardımcı sizin kendi kodunuzdur, düz RTL akışları üzerine kurulu

uses
  System.SysUtils, System.Classes;

// Kendi yardımcınız: Offset, startxref'ten sonra yazılan sayıdır.
// Okuyucunun o konumda bulacağını sınıflandırır
function DescribeXrefAt(const FileName: string; Offset: Int64): string;
var
  FS: TFileStream;
  Buf: TBytes;
  Head: string;
begin
  FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    if Offset >= FS.Size then
      Exit('offset beyond end of file: the file needs a repair pass');
    FS.Position := Offset;
    SetLength(Buf, 32);
    SetLength(Buf, FS.Read(Buf[0], Length(Buf)));
  finally
    FS.Free;
  end;
  Head := TEncoding.ASCII.GetString(Buf);
  if Head.StartsWith('xref') then
    Result := 'classic cross-reference table'
  else if Head.Contains(' obj') then
    Result := 'cross-reference stream (PDF 1.5 or later)'
  else
    Result := 'stale offset: the file needs a repair pass';
end;

Bir çapraz başvuru akışı (§7.5.8), /Type /XRef taşıyan sıkıştırılmış bir nesnedir; her sütunun bayt genişliğini veren bir /W dizisi ve ya bir bayt ofsetine ya da bir object stream'e (§7.5.7) işaret eden ikili satırlar taşır. Trailer anahtarları onun sözlüğüne taşınır; dosyada hiç trailer anahtar kelimesi kalmaz. HotPDF, THotPDF üzerinde UseXRefStream set edildiğinde bu biçimi yazar. Denetiminizden çıkan dosyalar için — e-posta ekleri ya da baskı atölyesi yüklemeleri gibi — klasik tablo daha güvenli seçim olmayı sürdürür; çünkü PDF 1.5 öncesi okuyucular onu hâlâ ayrıştırır. PDF/A-1 arşivler için soruyu kapatır: ISO 19005-1, PDF 1.4 üzerine kuruludur ve §6.1.4, çapraz başvuru akışlarının kullanılmayacağını söyler; HotPDF de bir PDF/A-1 profili etkinken klasik tablo yazar

48 0 obj
<< /Type /XRef /Size 49 /Root 1 0 R
   /W [1 3 1] /Index [0 49]
   /Filter /FlateDecode /Length 162 >>
stream
...binary rows: 1 + 3 + 1 bytes each...
endstream
endobj
startxref
48211
%%EOF

/Root'tan doğru sayfaya: katalog ve sayfa ağacı

Trailer'ın /Root'u belge kataloğunu işaret eder ve katalogun /Pages'i sayfa ağacının kökünü işaret eder. Her ara /Pages düğümü, altındaki yaprak sayfaların /Count'unu taşır; 5.000. sayfayı arayan bir okuyucu inerken sayıları çıkarır ve bütün alt ağaçları açmadan atlar. ISO 32000-1 §7.7.3 dengeli bir ağaç istemez; 10.000 girişli tek bir /Kids dizisi yasaldır, yalnızca araması yavaştır. Yalnızca yaprağı okuyan kod bir tuzağa düşer. /Resources, /MediaBox, /CropBox ve /Rotate kalıtsaldır (§7.7.3.4); kendi /MediaBox'ı olmayan bir sayfa hâlâ geçerlidir, çünkü değer bir atada yaşar

12 0 obj
<< /Type /Page
   /Parent 3 0 R
   /MediaBox [0 0 612 792]
   /Resources << /Font << /F1 7 0 R >> >>
   /Contents 13 0 R
>>
endobj

Medya kutusu, sayfaya çizilen her şey için koordinat uzayını belirler: başlangıç sol altta, Y yukarı doğru, bir birim 1/72 inç; bir PDF 1.6 /UserUnit'i ölçeklemedikçe. PDFium Component, bir sayfayı numarayla istediğinizde bütün inişi kendisi yapar. TPdf.PageMetrics, sayfa boyutunu kalıtsal kutular çoktan çözülmüş olarak point cinsinden döndürür, sayfa dönüşüyle birlikte; aşağıdaki listeleme böylece, yaprak sayfaları kendi kutusunu taşımayan dosyalarda bile doğru boyutlar raporlar

uses
  System.SysUtils, PDFium;

// Sayfa sayısını ve her sayfanın boyutunu point cinsinden yazdırır
procedure ListPages(const FileName: string);
const
  Degrees: array[TRotation] of Integer = (0, 90, 180, 270);
var
  Pdf: TPdf;
  I: Integer;
  M: TPdfPageMetrics;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;           // xref verisini, trailer'ı ve katalogu ayrıştırır
    if not Pdf.Active then        // başarısız bir yükleme Active = False bırakır
      raise Exception.Create('Cannot open ' + FileName);
    Writeln(Pdf.PageCount, ' pages');
    for I := 1 to Pdf.PageCount do
    begin
      M := Pdf.PageMetrics[I];    // 1 tabanlı; sayfayı ağaç üzerinden yükler
      Writeln(Format('Page %d: %.1f x %.1f pt, /Rotate %d',
        [I, M.Width, M.Height, Degrees[M.Rotation]]));
    end;
  finally
    Pdf.Free;
  end;
end;

Bir PDF ekranda bir sayfayı nasıl çizer?

Bir sayfanın görünür içeriği küçük bir programdır: bir ya da daha çok içerik akışındaki postfix operatörler; okuyucu /Filter'in adlandırdığı çözücüyü — çoğu zaman /FlateDecode — çalıştırdıktan sonra sırayla yorumlanır. BT bir metin nesnesi açar, Tf bir font kaynağı ve boyut seçer, Td metin konumunu taşır, Tj bir dize boyar, ET nesneyi kapatır. Yollar m, l, c ve re ile kurulur; sonra f ile doldurulur ya da S ile çizilir. Grafik durumu (§8.4), o operatörlerin örtük olarak okuduğunu tutar: dönüşüm matrisi, renkler, kırpma yolu gibi. q bir kopya iter, Q çeker; çiftin içinde kurulan bir dönüşüm sonraki içeriğe sızamaz

q
1 0 0 1 72 700 cm     % başlangıcı (72, 700) noktasına taşır
BT
  /F1 12 Tf           % 12 pointte F1 font kaynağı
  0 0 Td
  (Invoice 2026-0417) Tj
ET
0.2 0.2 0.8 RG        % kontur rengi, q/Q ile kapsamlı
0 -6 m 140 -6 l S
Q
Bir PDF bir sayfayı nasıl çizer: BT, Tf, Td ve Tj operatörlerinin boyadıkları izlere uzandığı içerik akışı bölümü
İşlenenler operatörlerden önce düşer ve q/Q çiftleri durum değişikliklerini çiteler; yerel dönüşümlerin komşu içeriğe sızmasını engeller

/F1 gibi isimler, sayfanın /Resources'ine açılan anahtarlardır. Bir font sözlüğü, font tanımlayıcısı (§9.8) üzerinden gömülü font programına ulaşır ve bir görsel, JPEG ya da Flate verisi çözüldükten sonra tek bir Do ile boyanır. Rasterleştirme en sona kalır. Renderer dönüşüm matrisini her yola ve glyph dış hattına uygular, pointleri cihaz piksellerine eşler ve sonucu yumuşatmayla doldurur. Sayfa o noktada hâlâ vektör verisi olduğundan yüzde 400'e yakınlaştırmak, bir bitmap'i germek yerine içerik akışını daha büyük ölçekte yeniden çalıştırır. Hedef çözünürlüğü çağıran seçer: ekran için inç başına 96 ya da 150 piksel, baskı için 300 ve üzeri. PDFium Component'te o aşama TPdf.RenderPage'tir; çıktı boyutunu piksel cinsinden alır

uses
  System.SysUtils, Vcl.Graphics, PDFium;

// FileName'in 1. sayfasını Dpi piksel/inç'te render eder ve BMP olarak kaydeder
procedure RenderFirstPage(const FileName: string; Dpi: Integer);
var
  Pdf: TPdf;
  Bmp: TBitmap;
  W, H: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    if not Pdf.Active then
      raise Exception.Create('Cannot open ' + FileName);
    Pdf.PageNumber := 1;        // 1 tabanlı
    // PageWidth ile PageHeight point cinsindendir (1/72 inç)
    W := Round(Pdf.PageWidth * Dpi / 72);
    H := Round(Pdf.PageHeight * Dpi / 72);
    Bmp := Pdf.RenderPage(0, 0, W, H, ro0, [reAnnotations]);
    try
      Bmp.SaveToFile(ChangeFileExt(FileName, '-p1.bmp'));
    finally
      Bmp.Free;                 // işlev formu yeni bir bitmap döndürür
    end;
  finally
    Pdf.Free;
  end;
end;

Sayfa düzgün görünürken kopyala-yapıştır neden çöp döndürür?

Kopyala-yapıştır, dosya hangi glyph'in çizileceğini kaydettiği hâlde hangi karakteri temsil ettiğini kaydetmediğinde başarısız olur. Render için gömülü font programındaki glyph şekilleri yeter. Çıkarım Unicode ister; o da standart bir kodlamadan ya da font sözlüğündeki bir /ToUnicode CMap'inden gelir (§9.10.3). /Encoding /Identity-H taşıyan bir bileşik font, içerik akışına ham glyph kimlikleri yazar ve bir ToUnicode haritası olmadan o kimliklerin hiçbir karakterle tanımlı bir ilişkisi yoktur. Ekran kusursuz görünürken pano sembollerle dolar. Alt kümeli fontlar bunu olası kılar; onlar yalnızca belgenin kullandığı glyphleri tutar ve /BaseFont /AAAAAA+ArialMT gibi adlardaki altı harfli önekten kolayca tanınır (§9.6.4). PDF font işleme rehberi, CJK fontlarını ve bozuk çıkarımı derinlemesine ele alır

Artımlı güncellemeler ve imza bayt aralığı

İmzalar, PDF'lerin yalnızca-ekle düzenlenme biçimine dayanır: artımlı bir güncelleme (§7.5.6), son %%EOF'den sonra nesneler ve yeni bir çapraz başvuru bölümü ekler ve yapı genel bakışı /Prev zincirini ile silinen içeriğin önceki revizyonlarda neden hayatta kaldığını anlatır. İmza sözlüğünün /ByteRange'i, [0 a b c] yazılışında, CMS imzasının durduğu /Contents hex dizesi hariç her şeyi karmalar; imzalı revizyon b + c baytında biter ve bir doğrulayıcı onu dosyayı orada keserek yeniden kurar. Sonraki revizyonlar o noktadan sonra düşer; tam bir yeniden yazım, bir linearizer ya da bir optimize geçişi ise imzalayanın karmaladığı baytları oynatır ve var olan her imzayı bir anda geçersiz kılar

PDF artımlı güncellemeleri nasıl çalışır: üst üste dizilmiş kayıt nesilleri; eklenen nesneleri, tazelenen trailer'ları ve bir dijital imzanın kapladığı bayt aralığını gösterir
Kaydetmeler yeniden yazmak yerine ekler; bu, doğrulayıcıların imza sonrası her değişikliği açığa çıkarmasını sağlarken yetkili düzenlemeler imzaları canlı tutar

Bir Delphi ya da Lazarus uygulaması, çapraz başvuru verisini ayrıştırmaktan ve sayfa ağacında yürümekten render ile metin çıkarımına kadar bu bütün hattı gerektirdiğinde, Delphi ve Lazarus için PDFium Component onu, yukarıdaki listelemelerde kullanılan TPdf belge bileşeni ile ekranda gösterim için TPdfView görüntüleyici kontrolü içine sarar