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

XPS και OpenXPS σε PDF σε Delphi: Συντεταγμένες και πινέλα

Το HotPDF μετατρέπει πακέτα XPS και OpenXPS σε PDF μέσα σε Delphi και C++Builder χωρίς οδηγό εκτύπωσης, αντιστοιχίζοντας κάθε συντεταγμένη fixed-page 96-DPI μέσα από έναν πίνακα σελίδας κλίμακας 0.75 με αναστροφή Y, δημοσιεύοντας κάθε VisualBrush ως κοινόχρηστο Form XObject, και μετατρέποντας τις λειτουργίες tile των ImageBrush σε εγγενή μοτίβα πλακόστρωσης PDF αντί για επαναλαμβανόμενες σχεδιάσεις εικόνων

Το σενάριο που παρασέρνει τις περισσότερες ομάδες που δουλεύουν σε Windows είναι βαρετό και αναπόφευκτο. Κάτι ήδη εκτυπώνεται στον Microsoft XPS Document Writer — μια αναφορά legacy ERP, ένα υπογεγραμμένο έντυπο, μια παρτίδα καταστάσεων — και η πολιτική αρχειοθέτησης λέει PDF. Το XPS είναι μια άψογη μορφή καταγραφής και μια φρικτή επιλογή για ένα σύστημα διαχείρισης αρχείων σε δέκα χρόνια από τώρα. Έτσι το αρχείο spool πρέπει να γίνει PDF σελίδα-προς-σελίδα, και τη στιγμή που αρχίζετε να γράφετε τον μετατροπέα ανακαλύπτετε πως το ενδιαφέρον μέρος δεν είναι το XML. Είναι ότι το XPS και το PDF διαφωνούν για το πού βρίσκεται η αρχή των αξόνων, πόσο αξίζει μια μονάδα και τι επιτρέπεται να είναι ένα πινέλο

Από πακέτο σε PDF σε ένα πέρασμα

Το σημείο εισόδου είναι το μητρώο χειριστών εγγράφων, όχι μια ειδική κλάση XPS. Το THPDFDocumentHandlerRegistry.RegisterStandardHandlers εγκαθιστά τους χειριστές XPS, EPUB και CBZ· η αναγνώριση βασίζεται στο περιεχόμενο, οπότε ένα πακέτο με [Content_Types].xml και τουλάχιστον ένα μέρος .fpage βγάζει σκορ 95 ακόμα κι όταν η επέκταση αρχείου λέει ψέματα, ενώ μια γυμνή επέκταση .xps ή .oxps βγάζει μόνο 10. Αυτή η σειρά έχει σημασία όταν δέχεστε μεταφορτώσεις, γιατί ένας επιτιθέμενος που μετονομάζει ένα EPUB σε .xps δεν πρέπει να καθοδηγεί τη γραμμή επεξεργασίας

var
  Handled: IHPDFHandledDocument;
  Info: THPDFDocumentHandlerInfo;
  Options: THPDFDocumentHandlerOptions;
  Registry: THPDFDocumentHandlerRegistry;
  Output: TFileStream;
begin
  Registry := THPDFDocumentHandlerRegistry.Create;
  Output := TFileStream.Create('spool.pdf', fmCreate);
  try
    Registry.RegisterStandardHandlers;
    Options := THPDFDocumentHandlerOptions.Default;
    if not Registry.OpenFromFile('spool.xps', '', Options, Handled, Info) then
      raise Exception.Create('no registered handler recognised the package');
    if Handled.Format = hdfXPS then
      Handled.WritePDF(Output, Options, Info);
    // Το Info.UnsupportedFeatureCount είναι το τίμιο σκορ για αυτή τη μετατροπή
  finally
    Output.Free;
    Registry.Free;
  end;
end;

Όλα στη μετατροπή έχουν προϋπολογιστεί πριν επιχειρηθούν. Το THPDFDocumentHandlerOptions.Default ορίζει όριο 10.000 καταχωρήσεις αρχειοθήκης, 1 GiB αναπτυγμένα bytes αρχειοθήκης, λόγο συμπίεσης 200, 4.096 πόρους και 10.000 σελίδες, και κουβαλά ένα προαιρετικό CancellationToken ώστε μια εργασία σε διακομιστή να μπορεί να σταματήσει μέσα στο πακέτο. Διαβάστε μετά το Info.UnsupportedFeatureCount και αντιμετωπίστε μη μηδενική τιμή ως πραγματικό εύρημα: το HotPDF μετράει σκόπιμα ό,τι δεν μπόρεσε να αντιστοιχίσει αντί να σχεδιάζει προσέγγιση και να σωπάσει

Γιατί μια σελίδα XPS χρειάζεται πίνακα αντί για επανεγραμμένες συντεταγμένες;

Επειδή η επανεγγραφή συντεταγμένων χάνει τη στοίβα μετασχηματισμών. Ένα FixedPage XPS ορίζεται σε μονάδες 96-DPI με την αρχή των αξόνων πάνω αριστερά και το Y να μεγαλώνει προς τα κάτω· ο χώρος χρήστη PDF είναι 72-DPI με την αρχή κάτω αριστερά και το Y προς τα πάνω. Η αφελής λύση είναι να πολλαπλασιάσετε κάθε αριθμό επί 0.75 και να αφαιρείτε κάθε Y από το ύψος της σελίδας όσο τον εκπέμπετε. Δουλεύει για ένα επίπεδο path και καταρρέει τη στιγμή που μπαίνει ένα RenderTransform, ένα εμφωλευμένο Canvas ή ένας πίνακας τοπικός του πινέλου, επειδή αυτοί οι μετασχηματισμοί ορίζονται στον χώρο XPS και η επανεγγραφή ανά συντεταγμένη έχει ήδη φύγει από εκεί. Το HotPDF λοιπόν κρατά την προβολή ως πίνακα και τη συνθέτει. Το HPDFXPSPageMatrix επιστρέφει τις σταθερές τιμές μία φορά ανά σελίδα, το HPDFMultiplyXPSMatrix τον συνενώνει με τον αθροισμένο μετασχηματισμό path, και το αποτέλεσμα εκπέμπεται ως ένας μοναδικός τελεστής cm πριν τη γεωμετρία. Τα δεδομένα path γράφονται μετά σε αμετάβλητους αριθμούς XPS — γεγονός που εξηγεί και γιατί η συντομογραφημένη σύνταξη γεωμετρίας μπορεί να μοιράζεται τον ίδιο φραγμένο parser που χρησιμοποιείται για δεδομένα path SVG — μόνο το αρχικό token fill-rule F0 ή F1 το χειρίζεται ο προσαρμογέας XPS. Αν έχετε ακολουθήσει την ίδια συλλογιστική για τη εισαγωγή διανυσματικών EMF και WMF, το σχήμα του επιχειρήματος είναι οικείο: οι μορφές εισαγωγής μετατρέπονται με πίνακα, ποτέ με αριθμητική πάνω στις τελικές συντεταγμένες

Το HotPDF συνθέτει τον σταθερό πίνακα σελίδας XPS με τον αθροισμένο μετασχηματισμό path ώστε ένα σύστημα συντεταγμένων XPS 96-DPI με αρχή πάνω αριστερά να φτάνει στον χώρο χρήστη PDF 72-DPI με αρχή κάτω αριστερά, εκπεμπόμενο ως ένας τελεστής cm ανά visual
Η προβολή παραμένει πίνακας και συντίθεται με κάθε εμφωλευμένο μετασχηματισμό, ώστε τα δεδομένα path να γράφονται σε αμετάβλητους αριθμούς XPS
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
  Result.A :=  0.75;       // Μονάδα XPS 96-DPI σε σημείο PDF 72-DPI
  Result.B :=  0;
  Result.C :=  0;
  Result.D := -0.75;       // Το Y του XPS μεγαλώνει προς τα κάτω, του PDF προς τα πάνω
  Result.E :=  0;
  Result.F := PageHeight;  // Ύψος σελίδας PDF, σε points
end;

// Ένα συντεθειμένο CTM ανά visual, εκπεμπόμενο πριν από οποιονδήποτε τελεστή path
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));

Πώς επαναχρησιμοποιείτε ένα VisualBrush χωρίς να το σχεδιάσετε δύο φορές;

Ένα VisualBrush ζωγραφίζει ένα αυθαίρετο visual δέντρο — παιδιά Canvas, Path και Glyphs — σε μια περιοχή, ενδεχομένως επαναλαμβανόμενα σε όλη της. Το HotPDF μεταφέρει αυτό το visual μία φορά σε ένα PDF Form XObject και μετά το τοποθετεί, η ίδια στρατηγική πόρων που περιγράφεται στη εισαγωγή SVG μέσω Form XObjects. Δύο λεπτομέρειες αποφασίζουν αν θα δουλέψει. Πρώτον, το visual πρέπει να διασχιστεί ως απευθείας παιδιά XML: μια επίπεδη σάρωση για στοιχεία άξια tile τραβάει εμφωλευμένα visuals στην κορυφή της σελίδας και καταστρέφει τόσο το scoping πόρων όσο και τη σειρά ζωγραφικής. Δεύτερον, το περιεχόμενο καταγράφεται με τον πίνακα σελίδας XPS-σε-PDF ήδη εφαρμοσμένο, οπότε η δημοσίευση του Form απαιτεί πολλαπλασιασμό με τον αντίστροφο του πίνακα, αλλιώς κάθε τοποθέτηση εφαρμόζει ξανά την κλίμακα 0.75 και την αναστροφή Y. Το Form πρέπει επίσης να έχει τους δικούς του πόρους: το HotPDF αντιγράφει μόνο τις γραμματοσειρές, τα XObjects, τα μοτίβα, τα ExtGStates και τους χρωματικούς χώρους που αναφέρει πραγματικά η καταγεγραμμένη ροή περιεχομένου· η κλωνοποίηση όλου του λεξικού πόρων της σελίδας θα παρέσυρε το Form που καταχωρείται στον δικό του γράφο πόρων και θα έφτιαχνε κύκλο. Οι γραμματοσειρές μένουν σε απευθείας λεξικό στις συνηθισμένες σελίδες και προάγονται σε κοινόχρηστο έμμεσο λεξικό μόνο όταν το καταγεγραμμένο περιεχόμενο περιέχει πραγματικά Tf, ώστε ένα έγγραφο χωρίς επαναχρησιμοποιήσιμα visuals να μην πληρώνει για τον μηχανισμό. Σημειώστε ένα όριο της προδιαγραφής που αξίζει να ξέρετε πριν υποβάλετε bug: το κεφάλαιο 13.4 του ECMA-388 απαιτεί και τα ViewboxUnits και ViewportUnits ενός VisualBrush να είναι Absolute, οπότε οι σχετικές μονάδες δεν είναι ελλιπές χαρακτηριστικό — είναι μη συμμορφούμενη είσοδος, και το HotPDF αρνείται να επινοήσει σημασιολογία συντεταγμένων για αυτές

Το HotPDF μεταφέρει ένα visual δέντρο VisualBrush XPS μία φορά σε PDF Form XObject, το δημοσιεύει μέσω του αντιστρόφου του πίνακα fixed-page ώστε οι τοποθετήσεις να μην εφαρμόζουν ξανά την κλίμακα, και αντιγράφει μόνο τους πόρους που αναφέρει πραγματικά το καταγεγραμμένο περιεχόμενο
Το περιεχόμενο καταγράφεται με τον πίνακα σελίδας ήδη εφαρμοσμένο, οπότε το Form δημοσιεύεται μέσω του αντιστρόφου του και κουβαλά μόνο τους πόρους που αναφέρει η δική του ροή περιεχομένου

Πλακόστρωση ImageBrush: τέσσερις λειτουργίες, τέσσερα μεγέθη κελιού

Οι λειτουργίες tile του XPS αντιστοιχίζονται σε μοτίβα πλακόστρωσης PDF από το κεφάλαιο 8.7.3 του ISO 32000-1 αντί να απλώνονται σε επαναλαμβανόμενες τοποθετήσεις εικόνων στην καλυμμένη περιοχή, κάτι που κρατά το μέγεθος εξόδου και τον χρόνο μετατροπής ανεξάρτητα από το πόση σελίδα καλύπτει το πινέλο. Η αντιστοίχιση είναι μηχανική μόλις τη δείτε: ο κατοπτρισμός εκφράζεται βάζοντας κατοπτρισμένες τοποθετήσεις μέσα σε ένα κελί μοτίβου και μεγαλώνοντας το κελί ώστε να ταιριάζει

  • Tile — μία τοποθέτηση, το κελί μένει viewport 1×1
  • FlipX — δύο τοποθετήσεις, το κελί πλαταίνει σε 2×1
  • FlipY — δύο τοποθετήσεις, το κελί υψώνεται σε 1×2
  • FlipXY — τέσσερις τοποθετήσεις, το κελί απλώνεται σε 2×2

Κάθε τοποθέτηση κουβαλά το δικό της ορθογώνιο περικοπής, γιατί μια αντιστοίχιση Viewbox που ξεχειλίζει από το υπο-κελί της θα αιμορραγούσε στον γειτονικό κατοπτρισμό. Το /Matrix του μοτίβου είναι το κομμάτι που παγιδεύει τους ανθρώπους. Ένα μοτίβο πλακόστρωσης αγκυρώνεται στον προεπιλεγμένο χώρο χρήστη της γονικής ροής περιεχομένου του, όχι στη γραφική κατάσταση που ίσχυε τη στιγμή της επιλογής του μοτίβου, οπότε ο πίνακας πρέπει να συνθέσει ρητά και τα τρία στρώματα — την προβολή fixed-page, τον μετασχηματισμό Path και το Transform τοπικό του πινέλου — αντί να βασιστεί σε έναν περιβάλλοντα CTM. Το HotPDF επίσης επικυρώνει πριν δεσμεύσει: το RegisterImageTilingPattern περιορίζει ένα μοτίβο σε 1.024 τοποθετήσεις και απορρίπτει εκφυλισμένες περικοπές, μη αντιστρέψιμους πίνακες και άκυρα ευρετήρια εικόνων. Αν θέλετε το γενικό μοντέλο πλευράς PDF πίσω από αυτό, το μοτίβα πλακόστρωσης και ο χρωματικός χώρος Pattern καλύπτει τους υποκείμενους τελεστές

// Η προβολή fixed-page διπλωμένη στον πίνακα του μοτίβου, μετά ο τοπικός του πινέλου
PatternMatrix := HPDFMatFromOps( 0.75 * PathMatrix.A, -0.75 * PathMatrix.B,
                                 0.75 * PathMatrix.C, -0.75 * PathMatrix.D,
                                 0.75 * PathMatrix.E,
                                 PageHeight - 0.75 * PathMatrix.F);
if Brush.HasTransform then
  PatternMatrix := HPDFMatMul(PatternMatrix, BrushMatrix);

PatternName := Document.RegisterImageTilingPattern(Resource.ImageIndex,
  Brush.Viewport.Left, Brush.Viewport.Top,
  Brush.Viewport.Left + CellWidth, Brush.Viewport.Top + CellHeight,
  CellWidth, CellHeight, Placements, pttNoDistortion, PatternMatrix);

Τι συμβαίνει όταν μια ακτινική διαβάθμιση δεν είναι κύκλος;

Το XPS ορίζει ένα RadialGradientBrush με GradientOrigin, Center, RadiusX και RadiusY, οπότε το πινέλο είναι έλλειψη. Η σκίαση τύπου 3 του PDF, στο κεφάλαιο 8.7.4.5.4 του ISO 32000-1, συνδυάζει μεταξύ δύο κύκλων και δεν έχει τρόπο να εκφράσει έλλειψη απευθείας. Ο μέσος όρος των δύο ακτίνων σε έναν αριθμό είναι η δελεαστική συντόμευση και είναι ορατά λάθος σε κάθε πινέλο που δεν είναι κοντά στο στρογγυλό. Το HotPDF αντ' αυτού μεταφέρει το πρόβλημα στο σύστημα συντεταγμένων: κλιμακώνει το Y επί RadiusY / RadiusX, καταχωρεί μια τίμια κυκλική σκίαση σε αυτόν τον κλιμακωμένο χώρο, επιλέγει το μοτίβο, και αμέσως εκπέμπει την αντίστροφη κλίμακα ώστε η γεωμετρία path που γράφεται μετά να βρίσκεται ακόμα στον αρχικό χώρο χρήστη XPS

ScaleY := Brush.RadiusY / Brush.RadiusX;
PatternName := Document.RegisterMultiStopRadialGradient(
  Brush.StartX, Brush.StartY / ScaleY, 0,
  Brush.EndX,   Brush.EndY   / ScaleY, Brush.RadiusX,
  StopPositions, StopColours, 3);

if Abs(ScaleY - 1) > 0.000001 then
  Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(ScaleY) + ' 0 0 cm'#10);
Page.SetFillPattern(PatternName);   // το μοτίβο δεσμεύει το CTM ακριβώς εδώ
if Abs(ScaleY - 1) > 0.000001 then
  Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);

Η σειρά στο απόσπασμα είναι όλο το κόλπο, και δεν είναι ζήτημα στιλ. Ένα μοτίβο σκίασης PDF δεσμεύει τον τρέχοντα πίνακα μετασχηματισμού τη στιγμή που επιλέγεται ως τρέχον χρώμα, οπότε η προσωρινή κλίμακα πρέπει να εκπεμφθεί πριν το SetFillPattern ή το SetStrokePattern, και η αντίστροφη πρέπει να έρθει μετά την επιλογή αλλά πριν τους τελεστές path. Πάρτε τη σειρά λάθος προς οποιαδήποτε κατεύθυνση και έχετε μια διαβάθμιση που αποδίδει σωστά στο πρώτο path και ολισθαίνει σε κάθε επόμενο. Σχετικός περιορισμός ισχύει για τη σχετική λειτουργία συντεταγμένων: το RadiusX και το RadiusY πρέπει να επιλύονται ξεχωριστά κατά το πλάτος και το ύψος του path, αφού η κλιμάκωση και των δύο με ένα μοναδικό μήκος πλευράς αλλάζει σιωπηλά την αναλογία διαστάσεων της έλλειψης σε κάθε μη τετράγωνο path

Το HotPDF αντιστοιχίζει ένα ελλειπτικό RadialGradientBrush XPS στη σκίαση τύπου 3 του PDF κλιμακώνοντας το Y, καταχωρώντας κυκλική σκίαση στον κλιμακωμένο χώρο, και εκπέμποντας την αντίστροφη κλίμακα μόνο αφού το μοτίβο έχει δεσμεύσει τον τρέχοντα πίνακα μετασχηματισμού
Η έλλειψη απορροφάται από το σύστημα συντεταγμένων και όχι από τη σκίαση, και η προσωρινή κλίμακα πρέπει να πλαισιώσει την επιλογή του μοτίβου με ακριβώς αυτή τη σειρά

Πού η μετατροπή είναι τίμια για τα όριά της

Κάποια constructs XPS μετατρέπονται προσεγγιστικά και κάποια δεν μετατρέπονται καθόλου, και η σχεδιαστική επιλογή σε όλο το μήκος είναι να μετριούνται αντί να πλαστογραφούνται. Τα μέρη TIFF και JPEG XR ραστεροποιούνται μέσω WIC και δεν κουβαλούν καμία υπόσχεση για διατηρημένο alpha, ενώ PNG με έγκυρο κανάλι alpha χωρίζεται σε εικόνα βάσης και ένα /SMask. Το εγγενές μέγεθος εικόνας προκύπτει ως pixel * 96 / DPI, διαβάζοντας πρώτα την πυκνότητα PNG pHYs ή JPEG JFIF και με επιστροφή σε 96 DPI, ώστε ένα κακό header πυκνότητας καταλήγει σε προβλέψιμο μέγεθος και όχι σε αυθαίρετο. Άλυτοι πόροι πινάκων, μη τυποποιημένοι σχετικοί μετασχηματισμοί, ColorConvertedBitmap, μη υποστηριζόμενες λειτουργίες εξάπλωσης διαβάθμισης και κακοσχηματισμένη γεωμετρία όλα αυξάνουν το UnsupportedFeatureCount, και η κακοσχηματισμένη είσοδος αποτυγχάνει ερμητικά αντί να εκφυλιστεί σε μια σιωπηλά διαφορετική σχεδίαση

Αυτή είναι η χρήσιμη στάση για έναν αρχειακό μετατροπέα: μια μετατροπή που προσεγγίζει σιωπηλά είναι χειρότερη από μία που σας λέει ποια τέσσερα στοιχεία δεν μπόρεσε να αναπαραστήσει, γιατί μόνο η δεύτερη σάς δίνει κάτι να ελέγξετε πριν το έγγραφο σφραγιστεί σε ένα σύστημα διαχείρισης αρχείων. Αν αξιολογείτε τη μετατροπή XPS και OpenXPS μαζί με την υπόλοιπη γραμμή εγγράφων — σελιδοποίηση, γραμματοσειρές, υπογραφές, έξοδο PDF/A — η σελίδα του HotPDF Delphi PDF component αναφέρει το πλήρες σύνολο χαρακτηριστικών και τις υποστηριζόμενες εκδόσεις Delphi και C++Builder