Articol tehnic

Matrice PDFium: Prepend vs Append în Delphi și rotația cu pivot

Matricile afine PDF folosesc convenția de vector-linie din ISO 32000-1 §8.3.3, unde un punct înmulțește matricea din stânga: point' = point * M. În PDFium Component pentru Delphi și C++Builder, acest singur fapt fixează întreaga suprafață de API a lui TPdfMatrix: Multiply adaugă la sfârșit, deci M := M * Op, în timp ce PreMultiply antepune, deci M := Op * M

Fiecare bug clasic de transformare se trage din faptul că acea propoziție e reținută invers. Watermark-ul care se rotește perfect în fișierul tău de test și ajunge jumătate în afara paginii în fișierul clientului. Miniatura care iese rotită de două ori pentru că pagina purta deja un sfert de tură. Ștampila al cărei offset e perfect pe A4 și derapează pe Letter. Niciunul dintre acestea nu e un bug de randare; sunt bug-uri de ordine a înmulțirii, și toate sunt reparabile odată ce poți spune cu voce tare în ce spațiu e scrisă fiecare operație

Convenția de vector-linie care stabilește regulile

TPdfMatrix stochează cele șase elemente numite de specificație și le aplică exact așa cum le definește formatul, așa că transformarea în sine e locul de unde pornește raționamentul. TPdfMatrix.TransformPoint calculează x' = x*a + y*c + e și y' = x*b + y*d + f, care e forma cu șase elemente pe care ISO 32000-1 §8.3.4 o definește pentru operatorul cm ce concatenează o matrice pe matricea de transformare curentă. Perechea (a, b) e primul rând, (c, d) al doilea, iar (e, f) rândul de translație. Obiceiurile de vector-coloană luate din OpenGL sau dintr-un curs de algebră liniară te vor induce în eroare aici, și te vor induce silențios, pentru că o matrice cu ordine greșită tot e o matrice perfect validă. Citește un compus în convenția de rând de la stânga la dreapta, și ordinea de aplicare reiese gratis: pentru că point * (M * Op) e egal cu (point * M) * Op, o operație adăugată la sfârșit acționează pe coordonatele pe care matricea existentă le-a produs deja, adică în spațiul paginii, în timp ce o operație antepusă acționează înainte ca matricea existentă să ruleze, în propriul spațiu de intrare al obiectului

var
  M: TPdfMatrix;
  Pt: FS_POINTF;
begin
  M := TPdfMatrix.Create;                // identity
  try
    // Append order: each call acts on what the previous calls produced.
    M.Scale(0.5, 0.5);                   // M := M * S   half size
    M.Rotate(90);                        // M := M * R   clockwise, degrees
    M.Translate(300, 400);               // M := M * T   then move on the page

    Pt := M.TransformPoint(0, 0);        // x*a + y*c + e, x*b + y*d + f
  finally
    M.Free;
  end;
end;

TPdfMatrix.Rotate e implicit în sensul acelor de ceasornic și în grade, cu ACounterClockwise și AAngleInRadians disponibile atunci când datele tale sursă sunt semnate în celălalt sens. Proprietățile read-only a până la f și proprietatea Handle îți dau înapoi FS_MATRIX-ul brut, ceea ce vrea FPDFPageObj_SetMatrix. Nimic din clasă nu-ți ascunde cele șase numere, și asta e deliberat: când o transformare se poartă urât, tipărirea lui a până la f e cel mai rapid diagnostic pe care-l ai

De ce antepunerea unei translații are nevoie de partea liniară?

Pentru că o deplasare antepusă e scrisă în spațiul de intrare al matricei, și trebuie purtată prin partea liniară curentă înainte să se poată alătura rândului de translație. TPdfMatrix.PreTranslate calculează deci e := dx*a + dy*c + e și f := dx*b + dy*d + f. Adăugarea la sfârșit e direcția ușoară: TPdfMatrix.Translate e scrisă în spațiul paginii, unde nimic nu trebuie convertit, așa că doar adaugă dx la e și dy la f. Oricine „optimizează” PreTranslate reducându-l la două adunări tocmai a șters rotația și scalarea din deplasare

M := TPdfMatrix.Create;
try
  M.Rotate(90);              // a=0, b=-1, c=1, d=0

  M.Translate(10, 0);        // append: e := e + 10
                             // -> 10 points to the right on the page

  M.Reset;
  M.Rotate(90);
  M.PreTranslate(10, 0);     // prepend: e := 10*a + 0*c + e  (unchanged)
                             //          f := 10*b + 0*d + f  (f - 10)
                             // -> 10 points along the stamp own x axis,
                             //    which after the turn points down the page
finally
  M.Free;
end;

Aceeași asimetrie străbate perechea de scalare, și merită să știi ce elemente atinge fiecare înainte să depanezi una la trei dimineața. TPdfMatrix.PreScale înmulțește rânduri, scalând a și b cu scaleX, iar c și d cu scaleY, și lasă translația neatinsă, pentru că deplasarea s-a întâmplat deja mai în aval. TPdfMatrix.Scale, care adaugă la sfârșit, înmulțește în schimb coloane, luând a, c, e cu scaleX și b, d, f cu scaleY, așa că offset-ul existent se scalează odată cu tot restul. Ambele sunt căi cu un singur scop care sar peste produsul general cu șase elemente, și ambele păstrează exact semantica de compunere a formei generale

Unde merg cele două translații într-o rotație cu pivot?

În jurul operației, nu în jurul întregii matrice, și în această ordine. TPdfMatrix.RotateAt adaugă la sfârșit Translate(-pivot), apoi rotația, apoi Translate(+pivot), ceea ce sub convenția de vector-linie se compune ca Translate(-pivot) * Op * Translate(pivot). Acea secvență e ceea ce ține pivotul fix sub noua operație, lăsând totodată matricea existentă să-și producă întâi coordonatele și să le predea mai departe. Scrie perechea în ordinea inversă, cum ar fi corect într-o bibliotecă de vector-coloană, și obiectul orbitează în jurul originii în loc să se rotească pe loc, ceea ce e exact cum ajunge un watermark centrat în afara crop box-ului

procedure RotateStampAboutPageCenter(AObj: FPDF_PAGEOBJECT;
  const AAngleDegrees, APageWidth, APageHeight: Single);
var
  M: TPdfMatrix;
  Raw: FS_MATRIX;
begin
  if not FPDFPageObj_GetMatrix(AObj, Raw) then
    raise Exception.Create('Page object carries no matrix');
  M := TPdfMatrix.Create(Raw);
  try
    // Appends Translate(-pivot) * Rotate * Translate(+pivot) in one call.
    M.RotateAt(AAngleDegrees, APageWidth / 2, APageHeight / 2);
    Raw := M.Handle;
    FPDFPageObj_SetMatrix(AObj, Raw);
  finally
    M.Free;
  end;
end;

Aceeași compunere susține ScaleAt, SkewAt, HorizontalFlipAt, VerticalFlipAt și CentralFlipAt, așa că odată ce ai încredere în tipar pentru rotație, poți avea încredere în el pentru rest. TPdfMatrix.CentralFlip merită menționat separat: negează toate cele șase elemente ca să-ți dea o întoarcere de 180 de grade fără nicio trigonometrie deloc, ceea ce înseamnă niciun cos al unei valori care ar fi trebuit să fie exact zero și nicio derivă acumulată atunci când o aplici într-o buclă. Dacă plasezi mărci repetate, nu întorci una singură, mecanica plasării în sine e acoperită în ștampile de pagină reutilizabile cu Form XObject, iar munca de matrice de aici stă direct deasupra ei

Ce îți spune TryDecompose despre o matrice?

TPdfMatrix.TryDecompose raportează translație, scală, rotație, forfecare, determinant și un flag de reflexie sub o convenție scală-apoi-rotație, și le raportează suficient de onest încât să fie utile pentru decizii, nu doar pentru logging. ScaleX vine din lungimea primului rând, Sqrt(a*a + b*b), așa că e mereu pozitiv. ScaleY e apoi Determinant / ScaleX, ceea ce îl face cu semn. Rotația vine din ArcTan2(-b, a) în grade, iar forfecarea din produsul scalar al celor două rânduri, normalizat de ambele scale

Acel semn de pe ScaleY e partea pe care lumea o șterge, iar a o șterge e un bug real, nu unul cosmetic. Un determinant negativ înseamnă că matricea conține o reflexie. Forțează ambii factori de scală să fie pozitivi ca numerele să arate mai ordonat, și tocmai ai aruncat reflexia, așa că o matrice reconstruită din descompunere vine înapoi în oglindă: textul se citește invers, o pagină scanată se răstoarnă, un logo importat privește în direcția greșită. Câmpul IsReflected există ca să nu trebuiască niciodată să o deduci. Aceasta e și verificarea care previne clasica rotație dublă, unde codul adaugă o întoarcere de afișare la o pagină care poartă deja una; versiunea de partea viewer-ului a acelei probleme e tratată în încadrarea miniaturilor, zoom și rotație dublă

var
  D: TPdfMatrixDecomposition;
begin
  if M.TryDecompose(D) then
  begin
    // D.ScaleX is always positive; D.ScaleY carries the determinant sign.
    if D.IsReflected then
      Log('mirrored, ScaleY = %.3f', [D.ScaleY]);

    if Abs(D.RotationDegrees) > 0.5 then
      SkipDisplayRotation;      // the object already carries its own turn
  end
  else
    UseIdentityFallback;        // near-singular or non-finite: no answer
end;

Încadrarea unui dreptunghi în altul fără presupuneri

TPdfMatrix.TryCreateRectMapping construiește pentru tine matricea sursă-la-destinație și primește un TPdfMatrixFitMode de pmfStretch, pmfContain sau pmfCover. Normalizează întâi ambele dreptunghiuri, pentru că dreptunghiurile PDF nu sunt obligate să sosească cu stânga sub dreapta sau josul sub sus, apoi derivă scale X și Y independente: pmfStretch le ține independente, pmfContain o ia pe cea mai mică și centrează letterbox-ul, pmfCover o ia pe cea mai mare și centrează decuparea. Însoțitorul MapRectToRect adaugă la sfârșit aceeași mapare pe o matrice existentă, iar NewRectMapping ridică EPdfMatrixError acolo unde forma Try returnează False. Acesta e primitivul de sub fiecare plasare de celulă din imposiția N-up și reordonarea paginilor, unde fiecare pagină sursă trebuie să aterizeze într-o celulă calculată, fără să rederivi aritmetica per layout

Matrici degenerate și traseul onest de eșec

Intrările finite nu garantează un rezultat finit, așa că fitting-ul calculează în Double și apoi reverifică pentru finitudine candidatul Single îngustat, înainte de a-l publica; o mapare care conține un infinit nu e niciodată dată înapoi ca și cum ar fi validă. Aceeași disciplină guvernează inversarea. TPdfMatrix.TryGetInverse respinge o matrice folosind un prag relativ, comparând determinantul contra epsilon-ului înmulțit cu pătratul celui mai mare element liniar, nu contra unei constante fixe, ceea ce ține testul semnificativ indiferent dacă unitățile tale sunt puncte sau micrometri. TryDecompose renunță în același fel, refuzând atunci când lungimea primului rând sau ScaleY-ul derivat cade la sau sub epsilon

Alege stilul de eșec potrivit locului de apel, nu împacheta totul în try-except din obișnuință. TryInvert, TryGetInverse, TryInverseTransformPoint, TryTransformBounds și TryCreateRectMapping returnează False și lasă țintele lor neatinse, ceea ce se potrivește cu hit-testing-ul și buclele per-obiect unde un obiect degenerat ar trebui sărit, nu fatal. Invert, InverseCopy, InverseTransformPoint, MapRectToRect și TransformBounds ridică în schimb EPdfMatrixError, ceea ce se potrivește codului de setup unde o matrice singulară înseamnă că apelantul a calculat ceva greșit. Pentru muncă în lot, TransformPoints și TransformRects alocă array-ul lor de rezultat exact o dată, TransformPointsInPlace și TransformRectsInPlace reutilizează stocarea ta, iar TryTransformBounds acumulează bounding box-ul într-o singură trecere, în loc să materializeze întâi puncte transformate

Nimic din toate astea nu e matematică exotică. E o singură convenție, aplicată consecvent, cu API-ul numit astfel încât convenția e vizibilă la locul de apel: Multiply și verbele simple adaugă la sfârșit, familia Pre antepune, familia At încadrează operația cu perechea ei de pivot. Scrie ordinea într-un comentariu lângă orice compus construiești, pentru că un cod care se citește corect azi e codul pe care cineva îl inversează peste șase luni. Referința completă TPdfMatrix, împreună cu API-urile de obiecte de pagină și randare pe care le alimentează aceste transformări, trăiește la PDFium Component pentru Delphi și C++Builder