Το HotPDF επιπεδώνει την περιστροφή σελίδων PDF με το THotPDF.FlattenLoadedPageRotation: η μέθοδος τυλίγει το content κάθε περιστραμμένης σελίδας σε έναν δεξιόστροφο μετασχηματισμό cm, ξαναγράφει κάθε page box που έχει πράγματι η σελίδα, γυρνά τη γεωμετρία σχολίων, τις matrices appearances, τους ρητούς προορισμούς και τη γεωμετρία της tagged δομής κατά την ίδια γωνία, και μετά θέτει το /Rotate σε 0. Η σελίδα φαίνεται πανομοιότυπη σε έναν viewer, αλλά το σύστημα συντεταγμένων της είναι πια όρθιο. Αυτό μετρά τη στιγμή που ένα downstream εργαλείο, ένα print RIP ή ο δικός σας κώδικας σφραγίδων αγνοεί το /Rotate και τοποθετεί πράγματα σε ακατέργαστο user space
Η τυπική αφορμή είναι ένας scanner ή μια mobile capture εφαρμογή που γράφει landscape σελίδες ως portrait media με /Rotate 90. Κάθε viewer τις δείχνει σωστά, οπότε κανείς δεν προσέχει μέχρι κάποιος να σφραγίσει αριθμό σελίδας στο «κάτω δεξιά» και να προσγειωθεί πλαγίως κατά μήκος της αριστερής ακμής, ή ένα βήμα imposition που διαβάζει μόνο το /MediaBox να στήσει υποδοχή portrait για landscape σελίδα. Η επιπέδωση ακούγεται σαν δουλειά matrix μιας γραμμής. Στην πράξη αγγίζει πέντε page boxes, τρία είδη γεωμετρίας σχολίων, τους στόχους συνδέσμων του εγγράφου και το δέντρο δομής, και το καθένα έχει τον δικό του κανόνα στο ISO 32000-1
Προς ποια μεριά γυρνά το /Rotate μια σελίδα PDF;
Το /Rotate γυρνά τη σελίδα δεξιόστροφα για απεικόνιση και εκτύπωση, σε πολλαπλάσια των 90 μοιρών (ISO 32000-1 §7.7.3.3, Table 30). Στις 90 μοίρες η αριστερή ακμή του media γίνεται η πάνω και η πάνω ακμή γίνεται η δεξιά πλευρά, οπότε σε y-down device space η αντιστοίχιση είναι X = (y - Bottom) * Scale και Y = (x - Left) * Scale. Στις 270 μοίρες η δεξιά ακμή γίνεται η πάνω. Το /Rotate είναι επίσης ένα από τα μόλις τέσσερα κληρονομήσιμα attributes σελίδας, μαζί με /Resources, /MediaBox και /CropBox (§7.7.3.4), οπότε ένα dictionary σελίδας χωρίς δικό του /Rotate μπορεί ακόμα να γυρίσει από έναν πρόγονο /Pages. Το THotPDF.GetLoadedPageRotation περπατά την αλυσίδα /Parent και κανονικοποιεί το αποτέλεσμα σε 0–359, που είναι η τιμή που θέλετε, όχι το ακατέργαστο key στη σελίδα
Η κατεύθυνση είναι εύκολο να πάει λάθος με τρόπο που επιβιώνει των τεστ, και παλαιότερα builds του HotPDF έκαναν ακριβώς αυτό. Η παλιά matrix σελίδας-σε-συσκευή αντάλλασσε τα y components για 90 και 270, που παράγει ανάκλαση στη διαγώνιο αντί για περιστροφή: ο προσανατολισμός της matrix αναποδογυρίζει σχετικά με την άστριφτη περίπτωση. Και οι δύο γωνίες ακόμα «φαίνονται γυρισμένες», το bitmap έχει την ανταλλαγμένη πλάτος και ύψος, και ένα round trip από σελίδα σε προβολή και πίσω επιστρέφει το σημείο εκκίνησης, οπότε οι έλεγχοι διαστάσεων και τα round-trip τεστ περνούν όλα. Ο μόνος αξιόπιστος έλεγχος είναι πού καταλήγει ένας δείκτης γωνίας, συγκρινόμενος pixel προς pixel με έναν reference renderer. Επειδή το μοντέλο viewer, το SIMD render backend και η αντιστοίχιση highlight είχαν αντιγράψει την ίδια matrix, όλα διορθώθηκαν μαζί, και ο κώδικας επιπέδωσης τώρα χρησιμοποιεί την ίδια δεξιόστροφη σύμβαση με τον renderer
Πώς ξαναγράφει το FlattenLoadedPageRotation μια σελίδα
Το FlattenLoadedPageRotation(PageRange, Info) επεξεργάζεται κάθε σελίδα στο PageRange της οποίας η ενεργή περιστροφή είναι 90, 180 ή 270, και επιστρέφει τον αριθμό των σελίδων που επεπέδωσε. Κενό PageRange σημαίνει όλες τις σελίδες· αλλιώς το string χρησιμοποιεί τη συνήθη σύνταξη one-based '1-3,7', και αριθμός σελίδας εκτός εύρους πετάει exception αντί να παραλειφθεί. Τα πρωτότυπα content streams δεν ξανακωδικοποιούνται ποτέ. Η μέθοδος προσετέχει ένα νέο stream που περιέχει q 0 -1 1 0 -Bottom Width+Left cm (για 90 μοίρες) στο /Contents της σελίδας, προσθέτει στο τέλος ένα stream που περιέχει Q, και τέλος γράφει ρητό /Rotate 0 στο dictionary της σελίδας ώστε κληρονομημένη τιμή πάνω σε κόμβο /Pages να μη γυρίσει τη σελίδα δεύτερη φορά
var
Pdf: THotPDF;
Info: THPDFRotationFlattenInfo;
Flattened: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('scanned-batch.pdf') > 0 then
begin
// '' = κάθε σελίδα· σελίδες στις 0 μοίρες σαρώνονται αλλά αφήνονται ήσυχες
Flattened := Pdf.FlattenLoadedPageRotation('', Info);
Writeln(Format('Scanned %d, flattened %d pages', [Info.ScannedPageCount, Info.FlattenedPageCount]));
Writeln(Format('Turned %d annotations, %d destinations, %d tagged geometry entries',
[Info.TransformedAnnotationCount, Info.TransformedDestinationCount,
Info.TransformedStructureGeometryCount]));
if Flattened > 0 then
Pdf.SaveLoadedDocument('scanned-batch-upright.pdf');
end;
finally
Pdf.Free;
end;
end;
Η εγγραφή THPDFRotationFlattenInfo αξίζει να μπαίνει στο log παρά να πετιέται. Το ScannedPageCount είναι το μέγεθος του εύρους, το FlattenedPageCount ισούται με την τιμή επιστροφής, και οι τρεις μετρητές Transformed... σας λένε αν το έγγραφο είχε συνδέσμους, σελιδοδείκτες ή tagged γεωμετρία που δείχνει στις γυρισμένες σελίδες. Ένα batch όπου κάθε αρχείο αναφέρει μηδέν προορισμούς είναι εντάξει· ένα tagged αρχείο PDF/UA που αναφέρει μηδέν γεωμετρία δομής όταν περιμένατε bounding boxes σχημάτων είναι σήμα να το εξετάσετε στο χέρι
Ποια page boxes ξαναγράφει η επιπέδωση, και με ποια σειρά;
Η επιπέδωση ξαναγράφει μόνο τα boxes που έχει ήδη η σελίδα, και διαβάζει κάθε box πριν γράψει οποιοδήποτε. Η σειρά μετράει λόγω της αλυσίδας default: το GetLoadedPageBox(PageIndex, pbCropBox, ...) επιστρέφει το /MediaBox όταν η σελίδα δεν έχει /CropBox, και τα /BleedBox, /TrimBox και /ArtBox έχουν default το CropBox (§14.11.2). Μια προγενέστερη έκδοση έκανε όντως read, transform και write ένα box τη φορά. Ξαναέγραψε πρώτα το MediaBox, μετά διάβασε το «CropBox», πήρε πίσω το ήδη γυρισμένο MediaBox, το γύρισε δεύτερη φορά και έγραψε ένα CropBox που η σελίδα δεν είχε ποτέ, που έκοβε μια landscape σελίδα σε τετράγωνο. Οι κανόνες κληρονομιάς χωρίζονται με τον ίδιο τρόπο: MediaBox και CropBox ψάχνονται κατά μήκος της αλυσίδας /Parent, ενώ Bleed, Trim και ArtBox μετρούν μόνο αν κάθονται στο ίδιο το dictionary της σελίδας, οπότε ένα πεταμένο /TrimBox πάνω σε κόμβο /Pages μεταχειρίζεται ως απόν και δεν αντιγράφεται ποτέ στη σελίδα
procedure DumpPageGeometry(Pdf: THotPDF; PageIndex: Integer);
var
L, B, R, T: Single;
begin
Writeln('Effective /Rotate: ', Pdf.GetLoadedPageRotation(PageIndex));
if Pdf.GetLoadedPageBox(PageIndex, pbMediaBox, L, B, R, T) then
Writeln(Format('MediaBox [%g %g %g %g]', [L, B, R, T]));
// True ακόμα και χωρίς key /TrimBox: η τιμή γυρνά στο CropBox, μετά στο MediaBox
if Pdf.GetLoadedPageBox(PageIndex, pbTrimBox, L, B, R, T) then
Writeln(Format('TrimBox [%g %g %g %g]', [L, B, R, T]));
// Προεπιλογή Letter· το GetLoadedPageVisibleBox αφήνει τις εξόδους ανέγγιχτες σε αποτυχία
L := 0; B := 0; R := 612; T := 792;
Pdf.GetLoadedPageVisibleBox(PageIndex, L, B, R, T);
Writeln(Format('Visible [%g %g %g %g]', [L, B, R, T]));
end;
Τρέξτε τον helper πριν και μετά την επιπέδωση και οι αριθμοί εξηγούνται μόνοι τους. Για σελίδα 90 μοιρών με MediaBox [0 0 612 792], το επιπεδωμένο MediaBox γίνεται [0 0 792 612]· κάθε ξαναγραμμένο box χαρτογραφείται μέσω της ίδιας δεξιόστροφης στροφής, σχετικά με την αρχή του πρωτότυπου MediaBox, οπότε το νέο MediaBox πάντα ξεκινά από την αρχή και τα άλλα boxes κρατούν τη θέση τους μέσα του. Το GetLoadedPageVisibleBox επιστρέφει αυτό που δείχνουν οι viewers και τυπώνουν οι εκτυπωτές, το CropBox κομμένο στο MediaBox και κανονικοποιημένο ώστε το Left να είναι μικρότερο του Right, και ο renderer του HotPDF, το SVG export, ο viewer και η διαδρομή εκτύπωσης χρησιμοποιούν όλοι εκείνο το ίδιο box. Όταν θέλετε το μέγεθος σελίδας που βλέπει ένας άνθρωπος, καλέστε το GetLoadedPageVisibleBox αντί να διαβάσετε το /MediaBox
Γιατί χαλάγουν τα σχόλια όταν γυρίζετε μόνο το /Rect;
Τα σχόλια χαλάγουν επειδή ένα appearance stream δεν σχεδιάζεται κατευθείαν στο /Rect. Κατά το §12.5.5 ο viewer πρώτα μετασχηματίζει το /BBox της φόρμας με το /Matrix της, μετά κλιμακώνει και μεταφράζει το bounding box εκείνου του αποτελέσματος στο /Rect. Γυρίστε μόνο το /Rect και μια σφραγίδα 200 × 40 συμπιέζεται σε υποδοχή 40 × 200, αδιαβάστη και στο πλάι. Το FlattenLoadedPageRotation λοιπόν δεξιοπολλαπλασιάζει τη δεξιόστροφη στροφή της σελίδας πάνω σε κάθε /Matrix appearance (για 90 μοίρες, [0 -1 1 0 0 0] στη σύμβαση row-vector), στα appearances /N, /R και /D και σε κάθε κατάσταση μέσα τους. Ένα appearance stream μπορεί να μοιράζεται από αρκετά σχόλια ή καταστάσεις, οπότε κάθε stream γυρνά ακριβώς μία φορά ανά κλήση. Η μία περίπτωση χωρίς καθαρή απάντηση είναι ένα stream που μοιράζεται ανάμεσα σε σελίδες με διαφορετικές περιστροφές· ακολουθεί την πρώτη σελίδα που το φτάνει
Δύο ακόμα κανόνες κρατούν form fields και σημειώσεις στη θέση τους. Η εγγραφή /MK /R ενός widget (§12.5.6.19) είναι αριστερόστροφη γωνία, οπότε η δεξιόστροφη γωνία της σελίδας αφαιρείται από αυτήν, modulo 360· αν το παραλείψετε, η επόμενη αναγέννηση appearance σχεδιάζει το κείμενο του πεδίου προς λάθος κατεύθυνση. Σχόλια με τη σημαία NoRotate (θέση bit 5, τιμή 16, §12.5.3) μένουν όρθια σε γυρισμένη σελίδα και περιστρέφονται γύρω από την πάνω αριστερή γωνία του /Rect τους, οπότε η επιπέδωση κρατά πλάτος, ύψος και όρθια εμφάνισή τους και μόνο μετακινεί εκείνη τη γωνία εκεί που τη βάζει η στροφή. Πέρα από τα σχόλια, η μέθοδος γυρνά επίσης /QuadPoints, /Vertices, /L και /InkList, ξαναγράφει ρητούς προορισμούς που ονομάζουν τη σελίδα (σημεία /XYZ, ορθογώνια /FitR, και /FitH / /FitV ανταλλαγμένα στις 90 και 270 μοίρες, §12.3.2.2), και μετασχηματίζει tagged γεωμετρία όπως εγγραφές attribute /BBox για δομικά στοιχεία του οποίου το /Pg είναι η σελίδα
Τι δεν καλύπτει η επιπέδωση;
Η επιπέδωση είναι μια γεωμετρική ξαναγραφή των δικών της αντικειμένων μιας σελίδας, και αρκετές καταστάσεις μένουν έξω από αυτήν αθόρυβα και όχι δυνατά
- Σελίδες με ενεργή περιστροφή ήδη 0, ή με MediaBox που λείπει ή έχει μηδενική πλάτος ή ύψος, παραλείπονται χωρίς error· συγκρίνετε την τιμή επιστροφής με τον αριθμό σελίδων που περιμένατε να αλλάξουν
- Τα Form XObjects που αναφέρονται από τους πόρους της σελίδας κρατούν το δικό τους
/BBoxσε form space, επειδή ο εξωτερικόςcmτα γυρίζει ήδη· η σάρωση του δέντρου δομής ακολουθεί μόνο/Kκαι/Aοπότε δεν μπαίνει ποτέ δεύτερη φορά στους πόρους ή στα σχόλια της σελίδας - Οι προορισμοί βρίσκονται σαρώνοντας κάθε έμμεσο object μία φορά ανά εππεδωμένη σελίδα, οπότε ένα μεγάλο έγγραφο με εκατοντάδες γυρισμένες σελίδες πληρώνει εκείνο το πέρασμα για καθεμία
- Ο renderer σελίδων του HotPDF δεν σχεδιάζει σχόλια, οπότε ένας οπτικός έλεγχος γυρισμένων σφραγίδων θέλει πρώτα
FlattenLoadedAnnotations
// Ψήστε τις appearances στο content ώστε ο renderer να τις δείχνει,
// μετά αποδώστε τη σελίδα 1 πριν και μετά την αφαίρεση του /Rotate
Pdf.FlattenLoadedAnnotations('1');
Before := Pdf.RenderLoadedPageToBitmap(0, 96);
try
Pdf.FlattenLoadedPageRotation('1', Info);
After := Pdf.RenderLoadedPageToBitmap(0, 96);
try
Assert((Before.Width = After.Width) and (Before.Height = After.Height));
// Συγκρίνετε εδώ τα pixels του δείκτη γωνίας, όχι μόνο τις διαστάσεις
finally
After.Free;
end;
finally
Before.Free;
end;
Για βαθύτερο υπόβαθρο, η πλευρά των σχολίων της ιστορίας συνεχίζεται στο σύνθεση appearances σχολίων πριν την επιπέδωσή τους, ο renderer πίσω από τη σύγκριση πριν-μετά καλύπτεται στο απόδοση φορτωμένης σελίδας PDF σε bitmap, και η αποκάλυψη και N-up stitching σε φορτωμένα PDF δείχνει την ίδια τεχνική προσάρτησης content stream πάνω στην οποία βασίζονται το prefix και το suffix της περιστροφής. Το HotPDF, συμπεριλαμβανομένου του FlattenLoadedPageRotation και των readers page-box, είναι διαθέσιμο για Delphi και C++Builder στη σελίδα HotPDF Delphi PDF component