Pular para o conteúdo

Incorpore arquivos e crie portfólios PDF

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.

Terminal window
composer require nextpdf/core:^3

Nenhuma extensão opcional é necessária.

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.

  1. 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 /Subtype do stream.
  2. Dicionário de especificação de arquivo. É o encapsulador de metadados. Ele carrega o nome de exibição do arquivo (/F e o /UF em 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).
  3. 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 entrada EF cujo valor referencie um stream de arquivo incorporado. NextPDF constrói e equilibra essa árvore para você em save().

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.

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 $path e o anexa. NextPDF detecta o tipo MIME a partir da extensão; a relação tem como padrão Unspecified. Lê até 100 MB; use embedFileFromString() 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 literal AFRelationship (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ário Collection do catálogo. As constantes VIEW_DETAILS, VIEW_TILE, VIEW_HIDDEN, VIEW_CUSTOM e VIEW_NONE selecionam 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.

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.

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.

  • 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 com resolveWithinBase().
  • O limite de 100 MB se aplica somente a embedFile(). Um arquivo acima de 104,857,600 bytes gera PageLayoutException. Para payloads maiores, faça o streaming dos bytes você mesmo e passe-os para embedFileFromString().
  • Nomes de tipo MIME longos são rejeitados. O tipo MIME detectado torna-se o /Subtype do 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 gera PageLayoutException. 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 para Unspecified. 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 com gmdate() para que a mesma fixture produza uma data byte-idêntica entre máquinas, independentemente da configuração de fuso horário.

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.

  • 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.
DeclaraçãoEspecificaçãoCláusulareference_id
Os streams de arquivo incorporado se associam ao documento por meio da entrada EmbeddedFiles no dicionário de nomes.ISO 32000-27.11.4
A name tree EmbeddedFiles mapeia nomes para especificações de arquivo cuja entrada EF referencia um stream de arquivo incorporado.ISO 32000-27.7.4
Um arquivo associado exige um valor AFRelationship do conjunto fixo do PDF 2.0.PDF Association AN0023
O dicionário Collection do catálogo controla a apresentação dos anexos como portfólio.ISO 32000-27.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.