مقاله فنی

ایموجی و نویسه‌های CJK در PDFium، WideChar را در Delphi می‌شکنند

ایموجی یا یک نام خانوادگی ثبت‌شده‌ی ژاپنی را از یک PDF به‌عنوان متن بیرون بکشید، و خروجی جایی که نویسه باید باشد یک جعبه، یک علامت سؤال، یا اصلاً هیچ‌چیزی نشان می‌دهد. ویژگی Character[] در PDFium Component معمولاً دلیل آن است: این هر گلیف را از طریق FPDFText_GetUnicode می‌خواند، که یک نقطه کد یونیکد کامل را به‌عنوان یک مقدار بدون‌علامت ۳۲بیتی برمی‌گرداند، سپس آن را به Delphi به‌عنوان یک WideChar ۱۶بیتی تکی در معرض دید می‌گذارد. هر نقطه کد فراتر از U+FFFF نمی‌تواند آن سفر را در یک قطعه انجام دهد، و خرابی هرگز درحالی‌که به صفحه‌ی رندرشده نگاه می‌کنید نمایان نمی‌شود، چون رندر و استخراج متن از مسیرهای کد جداگانه‌ای در PDFium عبور می‌کنند — یک سند می‌تواند ایموجی‌اش را کاملاً نمایش دهد و همچنان به شما زباله بدهد همان لحظه‌ای که Character[] را در یک حلقه بخوانید و از آن یک رشته بسازید

صفحه‌ی چندزبانه‌ی پایه و چرا WideChar در U+FFFF متوقف می‌شود

WideChar در Delphi یک نوع ۱۶بیتی است که فقط می‌تواند یک واحد کد UTF-16 نگه دارد. صفحه‌ی چندزبانه‌ی پایه‌ی یونیکد (Basic Multilingual Plane)، بازه‌ی U+0000 تا U+FFFF، دقیقاً درون آن جا می‌گیرد، به همین دلیل لاتین، سیریلیک، یونانی، و بلوک متداول CJK Unified Ideographs همگی بدون حادثه از طریق یک WideChar تکی رفت‌وبرگشت می‌کنند. دو خانواده از نویسه‌ها به‌طور معمول در اسناد واقعی خارج از آن می‌افتند: ایموجی، بسیاری از آن‌ها در بلوک Emoticons که از U+1F600 شروع می‌شود، و ideographهای نادر CJK از CJK Unified Ideographs Extension B، بازه‌ی U+20000 تا U+2A6DF رزروشده برای نویسه‌های کمتر رایج چینی، ژاپنی، و کره‌ای از جمله بسیاری از نام‌های شخصی و مکانی. UTF-16 هر چیزی بالای U+FFFF را با یک جفت surrogate مدیریت می‌کند — دو واحد کد ۱۶بیتی، یک surrogate بالا در بازه‌ی $D800 تا $DBFF به‌دنبال آن یک surrogate پایین در $DC00 تا $DFFF، که با هم یک نقطه کد را کدگذاری می‌کنند — و محاسبات پشت آن جفت‌کردن به‌اندازه‌ی کافی ثابت است که مستقیم در Pascal نمایش داده شود

function ToSurrogatePair(CodePoint: LongWord; out Hi, Lo: WideChar): Boolean;
var
  V: LongWord;
begin
  Result := CodePoint > $FFFF;
  if Result then
  begin
    V := CodePoint - $10000;
    Hi := WideChar($D800 + (V shr 10));
    Lo := WideChar($DC00 + (V and $3FF));
  end;
end;

U+1F600، ایموجی چهره‌ی خندان، را از طریق آن تابع تغذیه کنید و نتیجه یک surrogate بالای $D83D و یک surrogate پایین $DE00 است، دو مقدار ۱۶بیتی، نه یک تا. هیچ‌کدام از نیمه‌ها به‌تنهایی هیچ معنایی ندارند؛ یک $D83D تنها که در یک رشته بدون $DE00 پشتش نشسته یک surrogate آویزان است، و اغلب کد مدیریت-متن که با یکی روبه‌رو می‌شود یا آن را حذف می‌کند، یک گلیف جایگزین می‌گذارد، یا یک خطا raise می‌کند

چرا FPDFText_GetUnicode مقداری برمی‌گرداند که Character[] نمی‌تواند نگه دارد؟

FPDFText_GetUnicode یک LongWord، یک مقدار ۳۲بیتی کامل، برمی‌گرداند، چون encoding متن PDF از پیش مقدار اسکالر کامل یونیکد را برای هر گلیف حمل می‌کند. CMap از نوع ToUnicode یک PDF کدهای نویسه را به متن یونیکد نگاشت می‌کند، و وقتی یک گلیف چیزی را نمایش می‌دهد که غیررسمی astral-plane نامیده می‌شود — هر چیزی فراتر از صفحه‌ی چندزبانه‌ی پایه — آن نگاشت یک نقطه کد کامل است، نه یک قطعه‌ی ۱۶بیتی. PDFium آن را داخلاً دوباره به یک مقدار اسکالر رمزگشایی می‌کند و آن را در سراسر مرز DLL از طریق FPDFText_GetUnicode برمی‌گرداند، و آن مرز دقیقاً همان‌جایی است که یک مقدار ۳۲بیتی باید به چیزی تبدیل شود که یک ویژگی Delphi می‌تواند به کد شما پس بدهد

پیاده‌سازی آشکار WideChar(FPDFText_GetUnicode(TextPage, Index)) است، و همچنین اشتباه است. یک cast سخت از یک مقدار ۳۲بیتی به یک نوع ۱۶بیتی فقط ۱۶ بیت پایین را نگه می‌دارد و بقیه را بی‌سروصدا دور می‌ریزد، بدون هیچ استثنایی و بدون هیچ بررسی بازه‌ای. برای U+1F600 این یعنی نگه‌داشتن $F600 و از‌دست‌دادن این واقعیت که مقدار واقعی هرگز بالاتر از U+FFFF بود، که یک واحد کد تولید می‌کند که حتی یک surrogate آویزان معتبر هم نیست، فقط یک نویسه‌ی بی‌ربط از صفحه‌ی چندزبانه‌ی پایه که اتفاقاً همان بیت‌های پایین را به اشتراک می‌گذارد. چند هزار تا از آن‌ها را به یک رشته الحاق کنید و کد پایین‌دستی هیچ راهی برای تفکیک یک نویسه‌ی خراب از یک نویسه‌ی قانونی باقی ندارد

Character[] و Charcode[] حالا برای نقاط کد astral-plane چه چیزی برمی‌گردانند

ویژگی‌های Character[] و Charcode[] در PDFium Component، هر زمان که نقطه کد زیرین از U+FFFF فراتر رود، به‌جای بریدن بی‌سروصدای آن، U+FFFD، نویسه‌ی جایگزین یونیکد، برمی‌گردانند. آن نگهبان مستقیم درون getter ویژگی پشت Character[] می‌نشیند

function TPdf.GetCharacter(Index: Integer): WideChar;
var
  Code: LongWord;
begin
  LoadTextPage;
  Code := FPDFText_GetUnicode(FTextPage, Index);
  if Code > $FFFF then
    Result := #$FFFD          // astral-plane code point: cannot fit in one WideChar
  else
    Result := WideChar(Code);
end;

برگرداندن U+FFFD به‌جای یک قطعه‌ی بریده‌شده یک رفع اشکال عمدی و محدود است نه یک بازطراحی. Character[] و Charcode[] هم روی TPdf و هم روی TPdfView از نوع WideChar هستند، و گسترش آن نوع بازگشتی برای حمل یک نقطه کد کامل، هر فراخواننده‌ی موجودی را که انتظار دارد یک گلیف به‌ازای هر اندیس به‌معنای یک مقدار ۱۶بیتی باشد می‌شکست. U+FFFD نگهدارنده‌جای اختصاص‌داده‌شده‌ی خودِ استاندارد یونیکد دقیقاً برای همین وضعیت است، پس فراخواننده‌ای که آن را بررسی می‌کند به‌جای داده‌ی بی‌سروصدا اشتباه، یک سیگنال تعریف‌شده و مستندشده می‌گیرد. یک مورد مرزی ارزش دانستن دارد: U+FFFD همچنین یک نویسه‌ی قانونی به‌خودی‌خود است، پس روی یک سند نادری که از پیش یک گلیف واقعی نویسه-جایگزین حمل می‌کند، آن اندیس فقط با مقدار از یک نویسه‌ی astral بریده‌شده تفکیک‌ناپذیر است

چطور ایموجی و متن CJK Extension B را در Delphi به‌درستی استخراج کنید؟

هر زمان که محتوای متنی واقعی اهمیت دارد، Text را فراخوانی کنید به‌جای پیمایش Character[]، چون Text از طریق FPDFText_GetText می‌خواند و یک WString کامل با جفت‌های surrogate درست برای هر نویسه‌ی astral-plane در بازه برمی‌گرداند، نه یک مقدار عرض-ثابت به‌ازای هر اندیس. Pdf.Text(0, MaxInt)، یا میان‌بر Pdf.Text، یک صفحه‌ی کامل را در یک فراخوانی به‌درستی استخراج می‌کند، و Pdf.Text(StartIndex, Count) یک بازه‌ی کوچک‌تر را به‌همان‌شیوه می‌کشد. Character[] همچنان جایگاهش را وقتی فقط به داده‌ی موقعیت، فونت، یا پرچم روی یک اندیس نیاز دارید و هرگز خودِ نقطه کد را لمس نمی‌کنید به‌دست می‌آورد — CharacterOrigin[]، FontSize[]، و CharacterMapError[] اهمیتی نمی‌دهند که آیا گلیف زیرین astral بوده یا نه

function ExtractLineSafely(Pdf: TPdf): WString;
var
  I: Integer;
begin
  Result := '';
  for I := 0 to Pdf.CharacterCount - 1 do
    if not (Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I]) then
      Result := Result + Pdf.Text(I, 1);   // full code point, never a truncated WideChar
end;

بررسی رد-تولیدشده‌و-نگاشت‌نشده در آن حلقه همان الگویی است که برای استخراج متن ساده در استخراج متن از اسناد PDF با PDFium Component استفاده می‌شود؛ تنها تغییر خط آخر است، که یک الحاق مستقیم Character[I] را با یک فراخوانی یک-اندیسی به Text معامله می‌کند پس نویسه‌های astral به‌عنوان جفت‌های surrogate کامل به‌جای نگهدارنده‌جای جایگزین می‌رسند

این واقعاً کجا نیش می‌زند: export چت، نام‌های شخصی، و فونت‌های جاسازی‌شده‌ی CJK

ایموجی هرجایی که یک PDF ارتباط غیررسمی را ثبت می‌کند نمایان می‌شود: لاگ‌های چت export‌شده، dumpهای بررسی app-store، رونوشت‌های سیستم تیکتینگ ذخیره‌شده به PDF برای یک آرشیو رعایت. CJK Extension B در جایی محدودتر اما با ریسک بالاتر نمایان می‌شود، نام‌های شخصی و مکانی، چون ثبت‌های خانوادگی ژاپنی، سوابق ثبت خانواری چینی، و اسناد هویتی تایوانی منابع کلاسیکی از نویسه‌هایی هستند که هرگز به بلوک متداول CJK راه نیافتند. یک خط لوله‌ی حقوق‌ودستمزد یا تأیید-هویت که نام‌ها را از کاغذبازی دولتی اسکن‌شده استخراج می‌کند دقیقاً همان نوع حجم‌کاری است که یک نویسه‌ی بی‌سروصدا خراب‌شده به یک تطابق شکست‌خورده تبدیل می‌شود نه یک نقص ظاهری

ideographهای نادر CJK هم تمایل دارند با مسائل فونت سفر کنند، نه فقط مسائل encoding، چون یک فونت باید یک گلیف برای یک نقطه کد در بازه‌ی U+20000 حمل کند پیش از اینکه اصلاً چیزی بتواند رندر شود، و کمی فونت‌های سیستمی نصب‌شده این کار را می‌کنند. هرکسی که از پیش FontIsEmbedded[] را به‌ازای هر نویسه پیمایش می‌کند همان‌طور که خواندن ویژگی‌های فونت PDF با PDFium Component توصیف می‌کند، باید همان اندیس را برای هر دو مسئله با هم بررسی کند: اندیسی که U+FFFD را از Character[] برمی‌گرداند و یک فونت غیر-جاسازی‌شده گزارش می‌دهد، سندی است که نه استخراج و نه چاپ آن نویسه را به‌درستی انجام نمی‌دهد، و رفع اشکال بالادست تعلق دارد به اینکه PDF چطور تولید شده، نه به کد استخراج شما

ویژگی‌های Character[]، Charcode[]، و Text که در اینجا توصیف شد بخشی از کامپوننت استاندارد PDFium برای Delphi و C++Builder هستند