Artigo Técnico

Descodificar códigos QR rodados em páginas PDF com HotPDF

O HotPDF descodifica símbolos QR rodados numa página PDF carregada normalizando a matriz de módulos amostrada por todas as oito orientações D4 dentro do próprio descodificador. A repetição por rotação exterior que funciona para simbologias lineares não pode funcionar para QR, e perceber porque poupa-lhe um dia a perseguir um descodificador que parece partido mas não está

O cenário é suficientemente corriqueiro. Guias de entrega digitalizadas chegam como PDFs, cada página traz uma etiqueta QR, e o operador do scanner alimentou uma pilha de folhas na direção que a bandeija aceitou. Umas etiquetas estão dereitas, outras desviadas um quarto de volta, algumas às avessas. Chama o descodificador de códigos de barras, metade das páginas resolve, e a outra metade volta vazia sem erro nenhum

Porque é que rodar a máscara de varrimento nunca arranja um QR rodado?

Porque a disposição dos padrões de deteção (finder patterns) de um QR é deliberadamente assimétrica, e uma rotação da imagem inteira preserva essa assimetria em vez de a remover. O QR Code coloca 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 em falta é a pista de orientação. Rode o bitmap da página noventa graus e o oco simplesmente muda para outro canto. Não há rotação não trivial do plano que mapeie uma disposição de três cantos de volta sobre si própria, por isso um descodificador que só aceita a disposição canónica rejeita todas as tentativas em sequência

Isto importa porque a correção óbvia é a errada. O instinto natural é pendurar a repetição do lado de fora: renderizar a página, entregar a máscara ao descodificador, e se falhar, rodar a máscara e tentar de novo a 90, 180 e 270 graus. Para Code 39 essa política é exatamente a certa, porque uma simbologia linear tem um padrão de início e fim que o scanner encontra mal as barras corram 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 sítio certo para a normalização é depois da amostragem, na grelha booleana de módulos e não na máscara de pixels. Quando o descodificador já resolveu o símbolo numa matriz n por n de módulos escuros e claros, pode enumerar o grupo diedral do quadrado: quatro rotações vezes duas reflexões, oito orientações candidatas no total. Para cada candidata verifica 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í o pipeline existente corre sem alterações, porque os bits de informação de formato, a colocação de dados em ziguezague e a correção Reed-Solomon todos assumem uma matriz canónica e agora recebem uma

Quatro representações da mesma matriz de módulos QR do HotPDF sob as rotações do grupo D4 a 0, 90, 180 e 270 graus, mostrando os três padrões de deteção a migrar de canto enquanto o canto vazio se move com eles, de modo que só a orientação canónica apresenta finders no superior esquerdo, superior direito e inferior esquerdo ao descodificador
Rodar a máscara de pixels não consegue remover a assimetria dos finders do QR, por isso o HotPDF enumera as orientações D4 sobre a matriz de módulos amostrada e fica com a primeira candidata cujos finders caem no superior esquerdo, superior direito e inferior esquerdo

Duas propriedades tornam isto barato. A matriz é pequena em comparação com o bitmap renderizado, por isso oito transposições custam muito menos do que oito renderizações de página. E a matriz é um array booleano limpo construído pelo amostrador, por isso nenhuma transformação ao longo do caminho pode introduzir valores que nunca foram amostrados

A deteção de versão é uma pesquisa 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 assumido, e errar aqui é uma fonte subtil de falhas de descodificação em renderizações de alta resolução. Um símbolo QR de versão v tem 4v + 17 módulos de lado, por isso 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 superiores a tamanhos de módulo menores. A divisão linear escolhe uma delas e normalmente erra

O que funciona é uma pesquisa de divisibilidade sobre as versões candidatas. Percorra da versão 40 até à versão 1, guarde as candidatas cuja contagem de módulos divide a largura amostrada de forma exata 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 pesquisa de aceitar uma leitura absurdamente densa de um símbolo grosseiro, e a regra da menor versão resolve a ambiguidade restante a favor da leitura que um scanner produziria de facto

A travessia de deteção de versão do HotPDF para um símbolo QR numa máscara amostrada de 126 pixels, testando cada contagem de módulos candidata 4v mais 17 da versão 40 até à versão 1 por divisibilidade exata e um piso de três pixels por módulo antes de a menor versão sobrevivente vencer
Uma contagem de módulos QR vem de uma pesquisa de divisibilidade sobre versões candidatas, e não de dividir a largura da máscara por um tamanho de módulo assumido, e a menor versão sobrevivente resolve a ambiguidade
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 registo preenchido em vez de um a zeros, o que importa porque um DPI de zero ou um teto de resultados de zero é uma forma com ar válido de não receber nada. RotationPolicy controla apenas a repetição exterior: bdrpNone renderiza uma vez, bdrpFallback volta a tentar as outras orientações depois de uma primeira passagem falhada, e bdrpAll renderiza todas as orientações incondicionalmente. Como a normalização QR acontece dentro do descodificador, as páginas QR resolvem à primeira tentativa sob qualquer das três políticas. A política está lá para as simbologias lineares que genuinamente a precisam

Como prova que uma transformação de bitmap não está a inventar pixels?

Conte a tinta dos dois lados e exija que os totais batam certo. Uma rotação é uma permutação de pixels, nada mais, por isso o número de células não nulas no output tem de igualar o número na input. Quando uma rotação de máscara no caminho de repetição exterior reportou 4800 células postas à entrada e 7439 à saída, essa única comparação chegou para condenar a transformação sem ler uma linha da sua geometria

A causa era mundana e vale a pena levar como regra. Um array dinâmico dimensionado com SetLength não tem garantia de chegar a zeros quando é o resultado de uma função que viaja por um caminho que o runtime não limpa, e as células que a rotação nunca escreve carregam então os bytes que lá estavam antes. Alguns desses bytes velhos são não nulos, e não nulo significa tinta. A correção é uma linha, FillChar(Result[0], N, 0) antes de o ciclo de permutação correr, e a disciplina que implica é mais ampla: qualquer função que devolva uma máscara ou um buffer de bitmap deve limpar explicitamente o seu output em vez de confiar na semântica de alocação

O que fez o defeito sobreviver a três releases é mais interessante do que o defeito. Quando o QR mudou o tratamento de orientação para dentro do descodificador, o QR deixou de exercitar de todo a rotação exterior de máscara, e o único consumidor restante desse caminho de código era o Code 39. A infraestrutura partilhada esconde bugs destes o tempo todo: a cobertura de uma funcionalidade faz um caminho parecer testado enquanto a funcionalidade que realmente dele depende não tem nenhuma própria. Todo o caminho de que uma funcionalidade nova deixa de precisar precisa de um teste que continue a usá-lo

Ler os resultados de volta em coordenadas da página

Todos os valores geométricos que o descodificador produz são expressos no referencial de coordenadas do bitmap da tentativa, e o chamador precisa deles no user space do PDF. Essa conversão corre em duas fases: desfazer o quarto de volta que a repetição aplicou, e depois desfazer a transformação de renderização que mapeou o user space no bitmap. O que chega em THPDFDecodedBarcode é uma caixa delimitadora alinhada aos eixos no user space, com Left, Bottom, Right e Top seguindo a convenção PDF de Y crescer para cima, mais um OrientationDegrees no sentido contrário ao dos ponteiros

O pipeline de códigos de barras do HotPDF, do bitmap da página renderizada através da amostragem para uma matriz booleana de módulos, normalização D4, deteção de versão por divisibilidade e descodificação Reed-Solomon, e depois a conversão de coordenadas em duas fases que desfaz o quarto de volta da repetição e a transformação de renderização antes de o THPDFDecodedBarcode publicar Left, Bottom, Right, Top e OrientationDegrees no user space
A normalização QR dentro do descodificador deixa as páginas resolverem à primeira tentativa, enquanto a conversão de coordenadas em duas fases transforma resultados do bitmap da tentativa em caixas do user space alinhadas aos eixos

Errar o sentido dessa segunda conversão e o sintoma é desagradável: o texto descodifica na perfeição, mas a caixa que desenha para uma sobreposição de revisão cai na imagem espelhada da posição certa. Quem constrói uma interface de revisão sobre o descodificador deve afirmar contra um fixture conhecido, com um símbolo colocado deliberadamente perto de um canto da página para que um eixo Y invertido se veja num relance. O mesmo raciocínio aplica-se a qualquer coordenada que cruze a fronteira de renderização, e é por isso que vale a pena compreender renderizar uma página PDF para um bitmap em Delphi antes de construir sobre o descodificador

O que o descodificador incorporado faz e não faz

O descodificador incorporado é uma implementação limitada e sem dependências, e é honesto quanto aos seus limites em vez de degradar em silêncio. Reconhece Code 39 e QR, valida os bits de formato protegidos por BCH e o padrão de máscara antes de publicar dados, e não tenta recuperação de erros em símbolos danificados. Se o seu input é a fotografia de uma etiqueta curva sob luz desigual, essa é uma classe de problema diferente e quer um motor especializado

// Troque pelo seu próprio motor: implemente IHPDFBarcodeDecoder e passe-o
// à overload consciente do descodificador. O HotPDF continua a tratar da
// renderização de páginas, orçamentos, mapeamento de coordenadas e 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 um pipeline de produção ganha o seu ordenado. RotationAttemptCount e DecoderCallCount dizem-lhe se a repetição exterior correu de todo, ReceivedResultCount contra AcceptedResultCount separa um descodificador que não encontrou nada de um limiar de confiança que rejeitou tudo o que encontrou, e RenderedPixels com PeakWorkingBytes é o que se gráfica quando um trabalho de lote começa a debater-se. Um conjunto de resultados vazio mais bdsSucceeded significa que a página realmente não tem símbolo legível, o que é um facto operacional diferente de bdsBudgetExceeded

Os campos de orçamento merecem uma decisão deliberada e não uma predefinição. MaxPixels e MaxWorkingBytes existem porque o DPI multiplica quadraticamente: passar de 300 para 600 DPI numa página A4 quadruplica tanto o custo de renderização como a alocação de pico, e um input não fidedigno que declare uma caixa de página enorme pode transformar um trabalho de digitalização num incidente de falta de memória. Fixe os tetos no de que o seu pior documento legítimo precisa, e depois deixe bdsBudgetExceeded encaminhar os valores atípicos para um caminho mais lento e isolado

Se os seus documentos misturam etiquetas legíveis por máquina com texto impresso que tenciona indexar, o descodificador de códigos de barras emparelha naturalmente com o motor de reconhecimento coberto em OCR por correspondência de templates dentro do HotPDF, e o lado da geração da mesma história está em desenhar códigos de barras num PDF com HotPDF. Ambos correm na mesma infraestrutura de renderização e orçamentos, por isso um pipeline que já fixa limites sensatos para um recebe o outro quase de graça

A tolerância à rotação é uma dessas funcionalidades que é invisível quando funciona e enfurecedora quando não funciona, e a lição de engenharia generaliza para além do QR: normalize o mais perto da representação semântica que conseguir alcançar, e não na camada de pixels onde os dados ainda transportam todos os acasos de como foram capturados. O HotPDF distribui isto como parte do componente PDF Delphi HotPDF, a par das peças de renderização, OCR e análise de páginas que os mesmos pipelines de entrada normalmente precisam