Το PDFlibPas, η losLab PDF Developer Library for Delphi, γράφει κάθε αριθμό που βάζει σε ένα content stream με δεκαδικό διαχωριστικό τελεία και χωρίς εκθέτη, ό,τι κι αν λένε οι regional settings των Windows. Από το v3.539.26 τα AddPageMatrix, ScalePage, DeskewPage, RedactRegion, η έξοδος text-to-path και ο αναχρωματισμός μορφοποιούν τελεστές μέσω PLDoubleToStrConst, και από το v3.539.33 οι parsers που διαβάζουν αυτούς τους αριθμούς πίσω χρησιμοποιούν το PLTryStrToFloatInvariant αντί για το locale του συστήματος. Σε μια γερμανική, γαλλική ή βραζιλιάνικη μηχανή ο ίδιος κώδικας παράγει τώρα τα ίδια bytes όπως σε μια αμερικανική, που είναι η μόνη συμπεριφορά που αντέχει ένα file format
Γιατί ένα locale με κόμμα ως δεκαδικό χαλάει ένα PDF χωρίς error;
Ένα locale με κόμμα ως δεκαδικό χαλάει ένα PDF αθόρυβα επειδή το κόμμα δεν είναι χαρακτήρας αριθμού στη σύνταξη PDF, οπότε η ζημιά διαβάζεται ως έγκυρα tokens με λάθος σημασία. Πριν το fix, το PLFloatToStr δεν ήταν τίποτα περισσότερο από μια γυμνή κλήση FloatToStr, και το FloatToStr ακολουθεί το FormatSettings.DecimalSeparator. Με διαχωριστικό κόμμα, το AddPageMatrix(0.5, 0.5, 0, 0) έγραφε 0,5 0 0 0,5 0 0 cm. Το ISO 32000-1 §7.3.3 επιτρέπει ψηφία, μια τελεία και πρόσημο στην αρχή του αριθμού και τίποτα άλλο, οπότε ένας content parser διαβάζει εκείνη τη γραμμή ως τον αριθμό 0 ακολουθούμενο από άγνωστο token ,5, και ο τελεστής cm καταλήγει με λάθος τελεστέους. Τίποτα δεν πετάει, τίποτα δεν γράφει στο log. Η σελίδα απλώς αποδίδεται με έναν transformation matrix που έχει παρασυρθεί, και το να δουλέψετε ανάποδα από μια παραστρατημένη σχεδίαση μέχρι μια ρύθμιση locale είναι απελπισμένη υπόθεση
Το δεύτερο ελάττωμα κρύβεται πίσω από το πρώτο. Το FloatToStr χρησιμοποιεί τη μορφή ffGeneral, που στρέφει σε notation εκθέτη μόλις το μέγεθος πέσει κάτω από 1E-4, οπότε μια μικροσκοπική μετατόπιση βγηνόταν ως 1E-5. Το ίδιο §7.3.3 δηλώνει ότι το PDF δεν υποστηρίζει τη μορφή εκθέτη, που σημαίνει ότι ακόμα και μια μηχανή με αμερικανικό locale μπορούσε να γράψει άκυρο τελεστή με αρκετά μικρή τιμή. Τα regression tests αυτής της έκδοσης καρφώνουν και τα δύο σχήματα αποτυχίας: αναποδογυρίζουν τον διαχωριστικό σε κόμμα, καλούν το API και σαρώνουν το προκύπτον content για κάθε token που περιέχει κόμμα ή εκθέτη
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
OldSeparator: Char;
begin
Lib := TPDFlib.Create;
try
Lib.SetPageDimensions(300, 200);
Lib.DrawBox(10, 10, 20, 20, 1);
OldSeparator := FormatSettings.DecimalSeparator;
try
FormatSettings.DecimalSeparator := ','; // προσομοίωση desktop de-DE
Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
// το v3.539.26 και μετά γράφουν: 0.5 0 0 0.25 0.00001 12.75 cm
// παλιότερα builds έγραφαν: 0,5 0 0 0,25 1E-5 12,75 cm
Writeln(Lib.GetPageContentToString);
finally
FormatSettings.DecimalSeparator := OldSeparator;
end;
finally
Lib.Free;
end;
end;
Δύο είδη αριθμών, δύο οικογένειες helpers
Το fix στο PDFlibPas είναι ένας αυστηρός διαχωρισμός: οι αριθμοί που δείχνονται σε ανθρώπους μπορούν να ακολουθούν το locale, και οι αριθμοί που γράφονται για μια μηχανή ποτέ. Το PLFloatToStr και το PLStrToFloat μένουν στο PDFlibExtra.pas για κείμενο προς τον χρήστη, και η δήλωσή τους κουβαλά τώρα ένα σχόλιο που λέει ακριβώς αυτό. Ό,τι καταλήγει ως σύνταξη PDF περνά από το PLDoubleToStrConst με σταθερό αριθμό δεκαδικών ψηφίων επιλεγμένο για τη δουλειά: έξι για matrices, τέσσερα για συντεταγμένες και προσαρμογές TJ, τρία για χρώματα και ορθογώνια FDF. Ο έλεγχος για το v3.539.26 άγγιξε περισσότερα call sites απ' όσα υπονόησε η αρχική αναφορά bug:
- Τα
AddPageMatrix,ScalePageκαιDeskewPage, που όλα προσετέχουν έναcmμπροστά στο υπάρχον content της σελίδας - Οι builders στοιχείων σελίδας που εκπέμπουν επαναφορές
Tm, προωθήσειςTJκαι μετασχηματισμούςcm - Οι matrices τοποθέτησης glyph και τα outline σημεία στον converter text-to-path
- Το μαύρο box γεμίσματος που προσετέχει το
RedactRegion, οι τιμές/Rectστο FDF export και οι τελεστές που γράφει ο αναχρωματισμός
Το PLDoubleToStrConst είναι ένας χειροποίητος formatter και όχι wrapper γύρω από το FloatToStrF, και τρεις από τις ιδιότητές του έχουν σημασία εδώ. Γράφει πάντα τελεία και κόβει τα τελικά μηδενικά, οπότε το 0.5 μένει 0.5 και όχι 0.500000. Δεν γράφει ποτέ εκθέτη για πεπερασμένη είσοδο. Και μια μη μηδενική τιμή μικρότερη από τη ζητούμενη ακρίβεια κρατά τα σημαντικά της ψηφία αντί να καταρρεύσει σε μηδέν, οπότε το PLDoubleToStrConst(1E-9, 6) επιστρέφει 0.000000001· μόνο τιμές κάτω από περίπου 5E-16 γίνονται 0. Ο τελευταίος κανόνας υπάρχει επειδή η στρογγυλοποίηση ενός μικροσκοπικού συντελεστή κλίμακας σε μηδέν μετατρέπει μια έγκυρη matrix σε ενιαία, που είναι χειρότερο bug από αυτό που διορθώνεται
uses
PDFlibExtra;
procedure CheckNumberHelpers;
var
V: Double;
begin
// Έξοδος μηχανής: δεκαδικό τελείας, χωρίς εκθέτη, με κομμένα τα τελικά μηδενικά
Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235'); // κρατά 4 σημαντικά ψηφία
Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');
// Είσοδος μηχανής: ήπιο failure αντί για EConvertError
Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
Assert(not PLTryStrToFloatInvariant('0,5', V)); // οι αριθμοί content δεν βάζουν ποτέ κόμμα
end;
Γιατί η πλευρά του parsing είναι πιο επικίνδυνη από την πλευρά της εγγραφής;
Η πλευρά του parsing είναι πιο επικίνδυνη επειδή ένας parser δεμένος με το locale δεν παράγει λάθος αριθμό, πετάει exception. Το PLStrToFloat καλεί το StrToFloat, που πετάει EConvertError όταν το κείμενο δεν ταιριάζει τον διαχωριστικό του συστήματος. Σε σύστημα με κόμμα ως δεκαδικό αυτό σήμαινε ότι το RecolorPage ματαίωνε τη στιγμή που συναντούσε έναν συνηθισμένο τελεστή 0.5 g, οπότε αποτύγχανε κάθε σελίδα του πραγματικού κόσμου, όχι μόνο οι εξωτικές. Το RenderPageRegionToFile απέρριπτε το δικό του τεκμηριωμένο clip format "10.5,20.5,50.5,40.5", και τα attributes μήκους SVG, τα χρώματα του SVG export, οι λίστες κορυφών σχολίων και οι τιμές solidity του output intent είτε απορρίπτονταν είτε αντικαθίσταντο αθόρυβα από defaults. Μια βιβλιοθήκη που δουλεύει τέλεια στη μηχανή του developer και αποτυγχάνει στον πρώτο πελάτη στο Μόναχο είναι ακριβώς το είδος κώδικα που, όπως οι περιπτώσεις στο άρθρο για κώδικα Delphi που δουλεύει κατά λάθος, φαίνεται σωστός μόνο λόγω του πού τεστάρηκε
Το v3.539.33 ταξινόμησε κάθε κλήση StrToFloat και TryStrToFloat ανάλογα με το πού προέρχεται η είσοδός της. Οι τελεστέοι του content stream, τα attributes SVG, οι συμβολοσειρές χρωμάτων του painter και οι λίστες clip και κορυφών διαχωρισμένες με κόμμα έχουν όλες σταθερή σύνταξη τελείας, οπότε περνούν τώρα από το PLTryStrToFloatInvariant, που κόβει τα κενά του κειμένου, το κάνει parse με PLInvariantFormatSettings και επιστρέφει False για κενή, κακοσχηματισμένη ή μη πεπερασμένη είσοδο αντί να πετάει exception. Μια λίστα διαχωρισμένη με κόμμα δεν αφήνει περιθώριο συμβιβασμού, επειδή ένα κόμμα δεν μπορεί να είναι ταυτόχρονα delimiter της λίστας και δεκαδικό σημείο. Το ίδιο πέρασμα διόρθωσε επίσης ένα out-of-bounds write: το RenderPageRegionToFile αποθήκευε παλιά μια πέμπτη τιμή clip πέρα από τον buffer τεσσάρων στοιχείων του. Για τον αγωγό αναχρωματισμού που περιγράφεται στον οδηγό μετατροπής ενός PDF σε έναν color space, το πρακτικό αποτέλεσμα είναι ότι το RecolorPage και το RecolorDocument δεν ματαιώνουν πλέον σε σύστημα με κόμμα. Οι τιμές κανόνων που πληκτρολογεί ο caller στο CheckDocumentPolicy είναι η μία περίπτωση parsing που χρησιμοποιεί τον επιεική helper αντ' αυτού, για τον λόγο που εξηγεί η επόμενη ενότητα
Τι συμβαίνει αν διορθώσετε μόνο ένα άκρο ενός round trip;
Η διόρθωση μόνο ενός άκρου ενός locale round trip χαλάει κώδικα που δούλευε, γι' αυτό η αλλαγή των attributes δομής στο v3.539.32 μετακίνησε writer και reader μαζί. Τα wrappers SetStructElem* μεταφέρουν αριθμούς ως strings: το SetStructElemBBox μορφοποιεί τέσσερις τιμές σε ένα string, το αποθηκεύει μέσω AddTagAttribute, και ο writer /A κάνει αργότερα parse εκείνο το string για να αποφασίσει αν γίνεται αριθμός, πίνακας ή όνομα. Και τα δύο άκρα χρησιμοποιούσαν το locale του συστήματος, οπότε σε σύστημα με κόμμα το round trip ήταν αυτοσυνεπές. Το bug εμφανίστηκε μόνο όταν ένας caller ακολούθησε την τεκμηρίωση και πέρασε "0.5" στο AddTagAttribute: ο reader δεν μπορούσε να το κάνει parse και εξέπεμπε το PDF όνομα /0.5. Το placeholder του PDF/VCR είχε το καθρεφισμένο πρόβλημα, επειδή η βιβλιοθήκη παρήγαγε GTS_BBox με τελεία και μετά το επικύρωνε με το locale πριν την αποθήκευση
Η αλλαγή μόνο του writer σε τελεία θα ήταν χειρότερη από το να μην κάνετε τίποτα, αφού κάθε τιμή SetStructElem* θα αποτύγχανε τότε τον reader δεμένο με το locale και θα υποβαθμιζόταν σε όνομα. Οπότε οι writers τώρα χρησιμοποιούν PLDoubleToStrConst(v, 6), και ο reader χρησιμοποιεί το νέο PLTryStrToFloatLenient, που δοκιμάζει πρώτα τη μορφή τελείας και γυρίζει στο locale του συστήματος. Ένας caller με locale κόμματος που πέρναε "1,25" στο παρελθόν παίρνει ακόμα τον αριθμό 1.25. Το trade-off είναι επίτηδες και τεκμηριωμένο: σε γερμανικό σύστημα το "1.500" γινόταν παλιά όνομα επειδή το StrToFloat απορρίπτει διαχωριστικά χιλιάδων, και διαβάζεται πλέον ως 1.5, ενώ τα literal strings NAN και INF δεν γίνονται πλέον δεκτά ως αριθμοί
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
begin
FormatSettings.DecimalSeparator := ','; // caller με κόμμα ως δεκαδικό
Lib := TPDFlib.Create;
try
Lib.BeginTag('Figure', 'Sales chart', '');
Lib.SetStructElemBBox(10.5, 20.25, 200.5, 100.75); // /BBox [ 10.5 20.25 200.5 100.75 ]
Lib.AddTagAttribute('Layout', 'SpaceAfter', '0.5'); // /SpaceAfter 0.5, πριν ήταν /0.5
Lib.AddTagAttribute('Layout', 'StartIndent', '1,25'); // εξακολουθεί /StartIndent 1.25
Lib.DrawText(20, 20, 'figure');
Lib.EndTag;
Lib.SaveToFile('tagged.pdf');
finally
Lib.Free;
end;
end;
Πού σταματούν το NaN και το άπειρο
Τα AddPageMatrix, ScalePage και RedactRegion απορρίπτουν τώρα ορίσματα NaN και άπειρα εκ των προτέρων και επιστρέφουν 0, αφού κανένας αριθμός PDF δεν μπορεί να τα αναπαραστήσει. Το ScalePage απέρριπτε ήδη συντελεστές μηδέν ή μικρότερους, αλλά το NaN περνά ένα τεστ <= 0, οπότε μια κλίμακα NaN ταξίδευε ως τον formatter. Στο v3.539.26 ο formatter καλούσε ακόμα Round πάνω σε NaN, που πετάει EInvalidOp σε Win32 όπου η μονάδα x87 δεν αποκρύπτει άκυρες πράξεις· το v3.539.31 έκανε το PLDoubleToStrConst να γράφει 0 για NaN ως τελευταία γραμμή άμυνας, αλλά ένα μηδέν σε matrix είναι ενιαίος μετασχηματισμός, οπότε ο έλεγχος σε επίπεδο API παραμένει η πραγματική διόρθωση. Δύο όρια μένουν στη θέση τους επίτηδες. Τα strings κατάστασης metafile γράφονται και διαβάζονται με το locale μέσα σε μία διεργασία και δεν την αφήνουν ποτέ, οπότε αφέθηκαν ήσυχα. Και ένα τεστ που μορφοποιεί 1E-5 μέσω της διαδρομής στοιχείου σελίδας πρέπει να διαβάσει το content πριν ξαναγραφτεί το layer, επειδή η επανεκποπή τελεστών σε ακρίβεια εγγράφου μετατρέπει νόμιμα εκείνη την τιμή σε 0
Αν η εφαρμογή σας φτάνει σε πελάτες έξω από τον κόσμο της τελείας ως δεκαδικού, η πιο ασφαλής συνήθεια είναι αυτή που χρησιμοποιεί τώρα η σουίτα τεστ του PDFlibPas: τρέξτε τις διαδρομές που παράγουν PDF μία φορά με FormatSettings.DecimalSeparator σε κόμμα και σαρώστε την έξοδο για κόμματα και εκθέτες. Το άρθρο για τη διατήρηση της ακρίβειας των parsed δεκαδικών καλύπτει το άλλο μισό της ίδιας ιστορίας, πώς οι αριθμοί που διαβάζονται από υπάρχον αρχείο κρατούν το ακριβές τους κείμενο στην αποθήκευση. Downloads, η πλήρης αναφορά API και το trial build βρίσκονται στη σελίδα προϊόντος PDFlibPas Delphi PDF library