Salta ai contenuti

Incorporare file e creare portfolio PDF

Questa ricetta mostra come allegare uno o più file a un PDF e, quando gli allegati sono più di uno, come organizzarli in un portfolio PDF. È utile quando un documento deve portare con sé le proprie evidenze di supporto nello stesso file: una fattura distribuita con il relativo foglio ore, una scheda tecnica di prodotto che include un export Computer-Aided Design (CAD) oppure un record d’archivio che conserva il foglio di calcolo di origine accanto al report renderizzato.

NextPDF espone due punti di accesso sull’oggetto documento. embedFile() legge un file dal disco; embedFileFromString() incorpora byte in memoria generati a runtime. Entrambi registrano l’allegato. Durante save(), il motore scrive ciascun allegato come flusso di file incorporato, lo racchiude in un dizionario di specifica del file e collega ogni specifica all’albero dei nomi EmbeddedFiles a livello di documento. ISO 32000-2 definisce tale albero dei nomi come il punto in cui i flussi di file incorporati vengono associati al documento nel suo complesso tramite il dizionario dei nomi.

È una funzionalità Core senza limitazioni commerciali. L’Application Programming Interface (API) degli allegati è stabile dalla versione 1.0.0 e funziona sull’intera matrice di backport 8.1-8.4.

Terminal window
composer require nextpdf/core:^3

Non è richiesta alcuna estensione opzionale.

Un allegato passa attraverso tre strutture PDF. Conoscerle aiuta a leggere l’output e a eseguire il debug di un file non conforme.

  1. Flusso di file incorporato. I byte grezzi del file allegato, compressi con Flate, vengono scritti come oggetto flusso con /Type pari a /EmbeddedFile. NextPDF registra la dimensione originale, un checksum MD5 e la data di modifica nel dizionario dei parametri del flusso. Il tipo Multipurpose Internet Mail Extensions (MIME) rilevato viene codificato come /Subtype del flusso.
  2. Dizionario di specifica del file. Il wrapper dei metadati. Contiene il nome file visualizzato (/F e l’Unicode /UF), una descrizione leggibile (/Desc), un riferimento al flusso incorporato (/EF) e la relazione tra il file e il documento ospite (/AFRelationship).
  3. Albero dei nomi EmbeddedFiles. Un singolo indice a livello di documento che mappa il nome di ciascun allegato alla relativa specifica del file. ISO 32000-2 richiede che ogni specifica del file raggiunta attraverso questo albero contenga una voce EF il cui valore riferisce un flusso di file incorporato. NextPDF costruisce e bilancia automaticamente questo albero durante save().

Il valore della relazione è importante per la conformità. La PDF Association Application Note 0002 stabilisce che un file associato richiede una voce AFRelationship scelta dall’insieme fisso di PDF 2.0: Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema oppure Unspecified. NextPDF modella tale insieme come enum AFRelationship e rifiuta qualsiasi altro valore. Scegliere il termine che descrive il motivo per cui il file è presente: un foglio ore a supporto di una fattura è Source; un set di dati leggibile dalla macchina a supporto di un grafico è Data.

Un portfolio PDF (chiamato collection in ISO 32000-2) è il livello successivo. Quando un documento contiene più allegati, il dizionario Collection del catalogo indica al reader come presentarli: una tabella di dettagli ordinabile, un layout a riquadri o una busta nascosta. ISO 32000-2 descrive il dizionario Collection come il controllo usato da un processore PDF per presentare gli allegati come portfolio organizzato. NextPDF lo modella come value object CollectionDictionary, con CollectionSort per l’ordine delle colonne di una vista a dettagli.

I metodi a livello di documento (dal concern HasFileAttachments su \NextPDF\Core\Document):

  • embedFile(string $path, string $description = ''): static — legge un file da $path e lo allega. Il tipo MIME viene rilevato dall’estensione; per impostazione predefinita, la relazione assume il valore Unspecified. Legge fino a 100 MB; usare embedFileFromString() per payload più grandi. Restituisce il documento per il concatenamento.
  • embedFileFromString(string $data, string $filename, string $description = '', string $afRelationship = '/Unspecified'): static — allega byte in memoria con il nome visualizzato $filename. Passare un valore letterale AFRelationship (con o senza la barra iniziale) per impostare la relazione. Restituisce il documento per il concatenamento.

I tipi di supporto (namespace \NextPDF\Navigation e \NextPDF\Document):

  • \NextPDF\Navigation\AFRelationship — l’enum degli otto valori di relazione validi. AFRelationship::coerce() normalizza una stringa o un caso enum e genera un’eccezione per un valore sconosciuto. toPdfName() emette il valore letterale /Name.
  • \NextPDF\Document\CollectionDictionary — costruisce il dizionario Collection del catalogo. Le costanti VIEW_DETAILS, VIEW_TILE, VIEW_HIDDEN, VIEW_CUSTOM e VIEW_NONE selezionano la modalità di presentazione; il costruttore accetta inoltre un nome di documento iniziale e un ordinamento facoltativo.
  • \NextPDF\Document\CollectionSort — il value object di ordinamento delle colonne per un portfolio in vista a dettagli.

Questo esempio minimale allega a una pagina di fattura un set di dati comma-separated values (CSV) generato, dichiarandolo come dati Source a partire dai quali è stata costruita la fattura.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Navigation\AFRelationship;
$doc = Document::createStandalone();
$doc->addPage();
$doc->setFont('helvetica', 'B', 18);
$doc->cell(0, 12, 'Invoice INV-2026-0042', newLine: true);
// Attach the line-item dataset the invoice was rendered from.
$csv = "sku,qty,unit_price\nA-100,3,49.00\nB-220,1,180.00\n";
$doc->embedFileFromString(
data: $csv,
filename: 'line-items.csv',
description: 'Source line items for INV-2026-0042',
afRelationship: AFRelationship::Source->value,
);
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/invoice-with-attachment.pdf');

Il reader mostra line-items.csv nel pannello degli allegati e la relazione lo contrassegna come l’origine da cui deriva la fattura.

Questo esempio completo allega un file dal disco e un set di dati in memoria, convalida il percorso su disco rispetto a una directory di base inclusa in una allowlist prima della lettura e costruisce un portfolio ordinabile per gli allegati. Cattura le eccezioni NextPDF più specifiche che il percorso dell’allegato può sollevare, quindi restituisce un codice di uscita definito anziché ignorare l’errore.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Document\CollectionDictionary;
use NextPDF\Document\CollectionSort;
use NextPDF\Exception\CompressionException;
use NextPDF\Exception\InvalidConfigException;
use NextPDF\Exception\PageLayoutException;
use NextPDF\Navigation\AFRelationship;
/**
* Resolve a caller-supplied filename against an allowed base directory.
*
* Rejects path traversal and stream wrappers so an embedded attachment can
* never read outside the directory the application owns. Returns the
* canonical absolute path, or null when the input escapes the base.
*
* @param non-empty-string $baseDir Absolute path to the allowed directory.
* @param non-empty-string $userName Untrusted filename from the request.
*/
function resolveWithinBase(string $baseDir, string $userName): ?string
{
$base = \realpath($baseDir);
if ($base === false) {
return null;
}
$candidate = \realpath($base . \DIRECTORY_SEPARATOR . \basename($userName));
if ($candidate === false || !\str_starts_with($candidate, $base . \DIRECTORY_SEPARATOR)) {
return null;
}
return $candidate;
}
$attachmentsDir = __DIR__ . '/attachments';
$requestedFile = 'timesheet-2026-05.pdf';
$safePath = resolveWithinBase($attachmentsDir, $requestedFile);
if ($safePath === null) {
\fwrite(\STDERR, "Rejected attachment path: outside the allowed directory\n");
exit(2);
}
try {
$doc = Document::createStandalone();
$doc->setTitle('Invoice INV-2026-0042 with supporting documents');
$doc->addPage();
$doc->setFont('helvetica', 'B', 18);
$doc->cell(0, 12, 'Invoice INV-2026-0042', newLine: true);
// 1. A validated file from disk: the supporting timesheet.
$doc->embedFile(
$safePath,
'Timesheet supporting the billed hours',
);
// 2. An in-memory dataset generated at runtime.
$lineItems = "sku,qty,unit_price\nA-100,3,49.00\nB-220,1,180.00\n";
$doc->embedFileFromString(
data: $lineItems,
filename: 'line-items.csv',
description: 'Machine-readable line items',
afRelationship: AFRelationship::Data->value,
);
// Present both attachments as a sortable details portfolio. The sort
// keys reference columns declared in the portfolio /Schema; here the
// built-in filename and modification-date fields order the view.
$portfolio = new CollectionDictionary(
view: CollectionDictionary::VIEW_DETAILS,
initialDocument: 'line-items.csv',
sort: new CollectionSort(
keys: ['_Filename', '_ModDate'],
ascending: [true, false],
),
);
// $portfolio->toPdfDictionary() yields the catalog /Collection literal,
// shared with the unencrypted-wrapper envelope path.
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/invoice-portfolio.pdf';
$doc->save($out);
echo "Wrote {$out} with 2 attachments and a details portfolio\n";
} catch (PageLayoutException $e) {
// Unreadable path, oversized file, null byte, or a MIME-type name that
// exceeds the 127-byte PDF name limit.
\fwrite(\STDERR, "Attachment rejected: {$e->getMessage()}\n");
exit(1);
} catch (CompressionException | InvalidConfigException $e) {
// The attachment data could not be compressed, or a config value was invalid.
\fwrite(\STDERR, "Write failed: {$e->getMessage()}\n");
exit(1);
}

CollectionDictionary e CollectionSort sono value object. Convalidano i propri input alla costruzione e si serializzano nel valore letterale /Collection del catalogo, che pilota la vista del portfolio nel reader.

  • L’input del percorso è una tua responsabilità. embedFile() si protegge dai byte null e dagli stream wrapper e risolve il percorso reale, ma non impone una allowlist per la directory di base. Quando il percorso proviene da una richiesta, convalidarlo prima, come fa l’esempio di produzione con resolveWithinBase().
  • Il limite massimo di 100 MB si applica solo a embedFile(). Un file superiore a 104,857,600 byte solleva PageLayoutException. Per payload più grandi, gestire autonomamente i byte come flusso e passarli a embedFileFromString().
  • I nomi MIME lunghi vengono rifiutati. Il tipo MIME rilevato diventa il /Subtype del flusso incorporato, un token di nome PDF limitato a 127 byte da ISO 32000-2. Un tipo insolitamente lungo (alcuni formati Office si avvicinano ai 90 byte) resta ben al di sotto del limite, ma un tipo fornito manualmente che lo supera solleva PageLayoutException. Lasciare che il motore rilevi il tipo dall’estensione, a meno che non vi sia una ragione specifica per sovrascriverlo.
  • Una relazione sconosciuta genera un’eccezione. AFRelationship::coerce() rifiuta qualsiasi valore esterno all’insieme fisso anziché ripiegare su Unspecified. Passare un caso enum (AFRelationship::Source->value) per impedire che un refuso arrivi al runtime.
  • I nomi file devono essere distinti nell’albero dei nomi. Due allegati con lo stesso nome visualizzato collidono nell’indice EmbeddedFiles. Assegnare a ciascun allegato un nome file univoco.
  • _ModDate viene registrata in Coordinated Universal Time (UTC). embedFile() legge l’ora di modifica del file e la scrive con gmdate(), in modo che la stessa fixture produca una data identica byte per byte su macchine diverse, indipendentemente dall’impostazione del fuso orario.

Ogni allegato viene compresso una sola volta con gzcompress() al livello 9 e scritto come singolo flusso durante save(). La compressione domina il costo e scala con la dimensione del payload allegato, non con il contenuto della pagina. Una manciata di piccoli file di supporto (set di dati, fogli di calcolo, un PDF di foglio ore) rimane entro il budget di 2000 ms / 64 MB. Con molti allegati di grandi dimensioni, i byte incorporati costituiscono il limite minimo di memoria: un allegato da 50 MB conservato come stringa occupa almeno tale quantità prima della compressione. Preferire embedFileFromString() con generazione a blocchi rispetto al caricamento simultaneo di più file di grandi dimensioni.

L’albero dei nomi viene costruito una sola volta durante save(). Fino a 64 voci rimangono in un albero piatto a radice singola. Oltre tale soglia, NextPDF partiziona l’albero in intervalli bilanciati Kids e Limits, in modo che il costo dell’indice rimanga logaritmico anche per insiemi di allegati di grandi dimensioni.

  • Convalidare ogni percorso non attendibile rispetto a una allowlist. L’incorporamento legge qualsiasi file raggiungibile dal processo PHP. Senza un controllo sulla directory di base, un nome file artefatto trasforma l’allegato in una Local File Inclusion (LFI). L’esempio di produzione mostra la protezione tramite allowlist; applicarla ogni volta che il nome file non è una costante in fase di compilazione.
  • Trattare i byte allegati come non attendibili sul lato consumatore. Un file incorporato è opaco per NextPDF. Il motore non lo analizza né lo esegue. Il rischio emerge nel punto in cui il file viene aperto successivamente. Impostare la relazione e la descrizione affinché un consumatore a valle sappia che cosa sia ciascun allegato prima di estrarlo.
  • Nessun segreto negli allegati o nelle descrizioni. Il nome file, la descrizione e i byte vengono memorizzati in chiaro a meno che l’intero documento non sia cifrato. Per proteggere un allegato, cifrare il documento con un criterio di autorizzazioni (vedere la ricetta correlata). Non incorporare credenziali, chiavi o dati personali che non si collocherebbero nella pagina renderizzata.
  • In questa ricetta non si verifica alcun accesso di rete. Ogni byte viene letto dal percorso locale convalidato o fornito in memoria.
AffermazioneSpecClausolareference_id
I flussi di file incorporati vengono associati al documento attraverso la voce EmbeddedFiles nel dizionario dei nomi.ISO 32000-27.11.4
L’albero dei nomi EmbeddedFiles mappa i nomi alle specifiche del file la cui voce EF riferisce un flusso di file incorporato.ISO 32000-27.7.4
Un file associato richiede un valore AFRelationship dall’insieme fisso di PDF 2.0.PDF Association AN0023
Il dizionario Collection del catalogo controlla la presentazione a portfolio degli allegati.ISO 32000-27.11.6

Profilo di riproducibilità — strutturale. Il /ID del trailer, gli atomi di data per ogni salvataggio e il /ModDate del flusso incorporato variano tra le esecuzioni; per questo un confronto strutturale li rimuove prima di confrontare il grafo degli oggetti. Questa ricetta descrive come NextPDF produce la struttura. Non asserisce una conformità PDF/A-4f generalizzata, che dipende dal documento completo. Per un profilo d’archivio che richiede a ogni allegato di dichiarare una relazione e una descrizione, vedere la ricetta PDF/A-4.