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 PDF Library for Delphi atveju pasako ir ką nors naudinga apie tai, kaip plėsti renderintuvą, kurio nenorite perrašyti

PDF Library for Delphi 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ė

PDF Library for Delphi procesų grandinė, kurioje PDF puslapis atvaizduojamas į laikiną 24 bitų BMP, GDI HALFTONE blit suspaudžiamas į 1 bito vienspalvę taškinę grafiką ir suvartojamas faksų, archyvavimo ir OCR darbo eigų
Naujieji metodai pirmiausia atvaizduoja viso spalvų diapazono puslapį, tada konvertuoja paruoštąjį rastrą už atvaizduotojo ribų. Faksų šliuzai, archyvinės saugyklos ir OCR sąsajos gauna tikrąjį pf1bit bitmap
var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.LoadFromFile('invoice.pdf');
    // 200 DPI yra klasikinė Group 4 fakso raiška; puslapio indeksas skaičiuojamas nuo 1
    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:

PDF Library for Delphi: GDI suspaudimo detalė, lyginanti HALFTONE tempimo blit, sulysiantį pilkus atspalvius į taškų raštus, su gabiu BLACKONWHITE numatytuoju slenkstiu
RenderPageToMonochromeFile viduje vienas StretchBlt perkelia kiekvieną pikselį ant to paties dydžio pf1bit paviršiaus. Su įjungtu HALFTONE pilkos spalvos virsta ditheriuotaisiais tušo taškų raštais vietoje blokinių figūrų, kurias duoda numatytasis slenkstis
// RenderPageToMonochromeFile viduje, po 24 bitų ColorBmp įkėlimo
MonoBmp.PixelFormat := pf1bit;
MonoBmp.Width  := ColorBmp.Width;
MonoBmp.Height := ColorBmp.Height;
// HALFTONE nurodo GDI sumaišyti (dither) 24 bitų šaltinį iki 1 bito
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:

PDF Library for Delphi: srities iškarpos atvaizdavimas PDF taškais: 72,72,180,72 stačiakampis pilnos raiškos atvaizduotame PDF puslapyje virsta 375 į 150 pikselių taškine grafika ties 150 DPI, nes iškarpa apkarpo, o ne keičia mastelį
RenderPageRegionToFile iškirpia langą, matuojamą PDF taškais, iš pilnos raiškos atvaizdavimo. Išvesties bitmap dydį lemia plotis ir aukštis, padauginti iš DPI ir padalyti iš 72, o niekada nesumažintas visas puslapis
// Clip yra "Left,Top,Width,Height" PDF taškais (72 pt = 1 colis)
// Čia: 2,5x1 colio dėžė, viena colio atstumu nuo puslapio viršutinio kairiojo kampo
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:

// TPDFPageTree.RenderPageToDC viduje, kai Clip nėra tuščias
ScaleFactor := DPI / 72;
SaveDC(TargetDC);
IntersectClipRect(TargetDC,
  Round(ClipLeft * ScaleFactor),
  Round(ClipTop * ScaleFactor),
  Round((ClipLeft + ClipWidth) * ScaleFactor),
  Round((ClipTop + ClipHeight) * ScaleFactor));
// ... čia atvaizdavimo variklis nupiešia puslapį ...
// finally bloke:
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 PDF Library for Delphi 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ė: PDF Library for Delphi 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 PDF Library for Delphi Delphi PDF Library produkto puslapis