Osadzanie plików i tworzenie portfolio PDF
W skrócie
Dział zatytułowany „W skrócie”Ten przepis dołącza jeden lub więcej plików do pliku PDF, a gdy załączników jest kilka, porządkuje je jako portfolio PDF. Użyj go, gdy dokument musi zawierać materiały potwierdzające w tym samym pliku: faktura wraz z bazową ewidencją czasu pracy, karta katalogowa produktu z eksportem CAD lub zapis archiwalny, który przechowuje źródłowy arkusz kalkulacyjny obok wyrenderowanego raportu.
NextPDF udostępnia dwa punkty wejścia na obiekcie dokumentu. embedFile() odczytuje plik z dysku; embedFileFromString() osadza bajty z pamięci generowane w czasie wykonywania. Obie metody rejestrują załącznik. Podczas save() silnik zapisuje każdy załącznik jako osadzony strumień pliku, opakowuje go w słownik specyfikacji pliku i łączy każdą specyfikację z drzewem nazw EmbeddedFiles na poziomie dokumentu. ISO 32000-2 definiuje to drzewo nazw jako miejsce, w którym osadzone strumienie plików są dołączane do dokumentu jako całości za pośrednictwem słownika nazw.
Jest to funkcja edycyjna Core bez ograniczeń komercyjnych. API załączników jest stabilne od wersji 1.0.0 i działa w całej macierzy backportów 8.1–8.4.
Instalacja
Dział zatytułowany „Instalacja”composer require nextpdf/core:^3Nie jest wymagane żadne opcjonalne rozszerzenie.
Przegląd koncepcyjny
Dział zatytułowany „Przegląd koncepcyjny”Załącznik jest reprezentowany przez trzy struktury PDF. Ich znajomość pomaga sprawdzić wynik i diagnozować niezgodny plik.
- Osadzony strumień pliku. Surowe bajty dołączonego pliku, skompresowane algorytmem Flate i zapisane jako obiekt strumienia z
/Typeustawionym na/EmbeddedFile. NextPDF zapisuje oryginalny rozmiar, sumę kontrolną MD5 oraz datę modyfikacji w słowniku parametrów strumienia. Wykryty typ MIME (Multipurpose Internet Mail Extensions) koduje jako/Subtypestrumienia. - Słownik specyfikacji pliku. Opakowanie metadanych. Przechowuje wyświetlaną nazwę pliku (
/Foraz unikodowy/UF), czytelny dla człowieka opis (/Desc), odwołanie do osadzonego strumienia (/EF) oraz relację pliku względem dokumentu nadrzędnego (/AFRelationship). - Drzewo nazw
EmbeddedFiles. Pojedynczy indeks na poziomie dokumentu, który mapuje nazwę każdego załącznika na jego specyfikację pliku. ISO 32000-2 wymaga, aby każda specyfikacja pliku dostępna przez to drzewo zawierała wpisEF, którego wartość odwołuje się do osadzonego strumienia pliku. NextPDF buduje i równoważy to drzewo podczassave().
Wartość relacji ma znaczenie dla zgodności. Nota aplikacyjna PDF Association 0002 stanowi, że plik powiązany wymaga wpisu AFRelationship wybranego ze stałego zbioru PDF 2.0: Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema lub Unspecified. NextPDF modeluje ten zbiór jako enum AFRelationship i odrzuca każdą inną wartość. Wybierz termin, który wyjaśnia, dlaczego plik jest obecny: ewidencja czasu pracy będąca podstawą faktury to Source; zbiór danych odczytywalny maszynowo będący podstawą wykresu to Data.
Portfolio PDF (nazywane kolekcją w ISO 32000-2) to kolejna warstwa. Gdy dokument zawiera kilka załączników, słownik Collection w katalogu informuje czytnik, jak je zaprezentować: jako sortowalną tabelę szczegółów, układ kafelkowy albo ukrytą kopertę. ISO 32000-2 opisuje słownik Collection jako element sterujący, którego procesor PDF używa do prezentowania załączników plików jako uporządkowanego portfolio. NextPDF modeluje go jako obiekt wartości CollectionDictionary, z CollectionSort dla kolejności kolumn w widoku szczegółów.
Interfejs API
Dział zatytułowany „Interfejs API”Metody na poziomie dokumentu pochodzą z traitu HasFileAttachments w \NextPDF\Core\Document:
embedFile(string $path, string $description = ''): static— odczytuje plik z$pathi dołącza go. NextPDF wykrywa typ MIME na podstawie rozszerzenia; relacja domyślnie przyjmuje wartośćUnspecified. Obsługuje odczyt plików do 100 MB; w przypadku większych ładunków użyjembedFileFromString(). Zwraca dokument do łańcuchowania wywołań.embedFileFromString(string $data, string $filename, string $description = '', string $afRelationship = '/Unspecified'): static— dołącza bajty z pamięci pod wyświetlaną nazwą$filename. Przekaż literałAFRelationship(z początkowym ukośnikiem lub bez niego), aby ustawić relację. Zwraca dokument do łańcuchowania wywołań.
Typy pomocnicze znajdują się w przestrzeniach nazw \NextPDF\Navigation i \NextPDF\Document:
\NextPDF\Navigation\AFRelationship— enum dla ośmiu prawidłowych wartości relacji.AFRelationship::coerce()normalizuje ciąg znaków lub wariant enuma i zgłasza wyjątek przy nieznanej wartości.toPdfName()emituje literał/Name.\NextPDF\Document\CollectionDictionary— buduje słownikCollectionw katalogu. StałeVIEW_DETAILS,VIEW_TILE,VIEW_HIDDEN,VIEW_CUSTOMiVIEW_NONEwybierają tryb prezentacji; konstruktor przyjmuje także początkową nazwę dokumentu oraz opcjonalne sortowanie.\NextPDF\Document\CollectionSort— obiekt wartości porządkujący kolumny w widoku szczegółów portfolio.
Przykład kodu — szybki start
Dział zatytułowany „Przykład kodu — szybki start”Ten minimalny przykład dołącza do strony faktury wygenerowany zbiór danych w formacie wartości rozdzielanych przecinkami (CSV) i deklaruje go jako dane Source, na podstawie których zbudowano fakturę.
<?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');Czytnik pokazuje line-items.csv w panelu załączników, a relacja oznacza plik jako źródło faktury.
Przykład kodu — produkcyjny
Dział zatytułowany „Przykład kodu — produkcyjny”Ten kompletny przykład dołącza plik z dysku oraz zbiór danych z pamięci, sprawdza ścieżkę na dysku względem dozwolonego katalogu bazowego przed odczytem i buduje sortowalne portfolio załączników. Przechwytuje najbardziej szczegółowe wyjątki NextPDF, które mogą wystąpić podczas obsługi załącznika, a następnie zwraca zdefiniowany kod wyjścia zamiast ukrywać niepowodzenie.
<?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 i CollectionSort to obiekty wartości. Walidują dane wejściowe podczas konstrukcji i serializują się do literału /Collection w katalogu, który steruje widokiem portfolio w czytniku.
Przypadki brzegowe i pułapki
Dział zatytułowany „Przypadki brzegowe i pułapki”- Za ścieżkę wejściową odpowiada aplikacja.
embedFile()zabezpiecza przed bajtami null i wrapperami strumieni oraz rozwiązuje rzeczywistą ścieżkę, ale nie wymusza ograniczenia do listy dozwolonych katalogów bazowych. Gdy ścieżka pochodzi z żądania, najpierw ją zweryfikuj, tak jak robi to przykład produkcyjny za pomocąresolveWithinBase(). - Limit 100 MB dotyczy wyłącznie
embedFile(). Plik większy niż104,857,600bajtów zgłaszaPageLayoutException. W przypadku większych ładunków przesyłaj bajty strumieniowo samodzielnie i przekaż je doembedFileFromString(). - Długie nazwy typów MIME są odrzucane. Wykryty typ MIME staje się
/Subtypeosadzonego strumienia, tokenem nazwy PDF ograniczonym do 127 bajtów przez ISO 32000-2. Nietypowo długi typ MIME (niektóre formaty Office zbliżają się do 90 bajtów) pozostaje dobrze poniżej limitu, ale podany ręcznie typ, który go przekracza, zgłaszaPageLayoutException. Pozwól silnikowi wykryć typ na podstawie rozszerzenia, chyba że masz konkretny powód, aby go nadpisać. - Nieznana relacja zgłasza wyjątek.
AFRelationship::coerce()odrzuca każdą wartość spoza stałego zbioru, zamiast sprowadzać ją doUnspecified. Przekaż wartość wariantu enuma (AFRelationship::Source->value), aby literówka nie trafiła do środowiska wykonawczego. - Nazwy plików muszą być unikalne w drzewie nazw. Dwa załączniki o tej samej wyświetlanej nazwie kolidują w indeksie
EmbeddedFiles. Nadaj każdemu załącznikowi unikalną nazwę pliku. _ModDatejest zapisywany w uniwersalnym czasie koordynowanym (UTC).embedFile()odczytuje czas modyfikacji pliku i zapisuje go za pomocągmdate(), dzięki czemu ten sam zestaw testowy daje bajtowo identyczną datę na różnych maszynach, niezależnie od ustawienia strefy czasowej.
Wydajność
Dział zatytułowany „Wydajność”Każdy załącznik jest kompresowany jednokrotnie za pomocą gzcompress() na poziomie 9 i zapisywany jako pojedynczy strumień podczas save(). Kompresja dominuje w koszcie i skaluje się wraz z rozmiarem dołączonego ładunku, a nie z zawartością strony. Kilka małych plików pomocniczych (zbiory danych, arkusze kalkulacyjne, plik PDF z ewidencją czasu pracy) mieści się w budżecie 2000 ms / 64 MB. W przypadku wielu dużych załączników osadzone bajty wyznaczają dolną granicę zużycia pamięci: załącznik o rozmiarze 50 MB przechowywany jako ciąg znaków zajmuje co najmniej tyle przed kompresją. Preferuj embedFileFromString() z generowaniem w porcjach zamiast ładowania kilku dużych plików naraz.
Drzewo nazw jest budowane jednokrotnie podczas save(). Przy maksymalnie 64 wpisach pozostaje płaskim drzewem z jednym korzeniem. Powyżej tego progu NextPDF dzieli drzewo na zrównoważone zakresy Kids i Limits, dzięki czemu koszt indeksowania pozostaje logarytmiczny dla dużych zbiorów załączników.
Uwagi dotyczące bezpieczeństwa
Dział zatytułowany „Uwagi dotyczące bezpieczeństwa”- Weryfikuj każdą niezaufaną ścieżkę względem listy dozwolonych katalogów. Osadzanie odczytuje dowolny plik, do którego ma dostęp proces PHP. Bez sprawdzenia katalogu bazowego spreparowana nazwa pliku zmienia mechanizm załączników w lokalne dołączanie plików (LFI). Przykład produkcyjny pokazuje kontrolę względem listy dozwolonych; zastosuj ją zawsze, gdy nazwa pliku nie jest stałą czasu kompilacji.
- Traktuj dołączone bajty jako niezaufane po stronie odbiorcy. Osadzony plik jest dla NextPDF nieprzejrzysty. Silnik nie parsuje go ani nie wykonuje. Ryzyko pojawia się tam, gdzie plik jest później otwierany. Ustaw relację i opis, aby kolejny odbiorca wiedział, czym jest każdy załącznik, zanim go wyodrębni.
- Nie umieszczaj sekretów w załącznikach ani opisach. Nazwa pliku, opis i bajty są przechowywane jawnie, chyba że cały dokument jest zaszyfrowany. Aby chronić załącznik, zaszyfruj dokument z polityką uprawnień (zobacz powiązany przepis). Nie osadzaj poświadczeń, kluczy ani danych osobowych, których nie należy umieszczać na wyrenderowanej stronie.
- W tym przepisie nie występuje żaden dostęp do sieci. Każdy bajt jest odczytywany ze zweryfikowanej ścieżki lokalnej lub dostarczany w pamięci.
Zgodność
Dział zatytułowany „Zgodność”| Stwierdzenie | Specyfikacja | Klauzula | reference_id |
|---|---|---|---|
Osadzone strumienie plików są dołączane do dokumentu poprzez wpis EmbeddedFiles w słowniku nazw. | ISO 32000-2 | 7.11.4 | |
Drzewo nazw EmbeddedFiles mapuje nazwy na specyfikacje plików, których wpis EF odwołuje się do osadzonego strumienia pliku. | ISO 32000-2 | 7.7.4 | |
Plik powiązany wymaga wartości AFRelationship ze stałego zbioru PDF 2.0. | PDF Association AN002 | 3 | |
Słownik Collection w katalogu steruje prezentacją załączników w portfolio. | ISO 32000-2 | 7.11.6 |
Profil powtarzalności — strukturalny. /ID w trailerze, wartości daty z każdego zapisu oraz /ModDate osadzonego strumienia różnią się między uruchomieniami, dlatego porównanie strukturalne usuwa te wartości przed porównaniem grafu obiektów. Ten przepis opisuje, jak NextPDF tworzy tę strukturę. Nie potwierdza pełnej zgodności z PDF/A-4f, która zależy od całego dokumentu. Profil archiwalny wymagający, aby każdy załącznik deklarował relację i opis, znajdziesz w przepisie PDF/A-4.