Το PDFlibPas αποκωδικοποιεί TIFF με χειρόγραφο parser σε Object Pascal αντί για binding της libtiff, και η έκδοση 3.534.1 όξυνε ακριβώς τα σημεία όπου αυτός ο parser απορρίπτει είσοδο. Το BigTIFF magic 43 απορρίπτεται πλέον με ρητή αναφορά στο όνομά του, τα TileOffsets και TileByteCounts απορρίπτονται κατά την ανάλυση των tags, και κάθε buffer υπολογίζεται με αριθμητική Int64 κάτω από ανώτατο όριο αποκωδικοποίησης 256 MiB
Το ελάττωμα που κλείνει εδώ δεν εμφανίζεται ποτέ σε εργαστήριο. Εμφανίζεται σε μια πύλη σάρωσης που τρέχει αθόρυβα επί τρία χρόνια, μέχρι ένας πελάτης να περάσει από εκεί ένα γεωχωρικό αρχείο ή μια ολόκληρη ιατρική εικόνα μεγέθους slide. Το αρχείο έχει νόμιμη επικεφαλίδα TIFF. Αναλύεται κανονικά. Αυτό που βγαίνει είναι μια σελίδα ριγέ θορύβου, ή μια κατανομή πολλών gigabyte που ρίχνει την υπηρεσία, και πουθενά στην πορεία κανείς δεν δήλωσε την είσοδο άκυρη. Αυτό είναι το σχήμα αποτυχίας που αξίζει μηχανικής αντιμετώπισης: όχι ένα crash, αλλά μια λάθος απάντηση που παραδίδεται με απόλυτη σιγουριά
Γιατί το II ή το MM δεν αποδεικνύει ότι έχετε κλασικό TIFF;
Επειδή ο δείκτης σειράς byte είναι κοινός και για τις δύο παραλλαγές. Το κλασικό TIFF και το BigTIFF ανοίγουν και τα δύο με II ή MM, και το πεδίο που πραγματικά τα ξεχωρίζει είναι το magic των 16 bit αμέσως μετά: 42 για το κλασικό TIFF όπως ορίζεται στην προδιαγραφή TIFF 6.0, 43 για το BigTIFF με τις μετατοπίσεις 64 bit. Ένας loader γραμμένος ως FValidTIFF := PopWord = 42 δεν κάνει λάθος για το κλασικό TIFF, αλλά συμπυκνώνει δύο εντελώς διαφορετικές απορρίψεις σε ένα σιωπηλό boolean, οπότε ένα BigTIFF δεν ξεχωρίζει πια από ένα κομμένο JPEG που μετονόμασε κάποιος. Το PDFlibPas διαχωρίζει πλέον τις περιπτώσεις και καταγράφει καθεμία στο TPDFTIFF.LastError: επικεφαλίδα μικρότερη από τέσσερα bytes, άκυρος δείκτης σειράς byte, magic 43, και κάθε άλλη τιμή magic παράγουν διακριτό κείμενο. Η βιβλιοθήκη εξακολουθεί να μην αποκωδικοποιεί BigTIFF, και η ειλικρινής δήλωση αυτού είναι το ζητούμενο. Ο καλών παίρνει τη διαφορά ανάμεσα στο «αυτό δεν είναι TIFF» και στο «αυτό είναι TIFF του οποίου τη διάταξη μετατοπίσεων 64 bit ο ενσωματωμένος αποκωδικοποιητής δεν υλοποιεί», που είναι η διαφορά ανάμεσα σε ένα ticket υποστήριξης που απαντάτε σε μία απάντηση και σε ένα που γίνεται εβδομάδα εικασιών
var
Tiff: TPDFTIFF;
Page: Integer;
begin
Tiff := TPDFTIFF.Create;
try
Tiff.LoadFromFile('inbox\scan-0417.tif');
if not Tiff.ValidTIFF then
raise Exception.Create('TIFF rejected: ' + Tiff.LastError);
if Tiff.PageCount < 1 then
raise Exception.Create('TIFF carries no decodable page');
for Page := 1 to Tiff.PageCount do
Writeln(Format('page %d: %dx%d, %d spp',
[Page,
Tiff.PageInfo[Page].Width,
Tiff.PageInfo[Page].Height,
Tiff.PageInfo[Page].SamplesPerPixel]));
finally
Tiff.Free;
end;
end;
Τα tiles είναι διαφορετική γεωμετρία, όχι άλλος ένας πίνακας offsets
Το PDFlibPas απορρίπτει tiled TIFF κατά την ανάλυση των tags, πριν αγγίξει οποιαδήποτε δεδομένα pixel. Η συντόμευση που καλεί το bug φαίνεται εύκολα: το tag 324 (TileOffsets) και το tag 325 (TileByteCounts) είναι πίνακες μετατοπίσεων αρχείου και μετρήσεων byte, δομικά πανομοιότυποι με τους πίνακες strips, οπότε το να δείξεις τα υπάρχοντα πεδία strips σε αυτούς κοστίζει δύο γραμμές και μεταγλωττίζεται καθαρά. Είναι όμως λάθος. Τα tiles σχηματίζουν δισδιάστατο πλέγμα με γεμισμένα ακραία μπλοκ, δικό τους stride ανά γραμμή μέσα σε κάθε tile, και καμία σημασιολογία RowsPerStrip, όπως ξεκαθαρίζει η ενότητα tiled-image της TIFF 6.0. Η τροφοδότηση payloads tiles σε αποκωδικοποιητή strips συνεπώς δεν αποτυγχάνει δυνατά. Τα SimpleExtract και CompDecode διατρέχουν τα δεδομένα με λάθος stride και βγάζουν εικόνα με σωστές διαστάσεις και λάθος pixels. Ο παλιότερος κώδικας χειροτέρευε το θέμα κρατώντας StripsAreTiles, ColumnsPerTile και RowsPerTile στο TTIFFPage: γεωμετρία tiles καταγεγραμμένη από αποκωδικοποιητή χωρίς assembler tiles πίσω του. Στην 3.534.1 οι χειριστές των tags 324 και 325 ενεργοποιούν το σφάλμα tiles και εγκαταλείπουν αμέσως το IFD, ώστε η άρνηση κουβαλά τη λέξη «tiled» αντί να αναδύεται εβδομάδες αργότερα ως παράπονο rendering
Ένας περιορισμός διάστασης δεν είναι προϋπολογισμός μνήμης
Ο περιορισμός πλάτους και ύψους στα 65.535 το καθένα είναι απαραίτητος και καθόλου επαρκής, γιατί το μέγεθος που καθορίζει την κατανομή είναι ένα γινόμενο. Το RowsPerStrip * Width * SamplesPerPixel μπορεί να ξεχειλίσει την αριθμητική 32 bit πολύ πριν η κάθε πλευρά φτάσει το δικό της όριο, και ακόμα και χωρίς υπερχείλιση μπορεί να ονομάσει μια κατανομή που καμία υπηρεσία δεν πρέπει να επιχειρήσει. Το PDFlibPas υπολογίζει τα row bytes σε Int64 και επιβάλλει τρία ταυτόχρονα ανώτατα όρια: 65.535 ανά διάσταση, 32 χρωματικά στοιχεία, και 256 MiB αποκωδικοποιημένων bytes
const
PDFLIB_TIFF_MAX_IMAGE_DIM = 65535;
PDFLIB_TIFF_MAX_COLOR_COMPONENTS = 32;
PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES = 256 * 1024 * 1024;
// μέσα στο TPDFTIFF.ValidatePageForDecode
BitsPerPixel := Int64(P.BitsPerSample) * P.SamplesPerPixel;
RowBytes := (Int64(P.Width) * BitsPerPixel + 7) div 8;
if (RowBytes < 1) or
(RowBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
(Int64(P.Height) > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES div RowBytes) then
Exit(False);
DecodedBytes := RowBytes * P.RowsPerStrip;
if (DecodedBytes < 1) or
(DecodedBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
(DecodedBytes > MaxInt) then
Exit(False);
Τρεις λεπτομέρειες εκεί μετράνε περισσότερο από τις σταθερές. Το τεστ ύψους είναι γραμμένο ως διαίρεση αντί για πολλαπλασιασμό, ώστε το υπερμεγέθες γινόμενο δεν σχηματίζεται ποτέ. Ένα RowsPerStrip κάτω από 1 ή πάνω από το ύψος της εικόνας κανονικοποιείται πρώτα στο ύψος, κάτι που η ανάγνωση single-strip της TIFF 6.0 ήδη συνεπάγεται και που εμποδίζει ένα εχθρικό tag να φουσκώσει το strip buffer. Και η ρουτίνα είναι κοινή: η ValidatePageForDecode τρέχει στο τέλος της ανάλυσης των tags και ξανά στην είσοδο τόσο του SimpleExtract όσο και του CompDecode, ώστε κώδικας που φτάνει απευθείας σε αποκωδικοποιητή δεν μπορεί να παρακάμψει τον προϋπολογισμό. Είναι ο ίδιος κανόνας που ακολουθεί το PDFlibPas όταν αναλύει μη αξιόπιστα γραφήματα αντικειμένων PDF, γιατί ένα όριο που επιβάλλεται σε μία από τις τρεις πόρτες δεν είναι όριο
Τι πρέπει να ελέγξει ο καλών πριν διαβάσει το PageInfo;
Έλεγξε πρώτα το ValidTIFF, μετά το PageCount, και μόνο τότε προσπέλασε το PageInfo. Ένα απορριφθέν αρχείο μπορεί να αφήσει το PageCount στο μηδέν, και η GetPageInfo απαντά σε δείκτη εκτός εύρους με ένα μη αρχικοποιημένο record TTIFFPage, οπότε ένα μονοπάτι σφάλματος που διαβάζει ανάλυση ή πλήθος δειγμάτων στο δρόμο του προς την αναφορά της αποτυχίας καταλήγει να διαβάζει θόρυβο. Η έκδοση 3.534.1 διόρθωσε και τους δύο καλούντες μέσα στη βιβλιοθήκη: το μονοπάτι εισαγωγής εικόνας διαβάζει XRes και YRes μόνο μέσα στον έγκυρο κλάδο, και η TPDFlib.GetImagePageCount απαιτεί ValidTIFF αντί να εμπιστεύεται από μόνη της ένα μη μηδενικό πλήθος σελίδων. Στη συνέχεια, το όρισμα Options της AddImageFromFile είναι ο αριθμός σελίδας με βάση το 1 για ένα multipage TIFF, οπότε η GetImagePageCount πρέπει να είναι αξιόπιστη πριν ξεκινήσει ο βρόχος και όχι μετά. Το μηδενικό πλήθος σελίδων είναι πλέον μια πραγματική απάντηση που σημαίνει «εδώ δεν αποκωδικοποιείται τίποτα», όχι ατύχημα μιας πρόωρης επιστροφής, κάτι που μετράει περισσότερο όταν συντάσσεις και εναλλάσσεις παρτίδες σάρωσης duplex και ένα φύλλο που αποκωδικοποιήθηκε σιωπηλά λάθος θα κατέληγε στη λάθος θέση
var
Pdf: TPDFlib;
Pages, I, ImageID: Integer;
begin
Pdf := TPDFlib.Create;
try
Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
if Pages < 1 then
Exit; // κακή επικεφαλίδα, BigTIFF, tiled διάταξη ή πάνω από τον προϋπολογισμό
Pdf.NewDocument;
for I := 1 to Pages do
begin
Pdf.NewPage;
ImageID := Pdf.AddImageFromFile('inbox\scan-0417.tif', I);
if ImageID > 0 then
begin
Pdf.SelectImage(ImageID);
Pdf.DrawImage(0, 0, 595, 842);
end;
end;
Pdf.SaveToFile('scan-0417.pdf');
finally
Pdf.Free;
end;
end;
Να χτίσεις τον αποκωδικοποιητή ή να συνδέσεις τη libtiff;
Το PDFlibPas κρατά τον ενσωματωμένο αποκωδικοποιητή, και ο αποφασιστικός παράγοντας είναι η εμβέλεια πλατφορμών και όχι η πατρότητα του κώδικα. Περίπου 1.873 γραμμές Object Pascal μεταγλωττίζονται όπου πάει ο compiler: Win32, Win64, macOS, iOS, Android, και FPC σε Linux. Η libtiff 4.7.1 είναι γύρω στις 30.000 γραμμές C μοιρασμένες σε 34 translation units tif_*.c, και τα προμεταγλωττισμένα object files που υπάρχουν σήμερα καλύπτουν μόνο Windows. Η υιοθέτησή της θα αντάλλασσε πλήρη κάλυψη TIFF με μια λίστα υποστηριζόμενων πλατφορμών που στενεύει σε όποια μηχανή τρέχει το C toolchain, συν ένα πέρασμα linker που κανείς δεν έχει περπατήσει ακόμα
Αξίζει να δηλωθεί τι κοστίζει αυτό, χωρίς στρογγυλοποιήσεις. Ο ενσωματωμένος αποκωδικοποιητής χειρίζεται ό,τι παράγει πραγματικά η δουλειά με σαρωμένα έγγραφα: CCITT Group 3 μονοδιάστατο και δισδιάστατο, Group 4, LZW, Deflate, PackBits και JPEG-in-TIFF, σε photometrics WhiteIsZero, BlackIsZero, RGB, palette και CMYK με Predictor 1 και 2. Αυτά τα payloads ταιριάζουν με τα φίλτρα PDF στο ISO 32000-1 §7.4.4 και §7.4.6, εξ ου και το TIFF front-end ζυγίζει τόσο σε μια γραμμή σάρωσης. Αυτό που δεν χειρίζεται είναι BigTIFF, tiles, floating-point Predictor 3, PixarLog και SGILog, παλιού τύπου JPEG compression 6, και πυραμίδες sub-IFD. Από την 3.534.1 καθεμία από αυτές τις περιπτώσεις είναι μια κατονομαζόμενη άρνηση αντί για λάθος εικόνα, και η βιβλιοθήκη κρατά γραπτή λίστα εναυσμάτων για το άνοιγμα ξανά της απόφασης libtiff:
- ένας πελάτης αναφέρει αρχείο BigTIFF και χρειάζεται εγγενή υποστήριξη αντί για βήμα μετατροπής
- ένας πελάτης αναφέρει tiled TIFF από ιατρικές, GIS ή βιομηχανικές πηγές και χρειάζεται αποκωδικοποίηση επιτόπου
- ένας πελάτης αναφέρει floating-point Predictor 3 TIFF
- δημοσιευμένη ευπάθεια χτυπά τις ενσωματωμένες διαδρομές αποκωδικοποίησης CCITT ή LZW
- το επιχείρημα του cross-platform πάψει να ισχύει, είτε επειδή η υποστήριξη macOS, iOS και Android αποσύρεται, είτε επειδή μια επαναχρησιμοποιήσιμη ενσωμάτωση libtiff καλύπτει ήδη macOS και Linux
Η ίδια η μετάβαση είναι περιγραφόμενη και όχι υποθετική: ένα conditional USE_LIBTIFF θα κρατούσε τη δημόσια επιφάνεια του TPDFTIFF ανέπαφη, θα δρομολογούσε το LoadFromStream μέσω TIFFClientOpen με stream callbacks, και θα άφηνε τον parser Pascal ως fallback σε μη Windows. Μέχρι να πυροδοτηθεί πραγματικά ένα από αυτά τα εναύσματα, η συντήρηση δύο αποκωδικοποιητών και ενός διπλασιασμένου πίνακα τεστ δεν αγοράζει τίποτα που να νιώθει ο πελάτης. Η αναβολή ενός κόστους με τη διαδρομή διαφυγής ήδη γραμμένη είναι διαφορετικό πράγμα από την αγνόησή του
Πού αφήνει αυτό μια γραμμή επεξεργασίας σαρωμένων εγγράφων
Μεχειρίσου το TPDFTIFF ως πύλη και όχι ως μετατροπέα. Φόρτωσε το αρχείο, διάβασε το ValidTIFF, και κατέγραψε το LastError αυτούσιο κάθε φορά που είναι false, γιατί αυτό το string είναι πλέον η συντομότερη διαδρομή από μια αναφορά πεδίου σε μια διάγνωση. Τα αρχεία που αποτυγχάνουν στην πύλη παραμένουν ανακτήσιμα μετατρέποντάς τα ανάντη, που είναι η πρακτική απάντηση για πηγές BigTIFF και tiled σήμερα. Για είσοδο εντελώς εκτός TIFF, το PDFlibPas παίρνει ξεχωριστή διαδρομή μέσω του μονοπατιού εισαγωγής εικόνων AVIF, HEIF και JPEG XL, ώστε το ερώτημα ποιος αποκωδικοποιητής κατέχει ποια μορφή να μένει ρητό και όχι να προκύπτει
Όλα αυτά κάθονται πίσω από το συνηθισμένο image API, οπότε μια γραμμή εγγράφων κερδίζει το στενότερο όριο χωρίς να αλλάξει ούτε μια γραμμή καλούντος κώδικα πέρα από τον έλεγχο του πλήθους σελίδων που όφειλε ήδη να ελέγχει. Αν ζυγίζεις εγγενή διαδρομή TIFF προς PDF για Delphi ή C++Builder, το πλήρες component και ο χειρισμός εικόνων του τεκμηριώνονται στη σελίδα PDF Library for Delphi