Teknik Makale

PDF Meta Verileri, Ana Hatları ve Ek Açıklamaları Açıklandı

Aynı PDF iki farklı yazar bildirebilir. Masaüstü bir görüntüleyicinin özellikler penceresi bir isim gösterirken, XMP'yi dizinleyen bir varlık yönetim sistemi başka bir isim gösterir ve her iki program da dosyayı doğru okuyor olur. PDF, belge meta verisini iki kez saklar; yer imlerini, bağlantıları ve notları ise sayfa içeriğine hiç dokunmayan yapılarda tutar. Bu yüzden her sayfa kusursuz görünürken bunlardan herhangi biri yanlış olabilir

Altındaki nesne modeli (dolaylı nesneler, çapraz referans tablosu, trailer) PDF belge yapısı incelemesinde ele alınıyor. Buradaki odak, belge kataloğundan asılan yapılarda kalıyor

Katalogdaki dört girdi, dört bağımsız alt sistem

Aşağıda anlatılan her şeye, trailer'ın /Root girdisinin işaret ettiği sözlük olan belge kataloğundan ulaşılır (ISO 32000-1 §7.7.2). Hepsini birbirine bağlayan bir katalog şöyle görünür:

1 0 obj
<< /Type /Catalog
   /Pages 2 0 R
   /Outlines 3 0 R
   /Names << /EmbeddedFiles 4 0 R /Dests 6 0 R >>
   /Metadata 5 0 R
>>
endobj
Sayfa ağacına, ana hat köküne, ad sözlüğüne ve XMP meta veri akışına dört isteğe bağlı alt sistem olarak bağlanan bir PDF belge kataloğu şeması
Belge kataloğu dört bağımsız alt sistemi birbirine bağlar; her biri kendi girdisi üzerinden ulaşılır. Sayfalar normal şekilde görünmeye devam ederken herhangi biri yok olabilir

/Pages görünür belgedir ve kataloğun zorunlu kıldığı dört girdiden tekidir. /Outlines yer imi ağacına, /Metadata XMP akışına, /Names ise ad sözlüğüne ulaşır; bu sözlük gömülü dosyaları /EmbeddedFiles altında, adlandırılmış varış noktalarını ise /Dests altında tutar. İsteğe bağlı üçünü hiç bulamayan bir okuyucu yine de her sayfayı gösterir; bu yüzden dosya yalnızca sayfaları anlayan araçlardan geçtiğinde ilk çürüyen katman burasıdır. Bir sayfa nesnesinin içindeki hiçbir şey ana hattın, /Dests ağacının veya XMP akışının yolunu göstermez. Yalnızca sayfaları yeni bir dosyaya kopyalayın, üçü de geride kalır

Bir PDF neden iki meta veri deposu taşır?

Bir PDF'in iki meta veri deposu taşımasının nedeni, biçimin ilkini emekliye ayırmadan ikinci bir depo büyütmesidir. Orijinal mekanizma, trailer'daki /Info ile referans verilen belge bilgi sözlüğüdür (ISO 32000-1 §14.3.3, Tablo 317): /Title, /Author, /Subject, /Keywords, /Creator, /Producer için düz metin girdileri, ayrıca /CreationDate, /ModDate ve /Trapped. ISO 32000-2, iki tarih girdisi dışındaki sözlüğü tamamıyla kullanımdan kaldırır ve yazarları XMP meta veri akışına yönlendirir

PDF Info sözlüğü ile XMP meta veri akışını karşılaştıran şema: çakışan alanlar taşıyan ve XMP doğruluk kaynağı olarak senkron kalması gereken iki depo
Info sözlüğü ile XMP akışı aynı sorulara paralel yanıt verir. İkisini tek bir değer kümesinden yeniden üreterek senkron tutun

XMP (ISO 32000-1 §14.3.2), RDF ile yazılmış bir XML belgesidir ve sözlüğü /Type /Metadata /Subtype /XML taşıyan bir akışta saklanır. Info sözlüğünün aksine bir XMP paketi, PDF sözdizimi hakkında hiçbir şey bilmeyen araçlar tarafından bulunup ayrıştırılacak şekilde tasarlanmıştır:

5 0 obj
<< /Type /Metadata /Subtype /XML /Length 1235 >>
stream
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
  <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
    <rdf:Description rdf:about=""
        xmlns:dc="http://purl.org/dc/elements/1.1/"
        xmlns:xmp="http://ns.adobe.com/xap/1.0/"
        xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
      <dc:title><rdf:Alt><rdf:li xml:lang="x-default">Quarterly Report</rdf:li></rdf:Alt></dc:title>
      <dc:creator><rdf:Seq><rdf:li>A. Author</rdf:li></rdf:Seq></dc:creator>
      <xmp:CreateDate>2026-06-16T10:46:27+08:00</xmp:CreateDate>
      <xmp:CreatorTool>Reporting Service 4.2</xmp:CreatorTool>
      <pdf:Producer>losLab PDF Library</pdf:Producer>
    </rdf:Description>
  </rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>
endstream
endobj

O bloktaki üç ayrıntı, paketin gerçek araç karşısında hayatta kalıp kalmayacağını belirler. xpacket işleme talimatları paketi çerçeveler, böylece bir tarayıcı onu rastgele bir bayt akışının içinde bulabilir; kapanış <?xpacket end="w"?> satırını düşürürseniz dosya yine açılır ama tarayıcılar son işaretini kaybeder. Değer türleri de önemlidir: dc:title bir dil alternatifidir ve rdf:Alt ister; dc:creator ise sıralı bir listedir ve rdf:Seq ister. İkisini de çıplak metin düğümü olarak yazmak kolayca yapılan bir hatadır. Ön ekler ise yalnızca gelenektir; ayrıştırıcı ad alanı URI'sine bakar, yanlış URI'ye bağlı bir dc: baştan başka bir özelliktir

Info anahtarları ile XMP karşılıkları şu şekilde eşleşir. PDF/A-1 (ISO 19005-1 §6.7.3) bu eşleştirmeyi bir uygunluk kuralı olarak koyar: iki depo da mevcutsa değerler eşdeğer olmalıdır

Info sözlüğüXMP özelliği
/Titledc:title (rdf:Alt)
/Authordc:creator (rdf:Seq)
/Subjectdc:description (rdf:Alt)
/Keywordspdf:Keywords
/Creatorxmp:CreatorTool
/Producerpdf:Producer
/CreationDate, /ModDatexmp:CreateDate, xmp:ModifyDate

Elle düzenlemeler ve birleştirmeler, iki deponun birbirinden uzaklaştığı yerdir. Bir birleştirme, /Info'yu bir girdiden, XMP paketini ise başka bir girdiden devralabilir. Daha güvenli kural, XMP'yi doğruluk kaynağı saymak, /Info'yu eski tüketiciler için tutmak ve ikisinden biri değiştiğinde ikisini de tek bir değer kümesinden yeniden üretmektir

Her iki depoyu tek bir özellik kümesinden yazmak

HotPDF her iki depoyu da aynı belge özelliklerinden kurar. Title, Author, Subject ve Keywords /Info'ya gider; EnableXMPMetadata True olduğunda EndDoc, aynı değerlerden bir XMP paketi daha yazar — dc:title bir rdf:Alt, dc:creator bir rdf:Seq olarak — ve onu kataloğun /Metadata girdisinden bağlar. EnableXMPMetadata varsayılan olarak False'tur ve elinizden çıkan her şey için açmaya değer: yalnızca /Info taşıyan bir dosya, salt XMP okuyan hiçbir araca görünmez. Ek şemalara ihtiyaç duyduğunuzda CustomXMP, üretilen paketi kendi metninizle değiştirir

uses
  System.SysUtils, Vcl.Graphics, HPDFDoc;

procedure BuildReport(const FileName: string);
var
  PDF: THotPDF;
  Chapter: THPDFDocOutlineObject;
begin
  PDF := THotPDF.Create(nil);
  try
    PDF.AutoLaunch := False;
    PDF.FileName := FileName;
    // Tek değer kümesi hem /Info'ya hem de XMP paketini besler
    PDF.Title := 'Quarterly Report';
    PDF.Author := 'A. Author';
    PDF.Subject := 'Results for the third quarter';
    PDF.Keywords := 'quarterly, results';
    PDF.EnableXMPMetadata := True;
    PDF.BeginDoc;

    PDF.CurrentPage.SetFont('Arial', [fsBold], 20);
    PDF.CurrentPage.TextOut(50, 50, 0, 'Chapter 1: Results');
    Chapter := PDF.OutlineRoot.AddChild('Chapter 1: Results', 0, 50);
    Chapter.Opened := True;   // pozitif /Count olarak kaydedilir

    PDF.AddPage;
    PDF.CurrentPage.SetFont('Arial', [fsBold], 16);
    PDF.CurrentPage.TextOut(50, 50, 0, 'Introduction');
    Chapter.AddChild('Introduction', 0, 50);

    PDF.AddPage;
    PDF.CurrentPage.SetFont('Arial', [fsBold], 16);
    PDF.CurrentPage.TextOut(50, 50, 0, 'Methodology');
    Chapter.AddChild('Methodology', 0, 50);

    PDF.EndDoc;
  finally
    PDF.Free;
  end;
end;

Mevcut bir dosyayı düzenlemek farklıdır. Yüklenmiş bir belgede SetLoadedTitle, SetLoadedAuthor, SetLoadedCreator ve kardeşleri Info sözlüğü girdisini yazar; XMP akışı ise SetLoadedXMPMetadata ile değiştirdiğiniz ayrı bir nesnedir. Yazarı bir çağrıyla değiştirip diğerini atladıysanız SaveLoadedDocument, açılış paragrafındaki iki yazarlı dosyanın aynısını üretir

Yer imi panelinin arkasındaki ana hat ağacı

Yer imi paneli, belge ana hattı (document outline) adı verilen çift yönlü bağlı bir sözlük ağacından yeniden kurulur (ISO 32000-1 §12.3.3, Tablo 152 ve 153). Dosyanın hiçbir yerinde yer imi dizisi yoktur; görüntüleyici, kataloğun /Outlines girdisinden referansları izleyerek paneli yeniden inşa eder

Görüntüleyici yer imi panelini yeniden kuran Parent, First, Last, Prev, Next ve Count bağlantılarını gösteren PDF ana hat ağacı şeması
Yer imi paneli, Parent, First, Last, Prev, Next ve Count ile örülmüş çift yönlü bağlı bir anahat ağacından yeniden kurulur. Tek bir bayat bağlantı, hiçbir hata vermeden bütün bir dalı gizler
8 0 obj                                    % ana hat kökü
<< /Type /Outlines /Count 3 /First 9 0 R /Last 9 0 R >>
endobj
9 0 obj                                    % üst düzey: bir bölüm
<< /Title (Chapter 1: Results)
   /Parent 8 0 R /Count 2
   /First 12 0 R /Last 15 0 R
   /Dest [3 0 R /XYZ 0 792 0] >>
endobj
12 0 obj                                   % ilk çocuk
<< /Title (Introduction)
   /Parent 9 0 R /Next 15 0 R
   /Dest [4 0 R /XYZ 72 720 0] >>
endobj
15 0 obj                                   % ikinci çocuk, son kardeş
<< /Title (Methodology)
   /Parent 9 0 R /Prev 12 0 R
   /Dest [5 0 R /Fit] >>
endobj

Değişmezler doğrudan bağlantılardan okunur. Her öğe kendi /Parent girdisini işaret eder. Kardeşler /Prev ve /Next üzerinden zincirlenir; ilk çocuğun /Prev'i, sonun /Next'i yoktur. Bir ebeveyn iki ucunu /First ve /Last ile adlandırır; aradakiler yalnızca kardeş gezinmesinin birer adımı olarak var olur. Tek bir bağlantıyı bozun, görüntüleyiciler hâlâ ulaşabildiklerini gösterir: bayat bir /Next bir bölümü kısaltır, zincirin sonuyla uyuşmayan bir /Last ise farklı görüntüleyicilerin farklı ağaçlar çizmesine yol açar

/Count insanları şaşırtır çünkü panel durumunu saklar. Kökte, tüm seviyelerde görünür öğelerin sayısıdır. Bir öğede pozitif değer, öğenin açık olduğu ve görünen soy sayısını verdiği anlamına gelir; negatif değer ise kapalı olduğu anlamına gelir ve büyüklüğü, genişletilince kaç soyun görüneceğini bildirir. Örnekte kök 3 tutar: bir bölüm artı iki açık çocuğu. Her /Count'u pozitif toplam olarak yazan bir üretici, yazarın kapattığı her dalı sessizce yeniden açar

HotPDF'de OutlineRoot bir THPDFDocOutlineObject'tir; AddChild(Title, X, Y) bir öğe ekler, /Parent, /Prev, /Next, /First ve /Last bağlantılarını bağlar ve öğeyi geçerli sayfaya [page /XYZ left top 0] olarak işaret eder. İşareti Opened özelliği belirler ve yazıcı, EndDoc anında tamamlanmış ağaçtan her /Count'u yeniden hesaplar; bu yüzden çocuklar eklendikten sonra Opened ayarlamak sorunsuzdur. InsertBefore, InsertAfter ve MoveTo aynı defter tutmayla ağacı yeniden yapılandırır

Varış noktaları: bir tıklamanın gerçekte indiği yer

Yer imleri de bağlantı ek açıklamaları da bir varış noktasına (destination) çözümlenir (ISO 32000-1 §12.3.2, Tablo 151) ve varış noktası bir sayfa numarasından fazlasıdır. Bir sayfa nesnesini adlandıran, ardından görüntüleyiciye onu nasıl çerçeveleyeceğini söyleyen bir fit türü içeren dizidir. Ana iş atı [page /XYZ left top zoom]'dur. Üç işlenenden herhangi biri "okuyucunun ne bıraktıysa onu koru" anlamına gelen null olabilir ve 0 yakınlaştırma null ile aynı anlama gelir. Dolayısıyla [page /XYZ null null null] sayfayı değiştirir, başka hiçbir şeye dokunmaz; "sayfaya git" bağlantısının genellikle yapması gereken de tam olarak budur

Fit ailesi kesin konumlandırmayı dayanıklılıkla takas eder. [page /Fit] tüm sayfayı pencereye ölçekler, [page /FitH top] sayfa genişliğini verilen üst kenara sığdırır, [page /FitR left bottom right top] ise bir dikdörtgeni görünümü dolduracak şekilde yakınlaştırır. Bunlar ölçeği tıklama anında sayfa geometrisinden hesaplar; içine kalıptan yakınlaştırma gömülmüş bir /XYZ, sayfa yeniden boyutlandırıldıktan sonra bile onu korur. İçindekiler tablosu için bölümün üst kenarındaki /FitH daha iyi bir varsaylandır

Asıl insanları yakan yer koordinatlar. Varış noktası sayıları varsayılan kullanıcı alanındadır (default user space): kök sol alttadır, y yukarı doğru büyür (§8.3.2.3); oysa ekran yerleşim alışkanlıkları üstten ölçer. HotPDF'in tuval tarzı çağrıları (TextOut, bağlantı dikdörtgenleri, AddChild'in X ve Y'si) yukarıdan aşağıya koordinat alır ve onları sizin için dönüştürür. RegisterNamedDestination ise Left ve Top değerlerini verildikleri gibi varış dizisine yazar; yani bunların zaten PDF kullanıcı alanında olması gerekir:

// BeginDoc ile EndDoc arasında, 0..2 sayfaları var olduktan sonra çağırın
procedure AddContentsLinks(PDF: THotPDF);
var
  R: TRect;
begin
  // 1. sayfanın üstünden 50 pt aşağıda adlandırılmış varış noktası.
  // Top PDF kullanıcı alanındadır; alt kenardan yukarıya ölçülür
  PDF.CurrentPageNumber := 1;
  PDF.RegisterNamedDestination('chapter-1', 1, dfmFitH,
    0, PDF.CurrentPage.Height - 50, 0, 0, 0);

  // 0. sayfadaki içindekiler girdileri; dikdörtgenler yukarıdan aşağıya koordinat kullanır
  PDF.CurrentPageNumber := 0;
  R := Rect(50, 175, 545, 209);
  PDF.CurrentPage.AddNamedDestinationLink(R, 'chapter-1', 'Chapter 1: Results');

  // YPos = -1, [page /XYZ null null null] yazar: sayfayı değiştir, görünümü koru
  R := Rect(50, 225, 545, 259);
  PDF.CurrentPage.AddGoToLink(R, 2, -1, 'Methodology');

  R := Rect(50, 275, 545, 309);
  PDF.CurrentPage.AddURILink(R, 'https://www.example.com', 'Project website');
end;

Top olarak ekran tarzı 50 değerini geçerseniz bağlantı, alt kenardan 50 punto yukarıya iner ve başlık yerine altbilgiyi gösterir. Adlandırılmış varış noktası kataloğun /Dests ad ağacında yaşar ve her bağlantı isim üzerinden çözümlenir; aynı bölümlere onlarca bağlantı verirken doğru tercih budur. Bir bölümü taşıdığınızda /Dests içinde tek bir girdi düzenlersiniz; onu adlandıran her bağlantı peşinden gelir. Bu varış noktalarının dayandığı sayfa sırası, sayfa ağacı sırasının neden önemli olduğunda ele alınıyor

Ek açıklamalar: içerik akışının dışındaki etkileşimli nesneler

Bir ek açıklama (annotation), içerik akışının parçası olmadan sayfanın üzerine oturur (ISO 32000-1 §12.5). Bağlantılar, yapışkan notlar, vurgulamalar, form bileşenleri, dosya eki simgeleri ve damgalar hep kendi sayfalarının /Annots dizisinde yaşar. Bir girdiyi kaldırırsınız, ek açıklama kaybolur; içerik akışı ise bayt bayt aynı kalır

Her ek açıklama küçük bir çekirdeği paylaşır (Tablo 164): /Subtype türü adlandırır, /Rect varsayılan kullanıcı alanında sınırlayıcı kutuyu verir, /Contents ise yardımcı teknolojinin okuduğu alternatif açıklama görevini de gören metni tutar. Bağlantı ek açıklamaları (§12.5.6.5, Tablo 173) iki biçimde gelir:

20 0 obj                                    % bir varış noktasına bağlantı
<< /Type /Annot /Subtype /Link
   /Rect [100 200 300 250]
   /Border [0 0 0]
   /Dest [5 0 R /XYZ null null null] >>
endobj
21 0 obj                                    % eylem çalıştıran bağlantı
<< /Type /Annot /Subtype /Link
   /Rect [50 50 200 100]
   /Border [0 0 0]
   /Contents (Project website)
   /A << /Type /Action /S /URI /URI (https://www.example.com) >> >>
endobj

/Border [0 0 0] boşuna yoktur: varsayılan kenarlık [0 0 1]'dir ve birçok görüntüleyici her bağlantının çevresine bir punto kalınlığında çerçeve çizer. /Dest ile /A birbirini dışlar; eylemin /S alanı davranışı seçer: bu dosya içinde /GoTo (§12.6.4.2), başka bir PDF'deki sayfa için /GoToR (§12.6.4.3), web adresi için /URI (§12.6.4.7), harici bir uygulama veya dosya açmak için /Launch (§12.6.4.5). Dağıttığınız belgelerde /Launch'a yer vermeyin: görüntüleyiciler genellikle onu engeller ya da önüne güvenlik sorusu koyar, PDF/A ise tümden yasaklar (ISO 19005-1 §6.6.1). Yukarıdaki bağlantı çağrılarının Description argümanı /Contents'u doldurur; HotPDF'de PDFUACompliance açıkken boş açıklama istisna fırlatır, çünkü PDF/UA-1 bu girdiyi her bağlantıda zorunlu kılar

Vurgulama ve not gibi işaretleme ek açıklamaları ile /Square gibi şekiller bir pürüz ekler: türleri görünümlerini belirlemez. Bir görünüm akışı (/AP girdisi, §12.5.5) olmadan her görüntüleyici kendi yorumunu çizer. ISO 32000-2, çoğu ek açıklama türü için /AP'yi zorunlu kılar; Popup ile Link istisnalar arasındadır. Dosya ekleri aynı düzeneği yeniden kullanır: bir dosya belirtimine (§7.11) sarılmış gömülü dosya akışı; ya sayfada /FileAttachment ek açıklaması olarak ya da kataloğun /Names altındaki /EmbeddedFiles ad ağacı üzerinden ortaya çıkar

Dosya gönderilmeden önce bu katmanı nasıl kontrol edersiniz?

İki kontrol çoğunu yakalar: gerçek bir görüntüleyicide yer imi panelini ve bir bağlantı örneğini tıklayarak geçin, sonra meta veriyi bir programla geri okuyun; ne kadar çok tıklarsanız tıklayın XMP ile çelişen bir /Info bu yolla ortaya çıkmaz. HotPDF'in yüklenmiş belge çağrıları iki yarısını da kapsar. GetLoadedOutlineTitlesDeep, /First ve /Next zincirini derinlemesine izler — görüntüleyicinin yaptığı gezinmenin aynısı; bayat bir bağlantıyla kopmuş dal onun sonucunda da eksiktir. FindLoadedOutlineByTitle bir yer iminin hedef sayfasını çözer ve hedef adlandırılmış bir varış noktasıysa veya bir sayfaya çözümlenmiyorsa -1 bildirir

// StripXmlTags sizin eklediğiniz bir yardımcıdır; kütüphane çağrısı değildir.
// GetLoadedXMPValue öğenin iç işaretlemesini döndürür; dc:creator
// rdf:Seq ve rdf:li etiketlerine sarılı olarak geri gelir
function StripXmlTags(const S: AnsiString): AnsiString;
var
  I: Integer;
  InTag: Boolean;
begin
  Result := '';
  InTag := False;
  for I := 1 to Length(S) do
    if S[I] = '<' then InTag := True
    else if S[I] = '>' then InTag := False
    else if not InTag then Result := Result + S[I];
  Result := AnsiString(Trim(string(Result)));
end;

procedure CheckNavigationLayer(const FileName: string);
var
  PDF: THotPDF;
  Title, Author, Subject, Keywords, Creator, Producer, XmpAuthor: AnsiString;
  Titles: THPDFAnsiStringArray;
  I, PageIndex: Integer;
begin
  PDF := THotPDF.Create(nil);
  try
    PDF.AutoLaunch := False;
    if PDF.LoadFromFile(FileName) <= 0 then
      raise Exception.Create('No readable pages');

    if PDF.GetLoadedDocumentInfo(Title, Author, Subject, Keywords,
      Creator, Producer) then
    begin
      XmpAuthor := StripXmlTags(PDF.GetLoadedXMPValue('dc', 'creator'));
      if (XmpAuthor <> '') and (XmpAuthor <> Author) then
        Writeln('Author mismatch: /Info="', Author, '" XMP="', XmpAuthor, '"');
    end;

    Titles := PDF.GetLoadedOutlineTitlesDeep;
    Writeln(Length(Titles), ' bookmarks reachable');
    for I := 0 to High(Titles) do
      if PDF.FindLoadedOutlineByTitle(Titles[I], PageIndex, False, True)
        and (PageIndex < 0) then
        Writeln('No direct page target: ', Titles[I]);
  finally
    PDF.Free;
  end;
end;

Yazdığınızdan düşük bir yer imi sayısı, bozuk bir kardeş zincirine işaret eder. Adlandırılmış varış noktası kullanan bir yer imindeki -1 beklenen durumdur; FindLoadedOutlineByTitle ilk eşleşmede durur, bu yüzden yinelenen başlıklar ilk başlığın hedefini bildirir. GetLoadedXMPValue paket üzerinde düz metin aramasıdır ve karşılaştırma bayt bayttır; ASCII dışı isimlerde uyuşmazlığa güvenmeden önce iki değerin nasıl döndüğüne bakın

Bu katmandaki her şey referans defter tutmadır ve defter tutma, bir kütüphaneye devredilmeye değer kısımdır. Delphi ve C++Builder için HotPDF Delphi Component, her iki meta veri deposunu da tek bir özellik kümesinden yazar, öğe ekledikçe ana hat bağlantılarını örür ve bağlantı ek açıklamalarını eylemleriyle ve açıklamalarıyla iliştirir; yüklenmiş belge API'si aynı yapıları kontrol için geri okur