Un jeu de données est composé de lignes et de colonnes ; une page PDF est une grille de coordonnées vierge dépourvue de ces deux concepts. Combler cet écart est tout le travail requis ici. Il n'y a pas d'appel DrawTable dans HotPDF acceptant un jeu de données et renvoyant une grille formatée. Ce que vous obtenez sont les éléments de base constituant la grille : TextOut pour placer une chaîne à un point donné, SetFont pour sélectionner la police, Rectangle et Fill pour colorer les bandes, et MoveTo/LineTo/Stroke pour tracer les lignes. Un exportateur de tableau utilisable consiste à traduire la pensée en lignes et colonnes en coordonnées x et y explicites, puis à maintenir la validité de ces coordonnées lorsque les données dépassent le bas de la page
Le modèle ci-dessous montre un scénario plus réaliste : générer un court rapport avec une zone de titre, du texte de corps et une ligne de lien en bas à partir du code plutôt que d'un formulaire contenant des champs TEdit :
Priorité à la géométrie de colonne
Déterminez la position de chaque colonne avant de dessiner le moindre caractère. Un tableau comporte ici quatre colonnes, ce qui nécessite quatre bords gauches et une marge droite connue. Coder en dur un nombre magique à chaque appel TextOut — comme le font souvent les exemples rapides — est précisément la cause des difficultés rencontrées lors de l'élargissement ultérieur du tableau. Nommez les bords une fois pour toutes en points depuis l'origine inférieure gauche, et chaque appel de dessin s'y référera par son nom :
const
ColNo = 70; // left edge of the "No." column
ColName = 110; // company name
ColAddr = 300; // street address
ColCity = 480; // city
RowLeft = 50; // table frame: left rule
RowRight = 570; // table frame: right rule
RowStep = 20; // vertical distance between baselines
procedure PrintRow(Page: THPDFPage; Y: Single;
const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
if Shaded then
begin
// A shaded band behind the row. Rectangle takes X, Y, Width, Height.
Page.SetRGBFillColor($00FFF3DD);
Page.Rectangle(RowLeft, Y - 4, RowRight - RowLeft, RowStep);
Page.Fill;
Page.SetRGBFillColor(clBlack);
end;
Page.TextOut(ColNo, Y, 0, ANo);
Page.TextOut(ColName, Y, 0, AName);
Page.TextOut(ColAddr, Y, 0, AAddr);
Page.TextOut(ColCity, Y, 0, ACity);
end;
Deux détails sont utiles ici. Les bandes de couleur sont dessinées en premier, puis le texte est tracé par-dessus, car l'ordre de dessin dans le PDF correspond à l'ordre sur l'axe Z : remplir un rectangle après le texte recouvrirait le contenu de la ligne. De plus, l'ombrage alterné n'est pas qu'une simple décoration. Dans les rapports denses, c'est le moyen le plus économique d'éviter que l'œil ne glisse sur la mauvaise ligne, c'est pourquoi la boucle bascule une variable booléenne à chaque ligne et la transmet directement à Shaded
Chacun des huit types ci-dessus peut apparaître de deux manières. Les objets directs sont écrits sur place, comme la valeur 612 dans le tableau MediaBox. Les objets indirects reçoivent une identité permettant à d'autres objets de pointer vers eux : deux entiers — le numéro d'objet et le numéro de génération — définissent l'enveloppement entre obj et endobj :
En-têtes, lignes et l'unique emplacement qui les détient
Une page A4 rendue sous forme de bitmap 32 bits à un niveau de zoom confortable pour la lecture représente environ plusieurs mégaoctets. Multipliez cela par un contrat de 400 pages, et ce nombre n'est plus abstrait : effectuer le rendu préalable de toutes les pages signifie demander à Windows une mémoire bitmap bien supérieure à 1 Go, alors que l'utilisateur ne consultera qu'un seul écran de contenu à la fois. L'application risque soit d'épuiser l'espace d'adressage dans un build 32 bits, soit de se bloquer complètement dans les premières secondes pendant que le GPU et l'analyseur de pages traitent des pages que personne ne fera jamais défiler. Un lecteur à défilement continu doit se présenter visuellement comme une longue bande de pages, mais en réalité, il est impossible de conserver toutes les pages en mémoire simultanément
procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
// Left: source label and page number. Right: generation time.
Page.SetFont('Arial', [fsItalic], 10);
Page.TextOut(RowLeft, Y, 0, 'customer.db Page ' + IntToStr(PageNo));
Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));
// Two horizontal rules that box the column titles.
Page.MoveTo(RowLeft, Y + 15);
Page.LineTo(RowRight, Y + 15);
Page.MoveTo(RowLeft, Y + 45);
Page.LineTo(RowRight, Y + 45);
Page.Stroke;
// The column titles, in a heavier face so they read as headings.
Page.SetFont('Times New Roman', [fsBold], 12);
Page.SetRGBFillColor(clNavy);
PrintRow(Page, Y + 25, 'No.', 'Company', 'Address', 'City', False);
Page.SetRGBFillColor(clBlack);
Y := Y + RowStep + 45; // advance past the boxed header before the first body row
end;
Notez que DrawHeader transmet par référence et le déplace vers l'avant. L'appelant n'a jamais besoin de se souvenir de la hauteur de l'en-tête ; la routine qui le dessine est celle qui détient cette information. Cette règle de propriété unique garantit que la mise en page ne dérive pas lorsque vous ajoutez ultérieurement un logo ou un résumé de filtre à l'en-tête. La boucle principale l'ignore, se contentant de dessiner les lignes à partir de l'endroit où Y pointe actuellementY
Le composant expose ces propriétés via les mêmes objets TPdf et TPdfView que vous utilisez pour le rendu et l'extraction de texte, sans nécessiter l'ouverture distincte d'un objet "table des polices". Dès que le texte d'une page est analysé, les propriétés de police se rattachent aux index de caractères et peuvent être lues glyphe par glyphe. Cette conception correspond à la façon dont le PDF stocke les informations : une seule page pouvant changer de police des dizaines de fois, la seule réponse honnête à "quelles polices sont utilisées dans ce document" est "cela dépend du caractère concerné"
La boucle de curseur détient la logique de pagination
Positionnement absolu : placer du texte à des coordonnées spécifiques
var
Pdf: THotPDF;
Page: THPDFPage;
Y: Single;
PageNo: Integer;
Shaded: boolean;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'CustomerReport.pdf';
Pdf.BeginDoc;
Page := Pdf.CurrentPage;
// Report title, once, at the top of the first page.
Page.SetFont('Arial', [fsBold], 24);
Page.TextOut(200, 800, 0, 'Customer Report');
PageNo := 1;
Y := 760;
DrawHeader(Page, Y, PageNo);
Shaded := False;
CustomerTable.First;
while not CustomerTable.Eof do
begin
// Out of room? Open a new page and repeat the header there.
if Y < 60 then
begin
Pdf.AddPage;
Page := Pdf.CurrentPage; // AddPage moves CurrentPage forward
Inc(PageNo);
Y := 760;
DrawHeader(Page, Y, PageNo);
end;
Shaded := not Shaded;
Page.SetFont('Arial', [], 10); // SetFont must be reissued on every new page
PrintRow(Page, Y,
VarToStr(CustomerTable['CustNo']),
VarToStr(CustomerTable['Company']),
VarToStr(CustomerTable['Addr1']),
VarToStr(CustomerTable['City']),
Shaded);
Y := Y - RowStep;
CustomerTable.Next;
end;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Deux instances de TPdf suffisent. L'une contient le document cible, créé vide via CreateDocument ; l'autre ouvre successivement chaque fichier source. Voici une procédure qui reçoit une liste de chemins de fichiers et écrit le résultat fusionné dans un seul chemin :
Un autre fait sur lequel presque tout le monde trébuche une fois : crée une nouvelle page et redirige AddPageCurrentPage vers elle, mais elle n'emporte aucun contenu : aucune police, aucune couleur de remplissage, aucune position. C'est pourquoi il faut relire Page après chaque AddPage, et pourquoi SetFont doit être réappelé avant la ligne principale. Omettez la relecture, et vous continuerez à dessiner sur la page que vous venez de quitter ; omettez la police, et la nouvelle page effectuera le rendu avec la police par défaut du visualiseur
Ce qui casse les exportateurs de tableaux
La plupart des bogues de tableaux ne se produisent pas sur le chemin normal de quelques lignes bien rangées. Ils existent aux limites, et les limites sont faciles à tester une fois que l'on sait où elles se trouvent
- Jeu de données vide : boucler sur zéro ligne produit une page contenant uniquement l'en-tête, ce qui semble au moins intentionnel. Une page blanche sans en-tête ressemble à un échec. Décidez de ce que vous souhaitez avant la publication
- Lignes tombant exactement sur la limite : générez un rapport dont la dernière ligne se trouve exactement un pas au-dessus de la marge, puis un autre rapport dont la ligne suivante se trouve exactement un pas en dessous. Un défaut de pagination d'une ligne reste caché jusqu'à ce que les données atteignent précisément la longueur critique
- Valeurs trop longues : un nom d'entreprise plus large que la colonne débordera sur la colonne suivante. Mesurez les champs et choisissez une stratégie : retour à la ligne sur une deuxième ligne, rognage ou troncature avec points de suspension. Le silence n'est pas une stratégie
- Champs vides : lire directement une valeur nulle dans
TextOutpeut afficher le texte littéralNullou un espace blanc, selon la manière dont vous le convertissez. Choisissez délibérément le mode de rendu plutôt que de laisser le variant décider à votre place
Avant de déclarer le travail terminé, exécutez le résultat dans plusieurs visualiseurs. Les comportements de substitution de polices et de rognage varient selon les moteurs de rendu, et un tableau propre dans un lecteur PDF peut afficher des colonnes mal alignées ou des textes tronqués dans un autre. Confirmez que la répétition des en-têtes, l'ombrage des lignes et les marges persistent après le franchissement de la limite de page, et que la numérotation reste continue lorsque les données débordent
Dessiner soi-même la grille au lieu de s'appuyer sur un concepteur de rapports visuels nécessite plus de code, mais le compromis mérite d'être explicité : vous contrôlez chaque coordonnée, ce qui est précisément requis pour les tâches par lots sur serveur, les factures et les exports d'audit — qui doivent s'afficher de manière identique sur chaque machine —, tandis que pour un simple listing interne, c'est un coût que vous préféreriez éviter. Pour les premiers, dès lors que le rapport en production doit correspondre exactement à ce que vous voyez sur votre bureau, le contrôle absolu en vaut la peine
La version assembleur ci-dessus ne vaut la peine d'être écrite que si vous avez un réel besoin de compatibilité binaire : un appelant externe attend le symbole _ftol avec une convention d'appel spécifique, et vous ne pouvez pas modifier ces appelants. Cette situation n'est pas courante. La plupart du temps, _ftol est simplement une fonction d'aide privée utilisée au sein de la même unité, sans aucune dépendance externe sur son nom ou sa convention