Aller au contenu

Métadonnées : construction du paquet XMP et lecture en flux

Le module Metadata constitue la couche XMP du moteur. Il construit le paquet XMP qu’un PDF transporte sous forme de flux de métadonnées, lit un paquet existant sans charger tout le document en mémoire et émet l’extension XMP du moteur dédiée à la piste d’audit.

Fenêtre de terminal
composer require nextpdf/core:^3

Un PDF transporte les métadonnées de niveau document sous forme d’un paquet XMP, stocké dans un flux de métadonnées rattaché au catalogue du document — ISO 32000-2 §14.3. Ce module est responsable de la production et de la consommation de ce paquet. Sa surface est volontairement réduite et ciblée : trois classes sous NextPDF\Metadata\Xmp.

XmpMetadataBuilder produit le paquet. Il sérialise un jeu de propriétés en un document XMP bien formé, encadré par les instructions de traitement <?xpacket?> standard. Il utilise le GUID de paquet canonique et la marque d’ordre des octets définis par la spécification XMP. Sa sortie est la chaîne d’octets que le Writer intègre comme flux de métadonnées — la représentation XMP intégrée au PDF décrite au §14.3.

XmpStreamReader consomme un paquet. Il est conçu pour traiter des entrées hostiles. La source est lue en flux, par blocs de 64 Ko, vers un fichier temporaire borné avant l’analyse. Un plafond total d’octets est appliqué pendant cette écriture. Le chargeur d’entités libxml est désactivé pendant toute l’analyse, puis rétabli. Un DOCTYPE déclenche un rejet ferme. Il expose iterateProperties(), un générateur qui produit des tuples (namespaceUri, localName, textContent) pour chaque élément feuille sans matérialiser l’arbre entier : seuls l’élément courant et son nœud texte sont présents en mémoire dans l’analyseur à un instant donné. Un paquet surdimensionné lève PacketTooLargeException ; un XML mal formé, un DOCTYPE ou une entrée non-UTF-8 déclenche InvalidConfigException.

XmpAuditFieldEmitter est l’extension propre au moteur. Il rend un AuditReport dans un champ XMP personnalisé sous l’espace de noms nextpdfAudit, de sorte que l’audit de conformité d’un document accompagne le fichier sous forme de XMP conforme aux standards plutôt que dans un fichier annexe. Le rapport AuditReport qu’il rend n’est pas produit par l’émetteur lui-même : l’enrichissement est activé lorsqu’un rendu s’exécute sous CssRenderingMode::Audit avec un auditCollector fourni par l’appelant, configuré via Config(auditCollector: ...). Le collecteur est piloté par l’appelant : celui-ci l’alimente, et l’émetteur rend tout ce qui a été collecté. Cette extension est plus récente que la surface XMP du cœur (@since 5.4.0). Le constructeur et le lecteur sont @since 2.0.0.

ClasseMembres principauxRôle
XmpMetadataBuilderbuild(): string, XPACKET_GUID, XPACKET_BOMSérialise un jeu de propriétés en un paquet XMP (@since 2.0.0)
XmpStreamReaderiterateProperties(mixed $source, int $byteCap = DEFAULT_BYTE_CAP): \Generator, DEFAULT_BYTE_CAPLecteur XMP borné en flux, qui rejette les DOCTYPE (@since 2.0.0)
PacketTooLargeExceptionétend NextPdfExceptionException levée lorsqu’un paquet XMP dépasse le plafond d’octets (@since 2.0.0)
XmpAuditFieldEmitterrender(?AuditReport $report): string, NAMESPACE_URIRend la piste d’audit sous forme de champ XMP personnalisé (@since 5.4.0)

Lance composer docs:generate-api-php -- --module=Metadata pour obtenir le tableau PHPDoc complet.

Parcours en flux les propriétés d’un paquet XMP existant avec un plafond d’octets explicite.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Metadata\Xmp\XmpStreamReader;
$reader = new XmpStreamReader();
foreach ($reader->iterateProperties(file_get_contents('/srv/in/xmp.xml'), byteCap: 1_048_576) as [$ns, $name, $value]) {
printf("%s:%s = %s\n", $ns, $name, $value);
}

Lis un paquet de façon défensive, en transformant les échecs typés du module en résultat applicatif plutôt qu’en laissant remonter les erreurs brutes de l’analyseur.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Exception\InvalidConfigException;
use NextPDF\Metadata\Xmp\PacketTooLargeException;
use NextPDF\Metadata\Xmp\XmpStreamReader;
use Psr\Log\LoggerInterface;
final readonly class XmpIngestService
{
public function __construct(
private XmpStreamReader $reader,
private LoggerInterface $logger,
) {}
/**
* @param resource|string $source A stream resource or XMP byte string.
*
* @return array<string, string> Flattened "ns:name" => value map.
*/
public function ingest(mixed $source): array
{
$properties = [];
try {
// Cap untrusted XMP at 4 MB regardless of the 1 GiB default.
foreach ($this->reader->iterateProperties($source, byteCap: 4_194_304) as [$ns, $name, $value]) {
$properties["{$ns}:{$name}"] = $value;
}
} catch (PacketTooLargeException $e) {
$this->logger->warning('XMP packet exceeded ingest cap; rejected.', ['error' => $e->getMessage()]);
return [];
} catch (InvalidConfigException $e) {
$this->logger->warning('XMP packet malformed or unsafe; rejected.', ['error' => $e->getMessage()]);
return [];
}
return $properties;
}
}
  • XmpStreamReader rejette d’emblée tout DOCTYPE. C’est une défense contre les XXE, pas un raffinement de validation : un paquet qui a besoin d’un DOCTYPE n’est pas accepté ; assainis-le en amont.
  • Le plafond d’octets vaut par défaut 1 Gio (DEFAULT_BYTE_CAP). Cette valeur par défaut est un plafond, pas une recommandation. Passe un byteCap strict pour les entrées non fiables.
  • iterateProperties() est un générateur. Consomme-le une seule fois ; l’itérer deux fois ne le rejoue pas.
  • Le lecteur désactive le chargeur d’entités libxml le temps de l’analyse, puis le rétablit. Ne l’exécute pas en parallèle, dans la même requête, avec une autre analyse basée sur libxml qui dépend du chargeur d’entités.
  • XmpAuditFieldEmitter::render(null) est valide et produit un rendu vide ; un AuditReport null signifie « aucun audit », pas une erreur.

Le constructeur est linéaire en fonction du nombre de propriétés. La consommation mémoire du lecteur est dominée par le plus long nœud texte, et non par la taille du document, car seul l’élément courant est présent en mémoire dans l’analyseur — les gros paquets sont parcourus en flux au lieu d’être chargés. La charge de référence par défaut respecte un budget de 1500 ms de temps réel / 64 Mo en pic. Le profil de reproductibilité est structural : un paquet XMP enregistre des horodatages de modification. Deux constructions des mêmes métadonnées logiques diffèrent au niveau de ces champs alors que la structure est identique.

XmpStreamReader analyse du XML non fiable et est durci en conséquence. Le traitement en flux, associé à un plafond d’octets effectivement appliqué, limite les dénis de service par amplification mémoire. Un DOCTYPE est rejeté pour bloquer les XXE. LIBXML_NONET bloque la résolution d’entités réseau. Une entrée non-UTF-8 est refusée. Définis tout de même un byteCap adapté au déploiement pour tout paquet d’origine externe, plutôt que de te reposer sur la valeur par défaut en gigaoctet. Traite les valeurs des propriétés XMP comme des chaînes non fiables lorsqu’elles reviennent dans l’application. Consulte le modèle de menace du moteur dans /modules/core/security/.

Le paquet que XmpMetadataBuilder produit correspond à la représentation du flux de métadonnées XMP intégré au PDF définie dans ISO 32000-2 §14.3 (). La forme de sérialisation XMP elle-même est régie par la spécification XMP (ISO 16684-1), qui ne figure pas dans le corpus de citations vérifiables. Cette exigence est référencée par numéro, sans être rattachée à un fragment précis. Ces éléments sont des faits d’implémentation issus de src/Metadata/Xmp/ et vérifiés par tests/Unit/Metadata/Xmp/. La conformité de bout en bout des métadonnées pour un profil (PDF/A, PDF/UA) est validée par l’oracle et les suites de référence décrits dans /modules/core/conformance/.