مقاله فنی

باگ Flatten چک‌باکس PDF: مقدار فیلد در برابر ویجت در Delphi

چک‌باکس‌ها و دکمه‌های رادیویی به‌صورت تیک‌نخورده مسطح می‌شوند چون وضعیت ظاهر /AS هرگز با مقدار فیلد /V همگام نشده بود. PDFium Component، مؤلفه VCL و LCL بر پایه PDFium برای Delphi، C++Builder، و Lazarus، اکنون آن مقدار را با FPDFAnnot_GetFormFieldValue می‌خواند، که دیکشنری فیلد والد را به‌جای حاشیه‌نویسی ویجت حل می‌کند

گزارش باگی که به اینجا رسید نوعی است که ابتدا به آن بی‌اعتماد می‌شوید. یک مشتری یک فرم رضایت امضاشده را مسطح می‌کند، نتیجه را باز می‌کند، و هر چک‌باکس خالی است. فایل منبع را در Acrobat باز کنید و جعبه‌ها به‌طور مرئی تیک‌خورده‌اند. فایل منبع را از راه همان مؤلفه بازخوانی کنید و مقادیر فیلد درست‌اند. فقط خروجی مسطح‌شده آن‌ها را از‌دست‌می‌دهد، و فقط برای چک‌باکس‌ها و دکمه‌های رادیویی: فیلدهای متنی روی همان صفحه درست درمی‌آیند

چرا چک‌باکس‌ها پس از مسطح‌شدن تیک‌نخورده‌اند؟

چون مسطح‌کردن هرگز به /V نگاه نمی‌کند. FPDFPage_Flatten جریان ظاهر ویجت را در محتوای صفحه می‌پزد، و ظاهری که انتخاب می‌کند آنی است که با نام /AS نام‌گذاری شده. اگر /AS همچنان بگوید /Off در حالی که مقدار فیلد بگوید جعبه روشن است، مسطح‌کردن با وفاداری ظاهر خاموش را می‌پزد. مقدار هرگز گم نشده بود؛ هرگز مشورت نشده بود

ISO 32000-1 §12.5.5 دیکشنری ظاهر /AP را با سه مدخل ممکن تعریف می‌کند، /N، /R، و /D. برای یک چک‌باکس یا دکمه رادیویی، مدخل /N یک جریان نیست بلکه یک زیردیکشنری است که کلیدهایش نام‌های وضعیت ظاهر‌اند، و §12.5.2 /AS را به‌عنوان گزینگر الزامی وقتی /N یک زیردیکشنری است می‌کند. پس یک چک‌باکس دو ظاهر از‌پیش‌ساخته و یک اشاره‌گر حمل می‌کند. اشاره‌گر را اشتباه بگیرید و رندرینگ به شکلی اشتباه است که هیچ مقدار /V درستی نمی‌تواند تعمیرش کند. این همچنین چرا حالت شکست با فیلدهای متنی متفاوت است، که هیچ ظاهر از‌پیش‌ساخته‌ای برای انتخاب ندارند: یک /N فیلد متنی یک جریان منفرد است که باید از صفر پس از تغییر مقدار بازتولید شود، پس GenerateFormAppearances دو مورد را از راه مسیرهای کد کاملاً جداگانه مدیریت می‌کند و فقط مسیر دکمه شکسته بود

مقدار چک‌باکس واقعاً کجا زندگی می‌کند؟

روی دیکشنری فیلد، نه روی ویجت. ISO 32000-1 §12.7.5.2 چک‌باکس‌ها و دکمه‌های رادیویی را به‌عنوان فیلدهای دکمه‌ای توصیف می‌کند که /V آن‌ها یک آبجکت نام است که وضعیت ظاهر جاری را نام می‌برد، و §12.7.3.1 /V را در میان مدخل‌های مشترک همه دیکشنری‌های فیلد قرار می‌دهد. حاشیه‌نویسی ویجت تعریف‌شده در §12.5.6.19 /AS و /AP را مشارکت می‌دهد. هیچ‌چیز در مشخصات یک ویجت را ملزم به حمل /V نمی‌کند

// Wrong: reads the widget annotation dictionary directly
buflen := FPDFAnnot_GetStringValue(Annot, 'V', nil, 0);
// For most real forms buflen comes back as 2 (an empty UTF-16 string),
// so /AS is never written and the box flattens as Off

{ What the two objects look like when the field has several widgets:

  12 0 obj                          % field dictionary (the parent)
  << /FT /Btn  /T (Consent)  /V /On
     /Kids [ 13 0 R 14 0 R ] >>
  endobj

  13 0 obj                          % widget annotation (a kid)
  << /Type /Annot  /Subtype /Widget  /Parent 12 0 R
     /AS /Off
     /AP << /N << /On 20 0 R  /Off 21 0 R >> >> >>
  endobj }

FPDFAnnot_GetStringValue خراب نیست. قراردادش دقیقاً همان است که نامش می‌گوید: یک مدخل رشته‌ای از دیکشنری حاشیه‌نویسی که به آن سپرده‌اید بیاور. پرسیدن از آن درباره /V روی آبجکت ۱۳ چیزی برنمی‌گرداند چون آبجکت ۱۳ واقعاً هیچ /V ندارد. نقص در فراخوان‌کننده بود، که یک مدل آبجکت تخت را فرض کرده بود که ISO 32000-1 هرگز وعده نداده بود

چه زمانی فیلد و ویجت یک دیکشنری را به‌اشتراک می‌گذارند؟

هرجا یک فیلد دقیقاً یک ویجت داشته باشد. §12.5.6.19 اجازه می‌دهد دیکشنری فیلد و حاشیه‌نویسی ویجت منفردش در یک آبجکت ادغام شوند، و بیشتر ابزارهای نویسندگی این میان‌بر را می‌گیرند. در یک آبجکت ادغام‌شده /FT، /T، /V، /AS، و /AP همه کنار هم می‌نشینند، پس یک خواندن سطح-ویجت از /V موفق می‌شود و کل باگ نامرئی می‌ماند

لحظه‌ای که یک فیلد مالک دو یا چند ویجت شود ادغام غیرممکن است، و §12.7.3.1 ویجت‌ها را ملزم می‌کند /Kids یک دیکشنری فیلد جداگانه شوند. هر گروه رادیویی به‌طور ساختاری در این شکل است. همین‌طور چک‌باکس‌های رضایت تکرارشده در یک سربرگ و پاورقی، و هر فیلدی که یک ابزار نویسندگی به صفحه دوم کپی کرده. این کل توضیح این است که چرا نقص یک مجموعه رگرسیون را جان سالم به‌در برد: مجموعه آزمون پر از فرم‌های تک‌ویجتی بود و فایل‌های مشتری نبودند. اگر خودتان ویجت‌ها را بپیمایید به‌جای اتکا به مؤلفه، همان عدم‌تقارن در ترتیب شمارش ظاهر می‌شود، و یادداشت‌های ناوبری فیلد فرم PDF با PDFium Component چگونگی ارتباط یک پیمایش حاشیه‌نویسی سطح-صفحه با درخت فیلد سطح-سند را پوشش می‌دهد

خواندن مقدار به شکلی که PDFium در نظر دارد

FPDFAnnot_GetFormFieldValue API درست است، و مدتی در مؤلفه سیم‌کشی شده بود بدون آنکه مسیر چک‌باکس از آن استفاده کند. این هم دسته فرم و هم حاشیه‌نویسی را می‌گیرد، که سیگنالی است که اهمیت دارد: با محیط پرکردن فرم در دسترس، PDFium حاشیه‌نویسی را به کنترل فرمش حل می‌کند و مقدار را از آبجکت فیلد می‌خواند، پس برای چیدمان‌های ادغام‌شده و تفکیک‌شده به‌یکسان پاسخ درست برمی‌گرداند

FPDF_FORMFIELD_CHECKBOX, FPDF_FORMFIELD_RADIOBUTTON:
  begin
    // /AP is prebuilt per state; only /AS has to be synchronised with /V.
    // FPDFAnnot_GetFormFieldValue resolves the parent field dictionary,
    // which is where ISO 32000-1 12.7.5.2 keeps the value.
    buflen := FPDFAnnot_GetFormFieldValue(FFormHandle, Annot, nil, 0);
    if buflen >= 4 then
    begin
      SetLength(OrigVal, buflen div 2 - 1);
      FPDFAnnot_GetFormFieldValue(FFormHandle, Annot, PWideChar(OrigVal), buflen);
      FPDFAnnot_SetStringValue(Annot, 'AS', Pointer(OrigVal));
    end;
  end;

دو جزئیات در آن قطعه به‌آسانی اشتباه گرفته می‌شوند. طول برگشتی یک شمارش بایتی برای متن UTF-16 شامل ترمیناتور است، پس شمارش کاراکتری buflen div 2 - 1 است و مقدار ۲ یعنی یک رشته خالی. نگهبان buflen >= 4 بنابراین یعنی دست‌کم یک کاراکتر واقعی، که چیزی است که یک فیلد بدون هیچ /V را از داشتن /AS اش بازنویسی‌شده با یک نام خالی بازمی‌دارد

/AS و /AP /N واقعاً روی چه چیزی توافق دارند

روی یک نام توافق دارند، و نام توسط هرکسی که فایل را تولید کرده انتخاب می‌شود. §12.7.5.2 الزام می‌کند وضعیت خاموش /Off نامیده شود، و وضعیت روشن را کاملاً به تولیدکننده واگذار می‌کند. /Yes یک قرارداد است، نه یک قاعده. Acrobat /Yes می‌نویسد، اما بسیاری مولدها /On، /1، /Choice1، یا یک کلمه محلی‌سازی‌شده می‌نویسند، و یک گروه رادیویی معمولاً به هر فرزند یک نام وضعیت-روشن متمایز می‌دهد پس گروه می‌تواند بیان کند کدام دکمه انتخاب شده. این دقیقاً چرا کپی‌کردن /V کلمه‌به‌کلمه در /AS عملیات درست است نه یک ترفند: برای یک کنترل تیک‌خورده، PDFium نام وضعیت-روشنی را گزارش می‌کند که خود فایل تعریف می‌کند، و برای یکی تیک‌نخورده، Off گزارش می‌کند، پس مقداری که در /AS می‌نویسید تضمین‌شده کلیدی است که در آن زیردیکشنری /AP /N ویجت وجود دارد. سخت‌کدکردن /Yes روی خروجی Acrobat کار می‌کرد و در همه‌جای دیگر در سکوت می‌شکست

ترتیب عملیات‌ها، و کجا هنوز نیاز به مراقبت دارد

دنباله ثابت و بی‌گذشت است: پرکردن فرم را فعال کنید، مقادیر را تخصیص دهید، ظاهرها را بازتولید کنید، مسطح کنید، سپس ذخیره کنید. گام بازتولید را رد کنید و FPDFPage_Flatten جریان‌های ظاهر خالی یا کهنه را می‌یابد و بدون شکایت آن‌ها را می‌پزد، که از‌دست‌رفتن داده خاموش است نه یک بازگشت خطا

Pdf.FileName := FormPath;
Pdf.FormFill := True;          // required: FormHandle must exist
Pdf.Active := True;

Pdf.FormField[0] := 'On';      // writes /V only

Pdf.GenerateFormAppearances;   // syncs /AS for buttons, rebuilds /AP for text
if Pdf.FlattenAllPages(FLAT_PRINT) then
  Pdf.SaveAs('consent-flat.pdf');

دو محدودیت صادقانه باقی می‌مانند. اول، همگام‌سازی مقدار فیلد را در /AS هر ویجت آن فیلد می‌نویسد، که برای چک‌باکس‌ها درست است اما برای گروه‌های رادیویی که هر فرزندش نام وضعیت-روشن خودش را تعریف می‌کند تقریبی است؛ فرزندی که /AP /N اش هیچ مدخل مطابق با /AS نوشته‌شده را ندارد هیچ ظاهری برای انتخاب زیر §12.5.5 ندارد، پس یک دکمه انتخاب‌نشده می‌تواند به هیچ به‌جای یک دایره خالی مسطح شود. بازبینی یک گروه رادیویی با FPDFAnnot_GetFormControlIndex پیش از مسطح‌کردن ارزش چند خط دارد. دوم، هیچ‌کدام از این‌ها به XFA اعمال نمی‌شود، جایی که مقدار در یک بسته داده XML زندگی می‌کند نه در دیکشنری‌های AcroForm، تفکیکی که در یادداشت‌های ویرایش‌های فیلد XFA که ماندگار نمی‌شوند پوشش داده شده. درس عمومی ارزش نگه‌داشتن فراتر از این یک رفع مشکل را دارد: هرجا یک API دسته فرم را علاوه بر حاشیه‌نویسی بگیرد، دارد می‌گوید سلسله‌مراتب فیلد را برایتان حل می‌کند، و هرجا فقط حاشیه‌نویسی بگیرد دقیقاً همان آبجکتی را می‌خواند که سپردید. آن تمایز همچنین بر تبادل داده حاکم است، چون صادرات و واردات داده فرم XFDF در نام‌های فیلد کاملاً‌واجد‌شرایط کار می‌کند، هرگز در موقعیت‌های ویجت

مسطح‌کردن فرم یکی از آن ویژگی‌هایی است که شبیه یک فراخوانی API منفرد به‌نظر می‌رسد و می‌گردد یک قرارداد میان سه دیکشنری باشد. اگر ترجیح می‌دهید علیه مؤلفه‌ای کار کنید که از قبل آن قرارداد را رمزگذاری کرده، PDFium Component برای Delphi و C++Builder بازتولید ظاهر، مسطح‌کردن، و دسترسی فیلد فرم توصیف‌شده در اینجا را به‌عنوان ویژگی‌ها و متدهای معمولی ارائه می‌دهد