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