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