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 con secciones, pilas, texto, listas y tablas, se entrega a THPDFDOMRenderer, y el renderizador mide, pagina, dibuja el mobiliario de página y, cuando se le solicita, emite 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 una dirección de cliente crece a tres líneas, una tabla gana filas, un encabezado localizado se ajusta, y cada posición y posterior queda equivocada. Las correcciones se acumulan como verificaciones manuales de salto de página dispersas por la lógica de negocio, y el requisito de PDF etiquetado que llega dos años después no se puede incorporar de forma retroactiva a un código que no tiene idea de qué es un párrafo

¿Qué posee el árbol, y por qué esa posesión es estricta?

El DOM impone una propiedad única en cada nivel: el documento posee sus secciones, una sección posee su cuerpo, encabezado y pie de página, y las pilas, contenedores y tablas poseen a sus hijos. La reutilización ocurre 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 apareciera 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 da una receta con nombre que produce un componente nuevo cada vez, que es como debe incluirse en el árbol un mobiliario repetido, como un bloque de firma o un pie de página legal

uses
  HPDFDoc, HPDFLayoutDOM;

var
  Doc: THPDFDOMDocument;
  Section: THPDFDOMSection;
  Table: THPDFDOMTable;
  Row: THPDFDOMTableRow;
  I: Integer;
begin
  Doc := THPDFDOMDocument.Create;
  Doc.GenerateStructure := True;        // emitir 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 encabezado
  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 costo 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

HotPDF divide de forma más acotada. El renderizador de nivel superior recorre los hijos del cuerpo por índice y nunca clona una sección o un cuerpo completos. 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 aún debe, y una continuación de tabla almacena el segmento de filas que todavía tiene que colocar. Los documentos largos se mantienen lineales, y los párrafos largos cuestan lo mismo ya sea que se corten una vez o 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, configura la propiedad de desbordamiento y registra diagnósticos. El renderizador DOM decide únicamente la política de página nueva, el mobiliario de página, el espaciado y el ciclo de vida de las continuaciones

Las reglas de encabezado de tabla que evitan un documento infinito

Repetir encabezados de tabla a través de las páginas suena simple y oculta dos modos de fallo. HotPDF exige que las filas de encabezado aparezcan solo en la primera serie de filas consecutivas, y que la primera división ajuste todas las filas de encabezado más al menos una fila de cuerpo. Sin esta segunda regla, un encabezado más alto que el espacio restante produce una página que no contiene nada más que el encabezado, seguida de otra página idéntica, indefinidamente

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

También existe un tope defensivo sobre 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 verifica el límite después de separar la cola y antes de comenzar la página siguiente, y la iteración actual libera la cola en su propio bloque finally, de modo que un componente de terceros que se comporte mal falle 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 solo párrafo lógico, así que debe seguir siendo un solo 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 solo elemento de estructura y agregando una referencia de contenido marcado a su arreglo /K por cada fragmento, con el par /Pg y /MCID identificando la página y el identificador. La ranura del ParentTree para ese MCID apunta de vuelta al mismo elemento. Esto es exactamente lo que exige la norma ISO 14289, y es la razón por la 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 a través de un índice de identidades semánticas ordenado por puntero de componente y consultado mediante comparación binaria, lo que mantiene la búsqueda logarítmica en árboles grandes. El índice conserva solo referencias no 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 verifican mientras el árbol se está renderizando, en lugar de después de que el archivo ya existe. Los encabezados comienzan en el nivel 1 y no pueden saltar 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 reporta un texto alternativo faltante después de que el documento ya se escribió te dice que un lote de diez mil estados de cuenta necesita regenerarse; un renderizador que rechaza el componente te dice cuál componente, mientras los datos que lo produjeron todavía están en alcance. La verificación de conformidad sigue perteneciendo a la canalización como un paso aparte, y la mecánica de eso se cubre en 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. Un SplitCount que aumenta bruscamente después de un cambio de plantilla suele significar que un componente empezó a medir más alto que su contenedor. MaximumContinuationDepth subiendo poco a poco es la advertencia temprana de un componente cuyo Split avanza muy poco por página. Y comparar ArtifactCount con el número de páginas de continuación confirma que los encabezados repetidos realmente se etiquetaron como artefactos

Dónde encaja el DOM junto a la API directa

El DOM no reemplaza el dibujo directo; se apoya sobre los mismos objetos de página. Cualquier cosa que coloque el renderizador puede intercalarse con llamadas directas sobre THotPDF, lo cual importa cuando un informe necesita un elemento posicionado a mano, 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 páginas terminadas en memoria y la memoria residente sigue gobernada por las continuaciones actuales, los recursos de fuente y el grafo de objetos del documento ordinario

Elige el DOM cuando el contenido está guiado por datos y el diseño está guiado por reglas, y conserva el dibujo directo para las piezas gráficas fijas. Si tu problema actual es específicamente la paginación de tablas, vale la pena leer primero el enfoque más acotado en generar tablas en PDF, y el comportamiento a nivel de texto, como la justificación, se describe en justificación de texto

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