مقاله فنی

شاخص ویجت در برابر شاخص حاشیه‌نویسی در فرم‌های PDFium Delphi

در PDFium Component، مؤلفه VCL/LCL بر پایه PDFium برای Delphi، C++Builder و Lazarus، شاخص فیلد فرم یک شاخص حاشیه‌نویسی نیست. یک صفحه حاشیه‌نویسی‌های Link، Text، و Ink در کنار ویجت‌هایش حمل می‌کند، پس شمارش فیلد باید روی FPDFAnnot_GetSubtype فیلتر کند و یک شاخص منطقی صفر-پایه نمایش دهد، که فقط در فراخوانی بومی به یک موقعیت حاشیه‌نویسی واقعی نگاشت می‌شود

باگی که این را آشکار می‌کند وقتی یک‌بار دیدیدش غیرقابل‌اشتباه است. یک آزماینده Tab را در یک فرم فاکتور پرشده فشار می‌دهد و مکان‌نما ناپدید می‌شود، چون فوکوس به یک هایپرلینک در پاورقی رفته. یا بدتر، هیچ اتفاقی نمی‌افتد: کد شما فیلد ۳ را فوکوس‌شده ثبت می‌کند، پنل رابط کاربری به‌روز می‌شود، و FORM_SetFocusedAnnot در سکوت تمام این مدت false برمی‌گرداند. هر دو علامت از همان اشتباه طراحی می‌آیند، و یکی از آن‌ها یک ریشه دوم پنهان زیرش دارد

دو فضای شاخصی که PDFium به شما می‌دهد

PDFium دو طرح شماره‌گذاری روی همان صفحه نمایش می‌دهد، و آن‌ها فقط روی اسنادی که اتفاقاً چیزی جز ویجت فرم ندارند منطبق می‌شوند. اولی شاخص حاشیه‌نویسی است: یک موقعیت در آرایه /Annots صفحه، که FPDFPage_GetAnnotCount می‌شمارد و FPDFPage_GetAnnot می‌گیرد (ISO 32000-1 §12.5.2). دومی شاخص فیلد منطقی است که یک API سطح‌اپلیکیشن باید ارائه دهد، که از صفر روی فیلدهای تعاملی که یک کاربر واقعاً می‌تواند به آن‌ها برسد اجرا می‌شود. ISO 32000-1 §12.5.6.19 حاشیه‌نویسی‌های ویجت را به‌عنوان نمایش بصری فیلدهای فرم تعاملی تعریف می‌کند، و §12.7 خود فرم را تعریف می‌کند. هرچیز دیگر روی صفحه یک زیرنوع متفاوت با معنای متفاوت است: یک حاشیه‌نویسی Link یک مقصد دارد، یک حاشیه‌نویسی Ink یک فهرست ضربه‌قلم دارد، یک حاشیه‌نویسی Text یک یادداشت چسبان است. هیچ‌کدام به یک شمارش فیلد تعلق ندارند، و هیچ‌کدام نمی‌توانند فوکوس فرم را بپذیرند. اما در آرایه /Annots آن‌ها با هر ترتیبی که اپلیکیشن تولیدکننده نوشته با ویجت‌ها درهم می‌نشینند، که غالباً همان ترتیبی نیست که هرچیز دیگری درباره سند پیشنهاد می‌کند

چرا Tab روی یک هایپرلینک فرود می‌آید به‌جای فیلد بعدی؟

چون شمارش فیلد واقعاً یک شمارش حاشیه‌نویسی بود. پیاده‌سازی اصلی مستقیماً FPDFPage_GetAnnotCount را از FormFieldCount برمی‌گرداند، در حالی که دسترسی‌گر اطلاعات فیلد، کمک‌کننده ترتیب Tab، و کمک‌کننده فوکوس همه همان عدد صحیح را به‌عنوان یک موقعیت ویجت رفتار می‌کردند. روی یک صفحه AcroForm تمیز با شش ویجت و هیچ‌چیز دیگر، شش برابر شش است و هر آزمون قبول می‌شود. یک هایپرلینک در پاورقی و یک کامنت بازبینی در حاشیه اضافه کنید، و شمارش هشت فیلد گزارش می‌کند، شاخص‌های ۶ و ۷ به آبجکت‌های غیرفرم حل می‌شوند، و Tab مستقیم داخل آن‌ها می‌رود

رفع مشکل در سمت شمارش این است که زیرنوع‌ها را بشماریم نه حاشیه‌نویسی‌ها. هر حاشیه‌نویسی را باز کنید، زیرنوعش را بپرسید، ویجت‌ها را نگه دارید، و دسته را در یک بلوک finally ببندید، چون FPDFPage_GetAnnot یک دسته مالکیت‌دار برمی‌گرداند که باید از راه FPDFPage_CloseAnnot برگردد

function WidgetCountForPage(Page: FPDF_PAGE): Integer;
var
  Count, I: Integer;
  Annot: FPDF_ANNOTATION;
begin
  Result := 0;
  if Page = nil then
    Exit;
  Count := FPDFPage_GetAnnotCount(Page);   // every annotation, not just fields
  for I := 0 to Count - 1 do
  begin
    Annot := FPDFPage_GetAnnot(Page, I);
    if Annot = nil then
      Continue;
    try
      if FPDFAnnot_GetSubtype(Annot) = FPDF_ANNOT_WIDGET then
        Inc(Result);
    finally
      FPDFPage_CloseAnnot(Annot);
    end;
  end;
end;

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

نگاشت شاخص منطقی بازگشتی در مرز بومی

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

function AnnotationIndexForField(Page: FPDF_PAGE;
  FieldIndex: Integer): Integer;
var
  Count, I, Current: Integer;
  Annot: FPDF_ANNOTATION;
begin
  Result := -1;
  if (Page = nil) or (FieldIndex < 0) then
    Exit;
  Count := FPDFPage_GetAnnotCount(Page);
  Current := 0;
  for I := 0 to Count - 1 do
  begin
    Annot := FPDFPage_GetAnnot(Page, I);
    if Annot = nil then
      Continue;
    try
      if FPDFAnnot_GetSubtype(Annot) = FPDF_ANNOT_WIDGET then
      begin
        if Current = FieldIndex then
          Exit(I);        // real /Annots position: native calls only
        Inc(Current);
      end;
    finally
      FPDFPage_CloseAnnot(Annot);
    end;
  end;
end;

دو ویژگی این کمک‌کننده ارزش گفتن صریح دارند. این یک اسکن خطی است، پس یک حلقه ساده‌لوح روی هر فیلد هزینه‌ای درجه‌دوم از بازکردن حاشیه‌نویسی روی صفحه‌ای با صدها ویجت می‌پردازد؛ اگر کل صفحه را می‌شمارید، حاشیه‌نویسی‌ها را یک‌بار بپیمایید و دسته‌های ویجت را همان‌طور که پیش می‌روید جمع کنید به‌جای فراخوانی نگاشت‌گر به‌ازای هر فیلد. و این -1 برمی‌گرداند به‌جای پرتاب خطا، که به فراخوان‌کننده اجازه می‌دهد تصمیم بگیرد یک شاخص کهنه یک خطای برنامه‌نویسی است که ارزش یک استثنا دارد یا یک مسابقه است که ارزش نادیده‌گرفتن دارد، مثلاً پس از ویرایشی که یک حاشیه‌نویسی را حذف کرده که یک فهرست رابط کاربری کش‌شده هنوز به آن ارجاع می‌دهد

چرا FORM_SetFocusedAnnot روی یک صفحه سربدون شکست می‌خورد؟

چون PDFium از فوکوس‌کردن یک ویجت که نمای صفحه‌اش هرگز معتبر علامت نخورده امتناع می‌کند. FORM_SetFocusedAnnot حاشیه‌نویسی را به یک نمای صفحه درون محیط پرکردن فرم حل می‌کند، و اگر آن نمای صفحه وجود نداشته باشد بدون هیچ تشخیصی false برمی‌گرداند. تصحیح فقط نگاشت شاخص بنابراین Tab را از فرودآمدن روی یک هایپرلینک رفع می‌کند اما علامت دوم را دست‌نخورده می‌گذارد: رکورد فوکوس منطقی شما می‌گوید فیلد ۳، ویجت فوکوس‌شده بومی همچنان هیچ است، و هر دسترسی‌گر ساخته‌شده روی فوکوس بومی، متن فوکوس‌شده، مقدار فوکوس‌شده، وضعیت انتخاب گزینه، خالی برمی‌گردد. نمای صفحه توسط FORM_OnAfterLoadPage ساخته و توسط FORM_OnBeforeClosePage نابود می‌شود. در یک نمایشگر ساخته‌شده حول یک کنترل بصری، آن فراخوانی‌ها به‌عنوان بخشی از نمایش یک صفحه اتفاق می‌افتند، به همین دلیل شکست اغلب شبیه یک باگ فقط-سربدون به‌نظر می‌رسد: همان کدی که در دموی رابط کاربری کار می‌کند در ابزار دسته‌ای شکست می‌خورد. چرخه حیات به آبجکت سند تعلق دارد، نه نمایشگر، پس PDFium Component اکنون هر دو فراخوانی را هرجا یک صفحه با حضور یک دسته فرم بارگذاری یا تخلیه شود منتشر می‌کند. امضای C ابتدا صفحه و سپس دسته فرم را می‌گیرد، که هنگام نوشتن سیم‌کشی با دست به‌آسانی معکوس می‌شود

procedure ReportFirstField(const FileName: string);
var
  Pdf: TPdf;
  Idx: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FormFill := True;      // form-fill environment, before Active
    Pdf.FileName := FileName;
    Pdf.Active := True;
    Pdf.PageNumber := 1;       // page load also runs FORM_OnAfterLoadPage

    Idx := Pdf.FocusNextFormField;   // logical index, 0-based over widgets
    if Idx < 0 then
      Exit;                    // page holds no widget annotations

    Writeln(string(Pdf.FormFieldInfo[Idx].Name), ' = ',
      string(Pdf.FocusedFormFieldValue));   // reads the native focused widget
  finally
    Pdf.Free;                  // page unload runs FORM_OnBeforeClosePage
  end;
end;

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

شاخص فیلد منطقی چه چیزی را وعده نمی‌دهد

یک شاخص فیلد صفر-پایه یک راحتی است، نه یک هویت معنایی، و چهار محدودیت از آن پیروی می‌کند. این به‌ازای صفحه است، نه به‌ازای سند، پس شاخص ۰ روی صفحه ۲ یک ویجت متفاوت از شاخص ۰ روی صفحه ۱ است و مقایسه‌شان بی‌معناست. این موقعیتی است، پس درج یا حذف یک حاشیه‌نویسی هر شاخص کش‌شده بالای تغییر را باطل می‌کند؛ یک شاخص ذخیره‌شده را فقط تا زمانی که صفحه بارگذاری‌شده و ویرایش‌نشده بماند معتبر رفتار کنید

محدودیت سوم آنی است که مردمی که یک فهرست فیلد را بازبینی می‌کنند غافلگیر می‌کند. شاخص ویجت‌ها را می‌شمارد، نه فیلدها را. یک گروه رادیویی یک فیلد با چند فرزند ویجت است، پس یک گروه سه‌دکمه‌ای سه شاخص پیوسته مشارکت می‌کند که همه همان Name را گزارش می‌دهند. رکورد TPdfFormFieldInfo GroupCount و GroupIndex را دقیقاً برای همین مورد حمل می‌کند، و یک رابط کاربری فهرستی که آن‌ها را نادیده بگیرد همان فیلد را سه‌بار نشان می‌دهد. محدودیت چهارم مربوط به ترتیب پیمایش است: ترتیب Tab نمایش‌داده‌شده در اینجا ترتیب شمارش ویجت است، که آرایه /Annots را دنبال می‌کند، نه مدخل /Tabs صفحه (ISO 32000-1 §7.7.3.3) و نه درخت فیلد AcroForm. برای بیشتر تولیدکنندگان آن‌ها توافق دارند؛ برای فرمی که با دو ستون توسط یک مولد که ستون راست را اول منتشر کرده چیده شده، ندارند، و مسیر صفحه‌کلید توصیف‌شده در مقاله ناوبری فیلد فرم اشتباه احساس می‌شود حتی اگر هر شاخص درست باشد. وقتی یک فایل مشتری عجیب رفتار می‌کند، دو فضای شاخص را کنار هم دامپ کنید پیش از نظریه‌پردازی: نمای حاشیه‌نویسی و نمای فیلد همان صفحه، چاپ‌شده کنار هم، معمولاً علت را در یک نگاه آشکار می‌کنند

procedure DumpIndexSpaces(Pdf: TPdf);
var
  I: Integer;
  Info: TPdfFormFieldInfo;
begin
  for I := 0 to Pdf.AnnotationCount - 1 do
    Writeln('annot ', I, ': subtype ', Ord(Pdf.Annotation[I].Subtype));

  for I := 0 to Pdf.FormFieldCount - 1 do
  begin
    Info := Pdf.FormFieldInfo[I];
    Writeln('field ', I, ': ', string(Info.Name),
      ' widget ', Info.GroupIndex, ' of ', Info.GroupCount);
  end;
end;

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

API های شمارش فیلد، فوکوس، و حاشیه‌نویسی که در اینجا شرح داده شد با PDFium Component برای Delphi، C++Builder، و Lazarus ارائه می‌شود، که صفحه محصولش مرجع کامل فیلد-فرم شامل رکورد اطلاعات فیلد و دسترسی‌گرهای فوکوس را در بر دارد