Δύο παράπονα φτάνουν την εβδομάδα μετά την κυκλοφορία μιας λειτουργίας συμπίεσης: η σαρωμένη σύμβαση έχει τώρα σκαλοπατωμένα, χνουδωτά γράμματα, και το διάφανο λογότυπο στο εξώφυλλο κάθεται μέσα σε ένα χλωμό φωτοστέφανο. Το PDFiumPas απαντά και στα δύο σε ένα σημείο. Το TPdf.OptimizeImages μετρά κάθε εικόνα πριν τη συρρικνώσει, και μετά επιλέγει πυρήνα επαναδειγματοληψίας και αθροίζει χρώμα σε μορφή με επίγνωση alpha
Αυτό δεν ίσχυε πάντα. Πριν τη v3.100.0 η ίδια μέθοδος επαναδειγματοληπτούσε κάθε μη bilevel εικόνα με ένα σταθερό βήμα πλησιέστερου γείτονα, που είναι ακριβώς ο αλγόριθμος που παράγει και τα δύο παράπονα: λαμβάνει σημειακά ένα pixel πηγής ανά pixel εξόδου, και αντιμετωπίζει το RGB που κάθεται κάτω από ένα πλήρως διάφανο pixel σαν να θα το έβλεπε ποτέ αναγνώστης. Η επανεγγραφή στη v3.100.0 αντικαθιστά αυτή τη μοναδική διαδρομή με πέντε πυρήνες, έναν μετρημένο κανόνα επιλογής και έναν ρητό προϋπολογισμό μνήμης εργασίας
Γιατί η επαναδειγματοληψία κάνει το σαρωμένο κείμενο να φαίνεται τραχύ;
Επειδή η σημειακή δειγματοληψία απαντά λάθος ερώτηση. Όταν μια σάρωση 300 DPI επαναπροσδιορίζεται σε 150 DPI, κάθε pixel προορισμού αντιπροσωπεύει ένα μπλοκ δύο-επί-δύο pixels πηγής, και ο πλησιέστερος γείτονας κρατά ένα από τα τέσσερα και πετάει τα υπόλοιπα. Ποιο επιζεί εξαρτάται από τη στρογγυλοποίηση, οπότε μια ακμή πινάκου που ήταν ομαλά εξομαλυμένη στην πηγή γίνεται ρίψη νομίσματος ανά pixel. Το αποτέλεσμα είναι η κλασική σκαλωτή σκάλωση κατά μήκος των ακμών των glyphs, συν moire σε περιοχές halftone όπου τα discarded δείγματα τυχαινόταν να μεταφέρουν το μοτίβο. Αυτό μετρά περισσότερο σε ένα PDF παρά στην οθόνη επειδή η ζημιά είναι μόνιμη. Ένα image XObject μεταφέρει τα δείγματά του μαζί με /Width, /Height και /BitsPerComponent (ISO 32000-1 §8.9.5), και η επαναδειγματοληψία ξαναγράφει και τα τρία μέσα στο αρχείο. Ένα κακό zoom σε viewer είναι ένα καρέ που μπορείς να ξαναζωγραφίσεις, και το PDFiumPas έχει ξεχωριστή μηχανή για αυτό στο render cache και απόδοση zoom. Ένα κακό downsample είναι ένα νέο έγγραφο που δίνεις στον πελάτη
Πώς μετρά το PDFiumPas τη λεπτομέρεια και επιλέγει πυρήνα
Το PDFiumPas αποφασίζει ανά εικόνα, όχι ανά έγγραφο. Πριν επιλέξει πυρήνα υπολογίζει μια κανονικοποιημένη βαθμολογία λεπτομέρειας φωτεινότητας από ένα οριοθετημένο πλέγμα δειγματοληψίας: τα βήματα οριζόντια και κάθετα είναι (Width + 63) div 64 και (Height + 63) div 64, οπότε μια σάρωση 12000 pixels και μια μικρογραφία 300 pixels κοστίζουν περίπου την ίδια σάρωση 64-επί-64. Σε κάθε δειγματοληπτούμενη θέση αθροίζει την απόλυτη διαφορά προς τον γείτονα στα δεξιά και τον γείτονα από κάτω, σε έως τρία κανάλια, και μετά διαιρεί με το πλήθος δειγμάτων επί 255. Η βαθμολογία καταλήγει στο 0 έως 1, όπου επίπεδα επιχειρηματικά γραφικά κάθονται κοντά στο μηδέν και πυκνή φωτογραφική υφή ανεβαίνει
Η σκάλα επιλογής εκτελείται μετά με σταθερή σειρά. Αν το ResampleFilter είναι οτιδήποτε άλλο από pirfAdaptive, αυτό το φίλτρο χρησιμοποιείται κατά λέξη. Διαφορετικά: περιεχόμενο 1-bit παίρνει pirfBilevel· ένα ContentClass piccLineArt παίρνει pirfBox· ένας συντελεστής κλίμακας 4 ή περισσότερο παίρνει επίσης pirfBox, επειδή σε αυτή τη μείωση ένας μέσος όρος περιοχής είναι ταυτόχρονα η φθηνότερη και η πιο σωστή απάντηση· piccPhoto, βαθμολογία λεπτομέρειας 0.08 ή υψηλότερη, ή PreferredQuality 0.9 ή υψηλότερο παίρνει pirfLanczos με τον τρίλοβο πυρήνα του· κλίμακα 2 ή περισσότερο ή ποιότητα 0.7 ή υψηλότερη παίρνει pirfBicubic σε ακτίνα 2· ό,τι απομένει παίρνει pirfBilinear. Εφόσον το TPdfImageOptimizeOptions.Default ορίζει το PreferredQuality σε 0.85, μια προεπιλεγμένη εκτέλεση δεν πέφτει ποτέ πίσω σε bilinear εκτός αν η μείωση είναι ήπια και το περιεχόμενο επίπεδο
uses
PDFium;
procedure ShrinkScannedPdf(const InputFile, OutputFile: string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := InputFile;
// Προεπιλογές: TargetDpi 150, MinDpiRatio 1.5, PreserveBilevel True,
// MinDimension 8, pirfAdaptive, piccAuto, ποιότητα 0.85, προϋπολογισμός 64 MiB.
Options := TPdfImageOptimizeOptions.Default;
Options.TargetDpi := 150;
Options.MinDpiRatio := 1.5;
Options.ContentClass := piccAuto;
Options.PreferredQuality := 0.85;
if Pdf.OptimizeImages(Options, Report) and (Report.OptimizedCount > 0) then
Pdf.SaveAs(OutputFile);
finally
Pdf.Free;
end;
end;
Μια εικόνα αγγίζεται μόνο όταν η μεγαλύτερη από την οριζόντια και κάθετη DPI τοποθέτησής της, διαιρεμένη με το TargetDpi, φτάνει το MinDpiRatio. Αυτή η ασφάλεια υπάρχει ώστε μια φωτογραφία 160 DPI με στόχο 150 DPI να μην επανακωδικοποιείται για ένα κέρδος έξι τοις εκατό που κοστίζει μια γενιά ποιότητας. Εικόνες κάτω από MinDimension σε οποιονδήποτε άξονα, 8 από προεπιλογή, παραλείπονται ως εικονίδια ή γραμμές
Γιατί τα διάφανα λογότυπα αποκτούν λευκό περίγραμμα;
Επειδή το χρώμα κάτω από ένα πλήρως διάφανο pixel είναι αυθαίρετο, και ένας απλός σταθμισμένος μέσος όρος του δίνει δικαίωμα ψήφου. Εξαγάγετε ένα λογότυπο από εργαλείο σχεδίασης και το αόρατο περιθώριο είναι συχνά λευκό, ή μαύρο, ή ό,τι ήταν ο καμβάς· το κανάλι alpha το κρύβει, και ένα απλό άθροισμα πάνω στο αποτύπωμα του πυρήνα το αναμειγνύει γρήγορα πίσω στην ορατή ακμή. Το PDFiumPas το αποφεύγει αθροίζοντας δείγματα BGRA σε premultiplied μορφή και αναιρώντας την premultiplication μόνο στο pixel προορισμού
Συγκεκριμένα, κάθε συνεισφέρον δείγμα προσθέτει channel * alpha * weight στον συσσωρευτή χρώματος, alpha * weight σε έναν συσσωρευτή alpha και weight στο άθροισμα βαρών. Το χρώμα προορισμού διαιρείται μετά με τον συσσωρευτή alpha αντί για το άθροισμα βαρών, και αυτό είναι το βήμα που μετράει: η διαίρεση με το άθροισμα βαρών θα πασσάλευε το χρώμα προς τα αόρατα pixels, ενώ η διαίρεση με το αθροισμένο alpha ανακατασκευάζει το χρώμα στο οποίο τα ορατά δείγματα πράγματι συμφώνησαν. Το alpha προορισμού είναι ξεχωριστό μέγεθος, 255 * AlphaSum / WeightSum. Οι μορφές χωρίς alpha διαιρούν με το άθροισμα βαρών όπως συνήθως, το byte συμπλήρωσης ενός προορισμού FPDFBitmap_BGRx γράφεται ως σταθερό 255, και κάθε κανάλι σφίγγεται στο 0 έως 255 πριν αποθηκευτεί. Αυτό το alpha προέρχεται κανονικά από εγγραφή soft mask στο λεξικό της εικόνας (ISO 32000-1 §11.4), που το PDFium έχει ήδη συνθέσει στον buffer BGRA που λαμβάνει ο επαναδειγματοληπτής
// Μορφή του εσωτερικού βρόχου αθροίσεως, ανά συνεισφέρον δείγμα πηγής
if SrcFormat = FPDFBitmap_BGRA then
Alpha := PByte(PAnsiChar(Pixel) + 3)^ / 255
else
Alpha := 1;
for Channel := 0 to Min(BytesPerPixel, 3) - 1 do
Accumulated[Channel] := Accumulated[Channel] +
PByte(PAnsiChar(Pixel) + Channel)^ * Alpha * Weight;
AlphaSum := AlphaSum + Alpha * Weight;
WeightSum := WeightSum + Weight;
// ... και στο pixel προορισμού, αναιρώντας το premultiply ως προς το άθροισμα alpha
if SrcFormat = FPDFBitmap_BGRA then
begin
if Abs(AlphaSum) > 1E-12 then
ValueSum := Accumulated[Channel] / AlphaSum
else
ValueSum := 0;
end
else
ValueSum := Accumulated[Channel] / WeightSum;
Κρατώντας το line art 1-bit έξω από τη γκρίζα ζώνη
Οποιοσδήποτε συνεχής πυρήνας εφαρμοσμένος σε bilevel σάρωση παράγει γκρι, και το γκρι είναι ακριβώς αυτό που μια εικόνα τύπου fax δεν επιτρέπεται να περιέχει. Το PDFiumPas επομένως αφήνει τις εικόνες 1-bit ήσυχες από προεπιλογή: το PreserveBilevel είναι True στο TPdfImageOptimizeOptions.Default, και τέτοιες εικόνες καταλήγουν στο SkippedCount ανέγγιχτες. Ορίστε το False και η διαδρομή pirfBilevel αναλαμβάνει αντί για πυρήνα εξομάλυνσης. Διασχίζει το ακριβές ορθογώνιο πηγής που καλύπτει κάθε pixel προορισμού, μεσοσταθμίζει τη φωτεινότητα με τα βάρη 0.114, 0.587 και 0.299 σε σειρά μνήμης BGR, και περικόπτει το αποτέλεσμα στα 127.5 σε καθαρό 0 ή 255. Τίποτα ενδιάμεσο δεν μπορεί να γραφτεί, οπότε οι ακμές μένουν crisp και δεν σχηματίζεται γκρι φωτοστέφανο γύρω από λεπτούς πίνακες· το κανάλι alpha πηγής BGRA μεσοσταθμίζεται κανονικά, και ένας προορισμός BGRx παίρνει το σταθερό 255. Αν χρειάζεστε τα υποκείμενα pixels αντί για μικρότερο έγγραφο, η εξαγωγή εικόνων από PDF έγγραφα είναι η ξεχωριστή διαδρομή
Τι συμβαίνει όταν μια εικόνα ξεπερνά τον προϋπολογισμό μνήμης εργασίας;
Μένει ακριβώς όπως ήταν, και μετράται. Το MaxWorkingBytes από προεπιλογή είναι 64 MiB και επιβάλλεται δύο φορές. Πριν δημιουργηθεί το bitmap προορισμού, το PDFiumPas απορρίπτει την εικόνα αν πλάτος επί ύψος επί bytes ανά pixel ξεπερνά τον προϋπολογισμό. Αφού πετύχει το FPDFBitmap_CreateEx ελέγχει ξανά με το πραγματικό stride επί ύψος, επειδή η συμπλήρωση γραμμών μπορεί να σπρώξει μια δέσμευση πέρα από όριο που το αφελές γινόμενο πέρασε. Καθεμία απόρριψη καταστρέφει τον προορισμό και δεν επιστρέφει τίποτα. Να είστε σαφείς για την υποβάθμιση που αυτό συνεπάγεται: μια εικόνα εκτός προϋπολογισμού δεν επαναδειγματοληπτείται σε χαμηλότερη ποιότητα, και δεν διαχωρίζεται σε tiles. Το πρωτότυπο μένει στο έγγραφο, το BudgetExceededCount και το SkippedCount αυξάνονται και τα δύο, και μια εκτέλεση μπορεί επομένως να αναφέρει επιτυχία ενώ ένα έγγραφο είναι μόνο μερικώς βελτιστοποιημένο. Αυτή είναι σκόπιμη συμπεριφορά fail-safe, αλλά σημαίνει ότι η αναφορά δεν είναι προαιρετική ανάγνωση. Υπάρχει επίσης ξεχωριστή λειτουργία αποτυχίας: εικόνες των οποίων το bitmap το PDFium δεν μπορεί καθόλου να παράγει, όπως CMYK, JPX, JBIG2 ή masked πηγές, αυξάνουν αντίθετα το FailedCount και αυτές με τη σειρά τους μένουν ανέγγιχτες
procedure OptimizeBatch(const Files: array of string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
I: Integer;
begin
Options := TPdfImageOptimizeOptions.Default;
Options.PreserveBilevel := False; // χρήση της ψήφου περιοχής bilevel
Options.ContentClass := piccPhoto; // εξαναγκασμός Lanczos για σύνολα φωτογραφιών
Options.MaxWorkingBytes := 256 * 1024 * 1024; // περιθώριο για μεγάλες σαρώσεις
Pdf := TPdf.Create(nil);
try
for I := Low(Files) to High(Files) do
begin
Pdf.FileName := Files[I];
if not Pdf.OptimizeImages(Options, Report) then
begin
WriteLn('optimize failed: ', Report.ErrorMessage);
Continue;
end;
if Report.BudgetExceededCount > 0 then
WriteLn(Files[I], ': ', Report.BudgetExceededCount,
' image(s) over budget and kept at full size');
if Report.FailedCount > 0 then
WriteLn(Files[I], ': ', Report.FailedCount,
' image(s) could not be decoded to a bitmap');
if Report.OptimizedCount > 0 then
Pdf.SaveAs(ChangeFileExt(Files[I], '.opt.pdf'));
end;
finally
Pdf.Free;
end;
end;
Διαβάζοντας την αναφορά πριν στείλετε το αρχείο
Το TPdfImageOptimizeReport είναι χτισμένο για διάγνωση, όχι απλώς καταγραφή. Μαζί με OptimizedCount, SkippedCount και FailedCount εκθέτει έναν μετρητή ανά πυρήνα, οπότε τα BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount και BilevelFilterCount σας λένε τι συμπέρανε πραγματικά ο προσαρμοστικός κανόνας για το corpus σας. Ένα αποτέλεσμα όλα-box σημαίνει ότι οι μειώσεις ήταν απότομες ή το περιεχόμενο ταξινομήθηκε ως line art· ένα αποτέλεσμα όλα-Lanczos σε έγγραφο που πιστεύατε ότι ήταν line art είναι σημάδι ότι το ContentClass πρέπει να οριστεί ρητά. Το AverageDetailScore είναι ο αριθμός προς σύγκριση με το κατώφλι Lanczos 0.08 όταν ρυθμίζετε το PreferredQuality, και το PeakWorkingBytes δείχνει πόσο από το MaxWorkingBytes χρειαζόταν πραγματικά η εκτέλεση. Οι άκυρες επιλογές αποτυγχάνουν δυνατά αντί για σιωπηλά: ένα μη θετικό TargetDpi, ένα MinDpiRatio κάτω από 1, ένα PreferredQuality εκτός 0 έως 1, ή ένα μη θετικό MaxWorkingBytes προκαλεί EPdfError πριν αγγίξει οποιαδήποτε σελίδα. Και το OptimizeImages επεξεργάζεται μόνο το έγγραφο στη μνήμη· κάθε τροποποιημένη σελίδα δεσμεύεται με FPDFPage_GenerateContent, μετά από το οποίο καλείτε ακόμη εσείς το SaveAs. Για να δείτε με το μάτι τι άλλαξε, αποδώστε τα έγγραφα πριν και μετά σε bitmaps όπως περιγράφεται στο μετατροπή PDF σελίδων σε εικόνες JPEG και συγκρίνετά τα σε πλήρες zoom
Η προσαρμοστική επαναδειγματοληψία είναι από τις λειτουργίες που είναι αόρατες όταν δουλεύουν και παράγουν tickets υποστήριξης όταν δεν δουλεύουν, γι' αυτό η μέτρηση, ο χειρισμός alpha και ο προϋπολογισμός μνήμης έπρεπε να προσγειωθούν μαζί και όχι ως τρεις ξεχωριστές βελτιώσεις. Αν το αξιολογείτε για προϊόν Delphi, C++Builder ή Lazarus, η πλήρης επιφάνεια API και οι λεπτομέρειες αδειοδότησης βρίσκονται στη σελίδα PDFiumPas Delphi PDFium component