Artigo Técnico

Funções PDF Tipo 2/3/4 em Delphi: Exponencial, PostScript

O HotPDF, o componente PDF nativo VCL para Delphi e C++Builder, avalia os três tipos de função PDF construídos a partir de fórmulas em vez de grades de amostra: interpolação exponencial Tipo 2, costura (stitching) Tipo 3 e funções de calculadora PostScript Tipo 4, correspondendo à ISO 32000-1 §7.10.3, §7.10.4 e §7.10.5. O Tipo 2 mescla entre dois vetores de saída ao longo de uma curva, o Tipo 3 encadeia várias subfunções ao longo de um único domínio de entrada, e o Tipo 4 executa um programa PostScript restrito capaz de ramificar, comparar e calcular quase qualquer coisa que um content stream precise a partir de suas entradas. Erre qualquer um dos três ligeiramente e a falha nunca se anuncia como um bug — ela aparece como um gradiente com uma faixa completamente chapada, uma cor spot que renderiza preto puro, ou uma função de calculadora que erra por exatamente um nas entradas que uma suíte de testes por acaso não experimentou

Esses três ficam ao lado de um quarto, o Tipo 0, que armazena uma grade amostrada em vez de uma fórmula e é coberto separadamente em o artigo complementar sobre tabelas de consulta de cor (LUT) do Tipo 0. As duas famílias resolvem o mesmo problema, mapear uma entrada para uma saída, mas o Tipo 0 é dado calculado uma vez e embutido no arquivo, enquanto os Tipos 2, 3 e 4 são código que o leitor avalia a cada chamada. Todos os quatro compartilham um único ponto de despacho no renderizador do HotPDF, indexado pela entrada /FunctionType do dicionário de função, de modo que um shading, uma transformação de matiz (tint transform) ou uma função spot de meio-tom nunca precisam saber qual dos quatro receberam antes de poder pedir uma cor

Como funciona uma função PDF Tipo 2 exponencial?

Uma função PDF Tipo 2 calcula uma única fórmula — y = C0 + x^N × (C1 − C0), aplicada componente por componente — onde x é a única entrada da função, normalizada contra seu /Domain antes de a fórmula rodar (ISO 32000-1 §7.10.3). /C0 e /C1 são os vetores de saída nas duas extremidades desse intervalo, um número por componente de saída, e /N é o expoente que molda a curva entre eles: N = 1 dá a rampa linear reta por trás da maioria dos pontos de parada de gradiente e conversões de duotone, N acima de 1 puxa a curva em direção a C0, e N entre 0 e 1 a empurra em direção a C1. RegisterExponentialFunction constrói esse dicionário a partir de cinco argumentos e devolve um objeto de função pronto para ser plugado em um shading, uma função spot de meio-tom, ou qualquer outro lugar onde a especificação aceite uma chave /Function

A relação de contagem de componentes entre C0 e C1 importa duas vezes: uma quando você cria uma função Tipo 2, e outra sempre que o HotPDF precisa renderizar uma que não criou. No lado da criação, RegisterExponentialFunction verifica C0 contra C1 e levanta uma exceção se discordarem, de modo que uma chamada que chega a BeginDoc já é um objeto de função autoconsistente. No lado da renderização, porém, o avaliador precisa confiar em quaisquer arrays /C0 e /C1 que um arquivo de origem de fato declare — um arquivo de gráfica aberto para pré-visualização, digamos, ou um documento assinado exibido de volta a um usuário — e versões anteriores à 2.376.0 liam esses arrays em um buffer dimensionado para quatro componentes, o caso CMYK. Um matiz exponencial DeviceGray ou DeviceRGB, com um /C0 e /C1 de um ou três elementos, falhava nessa leitura silenciosamente e deixava ambos os arrays em zero, de modo que o matiz pintava preto chapado em vez da cor pretendida. A versão 2.376.0 redimensionou o leitor para a contagem de saída realmente declarada pela função, em vez de um buffer fixo — exatamente o tipo de bug que só um caso de teste não-CMYK expõe, já que a suíte existente rodava CMYK o tempo todo, onde quatro em quatro sempre cabia

var
  EaseIn: THPDFDictionaryObject;
begin
  // Type 2: one input, N > 1 biases the ramp toward C0 (an ease-in curve)
  EaseIn := Pdf.RegisterExponentialFunction(
    [0,1],           // Domain: single input, clamped to [0,1]
    [0, 0, 0],       // C0: output at x = 0
    [0.8, 0, 0],     // C1: output at x = 1
    3,               // N: exponent, 1 = linear, > 1 eases toward C0
    []);             // Range omitted: defaults to a [0,1] clamp per output
end;

Costura (stitching) Tipo 3: encadeando subfunções ao longo de um array Bounds

Uma função PDF Tipo 3 costura k subfunções em um único mapeamento por partes sobre o /Domain de uma única entrada, e os dois arrays que fazem isso funcionar são /Bounds e /Encode (ISO 32000-1 §7.10.4). /Bounds contém k − 1 pontos de divisão internos que talham o /Domain em k intervalos consecutivos; o avaliador escolhe o primeiro intervalo cujo limite superior exceda a entrada, ou o último intervalo assim que a entrada alcança o limite final, e passa o controle à subfunção desse intervalo. /Encode então remapeia a entrada de sua posição dentro daquele intervalo para qualquer faixa de entrada que a subfunção escolhida de fato espera — tipicamente [0, 1] se a subfunção for mais um segmento exponencial — antes de a avaliação continuar, uma chamada mais profunda, no próprio /Domain e /Range dessa subfunção

O avaliador de costura do HotPDF costumava tratar apenas exatamente duas subfunções, e seu leitor de /Bounds exigia um array completo de oito elementos, de modo que o único ponto de divisão que um gradiente de dois segmentos de fato precisa — um número em /Bounds — sempre falhava ao analisar e a função retornava nada. /Encode não era aplicado de forma alguma. A versão 2.376.0 reescreveu a seleção como a busca geral de k subfunções que a especificação descreve, e passou a ler /Bounds contra seu comprimento real declarado, de modo que um gradiente de três, quatro ou cinco pontos de parada costurado a partir desse tanto de segmentos exponenciais agora se resolve da mesma forma que um de dois segmentos sempre alegou fazer. O exemplo abaixo constrói uma rampa de dois segmentos preto-para-vermelho-para-branco, a forma que um shading axial ou radial busca sempre que uma única curva exponencial não consegue carregar todos os pontos de cor que um design exige

var
  ToRed, ToWhite, Ramp: THPDFDictionaryObject;
begin
  // Two linear segments: black->red over [0, 0.5], red->white over [0.5, 1]
  ToRed   := Pdf.RegisterExponentialFunction([0,1], [0, 0, 0],   [0.8, 0, 0], 1, []);
  ToWhite := Pdf.RegisterExponentialFunction([0,1], [0.8, 0, 0], [1, 1, 1],   1, []);

  Ramp := Pdf.RegisterStitchingFunction(
    [0,1],                  // Domain: the stitched function's own input range
    [ToRed, ToWhite],       // Functions: k = 2 sub-functions
    [0.5],                  // Bounds: k - 1 = 1 split point
    [0,1, 0,1],             // Encode: 2 numbers per sub-function
    []);                    // Range omitted: inherited from each sub-function
end;

O que uma função de calculadora PostScript Tipo 4 consegue fazer que Tipo 2 e 3 não conseguem?

Uma função PDF Tipo 4 executa um programa genuíno, ainda que deliberadamente restrito: uma calculadora PostScript que empilha suas entradas em uma pilha de operandos, executa operadores aritméticos, de comparação, de manipulação de pilha e booleanos, além de condicionais if/ifelse, e deixa suas saídas na pilha ao terminar (ISO 32000-1 §7.10.5, Tabela 42). Não há construção de loop nem armazenamento de variável nomeada, apenas a pilha, o que mantém um programa em conformidade fácil de raciocinar — mas dentro desse conjunto restrito de operadores, o Tipo 4 consegue expressar coisas que Tipo 2 e Tipo 3 não conseguem, como uma fórmula real de mistura multi-tinta para uma separação DeviceN ou uma função spot de meio-tom com um limiar condicional. O avaliador do HotPDF, HPDFEvalPostScriptCalculator, tokeniza o programa uma vez — números, operadores e blocos de procedimento { } — depois percorre uma pilha de operandos de 100 entradas, a profundidade que a ISO 32000-1 §7.10.5 exige, atrás de um teto rígido de 50.000 operadores avaliados como uma salvaguarda defensiva contra programas patológicos ou escritos à mão

O operador roll: a direção é fácil de inverter por engano

roll é o operador com mais chance de sair invertido em uma primeira tentativa, porque tanto a ordem de seus argumentos quanto sua direção de rotação correm ao contrário de como o português (ou o inglês) os descreveria. n j roll desempilha uma contagem n e uma quantidade de rotação j, depois desloca ciclicamente as n entradas do topo da pilha por j posições, envolvendo os itens que caem de uma ponta de volta na outra; o exemplo canônico, direto da especificação, é a b c 3 1 roll produzindo c a b — o item do topo move para o fundo do grupo, não o contrário, e cada outro item desloca uma posição para cima para abrir espaço. O avaliador do HotPDF calcula a nova posição da entrada de pilha i como (i + j) mod n, o que corresponde exatamente a esse exemplo, mas é um loop de duas linhas igualmente fácil de escrever com a rotação invertida, e um roll espelhado ainda produz uma cor de aparência plausível — só não é a cor que o autor do arquivo pediu

const
  Prog = '{ 3 1 roll }';   // (a b c) -> (c a b): the third input moves to the front
var
  Reorder: THPDFStreamObject;
begin
  // Type 4: 3 inputs, 3 outputs, no extra clamping beyond Domain/Range
  Reorder := Pdf.RegisterPostScriptFunction(
    [0,1, 0,1, 0,1],   // Domain: 2 numbers per input
    [0,1, 0,1, 0,1],   // Range: 2 numbers per output (required for Type 4)
    Prog);
end;

round não é o Round do Delphi: arredondamento para cima versus arredondamento bancário

O operador round do PostScript resolve um empate em .5 sempre em direção ao inteiro maior, e a função Round embutida do Delphi não: ela arredonda para o par mais próximo (half-to-even), a convenção de arredondamento bancário que alterna para que lado um empate em .5 cai para que arredondamentos repetidos não acumulem viés. Os dois concordam em quase todos os lugares e discordam exatamente na fronteira que importa aqui — o Round(0.5) do Delphi retorna 0 e Round(2.5) retorna 2, enquanto o round da especificação PDF quer 1 e 3 para essas mesmas entradas — de modo que a incompatibilidade se esconde em testes casuais e depois se reproduz como um erro consistente de um a menos onde quer que a matemática intermediária de um programa de calculadora caia exatamente em um meio-inteiro. A ISO 32000-1 §7.10.5 Tabela 42 é explícita que round empurra uma fração .5 em direção ao inteiro maior, de modo que o HotPDF implementa o operador como Floor(x + 0.5) em vez de chamar o Round do Delphi, e qualquer código que reimplemente ou verifique manualmente a aritmética de um programa Tipo 4 precisa da mesma substituição

function PostScriptRound(const X: Double): Double;
begin
  // ISO 32000-1 7.10.5 Table 42: round pushes .5 toward the greater
  // integer. Delphi's Round() is banker's rounding and disagrees here:
  // Round(0.5) = 0, Round(2.5) = 2 - both one short of the spec value.
  Result := Floor(X + 0.5);
end;

A validação no momento do registro captura cedo um programa de calculadora ruim

Um programa Tipo 4 malformado é barato de capturar no momento da criação e caro de capturar em qualquer outro lugar, de modo que RegisterPostScriptFunction não apenas armazena o texto-fonte: ele avalia o programa experimentalmente uma vez, no ponto médio do /Domain declarado, antes de o objeto de função sequer ser gravado no documento. Blocos { } desbalanceados, um operador não reconhecido, um esvaziamento de pilha (underflow) ou uma contagem de saída que não corresponde a /Range, todos falham nessa execução experimental e levantam uma exceção imediatamente, com a pilha de chamadas apontando para a própria chamada RegisterPostScriptFunction em vez de para um artefato de renderização descoberto durante o QA em um arquivo que já foi para produção. A execução experimental no ponto médio não prova que o programa está correto em todo o seu /Domain — uma ramificação condicional que só se comporta mal perto de uma borda da faixa de entrada ainda pode escapar de um único ponto de amostra — mas ela fecha toda a classe de programas que são estruturalmente quebrados, e não apenas errados em um canto

Onde gradientes e cores spot põem essas funções para trabalhar

Tipo 2, 3 e 4 raramente aparecem isolados em um PDF real; eles aparecem em qualquer lugar onde a especificação aceite uma chave /Function, e os dois consumidores mais comuns são shadings e transformações de matiz (tint transform) de cor spot. O operador sh de um gradiente axial ou radial (ISO 32000-1 §8.7.4.5) avalia sua /Function uma vez por posição ao longo do eixo do gradiente, exatamente o caso multi-ponto-de-parada para o qual a costura Tipo 3 existe. Um espaço de cor Separation ou DeviceN é o outro lar frequente para esses três tipos, e é onde o Tipo 4 justifica sua existência: uma única tinta spot geralmente se reduz a uma curva Tipo 2 ou Tipo 0, mas uma mistura DeviceN de várias tintas com comportamento real de trapping e sobreimpressão frequentemente precisa da lógica condicional que só uma calculadora PostScript consegue expressar, o caso coberto no artigo sobre renderização de cores spot Separation e DeviceN. RegisterSeparationFunc é a chamada correspondente no lado da criação: ela recebe um nome de colorante, um espaço de cor alternativo e qualquer objeto que a família Register*Function devolva, e conecta essa transformação de matiz a um recurso de espaço de cor Separation que o resto da página pode selecionar com scn/SCN

Juntos, as grades amostradas do Tipo 0 e esses três tipos guiados por fórmula cobrem toda /Function que um PDF pode declarar, e escolher o tipo certo é principalmente uma questão do que você já tem: uma tabela de consulta calculada em outro lugar vira Tipo 0, uma mescla de dois pontos extremos vira Tipo 2, várias mesclas encadeadas ao longo de um domínio viram Tipo 3, e qualquer coisa com lógica condicional real vira Tipo 4. RegisterExponentialFunction, RegisterStitchingFunction e RegisterPostScriptFunction fazem parte do componente HotPDF padrão para Delphi e C++Builder, ao lado do restante de sua API de funções e shadings da ISO 32000-1