PDFlibPas (PDF Library for Delphi) comprueba cada objeto contra una tabla de reglas de versión PDF antes de escribir un archivo, y hasta hace poco ese preflight de versión PDF confundía diccionarios de medición CAD corrientes con geoespaciales. Un dibujo CAD de una página cargaba bien, y después SaveToFile devolvía 0 con LastErrorCode 602 y exigía 1.7 ExtensionLevel 3. Las reglas corregidas tratan los diccionarios /Measure rectilíneos (/Subtype /RL) como PDF 1.6 plano y reservan la compuerta de extensión para marcadores geoespaciales de verdad
El archivo entró por admisión de corpus: una página, un grupo de optional-content, dos viewports de medición rectilíneos, el tipo de salida que escribe un paquete CAD de arquitectura para que un visor pueda leer distancias de un plano. No tenía nada de exótico, que es justo por lo que importaba el rechazo. Un preflight que bloquea un archivo válido es peor que uno lento, porque quien llama recibe un diagnóstico de aspecto autoritario apuntando a una feature que el documento no contiene. El fix tuvo dos partes: la lectura de la especificación detrás de una regla, y la constatación de que la regla no podía distinguir dos tipos de diccionario al nivel al que miraba
¿Cómo funciona el preflight de versión en el guardado de PDFlibPas?
La compuerta de guardado, PrepareAndCheckSaveVersion, compara cada objeto indirecto contra PDFFeatureRules y falla en la primera regla que a la vez coincide y necesita más de lo que el objetivo permite. El objetivo es la versión del documento (o la versión fijada con LockSaveVersion), más el extension level de Adobe declarado bajo /Extensions /ADBE. Cada registro TPDFFeatureRule lleva un MinVersion, un MinExtensionLevel, un MatchKind como fmkDictKey o fmkDictSubtype, una cadena Match, un nombre de Feature legible por humanos y un callback opcional. AddRule registra una regla de versión normal; AddExtensionRule clava siempre MinVersion en 17 y añade un extension level encima, así que una regla de extensión solo puede satisfacerse con PDF 1.7 más la entrada /Extensions correcta. Cuando la compuerta salta, la versión requerida y el nombre de la feature se conservan para quien llama, y las claves 311, 312 y 313 de GetInformation las exponen
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('floor-plan.pdf', '') <> 1 then
raise Exception.Create('load failed');
if Pdf.SaveToFile('floor-plan-out.pdf') <> 1 then
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// 311: versión requerida, 312: feature que la disparó,
// 313: versión a la que está bloqueado el objetivo de guardado ('' sin bloqueo)
Writeln('Needs ', Pdf.GetInformation(311),
' for ', Pdf.GetInformation(312),
', locked at [', Pdf.GetInformation(313), ']');
finally
Pdf.Free;
end;
end;
¿Por qué un dibujo CAD corriente fallaba con error 602?
La tabla de reglas contenía AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil), que saltaba con cualquier diccionario que meramente tuviera una clave /Measure, y cada viewport de medición tiene una. El array /VP de la página guarda diccionarios de viewport, cada viewport apunta a su diccionario de medición vía /Measure, y la coincidencia por presencia de clave se paraba ahí sin mirar qué era realmente el diccionario de medición. El escaneo de features en carga podía entonces subir el número de versión del documento a 1.7, pero nunca escribe una declaración /Extensions en nombre de un archivo de entrada, así que la compuerta de guardado veía PDF 1.7 a extension level 0 y reportaba 1.7 ExtensionLevel 3. Esa negativa a inventar una declaración de extensión es deliberada: la librería no asciende en silencio un archivo de entrada para maquillar una regla equivocada
La especificación es inequívoca en el caso rectilíneo. Los diccionarios Measure llegaron con PDF 1.6, e ISO 32000-1 §12.9 da a /Subtype un valor por defecto de RL, un sistema de coordenadas rectilíneo descrito por su propio conjunto de entradas: ratio de escala, formatos numéricos X e Y, distancia y área. La medición geoespacial es la adición posterior del Adobe Extension Level 3 sobre PDF 1.7, identificada por /Subtype /GEO y con arrays de puntos geográficos, diccionarios de sistema de coordenadas y unidades de visualización, las estructuras que recorre leer viewports GeoPDF y arrays GPTS y LPTS en Delphi. Ambos diccionarios cuelgan de la misma clave /Measure, así que cualquier regla que se pare en la clave no puede tener razón para ambos. La información distintiva está un nivel más abajo, en el propio diccionario de medición
¿Qué sigue exigiendo el conjunto de reglas corregido?
El fix elimina la regla de clave incondicional y deja las compuertas que describen requisitos de versión reales. Una página que lleva /VP o /UserUnit sigue necesitando PDF 1.6 vía CB_PagePDF16Entries, una clave /PtData sigue necesitando extension level 3, y CB_GeospatialDictionary decide si un diccionario de medición es geoespacial por su contenido y no por la clave por la que llegó
// Eliminado: todo diccionario con clave /Measure contaba como geoespacial
// AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil);
AddRule(16, fmkCustom, '', 'Page PDF 1.6 entry /UserUnit /VP', CB_PagePDF16Entries);
AddExtensionRule(3, fmkDictKey, 'PtData', '/PtData geospatial dictionary', Nil);
AddExtensionRule(3, fmkCustom, '', 'geospatial measure dictionary', CB_GeospatialDictionary);
function CB_GeospatialDictionary(Obj: TPDFObject; const Ctx: TPDFRuleContext): Boolean;
var
Dict: TPDFDictionary;
begin
Result := False;
if not (Obj is TPDFDictionary) then
Exit;
Dict := TPDFDictionary(Obj);
Result := (Dict.StringValue('Subtype') = 'GEO') or
(Dict.FindIndexByKeyName('GCS') >= 0) or (Dict.FindIndexByKeyName('DCS') >= 0) or
(Dict.FindIndexByKeyName('GPTS') >= 0) or (Dict.FindIndexByKeyName('LPTS') >= 0) or
(Dict.FindIndexByKeyName('PDU') >= 0);
end;
Las regresiones compartidas Delphi y FPC fijan esa frontera por los dos lados. Un viewport cuyo diccionario de medición omite /Subtype y otro que escribe /RL pasan ambos en PDF 1.6, la misma página sigue rechazándose en PDF 1.5, y la detección de features ya no informa de una extensión para ella. Añadir un array /GPTS devuelve el veredicto a 1.7 ExtensionLevel 3, que pasa una vez declarado el extension level, y un diccionario /Subtype /GEO pelado se rechaza sin él. El callback es conservador a propósito: un diccionario rectilíneo que además lleve una clave /GCS o /PDU suelta se trata como geoespacial, porque esas claves no significan nada en el modelo RL
LockSaveVersion es donde este cambio se vuelve visible para quien llama. TPDFlib.LockSaveVersion acepta de '1.0' a '1.7', devuelve 0 ante cualquier otra cosa, fija la versión del documento y evita que las llamadas del lado del writer la suban en silencio, y aun así la compuerta de guardado corre contra el valor bloqueado. Con las reglas corregidas, un archivo CAD bloqueado en 1.6 guarda limpio. Un GeoPDF genuino bloqueado en 1.6 sigue llevándose un 602, que es la respuesta correcta, y las llamadas de autoría geoespacial como SetMeasureDictCoordinateSystem declaran ellas mismas el extension level 3 cuando construye ese contenido por la API
if Pdf.LockSaveVersion('1.6') <> 1 then
raise Exception.Create('unsupported version string');
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
begin
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// Contenido real por encima de 1.6, por ejemplo un diccionario measure GEO
raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
[string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;
¿Por qué el escaneo de reglas de versión era más lento de lo necesario?
El escaneo copiaba cada TPDFFeatureRule a un record local antes de probarla, y como el registro lleva dos campos AnsiString, cada copia ajustaba dos reference counts y liberaba los valores anteriores. El preflight visita cada nodo de cada árbol de objetos, escalares incluidos, así que ese coste multiplicaba número de objetos por número de reglas, y las reglas que ni siquiera aplicaban a la versión objetivo se copiaban primero y se saltaban después. Como PDFFeatureRules se rellena una vez en la inicialización de la unit y se trata como de solo lectura, la v3.539.17 pasa las entradas de la tabla directamente a MatchSingleRule y RuleExceedsTarget, cuyos parámetros const Rule toman una referencia sin tocar los strings
// Antes: una copia de record gestionado por regla, por objeto visitado
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
Continue;
// Después: los parámetros const leen la entrada inmutable de la tabla in situ
if not RuleExceedsTarget(PDFFeatureRules[X], TargetVersion, TargetExtensionLevel) then
Continue;
if MatchSingleRule(Obj, Ctx, PDFFeatureRules[X]) then
begin
RequiredVersion := RequiredVersionString(PDFFeatureRules[X]);
FeatureName := PDFFeatureRules[X].Feature;
Result := False;
Exit;
end;
El efecto medido es estrecho y conviene citarlo así. El benchmark comprueba un array de 20.000 objetos numéricos contra un objetivo PDF 1.4 diez veces por ronda; compilado con FPC Win64 a -O2, la mediana de cinco rondas bajó de 0,711 s a 0,203 s, y ejecutar los dos builds en orden inverso dio 0,459 s frente a 0,150 s. Eso es aproximadamente una ganancia de 3x solo en el camino de emparejamiento de reglas. Un guardado real también paga la detección de features diferida, la decodificación de objetos y la serialización, así que el ratio no se traslada al tiempo total de guardado. El orden de reglas, los callbacks, los umbrales de versión y el diagnóstico de primer fallo no cambian, y no se cacheó ninguna regla entre guardados ni se saltó ninguna para conseguirlo
¿Qué comprobar cuando un PDF cargado no pasa el preflight de versión?
Lea las claves 311 y 312 antes de tocar la versión. Si la feature nombra un diccionario geoespacial y el archivo solo dibuja mediciones rectilíneas, era este falso positivo, y un build actual guarda el archivo sin cambios. Si la feature es genuina, declare la extensión o bloquee la versión en una que contenga honestamente el contenido; subir la versión solo para callar la compuerta esconde la pregunta de si los consumidores aguas abajo pueden leer lo que usted distribuye. El mismo principio de comprobaciones acotadas y respaldadas por evidencia impulsa el preflight en modo autor PDF/E-1 para documentos de ingeniería, donde los dibujos CAD se miden contra un estándar de conformidad y no contra un número de versión
Las comprobaciones de conformidad de versión, los diccionarios de medición y geoespaciales, y el bloqueo de versión de guardado son todos parte de PDF Library for Delphi, el toolkit PDFlibPas para desarrolladores de Delphi, C++Builder y Lazarus