مقاله فنی

انتخاب خط متن PDF با Char Box های PDFium در Delphi

یک text page در PDF کاراکترها و boxها را ارائه می‌دهد، هرگز خطوط را. PDFium Component یک خط بصری را با خوشه‌بندی boxهای کاراکتری که مراکز عمودی آن‌ها در نیمی از ارتفاع کاراکتر seed می‌افتد می‌سازد، و از کاراکتر کلیک‌شده به بیرون اسکن می‌کند تا زمانی که tolerance بیش‌ازحد شود. هر مسیر انتخاب در viewer آن یک کمک‌کننده را فراخوانی می‌کند، پس ماوس، صفحه‌کلید و کد موافق‌اند

علامتی که شما را به دنبال این می‌فرستد خاص و ناخوشایند است. یک کاربر روی یک پاراگراف در یک گزارش دوستونی triple-click می‌کند و نیمی از صفحه را می‌گیرد. یا روی یک سلول جدول triple-click می‌کند و انتخاب کل ردیف به‌علاوه شماره صفحه در footer را می‌بلعد. viewer خراب نیست؛ دارد سؤالی می‌پرسد که فایل نمی‌تواند پاسخ دهد. هیچ خطی در یک PDF برای انتخاب کردن وجود ندارد، و هر پیاده‌سازی که وانمود کند غیر از این است دارد حدس می‌زند. این مقاله درباره عمدی کردن آن حدس و سازگار کردن آن است. اگر واقعاً چیزی که نیاز دارید بیرون کشیدن متن از یک سند است، به استخراج متن از اسناد PDF با PDFium مراجعه کنید؛ اگر دارید متن را چیدمان می‌کنید و به عرض‌ها نیاز دارید، به اندازه‌گیری متن و word wrap مراجعه کنید. اینجا موضوع باریک‌تر است: تصمیم‌گیری اینکه یک خط بصری کجا شروع و پایان می‌یابد، و انتخاب دقیقاً همان

چرا یک text page در PDF هیچ شیء خطی ندارد؟

چون یک content stream در PDF رسم را توصیف می‌کند، نه ساختار را. ISO 32000-1 §9.4 یک شیء متن را به‌عنوان یک جفت BT / ET حاوی operatorهای موقعیت‌دهی و نمایش تعریف می‌کند. operatorهای موقعیت‌دهی §9.4.2 (Td، TD، Tm، T*) یک ماتریس متن را دور صفحه حرکت می‌دهند، و operatorهای نمایش §9.4.3 (Tj، TJ، '، ") glyphها را هرجا آن ماتریس در حال حاضر اشاره می‌کند رنگ می‌زنند. هیچ‌چیز در آن مدل نمی‌گوید "این اجرای glyphها یک خط است". یک خط چیزی است که یک انسان پس از پایان رنگ‌زدن می‌بیند

producerها این را به روش‌هایی که نمی‌توانید کنترل کنید بدتر می‌کنند. یک پاراگراف justify‌شده ممکن است به‌صورت یک آرایه TJ به‌ازای هر خط منتشر شود، یا به‌صورت یک Tj به‌ازای هر کلمه با یک Tm صریح پیش از هر کدام، یا به‌صورت یک عملیات نمایش واحد با تنظیمات kerning که فاصله‌گذاری را حمل می‌کند. یک چیدمان دوستونی ممکن است ستون چپ را از بالا به پایین منتشر کند و سپس ستون راست را، یا اگر producer فهرست شیء داخلی خودش را به ترتیبی متفاوت طی کرده ممکن است آن‌ها را تودرتو کند. دنباله کاراکتری که PDFium به شما می‌دهد content stream را دنبال می‌کند، و content stream هرچه برنامه تولیدکننده احساس می‌کرد انجام دهد را دنبال می‌کند. پس دو تابعی که واقعاً می‌گیرید FPDFText_CountChars است، که گزارش می‌دهد صفحه چند کاراکتر نگه می‌دارد، و FPDFText_GetCharBox، که bounding box یک کاراکتر را در فضای صفحه برمی‌گرداند. آن کل واژگان خام است. هرچیز بالای آن، کلمات، خطوط، پاراگراف‌ها، ستون‌ها، استنتاجی است که شما روی هندسه انجام می‌دهید

چرا تشخیص CR و LF آزمون اشتباهی است؟

چون کاراکترهایی که در برابرشان تست می‌کردید به‌طور قابل‌اعتماد حاضر نیستند، و وقتی حاضرند به‌طور قابل‌اعتماد مال شما نیستند. PDFium کاراکترهای مصنوعی را داخل text page تزریق می‌کند تا متن استخراج‌شده خوانا باشد: یک فاصله جایی که دو اجرا از نظر بصری جدا شده‌اند، یک CR یا LF جایی که اجرای بعدی روی یک baseline جدید شروع می‌شود. FPDFText_IsGenerated دقیقاً برای همین وجود دارد تا بتوانید آن‌ها را از کاراکترهایی که از فایل بیرون آمدند تشخیص دهید، و PDFium Component آن را به‌عنوان ویژگی CharacterGenerated ارائه می‌دهد

روی آن کاراکترها تقسیم کنید و هر قضاوتی که PDFium هنگام سنتز آن‌ها کرده را به ارث می‌برید. یک شکست خط سخت داخل یک پاراگراف wrap‌شده و یک wrap نرم پس از سنتز یکسان به‌نظر می‌رسند. یک ردیف جدول که producer سلول‌به‌سلول منتشر کرده ممکن است اصلاً هیچ شکستی بین آخرین سلول و اولین سلول ردیف بعدی نگیرد، چون baselineها اتفاقاً به‌اندازه کافی نزدیک‌اند. در همین حال یک عنوان که با متن بدنه در اندازه‌ای متفاوت دنبال می‌شود ممکن است دو شکست بگیرد جایی که یک انسان یکی می‌بیند. کاراکترهای تولیدشده یک راحتی رندر برای استخراج کل-صفحه هستند؛ یک مدل خط نیستند، و دقیقاً در همان اسنادی تنزل می‌کنند که انتخاب بیشترین اهمیت را دارد

خوشه‌بندی boxهای کاراکتر با مرکز عمودی

سیگنال قابل‌اعتماد هندسه است. کاراکتری را که کاربر کلیک کرده به‌عنوان seed بگیرید، مرکز عمودی box آن را محاسبه کنید، و در هر دو جهت به بیرون طی کنید درحالی‌که boxهای همسایه مراکز عمودی خود را داخل tolerance نگه می‌دارند. PDFium Component نیمی از ارتفاع box seed را به‌عنوان آن tolerance استفاده می‌کند، با یک کف ۰٫۵ واحد صفحه پس boxهای تباهیده، یک نقطه، یک فاصله نازک، یک glyph با یک box ارتفاع تقریباً-صفر، tolerance را به هیچ فرو نریزند و خط را پس از یک کاراکتر نبرند

function TPdfView.LineRangeAt(TxtPage: FPDF_TEXTPAGE; CharIndex: Integer;
  out StartIndex, Count: Integer): Boolean;
var
  Lo, Hi, Total: Integer;
  SeedBox, Box: TPdfRectangle;
  SeedYMid, BoxYMid, HalfH: Double;
begin
  Result := False;
  StartIndex := -1;
  Count := 0;
  Total := FPDFText_CountChars(TxtPage);
  if (CharIndex < 0) or (CharIndex >= Total) then
    Exit;

  if FPDFText_GetCharBox(TxtPage, CharIndex, SeedBox.Left, SeedBox.Right,
    SeedBox.Bottom, SeedBox.Top) = 0 then
    Exit;
  SeedYMid := (SeedBox.Top + SeedBox.Bottom) / 2;
  HalfH := Abs(SeedBox.Top - SeedBox.Bottom) / 2;
  if HalfH < 0.5 then          // floor for degenerate boxes
    HalfH := 0.5;

  Lo := CharIndex;
  Hi := CharIndex;
  while Lo > 0 do
  begin
    if FPDFText_GetCharBox(TxtPage, Lo - 1, Box.Left, Box.Right,
      Box.Bottom, Box.Top) = 0 then
      Break;
    BoxYMid := (Box.Top + Box.Bottom) / 2;
    if Abs(BoxYMid - SeedYMid) > HalfH then
      Break;
    Dec(Lo);
  end;
  while Hi < Total - 1 do
  begin
    if FPDFText_GetCharBox(TxtPage, Hi + 1, Box.Left, Box.Right,
      Box.Bottom, Box.Top) = 0 then
      Break;
    BoxYMid := (Box.Top + Box.Bottom) / 2;
    if Abs(BoxYMid - SeedYMid) > HalfH then
      Break;
    Inc(Hi);
  end;
  StartIndex := Lo;
  Count := Hi - Lo + 1;
  Result := True;
end;

سه جزئیات در آن حلقه جای خودشان را کسب می‌کنند. tolerance از seed مشتق می‌شود نه از یک ثابت، پس یک عنوان ۲۴pt یک باند پهن می‌گیرد و متن پانویس ۷pt یک باند باریک، و هیچ‌کدام از دیگری کاراکتر نمی‌دزدند. مقایسه از مراکز عمودی استفاده می‌کند نه از baselineها یا بالای box، که یک بالانویس، یک اجرای اندازه-متفاوت درون‌خطی، یا یک جمله فونت-مخلوط را روی همان خط با همسایگانش نگه می‌دارد. و یک FPDFText_GetCharBox ناموفق اسکن را خاتمه می‌دهد به‌جای رد شدن، چون یک کاراکتر بدون هندسه قابل‌بازیابی هیچ مدرکی به هر دو جهت به شما نمی‌دهد، و ادامه پس از آن اجازه می‌داد پیمایش بر قدرت یک کاراکتر جلوتر از یک مرز واقعی بپرد

چرا هر مسیر انتخاب باید یک کمک‌کننده را به اشتراک بگذارد؟

چون سه مسیر کد که هر کدام "خط" را پیاده‌سازی می‌کنند واگرا می‌شوند، و آرام واگرا می‌شوند. در PDFium Component، گسترش triple-click، Shift+Home، Shift+End، و متد عمومی SelectLineAt همگی مرزهای خود را از طریق همان فراخوانی LineRangeAt حل می‌کنند. triple-click آن را از لنگر انتخاب seed می‌کند؛ کلیدهای shift آن را از cursor انتخاب seed می‌کنند و فقط آن انتها را حرکت می‌دهند؛ SelectLineAt آن را از یک اندیس کاراکتر تأمین‌شده توسط فراخواننده seed می‌کند و نتیجه را به SelectTextRange می‌دهد، همان اعتبارسنج بازه‌ای که مسیر ماوس استفاده می‌کند. در عوض منطق را تکرار کنید و شکست یک crash نیست، یک drift آهسته است. کسی tolerance triple-click را برای رفع یک گزارش با leading سفت تنظیم می‌کند، و اکنون Shift+End یک کاراکتر کوتاه‌تر از جایی که triple-click روی همان پاراگراف متوقف می‌شود متوقف می‌شود. یک کاربر یک خط را با ماوس انتخاب می‌کند، آن را با صفحه‌کلید گسترش می‌دهد، و انتخاب را می‌بیند که کوچک می‌شود. چون SelectLineAt pipeline انتخاب عادی را تغذیه می‌کند، انتخاب برنامه‌ریزی‌شده نیز مستقل از اینکه ورودی ماوس فعال است باقی می‌ماند، و همچنان اعتبارسنجی بازه، repaint، و اعلان OnSelectionChange را رایگان می‌گیرد

// Select the visual line under a client-space point, then read it back
procedure TForm1.SelectLineUnderCursor(X, Y: Integer);
var
  CharIndex: Integer;
begin
  CharIndex := PdfView1.CharacterIndexAtPos(X, Y, 6.0, 6.0);
  if CharIndex < 0 then
    Exit;
  if PdfView1.SelectLineAt(PdfView1.CurrentPage, CharIndex) then
    Memo1.Lines.Add(PdfView1.SelectedText);
end;

به آرگومان‌های tolerance روی CharacterIndexAtPos توجه کنید. hit testing تلورانس خودش را دارد، بیان‌شده در واحدهای صفحه، و دغدغه‌ای جداست از tolerance خط. یک کلیک که در leading بین دو خط فرود می‌آید به هرکدام از کاراکترهایی که داخل آن box نزدیک‌ترند حل می‌شود؛ اسکن خط سپس از هرچیزی که آن کاراکتر بوده اجرا می‌شود. دادن یک hit tolerance بیش‌ازحد سخاوتمند به seed یکی از راه‌های آسان‌تر برای انتخاب خطی است که کاربر به آن اشاره نمی‌کرد

دو فضای اندیس: اندیس کاراکتر و اندیس متن

وقتی یک بازه دارید، در برابر وسوسه استفاده از آن به‌عنوان یک offset رشته مقاومت کنید. FPDFText_GetText متن صفحه را به‌عنوان یک بافر UTF-16 برمی‌گرداند، اما اندیس‌های آن همان فضای اندیس اندیس‌های کاراکتری که FPDFText_GetCharBox و FPDFText_CountChars استفاده می‌کنند نیست. کاراکترهای تولیدشده‌ای که قبلاً بحث شد در بافر متن می‌نشینند درحالی‌که slotهای کاراکتری بدون هندسه قابل‌استفاده اشغال می‌کنند، و دو شماره‌گذاری در سراسر صفحه از هم دور می‌شوند. پل‌ها FPDFText_GetTextIndexFromCharIndex و FPDFText_GetCharIndexFromTextIndex هستند، که توسط PDFium Component به‌عنوان CharacterIndexToTextIndex و TextIndexToCharacterIndex پیچیده شده‌اند

var
  TextStart, TextEnd: Integer;
begin
  // char-index range from LineRangeAt -> offsets into the page text buffer
  TextStart := Pdf.CharacterIndexToTextIndex(StartIndex);
  TextEnd   := Pdf.CharacterIndexToTextIndex(StartIndex + Count - 1);
  if (TextStart >= 0) and (TextEnd >= TextStart) then
    Caption := Pdf.Text(TextStart, TextEnd - TextStart + 1);
end;

جهتی که سخت‌ترین گاز را می‌گیرد جهت معکوس است. یک جستجو که روی رشته استخراج‌شده پیاده‌سازی شده اندیس‌های متن به شما می‌دهد، و دادن مستقیم آن‌ها به یک API box یا انتخاب خاموش کاراکترهای اشتباه را آدرس‌دهی می‌کند، با خطایی که هرچه پایین‌تر در صفحه بروید بزرگ‌تر می‌شود. پیش از آنکه هرچیز هندسی به عدد دست بزند با TextIndexToCharacterIndex تبدیل کنید. جفت‌های surrogate یک مسئله offset دوم و مستقل روی این اضافه می‌کنند، که در مقاله emoji، CJK و جفت‌های surrogate پوشش داده شده

جایی که این heuristic خم می‌شود

با خودتان درباره محدودیت‌ها صادق باشید، چون آن‌ها واقعی و قابل‌دسترس‌اند. متن چرخانده‌شده واضح‌ترین حالت است: یک box کاراکتر یک مستطیل هم‌محور در فضای صفحه است، پس برای متن چرخانده‌شده ۹۰ درجه، boxهای یک خط بصری مراکز عمودی پخش‌شده در سراسر صفحه دارند، و اسکن تقریباً بلافاصله متوقف می‌شود. چیزی که می‌گیرید یک انتخاب کوتاه است نه یک اشتباه، که حالت شکست بهتری است، اما همچنان یک شکست است. حالت‌های نوشتار عمودی به همان دلیل به همان روش رفتار می‌کنند. چیدمان‌های دوستونی وقتی ستون‌ها از نظر عمودی نسبت به هم offset باشند کار می‌کنند و وقتی نیستند می‌شکنند. اگر هر دو ستون یک grid baseline مشترک داشته باشند، کاراکترهای ستون راست داخل tolerance خط ستون چپ می‌نشینند، و اسکن مستقیم در سراسر gutter اجرا می‌شود، چون در هندسه محض هیچ‌چیزی برای توقف در آنجا وجود ندارد. تشخیص آن به یک آزمون فاصله افقی بالای خوشه‌بندی عمودی نیاز دارد، و انتخاب آستانه فاصله قضاوت خودش درباره اینکه درباره کدام اسناد راضی‌اید اشتباه باشید است. اندازه‌های فونت مخلوط حالتی است که tolerance نسبی-به-seed به‌خوبی مدیریت می‌کند: یک بازه code درون‌خطی ۸pt داخل متن بدنه ۱۱pt مرکز خود را داخل باند نگه می‌دارد، و یک عنوان ۲۴pt روی baseline بعدی خط بدنه را به داخل خودش نمی‌کشد

معنای انتخاب خط که اینجا شرح داده شد در PDFium Component برای Delphi و C++Builder عرضه می‌شود، در کنار APIهای hit testing، بازه انتخاب و اندیس متن استفاده‌شده در مثال‌ها؛ صفحه محصول مرجع کامل مدل text page و انتخاب را حمل می‌کند