บทความเทคนิค

OCR, barcode และ redaction บนหน้า crop ด้วย HotPDF

text layer ของ OCR, ขอบเขตของ barcode และกล่อง redaction ของใบหน้าเหลื่อมตำแหน่งบนหน้า PDF ที่ถูก crop เมื่อ pixel ของ bitmap ถูกแมปกลับผ่าน MediaBox แทนกล่องที่ renderer rasterize จริง ๆ ซึ่งก็คือ CropBox ที่ถูก clip ด้วย MediaBox (ISO 32000-1 §14.11.2) HotPDF แก้เรื่องนี้ให้ ApplyLoadedOCRTextLayer ใน v2.770.153 และให้ DecodeLoadedPageBarcodes กับ DetectLoadedRedactionFindings ใน v2.770.154

รายงานบั๊กที่มักไหลเข้ามาหน้าตาแบบนี้ คลังสัญญาที่สแกนถูกส่งเข้า OCR ไฟล์ output ค้นหาได้ และ search hit ของเลขมาตราถูก highlight หลุดไปครึ่งนิ้วด้านล่างกับด้านซ้ายของเลขที่พิมพ์ไว้ ไฟล์ส่วนใหญ่ในล็อตไม่มีปัญหา ตัวที่พังทั้งหมดมาจากสถานีสแกนเครื่องเดียวที่เขียน /CropBox ตัดขอบของแท่นแก้ว รายละเอียดจุดเดียวนี้แหละที่ทำให้ภาพที่ OCR engine เห็นกับกรอบที่ text layer ถูกวางแยกจากกัน และความไม่ตรงกันแบบเดียวกันนี้ยังขยับขอบเขตของ barcode และที่จริงจังกว่านั้นคือกล่อง redaction ของใบหน้า

ทำไม text layer ของ OCR ถึงเหลื่อมหลุดจากคำที่สแกนมา

text layer เหลื่อมเพราะครึ่งซ้ายกับครึ่งขวาของ pipeline เห็นต่างกันว่า bitmap ครอบสี่เหลี่ยมไหน ใน v2.766.64 HotPDF เปลี่ยนการ render, การ export SVG, viewer และการพิมพ์ให้ยึด CropBox: หน้าถูกแสดงผ่าน CropBox ที่ถูก clip ด้วย MediaBox ตามที่ ISO 32000-1 §14.11.2 กำหนด และ GetLoadedPageVisibleBox ถูกเพิ่มเข้ามาคืนกล่องที่มองเห็นนั้น ส่วนฟีเจอร์ฝั่ง recognition ยังสร้าง device-to-page transform จาก GetLoadedPageBox(PageIndex, pbMediaBox, ...) อยู่ raster ตอนนี้ครอบ visible box transform ยังสมมติ MediaBox ตำแหน่งที่จดจำได้ทุกตำแหน่งจึงกลับมาเหลื่อมเท่าช่องว่างระหว่างสองกล่อง

ช่วงที่ได้รับผลกระทบจึงเป๊ะมาก ApplyLoadedOCRTextLayer วาง text ผิดที่ตั้งแต่ v2.766.64 ถึง v2.770.152 DecodeLoadedPageBarcodes แบบทั้งหน้ากับ face detection ข้างใน DetectLoadedRedactionFindings ผิดต่ออีกหนึ่ง build ถึง v2.770.153 ก่อน v2.766.64 renderer วาด MediaBox ทั้งกล่อง การแมปกับ raster จึงตรงกัน แลกกับการจดจำเนื้อหาที่ viewer ไม่เคยโชว์ การแก้เปลี่ยนสามอย่างพร้อมกันให้แต่ละฟีเจอร์: transform, การประมาณงบ pixel และ page box ที่ส่งให้ engine แบบ custom ใน request record

บางเคสไม่เคยได้รับผลเลย:

  • หน้าที่ไม่มี /CropBox หรือหน้าที่ CropBox เท่ากับ MediaBox แมปเหมือนเดิมก่อนและหลังการแก้
  • DecodeLoadedPageBarcodes ที่เซ็ต HasRegion จะ render เฉพาะ region ที่ส่งให้พอดี แล้วแมปผ่าน region เดียวกัน การถอดแบบระบุ region จึงถูกต้องมาตลอด ส่วนเช็คว่า region อยู่ข้างในหน้ายังใช้ MediaBox อยู่
  • redaction finding แบบอิงรูปแบบ (อีเมล เลขบัตร ฯลฯ) มาจากการ extract text ใน user space ไม่ใช่จาก raster มีแต่ finding ของ face detection เท่านั้นที่ขยับ

สามกรอบพิกัด กับ API ของ HotPDF ที่ใช้กรอบไหน

โค้ด HotPDF ที่แตะฝั่ง recognition ต้องจัดการกับสามกรอบ และบั๊กการแมปส่วนใหญ่มาจากการเอาสองกรอบมาคละกัน

  • pixel ของ bitmap: origin บนซ้าย Y โตลงล่าง หน่วยเป็น pixel ที่ DPI ของคำขอ THPDFOCRWord.Left, Top, Right กับ Bottom อยู่ในกรอบนี้ จุด baseline แบบเลือกได้ก็เช่นกัน รวมถึงผลลัพธ์ที่ IHPDFBarcodeDecoder แบบ custom คืนมา และกล่องจาก IHPDFFaceDetector แบบ custom
  • PDF user space ของหน้าที่โหลด: origin ล่างซ้าย Y โตขึ้นบน หน่วยเป็นพอยต์ โดย Bottom < Top GetLoadedPageBox กับ GetLoadedPageVisibleBox คืน Left, Bottom, Right, Top ในกรอบนี้ field PageLeft, PageBottom, PageRight กับ PageTop ของ THPDFOCRRequest ก็เช่นกัน รวมถึงขอบเขตใน THPDFDecodedBarcode และสี่เหลี่ยมใน THPDFRedactionFinding
  • พิกัดวาดหน้าของ HotPDF: API ที่ใช้สร้างหน้าใหม่ (ข้อความ รูปทรง barcode ลิงก์ form field) ทำงานด้วย origin บนซ้ายและ Y โตลงล่าง กรอบนี้เป็นของการสร้างเอกสาร ไม่เกี่ยวกับ API ของเอกสารที่โหลดด้านบนเลย ห้ามยัดสี่เหลี่ยม user space ของหน้าที่โหลดเข้ามันโดยไม่แปลงเด็ดขาด

record ของคำ OCR ถูกออกแบบให้อิง pixel ตั้งใจ engine รายงานสิ่งที่มันเห็นในภาพ และ ApplyLoadedOCRTextLayer เป็นเจ้าของการแปลงเอง การแบ่งงานแบบนี้ทำงานได้ก็ต่อเมื่อการแปลงใช้กล่องที่ถูกตัว ซึ่งคือสิ่งที่ v2.770.153 กู้คืนมา

กรอบพิกัดของ recognition ใน HotPDF: pixel ของ bitmap ที่มี origin บนซ้ายซึ่งกล่อง THPDFOCRWord กับ decoder แบบ custom ใช้, PDF user space ที่มี origin ล่างซ้ายซึ่ง GetLoadedPageBox กับ GetLoadedPageVisibleBox คืนค่า และ API วาดหน้าแบบ origin บนซ้ายที่ห้ามรับสี่เหลี่ยมของหน้าที่โหลดโดยไม่แปลงเด็ดขาด
engine รายงาน pixel เพราะนั่นคือสิ่งที่มันเห็น HotPDF เป็นคนแมปต่อ และการเอาสองกรอบมาคละกันแหละที่ทำให้ layer กับกล่อง redaction เหลื่อมไป

device-to-page transform ที่อยู่เบื้องหลัง OCR, barcode และใบหน้า

HotPDF แมป pixel ของ bitmap กลับสู่หน้าด้วย affine matrix ตัวเดียวที่ประกอบจาก input ห้าตัว: การหมุน, อัตราส่วน DPI / 72, ความสูงของ bitmap และ Left, Bottom, Right กับ Top ของกล่องที่ render OCR, การถอด barcode และ face detection แชร์ routine ตัวเดียวกันร่วมกัน กล่อง input ผิดหนึ่งตัวจึงทำให้ทั้งสามพังแบบเดียวกัน สำหรับหน้าที่ไม่หมุน matrix แบบ page-to-device [A B C D E F] เป็น:

  • A = Scale กับ D = -Scale โดย Scale = DPI / 72 D ที่ติดลบพลิก user space (Y ขึ้น) ไปเป็นพื้นที่ bitmap (Y ลง)
  • B = C = 0 เพราะหน้าที่ไม่หมุนไม่มีการเฉือนหรือสลับแกน
  • E = -Left * Scale ซึ่งเลื่อนขอบซ้ายของกล่องไปที่ column 0 ของ pixel
  • F = BitmapHeight + Bottom * Scale ซึ่งแมปขอบล่างของกล่องไปที่ y = BitmapHeight ขอบล่างของ bitmap ขอบบนจึงตกลงที่แถว 0

pixel กลับสู่หน้าผ่าน inverse ของ matrix นั้น Request.PageRotation หอบ /Rotate ของหน้ามาแบบ normalize เป็น 0, 90, 180 หรือ 270 (ค่าใดที่ไม่ใช่ผลคูณของ 90 ถือเป็น 0) และ renderer หมุนหน้าตามเข็มนาฬิกาตามที่ ISO 32000-1 §7.7.3.3 กำหนด ภายใต้การหมุนแกนจะสลับกันและคู่ขอบกล่องคนละคู่ถูกตรึงกับ origin ของ bitmap เขียนเป็นสูตร inverse โดย S = DPI / 72, x กับ y หน่วย pixel และ H คือความสูงของ bitmap:

/RotatePage XPage Yขอบกล่องที่การแมปพึ่งพา
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

column สุดท้ายอธิบายว่าทำไมบั๊กถึงดูเหมือนสุ่มใน production CropBox ที่ตัดแค่ส่วนบนของหน้าปล่อย Left กับ Bottom ไว้เหมือนเดิม หน้าตั้งตรงจึงออกมาสมบูรณ์ และมีแต่หน้าที่แปะ /Rotate 270 เท่านั้นที่เหลื่อม การหมุนยังสลับขนาดของ bitmap ด้วย ที่ 90 กับ 270 bitmap กว้าง (Top - Bottom) * S pixel และสูง (Right - Left) * S pixel

ตารางการแมป inverse ของ HotPDF ตามการหมุนหน้า: ที่ /Rotate 0 กับ 90 transform ตรึงขอบ Left กับ Bottom ของกล่องที่ render ที่ 180 ตรึง Right กับ Bottom ที่ 270 ตรึง Right กับ Top เอกสารที่ถูก crop จึงเหลื่อมไปคนละทิศตามแต่ละ orientation ในเอกสารที่ผสมกัน
การ crop ครึ่งนิ้วชุดเดียวกันดูเหมือนบั๊กสามแบบเมื่อหน้าพา /Rotate ต่างกัน เพราะแต่ละ orientation ตรึงคู่ขอบกล่องคนละคู่

MediaBox [0 0 612 792] กับ CropBox [36 36 576 756] พังตรงไหน

ด้วยการ crop ครึ่งนิ้วทุกด้าน text layer ของหน้าที่ไม่หมุนจะตกเหลื่อมพอดี 36 พอยต์ไปทางซ้ายและ 36 พอยต์ลงล่างจากคำที่สแกนเมื่อใช้ MediaBox ยกหน้า US Letter ที่ CropBox ตัด 36 พอยต์ (0.5 นิ้ว) ออกจากทุกขอบ visible box คือ 540 คูณ 720 พอยต์ ที่ความละเอียด OCR เริ่มต้น 300 DPI อัตราส่วนคือ 300 / 72 ≈ 4.1667 และ bitmap คือ 2250 คูณ 3000 pixel

สมมติ engine รายงานคำหนึ่งพร้อม box เป็น pixel ที่ Left 450, Top 600, Right 900, Bottom 660 และไม่มี baseline HotPDF จะวาง baseline ไว้สูงจากขอบล่าง 20 เปอร์เซ็นต์ของความสูงคำ ที่แถว pixel 648 แล้วแมปจุดเริ่ม (450, 648):

  • ผ่าน visible box: x = 36 + 450 / 4.1667 = 144.0 และ y = 36 + (3000 - 648) / 4.1667 = 600.48 ซึ่งคือที่ที่คำถูกพิมพ์จริง
  • ผ่าน MediaBox: x = 0 + 108.0 = 108.0 และ y = 0 + 564.48 = 564.48 เหลื่อมเป็นชุดเดียว (-36, -36) พอยต์
กายวิภาคการเหลื่อมของ CropBox ใน HotPDF บนหน้า US Letter ที่ MediaBox 0 0 612 792 และ CropBox 36 36 576 756: renderer rasterize visible box ที่ 300 DPI การแมป pixel 450 ของคำผ่าน GetLoadedPageVisibleBox จึงได้ 144.0 กับ 600.48 ขณะที่ transform ที่ใช้ MediaBox ตกที่ 108.0 กับ 564.48
raster ครอบ CropBox transform ใด ๆ ที่สร้างจาก MediaBox จึงขยับทุกคำที่จดจำได้เท่าขอบที่ถูก crop พอดี

หมุนหน้าเดิมแล้วทิศของความผิดพลาดเปลี่ยน เพราะขอบที่เกี่ยวข้องไม่เหมือนเดิม ที่ /Rotate 180 พจน์ X ใช้ Right และ 612 แทน 576 ดัน layer ไปทางขวา 36 พอยต์ ขณะที่ Bottom ยังลากมันลง 36 พอยต์ ที่ /Rotate 270 ทั้ง Right กับ Top ใหญ่เกิน layer จึงไปขวา 36 พอยต์และขึ้นบน 36 พอยต์ เอกสารที่ orientation ปนกันสามารถโชว์การเหลื่อมสามทิศ ซึ่งเป็นลายนิ้วมือที่เชื่อถือได้ของบั๊กตัวนี้ โค้ดที่เขียนมือซึ่ง derive อัตราส่วนจากกล่อง อย่าง Bitmap.Width / (Right - Left) ยังดึงพิกัดทุกตัวยืดเพิ่ม 612 / 540 หรือราว 13 เปอร์เซ็นต์ ทับไปบนการเหลื่อมอีกชั้น

เอกสาร PDF ของคุณตัวไหนได้รับผลกระทบ

เอกสาร PDF เสี่ยงเมื่อมีหน้าอย่างน้อยหนึ่งหน้าที่ visible box ต่างจาก MediaBox ของมัน และ HotPDF บอกได้ในไม่กี่บรรทัด เทียบ GetLoadedPageBox ด้วย pbMediaBox กับ GetLoadedPageVisibleBox ทีละหน้า พร้อมพิมพ์ GetLoadedPageRotation คู่ไว้ด้วยเพื่อทำนายทิศการเหลื่อมจากตารางข้างบน THPDFPageBoundary ยังมี pbCropBox, pbBleedBox, pbTrimBox กับ pbArtBox ให้ด้วย แต่ GetLoadedPageBox(pbCropBox) จะย้อนไปใช้ MediaBox เมื่อไม่มี crop box และไม่ clip ให้ visible box จึงเป็นสิ่งที่ถูกต้องที่สุดที่จะเอามาเทียบ

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 ที่เก็บไว้อาจลิสต์มุมสลับลำดับกันได้
      if MR < ML then begin Tmp := ML; ML := MR; MR := Tmp; end;
      if MT < MB then begin Tmp := MB; MB := MT; MT := Tmp; end;
      // normalize และ clip ด้วย 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;

รายละเอียดสองจุดของ GetLoadedPageVisibleBox สำคัญกับ script แบบนี้ ฟังก์ชันปล่อย out parameters ไว้เหมือนเดิมเมื่อล้ม การตั้งค่าขนาดหน้าค่าเริ่มต้นไว้ก่อนเรียกจึงเป็น pattern ที่ปลอดภัย และเมื่อ CropBox ที่เสียรูปไม่ตัดกับ MediaBox เลย ฟังก์ชันจะคืน MediaBox แทนสี่เหลี่ยมว่าง ถ้ารายงานลิสต์หน้าและ build ที่ deploy อยู่เก่ากว่า v2.770.153 สำหรับ OCR หรือ v2.770.154 สำหรับ barcode กับใบหน้า ให้รัน recognition ซ้ำกับหน้าพวกนั้นหลังอัปเกรด layer ของ OCR ที่ build ที่ได้รับผลกระทบ commit ไว้จะค้างอยู่ในไฟล์ที่ save และตัวเลือก SkipPagesWithText ค่าเริ่มต้นจะข้ามหน้าพวกนั้นในรอบที่สอง เว้นแต่คุณจะปิดมันหรือถอด layer เก่าออกก่อน

IHPDFOCREngine แบบ custom ควรแมป pixel กลับสู่พื้นที่ PDF อย่างไร

IHPDFOCREngine แบบ custom ควรคืน box ของคำเป็น pixel ของ bitmap แล้วให้ HotPDF ทำการแมป แปลงเป็น user space เฉพาะเพื่อการตัดสินใจของคุณเอง และตอนนั้นให้ใช้กล่องจาก request ห้ามใช้ MediaBox ตั้งแต่ v2.770.153 field PageLeft, PageBottom, PageRight กับ PageTop ของ request บรรยาย visible box ที่ render แล้ว จึงตรงกับ Request.Bitmap เป๊ะ ๆ helper ด้านล่างเป็น inverse ของ transform ของ library รวมถึงการใช้ความสูง bitmap จริงกับหน้าที่ตั้งตรง จึงเห็นตรงกับ HotPDF ระดับ pixel

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

// pixel ของ bitmap (origin บนซ้าย Y ลง) ไปหา PDF user space
// (origin ล่างซ้าย Y ขึ้น) ผ่านกล่องที่ bitmap ถูก render ออกมา
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;

เหตุผลที่สมจริงในการต้องการ user space ข้างใน engine คือกฎเขตงาน: ใบแจ้งหนี้ที่ไม่ต้องการให้ letterhead ค้นหาได้ หรือพื้นที่ตราประทับที่รบกวน recognizer engine ด้านล่างเขียนด้วย TInterfacedObject เพื่อให้ reference counting ดูแลอายุของมัน กรองคำตามตำแหน่งกึ่งกลางที่ตกบนหน้า แล้วคืนตัวรอดชีวิตกลับไปเฉย ๆ ในพิกัด pixel RunRecognizer เป็นตัวแทนของ call recognizer ของคุณ

type
  TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
  private
    FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single;  // user space
    function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
      out Words: THPDFOCRWords): boolean;  // recognizer ของคุณ ให้ box เป็น pixel
  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];  // ยังเป็น pixel อยู่ ให้ HotPDF แมปเอง
    Inc(Count);
  end;
  SetLength(Words, Count);
  Result := True;
end;

ส่ง engine เข้า ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) เหมือน engine อื่น ๆ library จะตรวจสิ่งที่ไหลกลับมาก่อนเชื่อมัน คำหนึ่งจะถูกทิ้งและนับเข้า Info.DroppedWordCount เมื่อ box ของมันหลุดออกนอก bitmap เมื่อ Right <= Left หรือ Bottom <= Top หรือเมื่อ Confidence อยู่นอก 0..1 หรือต่ำกว่า MinimumConfidence การคืนคำเกิน MaxWordsPerPage หรือดันยอดสะสมเกิน MaxTotalWords ทำให้ call ทั้งชุด fail ด้วย budget error จึงควรเคารพ Request.MaxWords ใน engine ห้ามแปลง box ของคำเป็น user space ก่อนคืนค่า ไม่งั้น HotPDF จะถือค่าพอยต์เป็น pixel และ layer จะหุบยุบไปกอด origin ของ bitmap

แมป output ของ detector ที่เขียนเอง

helper ตัวเดียวกันเสิร์ฟ pipeline ที่ปั้นเองบน RenderLoadedPageToBitmap ซึ่ง render กล่องที่มองเห็นและใช้ /Rotate แบบเดียวกับที่ฟีเจอร์ฝั่ง recognition ทำ อ่านกล่องด้วย GetLoadedPageVisibleBox normalize การหมุนแบบเดียวกับที่ HotPDF ทำ แล้วแมปมุมตรงข้ามกันสองจุดของ box เป็น pixel แกน Y ถูกพลิก และที่ 90 กับ 270 องศาแกนสลับกัน มุมที่แมปแล้วจึงออกมาไม่มีลำดับตายตัว ให้หยิบค่าต่ำสุดกับสูงสุดของจุดที่แมปแล้ว ซึ่งเป็นวิธีเดียวกับที่ HotPDF สร้างขอบเขตของ 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);  // โค้ดของคุณ ให้ box เป็น pixel
      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;

พฤติกรรมการหมุนเล่าลึกกว่านี้ในการ flatten page rotation โดยไม่ทำ page box พัง และ pipeline ถอด barcode ที่กิน transform ตัวเดียวกันในการถอด QR code ที่หมุนจากหน้า PDF ถ้า engine ของคุณห่อ recognizer ภายนอก adapter ของ Tesseract OCR สำหรับ searchable PDFโชว์ฝั่งการแยก process กับ cancellation ของ interface เดียวกันนี้

สรุปด่วน: การแมปพิกัดที่ปลอดภัยต่อ CropBox

  • renderer rasterize กล่องที่มองเห็น คือ CropBox ที่ถูก clip ด้วย MediaBox (ISO 32000-1 §14.11.2) การแมป pixel ไปหน้าทุกชุดต้องใช้กล่องนั้น อ่านด้วย GetLoadedPageVisibleBox
  • HotPDF v2.770.153 แก้ ApplyLoadedOCRTextLayer v2.770.154 แก้ DecodeLoadedPageBarcodes แบบทั้งหน้ากับ face finding จาก DetectLoadedRedactionFindings build ตั้งแต่ v2.766.64 ถึงเวอร์ชันก่อนทั้งคู่ได้รับผล
  • box ของ THPDFOCRWord เป็น pixel ของ bitmap ที่มี origin บนซ้าย GetLoadedPageBox กับ GetLoadedPageVisibleBox คืน PDF user space ที่มี origin ล่างซ้ายโดย Bottom < Top
  • อัตราส่วนคือ DPI / 72 derive มันจาก DPI ห้าม derive จากการเอา page box ไปหารความกว้าง bitmap เด็ดขาด
  • /Rotate ตัดสินว่าขอบไหนมีผล: Left กับ Bottom ที่ 0 กับ 90, Right กับ Bottom ที่ 180, Right กับ Top ที่ 270
  • คืนคำของ OCR เป็น pixel แล้วให้ HotPDF แมปต่อ แปลงเองเฉพาะสำหรับ logic กรองของคุณ
  • รัน OCR ซ้ำกับหน้าที่ถูก crop ซึ่งผ่าน build ที่ได้รับผล และจำไว้ว่า SkipPagesWithText จะข้ามหน้าที่พา layer เก่าไว้แล้ว

ฟีเจอร์ฝั่ง recognition, การ query page box และการ render เอกสารที่โหลดแล้วที่ใช้ในนี้ทั้งหมดมาพร้อม HotPDF component สำหรับ Delphi กับ C++Builder การ license, ดาวน์โหลดเวอร์ชันทดลองและรายการฟีเจอร์เต็มอยู่ที่หน้า product ของ HotPDF Delphi PDF component