O PDFlibPas, a losLab PDF Developer Library for Delphi, escreve todos os números que mete num content stream com separador decimal por ponto e sem expoente, digam o que disserem as configurações regionais do Windows. Desde a v3.539.26 que o AddPageMatrix, o ScalePage, o DeskewPage, o RedactRegion, o output de text-to-path e a recoloração formatam operandos através do PLDoubleToStrConst, e desde a v3.539.33 que os parsers que leem esses números de volta usam o PLTryStrToFloatInvariant em vez da locale do sistema. Numa máquina alemã, francesa ou brasileira o mesmo código produz agora os mesmos bytes que numa americana, que é o único comportamento que um formato de ficheiro tolera
Porque é que uma locale de vírgula decimal corrompe um PDF sem erro nenhum?
Uma locale de vírgula decimal corrompe um PDF em silêncio porque a vírgula não é um carácter de número na sintaxe PDF, por isso o dano lê-se como tokens válidos com o significado errado. Antes da correção, o PLFloatToStr não passava de uma chamada nua ao FloatToStr, e o FloatToStr segue o FormatSettings.DecimalSeparator. Com separador por vírgula, o AddPageMatrix(0.5, 0.5, 0, 0) escrevia 0,5 0 0 0,5 0 0 cm. A ISO 32000-1 §7.3.3 permite dígitos, um ponto e um sinal inicial num número e mais nada, por isso um parser de conteúdo lê essa linha como o número 0 seguido de um token desconhecido ,5, e o operador cm acaba com os operandos errados. Nada levanta, nada regista. A página simplesmente renderiza com uma matriz de transformação que deslizou, e retroceder de um desenho deslocado até a uma configuração regional é uma tarde miserável
O segundo defeito esconde-se atrás do primeiro. O FloatToStr usa o formato ffGeneral, que passa a notação de expoente mal a magnitude desça abaixo de 1E-4, por isso um offset minúsculo saía como 1E-5. A mesma §7.3.3 declara que o PDF não suporta a forma de expoente, o que significa que até uma máquina com locale americana podia escrever um operando inválido dado um valor pequeno suficiente. Os testes de regressão desta versão pregam as duas formas de falha: viram o separador para vírgula, chamam a API e vasculham o conteúdo resultante à procura de qualquer token com vírgula ou expoente
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
OldSeparator: Char;
begin
Lib := TPDFlib.Create;
try
Lib.SetPageDimensions(300, 200);
Lib.DrawBox(10, 10, 20, 20, 1);
OldSeparator := FormatSettings.DecimalSeparator;
try
FormatSettings.DecimalSeparator := ','; // simular um desktop de-DE
Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
// v3.539.26 e posteriores escrevem: 0.5 0 0 0.25 0.00001 12.75 cm
// builds antigas escreviam: 0,5 0 0 0,25 1E-5 12,75 cm
Writeln(Lib.GetPageContentToString);
finally
FormatSettings.DecimalSeparator := OldSeparator;
end;
finally
Lib.Free;
end;
end;
Dois tipos de números, duas famílias de helpers
A correção no PDFlibPas é uma divisão estrita: os números mostrados a pessoas podem seguir a locale, e os números escritos para uma máquina nunca a seguem. O PLFloatToStr e o PLStrToFloat ficam no PDFlibExtra.pas para texto voltado ao utilizador, e a declaração deles agora carrega um comentário a dizer precisamente isso. Tudo o que acaba como sintaxe PDF passa pelo PLDoubleToStrConst com um número fixo de casas decimais escolhido para o trabalho: seis para matrizes, quatro para coordenadas e ajustes TJ, três para cores e retângulos FDF. A auditoria da v3.539.26 tocou mais sítios de chamada do que o relatório de bug original sugeria:
AddPageMatrix,ScalePageeDeskewPage, que todos antepõem umcmao conteúdo existente da página- Os construtores de elementos de página que emitem resets
Tm, avançosTJe transformaçõescm - Matrizes de posicionamento de glifos e pontos de contorno no conversor text-to-path
- A caixa de preenchimento preta que o
RedactRegionantepõe, os valores/Rectna exportação FDF e os operandos que a recoloração escreve
O PLDoubleToStrConst é um formatador escrito à mão em vez de um wrapper em volta do FloatToStrF, e três das suas propriedades interessa aqui. Escreve sempre um ponto e corta os zeros finais, por isso 0.5 continua 0.5 em vez de 0.500000. Nunca escreve expoente para input finito. E um valor não nulo menor do que a precisão pedida guarda os seus dígitos significativos em vez de colapsar para zero, por isso o PLDoubleToStrConst(1E-9, 6) devolve 0.000000001; só valores abaixo de cerca de 5E-16 se tornam 0. Essa última regra existe porque arredondar um fator de escala minúsculo para zero transforma uma matriz válida numa singular, que é um bug pior do que o que se estava a corrigir
uses
PDFlibExtra;
procedure CheckNumberHelpers;
var
V: Double;
begin
// Output de máquina: decimal por ponto, sem expoente, zeros finais cortados
Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235'); // guarda 4 dígitos significativos
Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');
// Input de máquina: falha suave em vez de EConvertError
Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
Assert(not PLTryStrToFloatInvariant('0,5', V)); // números de conteúdo nunca usam vírgula
end;
Porque é que o lado da interpretação é mais perigoso do que o da escrita?
O lado da interpretação é mais perigoso porque um parser preso à locale não produz um número errado, ele rebenta. O PLStrToFloat chama o StrToFloat, que levanta EConvertError quando o texto não casa com o separador do sistema. Num sistema de vírgula decimal isso significava que o RecolorPage abortava mal encontrasse um operador 0.5 g banal, por isso toda a página real falhava, e não só as exóticas. O RenderPageRegionToFile recusava o formato de recorte "10.5,20.5,50.5,40.5" que a própria documentação descreve, e atributos de comprimento SVG, cores de exportação SVG, listas de vértices de anotações e valores de solidez de output intent eram recusados ou substituídos em silêncio por predefinições. Uma biblioteca que funciona na perfeição na máquina do programador e falha no primeiro cliente em Munique é exatamente o tipo de código que, como os casos em o artigo sobre código Delphi que funciona por acidente, só parece correto por causa de onde foi testado
A v3.539.33 classificou cada chamada a StrToFloat e TryStrToFloat pela origem do input. Operandos de content stream, atributos SVG, strings de cor do painter e listas de recorte e vértices separadas por vírgulas têm todos sintaxe de ponto fixa, por isso agora passam pelo PLTryStrToFloatInvariant, que corta os espaços do texto, interpreta-o com PLInvariantFormatSettings e devolve False para input vazio, mal formado ou não finito em vez de levantar. Uma lista separada por vírgulas não deixa espaço para compromisso, porque uma vírgula não pode ser ao mesmo tempo delimitador de lista e marca decimal. O mesmo passe corrigiu também uma escrita fora dos limites: o RenderPageRegionToFile guardava um quinto valor de recorte para lá do seu buffer de quatro elementos. Para a pipeline de recoloração descrita em o guia para converter um PDF para um só espaço de cor, o resultado prático é que o RecolorPage e o RecolorDocument já não abortam num sistema de vírgula decimal. Os valores de regras que um chamador digita no CheckDocumentPolicy são o caso de interpretação que usa o helper leniente em vez disso, pela razão que a secção seguinte explica
O que acontece se corrigir só um lado de um round trip?
Corrigir só um lado de um round trip de locale parte código que funcionava, e é por isso que a alteração dos atributos de estrutura na v3.539.32 moveu o escritor e o leitor juntos. Os wrappers SetStructElem* transmitem os números como strings: o SetStructElemBBox formata quatro valores numa string, guarda-a através do AddTagAttribute, e o escritor de /A interpreta depois essa string para decidir se se torna num número, numa matriz ou num nome. Os dois lados usavam a locale do sistema, por isso num sistema de vírgula decimal o round trip era auto-consistente. O bug aparecia só quando um chamador seguia a documentação e passava "0.5" ao AddTagAttribute: o leitor não conseguia interpretá-lo e emitia o nome PDF /0.5. O placeholder PDF/VCR tinha o problema em espelho, porque a biblioteca gerava GTS_BBox com ponto e depois validava-o com a locale antes de guardar
Mudar só o escritor para ponto teria sido pior do que não fazer nada, já que todos os valores SetStructElem* falhariam então no leitor preso à locale e degradariam para nome. Por isso os escritores agora usam PLDoubleToStrConst(v, 6), e o leitor usa o novo PLTryStrToFloatLenient, que tenta primeiro a forma com ponto e recua para a locale do sistema. Um chamador de locale de vírgula que passasse "1,25" no passado continua a obter o número 1.25. A troca é deliberada e documentada: num sistema alemão "1.500" tornava-se num nome porque o StrToFloat recusa separadores de milhares, e agora lê-se como 1.5, enquanto as strings literais NAN e INF já não são aceites como números
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
begin
FormatSettings.DecimalSeparator := ','; // chamador de vírgula decimal
Lib := TPDFlib.Create;
try
Lib.BeginTag('Figure', 'Sales chart', '');
Lib.SetStructElemBBox(10.5, 20.25, 200.5, 100.75); // /BBox [ 10.5 20.25 200.5 100.75 ]
Lib.AddTagAttribute('Layout', 'SpaceAfter', '0.5'); // /SpaceAfter 0.5, antes /0.5
Lib.AddTagAttribute('Layout', 'StartIndent', '1,25'); // continua /StartIndent 1.25
Lib.DrawText(20, 20, 'figure');
Lib.EndTag;
Lib.SaveToFile('tagged.pdf');
finally
Lib.Free;
end;
end;
Onde NaN e infinito são travados
O AddPageMatrix, o ScalePage e o RedactRegion agora recusam argumentos NaN e infinitos à partida e devolvem 0, porque número PDF nenhum os consegue representar. O ScalePage já recusava fatores zero ou negativos, mas NaN passa num teste <= 0, por isso uma escala NaN viajava até ao formatador. Na v3.539.26 esse formatador ainda chamava Round sobre NaN, o que levanta EInvalidOp em Win32 onde a unidade x87 não mascara operações inválidas; a v3.539.31 fez o PLDoubleToStrConst escrever 0 para NaN como última linha de defesa, mas um zero numa matriz é uma transformação singular, por isso a verificação ao nível da API continua a ser a correção verdadeira. Duas fronteiras ficam de propósito. As strings de estado de metafile são escritas e lidas com a locale dentro de um processo e nunca saem dele, por isso ficaram como estão. E um teste que formata 1E-5 pelo caminho dos elementos de página tem de ler o conteúdo antes de a camada ser reescrita, porque reemitir operandos à precisão do documento transforma legitimamente esse valor em 0
Se a sua aplicação chega a clientes fora do mundo do ponto decimal, o hábito mais seguro é o que a suite de testes do PDFlibPas agora usa: corra os caminhos que produzem PDF uma vez com o FormatSettings.DecimalSeparator posto em vírgula e vasculhe o output à procura de vírgulas e expoentes. O artigo sobre preservar a precisão decimal interpretada cobre a outra metade da mesma história, como os números lidos de um ficheiro existente mantêm o texto exato ao guardar. Downloads, a referência completa da API e a build de avaliação estão na página de produto da PDFlibPas Delphi PDF library