Teknisk artikkel

HTML til PDF i Delphi: HTML5 Paged Media-metoden

HotPDF konverterer HTML til PDF-sider via THPDFHTMLImporter.RenderHTML5, en versjonert paged-media-profil med en ekte DOM, en ekte CSS-kaskade, tabeller, sideskift og valgfri AcroForm-generering fra HTML-skjemakontroller. Den kjører helt in-process: ingen nettlesermotor, ingen Chromium, ingen ekstern konverterer, og som standard ingen fil- eller nettverkstilgang i det hele tatt

Grunnen til at det finnes et eget inngangspunkt i stedet for en forbedret RenderHTML er kompatibilitet. Den eldre metoden har gjengitt fakturaer og brev i produksjonsapplikasjoner i årevis, og en overbærende DOM med en full kaskade endrer utdataen til dokumenter som var finjustert mot den gamle oppførselen. Så den gamle metoden beholder sin nøyaktige semantikk, og alt nytt ligger bak RenderHTML5, hvis profilversjon rapporteres av HTML5ProfileVersion og hvis støttede milepæler listes av HTML5ProfileMilestones

Hva legger HTML5-profilen faktisk til?

Fire evner betyr noe i praksis. En strukturell DOM der html og body er beholdere i stedet for tekstnoder. En CSS-kaskade med korrekt presedens. Tabellayout. Og eksplisitt kontroll av sideskift, som er det «paged media» betyr: HTML ble designet for en rullbar visningsflate, og å gjøre det om til sider krever at man bestemmer hvor innhold har lov til å brytes

Poenget om beholdere høres ut som en implementasjonsdetalj, men er faktisk forskjellen mellom at det fungerer og at det ikke gjør det. Hvis rotelementene behandles som tekstbeholdere, kollapser hele DOM-en til én tekstblokk under parsing, og hver stilarkregel, hvert sideskift, hver tabell og hvert skjematre deretter anvendes på ingenting. Enhver HTML-renderer som produserer én udifferensiert tekstvegg fra et strukturert dokument, har gjort nøyaktig denne feilen

Kaskaderekkefølge, forklart i detalj

CSS-presedens er ikke én enkel sortering. HotPDF anvender normale deklarasjoner fra matchende regler i rekkefølge etter spesifisitet og deretter kilderekkefølge, anvender innebygde normale deklarasjoner på toppen, deretter !important-deklarasjonene fra matchende regler, og til slutt innebygd !important. Fire gjennomganger, i den rekkefølgen

HotPDF løser hver CSS-egenskap i Delphi gjennom fire ordnede kaskadegjennomganger, fra normal regelmatching opp til innebygde !important-deklarasjoner
Senere gjennomganger vinner uavgjorte tilfeller med hensikt: innebygde deklarasjoner rangerer over stilark, og !important rangerer over alt deklarert normalt

Implementasjoner som forenkler dette til «sist vinner» får de vanlige tilfellene riktige og de interessante tilfellene feil, noe som er verre enn å få dem jevnt over feil, fordi feilen bare dukker opp i maler som bruker !important for å overstyre et grunnleggende stilark — nøyaktig de malene skrevet av folk som forventer at kaskaden fungerer:

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;  // gjengi kontroller, ikke opprett felt

    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 andre argumentet er et forfatterstilark som anvendes ved siden av det dokumentet selv bærer med seg, som er den ryddige måten å håndheve interne regler som sidemarger og enkelinjekontroll uten å redigere hver mal

Hvorfor henter importøren aldri noe?

En HTML-til-PDF-konverterer som løser eksterne referanser, er en server-side request forgery-motor forkledd som dokumentgenerering. Pek den mot brukerlevert markup, og den vil villig hente http://169.254.169.254/ eller en fil-URL fra applikasjonsserveren din og bygge resultatet inn i en PDF som noen laster ned

HotPDF leser derfor verken filer eller nettverk som standard. Bilder og andre ressurser kommer inn på nøyaktig to måter: som data:-URI-er inne i markup-en, eller gjennom OnResource-tilbakekallet, der din kode bestemmer hva en URI betyr og leverer bytene:

HotPDFs HTML-importør i Delphi løser hver ressurs-URI lokalt, godtar data-URI-er og OnResource-tilbakekall, og avviser fil- og nettverkshenting bak grenser for antall og byte
Bare byte som ankommer lokalt når frem til siden, avviste referanser øker telleren, og ingen henting forlater noensinne prosessen
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 tillatelseslistet lokal ressurskatalog. Aldri
  // send URI-en til en HTTP-klient, og aldri sett den sammen med en sti
  // uten å validere den først
  if StartsText('asset:', URI) then
  begin
    Data := LoadAssetBytes(Copy(URI, 7, MaxInt));
    ContentType := 'image/png';
    Handled := Length(Data) > 0;
  end;
end;

Hver ressurs som faktisk kommer inn, telles likevel mot tre separate tak: hvor mange ressurser, hvor mange byte hver, og hvor mange byte totalt. Et tilbakekall som trofast returnerer det det blir bedt om, kan ikke lures til å tømme minnet av et dokument som refererer det samme 50 MB-bildet to hundre ganger

Fire budsjetter, én feilmodus

Parsernoder, CSS-regler, layoutoperasjoner og ressurser måles uavhengig av hverandre, fordi de svikter uavhengig av hverandre. Et dokument kan være lite og patologisk kostbart å legge ut; et annet kan være grunt med et stilark designet for å eksplodere selektormatching. Å tømme noen av dem utløser EHPDFHTMLImportBudget, en dedikert unntaksklasse slik at budsjettuttømming kan fanges separat fra feilformet inndata

Statistikk nullstilles ved starten av hvert RenderHTML5-kall, slik at tallene beskriver én import i stedet for hele levetiden til importørobjektet. I en tjeneste som gjengir ubetrodde maler, logg ParsedNodeCount, LayoutOperationCount og ResourceRejectCount per dokument: et stigende avvisningstall betyr som regel at en malforfatter refererer ressurser tilbakekallet ditt ikke gjenkjenner, og et layoutoperasjonstall nær taket betyr at det neste, litt større dokumentet vil feile

Å velge mellom HTML-import og layout-API-et

HTML-import vinner når innholdet forfattes som HTML av folk som ikke er Delphi-utviklere, når maler endres oftere enn kode, eller når det samme innholdet også må sendes som e-post. Det deklarative layouttreet beskrevet i deklarativ PDF-layout med tagget utdata vinner når dokumentet genereres fra data av din egen kode, når du trenger PDF/UA-struktur generert automatisk, eller når presis kontroll over paginering betyr mer enn skrivebekvemmelighet

De utelukker ikke hverandre. En vanlig form er HTML-import for selve brødteksten i en rapport, med topptekst, bunntekst og signaturblokker tegnet direkte, siden dette er fast geometri ingen mal bør eie. Tabeller er det ene området der det er verdt å måle begge deler: komplekse finansielle tabeller pagineres noen ganger bedre gjennom den dedikerte tabellstien beskrevet i generering av tabeller i PDF

Sett CreateAcroForms bevisst. Av, gjengis HTML-skjemakontroller som statisk grafikk, som er det en utskrevet kontoutskrift ønsker. På, blir de ekte interaktive AcroForm-felter, som er det et utfyllbart skjema ønsker, og feltoppførselen som følger, dekkes i å legge til skjemafelter i et lastet PDF-dokument

HTML-import, deklarativ layout og direkte-tegning-API-et retter seg alle mot det samme dokumentobjektet i Delphi og C++Builder; den fullstendige funksjonslisten finnes på HotPDF Delphi PDF-komponentsiden