Un plano de planta exportado por un paquete CAD de arquitectura lleva típicamente un array /VP en su primera página con dos viewports, y cada viewport lleva una entrada /Measure para que un visor pueda leer metros directamente del plano. Una hoja de mapa GeoPDF tiene el mismo array /VP y la misma clave /Measure. El plano de planta es PDF 1.6 corriente. La hoja de mapa necesita PDF 1.7 más Adobe Extension Level 3. Una comprobación de versión que se detiene en el nombre de la clave exigirá la extensión para ambos, y al plano de planta lo rechazan al guardar con un mensaje sobre contenido geoespacial que nunca contuvo
Una clave /Measure, dos diccionarios distintos
El nombre de la clave te dice que un viewport es medible; solo el diccionario detrás te dice cómo. Los diccionarios Measure llegaron en PDF 1.6, e ISO 32000-1 §12.9 define el tipo rectilíneo: una cadena de razón de escala en /R, arrays de formato numérico en /X y /Y, y arrays adicionales para distancia (/D) y área (/A). La medición geoespacial llegó después, como parte de Adobe Extension Level 3 encima de PDF 1.7. Se marca con /Subtype /GEO y lleva un sistema de coordenadas geográfico en /GCS, un sistema de coordenadas de visualización opcional en /DCS, arrays de puntos emparejados en /GPTS y /LPTS, y unidades de visualización preferidas en /PDU — las estructuras que recorre leer viewports GeoPDF y arrays GPTS y LPTS en Delphi
El detalle donde la gente tropieza es el valor por defecto. La §12.9 da a /Subtype un valor por defecto de RL, y muchos exportadores CAD se aprovechan de eso y dejan la entrada fuera. Un comprobador que lee un subtype ausente como "desconocido, asume lo peor" convierte cada uno de esos planos en un documento geoespacial. Un comprobador que empareja por la clave /Measure nunca llega siquiera a la pregunta del subtype, porque el diccionario de viewport que sostiene la clave está un nivel por encima del diccionario Measure que la responde
Cómo llega a un veredicto el preflight de guardado de PDF Library for Delphi
PDF Library for Delphi (PDFlibPas) ejecuta un preflight de versión dentro de SaveToFile antes de escribir ninguna salida. Visita cada objeto indirecto, descendiendo en diccionarios y arrays anidados, y prueba cada nodo contra una tabla de reglas de features. Cada regla nombra una versión mínima de PDF y, para features de extensión, un extension level de Adobe; una regla de extensión solo puede satisfacerla PDF 1.7 junto a una entrada /Extensions /ADBE coincidente en el catálogo. El objetivo contra el que comprueba es la versión del documento, o la versión que fijaste con LockSaveVersion, más el extension level que el catálogo declare. La primera regla que a la vez coincide y excede ese objetivo termina el guardado: SaveToFile devuelve 0 y LastErrorCode pasa a 602, el valor de PDFLIB_ERROR_VERSION_COMPLIANCE
Un código de error por sí solo no te dice qué arreglar. Tres claves de GetInformation llevan el resto: 311 es la versión que necesita la feature problemática (por ejemplo 1.7 ExtensionLevel 3), 312 es el nombre de la feature, y 313 es la versión a la que está fijado el objetivo de guardado, vacía cuando nada está fijado
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;
Para contenido de medición la tabla sostiene tres reglas y deliberadamente ninguna regla sobre la clave /Measure desnuda. Una página con /VP o /UserUnit necesita PDF 1.6. Un diccionario /PtData necesita extension level 3. Un diccionario Measure necesita extension level 3 cuando dice /Subtype /GEO o contiene cualquiera de /GCS, /DCS, /GPTS, /LPTS o /PDU. Así un viewport cuyo diccionario Measure omite /Subtype y otro que deletrea /RL pasan ambos a 1.6, mientras que la misma página a 1.5 se rechaza por el /VP. Añade un array /GPTS y la respuesta pasa a ser 1.7 ExtensionLevel 3
El test de contenido es conservador a propósito. Un diccionario rectilíneo que además lleva un /GCS o /PDU perdido se trata como geoespacial. Preferimos rechazar ese archivo a dejarlo pasar: esas claves no significan nada en el modelo rectilíneo, así que quien las escribió era o una herramienta geoespacial o algo que dañó el archivo, y un visor consciente de GEO bien podría intentar interpretarlas. El cargador es igual de cuidadoso en la otra dirección. Su escaneo de features puede 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 — declarar una extensión es una afirmación sobre el contenido, y solo tu código o las llamadas de autoría deberían hacerla
Comprobar los viewports de un plano antes de guardar
Cuando 312 nombra una feature geoespacial y estás seguro de que el archivo es un plano corriente, mira tú los diccionarios Measure. Las llamadas públicas de viewport bastan. GetMeasureDictCoordinateSystem lee /Subtype y devuelve 1 para RL, 2 para GEO, y 0 cuando la entrada está ausente — lo que según la §12.9 también significa rectilíneo. Los recuentos de arrays de puntos te dicen si hay datos de georreferenciación
// Reporta cada diccionario Measure del documento cargado.
// Todas las llamadas aquí solo leen; ninguna añade claves al archivo.
procedure ReportMeasureDicts(Pdf: TPDFlib);
var
Page, VP, VPID, MD, Kind: Integer;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.SelectPage(Page);
for VP := 1 to Pdf.GetPageViewPortCount do
begin
VPID := Pdf.GetPageViewPortID(VP);
MD := Pdf.GetViewPortMeasureDict(VPID);
if MD = 0 then
Continue;
Kind := Pdf.GetMeasureDictCoordinateSystem(MD); // 0 = omitido (RL), 1 = RL, 2 = GEO
Writeln(Format('page %d viewport "%s": subtype %d, GPTS %d, LPTS %d',
[Page, string(Pdf.GetViewPortName(VPID)), Kind,
Pdf.GetMeasureDictGPTSCount(MD), Pdf.GetMeasureDictLPTSCount(MD)]));
end;
end;
end;
Dos llamadas faltan en ese bucle a propósito: GetMeasureDictGCSDict y GetMeasureDictDCSDict. Esas dos son accesores get-or-create construidos para autoría. Sobre un diccionario Measure sin entrada /GCS, GetMeasureDictGCSDict añade uno vacío y declara extension level 3 para el documento. Llámalo como sonda sobre un plano rectilíneo y el diccionario pasa a contener genuinamente /GCS; un guardado posterior fijado a 1.6 se rechaza, correctamente, por contenido que tu diagnóstico acaba de escribir. Si necesitas saber si existe un sistema de coordenadas, el subtype y los recuentos de arrays de puntos lo responden sin efectos secundarios
Fijar la salida a PDF 1.6
Una versión fijada mantiene honesta la salida cuando quien la consume tiene un techo. TPDFlib.LockSaveVersion acepta las cadenas '1.0' a '1.7' y devuelve 0 para cualquier otra cosa. Fija la versión del documento y evita que las llamadas del lado escritor la suban por su cuenta, pero el preflight de guardado sigue ejecutándose contra el valor fijado. Un plano CAD fijado a 1.6 se guarda limpio. Un GeoPDF de verdad fijado a 1.6 recibe 602, y esa es la respuesta que quieres, porque la alternativa es un archivo cuya cabecera promete menos de lo que su contenido requiere
if Pdf.LockSaveVersion('1.6') <> 1 then
raise Exception.Create('unsupported version string');
try
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
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))]);
finally
Pdf.UnlockSaveVersion;
end;
Lee 311 y 312 antes de tocar la versión. Si la feature es contenido geoespacial genuino, tienes dos opciones honestas: dejar el documento sin fijar a 1.7 con la extensión declarada, o quitar los datos geoespaciales de la copia que envías a quien solo consume 1.6. Subir el fijado solo para que el error desaparezca es la opción a evitar. Esconde la pregunta real — ¿puede el lector aguas abajo manejar lo que estás enviando? Cuando construyes contenido geoespacial por la API, los setters SetMeasureDict* como SetMeasureDictCoordinateSystem y SetMeasureDictGPTSCount declaran ellos mismos extension level 3, así que los archivos escritos llegan al preflight ya etiquetados. El mismo hábito de comprobaciones acotadas y basadas en evidencia aparece en el preflight de modo autor PDF/E-1 para documentos de ingeniería, donde los planos CAD se enfrentan a un estándar de conformidad en lugar de a un número de versión
Por qué el escaneo de reglas lee la tabla in situ
El coste del preflight es aproximadamente nodos de objeto por reglas, y el recuento de nodos incluye cada número y nombre dentro de cada array. Eso hace que la sobrecarga por regla importe más de lo que parece. Cada regla de la tabla es un registro con dos campos AnsiString, la cadena de emparejamiento y el nombre de la feature. Copiar ese registro a una variable local hace que Delphi o FPC incrementen dos reference counts y liberen lo que la local sostenía antes. Hecho una vez por regla y por nodo, eso significa pagar la contabilidad de cadenas incluso para reglas que no aplican a la versión objetivo y se descartan en la línea siguiente
La tabla de reglas se llena una vez en la inicialización de la unidad y nunca se modifica después, así que el escaneo entrega las entradas de la tabla a su código de emparejamiento mediante parámetros de registro const y las lee donde viven. Un parámetro de registro const se pasa por referencia, así que probar una regla no toca ninguno de los dos campos de cadena y los reference counts se quedan donde estaban. El efecto se concentra en el camino de emparejamiento de reglas. Un guardado real también decodifica objetos, ejecuta la detección de features diferida y serializa la salida, y esas etapas no las toca cómo se pasan las reglas. El orden de reglas, los umbrales y el diagnóstico de primer fallo en 311 y 312 no dependen de esto, y ningún resultado de regla se cachea entre guardados, así que un documento que editas y guardas dos veces se comprueba entero las dos veces
El mismo patrón merece revisarse en tu propio código Delphi dondequiera que un bucle recorre una tabla de registros que contienen cadenas, interfaces o arrays dinámicos:
type
TMyRule = record
Name: string; // campo gestionado: las copias cuestan trabajo de reference count
Threshold: Integer;
end;
// código propio del lector, mostrado como patrón
function Applies(const Rule: TMyRule; Value: Integer): Boolean;
begin
Result := Value >= Rule.Threshold; // const: sin copia, sin tráfico de refcount
end;
// En el bucle caliente, pasa la entrada de la tabla directamente en lugar
// de asignarla primero a un registro local
for I := 0 to High(Rules) do
if Applies(Rules[I], V) then
Inc(Hits);
Viewports de medición, diccionarios geoespaciales, fijado de versión y la comprobación de conformidad al guardar viajan todos en PDF Library for Delphi, disponible para proyectos Delphi, C++Builder y Lazarus