O HotPDF decodifica símbolos QR rotacionados numa página PDF carregada normalizando a matriz de módulos amostrada por todas as oito orientações D4 dentro do próprio decoder. A retry de rotação externa que funciona para simbologias lineares não pode funcionar para QR, e entender o porquê te poupa um dia caçando um decoder que parece quebrado mas não está
O cenário é suficientemente comum. Notas de entrega digitalizadas chegam como PDFs, cada página carrega uma etiqueta QR, e o operador do scanner alimentou uma pilha de folhas na direção que a bandeja aceitou. Algumas etiquetas estão em pé, algumas com um quarto de giro, algumas de cabeça para baixo. Você chama o decoder de barcodes, metade das páginas resolve, e a outra metade volta vazia sem erro nenhum
Por que rotacionar a máscara de varredura nunca conserta um QR rotacionado?
Porque o layout de finder patterns de um QR é deliberadamente assimétrico, e uma rotação da imagem inteira preserva essa assimetria em vez de removê-la. O QR Code posiciona três quadrados finder nos cantos superior esquerdo, superior direito e inferior esquerdo, e deixa o canto inferior direito vazio (ISO/IEC 18004:2015 §6.3.3). Esse canto faltante é a pista de orientação. Rode o bitmap da página em noventa graus e o vão simplesmente se move para outro canto. Não existe rotação não trivial do plano que mapeie um layout de três cantos de volta sobre si mesmo, então um decoder que só aceita o arranjo canônico rejeita cada tentativa por vez
Isso importa porque o conserto óbvio é o errado. O instinto natural é pendurar a retry do lado de fora: renderizar a página, entregar a máscara ao decoder, e se falhar, rotacionar a máscara e tentar de novo para 90, 180 e 270 graus. Para Code 39 essa política é exatamente a certa, porque uma simbologia linear tem um padrão de start e stop que o scanner acha assim que as barras correm na horizontal. Para QR são quatro falhas garantidas seguidas de um relatório de nada encontrado
O grupo D4 aplicado à matriz de módulos
O lugar correto para a normalização é depois da amostragem, no grid booleano de módulos, e não na máscara de pixels. Uma vez que o decoder resolveu o símbolo numa matriz n por n de módulos escuros e claros, ele pode enumerar o grupo diedral do quadrado: quatro rotações vezes duas reflexões, oito orientações candidatas no total. Para cada candidata ele confere o triângulo de finders, e a primeira candidata cujos três finders caem nas posições superior esquerdo, superior direito e inferior esquerdo é a orientação verdadeira. A partir daí a pipeline existente roda sem mudanças, porque os bits de format information, o posicionamento zigzag dos dados e a correção Reed-Solomon todos assumem uma matriz canônica e agora recebem uma
Duas propriedades tornam isso barato. A matriz é pequena comparada ao bitmap renderizado, então oito transpostas custam muito menos que oito renderizações de página. E a matriz é um array booleano limpo construído pelo amostrador, então nenhuma transformação no caminho pode introduzir valores que nunca foram amostrados
Detecção de versão é uma busca de divisibilidade, não uma divisão
A contagem de módulos não pode ser derivada dividindo a largura amostrada por um tamanho de módulo presumido, e errar isso é uma fonte sutil de falhas de decode em renderizações de alta resolução. Um símbolo QR de versão v tem 4v + 17 módulos de lado, então a versão 1 tem 21 módulos e a versão 40 tem 177. Uma máscara que mede 126 pixels de largura é igualmente consistente com a versão 1 a seis pixels por módulo e com várias versões mais altas em tamanhos menores de módulo. A divisão linear escolhe uma delas e costuma estar errada
O que funciona é uma busca de divisibilidade sobre as versões candidatas. Percorra da versão 40 até a versão 1, mantenha as candidatas cuja contagem de módulos divide a largura amostrada sem resto e deixa pelo menos três pixels por módulo, e fique com a menor versão sobrevivente. O piso de três pixels é o que impede a busca de aceitar uma leitura absurdamente densa de um símbolo grosso, e a regra da menor versão resolve a ambiguidade restante em favor da leitura que um scanner realmente produziria
var
Pdf: THotPDF;
Options: THPDFBarcodeDecodeOptions;
Codes: THPDFDecodedBarcodes;
Info: THPDFBarcodeDecodeInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('delivery-notes.pdf');
Options := THPDFBarcodeDecodeOptions.Default;
Options.DPI := 300;
Options.RotationPolicy := bdrpFallback;
Options.MinimumConfidence := 0.5;
Options.MaxResults := 16;
if Pdf.DecodeLoadedPageBarcodes(0, Options, Codes, Info) then
for I := 0 to High(Codes) do
if Codes[I].Symbology = bsyQRCode then
Writeln(Codes[I].Text, ' at ',
Format('%.0f', [Codes[I].OrientationDegrees]), ' degrees');
finally
Pdf.Free;
end;
end;
THPDFBarcodeDecodeOptions.Default devolve um record preenchido em vez de um zerado, o que importa porque um DPI de zero ou um limite de resultados de zero é um jeito de aparência válida de não receber nada. O RotationPolicy controla só a retry externa: bdrpNone renderiza uma vez, bdrpFallback repete as outras orientações depois de uma primeira passada fracassada, e bdrpAll renderiza todas as orientações incondicionalmente. Como a normalização de QR acontece dentro do decoder, páginas QR resolvem na primeira tentativa sob qualquer uma das três políticas. A política está lá para as simbologias lineares que genuinamente precisam dela
Como provar que uma transformação de bitmap não está inventando pixels?
Conte a tinta dos dois lados e exija que os totais batam. Uma rotação é uma permutação de pixels, nada mais, então o número de células não nulas na saída precisa ser igual ao da entrada. Quando uma rotação de máscara no caminho de retry externo reportou 4800 células ligadas entrando e 7439 saindo, essa única comparação bastou para condenar a transformação sem ler uma linha da geometria dela
A causa era banal e vale levar como regra. Um array dinâmico dimensionado com SetLength não tem garantia de chegar zerado quando é resultado de função viajando por um caminho que a runtime não limpa, e as células que a rotação nunca escreve carregam os bytes que estavam ali antes. Alguns desses bytes velhos são não nulos, e não nulo significa tinta. O conserto é uma linha, FillChar(Result[0], N, 0) antes do loop de permutação rodar, e a disciplina que isso implica é mais ampla: toda função que devolve uma máscara ou buffer de bitmap deve limpar a saída explicitamente em vez de confiar na semântica de alocação
O que fez o defeito sobreviver a três releases é mais interessante que o defeito. Quando o QR moveu o tratamento de orientação para dentro do decoder, o QR parou de exercitar a rotação externa de máscara por completo, e o único consumidor restante daquele caminho de código era o Code 39. Infraestrutura compartilhada esconde bugs assim o tempo todo: a cobertura de uma funcionalidade faz um caminho parecer testado enquanto a funcionalidade que de fato depende dele não tem nenhuma própria. Todo caminho que uma funcionalidade nova para de usar precisa de um teste que ainda o use
Lendo os resultados de volta em coordenadas de página
Todo valor geométrico que o decoder produz é expresso no frame de coordenadas do bitmap da tentativa, e o caller precisa dele no user space do PDF. Essa conversão roda em dois estágios: desfazer o quarto de giro que a retry aplicou, e então desfazer a transformação de render que mapeou o user space no bitmap. O que chega em THPDFDecodedBarcode é uma bounding box alinhada aos eixos no user space, com Left, Bottom, Right e Top seguindo a convenção do PDF de Y crescer para cima, mais um OrientationDegrees anti-horário
Errar a direção dessa segunda conversão e o sintoma é desagradável: o texto decodifica perfeitamente, mas a caixa que você desenha para um overlay de revisão cai na imagem espelhada da posição certa. Quem estiver construindo uma interface de revisão sobre o decoder deve fazer assert contra um fixture conhecido, com um símbolo posicionado deliberadamente perto de um canto da página para que um eixo Y invertido fique visível de relance. O mesmo raciocínio vale para qualquer coordenada cruzando a fronteira de renderização, que é por que renderizar uma página PDF para um bitmap em Delphi vale a pena entender antes de construir sobre o decoder
O que o decoder embutido faz e o que ele não faz
O decoder embutido é uma implementação com limites e sem dependências, e é honesto quanto aos seus limites em vez de degradar em silêncio. Ele reconhece Code 39 e QR, valida os bits de formato protegidos por BCH e o padrão de máscara antes de publicar qualquer dado, e não tenta recuperação de erro em símbolos danificados. Se a sua entrada é uma fotografia de uma etiqueta curva sob luz desigual, isso é uma classe de problema diferente e pede um engine especializado
// Troque pelo seu próprio engine: implemente IHPDFBarcodeDecoder e passe-o ao
// overload que aceita decoder. O HotPDF continua dono da renderização de
// páginas, dos orçamentos, do mapeamento de coordenadas e da deduplicação
if not Pdf.DecodeLoadedPageBarcodes(PageIndex, MyDecoder, Options,
Codes, Info) then
case Info.Status of
bdsBudgetExceeded:
Log('raise MaxPixels or lower DPI: ' + string(Info.Diagnostic));
bdsRenderError:
Log('page did not render: ' + string(Info.Diagnostic));
bdsDecoderError:
Log(string(Info.DecoderName) + ' failed: ' + string(Info.Diagnostic));
end;
THPDFBarcodeDecodeInfo é onde uma pipeline de produção mostra o valor. RotationAttemptCount e DecoderCallCount dizem se a retry externa rodou ou não, ReceivedResultCount contra AcceptedResultCount separa um decoder que não achou nada de um limiar de confiança que rejeitou tudo que ele achou, e RenderedPixels com PeakWorkingBytes é o que você coloca num gráfico quando um job em lote começa a sofrer. Um conjunto de resultados vazio mais bdsSucceeded significa que a página realmente não tem símbolo legível, o que é um fato operacional diferente de bdsBudgetExceeded
Os campos de orçamento merecem uma decisão deliberada, não um padrão. MaxPixels e MaxWorkingBytes existem porque DPI multiplica quadraticamente: ir de 300 para 600 DPI numa página A4 quadruplica tanto o custo de render quanto a alocação de pico, e uma entrada não confiável declarando uma caixa de página enorme pode transformar um job de digitalização num incidente de out-of-memory. Ajuste os limites para o que o seu pior documento legítimo precisa, e então deixe o bdsBudgetExceeded encaminhar os outliers para um caminho mais lento e isolado
Se os seus documentos misturam etiquetas legíveis por máquina com texto impresso que você planeja indexar, o decoder de barcodes combina naturalmente com o engine de reconhecimento coberto em OCR de template matching dentro do HotPDF, e o lado de geração dessa mesma história está em desenhar barcodes num PDF com o HotPDF. Os dois rodam na mesma infraestrutura de renderização e orçamento, então uma pipeline que já define limites sensatos para um ganha o outro quase de graça
Tolerância a rotação é daquelas funcionalidades invisíveis quando funciona e infuriantes quando não funciona, e a lição de engenharia se generaliza para além do QR: normalize o mais perto da representação semântica que você conseguir, e não na camada de pixels, onde os dados ainda carregam cada acidente de como foram capturados. O HotPDF entrega isso como parte do componente PDF HotPDF para Delphi, junto com as peças de renderização, OCR e análise de página de que as mesmas pipelines de entrada costumam precisar