Tre offerte entrano in un PDF Portfolio, e la colonna Amount decrescente del viewer colloca l'offerta da 104.250 USD per ultima, sotto quella da 9.800 USD — gli importi sono entrati come testo. I valori di colonna vivono nella voce /CI di ogni file allegato, e SetPortfolioItemNumericMetadata di HotPDF li scrive lì come numeri PDF, così il sort confronta 104250 con 9800 invece che due stringhe
Cosa fa aprire un PDF come Portfolio?
Lo decide una chiave sola del catalogo. ISO 32000-1 §12.3.5 dice che quando è presente un dizionario /Collection, un reader conforme deve presentare il documento come portable collection (il nome della specifica per quello che Acrobat chiama PDF Portfolio), e ogni file nell'albero dei nomi /Names /EmbeddedFiles è un membro; gli allegati fuori da quell'albero non lo sono. Un PDF con dieci allegati e nessun /Collection è solo un PDF con un pannello allegati affollato, che è la forma giusta quando viaggia con il documento un solo file leggibile dalla macchina, come fa l'XML in una fattura elettronica ZUGFeRD o Factur-X. Il dizionario è piccolo e quasi interamente opzionale; i numeri di tabella sono cambiati fra le due edizioni, quindi li elenco entrambi:
| Voce | Cosa controlla | ISO 32000-1 | ISO 32000-2 |
|---|---|---|---|
/View, /D | Vista iniziale (D dettagli, T tile, H nascosta, più C per un navigator nel PDF 2.0) e la chiave nell'albero dei nomi del file mostrato per primo | Tabella 155 | Tabella 153 |
/Schema | Le colonne dell'elenco dei file | Tabelle 156, 157 | Tabelle 154, 155 |
/Sort | Campi e direzioni di ordinamento | Tabella 158 | Tabella 156 |
/Colors | Palette DeviceRGB suggerita per il layout | nessuna | Tabella 157 |
/Split | Direzione e posizione della barra di divisione | nessuna | Tabella 158 |
/Folders | Radice dell'albero delle cartelle | nessuna | Tabella 159 (§12.3.5.2) |
Le pagine del contenitore contano ancora, perché sono la copertina. Quando manca /D, il documento iniziale è il contenitore stesso; ISO 32000-2 aggiunge che un /D che non nomina nessun file esistente porta il reader a selezionare il primo file dell'elenco. Un viewer senza supporto portfolio ignora /Collection e mostra solo quelle pagine, e la NOTE 2 del §12.3.5 chiede lì un titolo e un paragrafo introduttivo; la risposta consueta è una pagina che dice cosa contiene il pacchetto e che serve un viewer capace di portfolio. ConfigurePortfolio(Name, View, InitialDocument) imposta vista e /D in una chiamata sola, e ConfigurePortfolioView passa in seguito fra pvmDetails, pvmTile e pvmHidden. La vista C richiede un dizionario navigator (ISO 32000-2 Tabella 160); per un pacchetto di offerte o un set di consegna, la vista dettagli con un ordinamento sensato è ciò che gli utenti si aspettano
Campi dello schema e i valori dietro ogni colonna
Le colonne vengono dal dizionario schema della collection, un dizionario collection field per ogni chiave che scegliete. Gran parte di un campo è presentazione — /N è l'intestazione, /O la posizione della colonna, /V e /E la visibilità iniziale e la modificabilità, passate a THotPDF.AddPortfolioSchemaField come DisplayName, Order, Visible e Editable. Il /Subtype è la decisione con conseguenze: dice chi fornisce il valore:
/Subtype | Costante HotPDF | Il valore viene da |
|---|---|---|
| S, D, N | cfsText, cfsDate, cfsNumber | Voi, per file, in /CI |
| F, Desc | cfsFileName, cfsDescription | La specifica del file (/UF o /F, /Desc) |
| ModDate, CreationDate, Size | cfsModDate, cfsCreationDate, cfsSize | Il /Params del file incorporato |
| CompressedSize (PDF 2.0) | cfsCompressedSize | La /Length dello stream incorporato |
I valori per file stanno nel dizionario collection item a cui punta la voce /CI della specifica del file (ISO 32000-1 §7.11.6 Tabella 48, Tabella 46 nella parte 2). Ogni voce è una stringa di testo, una data, un numero, oppure un dizionario collection subitem (Tabella 49, o Tabella 47 nella parte 2) il cui /D tiene i dati e il cui /P tiene un prefisso di visualizzazione che l'ordinamento ignora. L'Esempio 2 della specifica mostra perché il prefisso esiste: un oggetto di email conservato come /D (Let's have lunch on Friday!) con /P (Re:), così una risposta ordina accanto al messaggio a cui risponde. Stranamente, entrambe le edizioni dicono che lo schema di quell'esempio è in §12.4.2, Page Labels; intendono l'Esempio 1 subito sopra. Quanto alle colonne legate al file, AddDocumentAttachment timbra le date di /Params col momento dell'incorporamento, quindi una colonna cfsModDate mostra quando è stato costruito il pacchetto. Chi revisiona e ha bisogno della data del file sorgente si prende una colonna D da riempire voi, come received più sotto
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;
// Copertina: tutto ciò che mostrerà un viewer senza supporto portfolio
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); // nascosta all'inizio
for I := 0 to High(Files) do
begin
Pdf.AddDocumentAttachment(Files[I], Trades[I] + ' bid');
Key := AnsiString(ExtractFileName(Files[I])); // la chiave dell'albero dei nomi
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; // l'offerta più grande in cima
SortKeys[1].Key := 'trade';
SortKeys[1].Ascending := True; // per spareggio
Pdf.SetPortfolioSortOrder(SortKeys);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
SetPortfolioItemMetadata e SetPortfolioItemNumericMetadata sollevano eccezioni quando la chiave dello schema non è registrata o nessun file incorporato ha la chiave data, quindi registrate lo schema, allegate il file, poi impostate i valori. Il loro argomento FileName è la chiave dell'albero dei nomi, che per AddDocumentAttachment è il nome del file senza directory. Le chiavi sono stringhe di byte confrontate con distinzione maiuscole/minuscole (§7.9.6), come per le named destinations nell'albero /Dests, quindi Civil.pdf non troverà civil.pdf. Quando due file sorgente condividono il nome di base, HotPDF tiene entrambi e dà al secondo una chiave numerata tipo hvac (2).pdf, che le chiamate di metadati dovranno allora usare
Come finisce un'offerta da 104.250 sotto una da 9.800?
È stato conservato come testo. Le regole di ordinamento del §12.3.5 dipendono dal tipo di dati — i numeri numericamente, le date dalla più vecchia alla più recente, le stringhe di testo lessicalmente, con la NOTE 3 che lascia i dettagli dell'ordine lessicale a ogni reader. La Tabella 48 (Tabella 46 nella parte 2) richiede che ogni voce /CI corrisponda al /Subtype del suo campo, quindi "USD 104,250" scritto come testo in una colonna N viola già la regola. Al reader resta il confronto fra stringhe: dopo il "USD " comune, il carattere 1 viene prima di 9, e un sort discendente mette l'offerta più grande per ultima. SetPortfolioItemMetadata scrive sempre un /D di testo, giusto per i campi S e per le stringhe data. Per i campi N, SetPortfolioItemNumericMetadata(FileName, Key, Value, Prefix, UseSubitemDictionary) scrive un numero PDF e porta il simbolo della valuta in /P, che il sort ignora. Tenete unità e simboli di valuta nel prefisso su ogni colonna numerica: il valore che sembra numerico solo sullo schermo è proprio quello che ordina male. Con prefisso vuoto e UseSubitemDictionary a False, il numero va dritto in /CI
Le date seguono la stessa logica. Un campo D si aspetta una stringa data PDF (§7.9.4) tipo D:20261031143000; il 31/10/2026 preso dalle impostazioni locali di Windows è testo in una colonna data e atterra dove cadono i suoi caratteri. Il sort in sé vive nel dizionario sort della collection: /S è un nome di campo o un array in cui ogni nome successivo spezza i pareggi lasciati dai precedenti, e /A è un booleano o un array parallelo di booleani, con le voci in più ignorate e quelle mancanti lette come true, cioè ascendente. SetPortfolioSortOrder prende un array di record THPDFPortfolioSortKey e scrive la forma a nome singolo per una chiave, quella ad array per più chiavi. Usate le chiavi registrate nello schema; /S può nominare solo campi descritti nel dizionario collection genitore
Le cartelle PDF 2.0 vivono nei nomi dei file
Una cartella non contiene file dentro il PDF. ISO 32000-2 §12.3.5.2 definisce i dizionari cartella come oggetti indiretti collegati in un albero: /Folders punta alla radice, ogni figlio porta /Parent, una cartella raggiunge il suo primo figlio tramite /Child, e i fratelli si incateno tramite /Next. Ogni cartella ha un /Name obbligatorio e un /ID intero non negativo unico nel documento, e l'array /Free della radice elenca gli intervalli di ID ancora inutilizzati. L'appartenenza è codificata nella chiave EmbeddedFiles: una che inizia con <, continua con cifre e chiude con > appartiene alla cartella con quell'ID, e il resto è il nome del file, quindi <3>invoice-1031.pdf è invoice-1031.pdf nella cartella 3. La chiave è la cartella. Qualsiasi chiave fuori dalla convenzione appartiene alla radice, ed è così che fatture destinate a una cartella 2026 finiscono al livello più alto — allegate con AddDocumentAttachment semplice, le loro chiavi non portano alcun prefisso
HotPDF costruisce l'albero con tre chiamate. EnsureCollectionRootFolder(RootName, Description) crea la radice con ID 0, oppure restituisce l'ID della radice esistente. AddCollectionChildFolder(ParentFolderID, FolderName, Description) accoda un figlio, restituisce il suo nuovo ID e tiene /Free aggiornato. AddDocumentAttachmentToCollectionFolder(FolderID, FileName, Description) incorpora il file sotto <FolderID>name e scrive il nome nudo in /F e /UF, che è ciò che mostra una colonna cfsFileName. Ogni chiamata successiva che nomina il file ha bisogno della chiave completa, incluso InitialDocument in ConfigurePortfolio e le chiamate di metadati. Cartelle, /Split e /Colors sono voci del PDF 2.0, quindi impostate Version a pdf20 prima di BeginDoc
procedure BuildHandoverPackage;
var
Pdf: THotPDF;
RootID, Year2025, Year2026, Q4: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.Version := pdf20; // /Folders, /Split e /Colors sono PDF 2.0
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 al 30 % della finestra
Pdf.SetPortfolioColor('Background', 0.96, 0.97, 0.98);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
L'ordine fra fratelli è una raccomandazione dello standard: i fratelli dovrebbero seguire la chiave di sort della collection, o i nomi delle cartelle se non c'è. AddCollectionChildFolder collega i fratelli in ordine di chiamata, quindi createli già ordinati, come fa l'esempio con 2025 prima di 2026. La posizione di /Split (ISO 32000-2 Tabella 158) è una percentuale da 0 a 100, da qui 30 e non 0.3. Le regole sui nomi dopo la Tabella 159 valgono per i nomi delle cartelle e per i file dentro le cartelle, e siccome quei nomi di solito arrivano da utenti o form di upload, filtrateli prima della chiamata; AddCollectionChildFolder in persona rifiuta barre, caratteri di controllo e un nome di fratello che ne ripete uno esistente ignorando il maiuscolo/minuscolo ASCII:
- fra 1 e 255 caratteri, senza NUL incorporati
- nessuno dei caratteri
/ \ : * " < > |da qualsiasi parte nel nome - nessun punto come ultimo carattere
- niente due cartelle fratelli, né due file nella stessa cartella, i cui nomi differiscano solo per maiuscole/minuscole
Leggere un Portfolio scritto da un altro producer
Un reader deve accettare ogni forma legale di /CI, perché i producer differiscono: alcuni avvolgono ogni valore in un dizionario subitem, altri scrivono stringhe e numeri nudi, e l'Esempio 2 della specifica mescola entrambi dentro un solo item. Codice che cerca solo una stringa sotto il /D di un subitem mostra colonne vuote per il secondo gruppo. GetLoadedCollectionItemFields(FileName) restituisce un THPDFCollectionItemFieldValue per voce con IsSubitemDictionary, IsNumeric, NumericValue, StringValue e Prefix compilati. GetLoadedCollectionInfo restituisce schema, chiavi di sort, split, colori e i nodi cartella di GetLoadedCollectionFolderTree in ordine di albero, ciascuno con ID, Name, ParentID, FirstChildID e NextSiblingID. La cartella di ogni file viene dalla sua chiave, e il parse deve seguire la regola alla lettera: solo cifre fra le parentesi angolari. Una chiave tipo <draft>notes.txt, venuta da un producer che di cartelle non sa nulla, non ha cifre lì, quindi il file appartiene alla radice; tagliare tutto fino alla prima > inventerebbe una cartella di nome draft
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: '<' cifre '>' nome file; qualunque altra chiave è radice
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; // niente cartelle: una lista piatta
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;
Atteso per il file di consegna: quattro righe di cartelle con la radice all'ID 0 e Q4 invoices sotto il genitore 2, poi [3] invoice-1031.pdf: amount=USD 10400 riconosciuto come numerico. Fate girare lo stesso ciclo sul pacchetto di offerte e ogni voce amount torna con IsNumeric impostato; un importo che arriva solo in StringValue è proprio quello che ordinerà 104.250 sotto 9.800. Quel controllo sta in poche righe accanto al codice che costruisce pacchetti con il componente HotPDF per Delphi e C++Builder