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

Συνδυασμός Σαρωμένων Εικόνων σε ένα PDF με το PDFium Component στο Delphi

Μια ομάδα επεξεργασίας αξιώσεων (claims-processing) είχε αρχεία χαρτιού τριάντα ετών που περνούσαν από έναν σαρωτή τροφοδοσίας φύλλων (sheet-fed scanner). Ο σαρωτής έβγαζε ένα JPEG ανά σελίδα σε έναν φάκελο, με όνομα 0001.jpg, 0002.jpg, και ούτω καθεξής. Αυτό που χρειαζόταν πραγματικά το αρχείο ήταν ένα PDF ανά αρχείο υπόθεσης (case file), με τις σελίδες στη σειρά, ώστε ένας αξιολογητής (reviewer) να μπορεί να ανοίξει ένα μεμονωμένο έγγραφο (single document) αντί να κάνει κλικ σε εκατό μικρογραφίες εικόνων (image thumbnails). Αυτό το τελευταίο βήμα, η μετατροπή (turning) ενός αριθμημένου σωρού (pile) σαρώσεων (scans) σε ένα ενιαίο (single) διατεταγμένο (ordered) PDF, είναι η δουλειά εδώ

Το PDFium Component το χειρίζεται άμεσα (directly). Πέρα από την απόδοση (rendering) και την εξαγωγή κειμένου, το στοιχείο μπορεί να κατασκευάσει (build) ένα PDF από το μηδέν (from scratch): δημιουργήστε ένα κενό έγγραφο, προσθέστε μια κενή σελίδα σε όποιο μέγεθος θέλετε, ρίξτε (drop) μια εικόνα σε αυτήν τη σελίδα σε συντεταγμένες (coordinates) χώρου χρήστη (user-space) και στη συνέχεια αποθηκεύστε (save). Ολόκληρη η γραμμή εντολών (pipeline) ζει στο στοιχείο TPdf, επομένως ένας μετατροπέας παρτίδας (batch converter) είναι ένας βρόχος (loop) πάνω σε ονόματα αρχείων συν μερικές (handful) κλήσεις

Ένας αγωγός παρτίδας Delphi χρησιμοποιεί τις κλήσεις AddPage, PageNumber, AddImage και SaveAs του PDFium Component για να μετατρέψει έναν φάκελο αριθμημένων σαρώσεων σε ένα διατεταγμένο PDF
Κάθε σάρωση γίνεται μία σελίδα δημιουργημένη από την AddPage και στοχευμένη από τη PageNumber πριν η AddImage τη σχεδιάσει· η SaveAs γράφει το τελειωμένο έγγραφο μία φορά

Η μορφή της μετατροπής (The shape of the conversion)

Τρία πράγματα πρέπει να συμβούν για κάθε σάρωση (scan). Αποφασίζετε το μέγεθος της σελίδας, τοποθετείτε (place) την εικόνα μέσα στη σελίδα αφήνοντας ένα περιθώριο (margin), και προχωράτε (advance) στην επόμενη σελίδα. Το PDFium Component σας δίνει μια μέθοδο για το καθένα: το AddPage δημιουργεί μια κενή σελίδα σε ένα δεδομένο μέγεθος, το AddImageAddPicture αν κρατάτε ήδη ένα TPicture) σχεδιάζει το bitmap στην τρέχουσα (current) σελίδα, και το PageNumber λέει στο στοιχείο (component) σε ποια σελίδα στοχεύουν (target) οι επόμενες κλήσεις σχεδίασης (subsequent draw calls)

Η μόνη λεπτομέρεια που μπερδεύει (trips up) τους ανθρώπους είναι το σύστημα συντεταγμένων (coordinate system). Ο χώρος χρήστη (user space) του PDF τοποθετεί την αρχή (origin) στην κάτω αριστερή γωνία (lower-left corner) της σελίδας, με το Y να αυξάνεται προς τα πάνω, το αντίθετο από τις συντεταγμένες οθόνης που χρησιμοποιούν οι προγραμματιστές του Delphi από αντανακλαστικό. Το X, Y που περνάτε (pass) στο AddImage είναι η κάτω αριστερή γωνία του ορθογωνίου (rectangle) της εικόνας, και το Width, Height (Πλάτος, Ύψος) είναι το μέγεθος τοποθέτησης σε στιγμές (points), όχι το μέγεθος pixel του αρχείου προέλευσης (source file). Αν το κάνετε αυτό ανάποδα, οι σαρώσεις σας θα προσγειωθούν (land) εκτός της σελίδας (off the page) ή ανάποδα σε σχέση με το πού τις περιμένατε

Δημιουργία του εγγράφου και μίας σελίδας ανά σάρωση

Ξεκινήστε με ένα κενό έγγραφο (empty document). Το CreateDocument εκχωρεί (allocates) ένα φρέσκο PDF και αφήνει το στοιχείο (component) ενεργό (active), οπότε δεν υπάρχει ξεχωριστό βήμα ανοίγματος (open step). Από εκεί, διασχίζετε (walk) τη λίστα των σαρωμένων αρχείων, και για κάθε ένα προσθέτετε (add) μια σελίδα, την κάνετε τρέχουσα (current) και τοποθετείτε την εικόνα. Οι διαστάσεις σελίδας (Page dimensions) εδώ είναι A4 σε στιγμές (595 × 842 κατακόρυφος προσανατολισμός), το τυπικό (standard) μέγεθος φύλλου (sheet size) για αρχειοθετημένη αλληλογραφία

procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
  PageW = 595.0;   // πλάτος A4 σε σημεία
  PageH = 842.0;   // ύψος A4 σε σημεία
  Margin = 36.0;   // περιθώριο μισής ίντσας γύρω από κάθε σάρωση
var
  I: Integer;
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                       // νέο, κενό, ήδη ενεργό
    for I := 0 to Files.Count - 1 do
    begin
      Pdf.AddPage(I + 1, PageW, PageH);       // δεικτοδότηση σελίδας με βάση το 1
      Pdf.PageNumber := I + 1;                // κάνει τη νέα σελίδα τρέχουσα
      PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
    end;
    Pdf.SaveAs(OutputPath);
  finally
    Pdf.Free;
  end;
end;

Κάθε επανάληψη (iteration) δημιουργεί μια σελίδα και αμέσως (immediately) της ορίζει το PageNumber. Αυτή η δεύτερη γραμμή έχει σημασία: το AddPage εισάγει (inserts) τη σελίδα, αλλά οι μέθοδοι σχεδίασης ενεργούν (act) σε όποια σελίδα είναι τρέχουσα, επομένως η ρύθμιση του PageNumber είναι αυτό που στοχεύει (aims) το AddImage στη σελίδα που μόλις φτιάξατε. Παραλείψτε το και οι εικόνες σας θα στοιβαχτούν (stack) σε όποια σελίδα έτυχε να είναι φορτωμένη πριν

Μία υπόθεση (assumption) κρύβεται (hides) σε αυτόν τον βρόχο (loop): η σειρά (order) των αρχείων (Files). Ένας σαρωτής (scanner) ονομάζει σελίδες 0001.jpg έως 0100.jpg, αλλά μια απαρίθμηση καταλόγου (directory enumeration) δεν τις επιστρέφει πάντα ταξινομημένες (sorted), και τη στιγμή (moment) που συναντάτε το page9.jpg δίπλα στο page10.jpg, μια απλή (plain) ταξινόμηση συμβολοσειράς (string sort) βάζει (puts) τη σελίδα 10 πριν από τη σελίδα 9. Ταξινομήστε τη λίστα ρητά (explicitly) πριν από τον βρόχο (loop), και προτιμήστε ονόματα με μηδενικά στην αρχή (zero-padded) κατά τη στιγμή της σάρωσης (scan time), ώστε η λεξικογραφική (lexical) σειρά να ταιριάζει με τη σειρά των σελίδων. Η ακολουθία (sequence) σελίδων είναι το μόνο πράγμα (one thing) που ένας αξιολογητής (reviewer) παρατηρεί (notices) αμέσως (immediately), και είναι το φθηνότερο λάθος προς αποτροπή (to prevent)

Τοποθέτηση μιας σάρωσης (scan) και διατήρηση του λόγου διαστάσεων (aspect ratio) της

Μια σάρωση (scan) είναι σπάνια το ίδιο σχήμα με τη σελίδα. Αν την τεντώσετε (stretch) για να γεμίσει (fill) το φύλλο (sheet), παραμορφώνετε (distort) το κείμενο· αν την τοποθετήσετε (place) στο πλήρες (full) μέγεθος pixel της, υπερχειλίζει (overflows). Η λύση (fix) είναι να κλιμακώσετε (scale) με τον μικρότερο από τους δύο λόγους (ratios), προσαρμογή-πλάτους (width-fit) ή προσαρμογή-ύψους (height-fit), και να κεντράρετε (center) ό,τι περισσεύει (left over). Επειδή η αρχή (origin) βρίσκεται (sits) κάτω αριστερά (lower-left), κεντράρισμα (centering) σημαίνει διαχωρισμό (splitting) του υπόλοιπου (leftover) χώρου ομοιόμορφα (evenly) και προσθήκη (adding) του τόσο στο X όσο και στο Y

Διάγραμμα σελίδας A4 δείχνει πώς το AddImage του PDFium Component τοποθετεί κλιμακωμένη σάρωση μέσα στα περιθώρια χρησιμοποιώντας κώδικα Delphi και αρχή κάτω-αριστερά
Η AddImage παίρνει την κάτω-αριστερή γωνία του ορθογωνίου τοποθέτησης, οπότε η προσαρμογή και το κεντράρισμα υπολογίζονται σε points σελίδας από την αρχή
procedure TArchiveForm.PlaceScan(Pdf: TPdf; const FileName: string;
  PageW, PageH, Margin: Double);
var
  Pic: TPicture;
  AvailW, AvailH, Scale, DrawW, DrawH, X, Y: Double;
begin
  Pic := TPicture.Create;
  try
    Pic.LoadFromFile(FileName);              // BMP, JPG, PNG κ.λπ. μέσω των μονάδων γραφικών VCL

    AvailW := PageW - 2 * Margin;
    AvailH := PageH - 2 * Margin;

    // Προσαρμογή εντός των περιθωρίων χωρίς παραμόρφωση της σάρωσης.
    Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
    DrawW := Pic.Width * Scale;
    DrawH := Pic.Height * Scale;

    // Κεντράρισμα: ο περισσευούμενος χώρος μοιράζεται ομοιόμορφα. Το Y μετράται από το κάτω μέρος της σελίδας.
    X := (PageW - DrawW) / 2;
    Y := (PageH - DrawH) / 2;

    Pdf.AddImage(FileName, X, Y, DrawW, DrawH);
  finally
    Pic.Free;
  end;
end;

Αυτό φορτώνει (loads) το αρχείο μία φορά (once) για να διαβάσει τις διαστάσεις (dimensions) pixel του, υπολογίζει (computes) μια ενιαία (single) ομοιόμορφη (uniform) κλίμακα, και περνά (passes) το ορθογώνιο (rectangle) τοποθέτησης στο AddImage. Το AddImage δέχεται απευθείας μια διαδρομή (path) αρχείου και τη δρομολογεί (routes) μέσω της ίδιας γραμμής εντολών εικόνας (image pipeline) με το AddPicture, επομένως οποιαδήποτε μορφή αναγνωρίζουν οι μονάδες (units) γραφικών VCL λειτουργεί (works) χωρίς ειδικές περιπτώσεις (special-casing). Εάν έχετε ήδη αποκωδικοποιήσει την εικόνα σε ένα TPicture από ένα παράθυρο προεπισκόπησης (preview pane), καλέστε AddPicture(Pic, X, Y, DrawW, DrawH) με το ίδιο ορθογώνιο και παραλείψτε τη δεύτερη ανάγνωση αρχείου (file read)

Παράλειψη της αποκωδικοποίησης (decode) για σαρώσεις JPEG

Οι σαρωτές (Scanners) σχεδόν πάντα εκπέμπουν (emit) JPEG. Η φόρτωση ενός JPEG σε ένα TPicture το αποκωδικοποιεί (decodes) σε ένα bitmap (χάρτη pixel), και στη συνέχεια (then) το PDFium το επανακωδικοποιεί (re-encodes) κατά την αποθήκευση, δύο με απώλειες "ταξίδια" (lossy round trips) που δεν χρειάζεστε. Το AddJpegImage ενσωματώνει (embeds) τα αρχικά (original) συμπιεσμένα (compressed) byte (bytes) κατευθείαν (straight) στη σελίδα από μια ροή (stream), το οποίο είναι ταχύτερο και οπτικά (visually) καθαρότερο (cleaner) για μια παρτίδα (batch) μεγάλου όγκου (high-volume)

Το AddJpegImage ενσωματώνει τα πρωτότυπα bytes JPEG σε σελίδα PDFium Component, ενώ το AddImage αποκωδικοποιεί και το SaveAs επανακωδικοποιεί τα pixels σε Delphi
Η AddJpegImage ενσωματώνει τα συμπιεσμένα byte του scanner ως έχουν, αποφεύγοντας τα περάσματα αποκωδικοποίησης και επανακωδικοποίησης που εκτελούν η AddImage και η SaveAs
var
  Stream: TFileStream;
begin
  // ... μετά από AddPage + PageNumber για την τρέχουσα σελίδα ...
  Stream := TFileStream.Create(FileName, fmOpenRead);
  try
    // Ενσωματώνει τα byte JPEG ως έχουν· χωρίς κύκλο αποκωδικοποίησης/επανακωδικοποίησης.
    Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
  finally
    Stream.Free;
  end;
end;

Εξακολουθείτε (still) να υπολογίζετε τα X, Y, DrawW (ΠλάτοςΣχεδίασης), και DrawH (ΎψοςΣχεδίασης) με τον ίδιο τρόπο (way), αφού (since) χρειάζεστε τις διαστάσεις pixel για την κλίμακα (scale). Διαβάστε (Read) τα από το αρχείο (file) ή από μια γρήγορη ανάλυση επικεφαλίδας (quick header parse), και στη συνέχεια παραδώστε (hand) την ακατέργαστη (raw) ροή (stream) στο AddJpegImage. Για σαρώσεις (scans) PNG ή TIFF η διαδρομή (path) AddImage είναι η σωστή· κρατήστε (reserve) τη συντόμευση (shortcut) JPEG για τη μορφή στην οποία πραγματικά ισχύει (applies to)

Προσθήκη ετικετών (Labeling) σε κάθε σελίδα

Οι αρχειοθετημένες σαρώσεις (Archived scans) ελέγχονται (audit) πιο εύκολα όταν κάθε σελίδα (page) φέρει (carries) το όνομα αρχείου (filename) προέλευσής της. Το AddText σχεδιάζει (draws) μια συμβολοσειρά (string) σε μια συντεταγμένη χώρου χρήστη (user-space coordinate), οπότε μια λεζάντα (caption) κάθεται (sits) ακριβώς (just) κάτω από την εικόνα. Θυμηθείτε (Remember) τον ανεστραμμένο άξονα Y: για να βάλετε (put) μια ετικέτα (label) κάτω από (below) τη σάρωση, αφαιρείτε (subtract) από το κάτω (bottom) άκρο της εικόνας αντί να προσθέτετε σε αυτό

// Λεζάντα κάτω από τη σάρωση: το Y μειώνεται προς το κάτω μέρος της σελίδας.
Pdf.AddText('File: ' + ExtractFileName(FileName), 'Helvetica', 9,
  X, Y - 14, clGray);

Ένα τελευταίο (last) σημείο σχετικά με την αποθήκευση (saving). Το SaveAs είναι μια συνάρτηση (function) που επιστρέφει ένα Boolean, οπότε στον κώδικα παραγωγής (production code) ελέγξτε το αποτέλεσμά της (result) αντί να υποθέσετε (assuming) ότι η εγγραφή (write) πέτυχε· ένας γεμάτος δίσκος ή μια κλειδωμένη διαδρομή εξόδου (locked output path) αποτυγχάνει σιωπηλά (fails quietly) διαφορετικά. Μόλις ολοκληρωθεί ο βρόχος και γραφτεί το αρχείο, έχετε ακριβώς αυτό που χρειαζόταν το αρχείο: ένα διατεταγμένο (ordered) PDF ανά αρχείο υπόθεσης (case file), σελίδες κλιμακωμένες (scaled) για προσαρμογή (fit), έτοιμο (ready) για ανάγνωση (read) σε οποιοδήποτε πρόγραμμα προβολής (viewer)

Τα ίδια δομικά στοιχεία (building blocks) καλύπτουν (cover) σχετικές (related) εργασίες (jobs). Αλλάξτε (Swap) τον κανόνα μεγέθους ανά σελίδα (per-page sizing rule) και παίρνετε (get) ένα βιβλίο φωτογραφιών με μία εικόνα ανά φύλλο (sheet)· κρατήστε τον βρόχο αλλά διαβάστε από μια πηγή πολλαπλών σελίδων (multi-page source) TIFF και έχετε έναν μετατροπέα αρχείου-φαξ (fax-archive converter). Εάν θέλετε (want) την ευρύτερη (wider) εικόνα της δημιουργίας PDF προγραμματιστικά (programmatically), δείτε τη δημιουργία εγγράφων PDF από το μηδέν με το PDFium Component· για να αποδώσετε (render) το αποτέλεσμα πίσω στην οθόνη αργότερα, δείτε τη μετατροπή σελίδων PDF σε εικόνες JPEG με το PDFium Component

Το PDFium Component από το loslab.com ομαδοποιεί (bundles) τα API δημιουργίας εγγράφων (document-creation), απόδοσης (rendering) και κειμένου (text APIs) που χρησιμοποιούνται σε ολόκληρη αυτή τη σειρά