Teknisk artikel

Rendera PDF-sidor till 1-bitars monokrom i Delphi

En faxgateway vill inte ha din 24-bitars sidrendering. Inte heller arkiveringspipelinen som lagrar en miljon inskanningsliknande fakturor, eller OCR-frontenden som trösklar allt till svartvitt innan den ens letar efter ett tecken. Alla tre vill ha samma sak: en ren 1-bitars bitmap, en bit per pixel, där varje prick antingen är bläck eller papper. Ge dem en fullfärgs-BMP så kastar de ändå bort 23 bitar per pixel, oftast med en sämre dithering än du hade kunnat göra själv. Den intressanta frågan är var den där nedkonverteringen ska ske, och svaret i PDFlibPas visar sig säga något användbart om hur man utökar en renderare utan att skriva om den

PDFlibPas är ett inbyggt Object Pascal-bibliotek för PDF i Delphi och C++Builder. Dess renderingskärna rasteriserar en sida till en bitmap och kan ge ut BMP, PNG, JPEG, WMF och några andra format. Det den inte gjorde förrän nyligen var att lämna tillbaka en riktig monokrom bitmap, eller rendera bara en del av en sida. Båda landade i v3.83.0, och båda byggdes som tunna bekvämlighetslager ovanpå den befintliga renderaren i stället för som ändringar i rasteriseraren själv. Den begränsningen är hela historien

Varför nedkonvertera efter rendering, inte inne i renderaren

Det uppenbara sättet att skapa en 1-bitars bild är att be rasteriseraren rita i 1-bit. Det är också sättet som bryter allt annat. Renderarens interna bitmap skapas med ett hårdkodat PixelFormat := pf24bit i PDFlibRenderer konstruktor, och den där 24-bitarsytan delas av varje renderingsväg: PNG-export, förhandsgranskningen via enhetskontext, JPEG-utdata, allt. Vänder du det till pf1bit vid källan har du inte lagt till en monokrom funktion, du har försämrat färgtroheten för varje anropare i biblioteket och skrivit upp dig på att felsöka ett dussin följdregressioner

RenderPageToMonochromeFile tar motsatt väg. Den renderar sidan normalt, till en tillfällig 24-bitars BMP, och slår först därefter ihop den till 1-bit som ett efterbehandlingssteg. Renderaren lämnas orörd. Monokrombeteendet lever helt i bekvämlighetsmetoden, vilket betyder att det inte kan påverka någon som inte anropar den. Det här är den sortens avvägning som är värd att säga rakt ut: en efterprocess kostar en extra bitmapallokering och en temporär fil, och i gengäld håller den en bärande kärna helt utanför omfattningen. För en funktion som finns för fax- och arkivspecialfall är det rätt sida av ekvationen

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;

Hur 1-bitarsnedslaget faktiskt går till

Nedkonverteringen lutar sig mot GDI i stället för mot en egen tröskelrutin, och valet spelar roll för utdata-kvaliteten. Inne i metoden läses den 24-bitars temporära bitmapen in i en TBitmap, en andra TBitmap skapas med PixelFormat := pf1bit i samma dimensioner, och pixlarna flyttas över med ett enda blit-anrop:

// 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');

Tricket är SetStretchBltMode med HALFTONE. Även om källa och mål är lika stora, så ingen skalning sker, styr stretch-läget ändå hur GDI mappar färger till 1-bitspaletten. HALFTONE får den att använda halvtonsdithering, vilket förvandlar grå ytor och antialiasade textkanter till mönster av svarta och vita prickar i stället för ett hårt avklipp till närmaste av två färger. Tar du bort lägesanropet, eller använder standardläget BLACKONWHITE, posteriseras gråskalan till blockiga trösklade former. För utdata för skannade dokument och OCR-förbearbetning är det dithrade resultatet nästan alltid det du vill ha

En detalj är inte förhandlingsbar och lätt att göra fel: den temporära renderingen måste vara en BMP. RenderPageToMonochromeFile anropar den generella renderaren med en optionskod på 0, vilket är BMP. Optionsargumentet på RenderPageToFile är en liten heltalsenum, och värdena är inte utbytbara för det här syftet: 0 är BMP, 1 JPEG, 2 WMF, 3 EMF, 5 PNG, och så vidare. Nedkonverteraren gör sedan TBitmap.LoadFromStream på tempfilen. Mata den med en WMF, genom att skicka 2, och laddningen kastar "Bitmap image is not valid", eftersom en Windows-metafil är en vektoriserad posteringsström, inte en DIB. Monokrom nedkonvertering är en rasteroperation från början till slut, så mellansteget måste vara ett rasterformat

Rendera bara ett delområde av en sida

Den andra metoden, RenderPageRegionToFile, renderar bara en rektangel av sidan i stället för hela. Användningsfallen är bekanta så fort du har byggt någon dokumentvisare: att beskära ett signaturblock ur ett avtal, generera en ruta för en zoomad karta över en stor ritning, eller plocka ut ett stämplat område för en miniatyr utan att betala för att rasterisera hela sidan i hög DPI. Signaturen är rak:

// 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-strängen är fyra kommaseparerade dubbeltal i PDF-punkter, parsade manuellt inne i metoden för att kringgå lokalkänslighets- och DelimitedText funktionsfällor. Från bredden och höjden räknar metoden ut utdata-bitmapens storlek som Round(Width * DPI / 72) gånger Round(Height * DPI / 72), allokerar en in-memory pf24bit bitmap i exakt den storleken och renderar in i dess enhetskontext via RenderPageToDCClip. Resultatfilen innehåller bara den beskurna rektangeln, storleksatt till regionen snarare än till hela sidan

Clip-parametern som inte gjorde något

Här är det här arbetet var skarpare än det ser ut. RenderPageToDCClip hade burit en Clip parameter länge, och den var en lögn. Anropet accepterade argumentet, skickade det vidare till TPDFPageTree.RenderPageToDC, och den implementationen ignorerade det helt och hållet, utan att någonsin lämna det till renderaren. Du kunde skicka vilken rektangel du ville och få tillbaka hela sidan. Den som hade kopplat upp RenderPageToDCClip och väntat sig en beskärning fick en helsidesrendering och kanske, beroende på layout, inte ens märkte det

v3.83.0 kopplade in sladden. RenderPageToDC parser nu samma "Left,Top,Width,Height" punktrektangel och tillämpar den som en riktig GDI-klippregion på målens enhetskontext innan renderaren ritar. Omvandlingen från punkter till enhetspixlar är den vanliga DPI / 72 skalfaktorn, tillämpad på alla fyra kanter. Sekvensen runt renderingen är den vanliga spara/klipp/återställ-dansen:

// 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);

/ SaveDC paret är det som gör detta säkert att anropa upprepade gånger: klippregionen trycks upp på DC-tillståndstacken, sidan ritas, och den ursprungliga klippen plockas tillbaka oavsett hur renderingen avslutas. RestoreDC(-1) återställer det senast sparade tillståndet, vilket är den standardiserade idiomen för balanserad spara/återställ. Hoppar du över återställningen och en anropare som återanvänder samma DC för en senare fullsidesrendering skulle finna att den mystiskt är klippt till den senaste regionen. Att fixa den döda parametern fixade också RestoreDC(TargetDC, -1) gratis, eftersom den nya metoden går exakt genom den här vägen.RenderPageRegionToFileEn beteendepunkt att bära med sig: clip

beskär, den inte . Sidan rasteriseras fortfarande på den DPI du bad om, i sin normala position, och klippregionen kastar bara bort allt utanför rektangeln. Du zoomar inte regionen för att fylla utdata; du skär ut ett fönster ur renderingen i full upplösning. Om du vill förstora en region, höj DPI. Rektangelns koordinater tolkas i enhetsutrymme efter omvandlingen från punkter till pixlar, mätta från den renderade ytans övre vänstra hörn, så planera dina och från sidans topp och nedåt. För en djupare genomgång av hur PDFlibPas driver en enhetskontext för utdata på skärmen, går följeskrivelsen om Left förhandsgranskning och utdata via enhetskontextTop igenom samma DC-plumbing från visningssidan.Den ärliga gränsen: 1-bitars BMP, inte G4 TIFFDet vore lätt att sälja detta som "faxfärdig utdata", så här är gränsen säg rakt ut

ger en

1-bitars BMP. Den ger inte en CCITT Group 4 TIFF, vilket är formatet ett riktigt faxflöde eller ett TIFF-arkiv normalt förväntar sig. Orsaken är konkret snarare än en förbiseende: PDFlibPas CCITT-enhet avkodar i nuläget G4-strömmar men har ingen G4 RenderPageToMonochromeFile.pf1bit. Utan en kodare finns det ingenstans att skriva komprimerade monokroma körningar, så den monokroma vägen stannar vid en okomprimerad 1-bitars DIB.I praktiken är det ändå användbart. En 1-bitars BMP har rätt pixelformat, är dithrad och redo, och de flesta fax-, arkiv- eller OCR-verktygsvägar kommer gärna att läsa in den eller själva konvertera den till G4 med ett steg längre ner. Men om ditt krav bokstavligen är en Group 4 TIFF direkt ur biblioteket, så är det inte det här ännu, och du bör planera ett eget komprimeringssteg. Att veta var en funktion slutar är värt lika mycket som att veta vad den gör.Båda metoderna är medvetet små, och det är den designläxa som är värd att ta med sig härifrån: ett bekvämlighets-API som ligger ovanpå en renderare kan lägga till verklig funktionalitet, monokrom utdata, regionsbeskärning, utan att gå in i rasteriseraren och destabilisera alla andra anropare. När du väl behöver välja mellan renderingsmotorer för den underliggande rasteriseringen, täcker översikten av

flermotors-PDF-rendering i Delphi

avvägningarna på djupet. För att se hela renderingsytan och resten av API:et har PDFlibPas Delphi PDF Library produktsidan hela bilden.PDFlibPas Delphi PDF Library produktsidan har hela bilden