O PDFlibPas, a losLab PDF Developer Library, guarda o texto decimal exato que leu para todo número real de um documento e reescreve esse texto literalmente sempre que o valor nunca foi modificado. Desde a v3.539.19 a configuração SetPrecision governa apenas números que a biblioteca cria ou edita, então um load-and-save comum não arredonda mais um /Gamma de CalRGB de 2.22221 para 2.2222 nem desloca as cores de uma página que ninguém tocou. A mudança é pequena em código e grande no que ela diz sobre parsers: o valor que você decodifica e o literal que você emite são duas coisas diferentes, e um round trip por Double não é transformação de identidade
Por que um save que não mudou nada deslocou as cores da página?
Porque os parâmetros do color space estavam sendo reformatados, não a imagem. O arquivo que expôs isso é um documento de escritório de 35 páginas no corpus de regressão local, com uma imagem de header reutilizada em toda página. Carregá-lo e salvar de volta direto produzia image streams byte a byte idênticos à entrada, e uma comparação de hashes de stream reportava o documento inalterado. Uma comparação de renderização discordava: cada uma das 35 páginas mostrava diferenças de pixel no header, e em nenhum outro lugar
A imagem de header é desenhada através de um color space CalRGB, que a ISO 32000-1 §8.6.5.3 define por um /WhitePoint, um array /Gamma opcional de três elementos e uma /Matrix opcional de nove elementos. Esses arrays são objetos numéricos simples no dicionário do color space. O TPDFNumeric armazenava cada um como Double e nada mais, e TPDFNumeric.Output formatava esse Double via PDFPrecNum, que por padrão usa quatro casas decimais. Então o /Gamma foi de 2.22221 para 2.2222, uma entrada da matriz foi de 0.71519 para 0.7152, e o renderer produziu fielmente cores um pouco diferentes a partir de uma calibração um pouco diferente. Os bytes da imagem eram inocentes; os números em volta deles não. A parte desconfortável é o quão invisível isso era. Comparar bytes de streams decodificados não enxerga, porque os números moram num dicionário, não num stream. Comparar payloads de anexos não enxerga. Até o diff de revisão descrito no artigo sobre níveis de modificação faz fingerprint de um corpo de objeto normalizado, então as duas revisões geram o mesmo hash e o diff as reporta idênticas. Só a renderização pegou, e é por isso que a baseline do corpus renderiza todas as páginas em vez de confiar apenas em checagens estruturais
O valor que você leu não é o literal que você deve escrever
Um número real de PDF é uma string decimal, e a ISO 32000-1 §7.3.3 é explícita: é apenas uma string decimal, sem notação de base, sem forma exponencial. O Annex C então lista a precisão que se espera que uma implementação honre, aproximadamente cinco dígitos decimais significativos na parte fracionária. Uma precisão padrão de saída de quatro já está abaixo disso, e perto de zero piora: PLDoubleToStr escala o valor, arredonda para inteiro e emite 0 quando o resultado é zero, então uma entrada de matriz -0.000012345 não perde um dígito, ela desaparece por completo
Aumentar o padrão só mudaria o penhasco de lugar. A correção é parar de fingir que um Double é o número. Quando o tokenizer em TPDFStructure.Decode reconhece um real padrão — ou seja, o token contém ponto decimal e nenhum marcador de expoente — ele guarda o texto de origem no novo campo FOriginalText, ao lado do valor convertido. O Output então prefere esse texto e só cai na formatação quando não há nada a preferir
// Lib/PDFlibStruct.pas — a correção inteira do lado da saída
Function TPDFNumeric.Output: AnsiString;
Begin
If FOriginalText<> '' Then
Result:= FOriginalText
Else
Result:= PLDoubleToStr(FValue, Owner.PDFPrecNum);
End;
Procedure TPDFNumeric.SetTo(Const Value: Double);
Begin
FOriginalText:= ''; // um número editado é um número novo
FValue:= Value;
FChanged:= True;
End;
Duas fronteiras são deliberadas. Inteiros não são preservados, porque a formatação de inteiro já é sem perda. Formas exponenciais como 6.02E23 são toleradas na entrada por causa de produtores quebrados, mas não são preservadas na saída, já que reescrevê-las perpetuaria uma sintaxe que a §7.3.3 proíbe; elas passam pelo formatador como qualquer número gerado pela biblioteca. O tokenizer também aplica seu reparo mínimo de sempre antes de guardar o texto, então um literal com ponto à frente como .5 fica como 0.5 e um literal com ponto no fim como 5. fica como 5.0. Os dois são o mesmo número para todo leitor e são muito mais amplamente aceitos
O que SetPrecision garante depois da v3.539.19?
O TPDFlib.SetPrecision agora controla as casas decimais dos números que a própria biblioteca produz: valores desenhados pelo painter, números criados a partir de um Double, como via NewNumeric, e qualquer valor lido que desde então foi editado com SetTo. Repare que texto decodificado pela API de objeto, por exemplo um literal passado a SetObjectFromString, passa pelo mesmo tokenizer e é preservado do mesmo jeito. Um decimal lido que nunca foi modificado mantém a precisão de entrada independentemente da configuração, e mudar a configuração depois do load não o toca retroativamente. A entrada de referência de SetPrecision foi atualizada na mesma release para dizer exatamente isso, porque o texto antigo dava a entender que a configuração valia para todo número do arquivo
A limpeza acontece no SetTo, em vez de ser derivada da flag Changed, e essa distinção importa. O pipeline de save reseta Changed nos objetos depois que eles foram escritos, então uma checagem do tipo "emita o texto original a menos que tenha mudado" começaria a emitir texto obsoleto para um valor que foi editado, salvo e editado de novo na mesma sessão. Amarrar o texto original à própria atribuição torna impossível que os dois discordem. O teste de regressão fixa cada um desses comportamentos com os valores do arquivo original
uses
PDFlibStruct;
var
Structure: TPDFStructure;
Values: TPDFArray;
Number: TPDFNumeric;
begin
Structure := TPDFStructure.Create;
try
Structure.PDFPrecNum := 4;
Values := TPDFArray(Structure.Decode('[2.22221 0.71519 -0.000012345 1 0.12567]'));
// Entrada não editada sobrevive literalmente, incluindo o valor que
// a formatação de quatro casas teria colapsado em 0
Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');
// Uma edição descarta o texto original e segue o PDFPrecNum
Number := TPDFNumeric(Values.Item[0]);
Number.SetTo(0.123456);
Assert(Number.Output = '0.1235');
Assert(Structure.NewNumeric(0.123456).Output = '0.1235');
// Baixar a precisão depois não alcança entrada não editada
Structure.PDFPrecNum := 2;
Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
finally
Structure.Free;
end;
end;
Por que o content model ainda normaliza números?
Porque o TPDFContentProgram promete operandos numéricos canônicos, e essa promessa vale mais do que texto literal dentro de um content stream. O content model editável, o mesmo sobre o qual o tracker de graphics state é construído, existe para que NormalizeContentStreams, o otimizador e o Emit produzam saída estável e comparável a partir de entrada arbitrária. Se um operando lido carregasse o texto original para dentro do modelo, uma sequência de operadores como 0.50000 0 0 RG seria emitida diferente de 0.5 0 0 RG, e toda comparação a jusante derivaria conforme os hábitos de formatação do produtor
Então o modelo descarta o texto original nos seus dois pontos de entrada. O NormalizeContentNumbers roda em cada operando conforme o parser o empilha e de novo dentro de SetOperand quando código fornecido pelo chamador é decodificado, e ele recursiona por arrays e dicionários, de modo que dash patterns, arrays TJ e os dicionários de propriedades de marked content ficam cobertos. Chamar SetTo(AsDouble) em cada numérico já basta, já que é exatamente essa a operação que limpa o texto. Dados crus de inline image ficam como estão, como sempre ficaram
// Lib/PDFlibContentModel.pas — o content model mantém seu contrato
Procedure NormalizeContentNumbers(Obj: TPDFObject);
Var
K: Integer;
Begin
If Obj is TPDFNumeric Then
TPDFNumeric(Obj).SetTo(TPDFNumeric(Obj).AsDouble)
Else If Obj is TPDFArray Then
For K:= 0 To TPDFArray(Obj).Count- 1 Do
NormalizeContentNumbers(TPDFArray(Obj).Item[K])
Else If Obj is TPDFDictionary Then
For K:= 0 To TPDFDictionary(Obj).Count- 1 Do
NormalizeContentNumbers(TPDFDictionary(Obj).Entry[K].Value);
End;
A regra prática para quem chama é portanto simples. Um LoadFromFile comum seguido de SaveToFile deixa content streams intocados e números de dicionário intocados como estavam. Uma página que passa por NormalizeContentStreams, ou qualquer edição feita pelo content model, sai canônica por design, e o resto do documento continua preservado. São dois pedidos diferentes, e agora eles fazem duas coisas diferentes
Quanto custa, e onde a garantia termina
Todo TPDFNumeric agora carrega uma referência extra de AnsiString, e todo decimal lido mantém seu texto de origem vivo durante a vida do objeto. Num documento com milhões de números reais isso é memória de verdade, e pertence a qualquer medição de documento grande em vez de ser descartado com um aceno. A garantia também se limita ao documento do próprio número: copiar objetos entre documentos ou reconstruir valores pela API de objeto produz números novos, que seguem a precisão de saída como qualquer outro número novo. Vale ser preciso sobre o que a release afirma e o que não afirma. Um load-and-save de um documento intocado agora preserva os números de calibração que o renderer de fato consome, que é a propriedade que a baseline do corpus checa. Ela não afirma saída byte a byte idêntica, o que também depende de numeração de objetos, compressão de stream e do identificador de trailer discutido no artigo sobre o PDF ID determinístico. E ela não faz o diff de fingerprint enxergar diferenças de arredondamento em arquivos produzidos por outros softwares, já que esses continuam gerando hash do corpo normalizado. A lição generaliza bem além do CalRGB: quando um parser guarda apenas o valor convertido, todo save é uma edição, e a única forma de perceber é olhar o resultado renderizado. O tratamento numérico e a semântica do SetPrecision estão documentados na página do produto losLab PDF Developer Library