Odborný článok

PDFium Matrix Prepend vs Append v Delphi: Pivot rotácia

Afinné matice PDF používajú konvenciu riadkových vektorov z ISO 32000-1 §8.3.3, kde bod násobí maticu zľava: point' = point * M. V PDFium Component pre Delphi a C++Builder tento jeden fakt fixuje celý povrch API triedy TPdfMatrix: Multiply pripája (append), takže M := M * Op, kým PreMultiply predsúva (prepend), takže M := Op * M

Každý klasický bug transformácie sa dá vystopovať späť k tomu, že táto veta je zapamätaná pozadu. Vodoznak, ktorý sa úhľadne otočí vo vašom testovacom súbore a pristane napoly mimo stránku v súbore zákazníka. Miniatúra, ktorá vyjde otočená dvakrát, pretože stránka už niesla štvrťotočenie. Pečiatka, ktorej offset je dokonalý na A4 a driftuje na Letter. Nič z toho nie sú rendering bugy; sú to bugy poradia násobenia, a všetky sa dajú opraviť, len čo dokážete nahlas povedať, v akom priestore je napísaná každá operácia

Diagram poradia násobenia matíc PDF v PDFium Component pre Delphi: Multiply pripojí operácie pôsobiace na priestor strany, zatiaľ čo PreMultiply pripojí dopredu a prinúti posun cez lineárnu časť
Pripojené operátory pôsobia na súradnice, ktoré existujúca matica už vyprodukovala, zatiaľ čo predpojené zasiahnu surový vstup a musia cestovať cez lineárnu časť

Konvencia riadkových vektorov, ktorá stanovuje pravidlá

TPdfMatrix ukladá šesť prvkov pomenovaných špecifikáciou a aplikuje ich presne tak, ako ich formát definuje, takže transformácia samotná je miestom, kde uvažovanie začína. TPdfMatrix.TransformPoint počíta x' = x*a + y*c + e a y' = x*b + y*d + f, čo je šesťprvková forma, ktorú ISO 32000-1 §8.3.4 definuje pre operátor cm, ktorý spája maticu na aktuálnu transformačnú maticu. Pár (a, b) je prvý riadok, (c, d) druhý riadok, a (e, f) riadok posunu. Zvyky column-vector osvojené z OpenGL alebo kurzu lineárnej algebry vás tu zavedú, a zavedú vás potichu, pretože matica v nesprávnom poradí je stále úplne platná matica. Čítajte kompozíciu v konvencii riadkov zľava doprava, a poradie aplikácie vyplynie zadarmo: keďže point * (M * Op) sa rovná (point * M) * Op, pripojená (appended) operácia pôsobí na súradnice, ktoré už vyprodukovala existujúca matica, teda na priestor stránky, kým predsunutá (prepended) operácia pôsobí pred behom existujúcej matice, vo vlastnom vstupnom priestore objektu

var
  M: TPdfMatrix;
  Pt: FS_POINTF;
begin
  M := TPdfMatrix.Create;                // identity
  try
    // Poradie Append: každé volanie pracuje s tým, čo vytvorili predchádzajúce volania
    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   potom posun na strane

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

TPdfMatrix.Rotate je predvolene v smere hodinových ručičiek a v stupňoch, s ACounterClockwise a AAngleInRadians dostupnými, keď sú vaše zdrojové dáta označené opačne. Vlastnosti af len na čítanie a vlastnosť Handle vám dajú surovú FS_MATRIX späť, čo je to, čo chce FPDFPageObj_SetMatrix. Nič v triede pred vami neskrýva šesť čísel, a to je zámerné: keď sa transformácia zle správa, vypísanie af je najrýchlejšia diagnóza, akú máte

Prečo predsunutie posunu potrebuje lineárnu časť?

Pretože predsunutý posun je napísaný vo vstupnom priestore matice, a musí byť prenesený cez aktuálnu lineárnu časť skôr, než sa môže pripojiť k riadku posunu. TPdfMatrix.PreTranslate teda počíta e := dx*a + dy*c + e a f := dx*b + dy*d + f. Pripojenie je jednoduchší smer: TPdfMatrix.Translate je napísané v priestore stránky, kde sa nič nemusí konvertovať, takže len pridá dx k e a dy k f. Ktokoľvek „optimalizuje" PreTranslate na dve sčítania, práve vymazal rotáciu a škálovanie z posunu

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

  M.Translate(10, 0);        // append: e := e + 10
                             // -> 10 bodov doprava na strane

  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 bodov pozdĺž vlastnej osi x pečiatky,
                             //    ktorá po otočení smeruje nadol po strane
finally
  M.Free;
end;

Rovnaká asymetria prechádza cez pár na škálovanie, a oplatí sa vedieť, ktorých prvkov sa každý dotýka skôr, než jeden budete debugovať o tretej ráno. TPdfMatrix.PreScale násobí riadky, škáluje a a b o scaleX a c a d o scaleY, a ponechá posun na pokoji, pretože posun sa už stal downstream. Pripájajúca TPdfMatrix.Scale namiesto toho násobí stĺpce, berúc a, c, e o scaleX a b, d, f o scaleY, takže existujúci offset sa škáluje spolu so všetkým ostatným. Obe sú jednoúčelové cesty, ktoré preskočia všeobecný šesťprvkový súčin, a obe presne zachovávajú kompozičnú sémantiku všeobecnej formy

Kam idú dva posuny v pivot rotácii?

Okolo operácie, nie okolo celej matice, a v tomto poradí. TPdfMatrix.RotateAt pripojí Translate(-pivot), potom rotáciu, potom Translate(+pivot), čo sa v konvencii riadkových vektorov skladá ako Translate(-pivot) * Op * Translate(pivot). Táto sekvencia je to, čo udržuje pivot fixovaný pod novou operáciou, kým stále necháva existujúcu maticu najprv vyprodukovať jej súradnice a odovzdať ich ďalej. Napíšte pár opačne, ako by bolo správne v column-vector knižnici, a objekt obieha okolo počiatku namiesto toho, aby sa točil na mieste, čo je presne to, ako centrovaný vodoznak skončí mimo crop boxu

Diagram rotácie okolo pivota pre TPdfMatrix.RotateAt v Delphi, kde translate mínus pivot, rotate, translate plus pivot otočí pečiatku na mieste, zatiaľ čo obrátený pár obletí pôvod mimo crop boxu
Zátvorka RotateAt drží vodoznak točiaci sa okolo svojho pivotu, zatiaľ čo obrátené poradie ho pošle obiehať z crop boxu
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;

Rovnaká kompozícia stojí za ScaleAt, SkewAt, HorizontalFlipAt, VerticalFlipAt, a CentralFlipAt, takže len čo raz vzoru dôverujete pri rotácii, môžete mu dôverovať aj pri zvyšku. TPdfMatrix.CentralFlip stojí za samostatnú zmienku: neguje všetkých šesť prvkov, aby vám dal otočenie o 180 stupňov bez akejkoľvek trigonometrie, čo znamená žiaden cos hodnoty, ktorá mala byť presne nula, a žiadny kumulujúci sa drift, keď ho aplikujete v slučke. Ak umiestňujete opakované značky namiesto otáčania jednej, mechanika samotného umiestnenia je pokrytá v znovupoužiteľných pečiatkach stránok s Form XObjekt, a práca s maticami tu sedí priamo nad ňou

Čo vám TryDecompose povie o matici?

TPdfMatrix.TryDecompose hlási posun, škálu, rotáciu, skos, determinant a príznak zrkadlenia pod konvenciou škála-potom-rotácia, a hlási ich dostatočne poctivo na to, aby boli užitočné pre rozhodnutia, nie len pre logovanie. ScaleX pochádza z dĺžky prvého riadku, Sqrt(a*a + b*b), takže je vždy kladné. ScaleY je potom Determinant / ScaleX, čo z neho robí znamienkové číslo. Rotácia pochádza z ArcTan2(-b, a) v stupňoch, a skos z dot súčinu dvoch riadkov normalizovaného oboma škálami

PDFium Component: Mapa polí TPdfMatrix.TryDecompose ukazujúca znamienkové ScaleY z determinantu, príznak IsReflected a chybu zrkadleného textu spôsobenú vynútením oboch faktorov mierky kladných
TryDecompose drží ScaleY znamienkový úmyselne, pretože zovretie oboch škálovacích faktorov na kladné potichu zrkadlí akúkoľvek odrazenú maticu

Toto znamienko na ScaleY je časť, ktorú ľudia mažú, a jej vymazanie je skutočný bug, nie kozmetický. Záporný determinant znamená, že matica obsahuje zrkadlenie. Vynúťte oba škálovacie faktory kladné, aby čísla vyzerali úhľadnejšie, a zrkadlenie ste zahodili, takže matica prerekonštruovaná z dekompozície sa vráti zrkadlená: text sa číta pozadu, skenovaná stránka sa preklopí, importované logo smeruje nesprávnym smerom. Pole IsReflected existuje, aby ste to nikdy nemuseli odvodzovať. Toto je tiež kontrola, ktorá zabraňuje klasickej dvojitej rotácii, kde kód pridá zobrazovacie otočenie k stránke, ktorá už jedno nesie; strana problému na strane vieweru je rozpracovaná v fit miniatúry, zoome a dvojitej rotácii

var
  D: TPdfMatrixDecomposition;
begin
  if M.TryDecompose(D) then
  begin
    // D.ScaleX je vždy kladné; D.ScaleY nesie znamienko determinantu
    if D.IsReflected then
      Log('mirrored, ScaleY = %.3f', [D.ScaleY]);

    if Abs(D.RotationDegrees) > 0.5 then
      SkipDisplayRotation;      // objekt už obsahuje vlastné otočenie
  end
  else
    UseIdentityFallback;        // takmer singulárne alebo nekonečné: bez výsledku
end;

Vloženie jedného obdĺžnika do druhého bez hádania

TPdfMatrix.TryCreateRectMapping vám zostaví maticu zdroj-do-cieľa a berie TPdfMatrixFitMode z pmfStretch, pmfContain, alebo pmfCover. Najprv normalizuje oba obdĺžniky, pretože PDF obdĺžniky nemusia dorábať s left pod right alebo bottom pod top, potom odvodí nezávislé X a Y škály: pmfStretch ich ponechá nezávislé, pmfContain berie menšiu z nich a centruje letterbox, pmfCover berie väčšiu a centruje orez. Sprievodný MapRectToRect pripojí to isté mapovanie na existujúcu maticu, a NewRectMapping vyvolá EPdfMatrixError tam, kde forma Try vráti False. Toto je primitívum pod každým umiestnením bunky v N-up imposition a preusporadúvaní stránok, kde každá zdrojová stránka musí pristáť vnútri vypočítanej bunky bez toho, aby ste znovu odvodzovali aritmetiku pre každé rozloženie

Degenerované matice a poctivá cesta zlyhania

Konečné vstupy negarantujú konečný výsledok, takže fitovací kód počíta v Double a potom znovu skontroluje zúžený kandidát Single na konečnosť skôr, než ho publikuje; mapovanie obsahujúce nekonečno sa nikdy nevráti späť, akoby bolo platné. Rovnaká disciplína riadi inverziu. TPdfMatrix.TryGetInverse odmietne maticu použitím relatívneho prahu, porovnávajúc determinant voči epsilonu násobenému štvorcom najväčšieho lineárneho prvku namiesto voči pevnej konštante, čo je to, čo drží test zmysluplný bez ohľadu na to, či sú vaše jednotky body alebo mikrometre. TryDecompose odstúpi rovnakým spôsobom, odmietajúc, keď dĺžka prvého riadku alebo odvodené ScaleY padne na alebo pod epsilon

Vyberte si štýl zlyhania, ktorý sedí k miestu volania, namiesto zabaľovania všetkého do try-except zo zvyku. TryInvert, TryGetInverse, TryInverseTransformPoint, TryTransformBounds a TryCreateRectMapping vrátia False a ponechajú svoje ciele nedotknuté, čo sedí pre hit-testing a slučky per objekt, kde by sa degenerovaný objekt mal preskočiť, nie byť fatálny. Invert, InverseCopy, InverseTransformPoint, MapRectToRect a TransformBounds namiesto toho vyvolajú EPdfMatrixError, čo sedí pre setup kód, kde singulárna matica znamená, že volajúci niečo zle vypočítal. Pre dávkovú prácu TransformPoints a TransformRects alokujú svoje výsledné pole presne raz, TransformPointsInPlace a TransformRectsInPlace znovu použijú vaše úložisko, a TryTransformBounds akumuluje bounding box v jednom prechode namiesto materializácie transformovaných bodov najprv

Nič z tohto nie je exotická matematika. Je to jedna konvencia, aplikovaná konzistentne, s API pomenovaným tak, aby bola konvencia viditeľná na mieste volania: Multiply a obyčajné slovesá pripájajú, rodina Pre predsúva, rodina At obklopuje operáciu jej párom pivota. Napíšte poradie do komentára popri akejkoľvek kompozícii, ktorú staviate, pretože kód, ktorý sa dnes číta správne, je kód, ktorý niekto o šesť mesiacov obráti. Kompletná referencia TPdfMatrix, spolu s API page-objektov a renderovania, do ktorých tieto transformácie kŕmia, žije s PDFium Component pre Delphi a C++Builder