O HotPDF converte pacotes XPS e OpenXPS em PDF dentro de Delphi e C++Builder sem controlador de impressão, mapeando cada coordenada de página fixa de 96 DPI através de uma matriz de página com inversão de Y e escala 0.75, publicando cada VisualBrush como um Form XObject partilhado, e transformando os modos de mosaico de ImageBrush em padrões de mosaico PDF nativos em vez de desenhos de imagem repetidos
O cenário que arrasta a maioria das casas Windows para isto é aborrecido e inevitável. Algo já imprime para o Microsoft XPS Document Writer — um relatório ERP antigo, um formulário assinado, um lote de extratos — e a política de arquivo diz PDF. O XPS é um formato de captura perfeitamente bom e um péssimo para entregar a um sistema de arquivo daqui a uma década. Por isso o ficheiro de spool tem de se tornar num PDF página a página, e no momento em que começa a escrever esse conversor descobre que a parte interessante não é o XML. É que o XPS e o PDF discordam sobre onde está a origem, quanto vale uma unidade, e o que um pincel pode ser
Do pacote ao PDF numa passagem
O ponto de entrada é o registo de document handlers, não uma classe XPS especial. O THPDFDocumentHandlerRegistry.RegisterStandardHandlers instala os handlers de XPS, EPUB e CBZ; o reconhecimento é baseado no conteúdo, pelo que um pacote que carregue [Content_Types].xml mais pelo menos uma parte .fpage pontua 95 mesmo quando a extensão do ficheiro mente, enquanto uma extensão .xps ou .oxps isolada só pontua 10. Essa ordenação importa quando aceita uploads, porque um atacante que renomeie um EPUB para .xps não deve conduzir o pipeline
var
Handled: IHPDFHandledDocument;
Info: THPDFDocumentHandlerInfo;
Options: THPDFDocumentHandlerOptions;
Registry: THPDFDocumentHandlerRegistry;
Output: TFileStream;
begin
Registry := THPDFDocumentHandlerRegistry.Create;
Output := TFileStream.Create('spool.pdf', fmCreate);
try
Registry.RegisterStandardHandlers;
Options := THPDFDocumentHandlerOptions.Default;
if not Registry.OpenFromFile('spool.xps', '', Options, Handled, Info) then
raise Exception.Create('no registered handler recognised the package');
if Handled.Format = hdfXPS then
Handled.WritePDF(Output, Options, Info);
// Info.UnsupportedFeatureCount é a pontuação honesta desta conversão
finally
Output.Free;
Registry.Free;
end;
end;
Tudo na conversão tem orçamento antes de ser tentado. O THPDFDocumentHandlerOptions.Default limita as entradas de arquivo a 10,000, os bytes de arquivo expandidos a 1 GiB, o rácio de compressão a 200, os recursos a 4,096, e as páginas a 10,000, e traz um CancellationToken opcional para que um trabalho do lado do servidor possa ser parado a meio do pacote. Leia Info.UnsupportedFeatureCount depois e trate um valor diferente de zero como um achado real: o HotPDF conta deliberadamente o que não conseguiu mapear em vez de desenhar uma aproximação e ficar calado quanto a isso
Porque é que uma página XPS precisa de uma matriz em vez de coordenadas reescritas?
Porque reescrever coordenadas perde a pilha de transformações. Um FixedPage XPS é especificado em unidades de 96 DPI com a origem no canto superior esquerdo e Y a crescer para baixo; o espaço de utilizador PDF é de 72 DPI com a origem no canto inferior esquerdo e Y a crescer para cima. A correção ingénua é multiplicar cada número por 0.75 e subtrair cada Y à altura da página ao emiti-lo. Isso funciona para um caminho plano e desmorona-se no instante em que entra um RenderTransform, um Canvas aninhado, ou uma matriz local de pincel, porque essas transformações estão definidas no espaço XPS e a sua reescrita por coordenada já saiu dele. O HotPDF mantém por isso a projeção como matriz e compõe-a. O HPDFXPSPageMatrix devolve as constantes fixas uma vez por página, o HPDFMultiplyXPSMatrix concatena-a com a transformação de caminho acumulada, e o resultado é emitido como um único operador cm antes da geometria. Os dados de caminho são depois escritos em números XPS sem modificações, e é também por isso que a sintaxe de geometria abreviada pode partilhar o mesmo parser delimitado usado para dados de caminho SVG — só o token inicial de regra de preenchimento F0 ou F1 é tratado pelo adaptador XPS. Se seguiu o mesmo raciocínio para a importação de vetores EMF e WMF, a forma do argumento é familiar: formatos de importação são convertidos por matriz, nunca por aritmética sobre coordenadas folha
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
Result.A := 0.75; // unidade XPS de 96 DPI para ponto PDF de 72 DPI
Result.B := 0;
Result.C := 0;
Result.D := -0.75; // o Y do XPS cresce para baixo, o Y do PDF cresce para cima
Result.E := 0;
Result.F := PageHeight; // altura da página PDF, em pontos
end;
// Um CTM composto por visual, emitido antes de qualquer operador de caminho
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));
Como reutilizar um VisualBrush sem o desenhar duas vezes?
Um VisualBrush pinta uma árvore visual arbitrária — filhos Canvas, Path e Glyphs — numa região, possivelmente repetida por ela. O HotPDF compila esse visual uma vez num Form XObject PDF e depois coloca-o, que é a mesma estratégia de recursos descrita na importação de SVG via Form XObjects. Dois detalhes decidem se funciona. Primeiro, o visual tem de ser percorrido como filhos XML diretos: uma varredura plana por elementos dignos de mosaico puxa visuais aninhados para o topo da página e destrói tanto o âmbito dos recursos como a ordem de pintura. Segundo, o conteúdo é capturado com a matriz de página XPS-para-PDF já aplicada, pelo que publicar o Form exige multiplicar pelo inverso dessa matriz, caso contrário cada colocação reaplica a escala 0.75 e a inversão de Y. O Form também tem de ser dono dos seus recursos: o HotPDF copia apenas os tipos de letra, XObjects, padrões, ExtGStates e espaços de cor que o content stream capturado realmente referencia; clonar todo o dicionário de recursos da página arrastaria o Form a registar para o seu próprio grafo de recursos e construiria um ciclo. Os tipos de letra ficam num dicionário direto nas páginas comuns e são promovidos a um dicionário indireto partilhado só quando o conteúdo capturado realmente contém um Tf, pelo que um documento sem visuais reutilizáveis não paga a maquinaria. Note uma fronteira da especificação que vale a pena conhecer antes de abrir um bug: a secção 13.4 do ECMA-388 exige que tanto ViewboxUnits como ViewportUnits num VisualBrush sejam Absolute, pelo que unidades relativas não são uma funcionalidade em falta — são entrada não conforme, e o HotPDF recusa-se a inventar semântica de coordenadas para elas
Mosaico de ImageBrush: quatro modos, quatro tamanhos de célula
Os modos de mosaico do XPS mapeiam para padrões de mosaico PDF da secção 8.7.3 da ISO 32000-1 em vez de serem expandidos em colocações de imagem repetidas pela área coberta, o que mantém o tamanho da saída e o tempo de conversão independentes de quanto da página o pincel cobre. O mapeamento é mecânico uma vez que o veja: a reflexão exprime-se colocando colocações espelhadas dentro de uma célula de padrão e alargando a célula para corresponder
Tile— uma colocação, a célula mantém o viewport 1×1FlipX— duas colocações, célula alargada para 2×1FlipY— duas colocações, célula elevada para 1×2FlipXY— quatro colocações, célula expandida para 2×2
Cada colocação transporta o seu próprio retângulo de clip, porque um mapeamento de Viewbox que transponha a sua subcélula sangraria para a reflexão vizinha. O /Matrix do padrão é a parte que apanha as pessoas. Um padrão de mosaico ancora-se no espaço de utilizador predefinido do content stream pai, não ao estado gráfico corrente quando o padrão é selecionado, pelo que a matriz tem de compor explicitamente as três camadas — a projeção de página fixa, a transformação de Path, e a Transform local do pincel — em vez de confiar num CTM ambiente. O HotPDF também valida antes de alocar: o RegisterImageTilingPattern limita um padrão a 1,024 colocações e rejeita clips degenerados, matrizes não invertíveis e índices de imagem inválidos. Se quiser o modelo geral do lado PDF por trás disto, padrões de mosaico e o espaço de cor Pattern cobre os operadores subjacentes
// Projeção de página fixa dobrada na matriz do padrão, depois a local do pincel
PatternMatrix := HPDFMatFromOps( 0.75 * PathMatrix.A, -0.75 * PathMatrix.B,
0.75 * PathMatrix.C, -0.75 * PathMatrix.D,
0.75 * PathMatrix.E,
PageHeight - 0.75 * PathMatrix.F);
if Brush.HasTransform then
PatternMatrix := HPDFMatMul(PatternMatrix, BrushMatrix);
PatternName := Document.RegisterImageTilingPattern(Resource.ImageIndex,
Brush.Viewport.Left, Brush.Viewport.Top,
Brush.Viewport.Left + CellWidth, Brush.Viewport.Top + CellHeight,
CellWidth, CellHeight, Placements, pttNoDistortion, PatternMatrix);
O que acontece quando um gradiente radial não é um círculo?
O XPS define um RadialGradientBrush com GradientOrigin, Center, RadiusX e RadiusY, pelo que o pincel é uma elipse. O sombreamento tipo 3 do PDF, na secção 8.7.4.5.4 da ISO 32000-1, mistura entre dois círculos e não tem forma de expressar uma elipse diretamente. Média dos dois raios num só número é o atalho tentador e visivelmente errado em qualquer pincel que não seja quase redondo. O HotPDF move em vez disso o problema para o sistema de coordenadas: escala Y por RadiusY / RadiusX, registra um sombreamento circular honesto nesse espaço escalado, seleciona o padrão, e emite imediatamente a escala recíproca para que a geometria de caminho escrita a seguir continue no espaço de utilizador XPS original
ScaleY := Brush.RadiusY / Brush.RadiusX;
PatternName := Document.RegisterMultiStopRadialGradient(
Brush.StartX, Brush.StartY / ScaleY, 0,
Brush.EndX, Brush.EndY / ScaleY, Brush.RadiusX,
StopPositions, StopColours, 3);
if Abs(ScaleY - 1) > 0.000001 then
Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(ScaleY) + ' 0 0 cm'#10);
Page.SetFillPattern(PatternName); // o padrão captura o CTM exatamente aqui
if Abs(ScaleY - 1) > 0.000001 then
Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);
A ordenação nesse excerto é o truque inteiro, e não é estilística. Um padrão de sombreamento PDF captura a matriz de transformação atual no momento em que é escolhido como cor atual, pelo que a escala temporária tem de ser emitida antes de SetFillPattern ou SetStrokePattern, e a recíproca tem de seguir a seleção mas preceder os operadores de caminho. Erre a ordem em qualquer dos sentidos e tem um gradiente que renderiza corretamente no primeiro caminho e deriva em todos os seguintes. Uma restrição relacionada aplica-se ao modo de coordenadas relativas: RadiusX e RadiusY têm de ser resolvidos contra a largura e a altura do caminho separadamente, porque escalar ambos por um único comprimento de aresta muda em silêncio o rácio de aspeto da elipse em qualquer caminho não quadrado
Onde a conversão é honesta quanto aos seus limites
Alguns constructos XPS são convertidos aproximadamente e outros não são convertidos de todo, e a decisão de design ao longo de tudo é contá-los em vez de os fingir. As partes TIFF e JPEG XR são rasterizadas através do WIC e não trazem promessa quanto a alfa preservado, enquanto um PNG com canal alfa válido é dividido numa imagem base mais um /SMask. O tamanho intrínseco da imagem deriva como pixel * 96 / DPI, lendo primeiro a densidade pHYs de PNG ou JFIF de JPEG e recuando para 96 DPI, pelo que um cabeçalho de densidade mau aterra num tamanho previsível em vez de arbitrário. Recursos de matriz não resolvidos, transformações relativas fora do padrão, ColorConvertedBitmap, modos de espalhamento de gradiente não suportados e geometria malformada incrementam todos UnsupportedFeatureCount, e entrada malformada falha fechada em vez de degradar para um desenho silenciosamente diferente
Essa é a postura útil para um conversor de arquivo: uma conversão que aproxima em silêncio é pior do que uma que lhe diz que quatro elementos não conseguiu representar, porque só a segunda lhe dá algo a verificar antes de o documento ser selado num sistema de arquivo. Se está a avaliar a conversão de XPS e OpenXPS ao lado do resto do pipeline de documentos — composição de páginas, tipos de letra, assinatura, saída PDF/A — a página do componente Delphi PDF HotPDF lista o conjunto completo de funcionalidades e as versões de Delphi e C++Builder suportadas