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
- Dosyanın sonuna gidin ve
startxrefanahtar kelimesinden sonra yazılı bayt ofsetini okuyun - O ofsetteki çapraz başvuru verisini yükleyin; o, her nesne numarasını dosyadaki bir konuma eşler
- Trailer'ı okuyun ve
/Root'u izleyerek belge kataloğuna gidin - Katalogun
/Pagesgirişini izleyerek sayfa ağacından inin ve istenen sayfa nesnesine ulaşın - Sayfanın içerik akışını çözün ve operatörlerini sırayla yorumlayın
- O operatörlerin
/Resourcesüzerinden adlandırdığı fontları, görselleri ve form XObject'leri çözün - 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
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
/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
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