تنزاح طبقات نص OCR وحدود الرموز الشريطية وصناديق إخفاء الوجوه على صفحات PDF المقصوصة حين تُعيَّن بكسلات الـ bitmap عائدةً عبر الـ MediaBox بدل الصندوق الذي نقّطه المحرك فعلاً: الـ CropBox المقيَّد بالـ MediaBox (ISO 32000-1 §14.11.2). أصلحت HotPDF هذا في ApplyLoadedOCRTextLayer في v2.770.153، وفي DecodeLoadedPageBarcodes و DetectLoadedRedactionFindings في v2.770.154
تقرير الخلل الذي يصل عادة يشبه هذا. أرشيف عقود ممسوح يمر عبر OCR، والمخرج قابل للبحث، وإصابة البحث عن رقم بند تُميَّز نصف بوصة تحت الرقم المطبوع وعن يساره. وأكثر ملفات الدفعة سليمة. والمعطوبة كلها جاءت من محطة مسح واحدة تكتب /CropBox لقص هامش السطح الزجاجي. تلك التفصيلة الواحدة تفصل الصورة التي رآها محرك OCR عن الإطار الذي وُضعت فيه طبقة النص، والتعارض نفسه يزيح حدود الرموز الشريطية، والأخطر، صناديق إخفاء الوجوه
لماذا تنحرف طبقة نص OCR عن الكلمات الممسوحة؟
تنحرف طبقة النص لأن نصفي المسار اختلفا حول أي مستطيل يغطيه الـ bitmap. في v2.766.64 غيّرت HotPDF العرض وتصدير SVG والعارض والطباعة لتقديس الـ CropBox: صفحة تُعرض عبر CropBox مقيد بالـ MediaBox، وهو ما يصوغه ISO 32000-1 §14.11.2، وأُضيفت GetLoadedPageVisibleBox لتعيد ذلك الصندوق المرئي. أما ميزات التعرف فما زالت تبني تحويلها من الجهاز إلى الصفحة من GetLoadedPageBox(PageIndex, pbMediaBox, ...). فصار التنقيط يغطي الصندوق المرئي والتحويل ما زال يفترض الـ MediaBox، وكل موضع معروف عاد مزاحاً بمقدار الفجوة بين الاثنين
النافذة المتأثرة إذن دقيقة. أخطأت ApplyLoadedOCRTextLayer وصول النص من v2.766.64 حتى v2.770.152. وبقيت DecodeLoadedPageBarcodes على الصفحة الكاملة وكشف الوجوه داخل DetectLoadedRedactionFindings خاطئتين بناءً إضافياً، حتى v2.770.153. وقبل v2.766.64 كان المحرك يرسم الـ MediaBox كله، فاتفقت الخريطة والتنقيط، على حساب التعرف على محتوى لا تعرضه العارضات قط. وغيّرت الإصلاحات ثلاثة أشياء معاً لكل ميزة: التحويل، وتقدير ميزانية البكسل، وصندوق الصفحة المسلَّم إلى محرك مخصص في سجل الطلب
وعدة حالات لم تتأثر قط:
- الصفحات بلا
/CropBox، أو التي يساوي CropBox فيها الـ MediaBox، تُعيَّن بشكل متطابق قبل الإصلاح وبعده -
DecodeLoadedPageBarcodesبـHasRegionمضبوطاً تعرض المنطقة التي تمررها بالضبط وتعيّن عبر تلك المنطقة ذاتها، فكان تعلم المنطقة الصريح صحيحاً طوال الوقت؛ وفحص استقرار المنطقة داخل الصفحة ما زال يستخدم الـ MediaBox - نتائج الإخفاء القائمة على الأنماط (رسائل بريد وأرقام بطاقات ونحوها) تأتي من استخراج نص في فضاء المستخدم لا من تنقيط، فنتائج كشف الوجوه وحدها هي التي تحركت
ثلاثة أطر إحداثيات، وأي APIs لدى HotPDF يستخدم كل واحد
كود HotPDF الذي يلمس التعرف يتعامل مع ثلاثة أطر، وأغلب أعطاب التعيين تأتي من خلط اثنين منها
- بكسلات bitmap: أصل أعلى اليسار، و Y ينمو نزولاً، والوحدات بكسلات عند DPI الطلب. قيم
THPDFOCRWord.LeftوTopوRightوBottomفي هذا الإطار، وكذلك نقاط خط الأساس الاختيارية والنتائج التي يعيدهاIHPDFBarcodeDecoderمخصص والصناديق منIHPDFFaceDetectorمخصص - فضاء مستخدم PDF لصفحة محمّلة: أصل أسفل اليسار، و Y ينمو صعوداً، والوحدات نقاط، و
Bottom < Top. تعيدGetLoadedPageBoxوGetLoadedPageVisibleBoxقيم Left و Bottom و Right و Top في هذا الإطار، وكذلك حقولPageLeftوPageBottomوPageRightوPageTopفيTHPDFOCRRequestوالحدود فيTHPDFDecodedBarcodeوالمستطيلات فيTHPDFRedactionFinding - إحداثيات رسم صفحات HotPDF: الـ API الذي تستخدمه لبناء صفحات جديدة (إخراج نص وأشكال ورموز شريطية وروابط وحقول نماذج) يعمل بأصل أعلى اليسار و Y نازلاً. ذلك الإطار يعود لتوليد المستندات ولا علاقة له بـ APIs المستند المحمّل أعلاه، فلا تطعم مستطيل فضاء مستخدم لصفحة محمّلة فيه دون تغيير أبداً
وسجل كلمة OCR قائم على البكسل عمداً: محرك يبلّغ عما رآه في الصورة، و ApplyLoadedOCRTextLayer تملك التحويل. ولا يسري ذلك التقسيم إلا حين يستخدم التحويل الصندوق الصحيح، وهو ما أعادته v2.770.153
التحويل من الجهاز إلى الصفحة خلف OCR والرموز الشريطية والوجوه
تعيّن HotPDF بكسلات bitmap إلى الصفحة بمصفوفة أفين واحدة مبنية من خمسة مدخلات: الدوران، والمقياس DPI / 72، وارتفاع الـ bitmap، وقيم Left و Bottom و Right و Top للصندوق المعروض. ويتقاسم OCR وفك الرموز الشريطية وكشف الوجوه روتيناً واحداً لذلك، ولهذا كسر مدخل صندوق واحد خاطئ الثلاثةَ بالطريقة نفسها. لصفحة غير مدورة تكون مصفوفة من الصفحة إلى الجهاز [A B C D E F] هكذا:
A = ScaleوD = -Scale، حيثScale = DPI / 72؛ فالقيمةDالسالبة تقلب فضاء المستخدم (Y صاعداً) إلى فضاء bitmap (Y نازلاً)B = C = 0، لأن صفحة غير مدورة بلا قص أو تبديل بين المحورينE = -Left * Scale، وهو ينقل الحافة اليسرى للصندوق إلى عمود البكسل 0F = BitmapHeight + Bottom * Scale، وهو يعيّن الحافة السفلية للصندوق إلى y = BitmapHeight، الحافة الدنيا للـ bitmap، فتهبط الحافة العليا على الصف 0
وتعود البكسلات إلى الصفحة عبر معكوس تلك المصفوفة. يحمل Request.PageRotation قيمة /Rotate للصفحة مطبَّعة إلى 0 أو 90 أو 180 أو 270 (أي قيمة ليست مضاعف 90 تعامل كـ 0)، ويدير المحرك الصفحة باتجاه عقارب الساعة كما يتطلب ISO 32000-1 §7.7.3.3. وتحت الدوران يتبادل المحوران وتُثبَّت زوج مختلف من حواف الصندوق على أصل الـ bitmap. مكتوبةً صيغاً معكوسة، مع S = DPI / 72 و x و y بالبكسلات و H ارتفاع الـ bitmap:
| /Rotate | Page X | Page 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 |
العمود الأخير يفسر لماذا بدا الخلل عشوائياً في الإنتاج. الـ CropBox الذي يقص أعلى الصفحة فقط يترك Left و Bottom بلا مساس، فخرجت الصفحات المعتدلة تامة والصفحات الحاملة /Rotate 270 وحدها انحرفت. كما يتبادل الدوران بُعدي الـ bitmap: عند 90 و 270 يكون الـ bitmap بعرض (Top - Bottom) * S بكسل وارتفاع (Right - Left) * S بكسل
ما الذي يخيب مع MediaBox [0 0 612 792] و CropBox [36 36 576 756]؟
مع قص نصف بوصة على كل جانب، تهبط طبقة نص صفحة غير مدورة على بعد 36 نقطة يسار الكلمات الممسوحة و 36 نقطة تحتها بالضبط حين يستخدم الـ MediaBox. خذ صفحة US Letter يقص CropBox لها 36 نقطة (0.5 بوصة) من كل حافة. الصندوق المرئي 540 في 720 نقطة، فعند دقة OCR الافتراضية 300 DPI يكون المقياس 300 / 72 ≈ 4.1667 والـ bitmap بـ 2250 في 3000 بكسل
افترض أن المحرك يبلّغ كلمة بصندوق بكسلي Left 450 و Top 600 و Right 900 و Bottom 660 وبلا خط أساس. يضع HotPDF بعدها خط الأساس 20 بالمئة من ارتفاع الكلمة فوق الحافة السفلية، عند الصف البكسلي 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) نقطة
وأدر الصفحة نفسها فيتغير اتجاه الخطأ، لأن حواف مختلفة تدخل الحساب. عند /Rotate 180 يستخدم حد X قيمة Right، و 612 بدل 576 تدفع الطبقة 36 نقطة يميناً بينما ما زالت Bottom تجذبها 36 نقطة نزولاً. وعند /Rotate 270 تكون Right و Top كلاهما أكبر من اللازم، فتتحرك الطبقة 36 نقطة يميناً و 36 نقطة صعوداً. والمستند ذو الاتجاهات المختلطة يستطيع أن يُظهر الانحراف في ثلاثة اتجاهات، وهي بصمة موثوقة لهذا الخلل. والكود المكتوب يدوياً الذي يشتق المقياس من الصندوق، مثل Bitmap.Width / (Right - Left)، يمطّ كل إحداثي بـ 612 / 540، نحو 13 بالمئة، فوق الإزاحة
أي مستندات PDF لديك متأثرة؟
المستند معرَّض حين تحمل صفحة واحدة على الأقل صندوقاً مرئياً يخالف الـ MediaBox، وتستطيع HotPDF إخبارك بذلك في بضعة أسطر. قارن GetLoadedPageBox بـ pbMediaBox مع GetLoadedPageVisibleBox لكل صفحة، واطبع GetLoadedPageRotation بجوارها لتستطيع توقع اتجاه الانحراف من الجدول أعلاه. كما توفر THPDFPageBoundary القيم pbCropBox و pbBleedBox و pbTrimBox و pbArtBox، لكن GetLoadedPageBox(pbCropBox) ترجع إلى الـ 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 بدل مستطيل فارغ. وإن سرد التقرير صفحات وكان بناؤك المنشور أقدم من v2.770.153 للـ OCR، أو من v2.770.154 للرموز الشريطية والوجوه، فأعد تشغيل التعرف على تلك الصفحات بعد الترقية. طبقة OCR ثبّتها بناء متأثر تبقى في الملف المحفوظ، والخيار الافتراضي SkipPagesWithText سيخطئ تلك الصفحات في تمريرة ثانية إلا إذا أطفأته أو أزلت الطبقة القديمة أولاً
كيف ينبغي لمحرك IHPDFOCREngine مخصص أن يُعيّن البكسلات عائدةً إلى فضاء PDF؟
ينبغي لمحرك IHPDFOCREngine مخصص أن يعيد صناديق الكلمات ببكسلات bitmap ويدع HotPDF تقوم بالتعيين؛ وحوّل إلى فضاء المستخدم لقراراتك أنت فقط، وحينها استخدم الصندوق الوارد في الطلب لا الـ MediaBox. ومنذ v2.770.153 تصف قيم PageLeft و PageBottom و PageRight و PageTop في الطلب الصندوقَ المرئي المعروضاً، فتطابق Request.Bitmap تماماً. والمساعد أدناه معكوس تحويل المكتبة، بما في ذلك استخدامه الارتفاع الفعلي للـ bitmap للصفحات المعتدلة، فيوافق HotPDF حتى البكسل
uses
System.SysUtils, System.Math, Vcl.Graphics, HPDFDoc;
// بكسل bitmap (أصل أعلى اليسار، Y نزولاً) إلى فضاء مستخدم PDF
// (أصل أسفل اليسار، Y صعوداً)، عبر الصندوق الذي عُرض منه الـ bitmap
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;
والسبب الواقعي لحاجة فضاء المستخدم داخل محرك هو قاعدة منطقة: فواتير لا تريد أن تكون ترويستها قابلة للبحث أبداً، أو منطقة ختم تربك المُعَرِّف. يصفّي المحرك أدناه، المكتوب بـ TInterfacedObject ليتولى عدّ المراجع عمره، الكلماتَ بحسب أين تقع مراكزها على الصفحة، ثم يعيد الناجيات بلا تغيير بإحداثيات بكسلية. ويمثل RunRecognizer نداء معرِّفك أنت
type
TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
private
FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single; // فضاء المستخدم
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) كما تفعل مع أي محرك آخر. وتتحقق المكتبة مما يعود قبل أن تثق به: تُسقط الكلمة وتُعَدّ في Info.DroppedWordCount حين يخرج صندوقها من الـ bitmap، أو حين Right <= Left أو Bottom <= Top، أو حين تكون Confidence خارج 0..1 أو تحت MinimumConfidence. وإعادة كلمات أكثر من MaxWordsPerPage، أو دفع الإجمالي الجاري بعد MaxTotalWords، يُسقط النداء كله بخطأ ميزانية، فأكرِم Request.MaxWords في المحرك. ولا تحوّل صناديق الكلمات إلى فضاء المستخدم قبل إعادتها؛ فستعامل HotPDF قيم النقاط بكسلاتٍ وستنهار الطبقة نحو أصل الـ bitmap
تعيين مخرج كاشفك أنت
يخدم المساعد نفسه مساراً محلي الصنع مبنياً على 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;
وسلوك الدوران مغطى بعمق أكبر في تسطيح دوران الصفحة دون كسر صناديق الصفحات، ومسار فك الرموز الشريطية الذي يستهلك التحويل نفسه في فك رموز QR المدورة من صفحات PDF. وإن كان محركك يغلّف مُعَرِّفاً خارجياً فتُظهر مُكيِّف Tesseract OCR لـ PDF قابل للبحث جانبَ عزل العمليات والإلغاء للواجهة نفسها
مرجع سريع: تعيين إحداثيات آمن ضد الـ CropBox
- ينقّط المحرك الصندوق المرئي، أي الـ CropBox المقيَّد بالـ MediaBox (ISO 32000-1 §14.11.2)؛ وكل تعيين من البكسل إلى الصفحة يجب أن يستخدم ذلك الصندوق المقروء بـ
GetLoadedPageVisibleBox - أصلحت HotPDF v2.770.153
ApplyLoadedOCRTextLayer؛ وأصلحت v2.770.154 DecodeLoadedPageBarcodesعلى الصفحة الكاملة ونتائج الوجوه منDetectLoadedRedactionFindings؛ والبناءات من v2.766.64 حتى تلك النسخ متأثرة - صناديق
THPDFOCRWordبكسلات bitmap بأصل أعلى اليسار؛ وتعيدGetLoadedPageBoxوGetLoadedPageVisibleBoxفضاء مستخدم PDF بأصل أسفل اليسار وBottom < Top - المقياس هو
DPI / 72؛ اشتقه من الـ DPI لا من قسمة صندوق صفحة على عرض الـ bitmap أبداً - يقرر /Rotate أي الحواف يهم: Left و Bottom عند 0 و 90، و Right و Bottom عند 180، و Right و Top عند 270
- أعد كلمات OCR بالبكسل ودع HotPDF تعيّنها؛ وحوّل فقط لمنطق التصفية الخاص بك
- أعد تشغيل OCR على الصفحات المقصوصة التي عالجها بناء متأثر، وتذكر أن
SkipPagesWithTextيخطئ الصفحات التي تحمل الطبقة القديمة أصلاً
ميزات التعرف واستعلامات صناديق الصفحات وعرض المستند المحمّل المستخدمة هنا تُشحن جميعها في مكوّن HotPDF لـ Delphi و C++Builder؛ والترخيص وتنزيلات التجربة وقائمة الميزات الكاملة على صفحة مكوّن HotPDF Delphi PDF component