Technický článek

PDFium Matrix v Delphi: Prepend vs Append, pivot rotace

Afinní matice PDF používají konvenci řádkových vektorů podle ISO 32000-1 §8.3.3, kde se bod násobí maticí zleva: point' = point * M. V PDFium Component pro Delphi a C++Builder tento jediný fakt určuje celý povrch API třídy TPdfMatrix: Multiply přidává na konec, tedy M := M * Op, zatímco PreMultiply předsazuje na začátek, tedy M := Op * M

Každá klasická chyba v transformacích se dá vystopovat k tomu, že si tuto větu někdo zapamatoval obráceně. Vodoznak, který se v testovacím souboru pěkně otočí a v zákaznickém souboru skončí napůl mimo stránku. Náhled, který vyjde otočený dvakrát, protože stránka už čtvrt otáčky nesla. Razítko, jehož offset je perfektní na A4 a na Letter se rozjíždí. Nic z toho nejsou chyby vykreslování; jsou to chyby v pořadí násobení, a všechny se dají opravit ve chvíli, kdy dokážete nahlas říct, v jakém prostoru je zapsaná každá operace

Konvence řádkových vektorů, která určuje pravidla

TPdfMatrix ukládá šest prvků pojmenovaných podle specifikace a aplikuje je přesně tak, jak je definuje formát, takže samotná transformace je tam, kde úvahy začínají. TPdfMatrix.TransformPoint počítá x' = x*a + y*c + e a y' = x*b + y*d + f, což je šestiprvkový tvar, který ISO 32000-1 §8.3.4 definuje pro operátor cm, jenž zřetězuje matici do aktuální transformační matice. Dvojice (a, b) je první řádek, (c, d) druhý řádek a (e, f) je translační řádek. Zvyky se sloupcovými vektory z OpenGL nebo z kurzu lineární algebry vás tady svedou z cesty, a svedou vás potichu, protože matice se špatným pořadím je pořád naprosto platná matice. Přečtěte si složenou operaci v řádkové konvenci zleva doprava a pořadí aplikace vyplyne samo: protože point * (M * Op) se rovná (point * M) * Op, přidaná operace na konec působí na souřadnice, které už existující matice vytvořila, tedy v prostoru stránky, zatímco předsazená operace působí dřív, než existující matice proběhne, ve vlastním vstupním prostoru 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 má výchozí hodnotu ve směru hodinových ručiček a ve stupních, přičemž ACounterClockwise a AAngleInRadians jsou k dispozici, pokud jsou vaše zdrojová data znaménkově opačná. Vlastnosti af pouze pro čtení a vlastnost Handle vám vrátí surovou strukturu FS_MATRIX, což je přesně to, co chce FPDFPageObj_SetMatrix. Nic ve třídě před vámi těch šest čísel neskrývá, a to je záměr: když se transformace chová nesprávně, výpis af je nejrychlejší diagnóza, kterou máte

Proč potřebuje předsazený posun lineární část?

Protože předsazený posun je zapsaný ve vstupním prostoru matice a musí projít skrze aktuální lineární část dřív, než se může připojit k translačnímu řádku. TPdfMatrix.PreTranslate proto počítá e := dx*a + dy*c + e a f := dx*b + dy*d + f. Přidání na konec je snadný směr: TPdfMatrix.Translate je zapsaný v prostoru stránky, kde není co převádět, takže jen přičte dx k e a dy k f. Kdokoli si "optimalizuje" PreTranslate na dvě sčítání, právě smazal rotaci a měřítko 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;

Stejná asymetrie prochází i párem pro měřítko, a stojí za to vědět, kterých prvků se každý z nich dotýká dřív, než ho budete ladit ve tři ráno. TPdfMatrix.PreScale násobí řádky, škáluje a a b hodnotou scaleX a c a d hodnotou scaleY, a translaci nechává na pokoji, protože posun se už odehrál po proudu. Přidávající TPdfMatrix.Scale místo toho násobí sloupce, bere a, c, e hodnotou scaleX a b, d, f hodnotou scaleY, takže existující offset škáluje se vším ostatním. Obě jsou jednoúčelové cesty, které přeskočí obecný šestiprvkový součin, a obě zachovávají sémantiku skládání obecného tvaru přesně

Kam patří obě translace v pivotní rotaci?

Kolem operace, ne kolem celé matice, a v tomto pořadí. TPdfMatrix.RotateAt přidává na konec Translate(-pivot), poté rotaci, poté Translate(+pivot), což se pod konvencí řádkových vektorů skládá jako Translate(-pivot) * Op * Translate(pivot). Přesně tato posloupnost udržuje pivot pevný pod novou operací a přitom dovoluje existující matici vyprodukovat své souřadnice jako první a předat je dál. Napište tuto dvojici v opačném pořadí, jak by bylo správné ve sloupcové knihovně, a objekt obíhá kolem počátku místo toho, aby se točil na místě, což je přesně to, jak vycentrovaný vodoznak skončí mimo ořezový box

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;

Stejná kompozice stojí za ScaleAt, SkewAt, HorizontalFlipAt, VerticalFlipAt a CentralFlipAt, takže jakmile jednou důvěřujete tomuto vzoru u rotace, můžete mu důvěřovat i u zbytku. TPdfMatrix.CentralFlip stojí za to vyzdvihnout zvlášť: neguje všech šest prvků, aby vám dala otočku o 180 stupňů bez jediné trigonometrie, což znamená žádný cos hodnoty, která měla vyjít přesně nula, a žádný kumulující se drift, když ji aplikujete ve smyčce. Pokud umisťujete opakované značky místo otáčení jedné, mechanika samotného umístění je popsaná v znovupoužitelná razítka stránek s Form XObjects, a maticová práce zde na ní sedí přímo

Co vám TryDecompose řekne o matici?

TPdfMatrix.TryDecompose hlásí translaci, měřítko, rotaci, zkos, determinant a příznak zrcadlení pod konvencí nejdřív-měřítko-pak-rotace, a hlásí je poctivě natolik, aby byly použitelné pro rozhodování, ne jen pro logování. ScaleX pochází z délky prvního řádku, Sqrt(a*a + b*b), takže je vždy kladné. ScaleY je pak Determinant / ScaleX, což ho dělá znaménkovým. Rotace pochází z ArcTan2(-b, a) ve stupních, a zkos ze skalárního součinu obou řádků normalizovaného oběma měřítky

Právě to znaménko u ScaleY je to, co lidé mažou, a jeho smazání je skutečná chyba, ne kosmetická vada. Záporný determinant znamená, že matice obsahuje zrcadlení. Vynuťte oba faktory měřítka kladné, aby čísla vypadala úhledněji, a právě jste zahodili zrcadlení, takže matice zrekonstruovaná z dekompozice se vrátí zrcadlově obrácená: text čte pozpátku, naskenovaná stránka se překlopí, importované logo se dívá špatným směrem. Pole IsReflected existuje přesně proto, abyste to nikdy nemuseli odvozovat. Tohle je také kontrola, která brání klasické dvojité rotaci, kdy kód přidá zobrazovací otočku ke stránce, která už jednu nese; verzi tohoto problému na straně vieweru rozebírá přizpůsobení náhledu, zoom a dvojitá rotace

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;

Vejít se jedním obdélníkem do druhého bez hádání

TPdfMatrix.TryCreateRectMapping za vás postaví matici ze zdroje do cíle a přijímá TPdfMatrixFitMode s hodnotou pmfStretch, pmfContain nebo pmfCover. Nejdřív normalizuje oba obdélníky, protože obdélníky v PDF nemusí dorazit s levou hranou pod pravou nebo spodní pod horní, pak odvodí nezávislá měřítka X a Y: pmfStretch je nechává nezávislá, pmfContain vezme menší z nich a vycentruje letterbox, pmfCover vezme větší z nich a vycentruje ořez. Doprovodná metoda MapRectToRect přidává stejné mapování na konec existující matice a NewRectMapping vyvolá EPdfMatrixError tam, kde by forma Try vrátila False. Tohle je primitivum pod každým umístěním buňky v N-up imposition a přeuspořádání stránek, kde se každá zdrojová stránka musí vejít do vypočítané buňky, aniž byste tu aritmetiku znovu odvozovali pro každé rozvržení

Degenerované matice a poctivá cesta selhání

Konečné vstupy nezaručují konečný výsledek, takže kód pro přizpůsobení počítá v Double a poté znovu kontroluje zúžený kandidát Single na konečnost, než ho zveřejní; mapování obsahující nekonečno se nikdy nevrátí, jako by bylo platné. Stejná disciplína řídí i inverzi. TPdfMatrix.TryGetInverse zamítne matici pomocí relativního prahu, porovnává determinant proti epsilon vynásobenému druhou mocninou největšího lineárního prvku místo proti pevné konstantě, což udržuje test smysluplný bez ohledu na to, zda jsou vaše jednotky body nebo mikrometry. TryDecompose se vzdává stejným způsobem, odmítá, když délka prvního řádku nebo odvozené ScaleY klesne na hodnotu epsilon nebo pod ni

Zvolte styl selhání, který se hodí na dané místo volání, místo abyste ze zvyku všechno balili do try-except. TryInvert, TryGetInverse, TryInverseTransformPoint, TryTransformBounds a TryCreateRectMapping vrátí False a nechají své cíle nedotčené, což se hodí pro hit-testing a smyčky přes jednotlivé objekty, kde se má degenerovaný objekt přeskočit, ne způsobit pád. Invert, InverseCopy, InverseTransformPoint, MapRectToRect a TransformBounds místo toho vyvolají EPdfMatrixError, což se hodí pro inicializační kód, kde singulární matice znamená, že volající spočítal něco špatně. Pro dávkovou práci TransformPoints a TransformRects alokují své výsledné pole přesně jednou, TransformPointsInPlace a TransformRectsInPlace znovu použijí vaše úložiště, a TryTransformBounds akumuluje ohraničující box v jediném průchodu místo materializace transformovaných bodů předem

Nic z tohohle není exotická matematika. Je to jedna konvence, aplikovaná důsledně, s API pojmenovaným tak, aby byla konvence vidět přímo na místě volání: Multiply a obyčejná slovesa přidávají na konec, rodina Pre předsazuje na začátek, rodina At obklopuje operaci jejím pivotním párem. Napište pořadí do komentáře vedle jakékoli složené operace, kterou stavíte, protože kód, který se dnes čte správně, je kód, který za šest měsíců někdo obrátí. Kompletní referenci TPdfMatrix, spolu s API pro objekty stránky a vykreslování, do kterých tyto transformace vstupují, najdete u PDFium Component pro Delphi a C++Builder