Teknisk artikel

HTML till PDF i Delphi: HTML5-vägen med sidindelat medium

HotPDF konverterar HTML till PDF-sidor via THPDFHTMLImporter.RenderHTML5, en versionshanterad profil för sidindelat medium med en riktig DOM, en riktig CSS-kaskad, tabeller, sidbrytningar och valfri AcroForm-generering från HTML-formulärkontroller. Den körs helt in-process: ingen webbläsarmotor, ingen Chromium, ingen extern konverterare, och som standard ingen fil- eller nätverksåtkomst alls

Anledningen till att en separat ingångspunkt finns istället för en förbättrad RenderHTML är bakåtkompatibilitet. Den äldre metoden har renderat fakturor och brev i produktionsapplikationer i åratal, och en mer tillåtande DOM med en fullständig kaskad ändrar utdata för dokument som var finjusterade mot det gamla beteendet. Så den gamla metoden behåller sin exakta semantik och allt nytt lever bakom RenderHTML5, vars profilversion rapporteras av HTML5ProfileVersion och vars stödda milstolpar listas av HTML5ProfileMilestones

Vad tillför HTML5-profilen egentligen?

Fyra möjligheter spelar roll i praktiken. En strukturell DOM där html och body är behållare snarare än textnoder. En CSS-kaskad med korrekt precedens. Tabellayout. Och explicit kontroll över sidbrytningar, vilket är vad "sidindelat medium" betyder: HTML var designat för ett skrollande visningsfönster, och att omvandla det till sidor kräver att man bestämmer var innehåll får brytas

Behållarpunkten låter som en implementationsdetalj men är faktiskt skillnaden mellan att fungera och inte fungera. Om rotelementen behandlas som textbehållare kollapsar hela DOM:en till ett enda textblock vid parsningstillfället, och varje stilmallsregel, sidbrytning, tabell och formulärdelträd tillämpas därefter på ingenting. Varje HTML-renderare som producerar en enda odifferentierad textmur från ett strukturerat dokument har gjort exakt detta misstag

Kaskadordning, i klartext

CSS-precedens är inte en enkel sortering. HotPDF tillämpar normala deklarationer från matchande regler i ordning efter specificitet och sedan källordning, tillämpar sedan inline-normala deklarationer ovanpå, därefter !important-deklarationerna från matchande regler, och slutligen inline-!important. Fyra passeringar, i den ordningen

HotPDF löser varje CSS-egenskap i Delphi genom fyra ordnade kaskadpasseringar, från matchning av normala regler upp till inline-!important-deklarationer
Senare passeringar vinner oavgjorda avsiktligt: inline-deklarationer överträffar stilmallar, och !important överträffar allt som deklarerats normalt

Implementationer som förenklar detta till "sist vinner" får de vanliga fallen rätt och de intressanta fallen fel, vilket är värre än att få allt konsekvent fel, eftersom felet bara syns i mallar som använder !important för att åsidosätta en basstilmall — precis de mallar som skrivits av personer som förväntar sig att kaskaden ska fungera:

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;  // rendera kontroller, skapa inte fält

    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 andra argumentet är en egen stilmall som tillämpas jämte det dokumentet själv bär med sig, vilket är det rena sättet att genomdriva husregler som sidmarginaler och änkeradskontroll utan att redigera varje mall

Varför hämtar importören aldrig något?

En HTML-till-PDF-konverterare som löser upp externa referenser är en server-side request forgery-motor klädd i en dokumentgenereringsdräkt. Rikta den mot användarinmatad markup och den kommer villigt hämta http://169.254.169.254/ eller en file-URL från din applikationsserver och bädda in resultatet i en PDF som någon laddar ner

HotPDF läser därför varken filer eller nätverk som standard. Bilder och andra resurser anländer på exakt två sätt: som data:-URI:er inuti markupen, eller via callbacken OnResource, där din kod avgör vad en URI betyder och tillhandahåller byten:

HotPDF:s Delphi-HTML-importör löser varje resurs-URI lokalt, accepterar data-URI:er och OnResource-callbacks samtidigt som den avvisar fil- och nätverkshämtningar bakom tak för antal och byte
Bara byte som anländer lokalt når sidan, avvisade referenser ökar räknaren, och ingen hämtning lämnar någonsin processen
procedure TReportForm.SupplyResource(Sender: TObject;
  const URI: WideString; ResourceKind: THPDFHTMLResourceKind;
  var Data: TBytes; var ContentType: string; var Handled: boolean);
begin
  Handled := False;
  // Servera bara från en tillåtelselistad lokal resurskatalog. Skicka
  // aldrig URI:n till en HTTP-klient, och slå aldrig ihop den med en
  // sökväg utan att validera den först
  if StartsText('asset:', URI) then
  begin
    Data := LoadAssetBytes(Copy(URI, 7, MaxInt));
    ContentType := 'image/png';
    Handled := Length(Data) > 0;
  end;
end;

Varje resurs som ändå anländer räknas fortfarande mot tre separata tak: hur många resurser, hur många byte var, och hur många byte totalt. En callback som troget returnerar vad den blir ombedd om kan inte lockas att tömma minnet av ett dokument som refererar samma 50 MB-bild tvåhundra gånger

Fyra budgetar, ett felläge

Parsernoder, CSS-regler, layoutoperationer och resurser mäts oberoende av varandra, eftersom de misslyckas oberoende av varandra. Ett dokument kan vara litet och patologiskt kostsamt att lägga ut; ett annat kan vara ytligt med en stilmall designad för att spränga selektormatchning. Att tömma någon av dem utlöser EHPDFHTMLImportBudget, en dedikerad undantagsklass så att budgetuttömning kan fångas separat från felformad indata

Statistiken nollställs vid början av varje RenderHTML5-anrop, så siffrorna beskriver en enda import snarare än importörobjektets hela livstid. I en tjänst som renderar opålitliga mallar, logga ParsedNodeCount, LayoutOperationCount och ResourceRejectCount per dokument: ett stigande avvisningsantal betyder vanligtvis att en mallförfattare refererar resurser din callback inte känner igen, och ett antal layoutoperationer nära taket betyder att nästa lite större dokument kommer att misslyckas

Att välja mellan HTML-import och layout-API:et

HTML-import vinner när innehållet är författat som HTML av personer som inte är Delphi-utvecklare, när mallar ändras oftare än kod, eller när samma innehåll även måste skickas som e-post. Det deklarativa layoutträdet som beskrivs i deklarativ PDF-layout med taggad utdata vinner när dokumentet genereras från data av din egen kod, när du behöver PDF/UA-struktur genererad automatiskt, eller när exakt kontroll över paginering väger tyngre än författarbekvämlighet

De utesluter inte varandra. En vanlig form är HTML-import för brödtexten i en rapport, med sidhuvud, sidfot och signaturblock ritade direkt, eftersom de är fast geometri som ingen mall bör äga. Tabeller är det ena området där det lönar sig att mäta båda: komplexa finansiella tabeller pagineras ibland bättre via den dedikerade tabellvägen som beskrivs i generering av tabeller i PDF

Ange CreateAcroForms medvetet. Avstängd renderas HTML-formulärkontroller som statisk grafik, vilket är vad en utskriven kontoutdragssida vill ha. Påslagen blir de riktiga interaktiva AcroForm-fält, vilket är vad ett ifyllbart formulär vill ha, och fältbeteendet som följer täcks i att lägga till formulärfält i en inläst PDF

HTML-import, deklarativ layout och det direkta rit-API:et riktar sig alla mot samma dokumentobjekt i Delphi och C++Builder; den fullständiga funktionslistan finns på HotPDF Delphi PDF-komponentsidan