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
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
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
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