Een faxgateway wil je 24-bit pagerender niet. Evenmin de archiefpijplijn die een miljoen scannerachtige facturen bewaart, of de OCR-front-end die alles naar zwart-wit drempelt voordat hij ooit naar een karakter kijkt. Alle drie willen hetzelfde: een schone 1-bit bitmap, één bit per pixel, waarin elke dot inkt of papier is. Geef ze een full-color BMP en ze gooien toch 23 bits per pixel weg, meestal met een slechtere dithering dan je zelf had kunnen doen. De interessante vraag is waar die down-conversion moet gebeuren, en het antwoord in PDFlibPas zegt iets bruikbaars over hoe je een renderer uitbreidt die je liever niet herschrijft
PDFlibPas is een native Object Pascal PDF-bibliotheek voor Delphi en C++Builder. De rendering core rasteriseert een pagina naar een bitmap en kan BMP, PNG, JPEG, WMF en een handvol andere formaten uitgeven. Wat hij tot voor kort niet deed, was een echte monochrome bitmap teruggeven of slechts een deel van een pagina renderen. Beide kwamen in v3.83.0, en beide werden gebouwd als dunne convenience layers bovenop de bestaande renderer, niet als aanpassingen aan de rasterizer zelf. Die beperking is het hele verhaal
Waarom down-convert na rendering, niet in de renderer zelf
De voor de hand liggende manier om een 1-bit beeld te maken is de rasterizer vertellen om in 1-bit te tekenen. Dat is ook de manier waarop je alles kapotmaakt. De interne bitmap van de renderer wordt in de constructor van PDFlibRenderer aangemaakt met een hardcoded PixelFormat := pf24bit, en dat 24-bit oppervlak wordt gedeeld door elk renderpad: PNG-export, device-context preview, JPEG-output, alles. Zet je hem op de bron naar pf1bit, dan heb je geen monochrome feature toegevoegd, je hebt de kleurgetrouwheid voor elke aanroeper in de bibliotheek verlaagd en jezelf opgezadeld met een dozijn downstream regressies
Dus neemt RenderPageToMonochromeFile de omgekeerde route. Hij rendert de pagina normaal, naar een tijdelijke 24-bit BMP, en klapt die pas daarna terug naar 1-bit als post-processingstap. De renderer blijft onaangeraakt. Het monochrome gedrag leeft volledig in de convenience method, wat betekent dat het niemand kan raken die hem niet aanroept. Dit is het soort afruil dat de moeite waard is om expliciet te benoemen: een post-process betaalt één extra bitmapallocatie en een temp file, en in ruil blijft een dragende core volledig buiten scope. Voor een feature die fax- en archief-randgevallen dient, is dat de juiste kant van de rekening
Hoe de 1-bit collaps echt gebeurt
De down-conversion leunt op GDI in plaats van op een handgeschreven thresholdlus, en die keuze maakt uit voor de outputkwaliteit. In de methode wordt de 24-bit tijdelijke bitmap geladen in een TBitmap, wordt een tweede TBitmap aangemaakt met PixelFormat := pf1bit op dezelfde afmetingen, en verplaatsen de pixels zich met één blit:
De truc is SetStretchBltMode met HALFTONE. Hoewel bron en bestemming even groot zijn en er dus geen scaling plaatsvindt, bepaalt de stretch mode nog steeds hoe GDI kleuren naar de 1-bit palette mapt. HALFTONE laat halftone dithering toepassen, waardoor grijze vlakken en afgeronde tekst-randen patronen van zwarte en witte stippen worden in plaats van een harde clip naar de dichtstbijzijnde van twee kleuren. Laat de mode-aanroep weg, of gebruik de standaard BLACKONWHITE, en grijswaarden-content posteriseert naar blokkerige threshold-vormen. Voor gescande documenten en OCR-preprocessing is het geditherde resultaat bijna altijd wat je wilt
Eén detail is niet-onderhandelbaar en makkelijk fout te doen: de tijdelijke render moet een BMP zijn. RenderPageToMonochromeFile roept de algemene renderer aan met een options-code van 0, en dat is BMP. Het optiesargument van RenderPageToFile is een kleine integer enum, en de waarden zijn voor dit doel niet uitwisselbaar: 0 is BMP, 1 JPEG, 2 WMF, 3 EMF, 5 PNG, enzovoort. De down-converter doet daarna TBitmap.LoadFromStream op het temp-bestand. Geef hem een WMF door 2 te passeren, en die load gooit "Bitmap image is not valid", omdat een Windows Metafile een vector record stream is en geen DIB. Monochrome down-conversion is end to end een rasteroperatie, dus de tussenstap moet een rasterformaat zijn
Alleen een subregio van een pagina renderen
De tweede methode, RenderPageRegionToFile, rendert alleen een rechthoek van de pagina in plaats van het geheel. De use cases zijn vertrouwd zodra je ooit een document viewer hebt gebouwd: een signature block uit een contract croppen, een tile maken voor een ingezoomde kaart van een grote tekening, of één gestempeld gebied voor een thumbnail pakken zonder de hele pagina op hoge DPI te rasteriseren. De signature is rechttoe rechtaan:
De clipstring is vier komma-gescheiden doubles in PDF points, handmatig geparsed in de methode om locale- en DelimitedText-eigenaardigheden te omzeilen. Uit de breedte en hoogte berekent de methode de output bitmapmaat als Round(Width * DPI / 72) bij Round(Height * DPI / 72), maakt een in-memory pf24bit bitmap van exact die grootte aan en rendert daarin via RenderPageToDCClip. Het resultaatbestand bevat alleen de geclipte rechthoek, geschaald op de regio in plaats van op de hele pagina
De clip-parameter die niets deed
Hier was het werk scherper dan het leek. RenderPageToDCClip droeg al lang een Clip-parameter mee, en die loog. De call accepteerde het argument, gaf het door aan TPDFPageTree.RenderPageToDC, en die implementatie negeerde het volledig, en gaf het nooit door aan de renderer. Je kon elke rechthoek passeren die je wilde en kreeg de hele pagina terug. Wie RenderPageToDCClip had aangesloten in de verwachting te croppen, kreeg een full-page render en had dat, afhankelijk van de lay-out, misschien niet eens gemerkt
v3.83.0 zette die draad vast. RenderPageToDC parseert nu dezelfde "Left,Top,Width,Height"-rechthoek in points en past die toe als een echte GDI clip region op de doel-device-context voordat de renderer tekent. De conversie van points naar device pixels is de gebruikelijke DPI / 72-schaalfactor, toegepast op alle vier randen. De reeks rond de render is de standaard save/clip/restore-dans:
De SaveDC / RestoreDC(-1)-combinatie is wat dit veilig maakt om herhaaldelijk aan te roepen: de clipregion wordt op de DC state stack gezet, de pagina wordt getekend en de originele clip wordt teruggezet, ongeacht hoe de render eindigt. RestoreDC(TargetDC, -1) herstelt de meest recent opgeslagen staat, wat de standaard idiom is voor gebalanceerde save/restore. Laat de restore weg, en een aanroeper die dezelfde DC opnieuw gebruikt voor een volgende full-page render, zou hem mysterieuze clipped aantreffen tot de laatste regio. Het repareren van de dode parameter fixeerde ook RenderPageRegionToFile gratis, omdat die nieuwe methode exact via dit pad loopt
Eén gedragsfeit om vast te houden: de clip cropt, hij schaalt niet. De pagina wordt nog steeds op de door jou gevraagde DPI gerasterd, in zijn normale positie, en de clip region gooit simpelweg alles buiten de rechthoek weg. Je zoomt de regio niet om de output te vullen; je snijdt een venster uit de full-resolution render. Als je een regio vergroot wilt, verhoog dan de DPI. De rechthoekcoördinaten worden geïnterpreteerd in device space na de points-to-pixels-schaal, gemeten vanaf de linkerbovenhoek van het gerenderde oppervlak, dus plan je Left en Top vanaf de bovenkant van de pagina omlaag. Voor een diepere tour van hoe PDFlibPas een device context voor on-screen output gebruikt, loopt het begeleidende stuk over print preview and device-context output door dezelfde DC-plumbing vanuit de displaykant
De eerlijke grens: 1-bit BMP, geen G4 TIFF
Het zou makkelijk zijn dit te oversellen als "fax-ready output", dus hier staat de grens helder. RenderPageToMonochromeFile produceert een pf1bit BMP. Het produceert geen CCITT Group 4 TIFF, het formaat dat een echte faxworkflow of een TIFF-archief meestal verwacht. De reden is concreet en geen nalatigheid: PDFlibPas' CCITT-unit decodeert momenteel G4-streams, maar heeft geen G4 encoder. Zonder encoder is er nergens om gecomprimeerde monochrome runs te schrijven, dus het monochrome pad stopt bij een ongecomprimeerde 1-bit DIB
In de praktijk is dat nog steeds bruikbaar. Een 1-bit BMP is het juiste pixelformaat, geditherd en klaar, en de meeste fax-, archief- of OCR-toolchains nemen hem graag op of zetten hem zelf in één vervolgstap om naar G4. Maar als jouw eis letterlijk een Group 4 TIFF rechtstreeks uit de bibliotheek is, dan is dit dat nog niet, en moet je zelf een compressiestap plannen. Weten waar een feature stopt is net zo waardevol als weten wat hij doet
Beide methoden zijn bewust klein, en dat is de ontwerples die je van deze pagina moet meenemen: een convenience API bovenop een renderer kan echte mogelijkheden toevoegen, monochrome output en regio-cropping, zonder in de rasterizer te grijpen en alle andere aanroepen te destabiliseren. Wanneer je voor de onderliggende rasterisatie wel moet kiezen tussen rendering engines, bespreekt het overzicht van multi-engine PDF rendering in Delphi de trade-offs diepgaand. Om het volledige renderoppervlak en de rest van de API te zien, heeft de PDFlibPas Delphi PDF Library-productpagina het volledige plaatje
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;// 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');// 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');// 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);