Su código de detección reporta que un archivo PDF/UA de otro productor no declara nada, y un escaneo PDF/R deja de ser PDF/R en el momento en que un editor le adjunta una nota adhesiva. HotPDF lee estas declaraciones de un documento cargado mediante funciones de acceso como IsLoadedPDFUA, GetLoadedPDFR1Version y GetLoadedPDFDeclarations, cada una de las cuales mira donde su propio estándar guarda el identificador
Dónde escribe su identificador cada estándar PDF
Los estándares subsidiarios nunca acordaron un único lugar para la declaración. La mayoría usa propiedades XMP en el flujo /Metadata del catálogo, unos cuantos antiguos todavía confían en claves del diccionario de información del documento, PDF/R pone una línea de comentario junto al trailer del archivo, y la generación PDF 2.0 añade dos mecanismos más: diccionarios de developer extensions en el catálogo y PDF Declarations dentro del XMP. Un identificador es la declaración del archivo sobre sí mismo; si el contenido está a la altura lo decide un validador, y producir salida conforme a PDF/A, PDF/X y PDF/UA es un tema aparte. Para enrutar (cola de impresión para PDF/X, archivado para PDF/A, pipeline de accesibilidad para WTPDF) la declaración es lo que necesita, leída del lugar correcto:
| Estándar | Dónde vive la declaración | Función de acceso de HotPDF |
|---|---|---|
| PDF/A (ISO 19005) | XMP pdfaid:part y pdfaid:conformance, más pdfaid:rev en PDF/A-4 | IsLoadedPDFA, GetLoadedPDFAVersion, GetLoadedPDFARevision |
| PDF/X (ISO 15930) | GTS_PDFXVersion en el diccionario Info para PDF/X-1a y PDF/X-3, en XMP para las partes posteriores | IsLoadedPDFX, GetLoadedPDFXVersion |
| PDF/UA (ISO 14289) | XMP pdfuaid:part | IsLoadedPDFUA, GetLoadedPDFUAVersion |
| PDF/VT (ISO 16612-2) | XMP pdfvtid:GTS_PDFVTVersion | IsLoadedPDFVT, GetLoadedPDFVTVersion |
| PDF/E (ISO 24517-1) | ISO_PDFEVersion, leído del diccionario Info, del catálogo o del XMP | IsLoadedPDFE, GetLoadedPDFEVersion |
| PDF/R-1 (ISO 23504-1) | Comentario %PDF-raster-x.y justo encima del último startxref | IsLoadedPDFR1, GetLoadedPDFR1Version |
| PDF/VCR-1 (ISO 16613-1) | GTS_PDFVCRVersion en el catálogo o en XMP | IsLoadedPDFVCR, GetLoadedPDFVCRVersion |
| WTPDF 1.0, ISO/TS 32005 | Entradas pdfd:declarations en XMP | IsLoadedWTPDF, GetLoadedWTPDFVersion, IsLoadedISO32005 |
| Developer extensions de PDF 2.0 | /Extensions /ISO_ en el catálogo | HasLoadedISODeveloperExtension, GetLoadedISODeveloperExtensionLevels |
uses
System.SysUtils, HPDFDoc;
procedure ReportClaims(const FileName: string; const Password: AnsiString);
var
Pdf: THotPDF;
PDFX, PDFXLevel: AnsiString;
begin
Pdf := THotPDF.Create(nil); // una instancia dedicada, usada solo para inspección
try
// La sobrecarga con contraseña descifra los flujos, paquete XMP incluido;
// pase '' para archivos que abren sin preguntar
if Pdf.LoadFromFile(FileName, Password) <= 0 then
raise Exception.CreateFmt('Cannot open %s', [FileName]);
if Pdf.IsLoadedPDFA then
Writeln('PDF/A: ', Pdf.GetLoadedPDFAVersion); // p. ej. PDF/A-2b
PDFX := Pdf.GetLoadedPDFXVersion; // XMP, luego Info
if PDFX <> '' then
begin
PDFXLevel := Pdf.GetLoadedInfoKeyValue('GTS_PDFXConformance');
if PDFXLevel <> '' then
Writeln('PDF/X: ', PDFX, ', conformance ', PDFXLevel)
else
Writeln('PDF/X: ', PDFX);
end;
if Pdf.IsLoadedPDFUA then
Writeln('PDF/UA: ', Pdf.GetLoadedPDFUAVersion); // p. ej. PDF/UA-1
if Pdf.IsLoadedPDFVT then
Writeln('PDF/VT: ', Pdf.GetLoadedPDFVTVersion);
if Pdf.IsLoadedPDFE then
Writeln('PDF/E: ', Pdf.GetLoadedPDFEVersion);
if Pdf.IsLoadedPDFR1 then
Writeln('PDF/R: ', Pdf.GetLoadedPDFR1Version); // p. ej. PDF/R-1.0
if Pdf.IsLoadedWTPDF then
Writeln('WTPDF: ', Pdf.GetLoadedWTPDFVersion);
finally
Pdf.Free;
end;
end;
Use una instancia THotPDF aparte para inspeccionar, como hace el ejemplo. Las funciones IsLoaded… responden por cualquier documento que el componente tenga, incluido uno que está construyendo: IsLoadedPDFUA también es True cuando PDFUACompliance se activó para la salida, y IsLoadedPDFX coincide con un PDFXCompliance no vacío. Útil a mitad de construcción, pero en un componente reutilizado para ambos trabajos mezcla sus ajustes de salida en el veredicto sobre el archivo de alguien más
PDF/X es la única familia cuyo identificador se mudó entre partes. PDF/X-1a y PDF/X-3 son perfiles PDF 1.3 que ponen GTS_PDFXVersion en el diccionario de información del documento y no necesitan nada de XMP, mientras que PDF/X-4 y las partes posteriores exigen la propiedad XMP pdfx:GTS_PDFXVersion. GetLoadedPDFXVersion busca primero en XMP, bajo el prefijo pdfx o el pdfxid, luego en el diccionario Info, después en las claves del catálogo que escriben algunos productores antiguos, y devuelve el valor declarado sin cambios. PDF/X-1a tiene un detalle que conviene contemplar en el código: su valor de versión es PDF/X-1:2001, y el nivel de conformidad PDF/X-1a:2001 vive en una clave propia, /GTS_PDFXConformance, que el ejemplo lee con GetLoadedInfoKeyValue cuando necesita distinguir X-1a del X-1 simple
¿Cómo se ve una declaración XMP válida?
XMP es RDF serializado como XML, y RDF/XML permite escribir una propiedad simple de dos maneras dentro de un rdf:Description: como elemento hijo, <pdfuaid:part>1</pdfuaid:part>, o como atributo en la propia description, pdfuaid:part="1", con comillas simples o dobles. ISO 16684-1 acepta ambas, y un paquete puede repartir sus propiedades entre varios elementos rdf:Description. La división no es hipotética. HotPDF escribe la forma de elemento hijo para PDF/UA, PDF/VT, PDF/E y PDF/X, mientras que el corpus de pruebas de veraPDF y muchos productores externos escriben la forma abreviada, con descriptions como pdfuaid:part="1" o pdfvtid:GTS_PDFVTVersion="PDF/VT-1" que llevan la declaración completa en atributos. Un detector construido alrededor de Pos('<pdfuaid:part>', Xmp) y ejercitado solo con salida propia pasa todas las comprobaciones locales y luego reporta la primera muestra de veraPDF como sin declaración. El lector XMP de HotPDF maneja ambas formas para cada función de acceso de la tabla. GetLoadedPDFAVersion combina pdfaid:part y pdfaid:conformance en identificadores como PDF/A-1b, PDF/A-2u, PDF/A-4 o PDF/A-4f, y GetLoadedPDFARevision devuelve pdfaid:rev, el año de edición que PDF/A-4 exige (2020) y que las partes 1 a 3 no definen:
procedure ReportPDFALevel(Pdf: THotPDF);
var
Level, Revision: AnsiString;
begin
Level := Pdf.GetLoadedPDFAVersion; // 'PDF/A-1b', 'PDF/A-2u', 'PDF/A-4f', ...
if Level = '' then
Exit;
Revision := Pdf.GetLoadedPDFARevision; // '2020' para PDF/A-4, vacío para las partes 1-3
if Revision <> '' then
Writeln(Level, ', edition ', Revision)
else
Writeln(Level);
// Esperado: "PDF/A-2b" sin importar si el productor escribió
// <pdfaid:part>2</pdfaid:part> o pdfaid:part="2"
end;
Los prefijos son la otra parte móvil. En XML el prefijo es solo un alias local; lo que identifica al esquema PDF/A es el URI de espacio de nombres http://www.aiim.org/pdfa/ns/id/, que las erratas aprobadas de ISO 19005-4 también admiten en su grafía https://, y un productor puede atarlo al prefijo que le guste. GetLoadedPDFAVersion resuelve el prefijo que el paquete declara para ese URI antes de leer parte y nivel, así que un paquete que escribe pdfa:part en lugar de pdfaid:part igualmente se identifica. Cualquier otra propiedad XMP que usted lea por su cuenta del paquete decodificado que devuelve GetLoadedXMPMetadata merece el mismo cuidado: ambas serializaciones, el prefijo resuelto desde su declaración xmlns, y espacios en blanco permitidos alrededor del signo igual
Los archivos cifrados ocultan la declaración a las búsquedas de bytes
Un escaneo del archivo crudo buscando pdfaid o ISO_PDFEVersion se salta el parseo y falla en dos casos ordinarios. La mayoría de los flujos XMP están comprimidos con Flate, así que los nombres de las propiedades nunca aparecen como texto en disco. Y en un documento cifrado el flujo de metadatos es texto cifrado: ISO 32000-2 §7.6.4.2 define EncryptMetadata con un valor por defecto de true, así que salvo que el productor haya excluido explícitamente, el paquete va cifrado junto con todos los demás flujos. Una búsqueda de bytes contra un archivo PDF/E-1 cifrado reporta por tanto la declaración ISO_PDFEVersion como ausente aunque esté ahí. El orden confiable es cargar, descifrar, decodificar y luego buscar. Abra el archivo por la sobrecarga con contraseña, LoadFromFile(FileName, Password), y pase una cadena vacía para los archivos que solo llevan contraseña de owner y abren en cualquier visor sin preguntar. El LoadFromFile(FileName) de un solo argumento carga la estructura del documento sin descifrar sus flujos, lo que deja el paquete XMP ilegible; tras la sobrecarga con contraseña, GetLoadedXMPMetadata y todas las funciones de acceso de arriba trabajan sobre el paquete descifrado y descomprimido
¿Por qué un archivo PDF/R pierde su identidad tras una edición?
PDF/R-1 se identifica de una manera en la que ningún otro estándar subsidiario lo hace. ISO 23504-1, cláusula 5, exige una línea de comentario con el texto %PDF-raster-1.0 (o la versión que aplique) en la línea inmediatamente anterior a la última línea que contiene startxref. Un parser de PDF tira los comentarios mientras lee los datos de referencias cruzadas, así que un modelo de objetos no tiene nada que reportar; la evidencia existe solo en los bytes crudos al final del archivo. GetLoadedPDFR1Version vuelve a la fuente desde la que se cargó el documento, lee como mucho los últimos 64 KiB, encuentra el último startxref y revisa la línea de arriba. Ahora siga una actualización incremental. Un editor que añade una anotación adjunta objetos nuevos, una sección de referencias cruzadas nueva, un trailer nuevo y un startxref nuevo después del %%EOF original. La línea %PDF-raster-1.0 sigue en el archivo, pero ahora queda encima del penúltimo startxref, y el estándar es inequívoco sobre cuál cuenta. Resultado esperado: IsLoadedPDFR1 devuelve False tras la edición, salvo que la herramienta de edición reemita el comentario en su propia actualización. Un detector que escanea el archivo completo buscando %PDF-raster- diría que sí, y por la regla de la cláusula 5 esa respuesta está mal. Un flujo de captura que deba mantener sus archivos PDF/R pone las anotaciones en un archivo sidecar o en una herramienta que reescribe el archivo completo como PDF/R
Developer extensions: niveles enteros, nombres y arreglos
Los documentos PDF 2.0 anuncian las especificaciones técnicas ISO de las que dependen (hashes SHA-3 de ISO/TS 32001, firmas ECC de ISO/TS 32002, activos glTF de ISO/TS 32007) a través del diccionario /Extensions del catálogo, definido en ISO 32000-2 §7.12. Bajo el prefijo registrado /ISO_, la Tabla 48 admite un único diccionario de developer extensions o, novedad de PDF 2.0, un arreglo de ellos, y la Tabla 49 junto con §7.12.5 hace de /ExtensionLevel un entero. Un lector que maneja solo una de las dos formas de contenedor pierde todos los archivos escritos de la otra manera. HasLoadedISODeveloperExtension y GetLoadedISODeveloperExtensionLevels aceptan ambas y resuelven referencias indirectas en el camino, aunque §7.12.1 dice que estos diccionarios deben ser objetos directos:
procedure ReportISOExtensions(Pdf: THotPDF);
var
Levels: THPDFIntegerArray;
Revision, URL: AnsiString;
I: Integer;
begin
Levels := Pdf.GetLoadedISODeveloperExtensionLevels;
for I := 0 to High(Levels) do
Writeln('ISO_ extension level ', Levels[I]);
if Pdf.HasLoadedISODeveloperExtension(32001, Revision, URL) then
Writeln('ISO/TS 32001 revision ', Revision, ', documented at ', URL);
// Esperado para un archivo escrito con EnsureISO32001Extensions:
// ISO/TS 32001 revision :2022, documented at https://www.iso.org/standard/45874.html
if Pdf.HasLoadedISODeveloperExtension(32007) then
Writeln('glTF 3D assets per ISO/TS 32007');
end;
La primera edición de ISO/TS 32007:2024 tiene un ejemplo en §4.8 que escribe /ExtensionLevel /32007 como name; PDF Errata Issue #835 lo corrigió al entero 32007, pero un productor que copió el ejemplo original todavía escribe el name. El lector de HotPDF por eso recurre a parsear un valor name o string, con o sin la barra inicial, cuando la búsqueda del entero vuelve vacía, así que HasLoadedISODeveloperExtension(32007) encuentra ambas grafías. La otra errata concierne a /URL: la Tabla 49 de ISO 32000-2:2020 lista /URL como opcional, y el Issue #732 la promueve a obligatoria. Del lado de la escritura HotPDF emite la forma entera y un /URL en cada llamada EnsureISO…Extensions; del lado de la lectura, un parámetro out URL vacío es lo normal para archivos escritos según el texto de 2020, así que trátelo como información y nunca como razón para rechazar la extensión
¿Qué URI WTPDF escribió el productor?
Las declaraciones WTPDF 1.0 e ISO/TS 32005 son PDF Declarations: entradas en un bag pdfd:declarations en XMP, bajo el espacio de nombres http://pdfa.org/declarations/ definido por la nota técnica PDF Declarations de la PDF Association. Cada entrada lleva un URI conformsTo y campos opcionales claimBy, claimDate, claimCredentials y claimReport, que GetLoadedPDFDeclarations devuelve como un arreglo de registros THPDFDeclarationEntry; del lado de la escritura, AddPDFDeclaration pone los mismos cinco campos en el paquete XMP, junto a los espacios de nombres de identificación de PDF 2.0, PDF/A-4 y PDF/UA-2. Cortar el bag en entradas con una búsqueda de texto plano tiene su propia trampa: divida en cada aparición de conformsTo y la etiqueta de cierre </pdfd:conformsTo> de la forma de elemento hijo cuenta como inicio de una entrada nueva, lo que trunca la primera declaración antes de su propio final. El lector de HotPDF salta las etiquetas de cierre y acepta ambas serializaciones
Los URI en sí son la parte más difícil. La misma declaración le llega como http:// o https://, y el Issue #395 de WTPDF 1.0 cambió si una / va antes del #, así que los archivos escritos antes y después de la errata difieren en ese único carácter; una comparación de cadenas simple los trata como estándares distintos. HasLoadedPDFDeclaration compara URI sin distinguir mayúsculas, pliega https:// sobre http:// y quita una / justo antes del # en ambos lados, así que la consulta de abajo coincide con la grafía que haya elegido el productor. El fragmento queda intacto: #accessibility1.0 y #accessibility1.0-validated son declaraciones distintas, la segunda afirma que un validador revisó el archivo, y usted debe pedir la que le importa a su flujo de trabajo. GetLoadedWTPDFVersion colapsa ambas en 'WTPDF 1.0 Accessibility' o 'WTPDF 1.0 Reuse' para enrutar
procedure ReportDeclarations(Pdf: THotPDF);
var
Decls: THPDFDeclarationEntryArray;
I: Integer;
begin
Decls := Pdf.GetLoadedPDFDeclarations;
for I := 0 to High(Decls) do
Writeln(Decls[I].ConformsTo, ' claimed by ', Decls[I].ClaimBy,
' on ', Decls[I].ClaimDate);
// Coincide con http o https, con o sin la barra antes del '#'
if Pdf.HasLoadedPDFDeclaration(
'https://pdfa.org/declarations/wtpdf#accessibility1.0') then
Writeln('WTPDF 1.0 Accessibility (unvalidated claim)');
// Cubre tanto la variante simple como la -validated de Reuse y Accessibility
Writeln('WTPDF profile: ', Pdf.GetLoadedWTPDFVersion);
end;
Para un router de captura o de impresión, el orden de las operaciones importa más que cualquier función de acceso: pruebe IsLoadedPDFR1 sobre el archivo tal como llegó, antes de que un paso de anotación o de estampado adjunte su actualización, y enrute PDF/X con GetLoadedPDFXVersion, que cubre por igual el diccionario Info de los trabajos PDF/X-1a y PDF/X-3 y el XMP de los trabajos PDF/X-4, leyendo /GTS_PDFXConformance solo donde haya que distinguir X-1a de X-1. Ambas comprobaciones corren sobre cualquier documento cargado en el HotPDF Delphi Component, incluidos los archivos cifrados abiertos con su contraseña