Teknisk artikkel

Render PDF-sider til 1-bit monokrome bitmaps i Delphi

En faksserver vil ikke ha 24-bit sidegjengivelse. Det vil heller ikke arkivpipelinen som lagrer en million skannede fakturaer, eller OCR-frontenden som terskler alt til svart-hvitt før den i det hele tatt ser etter tegn. Alle tre vil ha det samme: et rent 1-bit bitmap, én bit per piksel, der hvert punkt enten er blekk eller papir. Gi dem en fullfarget BMP, og de kaster uansett bort 23 bits per piksel, som regel med en dårligere ditherprosess enn du kunne gjort selv. Det interessante spørsmålet er hvor denne nedkonverteringen bør skje, og svaret i PDFlibPas sier noe nyttig om hvordan du utvider en renderer du helst ikke vil skrive om

PDFlibPas er et native Object Pascal PDF-bibliotek for Delphi og C++Builder. Renderkjernen rasteriserer en side til et bitmap og kan skrive BMP, PNG, JPEG, WMF og noen andre formater. Det den ikke gjorde før nylig, var å levere et ekte monokromt bitmap eller å rendere bare en del av en side. Begge deler kom i v3.83.0, og begge ble bygd som tynne bekvemmelighetslag oppå den eksisterende rendereren, ikke som endringer i selve rasterisereren. Den begrensningen er hele historien

Hvorfor nedkonvertere etter renderingen, ikke inne i rendereren

Den åpenbare måten å lage et 1-bit-bilde på er å be rasterisereren om å tegne i 1-bit. Det er også måten som bryter alt annet. Rendererens interne bitmap opprettes med en hardkodet PixelFormat := pf24bit i konstruktøren PDFlibRenderer, og den 24-bit-flaten deles av alle renderingsveier: PNG-eksport, forhåndsvisning via en enhetskontekst, JPEG-utdata, alt sammen. Bytt den til pf1bit ved kilden, og du har ikke lagt til en monokrom funksjon, du har svekket fargenøyaktigheten for alle som bruker biblioteket og meldt deg frivillig til å feilsøke et dusin regresjoner nedstrøms

RenderPageToMonochromeFile tar motsatt vei. Den renderer siden normalt, til en midlertidig 24-bit BMP, og kollapser den først etterpå til 1-bit som et etterbehandlingssteg. Rendereren er urørt. Den monokrome oppførselen lever helt og holdent i bekvemmelighetsmetoden, som betyr at den ikke kan påvirke noen som ikke kaller den. Dette er typen avveining som er verdt å si høyt: en etterprosess betaler med én ekstra bitmap-allokering og en tempfil, og til gjengjeld holder den en bærende kjerne helt utenfor ansvarsområdet. For en funksjon som finnes for å dekke faks- og arkiv-tilfeller, er det riktig side av regnskapet

var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    // 200 DPI is the classic Group 4 fax resolution; page index is 1-based
    Pdf.RenderPageToMonochromeFile(200, 1, 'invoice-page1.bmp');
  finally
    Pdf.Free;
  end;
end;

Hvordan 1-bit-kollapsen faktisk skjer

Nedkonverteringen lener seg på GDI i stedet for en håndskrevet terskelløkke, og valget betyr noe for utdata-kvaliteten. Inne i metoden lastes den 24-bit midlertidige bitmappen inn i en TBitmap, en andre TBitmap opprettes med PixelFormat := pf1bit i samme dimensjoner, og pikslene flyttes over med én enkelt blit:

// inside RenderPageToMonochromeFile, after loading the 24-bit ColorBmp
MonoBmp.PixelFormat := pf1bit;
MonoBmp.Width  := ColorBmp.Width;
MonoBmp.Height := ColorBmp.Height;
// HALFTONE tells GDI to dither the 24-bit source down to 1-bit
SetStretchBltMode(MonoBmp.Canvas.Handle, HALFTONE);
StretchBlt(MonoBmp.Canvas.Handle, 0, 0, MonoBmp.Width, MonoBmp.Height,
  ColorBmp.Canvas.Handle, 0, 0, ColorBmp.Width, ColorBmp.Height, SRCCOPY);
MonoBmp.SaveToFile('out.bmp');

Trikset er SetStretchBltMode med HALFTONE. Selv om kilde og mål har samme størrelse, så ingen skalering skjer, styrer stretch-modusen fortsatt hvordan GDI mapper farger inn i 1-bit-paletten. HALFTONE får den til å bruke halvtone-dithering, og gjør grå områder og tekstkanter med antialiasing om til mønstre av svarte og hvite prikker i stedet for en hard klipping til nærmeste av to farger. Dropp moduskallet, eller bruk standard BLACKONWHITE, og gråtoner posteriseres til blokketøyede terskel-former. For utdata til skannede dokumenter og OCR-forbehandling er det ditherede resultatet nesten alltid det du vil ha

En detalj er ikke til å forhandle om og lett å gjøre feil: den midlertidige renderingen må være en BMP. RenderPageToMonochromeFile kaller den generelle rendereren med en opsjonskode på 0, som er BMP. Alternativargumentet på RenderPageToFile er en liten heltalls-enum, og verdiene er ikke utskiftbare for dette formålet: 0 er BMP, 1 JPEG, 2 WMF, 3 EMF, 5 PNG, og så videre. Nedkonvertereren gjør deretter TBitmap.LoadFromStream på tempfilen. Gir du den en WMF, ved å sende 2, kaster innlastingen "Bitmap image is not valid", fordi en Windows-metafil er en vektorbasert poststrøm, ikke en DIB. Monokrom nedkonvertering er en rasteroperasjon fra ende til annen, så mellomformatet må være et rasterformat

Å rendere bare et delområde av en side

Den andre metoden, RenderPageRegionToFile, renderer bare et rektangel av siden i stedet for hele arket. Bruksområdene er velkjente når du først har laget en dokumentviser: beskjære en signaturblokk ut av en kontrakt, lage en flis for et zoomet kartutsnitt av en stor tegning, eller hente ut ett stemplet område som miniatyr uten å betale for å rasterisere hele siden ved høy DPI. Signaturen er enkel:

// Clip is "Left,Top,Width,Height" in PDF points (72 pt = 1 inch)
// Here: a 2.5in x 1in box, one inch in from the top-left of the page
Pdf.RenderPageRegionToFile(150, 1, '72,72,180,72', 'sig-block.bmp');

Clip-strengen er fire kommaseparerte desimaltall i PDF-punkter, tolket manuelt inne i metoden for å omgå lokalinnstillinger og DelimitedText-rariteter. Ut fra bredde og høyde beregner metoden utdata-bitmapstørrelsen som Round(Width * DPI / 72) ganger Round(Height * DPI / 72), allokerer et in-memory pf24bit-bitmap med akkurat den størrelsen, og renderer inn i enhetskonteksten gjennom RenderPageToDCClip. Resultatfilen inneholder bare det beskårne rektangelet, dimensjonert til området i stedet for til hele siden

Clip-parametret som ikke gjorde noe

Her er det arbeidet var skarpere enn det ser ut. RenderPageToDCClip hadde hatt et Clip-parameter lenge, og det var en løgn. Kallet tok imot argumentet, sendte det videre til TPDFPageTree.RenderPageToDC, og den implementasjonen ignorerte det fullstendig, uten noen gang å gi det videre til rendereren. Du kunne sende inn hvilket som helst rektangel og likevel få hele siden tilbake. Alle som hadde koblet opp RenderPageToDCClip og forventet en beskjæring, fikk en fullside-render og ville, avhengig av layouten, kanskje ikke ha lagt merke til det

v3.83.0 koblet ledningen. RenderPageToDC parser nå det samme punktrektangelet "Left,Top,Width,Height" og bruker det som en reell GDI-klipsregion på målenhetskonteksten før rendereren tegner. Konverteringen fra punkter til enhetspiksler er den vanlige skaleringsfaktoren DPI / 72, brukt på alle fire kanter. Sekvensen rundt renderingen er den klassiske lagre/klipp/gjenopprett-rutinen:

// inside TPDFPageTree.RenderPageToDC, when Clip is non-empty
ScaleFactor := DPI / 72;
SaveDC(TargetDC);
IntersectClipRect(TargetDC,
  Round(ClipLeft * ScaleFactor),
  Round(ClipTop * ScaleFactor),
  Round((ClipLeft + ClipWidth) * ScaleFactor),
  Round((ClipTop + ClipHeight) * ScaleFactor));
// ... renderer draws the page here ...
// in the finally block:
RestoreDC(TargetDC, -1);

Paret SaveDC / RestoreDC(-1) er det som gjør dette trygt å kalle gjentatte ganger: klipsregionen skyves opp på DC-tilstands-stakken, siden tegnes, og den opprinnelige klippen poppes tilbake uansett hvordan renderingen avsluttes. RestoreDC(TargetDC, -1) gjenoppretter den sist lagrede tilstanden, som er den vanlige idiomatiske måten å balansere lagre/gjenopprett på. Hopper du over gjenopprettingen, vil kallende kode som gjenbruker samme DC til en senere fullsiderendering, finne den merkelig klippet til den siste regionen. Å fikse det døde parameteret fikset også RenderPageRegionToFile gratis, siden den nye metoden går gjennom akkurat denne stien

Et viktig atferdspoeng å ha i hodet: klippet beskjærer, det skalerer ikke. Siden rasteriseres fortsatt ved DPI-en du ba om, i sin normale posisjon, og klipsregionen kaster bare alt utenfor rektangelet. Du zoomer ikke regionen for å fylle utdataene; du skjærer ut et vindu av renderingen i full oppløsning. Hvis du vil ha en forstørret region, øk DPI-en. Rektangelets koordinater tolkes i enhetsrommet etter punkt-til-piksel-skaleringen, målt fra øvre venstre hjørne av den renderte flaten, så planlegg Left og Top fra toppen av siden og nedover. For en dypere gjennomgang av hvordan PDFlibPas driver en enhetskontekst for skjermutdata, går den tilhørende artikkelen om utskriftsforhåndsvisning og enhetskontekst-utdata gjennom det samme DC-opplegget fra visningssiden

Den ærlige grensen: 1-bit BMP, ikke G4 TIFF

Det ville vært lett å overselge dette som "faksklart utdata", så her er grensen sagt rett ut. RenderPageToMonochromeFile lager en pf1bit BMP. Den lager ikke en CCITT Group 4 TIFF, som er formatet en ekte faksflyt eller et TIFF-arkiv vanligvis forventer. Grunnen er konkret, ikke en forglemmelse: PDFlibPas sin CCITT-enhet dekoder G4-strømmer, men har ingen G4 encoder. Uten en encoder finnes det ingen plass å skrive komprimerte monokrome løp, så monokromstien stopper ved en ukomprimert 1-bit DIB

I praksis er det fortsatt nyttig. En 1-bit BMP har riktig pikselformat, er ditheret og klar, og de fleste faks-, arkiv- eller OCR-verktøykjeder vil gladelig ta den inn eller konvertere den til G4 selv i ett påfølgende steg. Men hvis kravet ditt bokstavelig talt er en Group 4 TIFF rett ut av biblioteket, er dette ikke det ennå, og du bør planlegge et eget komprimeringssteg. Å vite hvor en funksjon slutter er like verdifullt som å vite hva den gjør

Begge metodene er bevisst små, og det er designleksjonen verdt å ta med seg videre fra denne siden: et bekvemmelighets-API som ligger oppå en renderer kan legge til reell funksjonalitet, monokrom utdata og regionbeskjæring, uten å gå inn i rasterisereren og destabilisere alle andre kallere. Når du faktisk må velge mellom renderingsmotorer for den underliggende rasteriseringen, dekker oversikten over PDF-rendering med flere motorer i Delphi avveiningene i detalj. For å se hele renderingsflaten og resten av API-et, har PDFlibPas Delphi PDF Library-produktsiden det fullstendige bildet