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

Απόκρυψη σε PDF και σύνθεση N-up στο Delphi με το HotPDF

Στο γραφείο σας φτάνει ένα αίτημα: πάρτε μια παρτίδα από ήδη αποδοσμένες καταστάσεις, σβήστε τους αριθμούς λογαριασμών και δώστε δύο σελίδες ανά φύλλο για να εξοικονομήσετε χαρτί. Και τα δύο μισά αυτής της δουλειάς είναι χειρουργική στο content-stream ενός PDF που δεν δημιουργήσατε εσείς, άρα δεν υπάρχει φιλικός καμβάς σελίδας για να σχεδιάσετε ούτε διαχειριστής γραμματοσειρών για να στηριχτείτε. Επεξεργάζεστε απευθείας το γράφημα αντικειμένων ενός φορτωμένου εγγράφου, προσθέτοντας ακατέργαστους drawing operators σε μια σελίδα που διαμόρφωσε κάποιο άλλο εργαλείο. Το HotPDF εκθέτει ακριβώς δύο σημεία εισόδου για αυτό, και το πιο επικίνδυνο από τα δύο είναι εκείνο που μοιάζει αθώο

Το HotPDF είναι ένα εγγενές PDF component VCL για Delphi και C++Builder. Το API του για φορτωμένα έγγραφα στην ένατη έκδοση πρόσθεσε τις πρώτες μεθόδους που δημιουργούν ολοκαίνουργιο περιεχόμενο σε μια σελίδα που ανοίξατε από δίσκο, αντί για μια που χτίσατε από το μηδέν. Δύο από αυτές είναι το θέμα εδώ: RedactLoadedRect, το οποίο βάφει ένα αδιαφανές ορθογώνιο πάνω από μια περιοχή, και StitchLoadedPage, το οποίο κλιμακώνει μια σελίδα και τη σχεδιάζει πάνω σε μια άλλη. Και οι δύο δουλεύουν γράφοντας operators content-stream του ISO 32000-1 §8.5 στο /Contents stream της σελίδας. Η κατανόηση του τι κάνουν αυτοί οι operators, και εξίσου σημαντικό του τι δεν κάνουν, είναι η διαφορά ανάμεσα σε ένα εργαλείο που λειτουργεί και σε διαρροή δεδομένων

Προσθήκη operators σε φορτωμένη σελίδα

Όταν χτίζετε μια σελίδα με το κανονικό API του HotPDF, το component κατέχει το content stream και σειριοποιεί για εσάς τις TextOutκλήσεις κειμένου και vector για εσάς. Μια φορτωμένη σελίδα είναι διαφορετική: το /Contents είναι ένα υπάρχον stream object, πιθανώς κοινόχρηστο, πιθανώς μέρος ενός πίνακα περιεχομένου, και πρέπει να το κουμπώσετε μέσα του χωρίς να αλλοιώσετε ό,τι υπάρχει ήδη. Η ένατη έκδοση εισήγαγε τρεις μικρούς βοηθούς που το κάνουν αυτό ασφαλές. NewIndirectStream δημιουργεί μια φρέσκια έμμεση THPDFStreamObjectροή με κενό buffer και μια /Length 0καταχώριση; ResolveLoadedStream ακολουθεί μια έμμεση αναφορά ως την υποκείμενη ροή· και AppendLoadedStream γράφει ακατέργαστα bytes στο τέλος της ροής και ξαναγράφει /Length ώστε το αποθηκευμένο αντικείμενο να μένει σωστά σχηματισμένο

Το μοτίβο που ακολουθούν και οι δύο δημόσιες μέθοδοι είναι το ίδιο. Βρείτε το /Contents, επιλύστε το σε ροή, και αν δεν υπάρχει αξιοποιήσιμη ροή, δημιουργήστε μία και προσαρτήστε την. Έπειτα προσθέστε τους operators. Επειδή τα νέα bytes μπαίνουν στο τέλος της ροής, το μοντέλο του ζωγράφου εγγυάται ότι αποδίδονται πάνω από ό,τι σχεδίασε η αρχική διάταξη. Αυτή η σειρά είναι ολόκληρος ο μηχανισμός πίσω από το ορθογώνιο απόκρυψης, και είναι επίσης ο λόγος που αυτό το ορθογώνιο δεν είναι αυτό που νομίζουν οι περισσότεροι

RedactLoadedRect: ένα αδιαφανές κάλυμμα, όχι διαγραφή

RedactLoadedRect δέχεται έναν δείκτη σελίδας με βάση το μηδέν, τέσσερις συντεταγμένες user-space και τρία στοιχεία χρώματος στο εύρος 0–1:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('statement.pdf') > 0 then
    begin
      // Cover the account-number band on page 1 with solid black.
      // Coordinates are PDF user space: origin bottom-left, points.
      Pdf.RedactLoadedRect(0, 56, 690, 320, 706, 0, 0, 0);
      Pdf.SaveLoadedDocument('statement-covered.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Στο υπόβαθρο η μέθοδος εκπέμπει τρεις operators στο content stream: μια ρύθμιση χρώματος γεμίσματος σε DeviceRGB (r g b rg), ένα μονοπάτι ορθογωνίου (x y w h re), και ένα γέμισμα (f). Το πλάτος και το ύψος προκύπτουν ως X2 - X1 και Y2 - Y1, οπότε δίνετε δύο απέναντι γωνίες και αφήνετε τη μέθοδο να υπολογίσει την έκταση. Δώστε 0, 0, 0 για το χρώμα και παίρνετε μια μαύρη μπάρα· δώστε 1, 1, 1 για μια λευκή που ταιριάζει σε λευκή σελίδα. Οι συντεταγμένες είναι ο δικός της χώρος user space της φορτωμένης σελίδας, πράγμα που σημαίνει ότι η αρχή βρίσκεται στην κάτω αριστερή γωνία και οι μονάδες είναι points, και σημαίνει επίσης ότι χρειάζεστε το /MediaBox της σελίδας για να τοποθετήσετε οτιδήποτε με ακρίβεια· GetLoadedPageBox με pbMediaBox σας το δίνει αυτό

Διαβάστε το δύο φορές: ένα γεμισμένο ορθογώνιο καλύπτει οπτικά το περιεχόμενο, δεν το αφαιρεί. Το κείμενο, η εικόνα ή το vector art κάτω από το ορθογώνιο εξακολουθεί να υπάρχει στο PDF, εξακολουθεί να βρίσκεται στο γράφημα αντικειμένων, εξακολουθεί να εξάγεται από όποιον αντιγράφει τη σελίδα, εκτελεί έναν text extractor ή απλώς διαγράφει το ορθογώνιό σας από το content stream. Αυτό είναι οπτική απόκρυψη, όχι redaction με τη νομική ή την έννοια ασφάλειας. Αν κρύβετε πραγματικά ευαίσθητα δεδομένα, όπως αριθμούς λογαριασμών, ιατρικούς φακέλους, ταυτότητες ή οτιδήποτε ρυθμιζόμενο, το να τα σκεπάσετε με ένα μαύρο κουτί και να στείλετε το αρχείο είναι διαρροή δεδομένων που περιμένει να αποκαλυφθεί. Η πραγματική απόκρυψη απαιτεί διαγραφή των υποκείμενων content objects, όχι ζωγράφισμα από πάνω τους

Το όνομα της μεθόδου λέει "Redact" και αυτό είναι μια χρήσιμη προειδοποίηση για το πώς θα παρερμηνευτεί το αποτέλεσμα, όχι μια υπόσχεση για το τι διαγράφει. Η υλοποίηση το παραδέχεται αυτό στο ίδιο της το σχόλιο: αποκαλεί τον εαυτό της "visual redaction primitive" και σημειώνει ότι η redaction που αφαιρεί περιεχόμενο χρειάζεται έναν διερμηνευτή content-stream που διατρέχει και ξαναγράφει τους υπάρχοντες operators. Η διαδρομή του HotPDF για φορτωμένο έγγραφο δεν το κάνει αυτό εδώ. Άρα ο ασφαλής κανόνας είναι στενός: χρησιμοποίησε RedactLoadedRect για μη ευαίσθητη καλλυντική κάλυψη - απόκρυψη ενός πρόχειρου υδατογραφήματος, σβήσιμο μιας περιοχής πριν από ένα screenshot, κάλυψη ενός παρωχημένου λογότυπου σε εσωτερικό proof. Τη στιγμή που αυτό που κρύβεται κάτω από το πλαίσιο θα είχε σημασία αν διέρρεε, η μέθοδος αυτή είναι το λάθος εργαλείο, και η σωστή απάντηση είναι να ξαναδημιουργήσεις το έγγραφο χωρίς τα δεδομένα ή να χρησιμοποιήσεις έναν πραγματικό pipeline αφαίρεσης περιεχομένου

StitchLoadedPage: κλιμάκωση, μετατόπιση, σχεδίαση

Η επιβολή N-up είναι το πιο φιλικό πρόβλημα, γιατί τίποτα δεν κρύβεται, απλώς αναδιατάσσεται. StitchLoadedPage δέχεται έναν δείκτη σελίδας-στόχου, έναν δείκτη σελίδας-πηγής, μια μετατόπιση X/Y και έναν συντελεστή κλίμακας, και σχεδιάζει τη σελίδα πηγής πάνω στον στόχο σε εκείνη τη θέση και στο μέγεθος αυτό:

// Overlay page 2 (index 1) onto page 1 (index 0),
// scaled to 70% and nudged up-right.
Pdf.StitchLoadedPage(0, 1, 40, 380, 0.7);

// Convenience 2-up: source page on the right half of the target.
Pdf.StitchLoadedPageSideBySide(0, 1);

Η συμβολοσειρά τελεστών που προσθέτει είναι μια τυπική ακολουθία μετασχηματισμού και σχεδίασης: q για να αποθηκεύσει την κατάσταση γραφικών, ένα cm matrix που μεταφέρει την κλίμακα στη διαγώνιο και τη μετατόπιση στα πεδία μετάθεσης, /StitchSrc Do για να καλέσει ένα εξωτερικό αντικείμενο, και Q για να επαναφέρει την κατάσταση. Το q/Q ζεύγος έχει σημασία: απομονώνει τον μετασχηματισμό ώστε η συγχωνευμένη σελίδα να μην διαρρεύσει το σύστημα συντεταγμένων της σε οτιδήποτε προστεθεί μετά. Η μέθοδος προστατεύει επίσης από τα προφανή λάθη - δείκτες εκτός εύρους, στόχο ίσο με την πηγή, μη θετική κλίμακα (την οποία τη σφίγγει στο 1.0) - και τερματίζει σιωπηλά αντί να εγείρει εξαίρεση, οπότε έλεγξε τα δεδομένα εισόδου σου γιατί ένα σιωπηλό no-op μοιάζει ίδιο με επιτυχία

StitchLoadedPageSideBySide είναι μια λεπτή ευκολία πάνω από τη γενική μέθοδο. Διαβάζει το πλάτος του media-box του στόχου, το διαιρεί στα δύο, και καλεί το StitchLoadedPage με αυτό το μισό πλάτος ως μετατόπιση X και με σταθερή κλίμακα 0.5, τοποθετώντας την πηγή στο δεξί μισό. Αυτή η σκληροκωδικοποιημένη τιμή 0.5 υποθέτει ότι η πηγή και ο στόχος μοιράζονται το ίδιο πλάτος· αν όχι, η πηγή δεν θα γεμίσει καθαρά το μισό της, και θα θελήσεις τη γενική StitchLoadedPage με μια κλίμακα που υπολογίζεις εσύ από τα δύο media boxes

Η απλοποιημένη στρατηγική XObject και ο συμβιβασμός της με το ISO

Εδώ είναι που η υλοποίηση κάνει μια συνειδητή συντόμευση που πρέπει να ξέρεις πριν εμπιστευτείς το αποτέλεσμα σε όλους τους αναγνώστες. Μια σωστή επιβολή N-up τυλίγει το περιεχόμενο της σελίδας πηγής μέσα σε ένα Form XObject - ένα αυτοτελές αντικείμενο σχεδίασης που το ISO 32000-1 §8.10.1 λέει ότι πρέπει να φέρει /Type /XObject, /Subtype /Form, και το δικό του /BBox clipping box. Το round-nine stitch του HotPDF δεν χτίζει αυτό το wrapper. Αντί γι' αυτό εγγράφει το ίδιο το το ίδιο το page dictionary απευθείας κάτω από το /Resources /XObject του στόχου με το όνομα StitchSrc, και μετά το σχεδιάζει με Do. Ένα page dict και ένα Form XObject μοιράζονται αρκετό από το περιεχόμενό τους - και τα δύο αναφέρονται σε content stream και resource dictionary - ώστε πολλοί αναγνώστες PDF να αποδώσουν το αποτέλεσμα

Αλλά δεν είναι ένα συμβατό Form XObject. Του λείπει το /Subtype /Form marker και το δικό του /BBox, που σημαίνει ότι ένας αυστηρός consumer έχει κάθε δικαίωμα να αγνοήσει το Do ή να το περικόψει διαφορετικά από όσο περιμένεις. Οι TechnicalNotes για αυτόν τον γύρο το λένε καθαρά: η προσέγγιση "αποδίδει στους περισσότερους αναγνώστες" αλλά είναι "όχι ένα Form XObject που συμμορφώνεται αυστηρά με το ISO", και η πλήρης συμμόρφωση απαιτεί να συνθέσεις ένα πραγματικό Form XObject stream ως ξεχωριστό βήμα. Άρα αντιμετώπισε το stitch output όπως θα αντιμετώπιζες κάθε μη συμβατό κατασκεύασμα: επαλήθευσέ το στους συγκεκριμένους αναγνώστες που τρέχουν οι πελάτες σου, όχι μόνο σε αυτόν που έχεις στο μηχάνημά σου, και αν χρειάζεσαι αρχειακά ή αυστηρά validator-clean PDFs, μην βασίζεσαι σε αυτή τη διαδρομή. Η ίδια πειθαρχία ισχύει για οτιδήποτε χτίζεις πάνω στο φορτωμένο object graph, και γι' αυτό ένα έλεγχος preflight PDF στο Delphi κερδίζει τη θέση του στο release pipeline κάθε φορά που μεταβάλλεις έγγραφα προγραμματιστικά

Πού ταιριάζουν αυτά και πού όχι

Και οι δύο μέθοδοι είναι εργαλεία content-stream, οπότε το νοητικό μοντέλο είναι το ίδιο που χρησιμοποιείς για άμεση σχεδίαση. Αν έχεις χτίσει σελίδες από το μηδέν με το component, οι διανυσματικοί και χρωματικοί τελεστές πίσω από αυτές τις κλήσεις θα σου είναι γνώριμοι από το σχεδίαση καμβά HotPDF στο Delphi; η διαφορά είναι μόνο ότι εδώ προσθέτεις σε ένα stream που έγραψε κάποιος άλλος αντί για ένα που έγραψες εσύ. Κράτα τρία όρια στο μυαλό σου:

  • Η redaction είναι καλλυντική. RedactLoadedRect καλύπτει το περιεχόμενο και δεν το διαγράφει ποτέ. Για οτιδήποτε ευαίσθητο, ξαναδημιούργησε την πηγή ή χρησιμοποίησε πραγματική αφαίρεση περιεχομένου - ένα μαύρο πλαίσιο δεν είναι ασφάλεια
  • Το stitch είναι μη συμβατό εκ κατασκευής.Η σελίδα πηγής αναφέρεται ως pseudo-XObject χωρίς το §8.10.1 /Subtype /Form και /BBox, οπότε επιβεβαίωσε την απόδοση στα προγράμματα προβολής-στόχους σου και απόφυγέ το όπου απαιτείται αυστηρή επικύρωση
  • Οι συντεταγμένες είναι στον user space της σελίδας. Αφετηρία κάτω αριστερά, μονάδες σε points, καθοδηγούμενες από το ίδιο το media box της σελίδας. Διάβασε το box με GetLoadedPageBox πριν τοποθετήσεις οτιδήποτε, γιατί η σελίδα που φόρτωσες μπορεί να μην έχει το μέγεθος που υπέθεσες

Χρησιμοποιώντας τα μέσα σε αυτά τα όρια, το ζεύγος καλύπτει μια πραγματική ροή εργασίας: αναδιάταξη σελίδων για εκτύπωση, κάλυψη μη εμπιστευτικών περιοχών και εγγραφή του αποτελέσματος πίσω με SaveLoadedDocument χωρίς πλήρη επανασχεδίαση. Το API του φορτωμένου εγγράφου που περιλαμβάνει αυτά τα stitch και mask primitives διατίθεται με το HotPDF Component για Delphi και C++Builder, μαζί με τις μεθόδους για form fields, annotations και FDF από τον ίδιο γύρο