Üç teklif bir PDF Portfolio'ya girer ve viewer'ın azalan sıralı Amount sütunu, USD 104,250'lik teklifi en sona, USD 9,800'ün altına koyar — tutarlar metin olarak girilmiştir. Sütun değerleri, ekli her dosyanın /CI girdisinde yaşar ve HotPDF'teki SetPortfolioItemNumericMetadata bunları oraya PDF sayısı olarak yazar; böylece sıralama, iki string'i karşılaştırmak yerine 104250 ile 9800'ü karşılaştırır
Bir PDF'i Portfolio olarak açan şey nedir?
Tek bir catalog anahtarı kararı verir. ISO 32000-1 §12.3.5 şöyle der: bir /Collection sözlüğü varsa, kurallara uyan bir okuyucu belgeyi portable collection olarak sunmak zorundadır (Acrobat'ın PDF Portfolio dediği şeyin şartnamedeki adı) ve /Names /EmbeddedFiles ad ağacındaki her dosya bir üyedir; o ağacın dışındaki ekler değildir. On eki olan ama /Collection olmayan bir PDF, yalnızca ekler paneli kalabalık bir PDF'tir — tek bir makine tarafından okunabilir dosyanın yanında taşındığı, bir ZUGFeRD ya da Factur-X e-faturasındaki XML'in yaptığı gibi, bu doğru biçimdir. Sözlük küçüktür ve neredeyse tamamen isteğe bağlıdır; tablo numaraları iki sürüm arasında kaymıştır, bu yüzden ikisi de listelenir:
| Girdi | Neyi denetler | ISO 32000-1 | ISO 32000-2 |
|---|---|---|---|
/View, /D | İlk görünüm (D details, T tiles, H hidden; PDF 2.0'da navigator için C) ve ilk gösterilen dosyanın ad ağacı anahtarı | Table 155 | Table 153 |
/Schema | Dosya listesinin sütunları | Tables 156, 157 | Tables 154, 155 |
/Sort | Sıralama alanları ve yönleri | Table 158 | Table 156 |
/Colors | Yerleşim için önerilen DeviceRGB paleti | yok | Table 157 |
/Split | Splitter çubuğunun yönü ve konumu | yok | Table 158 |
/Folders | Klasör ağacının kökü | yok | Table 159 (§12.3.5.2) |
Konteynerin kendi sayfaları da önemlidir, çünkü kapak sayfası onlardır. /D yokken ilk belge konteynerin kendisidir; ISO 32000-2 ayrıca, var olmayan bir dosyayı adlandıran bir /D durumunda okuyucunun listedeki ilk dosyayı seçeceğini ekler. Portfolio desteği olmayan bir viewer /Collection'ı yok sayar ve yalnızca o sayfaları gösterir; §12.3.5'teki NOTE 2 orada bir başlık ve tanıtıcı bir paragraf ister. Alışılmış cevap, paketin ne taşıdığını ve portfolio destekli bir viewer gerektirdiğini söyleyen tek sayfalıktır. ConfigurePortfolio(Name, View, InitialDocument) görünümü ile /D'yi tek çağrıda ayarlar; ConfigurePortfolioView ise sonradan pvmDetails, pvmTile ve pvmHidden arasında geçiş yapar. C görünümü bir navigator sözlüğü ister (ISO 32000-2 Table 160); bir teklif paketi ya da teslim seti için kullanıcıların beklediği, makul bir sıralamayla details görünümüdür
Schema alanları ve her sütunun arkasındaki değerler
Sütunlar, collection schema sözlüğünden gelir: seçtiğiniz her anahtar için bir collection field sözlüğü. Alanın çoğu sunumdur — /N başlıktır, /O sütunun konumu, /V ile /E ilk görünürlük ve düzenlenebilirliktir; bunlar DisplayName, Order, Visible ve Editable olarak THotPDF.AddPortfolioSchemaField'a geçer. Sonuçları olan karar /Subtype'tır. Değeri kimin vereceğini söyler:
/Subtype | HotPDF sabiti | Değer nereden gelir |
|---|---|---|
| S, D, N | cfsText, cfsDate, cfsNumber | Siz, dosya başına, /CI içinde |
| F, Desc | cfsFileName, cfsDescription | Dosya tanımı (/UF ya da /F, /Desc) |
| ModDate, CreationDate, Size | cfsModDate, cfsCreationDate, cfsSize | Gömülü dosyanın /Params'ı |
| CompressedSize (PDF 2.0) | cfsCompressedSize | Gömülü stream'in /Length'i |
Dosya bazlı değerler, dosya tanımının /CI girdisinin işaret ettiği collection item sözlüğünde durur (ISO 32000-1 §7.11.6 Table 48, 2. kısımda Table 46). Her girdi ya bir text string'dir, ya bir tarih, ya bir sayı ya da bir collection subitem sözlüğüdür (Table 49, 2. kısımda Table 47): sözlüğün /D'si veriyi taşır, /P'si ise sıralamanın yok saydığı bir gösterim öneki tutar. Şartnamenin kendi Example 2'si önekin neden var olduğunu gösterir: e-posta konusu /D (Let's have lunch on Friday!) olarak saklanır ve yanına /P (Re:) gelir; böylece bir yanıt, cevapladığı mesajın dibinde sıralanır. Garip olan, iki sürümün de o örneğin şemasını §12.4.2 Page Labels'ta gösterdiği iddiasıdır; kastettikleri, hemen üstündeki Example 1'dir. Dosyaya bağlı sütunlara gelince: AddDocumentAttachment, /Params tarihlerini gömme anıyla damgalar; böylece bir cfsModDate sütunu paketin ne zaman kurulduğunu gösterir. Kaynak dosyanın kendi tarihini görmek isteyen gözden geçirenlere, aşağıdaki received gibi kendinizin doldurduğu bir D sütunu verin
uses
System.SysUtils, HPDFDoc;
procedure BuildBidPackage;
const
Files: array[0..2] of string =
('bids\civil.pdf', 'bids\electrical.pdf', 'bids\hvac.pdf');
Trades: array[0..2] of AnsiString = ('Civil', 'Electrical', 'HVAC');
Amounts: array[0..2] of Double = (104250.0, 98500.0, 9800.0);
var
Pdf: THotPDF;
SortKeys: THPDFPortfolioSortKeyArray;
Key: AnsiString;
Stamp: TDateTime;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.Version := pdf17;
Pdf.FileName := 'bid-package.pdf';
Pdf.BeginDoc;
// Kapak sayfası: portfolio desteği olmayan bir viewer'ın göreceği her şey
Pdf.CurrentPage.SetFont('Arial', [], 16);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Riverside depot - bid package');
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 736, 0,
'Three bids are attached. Open this PDF Portfolio in a portfolio-capable viewer.');
Pdf.ConfigurePortfolio('', 'D', 'civil.pdf');
Pdf.AddPortfolioSchemaField('file', 'File', cfsFileName, 0);
Pdf.AddPortfolioSchemaField('trade', 'Trade', cfsText, 1);
Pdf.AddPortfolioSchemaField('amount', 'Amount', cfsNumber, 2);
Pdf.AddPortfolioSchemaField('received', 'Received', cfsDate, 3);
Pdf.AddPortfolioSchemaField('size', 'Size', cfsSize, 4, False); // ilk başta gizli
for I := 0 to High(Files) do
begin
Pdf.AddDocumentAttachment(Files[I], Trades[I] + ' bid');
Key := AnsiString(ExtractFileName(Files[I])); // ad ağacı anahtarı
Pdf.SetPortfolioItemMetadata(Key, 'trade', Trades[I]);
Pdf.SetPortfolioItemNumericMetadata(Key, 'amount', Amounts[I], 'USD ');
if FileAge(Files[I], Stamp) then
Pdf.SetPortfolioItemMetadata(Key, 'received',
AnsiString('D:' + FormatDateTime('yyyymmddhhnnss', Stamp)));
end;
SetLength(SortKeys, 2);
SortKeys[0].Key := 'amount';
SortKeys[0].Ascending := False; // en büyük teklif üstte
SortKeys[1].Key := 'trade';
SortKeys[1].Ascending := True; // eşitlik bozucu
Pdf.SetPortfolioSortOrder(SortKeys);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
SetPortfolioItemMetadata ile SetPortfolioItemNumericMetadata, schema anahtarı kayıtlı değilse ya da verilen anahtarlı hiçbir gömülü dosya yoksa istisna fırlatır; o yüzden önce şemayı kaydedin, dosyayı ekleyin, sonra değerleri verin. FileName argümanları ad ağacı anahtarıdır; AddDocumentAttachment için bu, dizinsiz dosya adıdır. Anahtarlar, bayt string'leri olarak büyük/küçük harfe duyarlı karşılaştırılır (§7.9.6) — /Dests ağacındaki named destination'lar gibi — dolayısıyla Civil.pdf, civil.pdf'i bulamaz. İki kaynak dosya aynı ada sahipse HotPDF ikisini de tutar ve ikincisine hvac (2).pdf gibi numaralı bir anahtar verir; metadata çağrıları o zaman o anahtarı kullanmak zorundadır
104,250'lik bir teklif 9,800'ün altına nasıl düşer?
Metin olarak saklanmıştır. §12.3.5'teki sıralama kuralları veri türüne bağlıdır: sayılar sayısal, tarihler eskiden yeniye, text string'ler sözcüksel sıralanır; NOTE 3 sözcüksel sıranın ayrıntılarını her okuyucuya bırakır. Table 48 (2. kısımda Table 46), her /CI girdisinin alanının /Subtype'ıyla uymasını ister; bir N sütununa metin olarak yazılmış "USD 104,250" bu kuralı zaten bozar. Okuyucunun elinde string karşılaştırması kalır. Ortak "USD " sonrasında 1 karakteri 9'dan önce gelir ve azalan sıralama en büyük teklifi sona koyar. SetPortfolioItemMetadata her zaman bir text /D yazar; S alanları ve tarih string'leri için doğrudur. N alanları için SetPortfolioItemNumericMetadata(FileName, Key, Value, Prefix, UseSubitemDictionary) bir PDF sayısı yazar ve para birimini /P'de taşır; sıralama ona bakmaz. Birimleri ve para birimlerini her sayısal sütunda önekte tutun: ekranda yalnızca sayı gibi görünen değer, yanlış sıralanan değerdir. Boş bir önek ve UseSubitemDictionary özelliğine False atanmış haliyle sayı, doğrudan /CI'ya gider
Tarihler aynı mantığı izler. Bir D alanı, D:20261031143000 gibi bir PDF tarih string'i bekler (§7.9.4); Windows yerel ayarından gelen 31/10/2026 ise bir tarih sütununda metindir ve karakterleri nereye denk gelirse oraya düşer. Sıralamanın kendisi collection sort sözlüğünde yaşar: /S tek bir alan adı ya da bir dizidir; dizide sonraki adlar öncekilerin bıraktığı eşitlikleri bozar. /A ise bir boolean ya da ona paralel boolean dizisidir; fazla girişler yok sayılır, eksikler true okunur, yani artan. SetPortfolioSortOrder, THPDFPortfolioSortKey kayıtlarından oluşan bir dizi alır ve tek anahtar için tek adlı formu, birkaç anahtar için dizi formunu yazar. Şemada kaydettiğiniz anahtarları kullanın; /S yalnızca üst collection sözlüğünde tanımlı alanları adlandırabilir
PDF 2.0 klasörleri dosya adlarında yaşar
Bir klasör, PDF'in içinde hiçbir dosya tutmaz. ISO 32000-2 §12.3.5.2, klasör sözlüklerini tek bir ağaca bağlanan indirekt nesneler olarak tanımlar: /Folders kökü işaret eder, her çocuk /Parent taşır, bir klasör ilk çocuğuna /Child üzerinden ulaşır ve kardeşler /Next ile zincirlenir. Her klasörün zorunlu bir /Name'i ve belge içinde benzersiz, negatif olmayan bir integer /ID'si vardır; kökün /Free dizisi hâlâ kullanılmamış ID aralıklarını listeler. Üyelik EmbeddedFiles anahtarına kodlanır: < ile başlayıp rakamlarla devam eden ve > ile kapanan bir anahtar, o ID'li klasöre aittir; gerisi dosya adıdır. Yani <3>invoice-1031.pdf, 3 numaralı klasördeki invoice-1031.pdf'tir. Anahtar, klasörün kendisidir. Kuralın dışındaki her anahtar köke aittir; 2026 klasörüne gitmesi gereken faturaların en üst düzeye düşmesi böyledir — sıradan AddDocumentAttachment ile eklenirler, anahtarlarında hiçbir önek taşımazlar
HotPDF ağacı üç çağrıyla kurar. EnsureCollectionRootFolder(RootName, Description) ID 0 ile kökü oluşturur ya da mevcut kökün ID'sini döndürür. AddCollectionChildFolder(ParentFolderID, FolderName, Description) bir çocuk ekler, yeni ID'sini döndürür ve /Free'yi güncel tutar. AddDocumentAttachmentToCollectionFolder(FolderID, FileName, Description) dosyayı <FolderID>name altına gömer ve çıplak adı /F ile /UF'e yazar; bir cfsFileName sütununun göreceği de budur. Dosyayı adlandıran sonraki her çağrı tam anahtarı ister; InitialDocument'in ConfigurePortfolio içindeki kullanımı ve metadata çağrıları dahil. Klasörler, /Split ve /Colors PDF 2.0 girdileridir; bu yüzden Version değerini pdf20 yapın, BeginDoc'tan önce
procedure BuildHandoverPackage;
var
Pdf: THotPDF;
RootID, Year2025, Year2026, Q4: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.Version := pdf20; // /Folders, /Split ve /Colors PDF 2.0 girdileridir
Pdf.FileName := 'handover.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 16);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Project handover - PDF Portfolio');
Pdf.ConfigurePortfolio('', 'D', '<0>handover-summary.pdf');
Pdf.AddPortfolioSchemaField('file', 'File', cfsFileName, 0);
Pdf.AddPortfolioSchemaField('amount', 'Amount', cfsNumber, 1);
RootID := Pdf.EnsureCollectionRootFolder('Handover', 'All deliverables'); // 0
Year2025 := Pdf.AddCollectionChildFolder(RootID, '2025'); // 1
Year2026 := Pdf.AddCollectionChildFolder(RootID, '2026'); // 2
Q4 := Pdf.AddCollectionChildFolder(Year2026, 'Q4 invoices', 'Oct-Dec'); // 3
Pdf.AddDocumentAttachmentToCollectionFolder(RootID,
'out\handover-summary.pdf', 'Read this first'); // <0>handover-summary.pdf
Pdf.AddDocumentAttachmentToCollectionFolder(Year2025,
'out\as-built-2025.pdf', 'As-built drawings'); // <1>as-built-2025.pdf
Pdf.AddDocumentAttachmentToCollectionFolder(Q4,
'out\invoice-1031.pdf', 'October invoice'); // <3>invoice-1031.pdf
Pdf.SetPortfolioItemNumericMetadata(
AnsiString(Format('<%d>invoice-1031.pdf', [Q4])), 'amount', 10400.0, 'USD ');
Pdf.SetPortfolioSplit(csdVertical, 30); // splitter, pencerenin %30 unda
Pdf.SetPortfolioColor('Background', 0.96, 0.97, 0.98);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Kardeş sırası standartta bir öneridir: kardeşler, collection sıralama anahtarını, o yoksa klasör adlarını izlemelidir. AddCollectionChildFolder kardeşleri çağrı sırasına göre bağlar; o yüzden onları sıralı yaratın — örneğin 2025'i 2026'dan önce yarattığı gibi. /Split konumu (ISO 32000-2 Table 158) 0 ile 100 arası bir yüzdedir; 30 vardır, 0.3 değil. Table 159 sonrasındaki adlandırma kuralları klasör adları için olduğu kadar klasör içindeki dosyalar için de geçerlidir ve o adlar genellikle kullanıcılardan ya da yükleme formlarından geldiği için çağrı öncesinde süzün; AddCollectionChildFolder kendisi eğik çizgileri, kontrol karakterlerini ve ASCII harf büyük/küçüklüğü yok sayıldığında mevcut birini tekrarlayan kardeş adını reddeder:
- 1 ile 255 karakter arası; içine gömülü NUL yok
- adın herhangi bir yerinde şunlardan biri yok:
/ \ : * " < > | - son karakter olarak nokta yok
- adları yalnızca büyük/küçük harf farkıyla ayrılan iki kardeş klasör ve aynı klasörde iki dosya yok
Başka bir üreticinin yazdığı Portfolio'yu okumak
Bir okuyucu, /CI'nin her meşru biçimini kabul etmek zorundadır; çünkü üreticiler birbirinden farklıdır: kimisi her değeri bir subitem sözlüğüne sarar, kimisi çıplak string'ler ve sayılar yazar, şartnamenin Example 2'si ikisini tek bir item içinde harmanlar. Subitem /D'si altında yalnızca string arayan kod, ikinci grup için boş sütunlar gösterir. GetLoadedCollectionItemFields(FileName), girdi başına bir THPDFCollectionItemFieldValue döndürür; IsSubitemDictionary, IsNumeric, NumericValue, StringValue ve Prefix dolu olarak. GetLoadedCollectionInfo şemayı, sıralama anahtarlarını, split'i, renkleri ve GetLoadedCollectionFolderTree klasör düğümlerini ağaç sırasıyla verir; her düğümde ID, Name, ParentID, FirstChildID ve NextSiblingID vardır. Her dosyanın klasörü anahtarından gelir ve parse kuralı harfi harfine izlemelidir: köşeli ayraçlar arasında yalnızca rakam. Folders'tan habersiz bir üreticinin <draft>notes.txt gibi bir anahtarında orada rakam yoktur; dosya köke aittir. İlk >'ye kadar olan her şeyi kesmek, draft adında bir klasör icat ederdi
procedure DumpPortfolio(const PdfFile: string);
var
Pdf: THotPDF;
Info: THPDFCollectionInfo;
Keys: THPDFAnsiStringArray;
Fields: THPDFCollectionItemFieldValueArray;
BareName: AnsiString;
I, J, FolderID: Integer;
Line: string;
// ISO 32000-2 12.3.5.2: '<' rakamlar '>' dosya adı; öteki her anahtar köktedir
function FolderOfKey(const Key: AnsiString; RootID: Integer;
out Name: AnsiString): Integer;
var
P: Integer;
begin
Result := RootID;
Name := Key;
if (Length(Key) < 3) or (Key[1] <> '<') then
Exit;
P := 2;
while (P <= Length(Key)) and (Key[P] in ['0'..'9']) do
Inc(P);
if (P = 2) or (P > Length(Key)) or (Key[P] <> '>') then
Exit;
Result := StrToIntDef(string(Copy(Key, 2, P - 2)), RootID);
Name := Copy(Key, P + 1, MaxInt);
end;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile(PdfFile) <= 0 then
Exit;
if not Pdf.GetLoadedCollectionInfo(Info) then
begin
Writeln('No /Collection in the catalog: plain attachments only');
Exit;
end;
for I := 0 to High(Info.FolderNodes) do
Writeln(Format('folder %d "%s" parent %d',
[Info.FolderNodes[I].ID, Info.FolderNodes[I].Name,
Info.FolderNodes[I].ParentID]));
Keys := Pdf.GetLoadedEmbeddedFileNames;
for I := 0 to High(Keys) do
begin
if Info.HasFolderTree then
FolderID := FolderOfKey(Keys[I], Info.RootFolderID, BareName)
else
begin
FolderID := -1; // klasör yok: düz bir liste
BareName := Keys[I];
end;
Line := Format('[%d] %s:', [FolderID, BareName]);
Fields := Pdf.GetLoadedCollectionItemFields(Keys[I]);
for J := 0 to High(Fields) do
if Fields[J].IsNumeric then
Line := Line + Format(' %s=%s%g',
[Fields[J].Key, Fields[J].Prefix, Fields[J].NumericValue])
else
Line := Line + Format(' %s=%s%s',
[Fields[J].Key, Fields[J].Prefix, Fields[J].StringValue]);
Writeln(Line);
end;
finally
Pdf.Free;
end;
end;
Teslim dosyası için beklenen çıktı: ID 0'da kök ve parent 2 altında Q4 invoices olmak üzere dört klasör satırı, ardından sayısal işaretli [3] invoice-1031.pdf: amount=USD 10400 satırı. Aynı döngüyü teklif paketi üzerinde koşturun ve her amount girdisi IsNumeric setli geri gelir; yalnızca StringValue içinde gelen bir tutar, 104,250'yi 9,800'ün altına yazacak olandır. Bu denetim, Delphi ve C++Builder için HotPDF component ile paket kuran kodun yanına birkaç satırla sığar