Teknisk artikel

HTML til PDF i Delphi: Vejen via HTML5 paged media

HotPDF konverterer HTML til PDF-sider via THPDFHTMLImporter.RenderHTML5, en versioneret paged-media-profil med et rigtigt DOM, en rigtig CSS-cascade, tabeller, sideskift og valgfri AcroForm-generering ud fra HTML-formularelementer. Den kører helt in-process: ingen browsermotor, ingen Chromium, ingen ekstern konverter, og som standard slet ingen fil- eller netværksadgang

Grunden til, at der findes et separat indgangspunkt i stedet for en forbedret RenderHTML, er kompatibilitet. Den ældre metode har gengivet fakturaer og breve i produktionsapplikationer i årevis, og et mere tilgivende DOM med en fuld cascade ændrer output for dokumenter, der er finjusteret mod den gamle adfærd. Derfor bevarer den gamle metode sin nøjagtige semantik, og alt det nye ligger bag RenderHTML5, hvis profilversion rapporteres af HTML5ProfileVersion, og hvis understøttede milepæle er angivet af HTML5ProfileMilestones

Hvad tilføjer HTML5-profilen egentlig?

Fire egenskaber betyder noget i praksis. Et strukturelt DOM, hvor html og body er containere frem for tekstnoder. En CSS-cascade med korrekt præcedens. Tabellayout. Og eksplicit kontrol over sideskift, hvilket er, hvad "paged media" betyder: HTML blev designet til et scrollende viewport, og at gøre det til sider kræver, at man beslutter, hvor indhold må bryde

Pointen om containere lyder som en implementeringsdetalje og er i virkeligheden forskellen på, om det virker eller ej. Hvis rodelementerne behandles som tekstcontainere, kollapser hele DOM'et til én tekstblok ved parsing, og enhver efterfølgende regel i stylesheetet, sideskift, tabel og formularundertræ anvendes på ingenting. Enhver HTML-renderer, der producerer én udifferentieret tekstmur fra et struktureret dokument, har begået præcis denne fejl

Cascade-rækkefølgen, forklaret trin for trin

CSS-præcedens er ikke én enkelt sortering. HotPDF anvender normale deklarationer fra matchende regler i rækkefølge efter specificitet og derefter kildeorden, anvender dernæst inline normale deklarationer, så !important-deklarationerne fra matchende regler, og til sidst inline !important. Fire gennemløb, i den rækkefølge

HotPDF opløser hver CSS-egenskab i Delphi gennem fire ordnede cascade-gennemløb, fra normal regelmatch op til inline !important-deklarationer
Senere gennemløb vinder uafgjorte tilfælde med vilje: inline-deklarationer rangerer over stylesheets, og !important rangerer over alt, der er deklareret normalt

Implementationer, der reducerer dette til "den sidste vinder", får de almindelige tilfælde rigtige og de interessante forkerte, hvilket er værre end at få dem ensartet forkerte, fordi fejlen kun viser sig i skabeloner, der bruger !important til at tilsidesætte et base-stylesheet — netop de skabeloner, der er skrevet af folk, som forventer, at cascaden virker:

uses
  HPDFDoc, HPDFHTMLImport;

var
  Pdf: THotPDF;
  Importer: THPDFHTMLImporter;
  Html, Css: WideString;
begin
  Pdf := THotPDF.Create(nil);
  Importer := THPDFHTMLImporter.Create(Pdf);
  try
    Pdf.FileName := 'statement.pdf';
    Pdf.BeginDoc;

    Importer.Margin := 56;              // punkter
    Importer.BaseFontName := 'Arial';
    Importer.BaseFontSize := 10;
    Importer.CreateAcroForms := False;  // gengiv kontrolelementer, opret ikke felter

    Css := '@page { margin: 56pt } ' +
           'table { border-collapse: collapse } ' +
           'tr.total { page-break-inside: avoid; font-weight: bold }';
    Html := LoadTemplate('statement.html');

    if not Importer.RenderHTML5(Html, Css) then
      raise Exception.Create('nothing was emitted - check the markup');

    Pdf.EndDoc;

    Writeln(Format('profile=%d nodes=%d rules=%d layout ops=%d',
      [Importer.Statistics.ProfileVersion,
       Importer.Statistics.ParsedNodeCount,
       Importer.Statistics.CSSRuleCount,
       Importer.Statistics.LayoutOperationCount]));
  finally
    Importer.Free;
    Pdf.Free;
  end;
end;

Det andet argument er et forfatter-stylesheet, der anvendes sammen med, hvad dokumentet i forvejen bærer, hvilket er den rene måde at håndhæve husregler såsom sidemargener og enkelinjekontrol uden at redigere hver eneste skabelon

Hvorfor henter importeren aldrig noget?

En HTML-til-PDF-konverter, der opløser eksterne referencer, er en server-side request forgery-motor forklædt som dokumentgenerering. Peg den mod brugerleveret markup, og den vil villigt hente http://169.254.169.254/ eller en fil-URL fra din applikationsserver og indlejre resultatet i en PDF, som nogen downloader

HotPDF læser derfor hverken filer eller netværk som standard. Billeder og andre ressourcer ankommer på præcis to måder: som data:-URI'er inde i markuppen, eller via OnResource-callbacket, hvor din kode afgør, hvad en URI betyder, og leverer byttene:

HotPDF's Delphi HTML-importer opløser hver ressource-URI lokalt, accepterer data-URI'er og OnResource-callbacks, mens fil- og netværksforespørgsler afvises bag antals- og byteloft
Kun bytes, der ankommer lokalt, når frem til siden, afviste referencer forøger tælleren, og ingen forespørgsel forlader nogensinde processen
procedure TReportForm.SupplyResource(Sender: TObject;
  const URI: WideString; ResourceKind: THPDFHTMLResourceKind;
  var Data: TBytes; var ContentType: string; var Handled: boolean);
begin
  Handled := False;
  // Server kun fra en allowlistet lokal asset-mappe. Send aldrig
  // URI'en til en HTTP-klient, og sammenføj den aldrig med en sti
  // uden først at validere den
  if StartsText('asset:', URI) then
  begin
    Data := LoadAssetBytes(Copy(URI, 7, MaxInt));
    ContentType := 'image/png';
    Handled := Length(Data) > 0;
  end;
end;

Hver ressource, der rent faktisk ankommer, tælles stadig op mod tre separate lofter: hvor mange ressourcer, hvor mange bytes hver, og hvor mange bytes i alt. Et callback, der trofast returnerer, hvad det bliver bedt om, kan ikke lokkes til at udtømme hukommelsen af et dokument, der refererer det samme 50 MB-billede to hundrede gange

Fire budgetter, én fejltilstand

Parsernoder, CSS-regler, layoutoperationer og ressourcer måles uafhængigt af hinanden, fordi de fejler uafhængigt af hinanden. Et dokument kan være lille og patologisk dyrt at layoute; et andet kan være overfladisk med et stylesheet designet til at eksplodere selector-matching. Udtømmes nogen af dem, kaster det EHPDFHTMLImportBudget, en dedikeret exception-klasse, så budgetudtømning kan fanges separat fra fejlformet input

Statistikken nulstilles ved starten af hvert RenderHTML5-kald, så tallene beskriver én import frem for importer-objektets levetid. I en tjeneste, der gengiver utroværdige skabeloner, bør du logge ParsedNodeCount, LayoutOperationCount og ResourceRejectCount pr. dokument: et stigende reject-antal betyder som regel, at en skabelonforfatter refererer assets, dit callback ikke genkender, og et layoutoperationsantal tæt på loftet betyder, at det næste, lidt større dokument vil fejle

Valget mellem HTML-import og layout-API'en

HTML-import vinder, når indholdet er forfattet som HTML af folk, der ikke er Delphi-udviklere, når skabeloner ændres oftere end kode, eller når det samme indhold også skal sendes som e-mail. Det deklarative layout-træ, beskrevet i deklarativt PDF-layout med tagged output, vinder, når dokumentet genereres ud fra data af din egen kode, når du har brug for, at PDF/UA-struktur udsendes automatisk, eller når præcis kontrol over paginering betyder mere end bekvem forfatning

De udelukker ikke hinanden. En almindelig opbygning er HTML-import til brødteksten i en rapport, med header, footer og signaturblokke tegnet direkte, da disse er fast geometri, som ingen skabelon bør eje. Tabeller er det ene område, hvor det er værd at måle begge dele: komplekse finansielle tabeller paginerer nogle gange bedre gennem den dedikerede tabelsti, beskrevet i generering af tabeller i PDF

Sæt CreateAcroForms bevidst. Slået fra gengives HTML-formularelementer som statisk grafik, hvilket er, hvad en udskrevet kontoudskrift ønsker. Slået til bliver de rigtige interaktive AcroForm-felter, hvilket er, hvad en udfyldelig formular ønsker, og den efterfølgende feltadfærd er dækket i tilføjelse af formularfelter til en indlæst PDF

HTML-import, deklarativt layout og det direkte tegne-API rammer alle det samme dokumentobjekt i Delphi og C++Builder; den komplette funktionsliste findes på HotPDF Delphi PDF component-siden