HotPDF aplatit la rotation des pages PDF avec THotPDF.FlattenLoadedPageRotation : la méthode enveloppe le contenu de chaque page tournée dans une transformation cm horaire, réécrit chaque page box que la page possède réellement, tourne la géométrie des annotations, les matrices d'apparence, les destinations explicites et la géométrie de structure étiquetée du même angle, puis pose /Rotate à 0. La page se voit identique dans un lecteur, mais son système de coordonnées est maintenant droit. Ça compte dès qu'un outil en aval, un RIP d'impression ou votre propre code de tamponnage ignore /Rotate et place les choses dans l'espace utilisateur brut
Le déclencheur typique est un scanner ou une app de capture mobile qui écrit des pages paysage comme des médias portrait avec /Rotate 90. Tous les lecteurs les montrent correctement, donc personne ne remarque rien jusqu'à ce que quelqu'un tamponne un numéro de page « en bas à droite » et le voie atterrir de travers le long du bord gauche, ou qu'une étape d'imposition qui ne lit que /MediaBox dispose un emplacement portrait pour une page paysage. Aplatir sonne comme un job de matrice d'une ligne. En pratique ça touche cinq page boxes, trois sortes de géométrie d'annotation, les cibles de liens du document et l'arbre de structure, et chacun a sa propre règle dans ISO 32000-1
Dans quel sens /Rotate tourne-t-il une page PDF ?
/Rotate tourne la page dans le sens horaire pour l'affichage et l'impression, par multiples de 90 degrés (ISO 32000-1 §7.7.3.3, Table 30). À 90 degrés, le bord gauche du média devient le haut et le bord haut devient le côté droit, donc dans un espace périphérique y vers le bas le mapping est X = (y - Bottom) * Scale et Y = (x - Left) * Scale. À 270 degrés, le bord droit devient le haut. /Rotate est aussi l'une des quatre seules attributs de page héritables, avec /Resources, /MediaBox et /CropBox (§7.7.3.4), donc un dictionnaire de page sans /Rotate propre peut quand même être tourné par un ancêtre /Pages. THotPDF.GetLoadedPageRotation remonte la chaîne /Parent et normalise le résultat en 0–359, ce qui est la valeur que vous voulez, pas la clé brute de la page
Le sens est facile à rater d'une manière qui survit aux tests, et des builds HotPDF antérieurs ont fait exactement ça. L'ancienne matrice page-vers-périphérique échangeait les composantes y pour 90 et 270, ce qui produit une réflexion à travers la diagonale au lieu d'une rotation : l'orientation de la matrice bascule par rapport au cas non tourné. Les deux angles « ont l'air tournés » quand même, la bitmap a la largeur et la hauteur échangées, et un aller-retour de la page vers la vue et retour ramène au point de départ, donc les contrôles de dimensions et les tests d'aller-retour passent tous. Le seul contrôle fiable est l'endroit où finit un marqueur de coin, comparé pixel par pixel contre un renderer de référence. Comme le modèle du lecteur, le backend de rendu SIMD et le mapping des surlignages avaient copié la même matrice, tous ont été corrigés ensemble, et le code d'aplatissement emploie maintenant la même convention horaire que le renderer
Comment FlattenLoadedPageRotation réécrit une page
FlattenLoadedPageRotation(PageRange, Info) traite chaque page de PageRange dont la rotation effective est 90, 180 ou 270, et renvoie le nombre de pages qu'elle a aplaties. Un PageRange vide signifie toutes les pages ; sinon la chaîne emploie la syntaxe habituelle base 1 '1-3,7', et un numéro de page hors bornes lève une exception au lieu d'être sauté. Les flux de contenu d'origine ne sont jamais réencodés. La méthode préfixe un nouveau flux contenant q 0 -1 1 0 -Bottom Width+Left cm (pour 90 degrés) aux /Contents de la page, ajoute en suffixe un flux contenant Q, et finit par écrire un /Rotate 0 explicite dans le dictionnaire de page pour qu'une valeur héritée d'un nœud /Pages ne puisse pas tourner la page une seconde fois
var
Pdf: THotPDF;
Info: THPDFRotationFlattenInfo;
Flattened: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('scanned-batch.pdf') > 0 then
begin
// '' = toutes les pages ; les pages à 0 degré sont scannées mais laissées tranquilles
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;
Le record THPDFRotationFlattenInfo mérite d'être journalisé plutôt que jeté. ScannedPageCount est la taille de la plage, FlattenedPageCount égale la valeur de retour, et les trois compteurs Transformed... vous disent si le document avait des liens, des marque-pages ou de la géométrie étiquetée pointant vers les pages tournées. Un lot où chaque fichier rapporte zéro destination, c'est très bien ; un fichier PDF/UA étiqueté qui rapporte zéro géométrie de structure alors que vous attendiez des bounding boxes de figures est un signal pour l'inspecter à la main
Quelles page boxes l'aplatissement réécrit-il, et dans quel ordre ?
L'aplatissement ne réécrit que les boxes que la page possède déjà, et il lit chaque box avant d'en écrire une seule. L'ordre compte à cause de la chaîne de défauts : GetLoadedPageBox(PageIndex, pbCropBox, ...) renvoie la /MediaBox quand la page n'a pas de /CropBox, et /BleedBox, /TrimBox et /ArtBox valent par défaut le CropBox (§14.11.2). Une version antérieure lisait, transformait et écrivait une box à la fois. Elle réécrivait d'abord la MediaBox, puis lisait la « CropBox », récupérait la MediaBox déjà tournée, la tournait une seconde fois et écrivait une CropBox que la page n'a jamais eue, ce qui recadrait une page paysage en carré. Les règles d'héritage se coupent de la même façon : MediaBox et CropBox se cherchent le long de la chaîne /Parent, tandis que Bleed, Trim et ArtBox ne comptent que s'ils logent sur le dictionnaire de page lui-même, donc un /TrimBox égaré sur un nœud /Pages est traité comme absent et jamais copié sur la page
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]));
// Vrai même sans clé /TrimBox : la valeur retombe sur CropBox, puis MediaBox
if Pdf.GetLoadedPageBox(PageIndex, pbTrimBox, L, B, R, T) then
Writeln(Format('TrimBox [%g %g %g %g]', [L, B, R, T]));
// Letter préchargé ; GetLoadedPageVisibleBox laisse les sorties intactes en cas d'échec
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;
Faites tourner ce helper avant et après l'aplatissement et les nombres s'expliquent tout seuls. Pour une page à 90 degrés avec MediaBox [0 0 612 792], la MediaBox aplatie devient [0 0 792 612] ; chaque box réécrite est mappée par la même rotation horaire, relative à l'origine de la MediaBox d'origine, si bien que la nouvelle MediaBox commence toujours à l'origine et que les autres boxes gardent leur position dedans. GetLoadedPageVisibleBox renvoie ce que les lecteurs affichent et les imprimantes impriment, le CropBox clippé à la MediaBox et normalisé pour que Left soit inférieur à Right, et le renderer, l'export SVG, le lecteur et le chemin d'impression de HotPDF emploient tous cette même box. Quand vous avez besoin de la taille de page qu'un humain voit, appelez GetLoadedPageVisibleBox plutôt que de lire /MediaBox
Pourquoi les annotations cassent-elles quand on ne tourne que /Rect ?
Les annotations cassent parce qu'un flux d'apparence n'est pas dessiné directement dans /Rect. Sous §12.5.5, le lecteur transforme d'abord le /BBox du formulaire par son /Matrix, puis met à l'échelle et translate la bounding box de ce résultat vers /Rect. Ne tournez que /Rect et un tampon de 200 × 40 se fait comprimer dans un emplacement de 40 × 200, illisible et sur le flanc. FlattenLoadedPageRotation multiplie donc à droite la rotation horaire de la page dans chaque /Matrix d'apparence (pour 90 degrés, [0 -1 1 0 0 0] en convention vecteur-ligne), à travers les apparences /N, /R et /D et chaque état dedans. Un flux d'apparence peut être partagé par plusieurs annotations ou états, donc chaque flux est tourné exactement une fois par appel. Le seul cas sans réponse propre est un flux partagé entre des pages aux rotations différentes ; il suit la première page qui l'atteint
Deux règles de plus gardent champs de formulaire et post-its en place. L'entrée /MK /R d'un widget (§12.5.6.19) est un angle antihoraire, donc l'angle horaire de la page lui est soustrait, modulo 360 ; sautez ça et la prochaine régénération d'apparence dessinera le texte du champ dans le mauvais sens. Les annotations porteuses du drapeau NoRotate (position de bit 5, valeur 16, §12.5.3) restent droites sur une page tournée et pivotent autour du coin supérieur gauche de leur /Rect, donc l'aplatissement garde leur largeur, leur hauteur et leur apparence droite et ne déplace que ce coin là où la rotation le met. Au-delà des annotations, la méthode tourne aussi /QuadPoints, /Vertices, /L et /InkList, réécrit les destinations explicites qui nomment la page (points /XYZ, rectangles /FitR, et /FitH / /FitV échangés à 90 et 270 degrés, §12.3.2.2), et transforme la géométrie étiquetée comme les entrées /BBox d'attributs des éléments de structure dont le /Pg est la page
Que ne couvre pas l'aplatissement ?
L'aplatissement est une réécriture géométrique des objets propres à une page, et plusieurs situations en sortent discrètement plutôt que bruyamment
- Les pages dont la rotation effective vaut déjà 0, ou dont la MediaBox manque ou a une largeur ou hauteur nulle, sont sautées sans erreur ; comparez la valeur de retour au nombre de pages que vous espériez changer
- Les Form XObjects référencés depuis les ressources de la page gardent leur
/BBoxpropre en espace formulaire, parce que lecmexterne les tourne déjà ; le scan de l'arbre de structure ne suit que/Ket/Adonc il ne rentre jamais une seconde fois dans les ressources ou les annotations de la page - Les destinations se trouvent en scannant chaque objet indirect une fois par page aplatie, donc un gros document aux centaines de pages tournées paie cette marche à chacune
- Le renderer de pages de HotPDF ne dessine pas les annotations, donc un contrôle visuel de tampons tournés exige d'abord
FlattenLoadedAnnotations
// Cuisez les apparences dans le contenu pour que le renderer puisse les montrer,
// puis rendez la page 1 avant et après avoir retiré son /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));
// Comparez ici les pixels du marqueur de coin, pas seulement les dimensions
finally
After.Free;
end;
finally
Before.Free;
end;
Pour le fond, le versant annotation de cette histoire continue dans la synthèse d'apparences d'annotations avant de les aplatir, le renderer derrière la comparaison avant-après est couvert dans le rendu d'une page PDF chargée vers une bitmap, et la rédaction et l'assemblage N-up sur PDF chargés montre la même technique d'ajout de flux de contenu dont dépendent préfixe et suffixe de rotation. HotPDF, y compris FlattenLoadedPageRotation et les lecteurs de page boxes, est disponible pour Delphi et C++Builder sur la page HotPDF Delphi PDF component