مقاله فنی

استخراج متن، تصویر و font از PDF در Delphi با PDF Library for Delphi

استخراج text، image و font از یک PDF موجود در ظاهر یک مسئلهٔ حل‌شده است، تا وقتی یک corpus واقعی را از آن عبور دهید. یک search indexer را روی چهل‌هزار file مشتری نشانه بگیرید و خرابی‌ها در چند دستهٔ قابل‌شناسایی مرتب می‌شوند. wordها به‌هم می‌چسبند چون هیچ‌کس به extractor نگفته چه اندازه gap به‌عنوان یک space حساب می‌شود. pageهای دیگر به‌شکل gibberish برمی‌گردند چون یک subsetted font هیچ mappingی از glyph codeهایش به characterهای واقعی ندارد. و «logoی شرکت» در نه image object مجزا ظاهر می‌شود که پشت یک soft mask روی هم چیده شده‌اند. هیچ‌کدام از این‌ها bug در کتابخانه نیست. تفاوت بین صدا زدن یک extraction function و درک اینکه آن function چه چیزی را می‌تواند و چه چیزی را نمی‌تواند از byteهای روی disk بازیابی کند، همین است

losLab PDF Library، ویرایش Pascal، به codeهای Delphi و C++Builder بیش از یک راه برای خواندن هر یک از آن سه stream می‌دهد، و این levelها در آنچه تضمین می‌کنند تفاوت دارند. نکته این است که level را به job متناسب کنید: یک search index، یک redaction reviewer و یک PDF/A preflight pass هر کدام چیزهای متفاوتی از یک page یکسان می‌خواهند، و درخواست کردن call اشتباه یا تلاش را هدر می‌دهد یا خروجی‌ای تولید می‌کند که نمی‌توانید به آن اعتماد کنید

levelهای استخراج متن و آنچه هر کدام وعده می‌دهند

GetPageText یک مقدار options از ۰ تا ۸ می‌گیرد، و آن عدد یک engine را به‌جای یک format انتخاب می‌کند. مقدارهای ۰ تا ۲ یک pass سبک اجرا می‌کنند که برای preview سریع کافی است. مقدارهای ۳ تا ۸ از engine آگاه به layout عبور می‌کنند، که lineها و spacing را از جایی که glyphها واقعاً روی page قرار دارند بازسازی می‌کند. درون آن بازه variationها اهمیت دارند: ۴ و ۶ خروجی را به word تقسیم می‌کنند، ۵ و ۶ per-glyph width بیرون می‌دهند، و ۷ plain text را با font، color و block metadata که عمداً drop شده‌اند برمی‌گرداند. option ۷ همان چیزی است که باید به یک search index بدهید، چون index فقط word می‌خواهد و هیچ چیز دیگر

هیچ تنظیم optionی نمی‌تواند documentی را نجات دهد که اطلاعات را از ابتدا همراه خود نداشته است. PDF character codeها را به glyph shapeها نگاشت می‌کند، و تنها چیزی که آن codeها را به text قابل‌خواندن برمی‌گرداند یک ToUnicode CMap در font است (ISO 32000-1 §9.10). وقتی یک subsetted font بدون آن منتشر می‌شود، هر extractor گیر می‌کند. این کتابخانه، copy-paste در یک viewer، یک toolkit رقیب: همه‌شان به حدس زدن از glyph nameها یا برگرداندن هیچ‌چیز تقلیل می‌یابند. پاسخ عملی detection است، نه قهرمانی. page را به‌عنوان low-confidence امتیاز دهید و به OCR بفرستید، چون gibberish را به‌صورت بی‌صدا index کردن بدتر از پذیرفتن این است که نمی‌توانید بخوانیدش

نمودار سطوح استخراج متن PDF در Delphi: گزینه‌های 0 تا 8 در GetPageText به یک گذر سبک یا موتور آگاه از چیدمان می‌روند و صفحه‌هایی که فونت‌های زیرمجموعه‌شان CMap با نام ToUnicode ندارند به OCR برمی‌گردند
مقادیر گزینه 0 تا 8 در GetPageText میان گذر پیش‌نمایش سبک و موتور آگاه از چیدمان انتخاب می‌کنند، گزینه 7 برای ایندکس‌گذاری جستجو رزرو شده و CMapهای ToUnicode غایب به OCR منحرف می‌شوند

برای مواردی که optionهای flat پوشش نمی‌دهند، tokenization سفارشی، content-stream forensics، یک text funnel ساخته‌شده بر اساس ruleهای خودتان، decoder یک لایه پایین‌تر در دسترس است. TPDFExtractor روی resources dictionary و font collection یک page ساخته می‌شود. متد ExtractTextW آن عملیات text خام content-stream را دوباره از همان ماشین font عبور می‌دهد تا Unicode را بازیابی کند، و event OnFindObject آن هر object را به‌محض عبور به دست شما می‌سپارد. بیشتر code هرگز به این عمق نیاز ندارد. applicationهایی که نیاز دارند، همان‌هایی هستند که خوشحالند این لایه public است نه دفن‌شده

blockهای position‌دار: واحد search hitها و بازبینی redaction

plain text می‌گوید page چه می‌گوید. دیر یا زود یک product همچنین باید بداند کجا می‌گوید، تا بتواند یک search hit را highlight کند، دور یک redaction candidate کادر بکشد، یا یک annotation را به جای درست anchor کند. ExtractPageTextBlocks یک handle به یک list از text runها برمی‌گرداند، و هر run متن خود، bounding box خود، و font name و sizeیی که در آن set شده را حمل می‌کند:

var
  Pdf: TPDFlib;
  Blocks, I: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('contract.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    Pdf.SelectPage(1);
    Blocks := Pdf.ExtractPageTextBlocks(0);
    for I := 0 to Pdf.GetTextBlockCount(Blocks) - 1 do
      Writeln(Format('%s  [%s %.1f pt at %.0f,%.0f]',
        [Pdf.GetTextBlockText(Blocks, I),
         Pdf.GetTextBlockFontName(Blocks, I),
         Pdf.GetTextBlockFontSize(Blocks, I),
         Pdf.GetTextBlockBound(Blocks, I, 0),
         Pdf.GetTextBlockBound(Blocks, I, 1)]));
    Pdf.ReleaseTextBlocks(Blocks);
  finally
    Pdf.Free;
  end;
end;

یک detail در این حوزه بیشتر از هر چیز دیگری integrationها را به‌هم می‌ریزد. SetTextExtractionArea، SetTextExtractionWordGap و SetTextExtractionOptions state در سطح document هستند که باقی می‌مانند، نه argumentهایی که per call پاس می‌دهید. یک area restriction را برای یک feature پیکربندی کنید، مثلاً خواندن فقط header band برای classify کردن یک document، و آن بی‌صدا هر extractionی را که بعد از آن روی همان handle بیاید truncates می‌کند، از جمله levelهای آگاه به layout GetPageText که بعداً به آن‌ها دست می‌یابید. یا extraction state را بین taskهای منطقی reset کنید یا به هر task handle document خودش را بدهید

آستانه word-gap اهرم آن دسته اول از شکست‌هاست، یعنی wordهایی که به‌هم می‌چسبند. SetTextExtractionWordGap به engine layout می‌گوید چه مقدار فضای افقی، اندازه‌گیری‌شده در برابر glyph spacing خود page، یک word را از word بعدی جدا می‌کند. یک table فشرده gap کوچک‌تری می‌خواهد تا یک page تبلیغاتی با spacing شل، پس یک آستانه تنظیم‌شده per document class بهتر از یک ثابت سراسری عمل می‌کند. آن مانند بقیهٔ extraction state روی document باقی می‌ماند، پس عمداً set کنید به‌جای یک‌بار set کردن و فراموش کردنش

نموداری که نشان می‌دهد وضعیت استخراج PDF در سطح سند در Delphi روی یک handle در طول فراخوانی‌ها تا reset باقی می‌ماند و از برش بی‌صدای استخراج‌های بعدی جلوگیری می‌کند
ناحیه استخراج و فاصله کلمه و تنظیمات گزینه روی هندل سند ماندگارند، پس ناحیه‌ای که برای یک قابلیت تنظیم شده بی‌سروصدا هر استخراج بعدی را کوتاه می‌کند تا وضعیت ریست شود یا هندل عوض شود

imageها: streamهای اصلی، نه screenshot

راه اشتباه برای خارج کردن imageها از یک PDF این است که page را render کنید و آن را crop کنید. این کار pixelها را resample می‌کند، هر rotation را در آن bake می‌کند، و هرآنچه اصلی بوده را دور می‌ریزد. GetPageImageList به‌جای آن image resourceهای واقعی را که page به آن‌ها reference می‌دهد برمی‌شمارد، و هر مورد propertyهایش و دادهٔ اصلی و دست‌نخورده‌اش را برمی‌گرداند:

var
  ImgList, I: Integer;
begin
  Pdf.SelectPage(1);
  ImgList := Pdf.GetPageImageList(0);
  for I := 0 to Pdf.GetImageListCount(ImgList) - 1 do
  begin
    Writeln(Pdf.GetImageListItemFormatDesc(ImgList, I, 0));
    Pdf.SaveImageListItemDataToFile(ImgList, I, 0,
      Format('page1-img%.2d.bin', [I]));
  end;
  Pdf.ReleaseImageList(ImgList);
end;

قبل از هر فرضی درباره یک مورد، GetImageListItemFormatDesc را بررسی کنید، چون آنچه یک page reference می‌دهد به‌ندرت یک عکس مرتب per image قابل‌مشاهده است. یک soft mask به‌عنوان entry مجزای خودش ظاهر می‌شود. همان XObject اغلب در صفحات زیادی تکرار می‌شود، پس قبل از اینکه یک export «همهٔ imageها» را archive کنید، بر اساس content hash Deduplicate کنید، وگرنه همان logo را صد بار می‌نویسید. CMYK JPEGها نیاز دارند color management پایین‌دست اعمال شود، وگرنه در viewerهایی که channelها را به‌عنوان مقدار واقعی می‌گیرند معکوس render می‌شوند. وقتی یک inventory در سطح document به‌جای یک page در هر بار می‌خواهید، FindImages همراه با SetFindImagesMode کل file را در یک pass scan می‌کند

یک مرز هست که ارزش دارد قبل از اینکه کسی acceptance criteria بنویسد با stakeholderها مطرح شود: image extraction فقط raster resourceها را برمی‌گرداند. یک logo یا chart که به‌شکل vector path کشیده شده در معنای resource یک image نیست و در هیچ image listای ظاهر نمی‌شود، مهم‌نمی‌کند روی صفحه چقدر واضح به‌عنوان عکس خوانده می‌شود. وقتی نیاز واقعاً تحویل آن chart به‌عنوان یک file است، رویکرد صادقانه این است که region صفحه را به یک bitmap render کنید، که یک عملیات متفاوت با fidelity متفاوت است. این دو نوع خروجی نباید بدون یک label که بگوید کدام کدام است در یک export folder باشند

مقایسه رندر کردن صفحه PDF در Delphi برای گرفتن تصاویر با استخراج streamهای تصویر اصلی با GetPageImageList، به همراه نکات soft-mask و XObject تکراری و CMYK
رندر و برش پیکسل‌ها را بازنمونه می‌کنند و داده اصلی تصویر را دور می‌ریزند، در حالی که GetPageImageList منابع تصویر ذخیره‌شده را با ویژگی‌ها و استریم‌های دست‌نخورده‌شان برمی‌شمارد

fontها: یک سطح audit، نه یک feature export

font API به سوالاتی درباره fontها پاسخ می‌دهد. این API fileهای font خود را به دست شما نمی‌دهد، و این تمایز هرآنچه می‌توانید روی آن بسازید را شکل می‌دهد. بعد از اینکه FindFonts document را scan کرد، enumeration fontها را بر اساس ID طی می‌کند، و property callها درباره هر fontی که الان selected است گزارش می‌دهند:

var
  I: Integer;
begin
  Pdf.FindFonts;
  for I := 1 to Pdf.FontCount do        // اندیس‌های font از 1 شروع می‌شوند، نه از 0
    if Pdf.SelectFont(Pdf.GetFontID(I)) = 1 then
      Writeln(Format('%s  type=%d  embedded=%d  subset=%d',
        [Pdf.FontName, Pdf.FontType,
         Pdf.GetFontIsEmbedded, Pdf.GetFontIsSubsetted]));
end;

به loop boundها توجه کنید. font indexها از ۱ تا FontCount می‌روند، در حالی که text-block و image-list indexها چند پاراگراف بالاتر zero-based هستند. یک convention را به دیگری بیاورید و یک off-by-one می‌گیرید که یا اولین font را skip می‌کند یا از انتهای list بیرون می‌زند، و از testing معمولی عبور می‌کند چون بیشتر documentها چندین font دارند و font اشتباه هنوز قابل‌قبول به‌نظر می‌رسد. درباره scope هم واضح باشید. این API هیچ font export در سطح byte ندارد. هیچ callی برنامهٔ font embedded را به‌عنوان یک file TTF یا OTF برنمی‌گرداند، و enumeration به‌علاوهٔ inspection متادیتا کل model در نظر گرفته‌شده است. آن model با این حال آنچه کار production واقعاً از fontها می‌خواهد را پوشش می‌دهد: detection subset بر اساس name pattern، auditهای embedding قبل از یک تبدیل archival (یک font unembedded یک blocker سخت PDF/A است، همان‌طور که PDF/A و preflight PDF/UA در Delphi توضیح می‌دهد)، و diagnostics encoding برای وقتی confidence استخراج افت می‌کند. همچنین یک دلیل licensing هست که مرز اینجا قرار دارد. یک برنامهٔ subset font مادی licensed است و، با نبود بیشتر glyphهایش، به‌هرحال به‌عنوان یک font قابل‌نصب بی‌فایده است. در نظر گرفتن آن به‌عنوان audit metadata به‌جای یک asset قابل‌استخراج، جایگاهی است که می‌توانید از آن دفاع کنید

آن call آخر در triage وزن خودش را حس می‌کند. GetFontEncoding را روی هر font اجرا کنید، آن را در کنار subset flag بخوانید، و می‌توانید کیفیت استخراج را قبل از کشیدن حتی یک character پیش‌بینی کنید. یک page که fontهایش همگی subsetted با encodingهای non-standard هستند، فقط با inspection یک candidate برای OCR است، که به یک batch pipeline اجازه می‌دهد آن را بدون اینکه اول یک pass استخراج ناموفق رویش هدر دهد، درست route کند

استخراج در مقیاس بدون load کردن documentها

در یک batch pipeline، load کردن کل یک document فقط برای خواندن یک page، I/O هدر‌شده است، و این در سراسر یک corpus سریع جمع می‌شود. variantهای single-call، ExtractFilePageText و ExtractFilePageTextBlocks، مستقیماً یک file name، password و page number می‌گیرند و از full load عبور نمی‌کنند. برای fileهای در مقیاس gigabyte یک gear پایین‌تر هم هست. مسیر direct-access یک file را از طریق streaming xref readها باز می‌کند، پس DAOpenFileReadOnly به‌دنبال DAExtractPageText فقط objectهایی را touch می‌کند که آن page واقعاً نیاز دارد. این با یک shift در convention می‌آید که ارزش حفظ کردن دارد: functionهای DA pageها را بر اساس PageRef خطاب می‌کنند، یک handle object-reference که از DAFindPage می‌گیرید، نه بر اساس page number خام. number را جایی که handle باید باشد پاس کنید و call روی object اشتباه بدون raising کردن یک error عمل می‌کند، که بدترین نوع اشتباه برای debug است. بقیهٔ toolkit direct-access در merge، split و direct access بزرگ PDF توضیح داده شده است

اگر یک عادت واحد وجود دارد که code استخراجی که در برابر یک corpus واقعی دوام می‌آورد را از codeیی که limp می‌زند جدا می‌کند، آن در نظر گرفتن page به‌عنوان untrusted input به‌جای یک data source تمیز است. متنی که با آنچه viewer render می‌کند مخالف است، تقریباً همیشه یک مسئلهٔ encoding است، یک ligature که به یک glyph فرومی‌ریزد یا یک subset font که entryهای ToUnicode‌اش را گم کرده، و راه‌حل سنجش confidence و divert کردن pageهای بد به OCR است، نه جنگیدن با byteها. font API هرگز یک TTF یا OTF تولید نمی‌کند، بر اساس design، پس workflowهای font را دور سوالات audit بسازید. و extraction state ماندگار، area rectangle مهم‌تر از همه، یک تنظیم است که شما برای عمر یک document handle مالک آن هستید، نه یک parameter که بعد از یک call فراموش می‌کنید. آن سه رفلکس را درست در بیاورید و بقیهٔ API به‌خوبی رفتار می‌کند

buildهای evaluation، projectهای demo و مرجع کامل API استخراج در page محصول losLab PDF Library برای Delphi هستند