Artículo técnico

Preflight de versión PDF en Delphi: reglas Measure RL vs GEO

Un plano de planta exportado por un paquete CAD de arquitectura típicamente tiene un arreglo /VP en su primera página con dos viewports, y cada viewport trae una entrada /Measure para que un visor pueda leer metros directo del dibujo. Una hoja de mapa GeoPDF tiene el mismo arreglo /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 va a exigir la extensión para ambos, y el plano de planta queda rechazado al guardar con un mensaje sobre contenido geoespacial que nunca contuvo

Una clave /Measure, dos diccionarios distintos

El nombre de la clave le dice que un viewport es medible; solo el diccionario detrás le 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, arreglos de formato numérico en /X y /Y, y más arreglos para distancia (/D) y área (/A). La medición geoespacial llegó después, como parte de Adobe Extension Level 3 sobre PDF 1.7. Se marca con /Subtype /GEO y trae un sistema de coordenadas geográfico en /GCS, un sistema de coordenadas de visualización opcional en /DCS, arreglos de puntos emparejados en /GPTS y /LPTS, y unidades de visualización preferidas en /PDU — las estructuras que recorre leer viewports GeoPDF y arreglos GPTS y LPTS en Delphi

El detalle que hace tropezar a la gente es el valor por omisión. §12.9 le da a /Subtype un valor por omisión de RL, y muchos exportadores CAD se aprovechan de eso y dejan la entrada afuera. Un verificador que lee un subtype faltante como "desconocido, asuma lo peor" convierte cada uno de esos dibujos en un documento geoespacial. Un verificador que actúa por la clave /Measure nunca llega siquiera a la pregunta del subtype, porque el diccionario de viewport que sostiene la clave está un nivel arriba del diccionario measure que la responde

Una clave /Measure, dos diccionarios en PDFlibPas: la medición rectilínea, con /RL o el subtype omitido, solo necesita PDF 1.6, mientras un diccionario geoespacial necesita 1.7 ExtensionLevel 3, así que el preflight decide por el contenido del diccionario measure donde una regla a nivel de clave no puede distinguirlos
Un preflight que bloquea un archivo válido es peor que uno lento, porque quien llama recibe un diagnóstico autoritario sobre una feature que el documento nunca contuvo, y por eso la comprobación distinguidora está un nivel más abajo

Cómo llega el preflight de guardado de PDF Library for Delphi a un veredicto

PDF Library for Delphi (PDFlibPas) corre un preflight de versión dentro de SaveToFile antes de que se escriba cualquier salida. Visita cada objeto indirecto, descendiendo en diccionarios y arreglos 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 satisfacerse con PDF 1.7 junto con una entrada /Extensions /ADBE coincidente en el catálogo. El objetivo contra el que verifica es la versión del documento, o la versión que usted fijó 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 se vuelve 602, el valor de PDFLIB_ERROR_VERSION_COMPLIANCE

Preflight de versión al guardar en PDFlibPas: SaveToFile verifica cada objeto contra una tabla fija de reglas de features, cada una con una versión mínima de PDF y, para features de extensión, un extension level de Adobe que solo PDF 1.7 con una entrada /Extensions coincidente puede satisfacer, y la primera coincidencia que el objetivo no puede satisfacer detiene el guardado con error 602
Las claves 311, 312 y 313 de GetInformation convierten un rechazo en un diagnóstico: reportan la versión requerida, la feature que la disparó y el target fijado, así que quien llama puede arreglar el archivo o la regla en lugar de adivinar

Un código de error solo no le dice qué arreglar. Tres claves de GetInformation llevan el resto: 311 es la versión que la feature infractora necesita (por ejemplo 1.7 ExtensionLevel 3), 312 es el nombre de la feature, y 313 es la versión a la que el objetivo de guardado está fijado, 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á 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;

Para contenido de medición la tabla guarda 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 uno que deletrea /RL pasan ambos en 1.6, mientras la misma página en 1.5 queda rechazada por /VP. Agregue un arreglo /GPTS y la respuesta se vuelve 1.7 ExtensionLevel 3

La prueba de contenido es conservadora a propósito. Un diccionario rectilíneo que además trae un /GCS o /PDU perdido se trata como geoespacial. Preferimos rechazar ese archivo antes que dejarlo pasar: esas claves no significan nada en el modelo rectilíneo, así que quien las escribió era 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 su código o las llamadas de autoría deberían hacerla

Revisar los viewports de un dibujo antes de guardar

Cuando 312 nombra una feature geoespacial y usted está seguro de que el archivo es un dibujo plano, mire usted mismo los diccionarios measure. Las llamadas públicas de viewport alcanzan. GetMeasureDictCoordinateSystem lee /Subtype y devuelve 1 para RL, 2 para GEO, y 0 cuando la entrada está ausente — lo que según §12.9 también significa rectilíneo. Los conteos de arreglos de puntos le dicen si hay datos de georreferenciación presentes

// Reporte cada diccionario measure del documento cargado.
// Todas las llamadas aquí solo leen; ninguna agrega 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 agrega uno vacío y declara extension level 3 para el documento. Llámelo como sondeo sobre un plano rectilíneo y el diccionario ahora genuinamente contiene /GCS; un guardado siguiente fijado en 1.6 queda rechazado, correctamente, por contenido que su diagnóstico acaba de escribir. Si necesita saber si existe un sistema de coordenadas, el subtype y los conteos de arreglos de puntos responden eso sin efectos secundarios

Fijar la salida en PDF 1.6

Una versión fijada mantiene honesta la salida cuando sus consumidores tienen 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 igual corre contra el valor fijado. Un dibujo CAD fijado en 1.6 guarda limpio. Un GeoPDF de verdad fijado en 1.6 recibe 602, y esa es la respuesta que usted quiere, porque la alternativa es un archivo cuyo encabezado 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;

Lea 311 y 312 antes de tocar la versión. Si la feature es contenido geoespacial genuino, tiene dos opciones honestas: dejar el documento sin fijar en 1.7 con la extensión declarada, o quitar los datos geoespaciales de la copia que envía a un consumidor que solo llega a 1.6. Subir el bloqueo solo para que el error desaparezca es la opción a evitar. Esconde la pregunta real — ¿puede el lector aguas abajo manejar lo que usted está enviando? Cuando usted construye contenido geoespacial vía la API, los setters SetMeasureDict* como SetMeasureDictCoordinateSystem y SetMeasureDictGPTSCount declaran extension level 3 por sí mismos, así que los archivos autorizados llegan al preflight ya etiquetados. El mismo hábito de comprobaciones acotadas y respaldadas por evidencia aparece en el preflight de modo autor de PDF/E-1 para documentos de ingeniería, donde los dibujos CAD se enfrentan a un estándar de conformidad en vez de a un número de versión

Por qué el escaneo de reglas lee la tabla en el sitio

El costo del preflight es aproximadamente nodos de objeto por reglas, y el conteo de nodos incluye cada número y nombre dentro de cada arreglo. Eso hace que el overhead por regla importe más de lo que parece. Cada regla de la tabla es un record con dos campos AnsiString, la cadena de coincidencia y el nombre de la feature. Copiar ese record a una variable local hace que Delphi o FPC incremente dos reference counts y libere lo que la local sostenía antes. Hecho una vez por regla por nodo, 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

Escaneo de reglas en PDFlibPas: copiar un record de regla que sostiene campos de cadena ajusta reference counts en cada visita, mientras pasar la entrada de tabla de solo lectura como parámetro const la lee en el sitio sin tocar las cadenas
Leer la tabla de solo lectura en el sitio quita el trabajo de reference counting de la ruta de emparejamiento de reglas, mientras un guardado completo igual paga por detección de features, decodificación de objetos y serialización

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 tabla a su código de coincidencia mediante parámetros record const y las lee donde viven. Un parámetro record 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 la ruta de emparejamiento de reglas. Un guardado real también decodifica objetos, corre detección de features diferida y serializa la salida, y esas etapas no se ven afectadas por 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 usted edita y guarda dos veces se verifica por completo ambas veces

El mismo patrón vale la pena revisar en su propio código Delphi dondequiera que un bucle recorre una tabla de records que contienen cadenas, interfaces o arreglos dinámicos:

type
  TMyRule = record
    Name: string;      // campo administrado: las copias cuestan trabajo de reference counting
    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, pase la entrada de tabla directamente en vez de
// asignarla primero a un record local
for I := 0 to High(Rules) do
  if Applies(Rules[I], V) then
    Inc(Hits);

Viewports de medición, diccionarios geoespaciales, fijación de versión y la comprobación de cumplimiento al guardar vienen todos en PDF Library for Delphi, disponible para proyectos Delphi, C++Builder y Lazarus