Τεχνικό Άρθρο

Απόδοση σελίδων PDF σε μονοχρωμία 1 bit στο Delphi

Μια πύλη fax δεν θέλει την απόδοση της σελίδας σας σε 24 bit. Ούτε η αρχειακή ροή που αποθηκεύει ένα εκατομμύριο τιμολόγια που μοιάζουν σαρωμένα, ούτε το front-end OCR που κατωφλίζει τα πάντα σε ασπρόμαυρο πριν καν ψάξει για χαρακτήρα. Και οι τρεις θέλουν το ίδιο πράγμα: ένα καθαρό bitmap 1 bit, ένα bit ανά εικονοστοιχείο, όπου κάθε κουκκίδα είναι είτε μελάνι είτε χαρτί. Δώστε τους ένα BMP πλήρους χρώματος και έτσι κι αλλιώς θα πετάξουν τα 23 bit ανά εικονοστοιχείο, συνήθως με χειρότερο πέρασμα dithering από αυτό που θα μπορούσατε να είχατε κάνει μόνοι σας. Το ενδιαφέρον ερώτημα είναι πού πρέπει να συμβεί αυτή η υποβιβαστική μετατροπή, και η απάντηση στο PDFlibPas τελικά λέει κάτι χρήσιμο για το πώς να επεκτείνεις έναν renderer που θα προτιμούσες να μην ξαναγράψεις

Το PDFlibPas είναι μια εγγενής βιβλιοθήκη PDF σε Object Pascal για Delphi και C++Builder. Ο πυρήνας απόδοσής της μετατρέπει μια σελίδα σε bitmap και μπορεί να εξαγάγει BMP, PNG, JPEG, WMF και μερικές ακόμη μορφές. Μέχρι πρόσφατα όμως δεν μπορούσε να επιστρέψει ένα πραγματικό μονοχρωματικό bitmap ούτε να αποδώσει μόνο ένα μέρος μιας σελίδας. Και τα δύο ήρθαν στο v3.83.0, και τα δύο χτίστηκαν ως λεπτά επίπεδα ευκολίας πάνω από τον υπάρχοντα renderer αντί για αλλαγές στον ίδιο τον rasterizer. Αυτός ο περιορισμός είναι όλη η ιστορία

Γιατί να γίνει η υποβιβαστική μετατροπή μετά την απόδοση, όχι μέσα στον renderer

Ο προφανής τρόπος για να παραχθεί μια εικόνα 1 bit είναι να πεις στον rasterizer να σχεδιάσει σε 1 bit. Αυτός είναι επίσης ο τρόπος να χαλάσουν όλα τα υπόλοιπα. Το εσωτερικό bitmap του renderer δημιουργείται με ένα hardcoded PixelFormat := pf24bit στο PDFlibRenderer constructor, και αυτή η επιφάνεια 24 bit μοιράζεται σε κάθε διαδρομή απόδοσης: εξαγωγή PNG, προεπισκόπηση με device context, έξοδος JPEG, τα πάντα. Αν το γυρίσεις σε pf1bit στην πηγή, δεν έχεις προσθέσει μια δυνατότητα μονοχρωμίας, έχεις υποβαθμίσει την πιστότητα χρώματος για κάθε caller στη βιβλιοθήκη και έχεις αναλάβει να διορθώσεις μια ντουζίνα δευτερογενείς παλινδρομήσεις

Έτσι, RenderPageToMonochromeFile ακολουθεί την αντίθετη διαδρομή. Αποδίδει τη σελίδα κανονικά, σε ένα προσωρινό BMP 24 bit, και μόνο μετά το συμπτύσσει σε 1 bit ως βήμα μετα-επεξεργασίας. Ο renderer μένει ανέγγιχτος. Η συμπεριφορά μονοχρωμίας ζει ολόκληρη στη μέθοδο ευκολίας, πράγμα που σημαίνει ότι δεν μπορεί να επηρεάσει κανέναν που δεν τη καλεί. Αυτό είναι το είδος συμβιβασμού που αξίζει να κατονομάζεται ρητά: η μετα-επεξεργασία πληρώνει μία επιπλέον δέσμευση bitmap και ένα προσωρινό αρχείο, και ως αντάλλαγμα κρατά έναν πυρήνα κρίσιμης σημασίας εντελώς εκτός πεδίου. Για μια δυνατότητα που υπάρχει για να εξυπηρετεί περιπτώσεις fax και αρχειοθέτησης, αυτή είναι η σωστή πλευρά του λογαριασμού

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;

Πώς γίνεται πραγματικά η σύμπτυξη σε 1 bit

Η υποβιβαστική μετατροπή βασίζεται στο GDI αντί για έναν χειροποίητο βρόχο κατωφλίωσης, και η επιλογή έχει σημασία για την ποιότητα εξόδου. Μέσα στη μέθοδο το προσωρινό bitmap 24 bit φορτώνεται σε ένα TBitmap δεύτερο TBitmap δημιουργείται με PixelFormat := pf1bit στις ίδιες διαστάσεις, και τα εικονοστοιχεία μεταφέρονται με ένα μόνο 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');

Το τέχνασμα είναι SetStretchBltMode με HALFTONE. Παρότι η πηγή και ο προορισμός έχουν το ίδιο μέγεθος, άρα δεν γίνεται κλιμάκωση, η λειτουργία stretch εξακολουθεί να καθορίζει πώς το GDI αντιστοιχίζει τα χρώματα στην παλέτα 1 bit. HALFTONE κάνει να εφαρμόζεται ημιτονική dithering, μετατρέποντας τις γκρι περιοχές και τα εξομαλυμένα άκρα κειμένου σε μοτίβα από μαύρες και λευκές κουκκίδες αντί για σκληρή αποκοπή στο πλησιέστερο από τα δύο χρώματα. Αν παραλείψεις την κλήση της λειτουργίας ή χρησιμοποιήσεις την προεπιλογή BLACKONWHITE, τα γκρι κλιμακώνονται σε ογκώδη κατωφλισμένα σχήματα. Για έξοδο από σαρωμένα έγγραφα και προεπεξεργασία OCR, το αποτέλεσμα με dithering είναι σχεδόν πάντα αυτό που θέλεις

Μια λεπτομέρεια είναι αδιαπραγμάτευτη και εύκολα γίνεται λάθος: η προσωρινή απόδοση πρέπει να είναι BMP. RenderPageToMonochromeFile καλεί τον γενικό renderer με κωδικό επιλογών 0, ο οποίος είναι BMP. Το όρισμα options στο RenderPageToFile είναι ένα μικρό ακέραιο enum, και οι τιμές δεν είναι εναλλάξιμες για αυτόν τον σκοπό: 0 είναι BMP, 1 JPEG, 2 WMF, 3 EMF, 5 PNG, και ούτω καθεξής. Στη συνέχεια ο down-converter κάνει TBitmap.LoadFromStream στο προσωρινό αρχείο. Αν του δώσεις WMF, περνώντας 2, και αυτή η φόρτωση ρίχνει "Bitmap image is not valid", επειδή ένα Windows Metafile είναι μια ροή διανυσματικών εγγραφών, όχι ένα DIB. Η μονοχρωματική down-conversion είναι μια raster λειτουργία από άκρη σε άκρη, άρα το ενδιάμεσο πρέπει να είναι raster format

Απόδοση μόνο μιας υποπεριοχής μιας σελίδας

Η δεύτερη μέθοδος, RenderPageRegionToFile, αποδίδει μόνο ένα ορθογώνιο της σελίδας αντί για ολόκληρο το πράγμα. Οι χρήσεις είναι γνώριμες μόλις έχεις φτιάξει οποιοδήποτε πρόγραμμα προβολής εγγράφων: περικοπή ενός μπλοκ υπογραφής από ένα συμβόλαιο, δημιουργία ενός πλακιδίου για έναν μεγεθυμένο χάρτη ενός μεγάλου σχεδίου ή εξαγωγή μιας σφραγισμένης περιοχής για μικρογραφία χωρίς να πληρώσεις για να μετατρέψεις ολόκληρη τη σελίδα σε bitmap σε υψηλό DPI. Η υπογραφή είναι απλή:

// 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 είναι τέσσερις διπλές τιμές χωρισμένες με κόμματα, αναλυμένες χειροκίνητα μέσα στη μέθοδο για να παρακαμφθούν τα ζητήματα τοπικής ρύθμισης και DelimitedText ιδιορρυθμίες. Από το πλάτος και το ύψος η μέθοδος υπολογίζει το μέγεθος του bitmap εξόδου ως Round(Width * DPI / 72) επί Round(Height * DPI / 72), δεσμεύει ένα in-memory pf24bit bitmap ακριβώς αυτού του μεγέθους και αποδίδει στο device context του μέσω RenderPageToDCClip. Το αρχείο αποτελέσματος περιέχει μόνο το κομμένο ορθογώνιο, σε μέγεθος της περιοχής και όχι ολόκληρης της σελίδας

Η παράμετρος clip που δεν έκανε τίποτα

Εδώ ήταν που η δουλειά ήταν πιο κοφτερή απ' όσο φαίνεται. RenderPageToDCClip κουβαλούσε μια Clip παράμετρο για πολύ καιρό, και αυτό ήταν ψέμα. Η κλήση δεχόταν το όρισμα, το προωθούσε στο TPDFPageTree.RenderPageToDC, και εκείνη η υλοποίηση το αγνοούσε εντελώς, χωρίς να το παραδίδει ποτέ στον renderer. Θα μπορούσες να περάσεις όποιο ορθογώνιο ήθελες και να πάρεις πίσω ολόκληρη τη σελίδα. Όποιος είχε συνδέσει το RenderPageToDCClip περιμένοντας περικοπή έπαιρνε απόδοση ολόκληρης της σελίδας και, ανάλογα με τη διάταξή του, ίσως να μην το είχε προσέξει

Η v3.83.0 έκανε τη σύνδεση. RenderPageToDC τώρα αναλύει το ίδιο "Left,Top,Width,Height" ορθογώνιο σε σημεία και το εφαρμόζει ως πραγματική περιοχή αποκοπής GDI στο target device context πριν σχεδιάσει ο renderer. Η μετατροπή από points σε pixels συσκευής γίνεται με τον συνήθη DPI / 72 συντελεστή κλίμακας, εφαρμοσμένο και στα τέσσερα άκρα. Η ακολουθία γύρω από το render είναι ο κλασικός χορός save/clip/restore:

// 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 / RestoreDC(-1) ζεύγος είναι αυτό που κάνει αυτή την κλήση ασφαλή επανειλημμένα: η περιοχή αποκοπής ωθείται στη στοίβα κατάστασης του DC, η σελίδα σχεδιάζεται και το αρχικό clip επανέρχεται ανεξάρτητα από το πώς τερματίζει το render. RestoreDC(TargetDC, -1) επαναφέρει την πιο πρόσφατα αποθηκευμένη κατάσταση, που είναι η κλασική ιδιωματική λύση για ισορροπημένο save/restore. Παράλειψε το restore και ένας caller που επαναχρησιμοποιεί το ίδιο DC για επόμενη απόδοση ολόκληρης σελίδας θα το έβρισκε μυστηριωδώς κομμένο στην τελευταία περιοχή. Η διόρθωση της νεκρής παραμέτρου διόρθωσε επίσης RenderPageRegionToFile δωρεάν, επειδή η νέα μέθοδος περνά ακριβώς από αυτό το μονοπάτι

Ένα σημείο συμπεριφοράς που αξίζει να αφομοιώσεις: το clip περικόπτει, δεν κλιμακώνει. Η σελίδα εξακολουθεί να ραστεροποιείται στο DPI που ζήτησες, στη φυσική της θέση, και η περιοχή αποκοπής απλώς απορρίπτει ό,τι βρίσκεται έξω από το ορθογώνιο. Δεν κάνεις ζουμ στην περιοχή για να γεμίσει την έξοδο, κόβεις ένα παράθυρο από την απόδοση πλήρους ανάλυσης. Αν θέλεις μια περιοχή μεγεθυμένη, αύξησε το DPI. Οι συντεταγμένες του ορθογωνίου ερμηνεύονται στον χώρο της συσκευής μετά την κλιμάκωση από points σε pixels, μετρώντας από την πάνω αριστερή γωνία της επιφάνειας απόδοσης, άρα σχεδίασε τις Left και Top από την κορυφή της σελίδας προς τα κάτω. Για μια πιο βαθιά περιήγηση στο πώς το PDFlibPas οδηγεί ένα device context για έξοδο στην οθόνη, το συνοδευτικό άρθρο για print preview and device-context output περνά από την ίδια σωλήνωση DC από την πλευρά της προβολής

Το ειλικρινές όριο: 1-bit BMP, όχι G4 TIFF

Θα ήταν εύκολο να το πουλήσει κανείς ως "fax-ready output", οπότε εδώ είναι το όριο δηλωμένο καθαρά. RenderPageToMonochromeFile παράγει ένα pf1bit BMP. Δεν παράγει ένα CCITT Group 4 TIFF, που είναι η μορφή που συνήθως περιμένει μια πραγματική ροή εργασίας fax ή ένα αρχείο TIFF. Ο λόγος είναι συγκεκριμένος και όχι παράλειψη: η μονάδα CCITT του PDFlibPas προς το παρόν αποκωδικοποιεί ροές G4 αλλά δεν έχει G4 κωδικοποιητή. Χωρίς κωδικοποιητή δεν υπάρχει πού να γραφτούν συμπιεσμένες μονόχρωμες ακολουθίες, οπότε η μονόχρωμη διαδρομή σταματά σε ένα ασυμπίεστο 1-bit DIB

Στην πράξη αυτό παραμένει χρήσιμο. Ένα 1-bit BMP είναι η σωστή μορφή pixel, με dithering και έτοιμο, και τα περισσότερα εργαλεία fax, αρχειοθέτησης ή OCR θα το δεχτούν ευχαρίστως ή θα το μετατρέψουν σε G4 μόνα τους με ένα βήμα πιο κάτω στη ροή. Αλλά αν η απαίτησή σου είναι κυριολεκτικά ένα Group 4 TIFF κατευθείαν από τη βιβλιοθήκη, αυτό δεν είναι ακόμη αυτό, και πρέπει να προβλέψεις δικό σου στάδιο συμπίεσης. Το να ξέρεις πού σταματά ένα χαρακτηριστικό αξίζει όσο το να ξέρεις τι κάνει

Και οι δύο μέθοδοι είναι σκόπιμα μικρές, και αυτό είναι το μάθημα σχεδίασης που αξίζει να πάρεις μαζί σου από αυτή τη σελίδα: ένα convenience API που κάθεται πάνω από έναν renderer μπορεί να προσθέσει πραγματική δυνατότητα, μονόχρωμη έξοδο, περικοπή περιοχής, χωρίς να μπει μέσα στον rasterizer και να αποσταθεροποιήσει κάθε άλλο caller. Όταν χρειαστεί να διαλέξεις ανάμεσα σε rendering engines για το υποκείμενο rasterization, η επισκόπηση του multi-engine PDF rendering in Delphi καλύπτει σε βάθος τους συμβιβασμούς. Για να δεις ολόκληρη την επιφάνεια απόδοσης και το υπόλοιπο API, το PDFlibPas Delphi PDF Library η σελίδα προϊόντος δίνει την πλήρη εικόνα