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

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
    // 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 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 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;

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

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

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 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;

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