Artículo técnico

Diseño declarativo de PDF en Delphi con salida etiquetada

HotPDF puede construir un documento paginado a partir de un árbol declarativo en lugar de a partir de coordenadas. Se ensambla un THPDFDOMDocument a base de secciones, pilas, texto, listas y tablas, se entrega a THPDFDOMRenderer, y el renderizador mide, pagina, dibuja los elementos de página y, cuando se solicita, genera el árbol de estructura PDF/UA que hace accesible el resultado. El código de diseño nunca calcula una coordenada Y

Cualquiera que haya mantenido un generador de informes basado en coordenadas sabe por qué esto importa. La primera versión funciona. Luego la dirección de un cliente crece a tres líneas, una tabla gana filas, un encabezado localizado se ajusta, y cada posición Y posterior queda mal. Las correcciones se acumulan como comprobaciones manuales de salto de página dispersas por la lógica de negocio, y el requisito de PDF etiquetado que llega dos años más tarde no se puede incorporar retroactivamente a un código que no tiene ni idea de qué es un párrafo

Qué posee el árbol, y por qué la propiedad es estricta

El DOM impone una propiedad única en cada nivel: el documento posee sus secciones, una sección posee su cuerpo, cabecera y pie, y las pilas, los contenedores y las tablas poseen sus hijos. La reutilización se produce mediante Clone o mediante una fábrica registrada, nunca adjuntando el mismo objeto a dos padres. Esa regla no es mera formalidad. Un componente que aparezca dos veces en el árbol se mediría dos veces con restricciones distintas y se liberaría dos veces al desmontarlo

La consecuencia práctica para el código que llama es que los ayudantes devuelven instancias nuevas. Registrar una fábrica con RegisterComponent y llamar a CreateComponent proporciona una receta con nombre que produce un componente nuevo cada vez, que es la forma en que elementos repetidos como un bloque de firma o un pie de página legal encajan en el árbol

uses
  HPDFDoc, HPDFLayoutDOM;

var
  Doc: THPDFDOMDocument;
  Section: THPDFDOMSection;
  Table: THPDFDOMTable;
  Row: THPDFDOMTableRow;
  I: Integer;
begin
  Doc := THPDFDOMDocument.Create;
  Doc.GenerateStructure := True;        // emite el árbol de estructura PDF/UA
  Doc.Language := 'en-US';

  Section := Doc.AddSection;
  Section.PageWidth := 595;           // A4 en puntos
  Section.PageHeight := 842;
  Section.MarginLeft := 56;
  Section.MarginTop := 56;
  Section.MarginRight := 56;
  Section.MarginBottom := 56;
  Section.Style.FontName := 'Helvetica';
  Section.Style.FontSize := 10;

  Section.Body.AddHeading('Annual maintenance report', 1);
  Section.Body.AddText('Every asset inspected during the reporting ' +
    'period is listed below, grouped by site.');
  Section.Body.AddSpacer(12);

  Table := THPDFDOMTable.Create('assets');
  Table.AddColumn(3);                 // pesos, no anchos absolutos
  Table.AddColumn(1);
  Table.AddColumn(1);
  Table.RepeatHeaders := True;
  Row := Table.AddRow(18, True);      // fila de cabecera
  Row[0].Text := 'Asset';
  Row[1].Text := 'Last service';
  Row[2].Text := 'Status';
  for I := 0 to High(Assets) do
  begin
    Row := Table.AddRow(16);
    Row[0].Text := Assets[I].Name;
    Row[1].Text := Assets[I].ServiceDate;
    Row[2].Text := Assets[I].Status;
  end;
  Section.Body.Add(Table);
end;

¿Cómo evita la paginación un coste cuadrático?

La forma ingenua de paginar un árbol es clonar lo que no cupo y llevarlo a la página siguiente. En una tabla con diez mil filas, eso clona las filas restantes una vez por página y convierte un documento lineal en uno cuadrático

En lugar de eso, HotPDF divide de forma estrecha. El renderizador de nivel superior recorre los hijos del cuerpo por índice y nunca clona una sección o cuerpo entero. Solo las pilas y contenedores anidados que realmente cruzan un límite de página tienen su subárbol afectado clonado, y los dos tipos de hoja pesados llevan un cursor en lugar de una copia: una continuación de texto almacena el rango de caracteres de origen que todavía debe, y una continuación de tabla almacena el segmento de filas que aún no ha colocado. Los documentos largos se mantienen lineales, y los párrafos largos cuestan lo mismo tanto si se dividen una vez como cinco

La medición se mantiene honesta respecto a los efectos secundarios. Se exige que THPDFLayoutElement.Measure esté libre de efectos secundarios de dibujo, y la colocación real siempre pasa por THotPDF.PlaceLayoutElement, la misma rutina central que vuelve a medir el fragmento colocado, establece la propiedad del desbordamiento y registra diagnósticos. El renderizador DOM solo decide la política de página nueva, los elementos de página, el espaciado y el ciclo de vida de las continuaciones

Las reglas de cabecera de tabla que evitan un documento infinito

Repetir cabeceras de tabla entre páginas suena sencillo y oculta dos modos de fallo. HotPDF exige que las filas de cabecera aparezcan solo en la primera secuencia de filas consecutivas, y que la primera división quepa con todas las filas de cabecera más al menos una fila de cuerpo. Sin la segunda regla, una cabecera más alta que el espacio restante produce una página que no contiene más que la cabecera, seguida de otra página idéntica, indefinidamente

Las páginas de continuación vuelven a dibujar la cabecera, y esa copia redibujada se marca como artefacto en lugar de como contenido, que es la respuesta correcta tanto para la accesibilidad como para la extracción de texto. La fila de cabecera original permanece en la estructura lógica de la tabla exactamente una vez. Si se omite esto, un lector de pantalla anuncia de nuevo los títulos de columna en mitad de los datos, y un extractor de texto inserta una fila de cabecera duplicada entre las filas de cuerpo

También existe un límite defensivo en la profundidad de continuación, porque un componente personalizado es libre de implementar Split de una forma que siempre devuelva una cola equivalente. El renderizador comprueba el límite después de desprender la cola y antes de empezar la siguiente página, y la iteración actual libera la cola en su propio bloque finally, de modo que un componente de terceros mal comportado falla con un error diagnosticable en lugar de llenar un disco

Un elemento lógico, muchos fragmentos de página

El etiquetado automático es donde el modelo de paginación y el modelo de estructura tienen que ponerse de acuerdo. Un párrafo dividido entre dos páginas es un único párrafo lógico, así que debe seguir siendo un único elemento de estructura. Pero los identificadores de contenido marcado son por página, así que cada fragmento visible necesita su propio MCID en la página en la que aparece

HotPDF resuelve esto manteniendo un único elemento de estructura y añadiendo una referencia de contenido marcado a su array /K por cada fragmento, con el par /Pg y /MCID identificando la página y el identificador. La ranura del ParentTree para ese MCID vuelve a apuntar al mismo elemento. Esto es exactamente lo que exige la norma ISO 14289, y es el motivo por el que los clones de continuación son distintos de los clones ordinarios: un Clone ordinario significa contenido lógico nuevo y recibe una identidad semántica nueva, mientras que el clon de continuación interno hereda la identidad del componente que continúa

La reutilización de elementos se busca mediante un índice de identidades semánticas ordenado por puntero de componente y consultado por comparación binaria, lo que mantiene la búsqueda logarítmica en árboles grandes. El índice solo contiene referencias que no son propietarias; el ciclo de vida de los propios objetos de estructura permanece con el grafo de objetos del PDF

Reglas de estructura que el renderizador aplica de antemano

Con GenerateStructure activado, varias reglas de PDF/UA se comprueban mientras se renderiza el árbol, en lugar de después de que el archivo exista. Los encabezados empiezan en el nivel 1 y no pueden saltarse niveles. LI solo puede aparecer dentro de L, y Lbl y LBody solo dentro de LI. TR pertenece a una tabla, y TH y TD a una fila. Una figura sin texto alternativo se rechaza en modo PDF/UA

Rechazar de forma temprana es la decisión deliberada aquí. Un validador que informa de un texto alternativo ausente después de escribir el documento le dice que hay que regenerar un lote de diez mil extractos; un renderizador que rechaza el componente le dice cuál, mientras los datos que lo produjeron siguen en el ámbito. La verificación de conformidad sigue perteneciendo a la canalización como un paso independiente, y su mecánica se trata en la validación de PDF/A, PDF/X y PDF/UA

var
  Pdf: THotPDF;
  Renderer: THPDFDOMRenderer;
  Stats: THPDFDOMRenderStatistics;
begin
  Pdf := THotPDF.Create(nil);
  Renderer := THPDFDOMRenderer.Create;
  try
    Pdf.FileName := 'maintenance-report.pdf';
    Pdf.BeginDoc;
    Stats := Renderer.Render(Doc, Pdf);
    Pdf.EndDoc;

    Writeln(Format('%d page(s), %d placement(s), %d split(s)',
      [Stats.PageCount, Stats.PlacementCount, Stats.SplitCount]));
    Writeln(Format('structure elements=%d marked content=%d artifacts=%d',
      [Stats.StructureElementCount, Stats.MarkedContentCount,
       Stats.ArtifactCount]));
    Writeln(Format('deepest continuation chain: %d',
      [Stats.MaximumContinuationDepth]));
  finally
    Renderer.Free;
    Doc.Free;
    Pdf.Free;
  end;
end;

El registro de estadísticas es más útil de lo que parece a primera vista. Que SplitCount suba bruscamente tras un cambio de plantilla suele significar que un componente empezó a medir más alto que su contenedor. Que MaximumContinuationDepth aumente poco a poco es el aviso temprano de un componente cuyo Split avanza demasiado poco por página. Y comparar ArtifactCount con el número de páginas de continuación confirma que las cabeceras repetidas realmente se etiquetaron como artefactos

Dónde encaja el DOM junto a la API directa

El DOM no sustituye al dibujo directo; se sitúa encima de los mismos objetos de página. Cualquier cosa que coloque el renderizador se puede intercalar con llamadas directas sobre THotPDF, lo cual importa cuando un informe necesita un elemento colocado manualmente, como una imagen de firma en una ubicación exacta. El cierre de página sigue bajo el control de AddPage y EndDoc, así que el modo de vaciado inmediato no mantiene ninguna página completada en memoria y la memoria residente sigue gobernada por las continuaciones actuales, los recursos de fuente y el grafo de objetos de documento habitual

Elija el DOM cuando el contenido está impulsado por datos y el diseño está impulsado por reglas, y conserve el dibujo directo para material gráfico fijo. Si su problema actual es específicamente la paginación de tablas, merece la pena leer primero el enfoque más acotado de generar tablas en PDF, y el comportamiento a nivel de texto como la justificación se describe en la justificación de texto

El diseño declarativo, el etiquetado automático y la API de dibujo directo se incluyen en el mismo componente para Delphi y C++Builder; la lista completa de funciones está en la página del componente PDF para Delphi de HotPDF