Techninis straipsnis

PDF puslapių atvaizdavimas į 1 bitų monochromą Delphi aplinkoje

Fakso šliuzui jūsų 24 bitų puslapio atvaizdas nereikalingas. Jo nenori ir archyvavimo grandinė, kuri saugo milijoną nuskenuotų sąskaitų vaizdo pavidalu, nei OCR priekinė dalis, kuri dar prieš ieškodama simbolio viską nuleidžia iki juodos ir baltos. Visoms trims reikia to paties: švaraus 1 bito bitinio žemėlapio, vieno bito kiekvienam pikseliui, kuriame kiekvienas taškas yra arba rašalas, arba popierius. Paduokite joms pilnaspalvį BMP ir jos vis tiek išmes 23 bitus iš kiekvieno pikselio, paprastai dar ir blogesniu dithering etapu, nei būtumėte padarę patys. Įdomus klausimas yra kur šis konvertavimas turėtų įvykti, o atsakymas PDFlibPas atveju pasako ir ką nors naudinga apie tai, kaip plėsti renderintuvą, kurio nenorite perrašyti

PDFlibPas yra gimtoji Object Pascal PDF biblioteka, skirta Delphi ir C++Builder. Jos renderinimo branduolys rastrina puslapį į bitinį žemėlapį ir gali išvesti BMP, PNG, JPEG, WMF bei kelis kitus formatus. To, ko iki visai neseniai ji nemokėjo, buvo grąžinti tikrą monochrominį bitinį žemėlapį arba atvaizduoti tik dalį puslapio. Abi galimybės atsirado v3.83.0, ir abi buvo sukurtos kaip ploni patogumo sluoksniai virš esamo renderintuvo, o ne kaip pakeitimai pačiam rasterizatoriui. Šis apribojimas ir yra visa istorija

Kodėl konvertuoti po renderinimo, o ne renderintuvo viduje

Akivaizdus būdas gauti 1 bitų vaizdą yra liepti rasterizatoriui piešti 1 bito formatu. Tai kartu yra ir būdas sugadinti visa kita. Renderintuvo vidinis bitinis žemėlapis sukuriamas su kietai įrašytu PixelFormat := pf24bit konstruktoriuje PDFlibRenderer, o šis 24 bitų paviršius bendras visiems renderinimo keliams: PNG eksportui, peržiūrai įrenginio kontekste, JPEG išvesčiai, viskam. Pakeiskite jį į pf1bit pačiame šaltinyje ir jūs ne pridėjote monochrominę funkciją, o pabloginote spalvų tikslumą kiekvienam bibliotekos kviesiančiajam bei pasirašėte po tuzino vėlesnių regresijų derinimu

Todėl RenderPageToMonochromeFile pasuka priešingu keliu. Jis puslapį atvaizduoja įprastai, į laikiną 24 bitų BMP, ir tik tada po apdorojimo suspaudžia jį iki 1 bito. Pats renderintuvas lieka nepaliestas. Monochrominė elgsena gyvena tik patogumo metode, vadinasi, ji negali paveikti nieko, kas jo nekviečia. Tokį kompromisą verta įvardyti atvirai: postprocess žingsnis sumoka vienu papildomu bitinio žemėlapio paskirstymu ir laikinu failu, o mainais laiko svorį nešantis branduolys visiškai iškrenta iš scope. Funkcijai, kuri skirta faktiškai tik fakso ir archyvavimo kampiniams atvejams, tai teisinga sandorio pusė

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;

Kaip iš tikrųjų vyksta suspaudimas iki 1 bito

Konvertavimas žemyn remiasi GDI, o ne ranka rašytu slenksčio ciklu, ir šis pasirinkimas svarbus išvesties kokybei. Metodo viduje laikinas 24 bitų bitinis žemėlapis įkeliamas į TBitmap, antras TBitmap sukuriamas su PixelFormat := pf1bit tomis pačiomis dimensijomis, o pikseliai perkeliami vienu vieninteliu blit veiksmu:

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

Triukas yra SetStretchBltMode su HALFTONE. Nors šaltinis ir paskirtis yra tokio paties dydžio, todėl mastelio keitimo nevyksta, stretch režimas vis tiek nusprendžia, kaip GDI spalvas perkelia į 1 bitų paletę. HALFTONE verčia taikyti halftone dithering, todėl pilkos sritys ir antialiasinti teksto kraštai virsta juodų ir baltų taškų raštais, o ne kietu nukirpimu iki artimiausios iš dviejų spalvų. Pašalinkite šį kvietimą arba naudokite numatytąjį BLACKONWHITE, ir pilkų tonų turinys subyra į grubiai slenksčiuotas formas. Nuskenuotų dokumentų ir OCR paruošimo išvestims dithering rezultatas beveik visada yra tai, ko norite

Viena detalė yra nederinama ir ją lengva sugadinti: laikinas renderis turi būti BMP. RenderPageToMonochromeFile kviečia bendrą renderintuvą su parinkčių kodu 0, o tai reiškia BMP. Parinkčių argumentas metode RenderPageToFile yra mažas sveikasis enum, ir šioje vietoje reikšmės nėra sukeičiamo pobūdžio: 0 yra BMP, 1 JPEG, 2 WMF, 3 EMF, 5 PNG ir taip toliau. Po to mažintuvas kviečia TBitmap.LoadFromStream laikinam failui. Paduokite jam WMF, perduodami 2, ir įkėlimas išmes „Bitmap image is not valid“, nes Windows Metafile yra vektorinių įrašų srautas, o ne DIB. Monochrominis konvertavimas žemyn nuo pradžios iki galo yra rastrinė operacija, todėl tarpinis formatas irgi turi būti rastrinis

Kaip atvaizduoti tik puslapio subregioną

Antrasis metodas, RenderPageRegionToFile, atvaizduoja tik puslapio stačiakampį, o ne visą puslapį. Panaudojimo atvejai pažįstami kiekvienam, kas bent kartą kūrė dokumentų peržiūros programą: iškirpti parašo bloką iš sutarties, sugeneruoti priartinto didelio brėžinio žemėlapio plytelę arba ištraukti vieną antspauduotą regioną miniatiūrai, nemokant už viso puslapio rasterizavimą aukštu DPI. Signatūra yra tiesmuka:

// 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 eilutė yra keturios kableliais atskirtos double reikšmės PDF taškais, kurias metodas parsina rankiniu būdu, kad apeitų locale ir DelimitedText kaprizus. Iš pločio ir aukščio metodas suskaičiuoja išvesties bitinio žemėlapio dydį kaip Round(Width * DPI / 72) kart Round(Height * DPI / 72), paskiria tiksliai tokio dydžio atmintinį pf24bit bitinį žemėlapį ir per RenderPageToDCClip atvaizduoja į jo device context. Rezultato faile lieka tik iškirptas stačiakampis, kurio dydis atitinka regioną, o ne visą puslapį

Clip parametras, kuris nieko nedarė

Štai čia darbas pasirodė aštresnis, nei atrodė. RenderPageToDCClip jau seniai turėjo parametrą Clip, ir tai buvo melas. Kvietimas argumentą priimdavo, perduodavo jį žemyn į TPDFPageTree.RenderPageToDC, o ši implementacija jį visiškai ignoruodavo ir niekada nepaduodavo renderintuvui. Galėjote perduoti bet kokį stačiakampį ir vis tiek gauti visą puslapį. Kiekvienas, kuris tikėdamasis iškirpimo buvo pasikabinęs ant RenderPageToDCClip, iš tiesų gaudavo viso puslapio renderį ir, priklausomai nuo išdėstymo, galėjo to net nepastebėti

v3.83.0 sujungė laidą. RenderPageToDC dabar parsina tą patį "Left,Top,Width,Height" taškais apibrėžtą stačiakampį ir prieš renderintuvui piešiant pritaiko jį kaip tikrą GDI clip region ant tikslinio device context. Konvertavimas iš taškų į įrenginio pikselius yra įprastas DPI / 72 mastelio koeficientas, taikomas visoms keturioms kraštinėms. Seka aplink renderį yra standartinis save/clip/restore šokis:

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

Pora SaveDC ir RestoreDC(-1) daro šį kelią saugų pakartotiniams kvietimams: clip region įstumiamas į DC būsenos steką, puslapis nupiešiamas, o originalus clip grąžinamas nepriklausomai nuo to, kaip renderis baigiasi. RestoreDC(TargetDC, -1) atstato paskutinę išsaugotą būseną, ir tai yra standartinė subalansuoto save/restore idiom. Praleiskite atkūrimą, ir kviesiantysis, kuris tą patį DC panaudos vėlesniam viso puslapio renderiui, staiga ras jį mįslingai nukirptą iki paskutinio regiono. Sutvarkius negyvą parametrą, nemokamai susitvarkė ir RenderPageRegionToFile, nes naujasis metodas eina būtent šiuo keliu

Vieną elgsenos dalyką verta įsidėmėti: clip iškirpia, jis ne masteliuoja. Puslapis vis tiek rastrinamas tuo DPI, kurio paprašėte, savo įprastoje padėtyje, o clip region tiesiog išmeta viską, kas yra už stačiakampio ribų. Jūs nepriartinate regiono iki visos išvesties, jūs iš pilnos raiškos renderio išpjaunate langą. Jei norite regioną padidinti, kelkite DPI. Stačiakampio koordinatės interpretuojamos įrenginio erdvėje po taškų į pikselius mastelio keitimo, matuojant nuo viršutinio kairiojo renderinto paviršiaus kampo, todėl savo Left ir Top planuokite skaičiuodami žemyn nuo puslapio viršaus. Išsamesnį žvilgsnį į tai, kaip PDFlibPas per device context tiekia vaizdą ekranui, pateikia palydimasis straipsnis apie print preview ir device-context išvestį, kuris tą pačią DC santechniką aiškina iš rodymo pusės

Sąžininga riba: 1 bitų BMP, ne G4 TIFF

Būtų lengva tai parduoti kaip „faksui paruoštą išvestį“, todėl čia riba įvardyta tiesiai. RenderPageToMonochromeFile sukuria pf1bit BMP. Jis nesukuria CCITT Group 4 TIFF, kuris paprastai reikalingas tikram fakso srautui arba TIFF archyvui. Priežastis konkreti, o ne tiesiog praleista galimybė: PDFlibPas CCITT modulis šiuo metu moka dekoduoti G4 srautus, bet neturi G4 encoder. Be encoder nėra kur įrašyti suspaustų monochrominių eigų, todėl monochrominis kelias sustoja ties nesuspaustu 1 bitų DIB

Praktikoje tai vis tiek naudinga. 1 bitų BMP yra teisingas pikselių formatas, jau paditheringintas ir paruoštas, o dauguma fakso, archyvavimo ar OCR grandinių jį mielai priims arba vienu žingsniu žemiau pavers į G4 pačios. Bet jei jūsų reikalavimas tiesiogine prasme yra Group 4 TIFF tiesiai iš bibliotekos, tai dar ne tas atvejis, ir jums reikia planuoti savo suspaudimo etapą. Žinoti, kur funkcija baigiasi, verta tiek pat, kiek žinoti, ką ji daro

Abu metodai yra sąmoningai maži, ir tai yra svarbiausia dizaino pamoka, kurią verta pasiimti iš šio puslapio: patogumo API, sėdinti ant renderintuvo, gali pridėti tikrą galimybę, monochrominę išvestį, regiono iškirpimą, nelįsdama į rasterizatorių ir neišbalansuodama kiekvieno kito kviesiančiojo. Kai vis dėlto reikia rinktis tarp renderinimo variklių pačiam pagrindiniam rastrinimui, daugiavariklio PDF renderinimo Delphi aplinkoje apžvalga aptaria kompromisus išsamiai. O visą renderinimo paviršių ir likusį API vaizdą pateikia PDFlibPas Delphi PDF Library produkto puslapis