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 grelhas de amostras: interpolação exponencial Tipo 2, costura (stitching) Tipo 3, e funções calculadora PostScript Tipo 4, correspondendo à ISO 32000-1 §7.10.3, §7.10.4, e §7.10.5. O Tipo 2 mistura entre dois vetores de saída ao longo de uma curva, o Tipo 3 encadeia várias sub-funçõ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 praticamente tudo o que um fluxo de conteúdo precisa a partir das suas entradas. Errar ligeiramente em qualquer um dos três nunca se anuncia como um bug — surge como um gradiente com uma faixa completamente plana, uma cor spot que renderiza em preto puro, ou uma função calculadora que está desviada exatamente em um nos valores de entrada que uma suite de testes calhou de não experimentar
Estes três situam-se ao lado de um quarto, o Tipo 0, que armazena uma grelha amostrada em vez de uma fórmula, e é abordado separadamente em o artigo complementar sobre tabelas de consulta de cor (LUT) Tipo 0. As duas famílias resolvem o mesmo problema, mapear uma entrada para uma saída, mas o Tipo 0 são dados calculados uma vez e cozidos no ficheiro, enquanto os Tipos 2, 3, e 4 são código que o leitor avalia em cada chamada. Todos os quatro partilham um único ponto de despacho no motor de renderização do HotPDF, indexado pela entrada /FunctionType do dicionário da função, pelo que um shading, uma transformação de tom, ou uma função spot de meio-tom nunca precisam de saber qual dos quatro receberam antes de poderem 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 a componente — onde x é a entrada única da função, normalizada em função do seu /Domain antes de a fórmula correr (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 elas: N = 1 dá a rampa linear direta por trás da maioria das paragens de gradiente e conversões duotone, N acima de 1 puxa a curva para C0, e N entre 0 e 1 empurra-a para C1. RegisterExponentialFunction constrói esse dicionário a partir de cinco argumentos e devolve um objeto de função pronto a ligar a um shading, a uma função spot de meio-tom, ou a qualquer outro sítio onde a especificação aceite uma chave /Function
A relação de contagem de componentes entre C0 e C1 importa duas vezes: uma quando se cria uma função Tipo 2, e outra sempre que o HotPDF tem de renderizar uma que não criou. Do lado da criação, RegisterExponentialFunction verifica C0 e C1 um contra o outro e levanta uma exceção se discordarem, pelo que uma chamada que chegue a BeginDoc já corresponde a um objeto de função autoconsistente. Do lado da renderização, porém, o avaliador tem de confiar em quaisquer arrays /C0 e /C1 que um ficheiro de origem efetivamente declare — um ficheiro de gráfica aberto para pré-visualização, por exemplo, ou um documento assinado apresentado de volta a um utilizador — e as versões anteriores à 2.376.0 liam esses arrays para um buffer dimensionado para quatro componentes, o caso CMYK. Um tom exponencial DeviceGray ou DeviceRGB, com um /C0 e /C1 de um ou três elementos, falhava essa leitura silenciosamente e deixava ambos os arrays a zero, pelo que o tom pintava em preto puro em vez da cor pretendida. A versão 2.376.0 redimensionou o leitor para a contagem de saída efetivamente declarada da função, em vez de um buffer fixo — exatamente o tipo de bug que só um caso de teste que não seja CMYK expõe, uma vez que a suite existente corria sempre em CMYK, onde quatro-para-quatro sempre encaixava
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: encadear sub-funções ao longo de um array Bounds
Uma função PDF Tipo 3 cose k sub-funções num único mapeamento por partes sobre o /Domain de uma única entrada, e os dois arrays que tornam isso possível são /Bounds e /Encode (ISO 32000-1 §7.10.4). /Bounds contém k − 1 pontos de divisão interiores que cortam /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 atinja o limite final, e passa o controlo para a sub-função desse intervalo. /Encode remapeia então a entrada a partir da sua posição dentro desse intervalo para o intervalo de entrada que a própria sub-função escolhida espera — tipicamente [0, 1] se a sub-função for mais um segmento exponencial — antes de a avaliação continuar, uma chamada mais fundo, para o próprio /Domain e /Range dessa sub-função
O avaliador de costura do HotPDF costumava tratar apenas exatamente duas sub-funções, e o seu leitor de /Bounds exigia um array completo de oito elementos, pelo que o único ponto de divisão de que um gradiente de dois segmentos efetivamente precisa — um número em /Bounds — falhava sempre a análise e a função não devolvia nada. /Encode não era sequer aplicado. A versão 2.376.0 reescreveu a seleção como a pesquisa geral de k sub-funções que a especificação descreve e passou a ler /Bounds em função do seu comprimento efetivamente declarado, pelo que um gradiente de três, quatro, ou cinco paragens, cosido a partir de outros tantos segmentos exponenciais, resolve-se agora da mesma forma que um de dois segmentos sempre afirmou fazer. O exemplo abaixo constrói uma rampa de dois segmentos preto-para-vermelho-para-branco, a forma a que um shading axial ou radial recorre sempre que uma única curva exponencial não consegue transportar todas as paragens 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 consegue uma função calculadora PostScript Tipo 4 fazer que o Tipo 2 e o Tipo 3 não conseguem?
Uma função PDF Tipo 4 executa um programa genuíno, embora deliberadamente restrito: uma calculadora PostScript que empurra as suas entradas para uma pilha de operandos, executa operadores aritméticos, de comparação, de manipulação de pilha, e booleanos, mais condicionais if/ifelse, e deixa as suas saídas na pilha quando termina (ISO 32000-1 §7.10.5, Tabela 42). Não há construção de laço nem armazenamento em variáveis nomeadas, apenas a pilha, o que mantém um programa conforme fácil de acompanhar — mas dentro desse conjunto restrito de operadores, o Tipo 4 consegue exprimir coisas que o Tipo 2 e o Tipo 3 não conseguem, como uma verdadeira fórmula 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 { } — e depois percorre uma pilha de operandos de 100 entradas, a profundidade que a ISO 32000-1 §7.10.5 exige, protegida por um teto rígido de 50.000 operadores avaliados como 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 maior probabilidade de sair invertido numa primeira tentativa, porque tanto a ordem dos seus argumentos como a sua direção de rotação correm em sentido contrário ao que a descrição em inglês sugere. n j roll retira da pilha uma contagem n e uma quantidade de rotação j, depois desloca ciclicamente as n entradas de topo da pilha por j posições, envolvendo os itens que caem de um extremo de volta para o outro; o exemplo canónico, diretamente da especificação, é a b c 3 1 roll a produzir c a b — o item do topo move-se para o fundo do grupo, e não o contrário, e cada um dos outros itens desloca-se uma posição acima 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 ciclo de duas linhas igualmente fácil de escrever com a rotação invertida, e um roll invertido continua a produzir uma cor com aspeto plausível — só não é a cor que o autor do ficheiro 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 a favor do inteiro maior, e a função Round incorporada do Delphi não: arredonda para o par mais próximo, a convenção de arredondamento bancário que alterna o sentido em que um empate .5 cai, para que arredondamentos repetidos não acumulem enviesamento. Os dois concordam quase em toda a parte e discordam exatamente na fronteira que aqui importa — Round(0.5) do Delphi devolve 0 e Round(2.5) devolve 2, enquanto o round da especificação PDF quer 1 e 3 para essas mesmas entradas — pelo que a discrepância se esconde num teste casual e depois se reproduz como um desvio de um consistente sempre que a aritmética intermédia de um programa calculadora cai exatamente sobre um meio-inteiro. A ISO 32000-1 §7.10.5 Tabela 42 é explícita quanto a round empurrar uma fração .5 para o inteiro maior, pelo 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 em tempo de registo apanha cedo um programa calculadora defeituoso
Um programa Tipo 4 malformado é barato de apanhar em tempo de criação e caro de apanhar em qualquer outro sítio, pelo que RegisterPostScriptFunction não se limita a armazenar o texto-fonte: avalia o programa experimentalmente uma vez, no ponto médio do /Domain declarado, antes de o objeto de função ser sequer escrito no documento. Blocos { } desequilibrados, um operador não reconhecido, um esvaziamento de pilha, ou uma contagem de saída que não corresponda a /Range, todos falham essa execução experimental e levantam uma exceção de imediato, com a pilha de chamadas a apontar para a chamada RegisterPostScriptFunction, em vez de para um artefacto de renderização descoberto durante o QA num ficheiro já distribuído. A execução experimental no ponto médio não prova que o programa está correto em todo o seu /Domain — um ramo condicional que só se comporte mal perto de uma extremidade do intervalo de entrada ainda pode escapar a um único ponto de amostra — mas fecha toda a classe de programas que estão estruturalmente quebrados, e não meramente errados num canto
Onde os gradientes e as cores spot põem estas funções a trabalhar
Os Tipos 2, 3, e 4 raramente aparecem isolados num PDF real; surgem onde quer que a especificação aceite uma chave /Function, e os dois consumidores mais comuns são os shadings e as transformações de tom de cor spot. O operador sh de um gradiente axial ou radial (ISO 32000-1 §8.7.4.5) avalia a sua /Function uma vez por posição ao longo do eixo do gradiente, que é exatamente o caso de múltiplas paragens para o qual a costura Tipo 3 existe. A transformação de tom de um espaço de cor Separation ou DeviceN é o outro lar frequente destes três tipos, e é onde o Tipo 4 ganha o seu lugar: uma única tinta spot normalmente reduz-se 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 exprimir, o caso abordado no artigo sobre renderizar cores spot Separation e DeviceN. RegisterSeparationFunc é a chamada correspondente do lado da criação: recebe um nome de tinta, um espaço de cor alternativo, e qualquer objeto que a família Register*Function devolva, e liga essa transformação de tom a um recurso de espaço de cor Separation que o resto da página pode selecionar com scn/SCN
Em conjunto, as grelhas amostradas do Tipo 0 e estes três tipos orientados a fórmula cobrem cada /Function que um PDF pode declarar, e escolher o correto é sobretudo uma questão do que já se tem: uma tabela de consulta calculada noutro sítio torna-se Tipo 0, uma mistura de dois pontos finais torna-se Tipo 2, várias misturas encadeadas ao longo de um domínio tornam-se Tipo 3, e qualquer coisa com lógica condicional real torna-se Tipo 4. RegisterExponentialFunction, RegisterStitchingFunction, e RegisterPostScriptFunction fazem parte do componente HotPDF standard para Delphi e C++Builder, a par do resto da sua API de funções e shadings ISO 32000-1