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

Εναλλαγή layers προαιρετικού περιεχομένου PDF με PDFium

Το PDFium Component ελέγχει τα layers προαιρετικού περιεχομένου PDF (OCGs) στο Delphi μέσα από δύο μεθόδους του TPdf: το InspectOptionalContent απαριθμεί κάθε layer μαζί με την ορατότητα που θα αποδώσει πραγματικά το PDFium, και το SaveAsOptionalContentConfigured γράφει ένα επαληθευμένο αντίγραφο όπου τα layers που διαλέγετε είναι σβηστά ή ανοιχτά. Η δεύτερη μέθοδος εξουδετερώνει επίσης τους κανόνες Usage και /AS που αλλιώς θα ανέτρεπαν σιωπηλά την επεξεργασία σας. Και οι δύο δουλεύουν πάνω στο έγγραφο ήδη ανοιχτό στο TPdf, οπότε δεν υπάρχει δεύτερος parser για να συγχρονίζεται με ό,τι δείχνει ο viewer

Το αίτημα συνήθως έρχεται από γραφείο CAD ή GIS: το σύνολο σχεδίων παραδίδεται με διαστάσεις, σχολιασμούς και πινακίδα τίτλου σε ξεχωριστά layers, και ο πελάτης θέλει ένα αντίγραφο με κρυμμένες τις διαστάσεις πριν πάει σε προμηθευτή. Το PDFium αποδίδει σωστά το προαιρετικό περιεχόμενο, αλλά το public ABI του δεν έχει συνάρτηση να απαριθμήσει OCGs, να διαλέξει configuration ή να αναποδογυρίσει την κατάσταση ενός layer. Οπότε κατεβαίνεις στο επίπεδο αντικειμένων, επεξεργάζεσαι το /OCProperties, αποθηκεύεις, ξαναφορτώνεις, και το layer είναι ακόμη εκεί. Η αιτία είναι η λογική ορατότητας του PDFium, και αξίζει να την καταλάβεις πριν αγγίξεις οποιαδήποτε bytes

Γιατί η επεξεργασία των /ON και /OFF δεν αλλάζει ό,τι αποδίδει το PDFium;

Η επεξεργασία των array /ON και /OFF του λεξικού configuration δεν φτάνει, επειδή το PDFium αφήνει μια ρητή κατάσταση μέσα στο δικό του λεξικό /Usage του OCG να κερδίσει εκείνα τα array, και ένας κανόνας αυτόματης κατάστασης /AS μπορεί μετά να υπερισχύσει και των δύο. Το ISO 32000-1 §8.11.4 περιγράφει configurations και usage dictionaries ως ξεχωριστούς μηχανισμούς· ο renderer του PDFium τα διπλώνει σε μία απόφαση, και το InspectOptionalContent την αναπαράγει με αυτή τη σειρά:

  • Ξεκινάς από το /BaseState του configuration, όπου /ON και /Unchanged μετρούν και τα δύο ως ορατά και μόνο το /OFF κρύβει
  • Εφαρμόζεις το array /ON του configuration, μετά το /OFF του, ώστε μια ομάδα που εμφανίζεται και στα δύο να καταλήγει κρυμμένη
  • Εφαρμόζεις τη ρητή κατάσταση Usage της ομάδας για το ζητούμενο usage, όπως /Usage << /View << /ViewState /OFF >> >>, που υπερισχύει όλων των παραπάνω
  • Θεωρείς ορατή μια ομάδα της οποίας το /Intent δεν περιέχει ούτε /View ούτε /All, αφού δεν λαμβάνει μέρος στην ορατότητα view-intent
  • Τέλος τρέχεις το array /AS του επιλεγμένου configuration, του οποίου οι εγγραφές για το ταιριαστό event θέτουν την κατάσταση των ομάδων που απαριθμούν
Η απόφαση ορατότητας πέντε βημάτων που αναπαράγει το PDFium Component για κάθε ομάδα προαιρετικού περιεχομένου PDF στο Delphi: το BaseState ορίζει την αρχή, τα array ON και OFF του configuration εφαρμόζονται με σειρά, μια ρητή εγγραφή Usage ViewState ή PrintState υπερισχύει και των δύο, η μη συμμετοχή Intent μετρά ως ορατό, και το array AS τρέχει τελευταίο
Η επεξεργασία των array ON και OFF δεν φτάνει επειδή το PDFium διπλώνει το BaseState, και τα δύο array, την κατάσταση Usage της ομάδας και τέλος τους κανόνες αυτόματης κατάστασης AS σε μία ετυμηγορία που το InspectOptionalContent αναπαράγει βήμα βήμα

Το τρίτο βήμα είναι αυτό που καίει τον κόσμο. Ένα αρχείο αποθηκευμένο από εργαλείο layout συχνά κουβαλάει /ViewState /ON σε κάθε OCG, και το PDFium τότε αγνοεί το προσεκτικά επεξεργασμένο array /OFF σου: το save πετυχαίνει, το αρχείο ξανανοίγει καθαρά, και το layer συνεχίζει να ζωγραφίζει. Για Print και Export, το OcExplicitUsageState διαβάζει πρώτα PrintState ή ExportState και πέφτει στο ViewState όταν η ειδική εγγραφή απουσιάζει, ώστε ένα μοναχικό ViewState /ON καρφώνει το layer και για εκτύπωση. Το marked content που αναφέρεται σε OCMD (§8.11.2.2) επιλύεται έπειτα απέναντι σε αυτά τα αποτελέσματα ανά ομάδα, μέσω της πολιτικής /P ή, όταν υπάρχει, της έκφρασης ορατότητας /VE

Πώς απαριθμείς τα layers που θα δείξει πραγματικά το PDFium;

Το TPdf.InspectOptionalContent επιστρέφει ένα TPdfOptionalContentInventory του οποίου το array Groups κουβαλάει τον αριθμό αντικειμένου κάθε OCG, το όνομα, τα intents, τις τρεις καταστάσεις Usage, τη γλώσσα, το εύρος zoom, τη σημαία Locked, τον δείκτη radio-group και το υπολογισμένο EffectiveVisible. Η μέθοδος πρώτα βάζει το PDFium να αποθηκεύσει το τρέχον έγγραφο στη μνήμη, αναπτύσσει object streams, και σαρώνει το αποτέλεσμα, ώστε να αντανακλώνται οι επεξεργασίες που έγιναν νωρίτερα στη συνεδρία. Ο δείκτης configuration 0 είναι πάντα το προεπιλεγμένο λεξικό /D και οι εγγραφές του /Configs ακολουθούν από τον δείκτη 1· το προεπιλεγμένο όρισμα -1 διαλέγει τον δείκτη 0. Ένα έγγραφο χωρίς /OCProperties κάνει τη μέθοδο να επιστρέφει False με την αιτία στο ErrorMessage αντί να πετάει exception

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  G: TPdfOptionalContentGroup;
begin
  // Το Usage έχει προεπιλογή ocuView· το -1 διαλέγει configuration 0, το λεξικό /D
  if not Pdf.InspectOptionalContent(Inv) then
  begin
    Memo1.Lines.Add('No usable layers: ' + Inv.ErrorMessage);
    Exit;
  end;
  Memo1.Lines.Add(Format('Configuration %d: %s',
    [Inv.SelectedConfigurationIndex,
     string(Inv.Configurations[Inv.SelectedConfigurationIndex].Name)]));
  for G in Inv.Groups do
    Memo1.Lines.Add(Format('obj %d  %s  visible=%s  locked=%s  radio=%d',
      [G.ObjectNumber, string(G.Name),
       BoolToStr(G.EffectiveVisible, True),
       BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;

Το array Memberships αναφέρει κάθε OCMD με το Policy του (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff), το ακατέργαστο κείμενο VisibilityExpression και το δικό του EffectiveVisible. Μερικοί ακραίοι κανόνες είναι σκόπιμοι. Το /P έχει προεπιλογή το /AnyOn, και ένα OCMD χωρίς ομάδες μετρά ως ορατό. Μια αναφορά σε αριθμό αντικειμένου που δεν είναι γνωστό OCG αντιμετωπίζεται ως ορατή αντί να ρίξει ολόκληρη την έκφραση. Η αξιολόγηση του /VE σταματά σε βάθος εμφώλευσης 32 και θεωρεί οτιδήποτε πιο βαθύ κρυμμένο, που εμποδίζει μια εχθρική ή αυτοαναφερόμενη έκφραση να μετατρέψει την επιθεώρηση σε stack overflow

Γραφή νέας κατάστασης layer με SaveAsOptionalContentConfigured

Το TPdf.SaveAsOptionalContentConfigured παίρνει ένα array από records TPdfOptionalContentStateChange (αριθμός αντικειμένου ομάδας συν Visible) και γράφει ένα έγγραφο όπου το επιλεγμένο configuration παράγει ακριβώς εκείνη την κατάσταση. Το επιλεγμένο configuration παίρνει /BaseState /ON συν ολοκληρωμένα array /ON και /OFF που καλύπτουν κάθε ομάδα, και κάθε OCG που έχει ήδη λεξικό Usage λαμβάνει μια ρητή ViewState (ή PrintState / ExportState, ακολουθώντας το Options.Usage) που ταιριάζει στη νέα του κατάσταση. Με TPdfOptionalContentConfigureOptions.Default, το κλειδί /AS του επιλεγμένου configuration αφαιρείται ώστε ένα event open, print ή export να μην μπορεί να αναποδογυρίσει τα layers πίσω

procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer);
var
  Changes: TPdfOptionalContentStateChanges;
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  SetLength(Changes, 2);
  Changes[0].GroupObjectNumber := DimensionsObj;
  Changes[0].Visible := False;
  Changes[1].GroupObjectNumber := NotesObj;
  Changes[1].Visible := True;

  // Configuration 0, ocuView, DisableAutomaticState και EnforceRadioGroups True
  Options := TPdfOptionalContentConfigureOptions.Default;

  if not Pdf.SaveAsOptionalContentConfigured('C:\Out\Drawing-NoDims.pdf',
    Changes, Options, Report) then
    raise Exception.Create('Layer update rejected: ' + Report.ErrorMessage);

  Log(Format('%d of %d groups changed, %d Usage states rewritten, /AS removed: %s',
    [Report.ChangedGroupCount, Report.GroupCount,
     Report.UpdatedUsageStateCount,
     BoolToStr(Report.RemovedAutomaticState, True)]));
end;

Η διαδρομή εγγραφής κρατάει την ίδια την αποθηκευμένη έξοδο του PDFium ως πρόθεμα byte προς byte και προσαρτά μόνο τον ξαναγραμμένο ιδιοκτήτη configuration και τα αντικείμενα OCG που κουβαλάνε λεξικά Usage, ακολουθούμενα από μια νέα ενότητα xref και trailer. Πριν έστω ένα byte φτάσει στον προορισμό σας, το αποτέλεσμα ξανανοίγει σε ένα ξεχωριστό TPdf κάτω από την αυστηρή πολιτική φόρτωσης, και η μέθοδος αποτυγχάνει αν ο πίνακας cross-reference δεν επικυρωθεί. Η υπερφόρτωση αρχείου πηγαίνει ένα βήμα παραπέρα: γράφει σε προσωρινό αρχείο δίπλα στον στόχο και αντικαθιστά τον στόχο μόνο αφού η επαλήθευση πετύχει, ώστε μια απορριφθείσα ενημέρωση να μην αφήνει ποτέ πίσω ένα μισογραμμένο σχέδιο. Είναι η ίδια προσέγγιση επαληθευμένης σταδιακής αναθεώρησης που χρησιμοποιεί ο επεξεργαστής name tree και number tree PDF στο PDFium Component

Πώς γράφει το SaveAsOptionalContentConfigured στο PDFium Component ένα PDF Delphi με ανατεθειμένα layers: οι αλλαγές κατάστασης και οι επιλογές μπαίνουν, το επιλεγμένο configuration ξαναγράφεται με ολοκληρωμένα array ON και OFF και καταστάσεις Usage, η επαληθευμένη σταδιακή αναθεώρηση προσαρτάται, και ένα αυστηρό ξανα άνοιγμα πρέπει να επικυρωθεί πριν γραφτεί οτιδήποτε
Το configured save κρατάει το δικό του re-save του PDFium ως πρόθεμα bytes, προσαρτά τον ξαναγραμμένο ιδιοκτήτη configuration συν μια νέα ενότητα xref, και ξανανοίγει το αποτέλεσμα σε ξεχωριστό TPdf πριν αγγιχτεί ο προορισμός

Τι αρνείται να κάνει το configured save;

Το configured save αρνείται κάθε αλλαγή που το ίδιο το έγγραφο απαγορεύει ή δεν μπορεί να αναπαραστήσει με ασφάλεια, και κάθε άρνηση συμβαίνει πριν αγγιχτεί ο προορισμός. Ένας αριθμός αντικειμένου που δεν είναι στο /OCGs αποτυγχάνει ολότελα. Η αλλαγή μιας ομάδας που απαριθμείται στο array /Locked του configuration αποτυγχάνει, αν και η επανάληψη της τρέχουσας τιμής της επιτρέπεται. Με το EnforceRadioGroups ενεργό, κάθε σύνολο /RBGroups που θα κατέληγε με πάνω από ένα ορατό μέλος απορρίπτεται αντί να σβήνει σιωπηλά τα άλλα. Τα κρυπτογραφημένα έγγραφα απορρίπτονται επειδή plaintext σταδιακά αντικείμενα δεν μπορούν να κουβαλήσουν τον ενεργό security handler. Τα υπογεγραμμένα έγγραφα πετάνε EPdfError εκτός αν περάσετε AllowSignedDocument = True, αφού η αλλαγή του τι δείχνει μια σελίδα μπορεί να σπάσει την κάλυψη υπογραφής ή μια πολιτική πιστοποίησης

Οι πύλες άρνησης που εφαρμόζει το SaveAsOptionalContentConfigured στο PDFium Component πριν γράψει ένα configured PDF Delphi: αριθμός αντικειμένου εκτός OCGs αποτυγχάνει, κλειδωμένες ομάδες αποτυγχάνουν, σύνολα RBGroups με πάνω από ένα ορατό μέλος απορρίπτονται, κρυπτογραφημένα έγγραφα δεν μπορούν να κουβαλήσουν plaintext σταδιακά αντικείμενα, και υπογεγραμμένα αρχεία απαιτούν AllowSignedDocument
Κάθε άρνηση συμβαίνει πριν αγγιχτεί ο προορισμός, και η αιτία αποτυχίας καταλήγει στο Report.ErrorMessage αντί να αφήνει πίσω ένα μισογραμμένο σχέδιο
function TFormMain.SavePrintPreset(Target: TStream;
  const Changes: TPdfOptionalContentStateChanges): Boolean;
var
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  Options := TPdfOptionalContentConfigureOptions.Default;
  Options.Usage := ocuPrint;          // γράφει /Print << /PrintState ... >>
  Options.ConfigurationIndex := 1;    // πρώτη εγγραφή του /Configs, όχι /D
  try
    Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
      Report);                        // το AllowSignedDocument μένει False
    if not Result then
      ShowMessage(Report.ErrorMessage);
  except
    on E: EPdfError do
    begin
      ShowMessage(E.Message);         // υπογεγραμμένο αρχείο: τίποτα δεν γράφτηκε στο Target
      Result := False;
    end;
  end;
end;

Γνωρίστε τα αντισταθμίσματα πριν το δέσετε σε batch εργασία. Η προσαρτημένη αναθεώρηση κάθεται πάνω στο πλήρες re-save του PDFium, όχι στα bytes του αρχικού σας αρχείου, που είναι ακριβώς ο λόγος που υπογεγραμμένη είσοδος χρειάζεται ρητή συγκατάθεση. Το ξαναγράψιμο κανονικοποιεί επίσης το επιλεγμένο configuration σε /BaseState /ON, ώστε μια γραμμή βάσης /Unchanged ή /OFF του συγγραφέα αντικαθίσταται από ρητά array με την ίδια προκύπτουσα ορατότητα. Το να πέσει το /AS αφαιρεί κόλπα μόνο για εκτύπωση όπως ένα layer υδατογράφημα που εμφανίζεται μόνο στο χαρτί· ορίστε DisableAutomaticState σε False για να κρατηθούν εκείνοι οι κανόνες, δεχόμενοι ότι μπορεί να υπερισχύσουν της ζητούμενης κατάστασής σας για εκείνο το event. Στη θετική πλευρά, το PDF/A-2 (ISO 19005-2 clause 6.9) και το PDF/UA (ISO 14289-1 clause 7.10) απαγορεύουν και τα δύο το /AS στα λεξικά configuration, οπότε η προεπιλεγμένη έξοδος αφαιρεί ένα ζήτημα που διαφορετικά θα ανέφερε η προετοιμασία και επικύρωση PDF/A με PDFium Component

Πού χωράει ο έλεγχος layers σε έναν viewer PDF Delphi

Σε έναν viewer, ο έλεγχος layers είναι μια λίστα ελέγχου που την τροφοδοτεί το inventory συν ένα reload του αποθηκευμένου αποτελέσματος. Γεμίστε τη λίστα από το Groups, απενεργοποιήστε τις εγγραφές που είναι Locked, θεωρήστε τα μέλη που μοιράζονται RadioGroupIndex αμοιβαία αποκλειόμενα, και στο apply γράψτε σε ένα TMemoryStream και φορτώστε εκείνο το stream πίσω στο TPdf ώστε η προβολή να ζωγραφίσει τη νέα κατάσταση. Οι συνδέσεις ανάμεσα σε TPdf και TPdfView καλύπτονται στο χτίσιμο ενός πλούσιου σε features PDF viewer με PDFium VCL στο Delphi. Αδειοδότηση, trial downloads και το υπόλοιπο feature set βρίσκονται στη σελίδα προϊόντος PDFium Component για Delphi