Artikel Teknis

Pemetaan Koordinat OCR dan Barcode Aman CropBox di HotPDF

Text layer OCR, bounds barcode, dan box redaksi wajah bergeser di halaman PDF hasil crop ketika piksel bitmap dipetakan kembali lewat MediaBox alih-alih box yang benar-benar dirasterisasi renderer: CropBox yang di-clip ke MediaBox (ISO 32000-1 §14.11.2). HotPDF memperbaikinya untuk ApplyLoadedOCRTextLayer di v2.770.153, dan untuk DecodeLoadedPageBarcodes serta DetectLoadedRedactionFindings di v2.770.154

Laporan bug yang biasanya datang tampak seperti ini. Arsip kontrak hasil scan melewati OCR, hasilnya searchable, dan hit pencarian untuk nomor klausul ter-highlight setengah inci di bawah dan di kiri nomor cetaknya. Kebanyakan file di batch itu baik-baik saja. Yang rusak semuanya datang dari satu stasiun scan yang menulis /CropBox untuk memangkas margin platen. Satu detail itulah yang memisahkan gambar yang dilihat engine OCR dari frame tempat text layer diletakkan, dan ketidakcocokan yang sama menggeser bounds barcode dan, lebih serius lagi, box redaksi wajah

Kenapa text layer OCR bergeser menjauh dari kata hasil scan?

Text layer bergeser karena dua paruh pipeline berbeda pendapat soal rectangle mana yang dicover bitmap. Di v2.766.64, HotPDF mengubah rendering, export SVG, viewer, dan printing untuk menghormati CropBox: halaman ditampilkan lewat CropBox-nya yang di-clip ke MediaBox-nya, persis yang diresepkan ISO 32000-1 §14.11.2, dan GetLoadedPageVisibleBox ditambahkan untuk mengembalikan visible box itu. Fitur-fitur recognition tetap membangun transform device-ke-halaman mereka dari GetLoadedPageBox(PageIndex, pbMediaBox, ...). Raster kini mencover visible box, transform masih mengasumsikan MediaBox, dan setiap posisi yang dikenali kembali tergeser sejauh celah antara keduanya

Jendela yang terdampak karenanya presisi. ApplyLoadedOCRTextLayer salah menempatkan teks dari v2.766.64 sampai v2.770.152. DecodeLoadedPageBarcodes seluruh halaman dan face detection di dalam DetectLoadedRedactionFindings tetap salah satu build lebih lama, sampai v2.770.153. Sebelum v2.766.64 renderer menggambar seluruh MediaBox, jadi pemetaan dan raster sepakat, dengan biaya mengenali konten yang tak pernah ditampilkan viewer. Perbaikannya mengubah tiga hal sekaligus untuk tiap fitur: transformnya, estimasi anggaran pikselnya, dan page box yang diteruskan ke engine custom dalam record permintaan

Beberapa kasus tak pernah terdampak:

  • Halaman tanpa /CropBox, atau yang CropBox-nya sama dengan MediaBox, terpetakan identik sebelum dan sesudah perbaikan
  • DecodeLoadedPageBarcodes dengan HasRegion diset merender persis region yang Anda teruskan dan memetakan lewat region yang sama, jadi decode region-eksplisit sepanjang waktu benar; pengecekan bahwa region berada di dalam halaman tetap memakai MediaBox
  • Redaction finding berbasis pattern (email, nomor kartu, dan sebagainya) datang dari ekstraksi teks di user space, bukan dari raster, jadi hanya finding face detection yang bergeser

Tiga frame koordinat, dan API HotPDF mana yang memakai masing-masing

Code HotPDF yang menyentuh recognition berurusan dengan tiga frame, dan kebanyakan bug pemetaan datang dari mencampur dua di antaranya

  • Piksel bitmap: origin kiri-atas, Y tumbuh ke bawah, satuannya piksel pada DPI permintaan. THPDFOCRWord.Left, Top, Right, dan Bottom ada di frame ini, demikian pula titik baseline opsional, hasil yang dikembalikan IHPDFBarcodeDecoder custom, dan box dari IHPDFFaceDetector custom
  • PDF user space dari halaman loaded: origin kiri-bawah, Y tumbuh ke atas, satuan points, dengan Bottom < Top. GetLoadedPageBox dan GetLoadedPageVisibleBox mengembalikan Left, Bottom, Right, Top di frame ini, demikian pula field PageLeft, PageBottom, PageRight, dan PageTop milik THPDFOCRRequest, bounds di THPDFDecodedBarcode, dan rectangle di THPDFRedactionFinding
  • Koordinat menggambar halaman HotPDF: API yang Anda pakai untuk membangun halaman baru (output teks, shape, barcode, link, form field) bekerja dengan origin kiri-atas dan Y tumbuh ke bawah. Frame itu milik pembuatan dokumen dan tak ada hubungannya dengan API loaded document di atas, jadi jangan pernah menyuntikkan rectangle user space halaman loaded ke sana tanpa diubah

Record kata OCR sengaja berbasis piksel: sebuah engine melaporkan apa yang dilihatnya di image, dan ApplyLoadedOCRTextLayer yang memegang konversinya. Pembagian kerja itu hanya bekerja ketika konversinya memakai box yang benar, dan itulah yang dikembalikan v2.770.153

Frame koordinat recognition HotPDF: piksel bitmap berorigin kiri-atas yang dipakai box THPDFOCRWord dan decoder custom, PDF user space berorigin kiri-bawah yang dikembalikan GetLoadedPageBox dan GetLoadedPageVisibleBox, dan API menggambar halaman kiri-atas yang tak boleh menerima rectangle halaman loaded tanpa diubah
Engine melaporkan piksel karena itulah yang dilihatnya, HotPDF memetakannya, dan mencampur kedua frame itulah cara layer dan box redaksi bergeser

Transform device-ke-halaman di balik OCR, barcode, dan wajah

HotPDF memetakan piksel bitmap ke halaman dengan satu matriks affine yang dibangun dari lima input: rotasinya, skala DPI / 72, tinggi bitmap, serta Left, Bottom, Right, dan Top dari box yang dirender. OCR, decode barcode, dan face detection berbagi satu rutin untuk itu, dan itulah kenapa satu input box yang salah merusak ketiganya dengan cara yang sama. Untuk halaman tak berotasi, matriks page-ke-device [A B C D E F]-nya:

  • A = Scale dan D = -Scale, di mana Scale = DPI / 72; D yang negatif membalik user space (Y ke atas) menjadi bitmap space (Y ke bawah)
  • B = C = 0, karena halaman tak berotasi tak punya shear atau penukaran antar sumbu
  • E = -Left * Scale, yang memindahkan tepi kiri box ke kolom piksel 0
  • F = BitmapHeight + Bottom * Scale, yang memetakan tepi bawah box ke y = BitmapHeight, tepi bawah bitmap, sehingga tepi atasnya mendarat di baris 0

Piksel kembali ke halaman lewat invers matriks itu. Request.PageRotation membawa /Rotate milik halaman yang ternormalisasi ke 0, 90, 180, atau 270 (nilai apa pun yang bukan kelipatan 90 diperlakukan 0), dan renderer memutar halaman searah jarum jam sebagaimana diwajibkan ISO 32000-1 §7.7.3.3. Di bawah rotasi, sumbu bertukar dan pasangan tepi box yang berbeda dipatok ke origin bitmap. Ditulis sebagai formula invers, dengan S = DPI / 72, x dan y dalam piksel, dan H tinggi bitmap:

/RotatePage XPage YTepi box yang diandalkan pemetaan
0Left + x / SBottom + (H - y) / SLeft, Bottom
90Left + y / SBottom + x / SLeft, Bottom
180Right - x / SBottom + y / SRight, Bottom
270Right - y / STop - x / SRight, Top

Kolom terakhir menjelaskan kenapa bug itu tampak acak di produksi. CropBox yang hanya memangkas bagian atas halaman membiarkan Left dan Bottom tak tersentuh, sehingga halaman tegak keluar sempurna dan hanya halaman yang membawa /Rotate 270 yang bergeser. Rotasi juga menukar dimensi bitmap: pada 90 dan 270, bitmapnya selebar (Top - Bottom) * S piksel dan setinggi (Right - Left) * S piksel

Tabel pemetaan invers HotPDF untuk rotasi halaman: pada /Rotate 0 dan 90 transform mematok tepi Left dan Bottom dari box yang dirender, pada 180 mematok Right dan Bottom, pada 270 Right dan Top, itulah kenapa halaman ter-crop bergeser ke arah berbeda untuk tiap orientasi di dokumen campuran
Crop setengah inci yang sama terlihat seperti tiga bug berbeda begitu halaman membawa nilai /Rotate berbeda, karena tiap orientasi mematok pasangan tepi box yang berbeda

Apa yang salah dengan MediaBox [0 0 612 792] dan CropBox [36 36 576 756]?

Dengan crop setengah inci di setiap sisi, text layer halaman tak berotasi mendarat persis 36 point di kiri dan 36 point di bawah kata hasil scan ketika MediaBox yang dipakai. Ambil halaman US Letter yang CropBox-nya memangkas 36 point (0,5 inci) dari tiap tepi. Visible box-nya 540 kali 720 point, jadi pada resolusi OCR default 300 DPI skalanya 300 / 72 ≈ 4,1667 dan bitmapnya 2250 kali 3000 piksel

Andaikan engine melaporkan satu kata dengan box piksel Left 450, Top 600, Right 900, Bottom 660 dan tanpa baseline. HotPDF lalu menempatkan baseline 20 persen tinggi kata di atas tepi bawahnya, di baris piksel 648, dan memetakan titik awal (450, 648):

  • Lewat visible box: x = 36 + 450 / 4,1667 = 144,0 dan y = 36 + (3000 - 648) / 4,1667 = 600,48, itulah tempat kata itu dicetak
  • Lewat MediaBox: x = 0 + 108,0 = 108,0 dan y = 0 + 564,48 = 564,48, satu pergeseran seragam sejauh (-36, -36) point
Anatomi drift CropBox HotPDF di halaman US Letter dengan MediaBox 0 0 612 792 dan CropBox 36 36 576 756: renderer merasterisasi visible box pada 300 DPI, sehingga memetakan piksel kata 450 lewat GetLoadedPageVisibleBox memberi 144,0 dan 600,48 sementara transform MediaBox mendarat di 108,0 dan 564,48
Raster mencover CropBox, jadi transform apa pun yang dibangun dari MediaBox menggeser setiap kata yang dikenali persis sejauh margin crop-nya

Putar halaman yang sama dan arah errornya berganti, karena tepi yang terlibat berbeda. Pada /Rotate 180 suku X memakai Right, dan 612 alih-alih 576 mendorong layer 36 point ke kanan sementara Bottom masih menariknya 36 point ke bawah. Pada /Rotate 270 Right dan Top sama-sama terlalu besar, jadi layer berpindah 36 point ke kanan dan 36 point ke atas. Dokumen dengan orientasi campuran bisa menampilkan drift di tiga arah, fingerprint yang andal untuk bug ini. Code tulisan tangan yang menurunkan skala dari box, seperti Bitmap.Width / (Right - Left), juga meregangkan setiap koordinat sebesar 612 / 540, sekitar 13 persen, di atas offsetnya

Dokumen PDF Anda mana yang terdampak?

Dokumen PDF terpapar ketika setidaknya satu halamannya punya visible box yang berbeda dari MediaBox-nya, dan HotPDF bisa memberi tahu Anda itu dalam beberapa baris. Bandingkan GetLoadedPageBox dengan pbMediaBox terhadap GetLoadedPageVisibleBox untuk setiap halaman, dan cetak GetLoadedPageRotation di sampingnya agar Anda bisa memprediksi arah drift dari tabel di atas. THPDFPageBoundary juga menyediakan pbCropBox, pbBleedBox, pbTrimBox, dan pbArtBox, tapi GetLoadedPageBox(pbCropBox) jatuh kembali ke MediaBox ketika crop box tak ada dan tidak meng-clip, jadi visible box adalah pembanding yang tepat

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;
      // array tersimpan bisa saja mendaftar sudutnya dalam urutan apa pun
      if MR < ML then begin Tmp := ML; ML := MR; MR := Tmp; end;
      if MT < MB then begin Tmp := MB; MB := MT; MT := Tmp; end;
      // sudah ternormalisasi dan di-clip ke 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;

Dua detail GetLoadedPageVisibleBox penting untuk script seperti ini. Fungsi itu membiarkan parameter out-nya tak tersentuh ketika gagal, jadi preset ukuran halaman default sebelum panggilan adalah pola yang aman. Dan ketika CropBox yang malformed sama sekali tak beririsan dengan MediaBox, fungsi mengembalikan MediaBox alih-alih rectangle kosong. Kalau laporannya mencantumkan halaman dan build yang Anda deploy lebih tua dari v2.770.153 untuk OCR, atau v2.770.154 untuk barcode dan wajah, jalankan ulang recognition di halaman-halaman itu setelah upgrade. Layer OCR yang di-commit oleh build terdampak tetap tinggal di file tersimpan, dan opsi default SkipPagesWithText akan melewati halaman-halaman itu di pass kedua kecuali Anda mematikannya atau menghapus layer lama lebih dulu

Bagaimana seharusnya IHPDFOCREngine custom memetakan piksel kembali ke PDF space?

IHPDFOCREngine custom sebaiknya mengembalikan box kata dalam piksel bitmap dan membiarkan HotPDF yang memetakan; konversi ke user space hanya untuk keputusan Anda sendiri, dan pakailah box dari request, bukan MediaBox. Sejak v2.770.153, PageLeft, PageBottom, PageRight, dan PageTop milik request mendeskripsikan visible box yang dirender, jadi keduanya cocok dengan Request.Bitmap secara persis. Helper di bawah adalah invers transform milik library, termasuk pemakaiannya atas tinggi bitmap aktual untuk halaman tegak, jadi ia sepakat dengan HotPDF sampai ke piksel

uses
  System.SysUtils, System.Math, Vcl.Graphics, HPDFDoc;

// Piksel bitmap (origin kiri-atas, Y ke bawah) ke PDF user space
// (origin kiri-bawah, Y ke atas), lewat box tempat bitmap dirender
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;

Alasan realistis untuk butuh user space di dalam engine adalah aturan zona: invoice yang letterhead-nya tak pernah Anda mau searchable, atau area stempel yang membingungkan recognizer. Engine di bawah, ditulis dengan TInterfacedObject agar reference counting menangani umurnya, memfilter kata berdasarkan di mana pusat mereka jatuh di halaman, lalu mengembalikan yang selamat apa adanya dalam koordinat piksel. RunRecognizer berdiri untuk panggilan recognizer milik Anda sendiri

type
  TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
  private
    FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single;  // user space
    function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
      out Words: THPDFOCRWords): boolean;  // recognizer Anda, box piksel
  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];  // tetap piksel: HotPDF memetakannya sendiri
    Inc(Count);
  end;
  SetLength(Words, Count);
  Result := True;
end;

Serahkan engine itu ke ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) seperti engine lainnya. Library memvalidasi apa yang kembali sebelum memercayainya: sebuah kata dibuang dan dihitung di Info.DroppedWordCount ketika box-nya keluar dari bitmap, ketika Right <= Left atau Bottom <= Top, atau ketika Confidence di luar 0..1 atau di bawah MinimumConfidence. Mengembalikan lebih banyak kata dari MaxWordsPerPage, atau mendorong total berjalan melampaui MaxTotalWords, menggagalkan seluruh panggilan dengan budget error, jadi hormati Request.MaxWords di engine Anda. Jangan konversikan box kata ke user space sebelum mengembalikannya; HotPDF akan memperlakukan nilai point itu sebagai piksel dan layer akan runtuh ke arah origin bitmap

Memetakan output detector milik Anda sendiri

Helper yang sama melayani pipeline buatan sendiri yang dibangun di atas RenderLoadedPageToBitmap, yang merender visible box dan menerapkan /Rotate persis seperti fitur recognition. Baca boxnya dengan GetLoadedPageVisibleBox, normalisasi rotasinya dengan cara yang sama seperti HotPDF, dan petakan dua sudut berlawanan dari tiap box piksel. Sumbu Y membalik dan, pada 90 dan 270 derajat, sumbu bertukar, jadi sudut hasil pemetaan keluar dalam urutan yang tak tetap; ambil minimum dan maksimum dari titik-titik terpetakan, yang juga cara HotPDF membangun bounds barcode

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);  // code Anda, box piksel
      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;

Perilaku rotasinya dibahas lebih dalam di meratakan rotasi halaman tanpa merusak page box, dan pipeline decode barcode yang mengonsumsi transform yang sama di men-decode QR code yang berotasi dari halaman PDF. Kalau engine Anda membungkus recognizer eksternal, adapter OCR Tesseract untuk searchable PDF menunjukkan sisi isolasi proses dan pembatalan dari interface yang sama

Referensi cepat: pemetaan koordinat aman CropBox

  • Renderer merasterisasi visible box, CropBox yang di-clip ke MediaBox (ISO 32000-1 §14.11.2); setiap pemetaan piksel-ke-halaman harus memakai box itu, dibaca dengan GetLoadedPageVisibleBox
  • HotPDF v2.770.153 memperbaiki ApplyLoadedOCRTextLayer; v2.770.154 memperbaiki DecodeLoadedPageBarcodes seluruh halaman dan face finding dari DetectLoadedRedactionFindings; build dari v2.766.64 sampai versi-versi itu terdampak
  • Box THPDFOCRWord adalah piksel bitmap berorigin kiri-atas; GetLoadedPageBox dan GetLoadedPageVisibleBox mengembalikan PDF user space berorigin kiri-bawah dengan Bottom < Top
  • Skala adalah DPI / 72; turunkan dari DPI, jangan pernah dari page box yang dibagi ke lebar bitmap
  • /Rotate menentukan tepi mana yang penting: Left dan Bottom pada 0 dan 90, Right dan Bottom pada 180, Right dan Top pada 270
  • Kembalikan kata OCR dalam piksel dan biarkan HotPDF memetakannya; konversi hanya untuk logika filter Anda sendiri
  • Jalankan ulang OCR di halaman ter-crop yang diproses build terdampak, dan ingat bahwa SkipPagesWithText melewati halaman yang sudah membawa layer lama

Fitur recognition, query page box, dan rendering loaded document yang dipakai di sini semuanya dikirim di komponen HotPDF untuk Delphi dan C++Builder; lisensi, unduhan trial, dan daftar fitur lengkapnya ada di halaman komponen PDF Delphi HotPDF