HotPDF kan bygga ett paginerat dokument från ett deklarativt träd istället för från koordinater. Du sätter samman ett THPDFDOMDocument av sektioner, staplar, text, listor och tabeller, lämnar det till THPDFDOMRenderer, och renderaren mäter, paginerar, ritar sidmöbler och, när det begärs, skriver ut det PDF/UA-strukturträd som gör resultatet tillgängligt. Layoutkoden beräknar aldrig en y-koordinat
Alla som underhållit en koordinatstyrd rapportgenerator vet varför detta spelar roll. Den första versionen fungerar. Sedan växer en kundadress till tre rader, en tabell får fler rader, en lokaliserad rubrik radbryts, och varje nedströms y-position blir fel. Fixarna hopar sig som manuella sidbrytningskontroller utspridda genom affärslogiken, och kravet på taggad PDF som dyker upp två år senare kan inte eftermonteras på kod som inte har någon aning om vad ett stycke är
Vad äger trädet, och varför är ägandet strikt?
DOM:et tillämpar enskilt ägande på varje nivå: dokumentet äger sina sektioner, en sektion äger sin brödtext, sidhuvud och sidfot, och staplar, behållare och tabeller äger sina barn. Återanvändning sker via Clone eller via en registrerad fabrik, aldrig genom att fästa samma objekt vid två föräldrar. Den regeln är inte formalitet. En komponent som förekommer två gånger i trädet skulle mätas två gånger med olika begränsningar och frigöras två gånger vid nedmontering
Den praktiska konsekvensen för anropande kod är att hjälpfunktioner returnerar nya instanser. Att registrera en fabrik med RegisterComponent och anropa CreateComponent ger dig ett namngivet recept som skapar en ny komponent varje gång, vilket är hur upprepade sidmöbler som ett signaturblock eller en juridisk sidfot hör hemma i trädet
uses
HPDFDoc, HPDFLayoutDOM;
var
Doc: THPDFDOMDocument;
Section: THPDFDOMSection;
Table: THPDFDOMTable;
Row: THPDFDOMTableRow;
I: Integer;
begin
Doc := THPDFDOMDocument.Create;
Doc.GenerateStructure := True; // skriv ut PDF/UA-strukturträdet
Doc.Language := 'en-US';
Section := Doc.AddSection;
Section.PageWidth := 595; // A4 i punkter
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); // vikter, inte absoluta bredder
Table.AddColumn(1);
Table.AddColumn(1);
Table.RepeatHeaders := True;
Row := Table.AddRow(18, True); // rubrikrad
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;
Hur undviker pagineringen kvadratisk kostnad?
Det naiva sättet att paginera ett träd är att klona det som inte fick plats och föra det till nästa sida. På en tabell med tiotusen rader klonar det de återstående raderna en gång per sida och förvandlar ett linjärt dokument till ett kvadratiskt
HotPDF delar smalt istället. Renderaren på toppnivå går igenom brödtextens barn efter index och klonar aldrig en hel sektion eller brödtext. Bara nästlade staplar och behållare som verkligen sträcker sig över en sidgräns får sitt påverkade underträd klonat, och de två tunga bladtyperna bär en markör snarare än en kopia: en textfortsättning lagrar det källteckenintervall den fortfarande är skyldig, och en tabellfortsättning lagrar den radskiva den ännu inte placerat. Långa dokument förblir linjära, och långa stycken kostar detsamma oavsett om de bryts en eller fem gånger
Mätningen förblir ärlig om sidoeffekter. THPDFLayoutElement.Measure krävs vara fri från ritningens sidoeffekter, och den faktiska placeringen körs alltid genom THotPDF.PlaceLayoutElement, samma centrala rutin som mäter det placerade fragmentet på nytt, sätter upp överflödesägande och registrerar diagnostik. DOM-renderaren avgör bara policyn för ny sida, sidmöbler, mellanrum och fortsättningarnas livstid
Tabellrubrikreglerna som förhindrar ett oändligt dokument
Att upprepa tabellrubriker över sidor låter enkelt och döljer två felmoder. HotPDF kräver att rubrikrader bara förekommer i den första löpande sekvensen av rader, och att den första delningen får plats med alla rubrikrader plus minst en brödtextrad. Utan den andra regeln ger en rubrik högre än det återstående utrymmet en sida som inte innehåller något annat än rubriken, följd av ännu en identisk sida, i all oändlighet
Fortsättningssidor ritar om rubriken, och den omritade kopian markeras som en artefakt snarare än som innehåll, vilket är det korrekta svaret för både tillgänglighet och textextraktion. Den ursprungliga rubrikraden förblir i den logiska tabellstrukturen exakt en gång. Hoppa över detta och en skärmläsare läser upp kolumnrubrikerna igen mitt i datan, och en textextraherare infogar en duplicerad rubrikrad mellan brödtextrader
Det finns också ett defensivt tak för fortsättningsdjupet, eftersom en anpassad komponent fritt kan implementera Split på ett sätt som alltid returnerar en likvärdig svans. Renderaren kontrollerar gränsen efter att svansen kopplats loss och innan nästa sida påbörjas, och den aktuella iterationen frigör svansen i sitt eget finally-block, så en felaktigt beteende hos en tredjepartskomponent misslyckas med ett diagnostiserbart fel istället för att fylla en disk
Ett logiskt element, många sidfragment
Automatisk taggning är där pagineringsmodellen och strukturmodellen måste komma överens. Ett stycke som delas över två sidor är ett logiskt stycke, så det måste förbli ett enda strukturelement. Men markerade innehållsidentifierare är per sida, så varje synligt fragment behöver sitt eget MCID på den sida det förekommer på
HotPDF löser detta genom att behålla ett enda strukturelement och lägga till en markerad innehållsreferens till dess /K-array för varje fragment, med paret /Pg och /MCID som identifierar sidan och identifieraren. ParentTree-platsen för det MCID:et pekar tillbaka på samma element. Detta är precis vad ISO 14289 förväntar sig, och det är skälet till att fortsättningskloner skiljer sig från vanliga kloner: en vanlig Clone betyder nytt logiskt innehåll och får en ny semantisk identitet, medan den interna fortsättningsklonen ärver identiteten hos den komponent den fortsätter
Elementåteranvändning slås upp via ett index av semantiska identiteter sorterat efter komponentpekare och sökt genom binär jämförelse, vilket håller uppslagningen logaritmisk på stora träd. Indexet håller enbart icke-ägande referenser; livstiden för själva strukturobjekten stannar hos PDF-objektgrafen
Strukturregler som renderaren tillämpar i förväg
Med GenerateStructure aktiverat kontrolleras flera PDF/UA-regler medan trädet renderas snarare än efter att filen existerar. Rubriker börjar på nivå 1 och får inte hoppa över nivåer. LI får bara förekomma inuti L, och Lbl och LBody bara inuti LI. TR hör till en tabell, och TH och TD till en rad. En figur utan alternativtext avvisas i PDF/UA-läge
Att avvisa tidigt är det medvetna valet här. En validerare som rapporterar en saknad alternativtext efter att dokumentet har skrivits berättar för dig att en batch med tiotusen kontoutdrag behöver regenereras; en renderare som avvisar komponenten berättar för dig vilken komponent, medan den data som producerade den fortfarande är i sammanhang. Konformitetsverifiering hör fortfarande hemma i pipelinen som ett separat steg, och mekaniken kring det täcks i validering av PDF/A, PDF/X och 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;
Statistikposten är mer användbar än den ser ut vid första anblicken. Att SplitCount stiger kraftigt efter en malländring betyder oftast att en komponent började mäta högre än sin behållare. Att MaximumContinuationDepth smyger uppåt är den tidiga varningen för en komponent vars Split gör för lite framsteg per sida. Och att jämföra ArtifactCount mot antalet fortsättningssidor bekräftar att upprepade rubriker verkligen taggades som artefakter
Var passar DOM:et in vid sidan av det direkta API:et?
DOM:et ersätter inte direkt ritning; det sitter ovanpå samma sidobjekt. Allt renderaren placerar kan varvas med direkta anrop på THotPDF, vilket spelar roll när en rapport behöver ett handplacerat element som en signaturbild på en exakt plats. Sidstängning förblir under kontroll av AddPage och EndDoc, så det omedelbara flush-läget håller inga färdiga sidor i minnet och det resida minnet styrs fortsatt av de aktuella fortsättningarna, typsnittsresurserna och den vanliga dokumentobjektgrafen
Välj DOM:et när innehållet är datadrivet och layouten är regeldriven, och behåll direkt ritning för fast grafik. Om din nuvarande smärtpunkt specifikt är tabellpaginering är det snävare tillvägagångssättet i att generera tabeller i PDF värt att läsa först, och textnivåbeteende som justering beskrivs i textjustering
Deklarativ layout, automatisk taggning och det direkta rit-API:et levereras i samma komponent för Delphi och C++Builder; den fullständiga funktionslistan finns på sidan för HotPDF Delphi PDF-komponent