As matrizes afim do PDF usam a convenção de vetor-linha da ISO 32000-1 §8.3.3, em que um ponto multiplica a matriz pela esquerda: point' = point * M. No PDFium Component para Delphi e C++Builder, esse único facto fixa toda a superfície da API de TPdfMatrix: Multiply anexa, logo M := M * Op, enquanto PreMultiply prefixa, logo M := Op * M
Todo o bug clássico de transformação remonta a essa frase ser lembrada ao contrário. A marca de água que roda perfeitamente no ficheiro de teste e cai meio fora da página no ficheiro do cliente. A miniatura que sai rodada duas vezes porque a página já trazia um quarto de volta. O carimbo cujo deslocamento é perfeito em A4 e vai desviando em Letter. Nenhum destes é um bug de renderização; são bugs de ordem de multiplicação, e todos são corrigíveis assim que se consiga dizer em voz alta em que espaço cada operação está escrita
A convenção de vetor-linha que fixa as regras
TPdfMatrix armazena os seis elementos com nome da especificação e aplica-os exatamente como o formato os define, pelo que é aí que o raciocínio começa. TPdfMatrix.TransformPoint calcula x' = x*a + y*c + e e y' = x*b + y*d + f, que é a forma de seis elementos que a ISO 32000-1 §8.3.4 define para o operador cm que concatena uma matriz na matriz de transformação corrente. O par (a, b) é a primeira linha, (c, d) a segunda, e (e, f) a linha de translação. Hábitos de vetor-coluna adquiridos com OpenGL ou num curso de álgebra linear vão induzi-lo em erro aqui, e vão fazê-lo silenciosamente, porque uma matriz com ordem errada continua a ser uma matriz perfeitamente válida. Ler uma composição na convenção de vetor-linha da esquerda para a direita e a ordem de aplicação sai de graça: dado que point * (M * Op) é igual a (point * M) * Op, uma operação anexada atua sobre coordenadas que a matriz existente já produziu, ou seja, no espaço da página, enquanto uma operação prefixada atua antes de a matriz existente correr, no próprio espaço de entrada do objeto
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 assume por omissão o sentido horário e graus, com ACounterClockwise e AAngleInRadians disponíveis quando os dados de origem estiverem assinados de outra forma. As propriedades só de leitura a a f e a propriedade Handle devolvem o FS_MATRIX em bruto, que é o que FPDFPageObj_SetMatrix pede. Nada na classe esconde os seis números do programador, e isso é deliberado: quando uma transformação se comporta mal, imprimir a a f é o diagnóstico mais rápido disponível
Porque é que prefixar uma translação precisa da parte linear?
Porque um deslocamento prefixado está escrito no espaço de entrada da matriz, e tem de ser conduzido através da parte linear atual antes de poder juntar-se à linha de translação. TPdfMatrix.PreTranslate calcula, por isso, e := dx*a + dy*c + e e f := dx*b + dy*d + f. Anexar é a direção fácil: TPdfMatrix.Translate está escrito no espaço da página, onde nada precisa de conversão, pelo que apenas soma dx a e e dy a f. Quem "otimiza" PreTranslate reduzindo-o a duas somas acaba de eliminar a rotação e a escala do deslocamento
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;
A mesma assimetria percorre o par de escalas, e vale a pena saber que elementos cada uma toca antes de depurar uma delas às três da manhã. TPdfMatrix.PreScale multiplica linhas, escalando a e b por scaleX e c e d por scaleY, e deixa a translação intacta porque o deslocamento já aconteceu a jusante. O anexante TPdfMatrix.Scale multiplica colunas em vez disso, tomando a, c, e por scaleX e b, d, f por scaleY, pelo que o deslocamento existente escala junto com tudo o resto. Ambos são caminhos de propósito único que saltam o produto geral de seis elementos, e ambos preservam exatamente a semântica de composição da forma geral
Onde vão as duas translações numa rotação por pivô?
À volta da operação, não à volta de toda a matriz, e por essa ordem. TPdfMatrix.RotateAt anexa Translate(-pivot), depois a rotação, depois Translate(+pivot), o que, sob a convenção de vetor-linha, se compõe como Translate(-pivot) * Op * Translate(pivot). Essa sequência é o que mantém o pivô fixo sob a nova operação enquanto ainda permite que a matriz existente produza primeiro as suas coordenadas e as entregue adiante. Escreva o par pela ordem contrária, como seria correto numa biblioteca de vetor-coluna, e o objeto orbita à volta da origem em vez de girar no lugar, o que é precisamente como uma marca de água centrada acaba fora da crop 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;
A mesma composição sustenta ScaleAt, SkewAt, HorizontalFlipAt, VerticalFlipAt, e CentralFlipAt, pelo que, uma vez confiando no padrão para a rotação, pode confiar nele para o resto. TPdfMatrix.CentralFlip merece ser destacado: nega os seis elementos para dar uma volta de 180 graus sem qualquer trigonometria, o que significa nenhum cos de um valor que devia ter sido exatamente zero e nenhuma deriva acumulada ao aplicá-lo num ciclo. Se estiver a colocar marcas repetidas em vez de rodar uma só, a mecânica da própria colocação está coberta em carimbos de página reutilizáveis com Form XObjects, e o trabalho de matrizes aqui assenta diretamente sobre esse artigo
O que é que TryDecompose diz sobre uma matriz?
TPdfMatrix.TryDecompose reporta translação, escala, rotação, tosquia (shear), determinante e uma flag de reflexão sob uma convenção de escala-depois-rotação, e reporta-os com honestidade suficiente para servirem decisões e não apenas registo. ScaleX vem do comprimento da primeira linha, Sqrt(a*a + b*b), pelo que é sempre positivo. ScaleY é depois Determinant / ScaleX, o que o torna com sinal. A rotação vem de ArcTan2(-b, a) em graus, e a tosquia das duas linhas, produto escalar normalizado por ambas as escalas
Esse sinal em ScaleY é a parte que as pessoas apagam, e apagá-lo é um bug real, não cosmético. Um determinante negativo significa que a matriz contém uma reflexão. Forçar ambos os fatores de escala a positivo para os números parecerem mais arrumados e a reflexão é deitada fora, pelo que uma matriz reconstruída a partir da decomposição volta espelhada: o texto lê-se ao contrário, uma página digitalizada inverte-se, um logótipo importado fica virado para o lado errado. O campo IsReflected existe precisamente para nunca ser preciso inferir isto. Esta é também a verificação que evita a clássica dupla rotação, em que o código soma uma volta de exibição a uma página que já transporta uma; a versão do lado do visualizador desse problema é trabalhada em ajuste de miniatura, zoom e dupla rotação
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;
Encaixar um retângulo noutro sem adivinhar
TPdfMatrix.TryCreateRectMapping constrói por si a matriz de origem para destino e recebe um TPdfMatrixFitMode de pmfStretch, pmfContain, ou pmfCover. Normaliza primeiro os dois retângulos, porque os retângulos PDF não são obrigados a chegar com a esquerda abaixo da direita ou o fundo abaixo do topo, e depois deriva escalas X e Y independentes: pmfStretch mantém-nas independentes, pmfContain toma a menor e centra a faixa (letterbox), pmfCover toma a maior e centra o corte. O companheiro MapRectToRect anexa o mesmo mapeamento a uma matriz existente, e NewRectMapping levanta EPdfMatrixError onde a forma Try devolve False. Este é o primitivo por baixo de cada colocação de célula em imposição N-up e reordenação de páginas, onde cada página de origem tem de caber dentro de uma célula calculada sem ser preciso derivar a aritmética de novo por disposição
Matrizes degeneradas e o caminho honesto de falha
Entradas finitas não garantem um resultado finito, pelo que o código de encaixe calcula em Double e depois volta a verificar o candidato reduzido a Single quanto a ser finito antes de o publicar; um mapeamento contendo um infinito nunca é devolvido como se fosse válido. A mesma disciplina rege a inversão. TPdfMatrix.TryGetInverse rejeita uma matriz usando um limiar relativo, comparando o determinante com o épsilon multiplicado pelo quadrado do maior elemento linear em vez de contra uma constante fixa, o que mantém o teste com significado quer as unidades sejam pontos ou micrómetros. TryDecompose desiste da mesma forma, recusando quando o comprimento da primeira linha ou o ScaleY derivado ficam no ou abaixo do épsilon
Escolha o estilo de falha que se adequa ao ponto de chamada em vez de envolver tudo em try-except por hábito. TryInvert, TryGetInverse, TryInverseTransformPoint, TryTransformBounds e TryCreateRectMapping devolvem False e deixam os seus destinos intocados, o que serve bem testes de deteção e ciclos por objeto onde um objeto degenerado deve ser ignorado, não fatal. Invert, InverseCopy, InverseTransformPoint, MapRectToRect e TransformBounds levantam EPdfMatrixError em vez disso, o que serve código de configuração onde uma matriz singular significa que o chamador calculou algo de errado. Para trabalho em lote, TransformPoints e TransformRects alocam o seu array de resultado exatamente uma vez, TransformPointsInPlace e TransformRectsInPlace reutilizam o armazenamento do programador, e TryTransformBounds acumula a caixa delimitadora numa única passagem em vez de materializar primeiro os pontos transformados
Nada disto é matemática exótica. É uma convenção, aplicada de forma consistente, com a API nomeada de modo a que a convenção fique visível no ponto de chamada: Multiply e os verbos simples anexam, a família Pre prefixa, a família At encaixa a operação entre o seu par de pivô. Escreva a ordem num comentário junto a qualquer composição que construa, porque o código que hoje se lê corretamente é o código que alguém inverte daqui a seis meses. A referência completa de TPdfMatrix, junto com as APIs de objetos de página e renderização que estas transformações alimentam, está disponível com o PDFium Component para Delphi e C++Builder