Teknisk artikkel

PDFium matrise prepend vs append i Delphi: pivot-rotasjon

PDF affine matriser bruker radvektor-konvensjonen i ISO 32000-1 §8.3.3, hvor et punkt multipliserer matrisen fra venstre: point' = point * M. I PDFium Component for Delphi og C++Builder fester det ene faktumet hele API-overflaten til TPdfMatrix: Multiply setter til slutt, så M := M * Op, mens PreMultiply setter foran, så M := Op * M

Hver klassisk transformasjonsfeil spores tilbake til at den setningen huskes baklengs. Vannmerket som roterer pent i testfilen din og lander halvveis utenfor siden i kundefilen. Miniatyren som kommer ut rotert to ganger fordi siden allerede bar en kvart omdreining. Stempelet hvis forskyvning er perfekt på A4 og driver på Letter. Ingen av disse er renderingsfeil; de er multiplikasjonsrekkefølge-feil, og de er alle fiksbare så snart du kan si høyt hvilket rom hver operasjon er skrevet i

Radvektor-konvensjonen som setter reglene

TPdfMatrix lagrer de seks spesifikasjonsnavngitte elementene og anvender dem akkurat slik formatet definerer dem, så selve transformasjonen er der resonnementet starter. TPdfMatrix.TransformPoint beregner x' = x*a + y*c + e og y' = x*b + y*d + f, som er den seks-elements formen ISO 32000-1 §8.3.4 definerer for cm-operatoren som slår sammen en matrise inn i den gjeldende transformasjonsmatrisen. Paret (a, b) er den første raden, (c, d) den andre, og (e, f) forskyvningsraden. Kolonnevektor-vaner plukket opp fra OpenGL eller fra et lineær algebra-kurs vil villede deg her, og de vil villede deg stille, fordi en matrise i feil rekkefølge fortsatt er en fullstendig gyldig matrise. Les en sammensetning i radkonvensjonen fra venstre til høyre og anvendelsesrekkefølgen faller ut gratis: siden point * (M * Op) er lik (point * M) * Op, virker en tilføyd operasjon på koordinater den eksisterende matrisen allerede har produsert, altså på siderom, mens en forhåndssatt operasjon virker før den eksisterende matrisen kjører, i objektets eget inputrom

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 er som standard med klokken og i grader, med ACounterClockwise og AAngleInRadians tilgjengelig når kildedataene dine er fortegnet den andre veien. De skrivebeskyttede a-til-f-egenskapene og Handle-egenskapen gir deg den rå FS_MATRIX-en tilbake, som er det FPDFPageObj_SetMatrix vil ha. Ingenting i klassen skjuler de seks tallene fra deg, og det er bevisst: når en transformasjon oppfører seg dårlig, er å skrive ut a til f den raskeste diagnosen du har

Hvorfor trenger å sette en forskyvning foran den lineære delen?

Fordi et forhåndssatt skift er skrevet i matrisens inputrom, og det må føres gjennom den gjeldende lineære delen før det kan slutte seg til forskyvningsraden. TPdfMatrix.PreTranslate beregner derfor e := dx*a + dy*c + e og f := dx*b + dy*d + f. Å tilføye er den enkle retningen: TPdfMatrix.Translate er skrevet i siderom, hvor ingenting trenger konvertering, så den bare legger dx til e og dy til f. Enhver som «optimaliserer» PreTranslate ned til to addisjoner har nettopp slettet rotasjonen og skaleringen fra skiftet

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;

Samme asymmetri går gjennom skaleringsparet, og det er verdt å vite hvilke elementer hver berører før du feilsøker en klokken tre om natten. TPdfMatrix.PreScale multipliserer rader, skalerer a og b med scaleX og c og d med scaleY, og den lar forskyvningen være i fred fordi skiftet allerede skjedde nedstrøms. Den tilføyende TPdfMatrix.Scale multipliserer kolonner i stedet, og tar a, c, e med scaleX og b, d, f med scaleY, så den eksisterende forskyvningen skaleres med alt annet. Begge er enkeltformåls-veier som hopper over det generelle seks-elements produktet, og begge bevarer sammensetningssemantikken til den generelle formen nøyaktig

Hvor går de to forskyvningene i en pivot-rotasjon?

Rundt operasjonen, ikke rundt hele matrisen, og i den rekkefølgen. TPdfMatrix.RotateAt tilføyer Translate(-pivot), deretter rotasjonen, deretter Translate(+pivot), som under radvektor-konvensjonen setter seg sammen som Translate(-pivot) * Op * Translate(pivot). Den sekvensen er det som holder pivotet fast under den nye operasjonen mens den fortsatt lar den eksisterende matrisen produsere koordinatene sine først og gi dem videre. Skriv paret motsatt vei, som det ville vært korrekt i et kolonnevektor-bibliotek, og objektet kretser rundt origo i stedet for å spinne på stedet, som er akkurat hvordan et sentrert vannmerke ender opp utenfor beskjæringsboksen

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;

Samme sammensetning støtter ScaleAt, SkewAt, HorizontalFlipAt, VerticalFlipAt, og CentralFlipAt, så når du først stoler på mønsteret for rotasjon kan du stole på det for resten. TPdfMatrix.CentralFlip er verdt å trekke frem spesielt: den negerer alle seks elementer for å gi deg en 180-graders vending uten noen trigonometri i det hele tatt, som betyr ingen cos av en verdi som skulle vært nøyaktig null og ingen akkumulerende drift når du anvender den i en løkke. Hvis du plasserer gjentatte merker fremfor å snu ett, er mekanikken til selve plasseringen dekket i gjenbrukbare sidestempler med Form XObjects, og matrisearbeidet her sitter rett oppå det

Hva forteller TryDecompose deg om en matrise?

TPdfMatrix.TryDecompose rapporterer forskyvning, skala, rotasjon, skjæring, determinant og et refleksjonsflagg under en skaler-så-roter-konvensjon, og den rapporterer dem ærlig nok til å være nyttig for beslutninger fremfor bare for logging. ScaleX kommer fra lengden på den første raden, Sqrt(a*a + b*b), så den er alltid positiv. ScaleY er deretter Determinant / ScaleX, som gjør den fortegnet. Rotasjon kommer fra ArcTan2(-b, a) i grader, og skjæring fra de to radenes skalarprodukt normalisert av begge skalaene

Det fortegnet på ScaleY er delen folk sletter, og å slette det er en ekte feil fremfor en kosmetisk en. En negativ determinant betyr at matrisen inneholder en refleksjon. Tving begge skaleringsfaktorene positive for å få tallene til å se ryddigere ut, og du har kastet bort refleksjonen, så en matrise gjenoppbygget fra dekomponeringen kommer tilbake speilvendt: tekst leses baklengs, en skannet side snur, en importert logo vender feil vei. IsReflected-feltet finnes slik at du aldri trenger å utlede det. Dette er også sjekken som forhindrer den klassiske dobbel-rotasjonen, hvor kode legger en visningsvending til en side som allerede bærer en; visning-side-versjonen av det problemet gjennomgås i miniatyr-tilpassing, zoom, og dobbel rotasjon

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;

Å tilpasse ett rektangel inn i et annet uten å gjette

TPdfMatrix.TryCreateRectMapping bygger kilde-til-mål-matrisen for deg og tar en TPdfMatrixFitModepmfStretch, pmfContain, eller pmfCover. Den normaliserer begge rektanglene først, fordi PDF-rektangler ikke er påkrevd å ankomme med venstre under høyre eller bunn under topp, og utleder deretter uavhengige X- og Y-skalaer: pmfStretch holder dem uavhengige, pmfContain tar den mindre og sentrerer letterboxen, pmfCover tar den større og sentrerer beskjæringen. Følgesvennen MapRectToRect tilføyer samme mapping til en eksisterende matrise, og NewRectMapping kaster EPdfMatrixError der Try-formen returnerer False. Dette er primitivet under hver celleplassering i N-up-imposisjon og sideomorganisering, hvor hver kildeside må lande inne i en beregnet celle uten at du utleder aritmetikken på nytt per layout

Degenererte matriser og den ærlige feilveien

Endelige input garanterer ikke et endelig resultat, så tilpasningskoden beregner i Double og sjekker deretter den innsnevrede Single-kandidaten for endelighet på nytt før den publiserer den; en mapping som inneholder en uendelighet gis aldri tilbake som om den var gyldig. Samme disiplin styrer inversjon. TPdfMatrix.TryGetInverse avviser en matrise ved bruk av en relativ terskel, og sammenligner determinanten mot epsilon multiplisert med kvadratet av det største lineære elementet fremfor mot en fast konstant, som er det som holder testen meningsfull uansett om enhetene dine er punkter eller mikrometer. TryDecompose trekker seg ut på samme måte, og nekter når den første radens lengde eller den utledede ScaleY faller på eller under epsilon

Velg feilstilen som passer kallstedet fremfor å pakke alt inn i try-except av gammel vane. TryInvert, TryGetInverse, TryInverseTransformPoint, TryTransformBounds og TryCreateRectMapping returnerer False og lar målene sine stå urørt, som passer treff-testing og per-objekt-løkker hvor et degenerert objekt bør hoppes over, ikke være fatalt. Invert, InverseCopy, InverseTransformPoint, MapRectToRect og TransformBounds kaster i stedet EPdfMatrixError, som passer oppsettskode hvor en singulær matrise betyr at kalleren beregnet noe feil. For batch-arbeid allokerer TransformPoints og TransformRects resultat-arrayen sin nøyaktig én gang, TransformPointsInPlace og TransformRectsInPlace gjenbruker lagringen din, og TryTransformBounds akkumulerer den omsluttende boksen i én gjennomgang fremfor å materialisere transformerte punkter først

Ingenting av dette er eksotisk matematikk. Det er én konvensjon, anvendt konsekvent, med API-en navngitt slik at konvensjonen er synlig ved kallstedet: Multiply og de vanlige verbene tilføyer, Pre-familien setter foran, At-familien klammer operasjonen med sitt pivotpar. Skriv rekkefølgen ned i en kommentar ved siden av enhver sammensetning du bygger, fordi koden som leses korrekt i dag er koden noen reverserer om seks måneder. Den fullstendige TPdfMatrix-referansen, sammen med side-objekt- og rendering-API-ene disse transformasjonene mater inn i, bor med PDFium Component for Delphi og C++Builder