Műszaki cikk

PDFium mátrix prepend vs append Delphiben: pivot forgatás

A PDF affin mátrixok az ISO 32000-1 §8.3.3 sorvektor-konvencióját használják, ahol egy pont balról szorozza meg a mátrixot: point' = point * M. A PDFium Component-ben Delphihez és C++Builderhez ez az egyetlen tény rögzíti a TPdfMatrix teljes API-felszínét: a Multiply hozzáfűz, így M := M * Op, míg a PreMultiply elé illeszt, így M := Op * M

Minden klasszikus transzformáció-hiba visszavezethető arra, hogy ezt a mondatot fordítva jegyezték meg. A vízjel, amely szépen elfordul a tesztfájlodban, és félig az oldalon kívül landol az ügyfél fájljában. A miniatűr, amely kétszer elforgatva jön ki, mert az oldal már hordozott egy negyed fordulatot. A pecsét, amelynek eltolása tökéletes A4-en, és elcsúszik Letteren. Egyik sem renderelési hiba; szorzási-sorrend hiba, és mind javítható, ha egyszer ki tudod mondani hangosan, melyik térben van írva minden művelet

A sorvektor-konvenció, amely rögzíti a szabályokat

A TPdfMatrix tárolja a hat specifikáció-nevezte elemet, és pontosan úgy alkalmazza őket, ahogy a formátum definiálja, így maga a transzformáció az, ahol az érvelés kezdődik. A TPdfMatrix.TransformPoint kiszámítja az x' = x*a + y*c + e-t és az y' = x*b + y*d + f-et, ami a hatelemes forma, amelyet az ISO 32000-1 §8.3.4 definiál a cm operátorhoz, amely egy mátrixot konkatenál az aktuális transzformációs mátrixra. Az (a, b) pár az első sor, a (c, d) a második, az (e, f) pedig az eltolási sor. Az OpenGL-ből vagy egy lineáris algebra kurzusból felszedett oszlopvektor-szokások félrevezetnek itt, és csendben vezetnek félre, mert egy rossz-sorrendű mátrix még mindig tökéletesen érvényes mátrix. Olvass egy kompozitot a sorkonvencióban balról jobbra, és az alkalmazás sorrendje ingyen kiesik: mivel a point * (M * Op) egyenlő (point * M) * Op-vel, egy hozzáfűzött művelet olyan koordinátákon hat, amelyeket a meglévő mátrix már előállított, azaz oldaltéren, míg egy elé-illesztett művelet a meglévő mátrix futása előtt hat, az objektum saját bemeneti terében

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;

A TPdfMatrix.Rotate alapértelmezésben óramutató-irányú és fokban van, az ACounterClockwise és AAngleInRadians elérhető, amikor a forrásadatod máshogy van jelölve. A csak-olvasható a-tól f-ig terjedő tulajdonságok és a Handle tulajdonság visszaadja neked a nyers FS_MATRIX-ot, ami az, amit az FPDFPageObj_SetMatrix akar. Semmi az osztályban nem rejti el előled a hat számot, ez szándékos: amikor egy transzformáció rosszul viselkedik, az a-tól f-ig terjedő kiírás a leggyorsabb diagnózis, amivel rendelkezel

Miért van szüksége egy eltolás elé-illesztésének a lineáris részre?

Mert egy elé-illesztett eltolás a mátrix bemeneti terében van írva, és át kell vinni a jelenlegi lineáris részen, mielőtt csatlakozhatna az eltolási sorhoz. A TPdfMatrix.PreTranslate ezért kiszámítja az e := dx*a + dy*c + e-t és az f := dx*b + dy*d + f-et. A hozzáfűzés a könnyű irány: a TPdfMatrix.Translate oldaltérben van írva, ahol semmit nem kell konvertálni, így csak hozzáadja a dx-et az e-hez és a dy-t az f-hez. Bárki, aki "optimalizálja" a PreTranslate-et két összeadásra, éppen most törölte a forgatást és a skálázást az eltolásból

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;

Ugyanez az aszimmetria fut végig a skálázási páron, és érdemes tudni, mely elemekhez ér mindkettő, mielőtt hajnali háromkor debugolnád az egyiket. A TPdfMatrix.PreScale sorokat szoroz, az a-t és b-t scaleX-szel, a c-t és d-t scaleY-nal skálázva, és érintetlenül hagyja az eltolást, mert az eltolás már downstream megtörtént. A hozzáfűző TPdfMatrix.Scale ehelyett oszlopokat szoroz, az a, c, e-t scaleX-szel, a b, d, f-et scaleY-nal véve, így a meglévő eltolás mindennel együtt skálázódik. Mindkettő egyetlen-célú útvonal, amely kihagyja az általános hatelemes szorzatot, és mindkettő pontosan megőrzi az általános forma kompozíciós szemantikáját

Hova kerül a két eltolás egy pivot-forgatásban?

A művelet köré, nem az egész mátrix köré, és ebben a sorrendben. A TPdfMatrix.RotateAt hozzáfűzi a Translate(-pivot)-ot, majd a forgatást, majd a Translate(+pivot)-ot, ami a sorvektor-konvenció alatt Translate(-pivot) * Op * Translate(pivot)-ként komponálódik. Ez a szekvencia az, ami fixen tartja a pivotot az új művelet alatt, miközben még mindig hagyja, hogy a meglévő mátrix előbb állítsa elő a koordinátáit, és adja tovább. Írd a párt fordítva, ahogy egy oszlopvektoros könyvtárban helyes lenne, és az objektum az origó körül keringene ahelyett, hogy a helyén forogna, ami pontosan az, ahogyan egy középre helyezett vízjel a vágódobozon kívül végzi

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;

Ugyanez a kompozíció áll a ScaleAt, SkewAt, HorizontalFlipAt, VerticalFlipAt és CentralFlipAt mögött, így ha egyszer megbízol a mintában a forgatáshoz, megbízhatsz benne a többihez is. A TPdfMatrix.CentralFlip érdemes egyedül kiemelni: negálja mind a hat elemet, hogy 180 fokos fordulatot adjon minden trigonometria nélkül, ami azt jelenti, semmi cos egy olyan értékből, aminek pontosan nullának kellett volna lennie, és semmilyen felhalmozódó eltérés, amikor egy ciklusban alkalmazod. Ha ismétlődő jeleket helyezel el egy megfordítás helyett, magának az elhelyezésnek a mechanikáját az újrafelhasználható oldalpecsétek Form XObjectekkel tárgyalja, és az itt bemutatott mátrix-munka közvetlenül erre épül

Mit mond el a TryDecompose egy mátrixról?

A TPdfMatrix.TryDecompose jelenti az eltolást, skálát, forgatást, nyírást, determinánst és egy tükrözés-jelzőt egy skálázás-majd-forgatás konvenció alatt, és elég őszintén jelenti őket ahhoz, hogy hasznosak legyenek döntésekhez, nem csak naplózáshoz. A ScaleX az első sor hosszából származik, Sqrt(a*a + b*b), így mindig pozitív. A ScaleY ekkor Determinant / ScaleX, ami előjelessé teszi. A forgatás az ArcTan2(-b, a)-ból származik fokban, a nyírás pedig a két sor skalárszorzatából, mindkét skálával normalizálva

Ez az előjel a ScaleY-on az, amit az emberek törölnek, és a törlése valódi hiba, nem kozmetikai. Egy negatív determináns azt jelenti, hogy a mátrix tükrözést tartalmaz. Kényszerítsd mindkét skálafaktort pozitívra, hogy a számok rendezettebbnek tűnjenek, és eldobtad a tükrözést, így egy a dekompozícióból újraépített mátrix tükrözötten jön vissza: a szöveg visszafelé olvasható, egy szkennelt oldal megfordul, egy importált logó rossz irányba néz. Az IsReflected mező azért létezik, hogy sosem kelljen kikövetkeztetned. Ez az az ellenőrzés is, amely megelőzi a klasszikus dupla forgatást, ahol a kód hozzáad egy megjelenítési fordulatot egy oldalhoz, amely már hordoz egyet; ennek a problémának a nézet-oldali verzióját a a miniatűr illesztés, zoom és dupla forgatás dolgozza fel

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;

Egy téglalap illesztése egy másikba tippelés nélkül

A TPdfMatrix.TryCreateRectMapping felépíti neked a forrás-céldest mátrixot, és egy TPdfMatrixFitMode-ot vesz át pmfStretch, pmfContain vagy pmfCover értékkel. Először normalizálja mindkét téglalapot, mert a PDF téglalapoknak nem kötelező úgy érkezniük, hogy a bal a jobb alatt, vagy az alsó a felső alatt van, majd független X és Y skálákat vezet le: a pmfStretch függetlenül tartja őket, a pmfContain a kisebbet veszi, és középre igazítja a letterboxot, a pmfCover a nagyobbat veszi, és középre igazítja a vágást. A kísérő MapRectToRect ugyanazt a leképezést fűzi hozzá egy meglévő mátrixhoz, és a NewRectMapping EPdfMatrixError-t dob ott, ahol a Try forma False-t ad vissza. Ez a primitíva minden cellaelhelyezés alatt az N-up imponálásban és oldal-átrendezésben, ahol minden forrás-oldalnak egy kiszámított cellába kell landolnia anélkül, hogy újra levezetnéd az aritmetikát elrendezésenként

Elfajult mátrixok és az őszinte hiba-útvonal

Véges bemenetek nem garantálnak véges eredményt, így az illesztő kód Double-ban számol, majd újra ellenőrzi a szűkített Single jelöltet végesség szempontjából, mielőtt közzétenné; egy végtelent tartalmazó leképezés sosem kerül visszaadásra úgy, mintha érvényes lenne. Ugyanez a fegyelem irányítja az inverziót. A TPdfMatrix.TryGetInverse egy relatív küszöbérték segítségével utasít el egy mátrixot, összehasonlítva a determinánst az epsilon-nal szorzott legnagyobb lineáris elem négyzetével, ahelyett hogy egy fix konstanshoz hasonlítaná, ami az, ami értelmessé teszi a tesztet, akár pontokban, akár mikrométerben mérsz. A TryDecompose ugyanígy visszalép, elutasítva, amikor az első sor hossza vagy a levezetett ScaleY az epsilonnál vagy alatta esik

Válaszd a hívási helyhez illő hiba-stílust ahelyett, hogy megszokásból mindent try-except-be csomagolnál. A TryInvert, TryGetInverse, TryInverseTransformPoint, TryTransformBounds és TryCreateRectMapping False-t ad vissza, és érintetlenül hagyja a célértékeit, ami illik a hit-testinghez és per-objektum ciklusokhoz, ahol egy elfajult objektumot ki kell hagyni, nem halálosnak lenni. Az Invert, InverseCopy, InverseTransformPoint, MapRectToRect és TransformBounds ehelyett EPdfMatrixError-t dob, ami illik a beállító kódhoz, ahol egy szinguláris mátrix azt jelenti, a hívó valamit rosszul számított. Batch munkánál a TransformPoints és TransformRects pontosan egyszer allokálja az eredménytömbjét, a TransformPointsInPlace és TransformRectsInPlace újrahasznosítja a saját tárolásodat, és a TryTransformBounds egyetlen áthaladásban halmozza fel a határoló dobozt, ahelyett hogy előbb anyagosítaná a transzformált pontokat

Ebben semmi nem egzotikus matematika. Egy konvenció, konzisztensen alkalmazva, az API elnevezve úgy, hogy a konvenció látható a hívási helyen: a Multiply és a sima igék hozzáfűznek, a Pre család elé illeszt, az At család a pivot-párjával zárójelezi a műveletet. Írd le a sorrendet egy kommentben bármely kompozit mellett, amit építesz, mert a kód, amely ma helyesen olvasható, az a kód, amelyet valaki hat hónap múlva visszafejt. A teljes TPdfMatrix-referencia, az oldalobjektum- és renderelő-API-k mellett, amelyeket ezek a transzformációk táplálnak, a PDFium Component-ben él Delphihez és C++Builderhez