Technisch artikel

HTML naar PDF in Delphi: het HTML5 paged-media-traject

HotPDF converteert HTML naar PDF-pagina's via THPDFHTMLImporter.RenderHTML5, een geversioneerd paged-media-profiel met een echte DOM, een echte CSS-cascade, tabellen, pagina-einden en optionele AcroForm-generatie op basis van HTML-formulierelementen. Het draait volledig in-process: geen browser-engine, geen Chromium, geen externe converter, en standaard helemaal geen bestands- of netwerktoegang

De reden dat er een apart toegangspunt bestaat in plaats van een verbeterde RenderHTML is compatibiliteit. De oudere methode rendert al jaren facturen en brieven in productieomgevingen, en een soepelere DOM met een volledige cascade verandert de output van documenten die op het oude gedrag zijn afgestemd. Dus behoudt de oude methode zijn exacte semantiek en leeft al het nieuwe achter RenderHTML5, waarvan de profielversie wordt gerapporteerd door HTML5ProfileVersion en waarvan de ondersteunde mijlpalen worden vermeld door HTML5ProfileMilestones

Wat voegt het HTML5-profiel eigenlijk toe?

Vier mogelijkheden zijn in de praktijk belangrijk. Een structurele DOM waarin html en body containers zijn in plaats van tekstnodes. Een CSS-cascade met correcte precedentie. Tabellay-out. En expliciete controle over pagina-einden, wat "paged media" betekent: HTML is ontworpen voor een scrollend viewport, en het omzetten ervan in pagina's vereist beslissen waar inhoud mag afbreken

Het punt over containers klinkt als een implementatiedetail en is in werkelijkheid het verschil tussen werken en niet werken. Als de rootelementen als tekstcontainers worden behandeld, valt de hele DOM tijdens het parsen samen tot één tekstblok, en wordt elke stylesheetregel, pagina-einde, tabel en formuliersubboom daarna op niets toegepast. Elke HTML-renderer die van een gestructureerd document één ongedifferentieerde muur van tekst maakt, heeft precies deze fout gemaakt

Cascadevolgorde, uitgeschreven

CSS-precedentie is geen enkele sortering. HotPDF past normale declaraties uit overeenkomende regels toe op volgorde van specificiteit en vervolgens brondsvolgorde, past daarbovenop inline normale declaraties toe, dan de !important-declaraties uit overeenkomende regels, en tot slot inline !important. Vier passes, in die volgorde

HotPDF lost elke CSS-eigenschap in Delphi op via vier geordende cascade-passes, van normale regelmatching tot inline !important-declaraties
Latere passes winnen gelijke gevallen bewust: inline declaraties gaan boven stylesheets, en !important gaat boven alles wat normaal is gedeclareerd

Implementaties die dit versimpelen tot "laatste wint" krijgen de gewone gevallen goed en de interessante gevallen verkeerd, wat erger is dan ze uniform verkeerd doen, omdat de fout alleen optreedt in templates die !important gebruiken om een basisstylesheet te overschrijven — precies de templates geschreven door mensen die verwachten dat de cascade werkt:

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;              // punten
    Importer.BaseFontName := 'Arial';
    Importer.BaseFontSize := 10;
    Importer.CreateAcroForms := False;  // besturingselementen renderen, geen velden aanmaken

    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;

Het tweede argument is een auteursstylesheet die wordt toegepast naast wat het document zelf meedraagt, wat de nette manier is om huisregels zoals paginamarges en widow-controle af te dwingen zonder elk template te bewerken

Waarom haalt de importeerder nooit iets op?

Een HTML-naar-PDF-converter die externe verwijzingen oplost is een server-side request forgery-engine in het kostuum van een documentgenerator. Richt hem op door de gebruiker aangeleverde markup en hij zal gretig http://169.254.169.254/ of een file-URL van uw applicatieserver ophalen en het resultaat inbedden in een PDF die iemand downloadt

HotPDF leest daarom standaard noch bestanden noch het netwerk. Afbeeldingen en andere resources komen op precies twee manieren binnen: als data:-URI's binnen de markup, of via de OnResource-callback, waar uw code beslist wat een URI betekent en de bytes aanlevert:

De Delphi HTML-importeerder van HotPDF lost elke resource-URI lokaal op, accepteert data-URI's en OnResource-callbacks, en weigert bestands- en netwerkverzoeken achter plafonds voor aantal en bytes
Alleen bytes die lokaal binnenkomen bereiken de pagina, geweigerde verwijzingen verhogen de teller, en er verlaat nooit een verzoek het proces
procedure TReportForm.SupplyResource(Sender: TObject;
  const URI: WideString; ResourceKind: THPDFHTMLResourceKind;
  var Data: TBytes; var ContentType: string; var Handled: boolean);
begin
  Handled := False;
  // Alleen leveren vanuit een goedgekeurde lokale asset-directory. Geef de URI
  // nooit door aan een HTTP-client, en voeg hem nooit samen met een pad
  // zonder eerst te valideren
  if StartsText('asset:', URI) then
  begin
    Data := LoadAssetBytes(Copy(URI, 7, MaxInt));
    ContentType := 'image/png';
    Handled := Length(Data) > 0;
  end;
end;

Elke resource die wel binnenkomt, wordt nog steeds afgezet tegen drie afzonderlijke plafonds: hoeveel resources, hoeveel bytes elk, en hoeveel bytes in totaal. Een callback die trouw teruggeeft wat er ook gevraagd wordt, kan niet worden overgehaald om geheugen uit te putten door een document dat dezelfde afbeelding van 50 MB tweehonderd keer aanroept

Vier budgetten, één faalmodus

Parsernodes, CSS-regels, lay-outbewerkingen en resources worden onafhankelijk gemeten, omdat ze onafhankelijk falen. Een document kan klein zijn en pathologisch duur om op te maken; een ander kan ondiep zijn met een stylesheet ontworpen om selectormatching te laten ontploffen. Het uitputten van één ervan werpt EHPDFHTMLImportBudget op, een eigen exceptieklasse zodat budgetuitputting apart kan worden opgevangen van misvormde input

Statistieken worden gereset bij het begin van elke RenderHTML5-aanroep, dus de cijfers beschrijven één import in plaats van de levensduur van het importer-object. In een dienst die niet-vertrouwde templates rendert, logt u ParsedNodeCount, LayoutOperationCount en ResourceRejectCount per document: een stijgend afwijzingsaantal betekent meestal dat een templateauteur assets aanroept die uw callback niet herkent, en een aantal lay-outbewerkingen dicht bij het plafond betekent dat het volgende, iets grotere document zal mislukken

Kiezen tussen HTML-import en de lay-out-API

HTML-import wint wanneer de inhoud als HTML wordt opgesteld door mensen die geen Delphi-ontwikkelaars zijn, wanneer templates vaker veranderen dan code, of wanneer dezelfde inhoud ook per e-mail moet worden verstuurd. De declaratieve lay-outboom beschreven in declaratieve PDF-lay-out met tagged output wint wanneer het document door uw eigen code op basis van data wordt gegenereerd, wanneer u automatisch PDF/UA-structuur moet uitvoeren, of wanneer nauwkeurige controle over paginering belangrijker is dan gebruiksgemak bij het opstellen

Ze sluiten elkaar niet uit. Een veelvoorkomende vorm is HTML-import voor de body van een rapport, waarbij kop, voettekst en handtekeningblokken direct worden getekend, omdat dat vaste geometrie is die geen template zou moeten bezitten. Tabellen zijn het enige gebied waar het de moeite waard is beide te meten: complexe financiële tabellen pagineren soms beter via het speciale tabeltraject beschreven in het genereren van tabellen in PDF

Stel CreateAcroForms bewust in. Uit, renderen HTML-formulierelementen als statische afbeeldingen, wat een afgedrukte verklaring wil. Aan, worden ze echte interactieve AcroForm-velden, wat een invulbaar formulier wil, en het veldgedrag dat daarop volgt wordt behandeld in het toevoegen van formuliervelden aan een geladen PDF

HTML-import, declaratieve lay-out en de directe teken-API richten zich allemaal op hetzelfde documentobject in Delphi en C++Builder; de volledige functielijst staat op de HotPDF Delphi PDF component-pagina