Camadas de texto OCR, limites de códigos de barras e caixas de redação de rostos derivam em páginas PDF recortadas quando os pixels do bitmap são mapeados de volta através do MediaBox em vez da caixa que o renderer realmente rasterizou: o CropBox recortado ao MediaBox (ISO 32000-1 §14.11.2). O HotPDF corrigiu isto para o ApplyLoadedOCRTextLayer na v2.770.153, e para o DecodeLoadedPageBarcodes e o DetectLoadedRedactionFindings na v2.770.154
O relatório de bug que normalmente chega tem este aspecto. Um arquivo de contratos digitalizados passa por OCR, o output é pesquisável, e o hit de pesquisa de um número de cláusula fica realçado meia polegada abaixo e à esquerda do número impresso. A maioria dos ficheiros do lote está bem. Os partidos vieram todos de uma estação de digitalização que escreve um /CropBox para aparar a margem do vidro. Esse único detalhe separa a imagem que o engine OCR viu da frame em que a camada de texto foi colocada, e o mesmo desajuste move limites de códigos de barras e, mais grave, caixas de redação de rostos
Porque é que a camada de texto OCR se afasta das palavras digitalizadas?
A camada de texto deriva porque duas metades do pipeline discordavam sobre que retângulo o bitmap cobre. Na v2.766.64, o HotPDF mudou a renderização, a exportação SVG, o viewer e a impressão para honrar o CropBox: uma página é mostrada através do seu CropBox recortado ao seu MediaBox, que é o que a ISO 32000-1 §14.11.2 prescreve, e o GetLoadedPageVisibleBox foi acrescentado para devolver essa caixa visível. As funcionalidades de reconhecimento continuaram a construir a sua transformação de dispositivo para página a partir do GetLoadedPageBox(PageIndex, pbMediaBox, ...). O raster agora cobria a caixa visível, a transformação ainda assumia o MediaBox, e cada posição reconhecida voltava deslocada pela distância entre as duas
A janela afetada é por isso precisa. O ApplyLoadedOCRTextLayer deslocou texto da v2.766.64 até à v2.770.152. O DecodeLoadedPageBarcodes de página inteira e a deteção de rostos dentro do DetectLoadedRedactionFindings ficaram errados mais uma build, até à v2.770.153. Antes da v2.766.64 o renderer desenhava o MediaBox inteiro, por isso mapeamento e raster concordavam, ao custo de reconhecer conteúdo que os viewers nunca mostram. As correções mudaram três coisas em conjunto para cada funcionalidade: a transformação, a estimativa do orçamento de pixels, e a caixa de página entregue a um engine personalizado no registo de pedido
Vários casos nunca foram afetados:
- Páginas sem
/CropBox, ou cujo CropBox iguala o MediaBox, mapeiam-se de forma idêntica antes e depois da correção - O
DecodeLoadedPageBarcodescomHasRegiondefinido renderiza exatamente a região que passa e mapeia através dessa mesma região, por isso a descodificação por região explícita esteve correta o tempo todo; a verificação de que a região está dentro da página ainda usa o MediaBox - Descobertas de redação por padrões (emails, números de cartão e afins) vêm da extração de texto em user space, não de um raster, por isso só as descobertas de deteção de rostos se moveram
Três frames de coordenadas, e que APIs do HotPDF usam cada uma
O código HotPDF que toca no reconhecimento lida com três frames, e a maioria dos bugs de mapeamento vem de misturar duas delas
- Pixels de bitmap: origem no topo esquerdo, Y cresce para baixo, as unidades são pixels no DPI do pedido. O
THPDFOCRWord.Left,Top,RighteBottomestão nesta frame, tal como os pontos de baseline opcionais, os resultados que umIHPDFBarcodeDecoderpersonalizado devolve, e as caixas de umIHPDFFaceDetectorpersonalizado - User space PDF de uma página carregada: origem no fundo esquerdo, Y cresce para cima, as unidades são pontos, com
Bottom < Top. OGetLoadedPageBoxe oGetLoadedPageVisibleBoxdevolvem Left, Bottom, Right, Top nesta frame, e também os camposPageLeft,PageBottom,PageRightePageTopdoTHPDFOCRRequest, os limites noTHPDFDecodedBarcodee os retângulos noTHPDFRedactionFinding - Coordenadas de desenho de página do HotPDF: a API que usa para construir páginas novas (output de texto, formas, códigos de barras, links, campos de formulário) trabalha com origem no topo esquerdo e Y a crescer para baixo. Essa frame pertence à geração de documentos e não tem nada a ver com as APIs de documentos carregados acima, por isso nunca alimente um retângulo user space de página carregada nela inalterado
O registo de palavras OCR é deliberadamente baseado em pixels: um engine reporta o que viu na imagem, e o ApplyLoadedOCRTextLayer é dono da conversão. Essa divisão só funciona quando a conversão usa a caixa certa, que é o que a v2.770.153 restaurou
A transformação de dispositivo para página atrás de OCR, códigos de barras e rostos
O HotPDF mapeia pixels de bitmap para a página com uma única matriz afim construída a partir de cinco inputs: a rotação, a escala DPI / 72, a altura do bitmap, e o Left, Bottom, Right e Top da caixa renderizada. OCR, descodificação de códigos de barras e deteção de rostos partilham todos uma única rotina para isto, razão pela qual uma caixa errada partiu os três da mesma maneira. Para uma página não rodada a matriz página-para-dispositivo [A B C D E F] é:
A = ScaleeD = -Scale, em queScale = DPI / 72; oDnegativo vira o user space (Y para cima) em espaço de bitmap (Y para baixo)B = C = 0, porque uma página não rodada não tem shear nem troca entre eixosE = -Left * Scale, que move a margem esquerda da caixa para a coluna de pixel 0F = BitmapHeight + Bottom * Scale, que mapeia a margem inferior da caixa para y = BitmapHeight, a margem inferior do bitmap, por isso a margem superior cai na linha 0
Os pixels voltam à página pelo inverso dessa matriz. O Request.PageRotation transporta o /Rotate da página normalizado a 0, 90, 180 ou 270 (qualquer valor que não seja múltiplo de 90 é tratado como 0), e o renderer roda a página no sentido dos ponteiros como a ISO 32000-1 §7.7.3.3 exige. Sob rotação os eixos trocam e um par diferente de margens da caixa é pregado à origem do bitmap. Escrito como fórmulas inversas, com S = DPI / 72, x e y em pixels e H a altura do bitmap:
| /Rotate | Página X | Página Y | Margens da caixa de que o mapeamento depende |
|---|---|---|---|
| 0 | Left + x / S | Bottom + (H - y) / S | Left, Bottom |
| 90 | Left + y / S | Bottom + x / S | Left, Bottom |
| 180 | Right - x / S | Bottom + y / S | Right, Bottom |
| 270 | Right - y / S | Top - x / S | Right, Top |
A última coluna explica porque é que o bug parecia aleatório em produção. Um CropBox que apare apenas o topo da página deixa Left e Bottom intactos, por isso páginas verticais saíam perfeitas e só páginas com /Rotate 270 derivavam. A rotação também troca as dimensões do bitmap: a 90 e 270 o bitmap tem (Top - Bottom) * S pixels de largura e (Right - Left) * S pixels de altura
O que corre mal com MediaBox [0 0 612 792] e CropBox [36 36 576 756]?
Com um recorte de meia polegada em cada lado, a camada de texto de uma página não rodada cai exatamente 36 pontos à esquerda e 36 pontos abaixo das palavras digitalizadas quando o MediaBox é usado. Tome uma página US Letter cujo CropBox apare 36 pontos (0,5 polegada) de cada margem. A caixa visível tem 540 por 720 pontos, por isso à resolução OCR por omissão de 300 DPI a escala é 300 / 72 ≈ 4,1667 e o bitmap é 2250 por 3000 pixels
Suponha que o engine reporta uma palavra com caixa em pixels Left 450, Top 600, Right 900, Bottom 660 e sem baseline. O HotPDF põe então a baseline a 20 por cento da altura da palavra acima da margem inferior, na linha de pixel 648, e mapeia o ponto inicial (450, 648):
- Através da caixa visível: x = 36 + 450 / 4,1667 = 144,0 e y = 36 + (3000 - 648) / 4,1667 = 600,48, que é onde a palavra está impressa
- Através do MediaBox: x = 0 + 108,0 = 108,0 e y = 0 + 564,48 = 564,48, um deslocamento uniforme de (-36, -36) pontos
Rode a mesma página e a direção do erro muda, porque margens diferentes estão envolvidas. A /Rotate 180 o termo X usa Right, e 612 em vez de 576 empurra a camada 36 pontos para a direita enquanto Bottom ainda a puxa 36 pontos para baixo. A /Rotate 270 tanto Right como Top são demasiado grandes, por isso a camada move-se 36 pontos para a direita e 36 pontos para cima. Um documento com orientações mistas pode mostrar a deriva em três direções, uma impressão digital fiável deste bug. Código escrito à mão que derive a escala a partir da caixa, como Bitmap.Width / (Right - Left), também estica cada coordenada por 612 / 540, cerca de 13 por cento, por cima do deslocamento
Que documentos PDF seus são afetados?
Um documento PDF está exposto quando pelo menos uma página tem uma caixa visível que difere do seu MediaBox, e o HotPDF consegue dizer-lhe isso em poucas linhas. Compare o GetLoadedPageBox com pbMediaBox contra o GetLoadedPageVisibleBox para cada página, e imprima o GetLoadedPageRotation ao lado para poder prever a direção da deriva a partir da tabela acima. O THPDFPageBoundary também oferece pbCropBox, pbBleedBox, pbTrimBox e pbArtBox, mas o GetLoadedPageBox(pbCropBox) recua para o MediaBox quando não existe crop box e não recorta, por isso a caixa visível é a coisa certa com que comparar
uses
System.SysUtils, HPDFDoc;
procedure ReportCroppedPages(const FileName: string);
var
Pdf: THotPDF;
I: Integer;
ML, MB, MR, MT, VL, VB, VR, VT, Tmp: Single;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile(FileName) < 1 then
raise Exception.Create('Cannot load ' + FileName);
for I := 0 to Pdf.LoadedPageCount - 1 do
begin
if not Pdf.GetLoadedPageBox(I, pbMediaBox, ML, MB, MR, MT) then
Continue;
// o array guardado pode listar os seus cantos por qualquer ordem
if MR < ML then begin Tmp := ML; ML := MR; MR := Tmp; end;
if MT < MB then begin Tmp := MB; MB := MT; MT := Tmp; end;
// já normalizada e recortada ao MediaBox
if not Pdf.GetLoadedPageVisibleBox(I, VL, VB, VR, VT) then
Continue;
if (Abs(VL - ML) > 0.01) or (Abs(VB - MB) > 0.01) or
(Abs(VR - MR) > 0.01) or (Abs(VT - MT) > 0.01) then
Writeln(Format('Page %d MediaBox [%g %g %g %g] visible [%g %g %g %g] /Rotate %d',
[I + 1, ML, MB, MR, MT, VL, VB, VR, VT,
Pdf.GetLoadedPageRotation(I)]));
end;
finally
Pdf.Free;
end;
end;
Dois detalhes do GetLoadedPageVisibleBox interessam para scripts como este. A função deixa os seus parâmetros out intocados quando falha, por isso pré-definir um tamanho de página por omissão antes da chamada é um padrão seguro. E quando um CropBox malformado não intersecta o MediaBox de todo, a função devolve o MediaBox em vez de um retângulo vazio. Se o relatório listar páginas e a build que tem em produção for anterior à v2.770.153 para OCR, ou à v2.770.154 para códigos de barras e rostos, corra de novo o reconhecimento nessas páginas depois do upgrade. Uma camada OCR consolidada por uma build afetada fica no ficheiro gravado, e a opção por omissão SkipPagesWithText vai saltar essas páginas numa segunda passagem a menos que a desligue ou remova primeiro a camada antiga
Como deve um IHPDFOCREngine personalizado mapear pixels de volta ao espaço PDF?
Um IHPDFOCREngine personalizado deve devolver caixas de palavras em pixels de bitmap e deixar o HotPDF fazer o mapeamento; converta para user space só para as suas próprias decisões, e nesse caso use a caixa do pedido, nunca o MediaBox. Desde a v2.770.153 os PageLeft, PageBottom, PageRight e PageTop do pedido descrevem a caixa visível renderizada, por isso correspondem exatamente ao Request.Bitmap. O helper abaixo é o inverso da transformação da biblioteca, incluindo o seu uso da altura real do bitmap para páginas verticais, por isso concorda com o HotPDF ao pixel
uses
System.SysUtils, System.Math, Vcl.Graphics, HPDFDoc;
// Pixel de bitmap (origem no topo esquerdo, Y para baixo) para user space PDF
// (origem no fundo esquerdo, Y para cima), pela caixa de que o bitmap foi renderizado
procedure HotPixelToPage(Rotation, DPI, BitmapHeight: Integer;
Left, Bottom, Right, Top: Single; X, Y: Double;
out PageX, PageY: Double);
var
S: Double;
begin
S := DPI / 72.0;
case Rotation of
90: begin PageX := Left + Y / S; PageY := Bottom + X / S; end;
180: begin PageX := Right - X / S; PageY := Bottom + Y / S; end;
270: begin PageX := Right - Y / S; PageY := Top - X / S; end;
else
PageX := Left + X / S;
PageY := Bottom + (BitmapHeight - Y) / S;
end;
end;
Uma razão realista para precisar de user space dentro de um engine é uma regra de zona: faturas cujo membro letterhead nunca quer pesquisável, ou uma área de selo que confunde o reconhecedor. O engine abaixo, escrito com TInterfacedObject para a contagem de referências tratar do seu tempo de vida, filtra palavras por onde os seus centros caem na página, e devolve depois as sobreviventes intocadas em coordenadas de pixels. O RunRecognizer faz de chamada ao seu próprio reconhecedor
type
TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
private
FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single; // user space
function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
out Words: THPDFOCRWords): boolean; // o seu reconhecedor, caixas em pixels
public
constructor Create(SkipLeft, SkipBottom, SkipRight, SkipTop: Single);
function GetName: AnsiString;
function Recognize(const Request: THPDFOCRRequest;
out Words: THPDFOCRWords; out Diagnostic: AnsiString): boolean;
end;
function TZoneFilterOCREngine.Recognize(const Request: THPDFOCRRequest;
out Words: THPDFOCRWords; out Diagnostic: AnsiString): boolean;
var
Raw: THPDFOCRWords;
I, Count: Integer;
CX, CY: Double;
begin
Diagnostic := '';
SetLength(Words, 0);
if not RunRecognizer(Request.Bitmap, Request.MaxWords, Raw) then
begin
Diagnostic := 'recognizer failed';
Exit(False);
end;
SetLength(Words, Length(Raw));
Count := 0;
for I := 0 to High(Raw) do
begin
HotPixelToPage(Request.PageRotation, Request.DPI,
Request.Bitmap.Height, Request.PageLeft, Request.PageBottom,
Request.PageRight, Request.PageTop,
(Raw[I].Left + Raw[I].Right) / 2, (Raw[I].Top + Raw[I].Bottom) / 2,
CX, CY);
if (CX >= FSkipLeft) and (CX <= FSkipRight) and
(CY >= FSkipBottom) and (CY <= FSkipTop) then
Continue;
Words[Count] := Raw[I]; // ainda pixels: o HotPDF mapeia-os ele próprio
Inc(Count);
end;
SetLength(Words, Count);
Result := True;
end;
Entregue o engine ao ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) como a qualquer outro engine. A biblioteca valida o que volta antes de confiar nele: uma palavra é descartada e contada no Info.DroppedWordCount quando a sua caixa sai do bitmap, quando Right <= Left ou Bottom <= Top, ou quando Confidence está fora de 0..1 ou abaixo do MinimumConfidence. Devolver mais palavras do que MaxWordsPerPage, ou empurrar o total corrente além do MaxTotalWords, falha a chamada inteira com um erro de orçamento, por isso honre o Request.MaxWords no engine. Não converta caixas de palavras para user space antes de as devolver; o HotPDF trataria os valores de pontos como pixels e a camada colapsaria para a origem do bitmap
Mapear o output do seu próprio detetor
O mesmo helper serve um pipeline caseiro construído sobre o RenderLoadedPageToBitmap, que renderiza a caixa visível e aplica o /Rotate tal como as funcionalidades de reconhecimento. Leia a caixa com o GetLoadedPageVisibleBox, normalize a rotação da mesma maneira que o HotPDF, e mapeie dois cantos opostos de cada caixa em pixels. O eixo Y vira e, a 90 e 270 graus, os eixos trocam, por isso os cantos mapeados saem sem ordem fixa; tome o mínimo e o máximo dos pontos mapeados, que é também como o HotPDF constrói os limites de códigos de barras
const
DPI = 200;
var
Pdf: THotPDF;
Bmp: TBitmap;
VL, VB, VR, VT: Single;
Rotation: Integer;
PxL, PxT, PxR, PxB, X1, Y1, X2, Y2: Double;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('scanned-ids.pdf');
if not Pdf.GetLoadedPageVisibleBox(0, VL, VB, VR, VT) then Exit;
Rotation := Pdf.GetLoadedPageRotation(0) mod 360;
if Rotation < 0 then Inc(Rotation, 360);
if (Rotation <> 90) and (Rotation <> 180) and (Rotation <> 270) then
Rotation := 0;
Bmp := Pdf.RenderLoadedPageToBitmap(0, DPI);
if Bmp = nil then Exit;
try
MyDetector(Bmp, PxL, PxT, PxR, PxB); // o seu código, caixa em pixels
HotPixelToPage(Rotation, DPI, Bmp.Height, VL, VB, VR, VT,
PxL, PxT, X1, Y1);
HotPixelToPage(Rotation, DPI, Bmp.Height, VL, VB, VR, VT,
PxR, PxB, X2, Y2);
Writeln(Format('User-space box [%.2f %.2f %.2f %.2f]',
[Min(X1, X2), Min(Y1, Y2), Max(X1, X2), Max(Y1, Y2)]));
finally
Bmp.Free;
end;
finally
Pdf.Free;
end;
end;
O comportamento de rotação está coberto com mais profundidade em achatar a rotação de página sem partir as caixas de página, e o pipeline de descodificação de códigos de barras que consome a mesma transformação em descodificar códigos QR rodados de páginas PDF. Se o seu engine embrulha um reconhecedor externo, o adaptador Tesseract OCR para PDF pesquisável mostra o lado de isolamento de processo e cancelamento da mesma interface
Referência rápida: mapeamento de coordenadas seguro quanto ao CropBox
- O renderer rasteriza a caixa visível, o CropBox recortado ao MediaBox (ISO 32000-1 §14.11.2); todo o mapeamento de pixel para página tem de usar essa caixa, lida com o
GetLoadedPageVisibleBox - O HotPDF v2.770.153 corrigiu o
ApplyLoadedOCRTextLayer; a v2.770.154 corrigiu oDecodeLoadedPageBarcodesde página inteira e as descobertas de rostos doDetectLoadedRedactionFindings; builds da v2.766.64 até essas versões são afetadas - As caixas
THPDFOCRWordsão pixels de bitmap com origem no topo esquerdo; oGetLoadedPageBoxe oGetLoadedPageVisibleBoxdevolvem user space PDF com origem no fundo esquerdo eBottom < Top - A escala é
DPI / 72; derive-a do DPI, nunca de uma caixa de página dividida pela largura do bitmap - O /Rotate decide que margens interessam: Left e Bottom a 0 e 90, Right e Bottom a 180, Right e Top a 270
- Devolva palavras OCR em pixels e deixe o HotPDF mapeá-las; converta só para a sua própria lógica de filtragem
- Corra de novo o OCR em páginas recortadas processadas por uma build afetada, e lembre-se de que o
SkipPagesWithTextsalta páginas que já trazem a camada antiga
As funcionalidades de reconhecimento, as consultas de caixas de página e a renderização de documentos carregados usadas aqui vêm todas no componente HotPDF para Delphi e C++Builder; licenciamento, downloads de trial e a lista completa de funcionalidades estão na página do componente PDF Delphi HotPDF