مقاله فنی

آفست لایهٔ متنی OCR در HotPDF: نگاشت از میان CropBox

لایه‌های متنی OCR و محدوده‌های بارکد و جعبه‌های حذف چهره روی صفحات PDF بریده‌شده انحراف می‌گیرند وقتی پیکسل‌های بیت‌مپ از طریق MediaBox به عقب نگاشت می‌شوند به‌جای جعبه‌ای که renderer واقعاً rasterize کرده: یعنی CropBox بریده‌شده به MediaBox (‏ISO 32000-1 §14.11.2). ‏HotPDF این را برای ApplyLoadedOCRTextLayer در v2.770.153 و برای DecodeLoadedPageBarcodes و DetectLoadedRedactionFindings در v2.770.154 فیکس کرد

گزارش باگی که معمولاً می‌رسد چنین است. یک آرشیو قرارداد اسکن‌شده از OCR می‌گذرد، خروجی قابل‌جست‌وجو است، و hit جست‌وجو برای شمارهٔ یک بند نیم اینچ پایین‌تر و سمت چپِ شمارهٔ چاپ‌شده هایلایت می‌شود. بیشتر فایل‌های دسته سالم‌اند. خرابها همه از یک ایستگاه اسکن آمده‌اند که یک /CropBox برای بریدن حاشیهٔ پلاتن می‌نویسد. همان یک جزئیات تصویری را که موتور OCR دیده از قابی که لایهٔ متن در آن گذاشته شده جدا می‌کند، و همان ناهمخوانی محدوده‌های بارکد و جدی‌تر از آن جعبه‌های حذف چهره را جابه‌جا می‌کند

چرا لایهٔ متنی OCR از کلمات اسکن‌شده فاصله می‌گیرد؟

لایهٔ متن فاصله می‌گیرد چون دو نیمهٔ خط لوله دربارهٔ اینکه بیت‌مپ چه مستطیلی را می‌پوشاند اختلاف داشتند. در v2.766.64 ‏HotPDF رندر و export به SVG و viewer و چاپ را به‌رسمیت شناختن CropBox تغییر داد: صفحه از طریق CropBox اش بریده‌شده به MediaBox اش نمایش داده می‌شود، همان چیزی که ISO 32000-1 §14.11.2 تجویز می‌کند، و GetLoadedPageVisibleBox اضافه شد تا آن جعبهٔ مرئی را برگرداند. قابلیت‌های بازشناسی همچنان transform دستگاه-به-صفحه را از GetLoadedPageBox(PageIndex, pbMediaBox, ...) می‌ساختند. raster حالا جعبهٔ مرئی را می‌پوشاند، transform همچنان MediaBox را فرض می‌کرد، و هر موقعیت بازشناسی‌شده با فاصلهٔ بین آن دو آفست برمی‌گشت

پس پنجرهٔ درگیر دقیق است. ‏ApplyLoadedOCRTextLayer متن را از v2.766.64 تا v2.770.152 سر جای اشتباه می‌گذاشت. ‏DecodeLoadedPageBarcodes تمام‌صفحه و تشخیص چهره داخل DetectLoadedRedactionFindings یک بیلد بیشتر یعنی تا v2.770.153 اشتباه ماندند. قبل از v2.766.64 renderer کل MediaBox را می‌کشید، پس نگاشت و raster توافق داشتند، به بهای بازشناسی محتوایی که viewerها هرگز نشان نمی‌دهند. فیکس‌ها برای هر قابلیت سه چیز را با هم عوض کردند: transform، تخمین بودجهٔ پیکسل، و جعبهٔ صفحه‌ای که در رکورد درخواست به یک موتور سفارشی داده می‌شود

چند حالت هرگز درگیر نشدند:

  • صفحات بدون /CropBox یا صفحه‌هایی که CropBox شان با MediaBox برابر است، قبل و بعد از فیکس یکسان نگاشت می‌شوند
  • DecodeLoadedPageBarcodes با HasRegion ست‌شده دقیقاً همان ناحیه‌ای که می‌دهی را رندر و از طریق همان ناحیه نگاشت می‌کند، پس decode ناحیهٔ صریح در کل این مدت درست بود؛ چک اینکه ناحیه داخل صفحه باشد همچنان از MediaBox استفاده می‌کند
  • یافته‌های حذف مبتنی بر الگو (ایمیل‌ها و شماره کارت و از این قبیل) از استخراج متن در user space می‌آیند نه از یک raster، پس فقط یافته‌های تشخیص چهره جابه‌جا می‌شدند

سه دستگاه مختصات، و اینکه هر API در HotPDF از کدام استفاده می‌کند

کد HotPDF ای که با بازشناسی سر و کار دارد با سه دستگاه سروکار دارد و بیشتر باگ‌های نگاشت از قاطی کردن دو تایشان می‌آیند

  • پیکسل‌های بیت‌مپ: مبدأ بالا-چپ، Y رو به پایین رشد می‌کند، واحدها پیکسل در DPI درخواست‌اند. ‏THPDFOCRWord.Left و Top و Right و Bottom در این دستگاه‌اند، همانند نقاط baseline اختیاری، نتایجی که یک IHPDFBarcodeDecoder سفارشی برمی‌گرداند، و جعبه‌های یک IHPDFFaceDetector سفارشی
  • user space در PDF برای صفحهٔ بارگذاری‌شده: مبدأ پایین-چپ، Y رو به بالا رشد می‌کند، واحدها point هستند و ‏Bottom < Top. ‏GetLoadedPageBox و GetLoadedPageVisibleBox مقدارهای Left و Bottom و Right و Top را در این دستگاه برمی‌گردانند و فیلدهای PageLeft و PageBottom و PageRight و PageTop از THPDFOCRRequest و محدوده‌های THPDFDecodedBarcode و مستطیل‌های THPDFRedactionFinding هم همین‌طور
  • مختصات رسم صفحه در HotPDF: API ای که برای ساختن صفحات جدید استفاده می‌کنی (خروجی متن، شکل‌ها، بارکدها، لینک‌ها، فیلدهای فرم) با مبدأ بالا-چپ و Y رو به پایین کار می‌کند. آن دستگاه به تولید سند تعلق دارد و هیچ ربطی به APIهای سند بارگذاری‌شدهٔ بالا ندارد، پس هرگز یک مستطیل user space از صفحهٔ بارگذاری‌شده را بدون تغییر به آن نده

رکورد کلمهٔ OCR عمداً مبتنی بر پیکسل است: یک موتور گزارش می‌دهد در تصویر چه دیده و ApplyLoadedOCRTextLayer مالک تبدیل است. آن تقسیم کار فقط وقتی جواب می‌دهد که تبدیل از جعبهٔ درست استفاده کند، همان چیزی که v2.770.153 بازگرداند

دستگاه‌های مختصات بازشناسی در HotPDF: پیکسل‌های بیت‌مپ با مبدأ بالا-چپ که جعبه‌های THPDFOCRWord و decoderهای سفارشی استفاده می‌کنند، user space در PDF با مبدأ پایین-چپ که GetLoadedPageBox و GetLoadedPageVisibleBox برمی‌گردانند، و API رسم صفحه با مبدأ بالا-چپ که هرگز نباید یک مستطیل صفحهٔ بارگذاری‌شده بدون تغییر بگیرد
موتورها پیکسل گزارش می‌کنند چون همان چیزی است که دیده‌اند، ‏HotPDF آن‌ها را نگاشت می‌کند، و قاطی کردن آن دو دستگاه همان چیزی است که لایه‌ها و جعبه‌های حذف را منحرف می‌کند

transform دستگاه-به-صفحه پشت OCR و بارکدها و چهره‌ها

‏HotPDF پیکسل‌های بیت‌مپ را با یک ماتریس آفین تکی ساخته‌شده از پنج ورودی به صفحه نگاشت می‌کند: چرخش، مقیاس یعنی DPI / 72، ارتفاع بیت‌مپ، و Left و Bottom و Right و Top جعبهٔ رندرشده. ‏OCR و decode بارکد و تشخیص چهره همه یک رویهٔ مشترک برای این دارند، و برای همین یک ورودی جعبهٔ اشتباه هر سه را به یک شکل می‌شکست. برای صفحهٔ نچرخیده ماتریس صفحه-به-دستگاه یعنی [A B C D E F] چنین است:

  • A = Scale و D = -Scale، که در آن Scale = DPI / 72؛ آن D منفی user space را (Y رو به بالا) به فضای بیت‌مپ (Y رو به پایین) می‌چرخاند
  • B = C = 0، چون یک صفحهٔ نچرخیده هیچ shear یا جابه‌جایی بین محورها ندارد
  • E = -Left * Scale، که لبهٔ چپ جعبه را به ستون پیکسلی 0 می‌برد
  • F = BitmapHeight + Bottom * Scale، که لبهٔ پایین جعبه را به y = BitmapHeight یعنی لبهٔ پایینی بیت‌مپ نگاشت می‌کند، پس لبهٔ بالا روی ردیف 0 می‌نشیند

پیکسل‌ها از طریق معکوس آن ماتریس به صفحه برمی‌گردند. ‏Request.PageRotation مقدار /Rotate صفحه را نرمال‌شده به 0 و 90 و 180 یا 270 حمل می‌کند (هر مقداری که مضرب 90 نبود 0 تلقی می‌شود) و renderer صفحه را ساعتگرد می‌چرخاند همان‌طور که ISO 32000-1 §7.7.3.3 الزام می‌کند. زیر چرخش محورها جابه‌جا می‌شوند و جفت متفاوتی از لبه‌های جعبه به مبدأ بیت‌مپ پین می‌شود. به‌شکل فرمول‌های معکوس نوشته‌شده، با S = DPI / 72 و x و y بر حسب پیکسل و H ارتفاع بیت‌مپ:

/RotateX صفحهY صفحهلبه‌های جعبه‌ای که نگاشت به آن‌ها وابسته است
0Left + x / SBottom + (H - y) / S‏Left، ‏Bottom
90Left + y / SBottom + x / S‏Left، ‏Bottom
180Right - x / SBottom + y / S‏Right، ‏Bottom
270Right - y / STop - x / S‏Right، ‏Top

ستون آخر توضیح می‌دهد چرا باگ در production تصادفی به نظر می‌رسید. یک CropBox که فقط بالای صفحه را می‌بُرد Left و Bottom را دست‌نخورده می‌گذارد، پس صفحات ایستاده بی‌نقص بیرون می‌آمدند و فقط صفحات حامل /Rotate 270 منحرف می‌شدند. چرخش بعد بیت‌مپ را هم جابه‌جا می‌کند: در 90 و 270 بیت‌مپ (Top - Bottom) * S پیکسل عرض و (Right - Left) * S پیکسل ارتفاع دارد

جدول نگاشت معکوس HotPDF برای چرخش صفحه: در /Rotate 0 و 90 پین transform لبه‌های Left و Bottom جعبهٔ رندرشده است، در 180 یعنی Right و Bottom، در 270 یعنی Right و Top، و برای همین صفحهٔ بریده‌شده برای هر جهت در یک سند مخلوط در جهت متفاوتی منحرف می‌شود
همان برش نیم‌اینچی وقتی صفحات مقادیر متفاوتی از /Rotate دارند مثل سه باگ متفاوت به نظر می‌رسد، چون هر جهت جفت متفاوتی از لبه‌های جعبه را پین می‌کند

با MediaBox یعنی [0 0 612 792] و CropBox یعنی [36 36 576 756] چه چیزی خراب می‌شود؟

با یک برش نیم‌اینچی در هر سمت، لایهٔ متن صفحهٔ نچرخیده وقتی از MediaBox استفاده شود دقیقاً 36 point سمت چپ و 36 point زیر کلمات اسکن‌شده می‌نشیند. یک صفحهٔ US Letter در نظر بگیر که CropBox اش از هر لبه 36 point (نیم اینچ) می‌بُرد. جعبهٔ مرئی 540 در 720 point است، پس در رزولوشن پیش‌فرض OCR یعنی 300 DPI مقیاس 300 / 72 ≈ 4.1667 می‌شود و بیت‌مپ 2250 در 3000 پیکسل

فرض کن موتور کلمه‌ای با جعبهٔ پیکسلی Left برابر 450 و Top برابر 600 و Right برابر 900 و Bottom برابر 660 و بدون baseline گزارش کند. ‏HotPDF بعد baseline را بیست درصد ارتفاع کلمه بالاتر از لبهٔ پایین یعنی ردیف پیکسلی 648 می‌گذارد و نقطهٔ شروع یعنی (450, 648) را نگاشت می‌کند:

  • از طریق جعبهٔ مرئی: ‏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) point
کالبدشکافی انحراف CropBox در HotPDF روی صفحهٔ US Letter با MediaBox یعنی 0 0 612 792 و CropBox یعنی 36 36 576 756: ‏renderer جعبهٔ مرئی را در 300 DPI rasterize می‌کند، پس نگاشت پیکسل کلمهٔ 450 از طریق GetLoadedPageVisibleBox مقدار 144.0 و 600.48 می‌دهد در حالی که transform یعنی MediaBox روی 108.0 و 564.48 می‌نشیند
raster جعبهٔ CropBox را می‌پوشاند، پس هر transform ای که از MediaBox ساخته شده هر کلمهٔ بازشناسی‌شده را دقیقاً به اندازهٔ حاشیهٔ برش جابه‌جا می‌کند

همان صفحه را بچرخان و جهت خطا عوض می‌شود، چون لبه‌های دیگری درگیرند. در /Rotate 180 جملهٔ X از Right استفاده می‌کند و 612 به‌جای 576 لایه را 36 point به راست می‌راند در حالی که Bottom همچنان 36 point پایین می‌کشدش. در /Rotate 270 هر دو Right و Top بیش از حد بزرگ‌اند، پس لایه 36 point به راست و 36 point به بالا می‌رود. سندی با جهت‌های مخلوط می‌تواند انحراف را در سه جهت نشان بدهد، یک اثر انگشت قابل‌اتکا برای این باگ. کد دست‌نویسی که مقیاس را از جعبه مشتق می‌کند، مثل Bitmap.Width / (Right - Left)، هر مختصات را هم روی 612 / 540 یعنی حدود 13 درصد، روی همان آفست می‌کشد

کدام سندهای PDF تو درگیرند؟

یک سند PDF وقتی درگیر است که دست‌کم یک صفحه‌اش جعبهٔ مرئی متفاوت از MediaBox اش داشته باشد، و ‏HotPDF می‌تواند این را در چند خط به تو بگوید. ‏GetLoadedPageBox با pbMediaBox را برای هر صفحه با GetLoadedPageVisibleBox مقایسه کن و ‏GetLoadedPageRotation را کنارش چاپ کن تا جهت انحراف را از جدول بالا پیش‌بینی کنی. ‏THPDFPageBoundary ‏pbCropBox و pbBleedBox و pbTrimBox و pbArtBox را هم دارد، اما GetLoadedPageBox(pbCropBox) وقتی crop box وجود ندارد به MediaBox برمی‌گردد و برش نمی‌زند، پس جعبهٔ مرئی چیز درستی است که با آن مقایسه کنی

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;
      // ممکن است آرایهٔ ذخیره‌شده گوشه‌هایش را به هر ترتیبی فهرست کرده باشد
      if MR < ML then begin Tmp := ML; ML := MR; MR := Tmp; end;
      if MT < MB then begin Tmp := MB; MB := MT; MT := Tmp; end;
      // از قبل نرمال‌شده و بریده‌شده به 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 برای اسکریپت‌هایی مثل این مهم است. تابع وقتی شکست می‌خورد پارامترهای out اش را دست‌نخورده می‌گذارد، پس از پیش تنظیم کردن یک اندازهٔ صفحهٔ پیش‌فرض قبل از فراخوانی الگوی امنی است. و وقتی یک CropBox بدشکل اصلاً MediaBox را قطع نمی‌کند، تابع MediaBox را برمی‌گرداند نه یک مستطیل خالی. اگر گزارش صفحاتی را فهرست کرد و بیلد deploy‌شده‌ات برای OCR قدیمی‌تر از v2.770.153 یا برای بارکدها و چهره‌ها قدیمی‌تر از v2.770.154 است، بعد از ارتقا بازشناسی را روی آن صفحات دوباره اجرا کن. یک لایهٔ OCR که توسط یک بیلد درگیر ثبت شده در فایل ذخیره‌شده می‌ماند و آپشن پیش‌فرض SkipPagesWithText در گذر دوم آن صفحات را skip می‌کند مگر اینکه خاموشش کنی یا اول لایهٔ قدیمی را برداری

یک IHPDFOCREngine سفارشی باید پیکسل‌ها را چطور به فضای PDF برگرداند؟

یک IHPDFOCREngine سفارشی باید جعبه‌های کلمه را در پیکسل‌های بیت‌مپ برگرداند و نگاشت را به HotPDF بسپارد؛ فقط برای تصمیم‌های خودت به user space تبدیل کن، و بعدش از جعبهٔ داخل درخواست استفاده کن، هرگز از MediaBox نه. از v2.770.153 مقدارهای PageLeft و PageBottom و PageRight و PageTop درخواست جعبهٔ مرئی رندرشده را توصیف می‌کنند، پس دقیقاً با Request.Bitmap می‌خوانند. هلپر زیر معکوس transform کتابخانه است، از جمله استفاده‌اش از ارتفاع واقعی بیت‌مپ برای صفحات ایستاده، پس تا سطح پیکسل با HotPDF توافق دارد

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

// پیکسل بیت‌مپ (مبدأ بالا-چپ، Y رو به پایین) به user space در PDF
// (مبدأ پایین-چپ، Y رو به بالا)، از طریق جعبه‌ای که بیت‌مپ از آن رندر شده
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 داخل یک موتور یک قاعدهٔ ناحیه است: فاکتورهایی که سربرگشان را هرگز نمی‌خواهی قابل‌جست‌وجو شود، یا یک ناحیهٔ مهر که بازشناس را گیج می‌کند. موتور زیر که با TInterfacedObject نوشته شده تا شمارش ارجاع عمرش را اداره کند، کلمات را بر اساس اینکه مرکزهایشان کجای صفحه می‌افتد فیلتر می‌کند و بعد بازماندگان را بدون تغییر در مختصات پیکسلی برمی‌گرداند. ‏RunRecognizer جای فراخوانی بازشناس خودت می‌ایستد

type
  TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
  private
    FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single;  // user space
    function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
      out Words: THPDFOCRWords): boolean;  // بازشناس خودت، جعبه‌های پیکسلی
  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];  // همچنان پیکسل: HotPDF خودش نگاشت می‌کند
    Inc(Count);
  end;
  SetLength(Words, Count);
  Result := True;
end;

موتور را مثل هر موتور دیگری به ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) بده. کتابخانه قبل از اعتماد کردن چکی که برمی‌گردد را اعتبارسنجی می‌کند: یک کلمه وقتی از بیت‌مپ بیرون بزند یا وقتی Right <= Left یا Bottom <= Top یا وقتی Confidence بیرون 0..1 یا زیر MinimumConfidence باشد انداخته شده و در Info.DroppedWordCount شمرده می‌شود. برگرداندن کلمات بیشتر از MaxWordsPerPage یا هل دادن مجموع در حال پیشرفتی از MaxTotalWords، کل فراخوانی را با خطای بودجه شکست می‌دهد، پس در موتور به Request.MaxWords احترام بگذار. جعبه‌های کلمه را قبل از برگرداندن به user space تبدیل نکن؛ ‏HotPDF مقادیر point را پیکسل تلقی می‌کرد و لایه به سمت مبدأ بیت‌مپ فرو می‌ریخت

نگاشت خروجی detector خودت

همان هلپر به یک خط لولهٔ خانگی که روی RenderLoadedPageToBitmap ساخته شده هم سرویس می‌دهد، که جعبهٔ مرئی را رندر و /Rotate را همان‌طور که قابلیت‌های بازشناسی اعمال می‌کنند اعمال می‌کند. جعبه را با GetLoadedPageVisibleBox بخوان، چرخش را همان‌طور که HotPDF می‌کند نرمال کن، و دو گوشهٔ مقابل هر جعبهٔ پیکسلی را نگاشت کن. محور Y برمی‌گردد و در 90 و 270 درجه محورها جابه‌جا می‌شوند، پس گوشه‌های نگاشت‌شده به هیچ ترتیب ثابتی بیرون نمی‌آیند؛ کمینه و بیشینهٔ نقاط نگاشت‌شده را بگیر، که همان‌طور است که HotPDF محدوده‌های بارکد را می‌سازد

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);  // کد خودت، جعبهٔ پیکسلی
      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;

رفتار چرخش در تخت کردن چرخش صفحه بدون شکستن جعبه‌های صفحه عمیق‌تر پوشش داده شده و خط لولهٔ decode بارکد که همان transform را مصرف می‌کند در decode کردن کدهای QR چرخیده از صفحات PDF. اگر موتورت یک بازشناس بیرونی را می‌پیچد، ‏آداپتور Tesseract OCR برای PDF قابل‌جست‌وجو سمت جداسازی فرایند و لغو همان interface را نشان می‌دهد

مرجع سریع: نگاشت مختصات امن نسبت به CropBox

  • ‏renderer جعبهٔ مرئی یعنی CropBox بریده‌شده به MediaBox را rasterize می‌کند (‏ISO 32000-1 §14.11.2)؛ هر نگاشت پیکسل-به-صفحه باید از آن جعبه استفاده کند که با GetLoadedPageVisibleBox خوانده می‌شود
  • ‏HotPDF v2.770.153 ‏ApplyLoadedOCRTextLayer را فیکس کرد؛ ‏v2.770.154 ‏DecodeLoadedPageBarcodes تمام‌صفحه و یافته‌های چهره از DetectLoadedRedactionFindings را؛ بیلدهای از v2.766.64 تا آن نسخه‌ها درگیرند
  • جعبه‌های THPDFOCRWord پیکسل‌های بیت‌مپ با مبدأ بالا-چپ‌اند؛ ‏GetLoadedPageBox و GetLoadedPageVisibleBox مقدار user space در PDF با مبدأ پایین-چپ و ‏Bottom < Top برمی‌گردانند
  • مقیاس DPI / 72 است؛ از DPI مشتقش کن، هرگز از تقسیم یک جعبهٔ صفحه بر عرض بیت‌مپ نه
  • ‏/Rotate تعیین می‌کند کدام لبه‌ها مهم‌اند: ‏Left و Bottom در 0 و 90، ‏Right و Bottom در 180، ‏Right و Top در 270
  • کلمات OCR را در پیکسل برگردان و نگاشت را به HotPDF بسپار؛ فقط برای منطق فیلتر خودت تبدیل کن
  • ‏OCR را روی صفحات بریده‌شده‌ای که یک بیلد درگیر پردازششان کرده دوباره اجرا کن و یادت باشد SkipPagesWithText صفحاتی که از قبل لایهٔ قدیمی را دارند skip می‌کند

قابلیت‌های بازشناسی و پرس‌وجوهای page box و رندر سند بارگذاری‌شده‌ای که اینجا استفاده شد همگی در کامپوننت HotPDF برای Delphi و C++Builder عرضه می‌شوند؛ لایسنس و دانلود نسخهٔ آزمایشی و فهرست کامل قابلیت‌ها در صفحهٔ کامپوننت HotPDF Delphi PDF