HotPDF kan opbygge et pagineret dokument ud fra et deklarativt træ i stedet for ud fra koordinater. Man samler en THPDFDOMDocument af sektioner, stakke, tekst, lister og tabeller, overdrager den til THPDFDOMRenderer, og rendereren måler, paginerer, tegner sidedekoration og udsender, når det bedes om det, PDF/UA-strukturtræet, der gør resultatet tilgængeligt. Layoutkoden beregner aldrig en y-koordinat
Enhver, der har vedligeholdt en koordinatstyret rapportgenerator, ved, hvorfor dette har betydning. Den første version virker. Så vokser en kundeadresse til tre linjer, en tabel får flere rækker, en lokaliseret overskrift ombryder, og hver eneste efterfølgende y-position bliver forkert. Rettelserne hober sig op som manuelle sideskift-tjek spredt gennem forretningslogikken, og kravet om tagget PDF, der dukker op to år senere, kan ikke eftermonteres på kode, der ingen anelse har om, hvad et afsnit er
Hvad træet ejer, og hvorfor ejerskabet er strengt
DOM'en håndhæver enkelt ejerskab på hvert niveau: dokumentet ejer sine sektioner, en sektion ejer sin krop, header og footer, og stakke, containere og tabeller ejer deres børn. Genbrug sker via Clone eller via en registreret factory, aldrig ved at fastgøre det samme objekt til to forældre. Den regel er ikke ceremoni. En komponent, der optræder to gange i træet, ville blive målt to gange med forskellige begrænsninger og frigivet to gange ved nedlukning
Den praktiske konsekvens for kaldende kode er, at hjælpefunktioner returnerer nye instanser. At registrere en factory med RegisterComponent og kalde CreateComponent giver en navngivet opskrift, der producerer en frisk komponent hver gang, hvilket er, hvordan gentaget dekoration såsom en underskriftsblok eller en juridisk footer hører hjemme i træet
uses
HPDFDoc, HPDFLayoutDOM;
var
Doc: THPDFDOMDocument;
Section: THPDFDOMSection;
Table: THPDFDOMTable;
Row: THPDFDOMTableRow;
I: Integer;
begin
Doc := THPDFDOMDocument.Create;
Doc.GenerateStructure := True; // udsend PDF/UA-strukturtræet
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); // vægte, ikke absolutte bredder
Table.AddColumn(1);
Table.AddColumn(1);
Table.RepeatHeaders := True;
Row := Table.AddRow(18, True); // header-række
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;
Hvordan undgår paginering kvadratisk omkostning?
Den naive måde at paginere et træ på er at klone det, der ikke passede, og bære det med til næste side. På en tabel med ti tusind rækker kloner det de resterende rækker én gang pr. side og gør et lineært dokument til et kvadratisk et
HotPDF splitter i stedet snævert. Renderingen på øverste niveau gennemløber kropsbørn efter indeks og kloner aldrig en hel sektion eller krop. Kun indlejrede stakke og containere, der reelt spænder over en sidegrænse, får deres berørte undertræ klonet, og de to tunge bladtyper bærer en markør frem for en kopi: en tekstfortsættelse gemmer det kildekarakterinterval, den stadig skylder, og en tabelfortsættelse gemmer det rækkeudsnit, den endnu ikke har placeret. Lange dokumenter forbliver lineære, og lange afsnit koster det samme, uanset om de brydes én gang eller fem gange
Måling forbliver ærlig omkring sideeffekter. THPDFLayoutElement.Measure er påkrævet at være fri for tegne-sideeffekter, og den faktiske placering kører altid gennem THotPDF.PlaceLayoutElement, den samme centrale rutine, der genmåler det placerede fragment, opsætter overflow-ejerskab og registrerer diagnostik. DOM-rendereren beslutter kun politikken for nye sider, sidedekoration, afstand og levetiden for fortsættelser
Tabelheader-reglerne, der forhindrer et uendeligt dokument
At gentage tabelheadere på tværs af sider lyder simpelt og skjuler to fejltilstande. HotPDF kræver, at headerrækker kun optræder i det første løb af sammenhængende rækker, og at den første opdeling får plads til alle headerrækker plus mindst én krops-række. Uden den anden regel giver en header, der er højere end den resterende plads, en side, der ikke indeholder andet end headeren, efterfulgt af endnu en identisk side, i det uendelige
Fortsættelsessider gentegner headeren, og den gentegnede kopi markeres som et artefakt frem for som indhold, hvilket er det korrekte svar for både tilgængelighed og tekstudtræk. Den oprindelige headerrække forbliver i den logiske tabelstruktur præcis én gang. Springes dette over, annoncerer en skærmlæser kolonnetitlerne igen midt i dataene, og en tekstudtrækker indsætter en duplikeret headerrække mellem krops-rækker
Der er også et defensivt loft på fortsættelsesdybde, fordi en brugerdefineret komponent frit kan implementere Split på en måde, der altid returnerer en ækvivalent hale. Rendereren tjekker grænsen efter at have frakoblet halen og før den næste side påbegyndes, og den aktuelle iteration frigiver halen i sin egen finally-blok, så en fejlopførende tredjepartskomponent fejler med en diagnosticerbar fejl i stedet for at fylde en disk
Ét logisk element, mange sidefragmenter
Automatisk tagging er der, hvor pagineringsmodellen og strukturmodellen skal stemme overens. Et afsnit delt over to sider er ét logisk afsnit, så det skal forblive ét strukturelement. Men markerede indholds-identifikatorer er pr. side, så hvert synligt fragment har brug for sin egen MCID på den side, det optræder på
HotPDF løser dette ved at beholde ét enkelt strukturelement og tilføje en markeret-indhold-reference til dets /K-array for hvert fragment, hvor /Pg- og /MCID-parret identificerer siden og identifikatoren. ParentTree-slotten for den MCID peger tilbage på det samme element. Det er præcis, hvad ISO 14289 forventer, og det er grunden til, at fortsættelseskloner adskiller sig fra almindelige kloner: en almindelig Clone betyder nyt logisk indhold og får en ny semantisk identitet, mens den interne fortsættelseskopi arver identiteten fra den komponent, den fortsætter
Genbrug af elementer slås op via et indeks over semantiske identiteter sorteret efter komponentpointer og søgt med binær sammenligning, hvilket holder opslag logaritmisk på store træer. Indekset indeholder kun ikke-ejende referencer; levetiden for selve strukturobjekterne forbliver hos PDF-objektgrafen
Strukturregler, rendereren håndhæver på forhånd
Med GenerateStructure aktiveret bliver flere PDF/UA-regler tjekket, mens træet renderes, frem for efter filen findes. Overskrifter starter på niveau 1 og må ikke springe niveauer over. LI må kun optræde inden i L, og Lbl og LBody kun inden i LI. TR hører til en tabel, og TH og TD til en række. En figur uden alternativ tekst afvises i PDF/UA-tilstand
At afvise tidligt er det bevidste valg her. En validator, der rapporterer manglende alternativ tekst, efter dokumentet er skrevet, fortæller, at en batch på ti tusind opgørelser skal regenereres; en renderer, der afviser komponenten, fortæller, hvilken komponent, mens de data, der producerede den, stadig er i scope. Konformitetsverifikation hører stadig hjemme i pipelinen som et separat trin, og mekanikken bag det er beskrevet i PDF/A-, PDF/X- og PDF/UA-validering
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 er mere nyttig, end den ser ud ved første øjekast. At SplitCount stiger kraftigt efter en skabelonændring betyder som regel, at en komponent er begyndt at måle højere end sin container. At MaximumContinuationDepth kravler opad er den tidlige advarsel om en komponent, hvis Split gør for lidt fremskridt pr. side. Og at sammenligne ArtifactCount med antallet af fortsættelsessider bekræfter, at gentagne headere reelt blev tagget som artefakter
Hvor DOM'en passer ind ved siden af det direkte API
DOM'en erstatter ikke direkte tegning; den lægger sig oven på de samme sideobjekter. Alt, hvad rendereren placerer, kan flettes med direkte kald på THotPDF, hvilket betyder noget, når en rapport har brug for ét håndplaceret element såsom et underskriftsbillede på en præcis placering. Sidelukning forbliver under kontrol af AddPage og EndDoc, så den øjeblikkelige flush-tilstand ikke holder nogen færdige sider i hukommelsen, og den resideterende hukommelse forbliver styret af de aktuelle fortsættelser, skrifttyperessourcer og den almindelige dokumentobjektgraf
Vælg DOM'en, når indholdet er datadrevet, og layoutet er regeldrevet, og behold direkte tegning til fast grafik. Hvis din nuværende smerte specifikt er tabelpaginering, er den snævrere tilgang i generering af tabeller i PDF værd at læse først, og tekstniveau-adfærd såsom justering er beskrevet i tekstjustering
Deklarativt layout, automatisk tagging og det direkte tegne-API leveres i den samme komponent til Delphi og C++Builder; den komplette funktionsliste findes på HotPDF Delphi PDF-komponentsiden