O HotPDF converte pacotes XPS e OpenXPS para PDF dentro do Delphi e C++Builder sem print driver, mapeando cada coordenada fixed-page de 96 DPI por uma única matriz de página com escala 0.75 e inversão de Y, publicando cada VisualBrush como um Form XObject compartilhado, e transformando modos de tile de ImageBrush em padrões de tiling nativos do PDF em vez de desenhos de imagem repetidos
O cenário que arrasta a maioria das casas Windows para isso é sem graça e inevitável. Alguma coisa já imprime para o Microsoft XPS Document Writer — um relatório de ERP legado, um formulário assinado, um lote de extratos — e a política de arquivamento diz PDF. O XPS é um formato de captura perfeitamente bom e um péssimo para entregar a um sistema de registros daqui a uma década. Então o spool file precisa virar um PDF página por página, e no momento em que você começa a escrever esse conversor descobre que a parte interessante não é o XML. É que XPS e PDF discordam sobre onde está a origem, quanto vale uma unidade e o que um brush pode ser
Do pacote ao PDF em uma passada
O ponto de entrada é o registro de document handlers, não uma classe XPS especial. O THPDFDocumentHandlerRegistry.RegisterStandardHandlers instala os handlers XPS, EPUB e CBZ; o reconhecimento é baseado em conteúdo, então um pacote carregando [Content_Types].xml mais pelo menos uma part .fpage pontua 95 mesmo quando a extensão do arquivo mente, enquanto uma extensão nua .xps ou .oxps só pontua 10. Essa ordenação importa quando você aceita uploads, porque um atacante renomeando um EPUB para .xps não deve pilotar 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 para esta conversão
finally
Output.Free;
Registry.Free;
end;
end;
Tudo sobre a conversão tem orçamento antes de ser tentado. O THPDFDocumentHandlerOptions.Default limita entradas de archive em 10.000, bytes de archive expandidos em 1 GiB, a taxa de compressão em 200, recursos em 4.096 e páginas em 10.000, e carrega um CancellationToken opcional para que um job do lado do servidor possa ser parado no meio do pacote. Leia Info.UnsupportedFeatureCount depois e trate um valor não zero como um achado real: o HotPDF deliberadamente conta o que não conseguiu mapear em vez de desenhar uma aproximação e ficar calado sobre isso
Por que uma página XPS precisa de uma matriz em vez de coordenadas reescritas?
Porque reescrever coordenadas perde a pilha de transforms. Um FixedPage XPS é especificado em unidades de 96 DPI com a origem no canto superior esquerdo e Y crescendo para baixo; o user space do PDF é 72 DPI com a origem no canto inferior esquerdo e Y crescendo para cima. A correção ingênua é multiplicar cada número por 0.75 e subtrair cada Y da altura da página ao emiti-lo. Isso funciona para um path plano e desmorona no instante em que um RenderTransform, um Canvas aninhado ou uma matriz local de brush entra, porque esses transforms são definidos no espaço XPS e sua reescrita por coordenada já o deixou. O HotPDF por isso mantém a projeção como uma matriz e a compõe. O HPDFXPSPageMatrix retorna as constantes fixas uma vez por página, o HPDFMultiplyXPSMatrix concatena-a com o transform acumulado do path, e o resultado é emitido como um único operador cm antes da geometria. Os dados do path são então escritos em números XPS sem modificação, o que também é por que a sintaxe geométrica abreviada pode compartilhar o mesmo parser limitado usado para dados de path SVG — só o token de fill-rule F0 ou F1 inicial é tratado pelo adaptador XPS. Se você seguiu o mesmo raciocínio para importação vetorial EMF e WMF, a forma do argumento é familiar: formatos de importação são convertidos por matriz, nunca por aritmética em 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; // Y do XPS cresce para baixo, 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 path
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));
Como reutilizar um VisualBrush sem desenhá-lo duas vezes?
Um VisualBrush pinta uma árvore visual arbitrária — filhos Canvas, Path e Glyphs — em uma região, possivelmente repetida por ela. O HotPDF compila esse visual uma vez em um Form XObject do PDF e depois o posiciona, que é a mesma estratégia de recurso descrita em importação SVG via Form XObjects. Dois detalhes decidem se funciona. Primeiro, o visual precisa ser percorrido como filhos XML diretos: uma varredura plana por elementos dignos de tile puxa visuais aninhados para o nível de topo da página e destrói tanto o escopo de recursos quanto a ordem de pintura. Segundo, o conteúdo é capturado com a matriz de página XPS-para-PDF já aplicada, então publicar o Form exige multiplicar pelo inverso dessa matriz, caso contrário cada posicionamento reaplica a escala 0.75 e a inversão de Y. O Form também precisa ser dono de seus recursos: o HotPDF copia apenas as fonts, XObjects, patterns, ExtGStates e espaços de cor que o content stream capturado realmente referencia; clonar o dicionário de recursos da página inteira arrastaria o Form sendo registrado para seu próprio grafo de recursos e construiria um ciclo. Fonts ficam em um dicionário direto em páginas comuns e são promovidas a um dicionário indireto compartilhado só quando o conteúdo capturado realmente contém um Tf, então um documento sem visuais reutilizáveis não paga pela maquinaria. Note uma fronteira de especificação que vale conhecer antes de abrir um bug: a seção 13.4 do ECMA-388 exige que tanto ViewboxUnits quanto ViewportUnits em um VisualBrush sejam Absolute, então unidades relativas não são um recurso ausente — são entrada não conforme, e o HotPDF se recusa a inventar semântica de coordenadas para elas
Tiling de ImageBrush: quatro modos, quatro tamanhos de célula
Os modos de tile do XPS mapeiam para padrões de tiling do PDF da seção 8.7.3 do ISO 32000-1 em vez de serem expandidos em posicionamentos de imagem repetidos pela área coberta, o que mantém o tamanho da saída e o tempo de conversão independentes de quanta página o brush cobre. O mapeamento é mecânico uma vez que você o veja: reflexão é expressa colocando posicionamentos espelhados dentro de uma célula de padrão e ampliando a célula para acompanhar
Tile— um posicionamento, célula permanece viewport 1×1FlipX— dois posicionamentos, célula alargada para 2×1FlipY— dois posicionamentos, célula elevada para 1×2FlipXY— quatro posicionamentos, célula expandida para 2×2
Cada posicionamento carrega seu próprio retângulo de clip, porque um mapeamento de Viewbox que ultrapasse sua subcélula vaza para a reflexão vizinha. O /Matrix do pattern é a parte que pega as pessoas. Um padrão de tiling é ancorado ao user space padrão de seu content stream pai, não ao estado gráfico vigente quando o pattern é selecionado, então a matriz precisa compor as três camadas explicitamente — a projeção de página fixa, o transform do Path e o Transform local do brush — em vez de confiar em um CTM ambiente. O HotPDF também valida antes de alocar: o RegisterImageTilingPattern limita um pattern a 1.024 posicionamentos e rejeita clips degenerados, matrizes não inversíveis e índices de imagem inválidos. Se você quer o modelo geral do lado PDF por trás disso, padrões de tiling e o espaço de cor Pattern cobre os operadores subjacentes
// Projeção de página fixa dobrada na matriz do pattern, depois a local do brush
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, então o brush é uma elipse. O shading type 3 do PDF, na seção 8.7.4.5.4 do ISO 32000-1, faz blend entre dois círculos e não tem como expressar uma elipse diretamente. Média dos dois raios em um número é o atalho tentador e visivelmente errado em qualquer brush que não esteja perto de redondo. O HotPDF em vez disso move o problema para o sistema de coordenadas: escala Y por RadiusY / RadiusX, registra um shading circular honesto nesse espaço escalado, seleciona o pattern e imediatamente emite a escala recíproca para que a geometria do path escrita a seguir ainda esteja no user space 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 pattern captura o CTM bem aqui
if Abs(ScaleY - 1) > 0.000001 then
Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);
A ordenação nesse snippet é o truque inteiro, e não é estilístico. Um pattern de shading do PDF captura a matriz de transformação atual no momento em que é escolhido como cor atual, então a escala temporária precisa ser emitida antes de SetFillPattern ou SetStrokePattern, e a recíproca precisa seguir a seleção mas preceder os operadores de path. Erre a ordem em qualquer direção e você tem um gradiente que renderiza corretamente no primeiro path e deriva em cada um subsequente. Uma restrição relacionada se aplica ao modo de coordenadas relativas: RadiusX e RadiusY precisam ser resolvidos contra a largura e a altura do path separadamente, já que escalar ambos por um único comprimento de borda muda silenciosamente a proporção da elipse em qualquer path não quadrado
Onde a conversão é honesta sobre seus limites
Alguns constructs XPS são convertidos aproximadamente e alguns não são convertidos de forma alguma, e a escolha de design em todo lugar é contá-los em vez de fingi-los. Parts TIFF e JPEG XR são rasterizados via WIC e não carregam promessa sobre alpha preservado, enquanto PNG com canal alpha válido é dividido em uma imagem base mais um /SMask. O tamanho intrínseco da imagem é derivado como pixel * 96 / DPI, lendo primeiro a densidade pHYs do PNG ou JFIF do JPEG e caindo para 96 DPI, então um header de densidade ruim aterrissa em um tamanho previsível em vez de um arbitrário. Recursos de matriz não resolvidos, transforms relativos fora do padrão, ColorConvertedBitmap, modos de spread de gradiente não suportados e geometria malformada todos incrementam UnsupportedFeatureCount, e entrada malformada falha fechando em vez de degradar para um desenho silenciosamente diferente
Essa é a postura útil para um conversor de arquivo: uma conversão que aproxima silenciosamente é pior que uma que diz quais quatro elementos não conseguiu representar, porque só a segunda te dá algo para checar antes de o documento ser selado em um sistema de registros. Se você está avaliando conversão XPS e OpenXPS junto com o resto do pipeline de documentos — composição de páginas, fonts, assinatura, saída PDF/A — a página do HotPDF Delphi PDF component lista o conjunto completo de recursos e as versões suportadas de Delphi e C++Builder