PDFlibPas (PDF Library for Delphi) chequea 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 sola 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 pelado y reservan el gate 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 un paquete CAD de arquitectura escribe para que un visor pueda leer distancias sobre un plano. No tenía nada de exótico, que es exactamente por qué importaba el rechazo. Un preflight que bloquea un archivo válido es peor que uno lento, porque quien llama recibe un diagnóstico con aire de autoridad 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 donde estaba mirando
¿Cómo funciona el preflight de versión al guardar de PDFlibPas?
El gate de guardado, PrepareAndCheckSaveVersion, compara cada objeto indirecto contra PDFFeatureRules y falla en la primera regla que tanto matchee como pida más de lo que el target permite. El target es la versión del documento (o la versión fijada por LockSaveVersion), más el extension level de Adobe declarado bajo /Extensions /ADBE. Cada record TPDFFeatureRule lleva un MinVersion, un MinExtensionLevel, un MatchKind como fmkDictKey o fmkDictSubtype, un string Match, un nombre de Feature legible por humanos y un callback opcional. AddRule registra una regla de versión simple; AddExtensionRule siempre fija MinVersion en 17 y agrega un extension level encima, así que una regla de extensión solo puede satisfacerla PDF 1.7 más la entrada /Extensions correcta. Cuando el gate salta, la versión requerida y el nombre de la feature quedan guardados para quien llama, y las claves 311, 312 y 313 de GetInformation los 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á fijado el target de guardado ('' si no hay fijación)
Writeln('Needs ', Pdf.GetInformation(311),
' for ', Pdf.GetInformation(312),
', locked at [', Pdf.GetInformation(313), ']');
finally
Pdf.Free;
end;
end;
¿Por qué fallaba con error 602 un dibujo CAD corriente?
La tabla de reglas contenía AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil), que se disparaba ante cualquier diccionario que apenas 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 el match por presencia de clave se detenía ahí sin mirar qué era realmente el diccionario de medición. El scan de features en carga podía entonces subir el número de versión del documento a 1.7, pero jamás escribe una declaración /Extensions a nombre de un archivo de entrada, así que el gate 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 promueve en silencio un archivo de entrada para maquillar una regla que está mal
La especificación es inequívoca sobre el caso rectilíneo. Los diccionarios Measure llegaron en PDF 1.6, e ISO 32000-1 §12.9 le da a /Subtype un default de RL, un sistema de coordenadas rectilíneo descrito por su propio set de entradas: ratio de escala, formatos numéricos X e Y, distancia y área. La medición geoespacial es el agregado posterior de Adobe Extension Level 3 sobre PDF 1.7, identificado 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 ninguna regla que se detenga en la clave puede estar en lo cierto para ambos. La información distintiva está un nivel más abajo, en el propio diccionario de medición
¿Qué sigue exigiendo el set de reglas corregido?
El fix borra la regla incondicional de clave y deja los gates que describen requisitos de versión reales. Una página que lleve /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: cada 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 de Delphi y FPC fijan esa frontera desde ambos lados. Un viewport cuyo diccionario de medición omite /Subtype y otro que escribe explícitamente /RL pasan ambos a PDF 1.6, la misma página sigue rechazándose a PDF 1.5, y la detección de features ya no reporta una extensión para ella. Agregar un array /GPTS devuelve el veredicto a 1.7 ExtensionLevel 3, que pasa una vez declarado el extension level, y un diccionario pelado /Subtype /GEO se rechaza sin él. El callback es conservador a propósito: un diccionario rectilíneo que además lleve una clave suelta /GCS o /PDU se trata como geoespacial, porque esas claves no significan nada en el modelo RL
LockSaveVersion es donde este cambio se vuelve visible para quienes llaman. 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 de escritura la suban en silencio, aunque el gate de guardado igual corre contra el valor fijado. Con las reglas corregidas, un archivo CAD fijado a 1.6 guarda limpiamente. Un GeoPDF genuino fijado a 1.6 igual recibe 602, que es la respuesta correcta, y las llamadas de autoría geoespacial como SetMeasureDictCoordinateSystem declaran ellas mismas el extension level 3 cuando usted arma ese contenido vía 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 de medición 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 scan de reglas de versión era más lento de lo necesario?
El scan copiaba cada TPDFFeatureRule a un record local antes de testearlo, y como el record tiene 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 costo multiplicaba el conteo de objetos por el conteo de reglas, y las reglas que ni siquiera aplicaban a la versión del target se copiaban primero y se salteaban después. Como PDFFeatureRules se llena una vez en la inicialización de la unidad y se trata como de solo lectura, la v3.539.17 le pasa las entradas de la tabla directo a MatchSingleRule y RuleExceedsTarget, cuyos parámetros const Rule toman una referencia sin tocar los strings
// Antes: una copia de record administrado 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 place
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 angosto y conviene citarlo así. El benchmark chequea un array de 20,000 objetos numéricos contra un target 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 correr los dos builds en orden inverso dio 0.459 s contra 0.150 s. Eso es aproximadamente una ganancia de 3x solo en el camino de matcheo de reglas. Un guardado real también paga detección de features diferida, decodificación de objetos y serialización, así que el ratio no se traslada al tiempo total de guardado. El orden de las reglas, los callbacks, los umbrales de versión y el diagnóstico de primer fallo quedaron igual, y ninguna regla se cacheó entre guardados ni se salteó para lograrlo
¿Qué conviene revisar cuando un PDF cargado falla 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, ese era este falso positivo, y un build actual guarda el archivo sin cambios. Si la feature es genuina, declare la extensión o fije una versión que contenga honestamente el contenido; subir la versión solo para silenciar el gate esconde la pregunta de si los consumidores aguas abajo pueden leer lo que usted distribuye. El mismo principio de chequeos acotados y respaldados por evidencia maneja el preflight de modo autor PDF/E-1 para documentos de ingeniería, donde los dibujos CAD se enfrentan a un estándar de conformidad y no a un número de versión
Los chequeos de cumplimiento de versión, los diccionarios de medición y geoespaciales, y el fijado de versión de guardado son todos parte de PDF Library for Delphi, el toolkit PDFlibPas para desarrolladores Delphi, C++Builder y Lazarus