Articolo tecnico

Appiattire /Rotate nelle pagine PDF senza rompere i box

HotPDF appiattisce la rotazione delle pagine PDF con THotPDF.FlattenLoadedPageRotation: il metodo avvolge il contenuto di ogni pagina ruotata in una trasformazione cm oraria, riscrive ogni page box che la pagina ha davvero, gira la geometria delle annotazioni, le matrici di appearance, le destinazioni esplicite e la geometria della struttura dello stesso angolo, e infine imposta /Rotate a 0. La pagina appare identica in un viewer, ma il suo sistema di coordinate ora è dritto. Questo conta nell'istante in cui uno strumento a valle, un print RIP o il tuo codice di timbratura ignora /Rotate e piazza le cose nello user space grezzo

Il grilletto tipico è uno scanner o una app di acquisizione mobile che scrive pagine orizzontali come media verticali con /Rotate 90. Ogni viewer le mostra correttamente, quindi nessuno se ne accorge finché qualcuno non timbra un numero di pagina nell'«angolo in basso a destra» e atterra di lato lungo il bordo sinistro, o un passo di imposition che legge solo /MediaBox impagina uno slot verticale per una pagina orizzontale. Appiattire suona come un lavoro a matrice di una riga sola. In pratica tocca cinque page box, tre tipi di geometria delle annotazioni, i target dei link del documento e l'albero della struttura, e ognuno di questi ha la sua regola in ISO 32000-1

In che direzione /Rotate gira una pagina PDF?

/Rotate gira la pagina in senso orario per visualizzazione e stampa, a multipli di 90 gradi (ISO 32000-1 §7.7.3.3, Tabella 30). A 90 gradi il bordo sinistro del media diventa il lato superiore e il bordo superiore diventa il lato destro, quindi in uno spazio device con y verso il basso la mappatura è X = (y - Bottom) * Scale e Y = (x - Left) * Scale. A 270 gradi il bordo destro diventa il lato superiore. /Rotate è anche uno dei soli quattro attributi di pagina ereditabili, insieme a /Resources, /MediaBox e /CropBox (§7.7.3.4), quindi un dizionario di pagina senza un proprio /Rotate può comunque essere girato da un antenato /Pages. THotPDF.GetLoadedPageRotation percorre la catena /Parent e normalizza il risultato in 0-359, che è il valore che vuoi, non la chiave grezza sulla pagina

La direzione è facile sbagliare in un modo che sopravvive ai test, e build HotPDF precedenti hanno fatto esattamente quello. La vecchia matrice pagina-a-device scambiava le componenti y per 90 e 270, il che produce una riflessione attraverso la diagonale invece di una rotazione: l'orientamento della matrice si ribalta rispetto al caso non ruotato. Entrambi gli angoli «sembravano ruotati», la bitmap ha larghezza e altezza scambiate, e un round trip da pagina a vista e ritorno restituisce il punto di partenza, quindi i controlli sulle dimensioni e i test di round trip passano tutti. L'unico controllo affidabile è dove finisce un marcatore d'angolo, confrontato pixel per pixel con un renderer di riferimento. Dato che il modello viewer, il backend di render SIMD e la mappatura degli highlight avevano copiato la stessa matrice, tutti sono stati corretti insieme, e il codice di appiattimento ora usa la stessa convenzione oraria del renderer

Come HotPDF appiattisce la rotazione di pagina in Delphi: una pagina verticale memorizzata con /Rotate 90 appare in senso orario come una vista orizzontale di 792 per 612, la mappatura device X = (y - Bottom) * Scale, Y = (x - Left) * Scale sposta ogni angolo, e scambiare le componenti y della matrice produce una riflessione che solo un confronto con marcatore d'angolo coglie
I viewer girano la pagina in senso orario per la visualizzazione mentre i byte restano verticali — GetLoadedPageRotation percorre prima la catena /Parent, perché /Rotate è uno dei quattro attributi di pagina ereditabili

Come FlattenLoadedPageRotation riscrive una pagina

FlattenLoadedPageRotation(PageRange, Info) elabora ogni pagina in PageRange la cui rotazione effettiva è 90, 180 o 270, e restituisce il numero di pagine appiattite. Un PageRange vuoto significa tutte le pagine; altrimenti la stringa usa la consueta sintassi a base uno '1-3,7', e un numero di pagina fuori intervallo solleva un'eccezione invece di essere saltato. I content stream originali non vengono mai ricodificati. Il metodo prepone un nuovo stream contenente q 0 -1 1 0 -Bottom Width+Left cm (per 90 gradi) al /Contents della pagina, accoda uno stream contenente Q, e infine scrive un esplicito /Rotate 0 nel dizionario di pagina così un valore ereditato su un nodo /Pages non può girare la pagina una seconda volta

var
  Pdf: THotPDF;
  Info: THPDFRotationFlattenInfo;
  Flattened: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('scanned-batch.pdf') > 0 then
    begin
      // '' = tutte le pagine; le pagine a 0 gradi vengono scansionate ma lasciate stare
      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;

Il record THPDFRotationFlattenInfo merita di essere loggato piuttosto che scartato. ScannedPageCount è la dimensione dell'intervallo, FlattenedPageCount eguaglia il valore di ritorno, e i tre contatori Transformed... ti dicono se il documento aveva link, bookmark o geometria di struttura puntati sulle pagine girate. Un batch in cui ogni file riporta zero destinazioni va bene; un file PDF/UA con tag che riporta zero geometria di struttura quando ti aspettavi bounding box di figure è un segnale per ispezionarlo a mano

Quali page box riscrive l'appiattimento, e in che ordine?

L'appiattimento riscrive solo i box che la pagina ha già, e legge ogni box prima di scriverne uno qualsiasi. L'ordine conta per via della catena di default: GetLoadedPageBox(PageIndex, pbCropBox, ...) restituisce il /MediaBox quando la pagina non ha /CropBox, e /BleedBox, /TrimBox e /ArtBox ripiegano sul CropBox (§14.11.2). Una versione precedente leggeva, trasformava e scriveva un box alla volta. Riscriveva prima il MediaBox, poi leggeva il «CropBox», riceveva indietro il MediaBox già girato, lo girava una seconda volta e scriveva un CropBox che la pagina non aveva mai avuto, ritagliando una pagina orizzontale fino a farla quadrata. Le regole di ereditarietà si spaccano allo stesso modo: MediaBox e CropBox vengono cercati lungo la catena /Parent, mentre Bleed, Trim e ArtBox contano solo se siedono sul dizionario di pagina stesso, quindi un /TrimBox vagante su un nodo /Pages è trattato come assente e mai copiato sulla pagina

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 anche senza una chiave /TrimBox: il valore ripiega su CropBox, poi MediaBox
  if Pdf.GetLoadedPageBox(PageIndex, pbTrimBox, L, B, R, T) then
    Writeln(Format('TrimBox  [%g %g %g %g]', [L, B, R, T]));
  // Preset Letter; GetLoadedPageVisibleBox lascia gli output intatti in caso di fallimento
  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;

Esegui quell'helper prima e dopo l'appiattimento e i numeri si spiegano da soli. Per una pagina a 90 gradi con MediaBox [0 0 612 792], il MediaBox appiattito diventa [0 0 792 612]; ogni box riscritto è mappato attraverso la stessa girata oraria, relativa all'origine del MediaBox originale, così il nuovo MediaBox parte sempre dall'origine e gli altri box conservano la loro posizione dentro di esso. GetLoadedPageVisibleBox restituisce ciò che i viewer mostrano e le stampanti stampano, il CropBox ritagliato sul MediaBox e normalizzato così che Left sia minore di Right, e il renderer di HotPDF, l'export SVG, il viewer e il percorso di stampa usano tutti quello stesso box. Quando ti serve la dimensione di pagina che vede un essere umano, chiama GetLoadedPageVisibleBox piuttosto che leggere /MediaBox

Perché HotPDF legge ogni page box prima di scriverne uno durante FlattenLoadedPageRotation: BleedBox, TrimBox e ArtBox ripiegano sul CropBox, che a sua volta ripiega sul MediaBox, quindi girare i box uno alla volta faceva leggere al CropBox il MediaBox già riscritto e una seconda girata scriveva un box che la pagina non aveva mai avuto, ritagliando una pagina orizzontale a quadrata
La catena di default fa sì che l'output di un box sia l'input di un altro — leggi tutto prima, trasforma rispetto all'origine del MediaBox originale, poi scrivi

Perché le annotazioni si rompono se giri solo /Rect?

Le annotazioni si rompono perché uno stream di appearance non viene disegnato dritto nel /Rect. Sotto §12.5.5 il viewer prima trasforma il /BBox della form per la sua /Matrix, poi scala e trasla il bounding box di quel risultato nel /Rect. Gira solo /Rect e un timbro di 200 × 40 viene spremuto in uno slot di 40 × 200, illeggibile e di lato. FlattenLoadedPageRotation quindi moltiplica a destra la girata oraria della pagina sulla /Matrix di ogni appearance (per 90 gradi, [0 -1 1 0 0 0] nella convenzione row-vector), attraverso le appearance /N, /R e /D e ogni stato dentro di esse. Uno stream di appearance può essere condiviso da più annotazioni o stati, quindi ogni stream viene girato esattamente una volta per chiamata. L'unico caso senza risposta pulita è uno stream condiviso tra pagine con rotazioni diverse; segue la prima pagina che lo raggiunge

Altre due regole tengono campi modulo e note adesive al loro posto. La voce /MK /R di un widget (§12.5.6.19) è un angolo antiorario, quindi l'angolo orario della pagina viene sottratto da esso, modulo 360; salta questo e la prossima rigenerazione di appearance disegna il testo del campo nella direzione sbagliata. Le annotazioni con il flag NoRotate (bit posizione 5, valore 16, §12.5.3) restano dritte su una pagina ruotata e ruotano attorno all'angolo superiore sinistro del loro /Rect, quindi l'appiattimento conserva la loro larghezza, altezza e apparenza dritta e sposta solo quell'angolo dove la girata lo mette. Oltre alle annotazioni, il metodo gira anche /QuadPoints, /Vertices, /L e /InkList, riscrive le destinazioni esplicite che nominano la pagina (punti /XYZ, rettangoli /FitR, e /FitH / /FitV scambiati a 90 e 270 gradi, §12.3.2.2), e trasforma la geometria con tag come le voci /BBox degli attributi per gli elementi di struttura il cui /Pg è la pagina

Perché le annotazioni si rompono quando una pagina HotPDF viene appiattita girando solo /Rect: un timbro di 200 per 40 viene scalato in uno slot di 40 per 200 e diventa illeggibile, quindi FlattenLoadedPageRotation moltiplica a destra la girata oraria sulla /Matrix di ogni appearance attraverso /N, /R e /D, aggiusta l'antiorario /MK /R e fa ruotare le annotazioni NoRotate sul loro angolo superiore sinistro
Il viewer adatta il BBox trasformato dell'appearance nel /Rect, quindi lo stream stesso deve girare — un passaggio per appearance condivisa, esattamente una volta per chiamata

Che cosa non copre l'appiattimento?

L'appiattimento è una riscrittura geometrica degli oggetti propri di una pagina, e diverse situazioni ne restano fuori in silenzio piuttosto che con clamore

  • Le pagine la cui rotazione effettiva è già 0, o il cui MediaBox manca o ha larghezza o altezza zero, vengono saltate senza errore; confronta il valore di ritorno con il numero di pagine che ti aspettavi modificare
  • I Form XObjects referenziati dalle risorse della pagina conservano il loro /BBox nello spazio form, perché il cm esterno li gira già; la scansione dell'albero di struttura segue solo /K e /A quindi non entra mai una seconda volta nelle risorse o nelle annotazioni della pagina
  • Le destinazioni vengono trovate scansionando ogni oggetto indiretto una volta per pagina appiattita, quindi un documento grande con centinaia di pagine ruotate paga quella passeggiata su ognuna
  • Il renderer di pagine di HotPDF non disegna le annotazioni, quindi un controllo visivo dei timbri girati richiede prima FlattenLoadedAnnotations
// Cucina le appearance nel contenuto così il renderer può mostrarle,
// poi renderizza la pagina 1 prima e dopo aver tolto il suo /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));
    // Confronta qui i pixel del marcatore d'angolo, non solo le dimensioni
  finally
    After.Free;
  end;
finally
  Before.Free;
end;

Per approfondire, il lato annotazioni di questa storia continua in sintetizzare le appearance delle annotazioni prima di appiattirle, il renderer dietro al confronto prima-dopo è trattato in renderizzare una pagina PDF caricata in una bitmap, e redaction e stitching N-up su PDF caricati mostra la stessa tecnica di accodamento al content stream su cui si affidano il prefisso e il suffisso di rotazione. HotPDF, incluso FlattenLoadedPageRotation e i lettori di page box, è disponibile per Delphi e C++Builder sulla pagina di HotPDF Delphi PDF component