Teknisk artikel

Deklarativ PDF-layout i Delphi med taggad utdata

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