Tehnični članak

PDFium matrika Prepend proti Append v Delphiju: vrtenje okoli pivota

Afine matrike PDF uporabljajo konvencijo vrstičnih vektorjev ISO 32000-1 §8.3.3, kjer točka matriko množi z leve: point' = point * M. V PDFium Component za Delphi in C++Builder to eno dejstvo fiksira celotno API površino TPdfMatrix: Multiply dodaja na konec, tako da M := M * Op, medtem ko PreMultiply dodaja na začetek, tako da M := Op * M

Vsak klasičen hrošč transformacije se izsledi nazaj do tega, da je bil ta stavek zapomnjen obratno. Vodni žig, ki se v vaši testni datoteki lepo zavrti in v datoteki stranke pristane napol zunaj strani. Sličica, ki izpade dvakrat zavrtena, ker je stran že nosila četrt obrata. Žig, čigar odmik je popoln na A4 in odnosi na Letter. Nič od tega niso hrošči upodabljanja; so hrošči vrstnega reda množenja, vsi pa so popravljivi, ko lahko na glas poveste, v katerem prostoru je zapisana vsaka operacija

Konvencija vrstičnih vektorjev, ki postavi pravila

TPdfMatrix shrani šest elementov, poimenovanih po specifikaciji, in jih uporabi natanko tako, kot jih format definira, tako da je sama transformacija tam, kjer se razmišljanje začne. TPdfMatrix.TransformPoint izračuna x' = x*a + y*c + e in y' = x*b + y*d + f, kar je oblika s šestimi elementi, ki jo ISO 32000-1 §8.3.4 definira za operator cm, ki matriko pripne na trenutno transformacijsko matriko. Par (a, b) je prva vrstica, (c, d) druga, (e, f) pa vrstica prevoda. Navade stolpčnih vektorjev, pridobljene iz OpenGL ali tečaja linearne algebre, vas bodo tu zavedle, in to tiho, ker je matrika z napačnim vrstnim redom še vedno popolnoma veljavna matrika. Berite sestavljeno operacijo v konvenciji vrstic z leve proti desni in vrstni red uporabe pade ven zastonj: ker je point * (M * Op) enako (point * M) * Op, dodana operacija deluje na koordinate, ki jih je obstoječa matrika že proizvedla, torej na prostor strani, medtem ko operacija, dodana na začetek, deluje pred tekom obstoječe matrike, v lastnem vhodnem prostoru objekta

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 privzeto v smeri urinega kazalca in v stopinjah, ACounterClockwise in AAngleInRadians pa sta na voljo, kadar so vaši izvorni podatki podpisani obratno. Lastnosti a do f, samo za branje, in lastnost Handle vam vrneta surov FS_MATRIX, kar je tisto, kar hoče FPDFPageObj_SetMatrix. Nič v razredu ne skriva šestih števil pred vami, kar je namerno: ko se transformacija napačno obnaša, je izpis a do f najhitrejša diagnoza, ki jo imate

Zakaj dodajanje prevoda na začetek potrebuje linearni del?

Ker je premik, dodan na začetek, zapisan v vhodnem prostoru matrike, ga je treba prenesti skozi trenutni linearni del, preden se lahko pridruži vrstici prevoda. TPdfMatrix.PreTranslate torej izračuna e := dx*a + dy*c + e in f := dx*b + dy*d + f. Dodajanje na konec je lažja smer: TPdfMatrix.Translate je zapisan v prostoru strani, kjer ni treba ničesar pretvarjati, zato le doda dx k e in dy k f. Kdorkoli "optimizira" PreTranslate navzdol na dve seštevanji, je iz premika pravkar izbrisal rotacijo in skalo

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;

Ista asimetrija teče skozi par skaliranja, vredno pa je vedeti, katerih elementov se vsak dotakne, preden ob treh zjutraj razhroščujete enega od njiju. TPdfMatrix.PreScale množi vrstice, skalira a in b za scaleX ter c in d za scaleY, prevod pa pusti pri miru, ker se je premik že zgodil naprej po verigi. Dodajajoči TPdfMatrix.Scale namesto tega množi stolpce, pri čemer vzame a, c, e za scaleX in b, d, f za scaleY, tako da se obstoječi odmik skalira z vsem drugim. Obe sta enonamenski poti, ki preskočita splošen produkt šestih elementov, obe pa natanko ohranita semantiko sestave splošne oblike

Kam gresta dva prevoda v vrtenju okoli pivota?

Okoli operacije, ne okoli cele matrike, in v tem vrstnem redu. TPdfMatrix.RotateAt doda Translate(-pivot), nato rotacijo, nato Translate(+pivot), kar se pod konvencijo vrstičnih vektorjev sestavi kot Translate(-pivot) * Op * Translate(pivot). To zaporedje je tisto, kar drži pivot fiksen pod novo operacijo, medtem ko obstoječi matriki še vedno dovoli, da najprej proizvede svoje koordinate in jih preda naprej. Zapišite par v obratnem vrstnem redu, kot bi bilo pravilno v knjižnici stolpčnih vektorjev, in objekt kroži okoli izhodišča namesto da se vrti na mestu, kar je natanko način, kako centriran vodni žig konča zunaj obreznega okvirja

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;

Ista sestava podpira ScaleAt, SkewAt, HorizontalFlipAt, VerticalFlipAt in CentralFlipAt, tako da lahko, ko enkrat zaupate vzorcu za rotacijo, zaupate tudi preostalim. TPdfMatrix.CentralFlip je vredno posebej izpostaviti: negira vseh šest elementov, da vam da obrat za 180 stopinj brez kakršne koli trigonometrije, kar pomeni brez cos vrednosti, ki bi morala biti natanko nič, in brez kopičečega se drseža, kadar jo uporabite v zanki. Če postavljate ponovljene oznake namesto da bi eno obračali, je mehanika same postavitve obravnavana v ponovno uporabnih žigih strani s Form XObjects, delo z matriko tukaj pa sedi neposredno na njem

Kaj vam TryDecompose pove o matriki?

TPdfMatrix.TryDecompose poroča prevod, skalo, rotacijo, strig, determinanto in zastavico odseva pod konvencijo skala-nato-rotacija, poroča pa jih dovolj pošteno, da so uporabni za odločitve, ne le za beleženje. ScaleX izhaja iz dolžine prve vrstice, Sqrt(a*a + b*b), zato je vedno pozitiven. ScaleY je nato Determinant / ScaleX, kar ga naredi s predznakom. Rotacija izhaja iz ArcTan2(-b, a) v stopinjah, strig pa iz skalarnega produkta obeh vrstic, normaliziranega z obema skalama

Ta predznak na ScaleY je del, ki ga ljudje izbrišejo, izbris pa je resničen hrošč, ne pa kozmetičen. Negativna determinanta pomeni, da matrika vsebuje odsev. Prisilite oba faktorja skale, da sta pozitivna, da bi bila števila videti bolj urejena, in ste odsev zavrgli, tako da matrika, obnovljena iz razčlenitve, pride nazaj zrcaljena: besedilo se bere obratno, skenirana stran se obrne, uvožen logotip gleda v napačno smer. Polje IsReflected obstaja, da vam tega nikoli ni treba izpeljevati. To je tudi preverjanje, ki prepreči klasično dvojno rotacijo, kjer koda doda prikazni obrat strani, ki ga že nosi; različica te težave na strani pregledovalnika je razdelana v prilagajanju sličic, povečavi in dvojni rotaciji

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;

Prileganje enega pravokotnika v drugega brez ugibanja

TPdfMatrix.TryCreateRectMapping zgradi matriko izvor-v-cilj namesto vas in sprejme TPdfMatrixFitMode pmfStretch, pmfContain ali pmfCover. Najprej normalizira oba pravokotnika, ker pravokotniki PDF niso zahtevani, da pridejo z levo pod desno ali spodaj pod zgoraj, nato pa izpelje neodvisni skali X in Y: pmfStretch ju obdrži neodvisni, pmfContain vzame manjšo in centrira letterbox, pmfCover vzame večjo in centrira obreza. Spremljajoči MapRectToRect isto preslikavo doda na obstoječo matriko, NewRectMapping pa vrže EPdfMatrixError, kjer oblika Try vrne False. To je primitiv pod vsako postavitvijo celice v N-up razporeditvi in prerazvrščanju strani, kjer mora vsaka izvorna stran pristati znotraj izračunane celice, ne da bi za vsako postavitev na novo izpeljali aritmetiko

Degenerirane matrike in poštena pot odpovedi

Končni vhodi ne zagotavljajo končnega rezultata, zato koda prileganja računa v Double in nato ponovno preveri zoženega kandidata Single glede končnosti, preden ga objavi; preslikava, ki vsebuje neskončnost, nikoli ni vrnjena, kot da bi bila veljavna. Ista disciplina upravlja inverzijo. TPdfMatrix.TryGetInverse zavrne matriko z uporabo relativnega praga, tako da determinanto primerja z epsilonom, pomnoženim s kvadratom največjega linearnega elementa, namesto s fiksno konstanto, kar je tisto, kar test drži smiseln, ne glede na to, ali so vaše enote pike ali mikrometri. TryDecompose odstopi na isti način in zavrne, kadar dolžina prve vrstice ali izpeljan ScaleY pade na ali pod epsilon

Izberite slog odpovedi, ki ustreza mestu klica, namesto da iz navade vse zavijete v try-except. TryInvert, TryGetInverse, TryInverseTransformPoint, TryTransformBounds in TryCreateRectMapping vrnejo False in svoje cilje pustijo nedotaknjene, kar ustreza testiranju zadetkov in zankam po objektih, kjer je treba degeneriran objekt preskočiti, ne pa fatalno prekiniti. Invert, InverseCopy, InverseTransformPoint, MapRectToRect in TransformBounds namesto tega vržejo EPdfMatrixError, kar ustreza nastavitveni kodi, kjer singularna matrika pomeni, da je klicatelj nekaj napačno izračunal. Za paketno delo TransformPoints in TransformRects dodelita svoje polje rezultata natanko enkrat, TransformPointsInPlace in TransformRectsInPlace ponovno uporabita vaše shranjevanje, TryTransformBounds pa v enem prehodu akumulira omejitveni okvir namesto da bi najprej materializirala transformirane točke

Nič od tega ni eksotična matematika. Je ena konvencija, dosledno uporabljena, z API-jem, poimenovanim tako, da je konvencija vidna na mestu klica: Multiply in navadni glagoli dodajajo na konec, družina Pre dodaja na začetek, družina At operacijo oklepa s svojim parom pivota. Vrstni red zapišite v komentar zraven vsake sestavljene operacije, ki jo zgradite, ker je koda, ki se danes bere pravilno, koda, ki jo bo nekdo obrnil čez šest mesecev. Poln referenčni opis TPdfMatrix, skupaj z API-ji objektov strani in upodabljanja, ki jih te transformacije hranijo, živi pri PDFium Component za Delphi in C++Builder