Incorpore arquivos e crie portfólios PDF
Em resumo
Seção intitulada “Em resumo”Esta receita anexa um ou mais arquivos a um PDF e, quando houver vários anexos, os organiza como um portfólio PDF. Use-a quando um documento precisa carregar evidências de apoio no mesmo arquivo: uma fatura com a planilha de horas que a fundamenta, uma ficha técnica de produto com uma exportação de Computer-Aided Design (CAD) ou um registro de arquivamento que mantém a planilha de origem ao lado do relatório renderizado.
NextPDF oferece dois pontos de entrada no objeto do documento. embedFile() lê um arquivo do disco; embedFileFromString() incorpora bytes em memória gerados em tempo de execução. Ambos registram o anexo. Em save(), o engine grava cada anexo como um stream de arquivo incorporado, encapsula-o em um dicionário de especificação de arquivo e vincula cada especificação à name tree EmbeddedFiles no nível do documento. A ISO 32000-2 define essa name tree como o local em que os streams de arquivo incorporado se associam ao documento como um todo por meio do dicionário de nomes.
Este é um recurso do Core, sem restrição comercial. A Application Programming Interface (API) de anexos está estável desde a versão 1.0.0 e funciona em toda a matriz de backport 8.1-8.4.
Instalação
Seção intitulada “Instalação”composer require nextpdf/core:^3Nenhuma extensão opcional é necessária.
Visão conceitual
Seção intitulada “Visão conceitual”Um anexo passa por três estruturas do PDF. Conhecê-las ajuda você a inspecionar a saída e a depurar um arquivo não conforme.
- Stream de arquivo incorporado. Os bytes brutos do arquivo anexado, comprimidos com Flate e gravados como um objeto stream cujo
/Typeé/EmbeddedFile. NextPDF registra o tamanho original, uma soma de verificação MD5 e a data de modificação no dicionário de parâmetros do stream. O tipo Multipurpose Internet Mail Extensions (MIME) detectado é codificado como o/Subtypedo stream. - Dicionário de especificação de arquivo. É o encapsulador de metadados. Ele carrega o nome de exibição do arquivo (
/Fe o/UFem Unicode), uma descrição legível por humanos (/Desc), uma referência ao stream incorporado (/EF) e a relação do arquivo com o documento hospedeiro (/AFRelationship). - Name tree
EmbeddedFiles. Um único índice no nível do documento que mapeia o nome de cada anexo para sua especificação de arquivo. ISO 32000-2 exige que toda especificação de arquivo alcançada por essa árvore carregue uma entradaEFcujo valor referencie um stream de arquivo incorporado. NextPDF constrói e equilibra essa árvore para você emsave().
O valor da relação importa para a conformidade. A PDF Association Application Note 0002 afirma que um arquivo associado exige uma entrada AFRelationship escolhida do conjunto fixo do PDF 2.0: Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema ou Unspecified. NextPDF modela esse conjunto como o enum AFRelationship e rejeita qualquer outro valor. Escolha o termo que explica por que o arquivo está presente: uma planilha de horas que dá origem a uma fatura é Source; um conjunto de dados legível por máquina por trás de um gráfico é Data.
Um portfólio PDF (chamado de collection na ISO 32000-2) é a próxima camada acima. Quando um documento carrega vários anexos, o dicionário Collection do catálogo informa ao leitor como apresentá-los: uma tabela de detalhes ordenável, um layout em blocos ou um envelope oculto. A ISO 32000-2 descreve o dicionário Collection como o controle que um processador de PDF usa para apresentar anexos de arquivo como um portfólio organizado. NextPDF modela isso como o objeto de valor CollectionDictionary, com CollectionSort para a ordem das colunas em uma visualização de detalhes.
Superfície da API
Seção intitulada “Superfície da API”Os métodos no nível do documento vêm do concern HasFileAttachments em \NextPDF\Core\Document:
embedFile(string $path, string $description = ''): static— lê um arquivo de$pathe o anexa. NextPDF detecta o tipo MIME a partir da extensão; a relação tem como padrãoUnspecified. Lê até 100 MB; useembedFileFromString()para payloads maiores. Retorna o documento para encadeamento.embedFileFromString(string $data, string $filename, string $description = '', string $afRelationship = '/Unspecified'): static— anexa bytes em memória com o nome de exibição$filename. Passe um literalAFRelationship(com ou sem a barra inicial) para definir a relação. Retorna o documento para encadeamento.
Os tipos de apoio residem nos namespaces \NextPDF\Navigation e \NextPDF\Document:
\NextPDF\Navigation\AFRelationship— o enum dos oito valores de relação válidos.AFRelationship::coerce()normaliza uma string ou um caso de enum e lança uma exceção para um valor desconhecido.toPdfName()emite o literal/Name.\NextPDF\Document\CollectionDictionary— constrói o dicionárioCollectiondo catálogo. As constantesVIEW_DETAILS,VIEW_TILE,VIEW_HIDDEN,VIEW_CUSTOMeVIEW_NONEselecionam o modo de apresentação; o construtor também aceita um nome de documento inicial e uma ordenação opcional.\NextPDF\Document\CollectionSort— o objeto de valor de ordenação de colunas para um portfólio em visualização de detalhes.
Exemplo de código — Início rápido
Seção intitulada “Exemplo de código — Início rápido”Este exemplo mínimo anexa um conjunto de dados em comma-separated values (CSV) gerado a uma página de fatura e o declara como os dados Source a partir dos quais a fatura foi construída.
<?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');O leitor mostra line-items.csv no painel de anexos, e a relação o marca como a origem da fatura.
Exemplo de código — Produção
Seção intitulada “Exemplo de código — Produção”Este exemplo completo anexa um arquivo do disco e um conjunto de dados em memória, valida o caminho em disco em relação a um diretório base na lista de permissões antes de lê-lo e constrói um portfólio ordenável para os anexos. Ele captura as exceções mais específicas do NextPDF que o fluxo de anexos pode gerar e, em seguida, retorna um código de saída definido em vez de engolir a falha.
<?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 são objetos de valor. Eles validam suas entradas na construção e serializam para o literal /Collection do catálogo, que controla a visualização do portfólio no leitor.
Casos extremos e armadilhas
Seção intitulada “Casos extremos e armadilhas”- A entrada de caminho é responsabilidade sua.
embedFile()protege contra bytes nulos e stream wrappers e resolve o caminho real, mas não impõe uma lista de permissões para o diretório base. Quando o caminho vier de uma requisição, valide-o primeiro, como o exemplo de produção faz comresolveWithinBase(). - O limite de 100 MB se aplica somente a
embedFile(). Um arquivo acima de104,857,600bytes geraPageLayoutException. Para payloads maiores, faça o streaming dos bytes você mesmo e passe-os paraembedFileFromString(). - Nomes de tipo MIME longos são rejeitados. O tipo MIME detectado torna-se o
/Subtypedo stream incorporado, um token de nome PDF limitado a 127 bytes pela ISO 32000-2. Um tipo excepcionalmente longo (alguns formatos do Office se aproximam de 90 bytes) ainda fica bem abaixo do limite, mas um tipo fornecido manualmente que o exceda geraPageLayoutException. Deixe o engine detectar o tipo a partir da extensão, a menos que você tenha um motivo específico para sobrepô-lo. - Uma relação desconhecida lança uma exceção.
AFRelationship::coerce()rejeita qualquer valor fora do conjunto fixo em vez de rebaixá-lo paraUnspecified. Passe um caso de enum (AFRelationship::Source->value) para impedir que um erro de digitação chegue ao tempo de execução. - Os nomes de arquivo precisam ser distintos na name tree. Dois anexos com o mesmo nome de exibição colidem no índice
EmbeddedFiles. Dê a cada anexo um nome de arquivo único. _ModDateé registrado em Coordinated Universal Time (UTC).embedFile()lê a hora de modificação do arquivo e a grava comgmdate()para que a mesma fixture produza uma data byte-idêntica entre máquinas, independentemente da configuração de fuso horário.
Desempenho
Seção intitulada “Desempenho”Cada anexo é comprimido uma vez com gzcompress() no nível 9 e gravado como um único stream em save(). A compressão domina o custo e escala com o tamanho do payload anexado, não com o conteúdo da página. Um punhado de pequenos arquivos de apoio (conjuntos de dados, planilhas, um PDF de planilha de horas) permanece dentro do orçamento de 2000 ms / 64 MB. Para muitos anexos grandes, os bytes incorporados são o piso de memória: um anexo de 50 MB mantido como string ocupa pelo menos esse volume antes da compressão. Prefira embedFileFromString() com geração em blocos em vez de carregar vários arquivos grandes de uma vez.
A name tree é construída uma vez em save(). Até 64 entradas permanecem em uma árvore plana de raiz única. Acima disso, NextPDF particiona a árvore em faixas Kids e Limits equilibradas, de modo que o custo de indexação permanece logarítmico para grandes conjuntos de anexos.
Notas de segurança
Seção intitulada “Notas de segurança”- Valide todo caminho não confiável contra uma lista de permissões. A incorporação lê qualquer arquivo que o processo PHP consiga alcançar. Sem uma verificação de diretório base, um nome de arquivo malicioso transforma um anexo em Local File Inclusion (LFI). O exemplo de produção mostra a proteção por lista de permissões; aplique-a sempre que o nome de arquivo não for uma constante de tempo de compilação.
- Trate os bytes anexados como não confiáveis no lado consumidor. Um arquivo incorporado é opaco para o NextPDF. O engine não o analisa nem o executa. O risco está no ponto em que o arquivo é aberto posteriormente. Defina a relação e a descrição para que um consumidor downstream saiba o que é cada anexo antes de extraí-lo.
- Nenhum segredo em anexos ou descrições. O nome do arquivo, a descrição e os bytes são armazenados de forma legível, a menos que todo o documento seja criptografado. Para proteger um anexo, criptografe o documento com uma política de permissões (consulte a receita relacionada). Não incorpore credenciais, chaves ou dados pessoais que você não colocaria na página renderizada.
- Nenhum acesso à rede ocorre nesta receita. Cada byte é lido do caminho local validado ou fornecido em memória.
Conformidade
Seção intitulada “Conformidade”| Declaração | Especificação | Cláusula | reference_id |
|---|---|---|---|
Os streams de arquivo incorporado se associam ao documento por meio da entrada EmbeddedFiles no dicionário de nomes. | ISO 32000-2 | 7.11.4 | |
A name tree EmbeddedFiles mapeia nomes para especificações de arquivo cuja entrada EF referencia um stream de arquivo incorporado. | ISO 32000-2 | 7.7.4 | |
Um arquivo associado exige um valor AFRelationship do conjunto fixo do PDF 2.0. | PDF Association AN002 | 3 | |
O dicionário Collection do catálogo controla a apresentação dos anexos como portfólio. | ISO 32000-2 | 7.11.6 |
Perfil de reprodutibilidade — estrutural. O /ID do trailer, os átomos de data por gravação e o /ModDate do stream incorporado variam entre execuções; por isso, uma comparação estrutural remove esses valores antes de comparar o grafo de objetos. Esta receita descreve como o NextPDF produz a estrutura. Ela não afirma conformidade geral com PDF/A-4f, que depende do documento completo. Para um perfil de arquivamento que exige que todo anexo declare uma relação e uma descrição, consulte a receita PDF/A-4.