Bestanden insluiten en PDF-portfolio's maken
In het kort
Sectie met titel “In het kort”Dit recipe voegt een of meer bestanden toe aan een PDF en ordent ze bij meerdere bijlagen als een PDF-portfolio. Gebruik het wanneer een document ondersteunend bewijs in hetzelfde bestand moet meedragen: een factuur met de bijbehorende urenstaat, een productspecificatieblad met een Computer-Aided Design (CAD)-export of een archiefrecord dat het bronwerkblad naast het weergegeven rapport bewaart.
NextPDF biedt twee toegangspunten op het documentobject. embedFile() leest een bestand van schijf; embedFileFromString() sluit bytes in die u tijdens runtime in het geheugen genereert. Beide methoden registreren de bijlage. Bij save() schrijft de engine elke bijlage als ingesloten bestandsstream, verpakt die in een file specification dictionary en koppelt elke specificatie aan de EmbeddedFiles-naamboom op documentniveau. ISO 32000-2 definieert die naamboom als de plek waar ingesloten bestandsstreams via de name dictionary aan het document als geheel worden gekoppeld.
Dit is een Core-functie zonder commerciële drempel. De Application Programming Interface (API) voor bijlagen is stabiel sinds 1.0.0 en werkt op de volledige 8.1-8.4-backportmatrix.
Installeren
Sectie met titel “Installeren”composer require nextpdf/core:^3Er is geen optionele extensie vereist.
Conceptueel overzicht
Sectie met titel “Conceptueel overzicht”Een bijlage doorloopt drie PDF-structuren. Met die kennis kunt u de uitvoer inspecteren en een niet-conform bestand debuggen.
- Ingesloten bestandsstream. De ruwe bytes van het bijgevoegde bestand, met Flate gecomprimeerd en geschreven als een streamobject waarvan de
/Type/EmbeddedFileis. NextPDF registreert de oorspronkelijke grootte, een MD5-checksum en de wijzigingsdatum in de parameter dictionary van de stream. Het codeert het gedetecteerde Multipurpose Internet Mail Extensions (MIME)-type als de stream-/Subtype. - File specification dictionary. De metadata-wrapper. Deze bevat de weergegeven bestandsnaam (
/Fen de Unicode-/UF), een voor mensen leesbare beschrijving (/Desc), een verwijzing naar de ingesloten stream (/EF) en de relatie van het bestand tot het hostdocument (/AFRelationship). EmbeddedFiles-naamboom. Eén index op documentniveau die de naam van elke bijlage toewijst aan de bijbehorende file specification. ISO 32000-2 vereist dat elke file specification die via deze boom wordt bereikt eenEF-vermelding bevat waarvan de waarde naar een ingesloten bestandsstream verwijst. NextPDF bouwt en balanceert deze boom voor u bijsave().
De relatiewaarde is belangrijk voor conformiteit. PDF Association Application Note 0002 stelt dat een geassocieerd bestand een AFRelationship-vermelding vereist die uit de vaste PDF 2.0-set komt: Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema of Unspecified. NextPDF modelleert die set als de AFRelationship-enum en weigert elke andere waarde. Kies de term die verklaart waarom het bestand aanwezig is: een urenstaat achter een factuur is Source; een machineleesbare dataset achter een grafiek is Data.
Een PDF-portfolio (in ISO 32000-2 een collectie genoemd) is de laag daarboven. Wanneer een document meerdere bijlagen meedraagt, vertelt de Collection-dictionary in de catalogus de reader hoe die ze moet presenteren: een sorteerbare detailtabel, een tegel-lay-out of een verborgen envelop. ISO 32000-2 beschrijft de Collection-dictionary als het besturingselement dat een PDF-processor gebruikt om bestandsbijlagen als een georganiseerd portfolio te presenteren. NextPDF modelleert dit als het CollectionDictionary-waardeobject, met CollectionSort voor de kolomvolgorde in een detailweergave.
API-oppervlak
Sectie met titel “API-oppervlak”De methoden op documentniveau zijn afkomstig van de HasFileAttachments-concern op \NextPDF\Core\Document:
embedFile(string $path, string $description = ''): static— leest een bestand uit$pathen voegt het toe. NextPDF detecteert het MIME-type op basis van de extensie; de relatie heeft standaard de waardeUnspecified. Leest bestanden tot 100 MB; gebruikembedFileFromString()voor grotere payloads. Retourneert het document voor chaining.embedFileFromString(string $data, string $filename, string $description = '', string $afRelationship = '/Unspecified'): static— voegt bytes uit het geheugen toe onder de weergegeven naam$filename. Geef eenAFRelationship-literal door (met of zonder voorafgaande slash) om de relatie in te stellen. Retourneert het document voor chaining.
De ondersteunende typen bevinden zich in de namespaces \NextPDF\Navigation en \NextPDF\Document:
\NextPDF\Navigation\AFRelationship— de enum voor de acht geldige relatiewaarden.AFRelationship::coerce()normaliseert een string of enum-case en werpt een exception bij een onbekende waarde.toPdfName()geeft het/Name-literal uit.\NextPDF\Document\CollectionDictionary— bouwt deCollection-dictionary in de catalogus. De constantenVIEW_DETAILS,VIEW_TILE,VIEW_HIDDEN,VIEW_CUSTOMenVIEW_NONEselecteren de presentatiemodus; de constructor accepteert ook een initiële documentnaam en een optionele sortering.\NextPDF\Document\CollectionSort— het waardeobject voor de kolomvolgorde van een portfolio in detailweergave.
Codevoorbeeld — snelstart
Sectie met titel “Codevoorbeeld — snelstart”Dit minimale voorbeeld voegt een gegenereerde comma-separated values (CSV)-dataset toe aan een factuurpagina en declareert deze als de Source-gegevens waaruit de factuur is opgebouwd.
<?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');De reader toont line-items.csv in het bijlagenpaneel, en de relatie markeert het als de bron van de factuur.
Codevoorbeeld — productie
Sectie met titel “Codevoorbeeld — productie”Dit volledige voorbeeld voegt een bestand van schijf en een dataset uit het geheugen toe, valideert het pad op schijf tegen een toegestane basismap voordat het wordt gelezen en bouwt een sorteerbaar portfolio voor de bijlagen. Het vangt de meest specifieke NextPDF-exceptions op die rond het bijlagepad kunnen optreden en retourneert vervolgens een gedefinieerde exitcode in plaats van de fout te onderdrukken.
<?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 en CollectionSort zijn waardeobjecten. Ze valideren hun invoer bij constructie en serialiseren naar het /Collection-literal in de catalogus dat de portfolioweergave in de reader aanstuurt.
Randgevallen en valkuilen
Sectie met titel “Randgevallen en valkuilen”- Padinvoer is uw verantwoordelijkheid.
embedFile()beschermt tegen null-bytes en stream-wrappers en bepaalt het werkelijke pad, maar dwingt geen allowlist op basismapniveau af. Wanneer het pad uit een verzoek afkomstig is, valideert u het eerst, zoals het productievoorbeeld doet metresolveWithinBase(). - Het plafond van 100 MB geldt alleen voor
embedFile(). Een bestand groter dan104,857,600bytes werptPageLayoutException. Voor grotere payloads streamt u de bytes zelf en geeft u ze door aanembedFileFromString(). - Lange MIME-typenamen worden geweigerd. Het gedetecteerde MIME-type wordt de
/Subtypevan de ingesloten stream, een PDF-naamtoken dat door ISO 32000-2 tot 127 bytes is begrensd. Een ongewoon lang type (sommige Office-formaten benaderen 90 bytes) blijft ruim onder de limiet, maar een handmatig opgegeven type dat de limiet overschrijdt, werptPageLayoutException. Laat de engine het type op basis van de extensie detecteren, tenzij u een specifieke reden hebt om het te overschrijven. - Een onbekende relatie werpt een exception.
AFRelationship::coerce()weigert elke waarde buiten de vaste set in plaats van terug te vallen opUnspecified. Geef een enum-case door (AFRelationship::Source->value) om te voorkomen dat een typefout de runtime bereikt. - Bestandsnamen moeten uniek zijn in de naamboom. Twee bijlagen met dezelfde weergavenaam botsen in de
EmbeddedFiles-index. Geef elke bijlage een unieke bestandsnaam. _ModDatewordt vastgelegd in Coordinated Universal Time (UTC).embedFile()leest de wijzigingstijd van het bestand en schrijft deze metgmdate(), zodat dezelfde fixture op alle machines een byte-identieke datum oplevert, ongeacht de tijdzone-instelling.
Prestaties
Sectie met titel “Prestaties”Elke bijlage wordt eenmaal met gzcompress() op niveau 9 gecomprimeerd en bij save() als één stream geschreven. Compressie bepaalt het grootste deel van de kosten en schaalt mee met de grootte van de bijgevoegde payload, niet met de pagina-inhoud. Een handvol kleine ondersteunende bestanden (datasets, spreadsheets, een urenstaat-PDF) blijft binnen het budget van 2000 ms / 64 MB. Bij veel grote bijlagen vormen de ingesloten bytes de ondergrens van het geheugengebruik: een bijlage van 50 MB die als string wordt vastgehouden, neemt vóór compressie minstens zoveel geheugen in beslag. Geef de voorkeur aan embedFileFromString() met gefaseerde generatie boven het in één keer laden van meerdere grote bestanden.
De naamboom wordt eenmaal opgebouwd bij save(). Tot 64 vermeldingen blijven in een platte boom met één wortel. Daarboven verdeelt NextPDF de boom in gebalanceerde Kids- en Limits-bereiken, zodat de indexkosten logaritmisch blijven voor grote sets met bijlagen.
Beveiligingsnotities
Sectie met titel “Beveiligingsnotities”- Valideer elk niet-vertrouwd pad tegen een allowlist. Insluiten leest elk bestand dat het PHP-proces kan bereiken. Zonder controle op de basismap verandert een gemanipuleerde bestandsnaam een bijlage in Local File Inclusion (LFI). Het productievoorbeeld toont de allowlist-bewaking; pas deze toe wanneer de bestandsnaam geen compile-time-constante is.
- Behandel bijgevoegde bytes aan de verwerkende kant als niet-vertrouwd. Een ingesloten bestand is voor NextPDF ondoorzichtig. De engine parseert het niet en voert het niet uit. Het risico bevindt zich waar het bestand later wordt geopend. Stel de relatie en de beschrijving in, zodat een downstream-afnemer weet wat elke bijlage is voordat deze wordt geëxtraheerd.
- Geen geheimen in bijlagen of beschrijvingen. De bestandsnaam, beschrijving en bytes worden onversleuteld opgeslagen, tenzij het hele document is versleuteld. Versleutel het document met een rechtenbeleid om een bijlage te beschermen (zie het gerelateerde recipe). Sluit geen inloggegevens, sleutels of persoonsgegevens in die u ook niet op de weergegeven pagina zou plaatsen.
- In dit recipe vindt geen netwerktoegang plaats. Elke byte wordt gelezen uit het gevalideerde lokale pad of in het geheugen aangeleverd.
Conformiteit
Sectie met titel “Conformiteit”| Bewering | Specificatie | Clausule | reference_id |
|---|---|---|---|
Ingesloten bestandsstreams worden via de EmbeddedFiles-vermelding in de name dictionary aan het document gekoppeld. | ISO 32000-2 | 7.11.4 | |
De EmbeddedFiles-naamboom wijst namen toe aan file specifications waarvan de EF-vermelding naar een ingesloten bestandsstream verwijst. | ISO 32000-2 | 7.7.4 | |
Een geassocieerd bestand vereist een AFRelationship-waarde uit de vaste PDF 2.0-set. | PDF Association AN002 | 3 | |
De Collection-dictionary in de catalogus bestuurt de portfolioweergave van bijlagen. | ISO 32000-2 | 7.11.6 |
Reproduceerbaarheidsprofiel — structureel. De trailer-/ID, de datumwaarden per save en de /ModDate van de ingesloten stream variëren tussen runs, dus een structurele vergelijking verwijdert die waarden voordat de objectgraaf wordt vergeleken. Dit recipe beschrijft hoe NextPDF de structuur produceert. Het claimt geen volledige PDF/A-4f-conformiteit, die van het volledige document afhangt. Zie het PDF/A-4-recipe voor een archiveringsprofiel waarbij elke bijlage een relatie en een beschrijving moet declareren.