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

Δημιουργία PDF από το μηδέν με το PDFium Component στο Delphi

Το PDFium έχει τη φήμη ενός κινητήρα προβολής, του renderer πίσω από την καρτέλα PDF του Chrome, οπότε το πρώτο πράγμα που πρέπει να ξεκαθαριστεί είναι ότι το PDFium Component μπορεί επίσης να δημιουργήσει ένα έγγραφο που δεν υπήρχε ποτέ πριν. Η πλευρά της δημιουργίας περιβάλλει το page-object API του PDFium: δημιουργείτε ένα κενό έγγραφο, προσθέτετε σελίδες με ρητές διαστάσεις και τοποθετείτε κείμενο, διανυσματικές διαδρομές και εικόνες σε κάθε σελίδα στις συντεταγμένες που επιλέγετε. Δεν υπάρχει γλώσσα περιγραφής σελίδας για να μάθετε και κανένα πρόγραμμα οδήγησης εκτυπωτή στο βρόχο. Καλείτε μεθόδους, η βιβλιοθήκη συναρμολογεί αντικείμενα PDF και η SaveAs σειριοποιεί το αποτέλεσμα

Αυτό που δεν παίρνετε είναι ένας κινητήρας διάταξης. Αυτό έχει αρκετή σημασία για να το πούμε εκ των προτέρων, επειδή διαμορφώνει κάθε παράδειγμα παρακάτω. Το PDFium Component τοποθετεί το περιεχόμενο όπου του λέτε, σε απόλυτες συντεταγμένες, και πουθενά αλλού. Δεν θα αναδιπλώσει μια παράγραφο, δεν θα ρέει κείμενο σε μια αλλαγή σελίδας ή δεν θα υπολογίσει έναν πίνακα από γραμμές και στήλες. Αυτά είναι δική σας δουλειά. Αν ήρθατε περιμένοντας κάτι που αναδιαμορφώνει τον πεζό λόγο με τον τρόπο που το κάνει ένας επεξεργαστής κειμένου, προσαρμοστείτε τώρα: πρόκειται για ένα ακριβές API τοποθέτησης χαμηλού επιπέδου, πιο κοντά στο σχέδιο σε καμβά παρά στη στοιχειοθεσία ενός εγγράφου. Για παραγόμενα τιμολόγια, πιστοποιητικά, ετικέτες και σελίδες αναφορών όπου γνωρίζετε ήδη πού ανήκει κάθε στοιχείο, αυτή η ακρίβεια είναι ακριβώς αυτό που θέλετε

Το ελάχιστο που παράγει ένα αρχείο

Τρεις κλήσεις στέκονται ανάμεσα σε ένα κενό TPdf και ένα αποθηκευμένο PDF: δημιουργήστε το έγγραφο, προσθέστε μια σελίδα, γράψτε το. Οτιδήποτε άλλο είναι περιεχόμενο που βάζετε σε στρώματα ενδιάμεσα

uses
  Vcl.Graphics,   // for clBlack and TColor
  PDFium;         // TPdf lives here

procedure CreateBlankPdf(const FileName: string);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                 // empty in-memory document
    Pdf.AddPage(0, 595, 842);           // A4 portrait, in points
    Pdf.AddText('First page', 'Arial', 18, 50, 780);
    Pdf.SaveAs(FileName);               // serialize to disk
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Μια λεπτομέρεια μπερδεύει όσους έχουν δει παλαιότερα αποσπάσματα κώδικα: δεν αναθέτετε Pdf.Active := True μετά το CreateDocument. Η ιδιότητα Active αναφέρει εάν υπάρχει λαβή εγγράφου και η CreateDocument έχει ήδη δημιουργήσει μία, επομένως η ιδιότητα είναι True τη στιγμή που επιστρέφει αυτή η κλήση. Η ρύθμισή της ξανά είναι στην καλύτερη περίπτωση μη λειτουργική και στη χειρότερη παραπλανητική για τον επόμενο αναγνώστη. Η Active κερδίζει την αξία της κατά την έξοδο: η ανάθεση του False απελευθερώνει το υποκείμενο έγγραφο πριν από το Free, που είναι η καθαρή σειρά αποσυναρμολόγησης. Αντιμετωπίστε τη CreateDocument και το άνοιγμα φόρτωσης αρχείου ως αμοιβαία αποκλειόμενα. Η βιβλιοθήκη αρνείται να δημιουργήσει ένα νέο έγγραφο σε ένα TPdf που έχει ήδη ένα ανοιχτό, οπότε η επαναχρησιμοποίηση σημαίνει να κλείσετε πρώτα το τρέχον έγγραφο

Οι συντεταγμένες ξεκινούν από κάτω αριστερά

Το δεύτερο ζεύγος ορισμάτων στο AddText, και σε κάθε κλήση τοποθέτησης, είναι ένα σημείο στον χώρο χρήστη του PDF. Η αρχή βρίσκεται στην κάτω αριστερή γωνία της σελίδας, το X τρέχει δεξιά και το Y τρέχει πάνω. Μία μονάδα είναι μία στιγμή (point), 1/72 της ίντσας, οπότε μια σελίδα A4 είναι 595 επί 842 μονάδες και η US Letter είναι 612 επί 792. Αυτό το ανοδικό Y είναι η πιο κοινή πηγή σύγχυσης "το κείμενό μου είναι εκτός σελίδας", επειδή οι συντεταγμένες οθόνης και bitmap βάζουν την αρχή στην κορυφή με το Y να μεγαλώνει προς τα κάτω. Σε μια σελίδα ύψους 842 σημείων, μια επικεφαλίδα κοντά στην κορυφή βρίσκεται γύρω στο Y 780, όχι στο Y 60. Όταν μια εκτέλεση προσγειώνεται κάπου απροσδόκητα, το ύψος της σελίδας μείον το Y σας είναι σχεδόν πάντα ο αριθμός που πραγματικά εννοούσατε

Η AddPage δέχεται μια θέση εισαγωγής ως πρώτο όρισμά της, εκφρασμένη με βάση το ένα, με το 0 ως μια βολική συντομογραφία "αρχή εγγράφου". Περάστε 0 ή 1 για την πρώτη σελίδα και η σελίδα εισάγεται μπροστά· περάστε την τιμή που ταιριάζει με το πλήθος στο οποίο προσαρτάτε προκειμένου να προσθέσετε στο τέλος. Η νέα σελίδα που προστέθηκε γίνεται επίσης η τρέχουσα σελίδα, αυτή που στοχεύουν οι επόμενες κλήσεις σχεδίασης, οπότε δεν υπάρχει ξεχωριστό βήμα "επιλογή αυτής της σελίδας" μετά την προσθήκη της. Αν προσθέσετε πολλές σελίδες και αργότερα χρειαστεί να σχεδιάσετε ξανά σε μια προηγούμενη, ορίστε το PageNumber για να μετακινήσετε τον κέρσορα· ενώ γεμίζετε τις σελίδες με τη σειρά καθώς τις δημιουργείτε, μπορείτε να το αφήσετε ως έχει

Εγγραφή κειμένου και ο κανόνας γραμματοσειράς που δαγκώνει σιωπηλά

Η υπογραφή της AddText φέρει όλα όσα χρειάζεται μια μεμονωμένη εκτέλεση: τη συμβολοσειρά, ένα όνομα γραμματοσειράς, ένα μέγεθος σε σημεία (points), την άγκυρα X και Y, στη συνέχεια προαιρετικό χρώμα, ένα alpha byte για διαφάνεια και μια γωνία περιστροφής σε μοίρες

procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
  // Title in black, default opacity, no rotation
  Pdf.AddText(Title, 'Arial', 20, 50, 780);
  // A lighter byline 24 points below it
  Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
  // A faint diagonal draft stamp across the page
  Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;

Το alpha byte κυμαίνεται από $00 (αόρατο) έως $FF (αδιαφανές), πράγμα που καθιστά τη σφραγίδα πρόχειρου υδατογράφημα και όχι συμπαγές μπλοκ: το $30 είναι περίπου δεκαεννέα τοις εκατό αδιαφάνεια, αρκετή για να διαβάζεται από μέσα. Η γωνία περιστρέφει την εκτέλεση αριστερόστροφα γύρω από την άγκυρά της, οπότε οι 45 μοίρες δίνουν την κλασική σφραγίδα από γωνία σε γωνία. Τίποτα από αυτά δεν χρειάζεται ξεχωριστή λειτουργία υδατογραφήματος. Ένα υδατογράφημα είναι απλώς μια μεγάλη, ημιδιαφανής, περιστρεφόμενη κλήση AddText και η σχεδίασή του πριν ή μετά το σώμα αποφασίζει εάν θα βρίσκεται πίσω ή πάνω από το περιεχόμενο

Οι γραμματοσειρές αξίζουν μια προσεκτική πρόταση, επειδή ο τρόπος αποτυχίας είναι αθόρυβος. Όταν περνάτε ένα όνομα γραμματοσειράς, το PDFium Component ζητά από το λειτουργικό σύστημα τα δεδομένα TrueType αυτής της γραμματοσειράς και τα ενσωματώνει στο έγγραφο, γι' αυτό ένα αρχείο που δημιουργήθηκε στο μηχάνημά σας αποδίδεται πανομοιότυπα σε ένα που δεν είχε ποτέ εγκατεστημένη τη γραμματοσειρά. Η παγίδα είναι τι συμβαίνει όταν το όνομα δεν επιλύεται: ένα τυπογραφικό λάθος ή μια γραμματοσειρά που απλά δεν υπάρχει στο μηχάνημα κατασκευής. Δεν υπάρχει εξαίρεση. Η βιβλιοθήκη υποχωρεί στη δημιουργία ενός αντικειμένου κειμένου που φέρει το όνομα μόνο ως ετικέτα, χωρίς τίποτα ενσωματωμένο, και αφήνει το πρόγραμμα προβολής να αντικαταστήσει ό,τι θεωρεί κοντινό. Το κείμενο εμφανίζεται στις δοκιμές σας, φαίνεται εύλογο και μετατοπίζει τις μετρικές ή τα γλυφικά τη στιγμή που το αρχείο ανοίγει κάπου με διαφορετικές εγκατεστημένες γραμματοσειρές. Χρησιμοποιήστε ονόματα που γνωρίζετε ότι υπάρχουν στο μηχάνημα παραγωγής, αντιμετωπίστε τη λίστα γραμματοσειρών ως εξάρτηση ανάπτυξης και ανοίξτε ένα δείγμα σε ένα πρόγραμμα προβολής σε ένα καθαρό σύστημα προτού εμπιστευτείτε την έξοδο

Διανυσματικά σχήματα: δημιουργήστε μια διαδρομή, μετά υποβάλετέ την

Οι γραμμές, τα ορθογώνια και οι γεμάτες περιοχές περνούν μέσα από μια διαδρομή. Ανοίγετε μία με την CreatePath, η οποία ορίζει το σημείο εκκίνησης και όλο το στυλ ταυτόχρονα, λειτουργία γεμίσματος, χρώματα γεμίσματος και περιγράμματος με τα δικά τους alpha bytes, πλάτος περιγράμματος, καταλήξεις γραμμών και ενώσεις. Έπειτα την επεκτείνετε με LineTo, BezierTo και ClosePath, και τελικά η AddPath υποβάλλει την τελειωμένη διαδρομή στη σελίδα. Το βήμα της υποβολής είναι εύκολο να ξεχαστεί και δεν παράγει τίποτα αν το παραλείψετε

procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
  // A thin horizontal rule. The rectangle overload sets a box directly:
  // X, Y, Width, Height, then fill mode and colors.
  Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
    True, clBlack, $FF, 1.0);
  Pdf.AddPath;
end;

procedure DrawTriangle(Pdf: TPdf);
begin
  // Point overload: start at the first vertex, line to the rest, close.
  Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
  Pdf.LineTo(300, 300);
  Pdf.LineTo(250, 400);
  Pdf.ClosePath;
  Pdf.AddPath;          // nothing is drawn until this runs
end;

Δύο υπερφορτώσεις καλύπτουν τις κοινές περιπτώσεις. Η μορφή τεσσάρων συντεταγμένων δέχεται X, Y, πλάτος και ύψος και σας δίνει ένα ορθογώνιο ευθυγραμμισμένο με τον άξονα με μία κλήση, που είναι αυτό που αναζητάτε για να σχεδιάσετε έναν κανόνα, ένα περίγραμμα κελιού ή ένα γεμάτο πάνελ φόντου. Η μορφή δύο συντεταγμένων ορίζει μόνο ένα σημείο εκκίνησης και ανιχνεύετε το υπόλοιπο περίγραμμα μόνοι σας με LineTo και BezierTo. Η λειτουργία γεμίσματος ελέγχει πώς ζωγραφίζονται οι αλληλεπικαλυπτόμενες περιοχές: η fmWinding (nonzero winding) ταιριάζει στα περισσότερα συμπαγή σχήματα, η fmAlternate (even-odd) χειρίζεται αποκοπές και περιγράμματα που τέμνονται από μόνα τους, και η fmNone αφήνει μια διαδρομή μόνο με περίγραμμα χωρίς γέμισμα, που είναι αυτό που χρησιμοποιεί ο διαχωριστής παραπάνω

Οι πίνακες είναι διαδρομές και κείμενο, συναρμολογημένα με το χέρι

Επειδή δεν υπάρχει βασικό στοιχείο πίνακα, ένας πίνακας είναι ένας βρόχος. Αποφασίζετε τις μετατοπίσεις X των στηλών και το ύψος της γραμμής, γράφετε κάθε κελί με AddText και σχεδιάζετε τους κανόνες με ορθογώνιες διαδρομές. Η αριθμητική είναι δική σας, αλλά είναι απλή, και μόλις γραφτεί γενικεύεται σε οποιοδήποτε πλέγμα χρειάζεστε

procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
  ColX: array[0..2] of Double = (0, 110, 210);  // column offsets
  RowH = 20;
var
  Y: Double;
  Row: Integer;
begin
  // Header row
  Pdf.AddText('Item', 'Arial', 10, Left + ColX[0], Top);
  Pdf.AddText('Qty', 'Arial', 10, Left + ColX[1], Top);
  Pdf.AddText('Price', 'Arial', 10, Left + ColX[2], Top);

  // Rule under the header
  Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
  Pdf.AddPath;

  // Data rows, stepping Y downward each iteration
  Y := Top;
  for Row := 1 to 3 do
  begin
    Y := Y - RowH;
    Pdf.AddText('Item ' + IntToStr(Row), 'Arial', 9, Left + ColX[0], Y);
    Pdf.AddText(IntToStr(Row * 2), 'Arial', 9, Left + ColX[1], Y);
    Pdf.AddText('$' + IntToStr(Row * 10) + '.00', 'Arial', 9, Left + ColX[2], Y);
  end;
end;

Παρατηρήστε το Y να κατεβαίνει προς τα κάτω κατά το ύψος της γραμμής σε κάθε πέρασμα, και πάλι επειδή το πάνω είναι θετικό. Εδώ επίσης φαίνεται η απουσία μέτρησης κειμένου: τίποτα δεν εμποδίζει ένα μακρύ όνομα στοιχείου να ξεπεράσει τα όρια στην επόμενη στήλη, επειδή η βιβλιοθήκη δεν γνωρίζει πόσο πλατιά αποδόθηκε η συμβολοσειρά σας. Για έξοδο σταθερής μορφής όπου ελέγχετε τα δεδομένα, δίνετε μέγεθος στις στήλες γενναιόδωρα και προχωράτε. Για πραγματικά μεταβλητό περιεχόμενο, είτε περιορίζετε τις εισόδους είτε μετράτε μόνοι σας τα πλάτη των γλυφικών πριν τα τοποθετήσετε, που είναι το σημείο όπου μια εξειδικευμένη βιβλιοθήκη σύνθεσης αρχίζει να αποδίδει

Εικόνες και πολλαπλές σελίδες

Το περιεχόμενο ράστερ (raster) εισάγεται μέσω των βοηθημάτων εικόνας. Η AddPicture δέχεται ένα φορτωμένο TPicture και το τοποθετεί σε ένα σημείο, με προαιρετικό πλάτος και ύψος για να το κλιμακώσει· η AddImage δέχεται μια διαδρομή αρχείου ή ένα TBitmap απευθείας, και η AddJpegImage μεταδίδει bytes JPEG χωρίς επιστροφή μέσω ενός bitmap. Όπως με οτιδήποτε άλλο, οι συντεταγμένες τοποθέτησης είναι η κάτω αριστερή γωνία της εικόνας στον χώρο χρήστη, και το πλάτος και το ύψος είναι το μέγεθος στη σελίδα σε σημεία, όχι οι διαστάσεις pixel της πηγής

procedure CreateMultiPageReport(const FileName: string; PageCount: Integer);
var
  Pdf: TPdf;
  P: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    for P := 1 to PageCount do
    begin
      Pdf.AddPage(P, 595, 842);     // append; the new page becomes current
      Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
        'Arial', 10, 50, 30);       // footer near the bottom edge
      // ... draw this page's body here ...
    end;
    Pdf.SaveAs(FileName);
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Ένα έγγραφο πολλών σελίδων είναι το μοτίβο μονής σελίδας σε βρόχο. Κάθε AddPage προσαρτά μια σελίδα και την καθιστά τρέχουσα, έτσι ώστε το σώμα και το υποσέλιδο που σχεδιάζετε στη συνέχεια να προσγειώνονται στη σελίδα που μόλις προσθέσατε. Δεν αναθέτετε ξανά το PageNumber μέσα σε αυτόν τον βρόχο, επειδή η προσθήκη μιας σελίδας μετακίνησε ήδη τον κέρσορα εκεί· χρειάζεστε το PageNumber μόνο όταν επιστρέφετε σε μια σελίδα εκτός της σειράς δημιουργίας. Καλέστε το SaveAs μία φορά στο τέλος, αφού γεμίσει η τελευταία σελίδα. Αν χρειάζεστε ένα προφίλ αρχειοθέτησης αντί για ένα απλό αρχείο, το ίδιο αντικείμενο εγγράφου εκθέτει το SaveAsPdfA και τις άλλες παραλλαγές συμμόρφωσης, επομένως η επιλογή του προτύπου εξόδου είναι μια διαφορετική κλήση αποθήκευσης, όχι μια διαφορετική διαδρομή κατασκευής

Πού ταιριάζει αυτό

Το ειλικρινές πλαίσιο είναι ότι το API δημιουργίας του PDFium Component είναι ένα πιστό, λεπτό στρώμα πάνω από το μοντέλο page-object του PDFium: πραγματική δημιουργία εγγράφων, πραγματικές ενσωματωμένες γραμματοσειρές, πραγματικό διανυσματικό και ράστερ περιεχόμενο, σειριοποιημένο σε ένα αρχείο που συμμορφώνεται με τα πρότυπα. Δεν είναι, και δεν προσποιείται ότι είναι, ένας κινητήρας αναδιαμόρφωσης εγγράφων. Η διαχωριστική γραμμή είναι η διάταξη του κειμένου. Εάν η έξοδός σας βασίζεται σε πρότυπα, τιμολόγια, πιστοποιητικά, ετικέτες, πίνακες εργαλείων που αποδίδονται σε σταθερό πλέγμα, το μοντέλο απόλυτων συντεταγμένων είναι άμεσο και γρήγορο και ο κώδικας παραμένει ευανάγνωστος. Εάν η παραγωγή σας είναι πεζός λόγος μεγάλης μορφής που πρέπει να αναδιπλώνεται και να σελιδοποιείται μόνος του, θα ανακατασκευάσετε έναν κινητήρα διάταξης πάνω από αυτές τις κλήσεις, και αυτό είναι το λάθος εργαλείο για τη δουλειά. Η γνώση της πλευράς αυτής της γραμμής στην οποία βρίσκεστε είναι το μεγαλύτερο μέρος της απόφασης

Οι μέθοδοι δημιουργίας που περιγράφονται εδώ αποτελούν μέρος του PDFium Component για Delphi, το οποίο συνδυάζει αυτή τη διαδρομή δημιουργίας με τις δυνατότητες απόδοσης και εξαγωγής κειμένου για τις οποίες το PDFium είναι περισσότερο γνωστό