Articol tehnic

Aplatizarea rotației paginii PDF fără boxe sparte în Delphi

HotPDF aplatizează rotația paginilor PDF cu THotPDF.FlattenLoadedPageRotation: metoda învăluie conținutul fiecărei pagini rotite într-o transformare cm în sensul acelor de ceasornic, rescrie fiecare page box pe care pagina chiar îl are, rotește geometria adnotărilor, matricele de aparență, destinațiile explicite și geometria structurii etichetate cu același unghi, apoi setează /Rotate pe 0. Pagina arată identic într-un viewer, dar sistemul ei de coordonate e acum drept. Asta contează în clipa în care o unealtă din aval, un print RIP sau propriul vostru cod de ștampilare ignoră /Rotate și plasează lucruri în user space brut

Declanșatorul tipic e un scanner sau o aplicație de captură mobilă care scrie paginile landscape ca media portrait cu /Rotate 90. Fiecare viewer le arată corect, deci nimeni nu observă până când cineva ștampilează un număr de pagină în „colțul din dreapta-jos” și aterizează pe lateral, de-a lungul marginii din stânga, sau un pas de imposition care citește doar /MediaBox aranjează un slot portrait pentru o pagină landscape. Aplatizarea sună a treabă de o linie de matrice. În practică atinge cinci page box-uri, trei feluri de geometrie de adnotare, țintele de legătură ale documentului și arborele de structură, iar fiecare dintre ele are propria regulă în ISO 32000-1

În ce sens rotește /Rotate o pagină PDF?

/Rotate rotește pagina în sensul acelor de ceasornic pentru afișare și tipărire, în multipli de 90 de grade (ISO 32000-1 §7.7.3.3, Tabelul 30). La 90 de grade, muchia din stânga a media-ului devine partea de sus, iar muchia de sus devine partea din dreapta, deci într-un device space cu y în jos maparea e X = (y - Bottom) * Scale și Y = (x - Left) * Scale. La 270 de grade, muchia din dreapta devine partea de sus. /Rotate e de asemenea unul dintre cele doar patru atribute de pagină moștenibile, alături de /Resources, /MediaBox și /CropBox (§7.7.3.4), deci un dicționar de pagină fără /Rotate propriu poate fi rotit oricum de un strămoș /Pages. THotPDF.GetLoadedPageRotation parcurge lanțul /Parent și normalizează rezultatul în 0–359, valoarea de care aveți nevoie, nu cheia brută de pe pagină

Sensul e ușor de greșit într-un fel care supraviețuiește testării, iar build-urile HotPDF mai vechi au făcut exact asta. Vechea matrice pagină-către-dispozitiv swap-uia componentele y pentru 90 și 270, ceea ce produce o reflexie peste diagonală, nu o rotație: orientarea matricei se răstoarnă față de cazul nerotit. Ambele unghiuri arătau tot „rotite”, bitmap-ul avea lățimea și înălțimea inversate, iar un dus-întors de la pagină la view și înapoi întorcea punctul de start, deci verificările de dimensiuni și testele de dus-întors treceau toate. Singura verificare de încredere e unde ajunge un marker de colț, comparat pixel cu pixel cu un renderer de referință. Fiindcă modelul de viewer, backend-ul de randare SIMD și maparea highlight-urilor copiaseră aceeași matrice, toate au fost corectate împreună, iar codul de aplatizare folosește acum aceeași convenție clockwise ca renderer-ul

Cum aplatizează HotPDF rotația paginilor în Delphi: o pagină portrait stocată cu /Rotate 90 se afișează clockwise ca un view landscape de 792 pe 612, maparea către dispozitiv X = (y - Bottom) * Scale, Y = (x - Left) * Scale mută fiecare colț, iar swap-ul componentelor y ale matricei produce o reflexie pe care doar o comparație de marker de colț o prinde
Viewer-ele rotesc pagina clockwise pentru afișare în timp ce octeții rămân portrait — GetLoadedPageRotation parcurge întâi lanțul /Parent, fiindcă /Rotate e unul dintre cele patru atribute de pagină moștenibile

Cum rescrie FlattenLoadedPageRotation o pagină

FlattenLoadedPageRotation(PageRange, Info) procesează fiecare pagină din PageRange a cărei rotație efectivă e 90, 180 sau 270 și întoarce numărul de pagini aplatizate. Un PageRange gol înseamnă toate paginile; altfel șirul folosește sintaxa obișnuită pe bază de unu '1-3,7', iar un număr de pagină în afara intervalului ridică o excepție în loc să fie sărit. Content stream-urile originale nu sunt niciodată re-encodate. Metoda adaugă în fața /Contents-ului paginii un stream nou care conține q 0 -1 1 0 -Bottom Width+Left cm (pentru 90 de grade), sufixează un stream care conține Q și scrie în final un /Rotate 0 explicit în dicționarul paginii, ca o valoare moștenită pe un nod /Pages să nu mai rotească pagina a doua oară

var
  Pdf: THotPDF;
  Info: THPDFRotationFlattenInfo;
  Flattened: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('scanned-batch.pdf') > 0 then
    begin
      // '' = toate paginile; cele la 0 grade sunt scanate dar lăsate în pace
      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;

Înregistrarea THPDFRotationFlattenInfo merită logată, nu aruncată. ScannedPageCount e mărimea intervalului, FlattenedPageCount egalează valoarea întoarsă, iar cei trei contoare Transformed... vă spun dacă documentul avea legături, bookmark-uri sau geometrie etichetată îndreptată spre paginile rotite. Un lot în care fiecare fișier raportează zero destinații e în regulă; un fișier PDF/UA etichetat care raportează zero geometrie de structură când vă așteptați la bounding box-uri de figuri e un semnal să-l inspectați de mână

Ce page box-uri rescrie aplatizarea și în ce ordine?

Aplatizarea rescrie doar boxele pe care pagina le are deja, și le citește pe toate înainte să scrie vreuna. Ordinea contează din cauza lanțului de implicite: GetLoadedPageBox(PageIndex, pbCropBox, ...) întoarce /MediaBox-ul când pagina n-are /CropBox, iar /BleedBox, /TrimBox și /ArtBox cad implicit pe CropBox (§14.11.2). O versiune anterioară citea, transforma și scria câte o boxă pe rând. Rescria MediaBox-ul întâi, apoi citea „CropBox-ul”, primea înapoi MediaBox-ul deja rotit, îl rotea a doua oară și scria un CropBox pe care pagina nu-l avusese niciodată, ceea ce tăia o pagină landscape până la un pătrat. Regulile de moștenire se despart la fel: MediaBox și CropBox se caută de-a lungul lanțului /Parent, în vreme ce Bleed, Trim și ArtBox contează doar dacă stau pe dicționarul paginii însuși, deci un /TrimBox rătăcit pe un nod /Pages e tratat ca absent și nu e copiat niciodată pe pagină

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]));
  // Adevărat chiar fără cheie /TrimBox: valoarea cade pe CropBox, apoi pe 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 lasă output-urile neatinse la eșec
  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;

Rulați helper-ul ăsta înainte și după aplatizare și numerele se explică singure. Pentru o pagină de 90 de grade cu MediaBox [0 0 612 792], MediaBox-ul aplatizat devine [0 0 792 612]; fiecare boxă rescrisă e mapată prin același viraj clockwise, relativ la originea MediaBox-ului original, deci noul MediaBox pornește întotdeauna la origine, iar celelalte boxe își păstrează poziția în interiorul lui. GetLoadedPageVisibleBox întoarce ce arată viewer-ele și ce tipăresc imprimantele, CropBox-ul tăiat de MediaBox și normalizat astfel încât Left să fie mai mic decât Right, iar renderer-ul, exportul SVG, viewer-ul și calea de tipărire ale HotPDF folosesc toate aceeași boxă. Când vă trebuie dimensiunea paginii pe care o vede un om, apelați GetLoadedPageVisibleBox în loc să citiți /MediaBox

De ce citește HotPDF fiecare page box înainte să scrie vreunul în timpul FlattenLoadedPageRotation: BleedBox, TrimBox și ArtBox cad implicit pe CropBox, care la rândul lui cade pe MediaBox, deci rotirea boxelor câte una a făcut ca CropBox-ul să citească MediaBox-ul deja rescris, iar al doilea viraj a scris o boxă pe care pagina n-o avusese niciodată, tăind o pagină landscape la un pătrat
Lanțul de implicite înseamnă că output-ul unei boxe e input-ul alteia — citește totul întâi, transformă relativ la originea MediaBox-ului original, apoi scrie

De ce se strică adnotările când rotești doar /Rect?

Adnotările se strică pentru că un appearance stream nu e desenat direct în /Rect. Sub §12.5.5, viewer-ul transformă întâi /BBox-ul formei prin /Matrix-ul lui, apoi scalează și translatează bounding box-ul acelui rezultat în /Rect. Rotește doar /Rect și o ștampilă de 200 × 40 e storsă într-un slot de 40 × 200, ilizibilă și pe lateral. De aceea FlattenLoadedPageRotation înmulțește la dreapta virajul clockwise al paginii pe fiecare /Matrix de aparență (pentru 90 de grade, [0 -1 1 0 0 0] în convenția de vectori-rând), peste aparențele /N, /R și /D și fiecare stare din interiorul lor. Un appearance stream poate fi partajat de mai multe adnotări sau stări, deci fiecare stream e rotit exact o dată per apel. Singurul caz fără răspuns curat e un stream partajat între pagini cu rotații diferite; el urmează prima pagină care ajunge la el

Încă două reguli țin câmpurile de formular și notițele lipicioase la locul lor. Intrarea /MK /R a unui widget (§12.5.6.19) e un unghi în sens invers acelor de ceasornic, deci unghiul clockwise al paginii i se scade, modulo 360; săriți peste asta și următoarea regenerare de aparențe desenează textul câmpului în direcția greșită. Adnotările cu flag-ul NoRotate (poziția bitului 5, valoarea 16, §12.5.3) rămân drepte pe o pagină rotită și pivotă în jurul colțului din stânga-sus al /Rect-ului lor, deci aplatizarea le păstrează lățimea, înălțimea și aspectul drept și doar mută colțul acela acolo unde îl lasă virajul. Dincolo de adnotări, metoda rotește și /QuadPoints, /Vertices, /L și /InkList, rescrie destinațiile explicite care numesc pagina (puncte /XYZ, dreptunghiuri /FitR, iar /FitH / /FitV inversate la 90 și 270 de grade, §12.3.2.2) și transformă geometria etichetată precum intrările /BBox de atribut pentru elementele de structură al căror /Pg e pagina

De ce se strică adnotările când o pagină HotPDF e aplatizată rotind doar /Rect: o ștampilă de 200 pe 40 e scalată într-un slot de 40 pe 200 și devine ilizibilă, deci FlattenLoadedPageRotation înmulțește la dreapta virajul clockwise pe fiecare /Matrix de aparență peste /N, /R și /D, ajustează /MK /R-ul în sens invers acelor și pivotă adnotările NoRotate pe colțul lor din stânga-sus
Viewer-ul potrivește BBox-ul transformat al aparenței în /Rect, deci stream-ul însuși trebuie rotit — o trecere per aparență partajată, exact o dată per apel

Ce nu acoperă aplatizarea?

Aplatizarea e o rescriere geometrică a obiectelor proprii ale unei singure pagini, iar câteva situații rămân în afara ei în tăcere, nu cu zgomot

  • Paginile a căror rotație efectivă e deja 0, sau al căror MediaBox lipsește sau are lățime sau înălțime zero, sunt sărite fără eroare; comparați valoarea întoarsă cu numărul de pagini pe care vă așteptați să se schimbe
  • Form XObjects-urile referențiate din resursele paginii își păstrează propriul /BBox în spațiul formei, pentru că cm-ul exterior le rotește deja; scanarea arborelui de structură urmărește doar /K și /A, deci nu pășește niciodată a doua oară în resursele paginii sau în adnotări
  • Destinațiile se găsesc scanând fiecare obiect indirect o dată per pagină aplatizată, deci un document mare cu sute de pagini rotite plătește acea plimbare la fiecare
  • Renderer-ul de pagină al HotPDF nu desenează adnotări, deci o verificare vizuală a ștampilelor rotite are nevoie întâi de FlattenLoadedAnnotations
// Coaceți aparențele în conținut ca renderer-ul să le poată arăta,
// apoi randăți pagina 1 înainte și după ștergerea /Rotate-ului
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));
    // Comparați aici pixelii markerului de colț, nu doar dimensiunile
  finally
    After.Free;
  end;
finally
  Before.Free;
end;

Pentru context mai adânc, partea de adnotări a poveștii continuă în sintetizarea aparențelor de adnotare înainte de aplatizarea lor, renderer-ul din spatele comparației înainte-după e acoperit în randarea unei pagini PDF încărcate într-un bitmap, iar redactarea și îmbinarea N-up pe PDF-uri încărcate arată aceeași tehnică de adăugare la content stream de care se leagă prefixul și sufixul de rotație. HotPDF, inclusiv FlattenLoadedPageRotation și cititoarele de page box, e disponibil pentru Delphi și C++Builder pe pagina componentei HotPDF Delphi PDF