A PDF Library for Delphi remove um operador de matriz de texto identity, 1 0 0 1 0 0 Tm, durante a otimização peephole dos content streams no momento de guardar apenas quando a text matrix e a text line matrix já são a identidade: logo a seguir a BT, ou logo a seguir a um Tm identity anterior. Um cm identity continua sempre a ser eliminado, porque o cm multiplica a CTM enquanto o Tm substitui ambas as matrizes de texto por completo. Desde a v3.539.28 que qualquer outro Tm identity fica no stream
O bug que isto corrige é do tipo silencioso. Um gerador de relatórios emite BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET, confiando no Tm identity para mandar a segunda string de volta à origem do text space antes de aplicar a sua própria lógica de posicionamento. O otimizador antigo via seis números que soletravam a matriz identidade, decidiu que o operador não podia mudar coisa nenhuma, e apagou-o. Nada falhou, nada registou aviso, e a página guardada desenhou «Total» imediatamente a seguir a «Invoice» na mesma linha de base, que é exatamente a classe de defeito que ninguém repara até um cliente imprimir o PDF
Porque é que 1 0 0 1 0 0 Tm nem sempre é um no-op?
Um Tm identity só é um no-op quando substituiria duas matrizes que já guardam a identidade, e isso é uma propriedade dos operadores anteriores a ele, não dos seus próprios operandos. A ISO 32000-1 §9.4.1 diz que o BT inicializa tanto a text matrix (Tm) como a text line matrix (Tlm) com a identidade, e a §9.4.2 define o Tm como pondo ambas com os valores dados, e não como concatenar para cima delas. Compare com o cm (§8.4.4), que multiplica à direita a matriz de transformação corrente: multiplicar pela identidade deixa qualquer CTM intacta, por isso 1 0 0 1 0 0 cm pode ser apagado em qualquer sítio. Dentro de um objeto de texto o quadro é diferente. Td, TD, T* e um Tm não identity movem todos a Tlm, e cada operador que mostra texto (Tj, TJ, ', ") avança a Tm pela largura dos glifos que pintou. Depois de qualquer deles, um Tm identity é um reset genuíno para a origem. Se já segue posições de texto à mão com o seguidor de estado de CTM e text matrix do content stream, esta é a mesma distinção entre concatenar estado e substituí-lo
Como o scan para trás decide que Tm identity eliminar
O TPDFContentPeepholeOptimizer.RemoveIdentityMatrices agora caminha para trás a partir de cada Tm identity e só o elimina se o scan chegar primeiro a BT ou a outro Tm identity. O Tm identity anterior conta quer seja mantido quer tenha ele próprio acabado de ser agendado para eliminação, porque em qualquer dos casos deixou as duas matrizes na identidade, exatamente como o BT faz. A regra separa todos os operadores que pode encontrar em dois grupos:
- Parar e manter o Tm:
Td,TD,T*, umTmnão identity,Tj,TJ,',",ET, qualquer operador que o parser não reconheça, ou o início do stream - Passar por cima e continuar o scan: operadores que nunca tocam na Tm ou na Tlm, como
Tf,Tc, os definidores de cor,gs, operadores de marked-content ecm
Os casos conservadores são deliberados. Um operador desconhecido podia ser qualquer coisa, por isso o scan recusa raciocinar para lá dele. O ET fecha o objeto de texto, por isso um Tm depois dele não tem BT nenhum a garantir os valores da matriz. O scan também trabalha num content stream de cada vez, o que interessa nas páginas cujo /Contents é uma matriz: uma camada que começa a meio de um objeto de texto, sem BT próprio, mantém o seu Tm identity mesmo quando a camada anterior o teria tornado redundante. Isso custa uns bytes em ficheiros estranhos e nunca move um glifo. Se edita texto de página ao nível da instrução, como no passo a passo do mapeamento de carácter para byte de conteúdo, o mesmo modelo TPDFContentProgram interpretado é o que o otimizador reescreve
uses
PDFlibContentModel, PDFlibContentOptimize;
function OptimizeSnippet(const Source: AnsiString): AnsiString;
var
Prog: TPDFContentProgram;
Optimizer: TPDFContentPeepholeOptimizer;
begin
Result := Source;
Prog := TPDFContentProgram.Create;
try
if not Prog.Parse(Source) then
Exit; // stream danificado: deixar os bytes quietos
Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
try
Optimizer.Run; // devolve o número de instruções removidas
finally
Optimizer.Free;
end;
Result := Prog.Emit; // uma instrução por linha
finally
Prog.Free;
end;
end;
// Removido: Tm logo a seguir a BT, o segundo de dois Tm identity seguidos
// OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Mantido: Tm depois de Td, depois de Tj, depois de um Tm não identity, ou fora de BT
// OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')
Corra o helper no stream da fatura da abertura e o Tm identity sobrevive, porque o scan para trás encontra o Tj antes de chegar ao BT. Ponha /F1 12 Tf, 2 Tc e 0 g entre o BT e o Tm identity e ele continua a sair, já que nenhum deles toca nas matrizes de texto. Uma sequência como BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm perde exatamente um operador: o primeiro Tm identity repõe a matriz que o Td moveu, e só o segundo é redundante
Quando é que o otimizador peephole corre de facto?
O otimizador corre apenas durante o passe de compressão, dentro do TPDFPageTree.Compress, e apenas em content streams que ainda não estejam comprimidos com Flate. O TPDFlib.SetOptimizeContentStreams(1) é o predefinido, e o mesmo interruptor está exposto como o campo OptimizeContentStreams de TPDFlibSaveOptions; tanto o CompressContent como o CompressPage o respeitam. Um stream cujo /Filter já é /FlateDecode é saltado por inteiro, por isso carregar um PDF comprimido existente e voltar a guardá-lo não reescreve os seus operadores. Se o stream falhar a interpretação, os bytes descodificados originais são comprimidos sem alteração. O TPDFlib.NormalizeContentStreams interpreta e reemite conteúdo com espaçamento e números canónicos mas nunca chama o otimizador, o que o torna uma base de comparação útil quando quer ver quanto do ganho de tamanho vem das regras peephole, a par dos ganhos maiores cobertos em otimização do tamanho de ficheiros PDF com font subsetting
var
Lib: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
// Streams não comprimidos passam pelas regras peephole, depois Flate
Lib.SetOptimizeContentStreams(1);
Lib.CompressContent;
Lib.SaveToFile('report-optimized.pdf');
// A mesma escolha pelas opções de gravação incluídas; False recusa
Options.CompressContent := True;
Options.CompressFonts := True;
Options.CompressImages := True;
Options.Linearize := False;
Options.KeepModDate := False;
Options.OptimizeContentStreams := False;
Options.GarbageCollect := False;
Options.PackObjectStreams := True;
Lib.SaveToFileOptions('report-plain.pdf', Options);
finally
Lib.Free;
end;
end;
O que garantia de facto o antigo teste de regressão?
O antigo teste de regressão garantia uma única forma: um Tm identity logo a seguir a BT é removido. O Peephole_RemovesIdentityTextMatrix alimenta o otimizador com BT 1 0 0 1 0 0 Tm (hello) Tj ET e afirma que nenhum Tm resta. Um lançamento anterior já tinha notado que eliminar um Tm identity é inseguro quando a Tlm não é a identidade, e mesmo assim manteve o comportamento porque o teste «trancava» o caso. Relido com atenção, o teste nada diz sobre um Tm identity depois de um Td ou depois de texto mostrado; tratar a cobertura de uma amostra como o contrato de toda a regra foi o erro verdadeiro. A correção mantém esse caso original a passar e acrescenta seis casos que pregam tanto as formas removíveis como as mantidas, incluindo um Tm fora de qualquer objeto de texto e um que segue um ET
A troca é fácil de aceitar logo que está escrita. Geradores que embrulham cada objeto de texto como BT 1 0 0 1 0 0 Tm ... continuam a ver esse operador redundante removido, que é donde veio quase toda a poupança. O que o otimizador abre mão é do Tm identity ocasional a meio de um objeto de texto, um punhado de bytes por página antes de o Flate os ver, em troca de uma garantia que o cabeçalho do módulo afirma sem rodeios: cada transformação é output-equivalente e nunca muda a página visível. Um otimizador de tamanho que move texto não é um otimizador, é um bug de rendering com boas taxas de compressão
O parser de content streams, o otimizador peephole e as opções de compressão na gravação aqui descritos vêm todos na PDF Library for Delphi e C++Builder, que expõe igualmente NormalizeContentStreams, CompressContent e TPDFlibSaveOptions para afinar a forma como cada documento é escrito