مقاله فنی

بازگشت Form XObject: تشخیص چرخه در PDFlibPas در Delphi

PDFlibPas فراخوانی‌های بازگشتی Form XObject را در جریان‌های محتوای PDF در Delphi با پیگیری زنجیره‌ی فراخوانی فعال حل می‌کند، نه یک مجموعه‌ی بازدیدشده‌ی سراسری، پس TPDFlib.EnumPageContentStatesEx می‌تواند همان Form که چندبار روی یک صفحه فراخوانی شده را بدون اشتباه‌گرفتن استفاده‌ی دوباره‌ی مشروع با یک چرخه بپیماید. یک Form XObject مهر در یک الگوی فاکتور مورد معمول است: همان شیء از هدر، فوتر، و یک لایه‌ی واترمارک روی یک صفحه فراخوانی می‌شود، و فقط زنجیره‌ی فراخوانی‌ای که خودش را حلقه می‌کند یک چرخه‌ی واقعی است

ISO 32000-1 §8.10 یک Form XObject را به‌عنوان یک جریان محتوای خودکفا تعریف می‌کند که یک صفحه، یا یک Form دیگر، با عملگر Do فراخوانی می‌کند، کامل با سیستم مختصات خودش در /Matrix، یک مرز کلیپ در آن سیستم مختصات در /BBox، و اختیاری دیکشنری منبع خودش. هیچ چیزی در مشخصات محدود نمی‌کند که چند بار یک Form می‌تواند فراخوانی شود یا چقدر عمیق Formها می‌توانند یکدیگر را فراخوانی کنند، پس یک تجزیه‌گر مطابق باید استفاده‌ی دوباره‌ی مشروع و تودرتویی مشروع را بپذیرد درحالی‌که همچنان از خودش در برابر همان یک ترتیبی که مشخصات واقعاً ممنوع می‌کند دفاع کند: یک Form که جریان محتوایش، مستقیم یا انتقالی، خودش را فراخوانی کند. PDFlibPas آن تمایز را از طریق مقادیر TPDFlibContentFormTraversalStatus پیوست‌شده به هر عکس Do گزارش می‌دهد، به‌طور مشخص ftsEnumerated برای یک نزول موفق و ftsCycle برای همان یک موردی که واقعاً یک حلقه است

چرا استفاده‌ی دوباره از همان Form XObject یک چرخه‌ی کاذب ماشه نمی‌کشد؟

یک ارجاع Form XObject تکراری، به‌تنهایی، شواهدی از هیچ‌چیز اشتباه نیست. ISO 32000-1 اجازه می‌دهد همان شیء Form از هر تعداد جایی در یک جریان محتوا که نویسنده می‌خواهد فراخوانی شود، که دقیقاً همان روشی است که یک مهر لوگو، یک الگوی سربرگ، یا یک فوتر شماره‌صفحه در سراسر یک صفحه بدون تکرارکردن جریان محتوایش چندبار دوباره استفاده می‌شود. نگهبان ساده‌لوحانه در برابر بازگشت فرارونده یک مجموعه‌ی بازدیدشده‌ی تکی است کلیددهی‌شده با شماره‌ی شیء: اولین‌باری که یک پیمایشگر شیء Form 12 را می‌بیند، 12 را به‌عنوان دیده‌شده علامت می‌زند و امتناع می‌کند دوباره جای دیگری در درخت واردش شود. آن رویکرد همان لحظه‌ای می‌شکند که همان مهر در دو گوشه‌ی بی‌ربط از یک صفحه ظاهر شود، چون دومین فراخوانی، کاملاً مشروع، پس از اینکه شماره‌ی شیء از پیش به‌عنوان دیده‌شده علامت خورده می‌رسد و انگار حلقه بود رد می‌شود

PDFlibPas از آن مثبت-کاذب با محدوده‌بندی تشخیص چرخه به زنجیره‌ی فراخوانی فعلی به‌جای کل سند اجتناب می‌کند. EnumPageContentStatesEx جریان Form حل‌شده را بلافاصله پیش از نزول‌کردن درونش روی زنجیره‌ی فراخوانی فعال فشار می‌دهد، سپس همان ورودی را دوباره از آن pop می‌کند همان لحظه‌ای که نزول برمی‌گردد، موفق یا نه. یک فراخوانی خواهرانه از همان جریان یکسان فقط پس از اینکه اولی از پیش pop شده شروع می‌شود، پس زنجیره‌ی فراخوانی از آن جریان تا زمانی که فراخوانی خواهرانه بررسیش می‌کند پاک است، و پیمایشگر آن را دقیقاً همان‌طور که هر Form دیگری را می‌شمارد. یک چرخه‌ی واقعی روی همان زنجیره متفاوت به‌نظر می‌رسد: Form A، Form B را فرا می‌خواند، B همچنان روی زنجیره باز است وقتی محتوای خودش دوباره به A بازمی‌گردد، و A همچنان روی زنجیره از فراخوانی بیرونی‌ای که هنوز برنگشته می‌نشیند — این تنها شکلی است که ftsCycle گزارش می‌دهد، یک جریان Form که همچنان جایی زودتر روی زنجیره‌ی فراخوانی فعلی باز است، نه صرفاً حاضر در جای دیگری روی صفحه

بازگشت Form XObject چقدر می‌تواند عمیق برود پیش از اینکه PDFlibPas آن را متوقف کند؟

تشخیص چرخه و محدودسازی عمق دو مسئله‌ی متفاوت را حل می‌کنند، و PDFlibPas آن‌ها را دقیقاً به همین دلیل به‌عنوان دو نتیجه‌ی متفاوت TPDFlibContentFormTraversalStatus نگه می‌دارد. یک زنجیره‌ی بیست Form متمایز، هرکدام بعدی را فراخوانی می‌کند و هیچ‌کدام تکرار نمی‌شود، به هیچ تعریفی یک چرخه نیست — بررسی زنجیره-فعال هرگز یک جریان تکراری پیدا نمی‌کند — اما بیست سطح صادق از تودرتویی همچنان بیست سطح از تجزیه، به‌هم‌پیوستن ماتریس، و حل منبع است که یک PDF بدشکل یا خصمانه می‌توانست دلخواه بالاتر ببرد اگر چیز دیگری متوقفش نمی‌کرد. EnumPageContentStatesEx دقیقاً به همین دلیل یک پارامتر MaxFormDepth می‌گیرد و هر مقداری که پاس داده شود را به یک حداکثر ۶۴ گیره می‌زند، صرف‌نظر از اینکه فراخواننده چه چیزی می‌خواهد. یک عمق صفر یک حالت خاص است ارزش دانستن به‌تنهایی: کاملاً بازگشت Form را غیرفعال می‌کند و رفتار تخت و فقط-صفحه‌ای متد قدیمی‌تر EnumPageContentStates را بازتولید می‌کند، به همین دلیل هر عکس Do در آن حالت ftsNotRequested را گزارش می‌دهد به‌جای اینکه هرچیزی را امتحان کند

var
  Lib: TPDFlib;
  States: array of TPDFlibContentGraphicsState;
  Count, I: Integer;
begin
  Lib:= TPDFlib.Create;
  try
    if Lib.LoadFromFile('invoice-batch.pdf', '')<> 1 then
      Exit;
    Lib.SelectPage(1);
    Count:= Lib.EnumPageContentStatesEx(True, 8, States);  // count only
    SetLength(States, Count);
    Lib.EnumPageContentStatesEx(True, 8, States);          // fill
    for I:= 0 to Count- 1 do
      if States[I].FormTraversalStatus= ftsCycle then
        LogSuspectForm(States[I].XObjectResource, States[I].ContentDepth);
  finally
    Lib.Free;
  end;
end;

یک زیرتراکر به‌ازای هر فراخوانی: جداسازی وضعیت گرافیکی

هر نزول به درون یک Form XObject تراکر وضعیت-گرافیکی خودش را می‌گیرد به‌جای اینکه آن‌که از پیش صفحه را می‌پیماید به اشتراک بگذارد، چون جریان محتوای یک Form الزام دارد وضعیت گرافیکی را دقیقاً همان‌طور که یافته رها کند، و PDFlibPas نمی‌تواند فرض کند هر PDFای که باز می‌کند واقعاً آن الزام را رعایت می‌کند. تراکر فرزند از یک عکس هر آنچه CTM، وضعیت رنگ، و پارامترهای متن که در دستورالعمل فراخواننده‌ی Do فعال بودند شروع می‌شود، سپس پشته‌ی save-and-restore خودش و پیگیری مسیر-فعلی را پیش از اجرای یک دستورالعمل تکی از Form به خالی بازنشانی می‌کند. یک q نامتوازن بدون هیچ Q متناظری درون یک Form بی‌احتیاط یا آسیب‌دیده، چیزی نادر برای یافتن در PDFهای تولیدشده توسط ابزارسازی قدیمی‌تر نیست، درون تراکر همان یک فراخوانی محدود می‌ماند و هرگز به تراکر صفحه یا به یک فراخوانی خواهرانه‌ی همان مهر که یک خط بعدتر در جریان محتوا نشسته نشت نمی‌کند

/Matrix فرم با CTM مؤثر در Do دقیقاً همان‌طور که یک عملگر cm ترکیب می‌شود، ضرب-از-چپ در برابر تبدیل فعلی به‌جای جایگزینی آن، و PDFlibPas عمداً همان یک مسیر کد را دوباره استفاده می‌کند به‌جای نگه‌داشتن یک فرمول دوم، چون دو پیاده‌سازی مستقل از همان جبر ماتریسی دقیقاً همان نوع تکراری است که پس از چند دور ترکیب مقیاس، چرخش، و برش بی‌سروصدا از هم جدا می‌شود. سپس /BBox در فضای مختصات خودِ Form پس از اینکه ماتریس از پیش اعمال شده کلیپ می‌کند، و هر چهار گوشه‌ی آن جعبه به‌طور جداگانه تبدیل می‌شوند نه فقط گوشه‌های متقابل، چون یک Form چرخیده یا برش‌خورده در غیر این‌صورت می‌تواند یک جعبه‌ی محدودکننده گزارش دهد که محتوای واقعی نشسته در چیزی که سابقاً یک گوشه‌ی افراطی بود پیش از اینکه تبدیل آن را جای دیگری ببرد، از قلم می‌اندازد. گسترش حلقه از مثال قبلی روی همان آرایه‌ی States آن فیلدها را مستقیم می‌خواند

for I:= 0 to Count- 1 do
  if (States[I].OperatorName= 'Do')and (States[I].XObjectKind= cxkForm)and
    States[I].FormBBoxKnown then
    Writeln('Form ', States[I].XObjectResource, ' matrix ',
      States[I].FormMatrix.M11:0:3, ',', States[I].FormMatrix.M12:0:3,
      ' bbox ', States[I].FormBBoxLeft:0:1, '..', States[I].FormBBoxRight:0:1);

آیا دو Form با همان نام منبع یک فونت را به اشتراک می‌گذارند؟

نه. یک نام منبع مثل /F1 فقط نسبت به دیکشنری منبع فعال در نقطه‌ی استفاده‌اش معنایی دارد، و دو Form XObject متفاوت آزادند دو فونت کاملاً متفاوت را زیر همان نام یکسان تعریف کنند. PDFlibPas این را با پیگیری یک دامنه‌ی منبع در کنار هر نام منبع حل می‌کند: وقتی یک Form دیکشنری /Resources خودش را حمل می‌کند، آن دیکشنری دامنه‌ی منبع کاملی برای هر چیز درونش می‌شود، بدون هیچ فروبازگشت به‌ازای هر کلید به صفحه یا دیکشنری فراخواننده برای هر چیزی که دیکشنری خودِ Form اتفاقاً حذف کند. فقط یک Form بدون هیچ کلید /Resourcesای اصلاً، الگویی که برخی مولدهای قدیمی‌تر PDF هنوز تولید می‌کنند، دیکشنری فراخواننده را کاملاً به ارث می‌برد، و آن یک استثنای سازگاری عمدی است نه یک قاعده‌ی عمومی ارزش تکیه‌کردن در خروجی جدید. هویت فونت در یک عکس TPDFlibContentGraphicsState بنابراین جفت FontResource و FontResourceScope است، نه صرفاً نام، با FontObjectNumber در دسترس برای تأیید اینکه دقیقاً کدام شیء غیرمستقیم یک /F1 مشخص در آن دامنه‌ی خاص به آن حل شده

همان دامنه‌بندی روی هر منبع نام‌داری دیگری که یک Form می‌تواند حمل کند اعمال می‌شود، ورودی‌های ExtGState و ورودی‌های XObject تودرتو شامل، چون سازوکار حل زیرین فونت‌ها را مورد-خاص نمی‌کند — فقط اتفاقاً حالت فونت بیشترین اهمیت را دارد، چون یک هویت فونت اشتباه‌گرفته‌شده بی‌سروصدا گلیف‌های اشتباه تولید می‌کند به‌جای یک شکست آشکار. کدی که استخراج می‌کند و runهای متن را فقط بر اساس نام فونت گروه‌بندی می‌کند، بدون گروه‌بندی هم بر اساس دامنه‌ی منبع، دو فونت که از نظر بصری متفاوتند اما اتفاقاً یک نام را به اشتراک می‌گذارند ادغام می‌کند، و اشتباه تا زمانی که کسی متوجه شود اعداد از یک قلم اشتباه درون چیزی نشسته‌اند که قرار بود مثل یک فونت سازگار خوانده شود، نمایان نمی‌شود

for I:= 0 to Count- 1 do
  if (States[I].OperatorName= 'Tj')and States[I].TextAdvanceResolved then
    RecordGlyphRun(States[I].FontResource, States[I].FontResourceScope,
      States[I].FontObjectNumber, States[I].ContentDepth);

خواندن FormTraversalStatus در خط لوله‌ی خودتان

FormTraversalStatus هر عکس Do را به یک گزارش تشخیصی کوچک به‌خودی‌خود تبدیل می‌کند، و یک خط لوله که آن را نادیده می‌گیرد دقیقاً همان اطلاعاتی را دور می‌ریزد که یک استخراج ناقص را توضیح می‌داد. ftsNotApplicable یعنی دستورالعمل اصلاً هرگز یک فراخوانی Form حل‌شده نبود؛ ftsNotRequested یعنی بازگشت برای این فراخوانی خاموش شده بود؛ ftsEnumerated یعنی Form با موفقیت تجزیه و پیموده شد؛ ftsDepthLimit و ftsCycle دو روشی که یک نزول عمداً کوتاه می‌شود را نشانه‌گذاری می‌کنند؛ و ftsMalformed هر چیز دیگری را که پیمایش را متوقف کرده پوشش می‌دهد — یک ارجاع جریان حل‌نشدنی، یک /Matrix یا /BBox که تجزیه‌اش شکست خورده، یا یک استثنا که در حین اجرای محتوای خودِ Form raise شده. آن حالت آخر از نظر عملیاتی اهمیت دارد، چون یک پیمایش تودرتوی شکست‌خورده هر خروجی جزئی‌ای که از پیش برای آن شاخه تولید کرده بود را بازمی‌گرداند، پس یک فراخواننده هرگز مجبور نیست حدس بزند آیا یک Form واقعاً خالی بود یا صرفاً دو دستورالعمل درون جریان محتوایش منفجر شد

var
  Tally: array[TPDFlibContentFormTraversalStatus] of Integer;
  Status: TPDFlibContentFormTraversalStatus;
begin
  for Status:= Low(Tally) to High(Tally) do
    Tally[Status]:= 0;
  for I:= 0 to Count- 1 do
    Inc(Tally[States[I].FormTraversalStatus]);
  if (Tally[ftsCycle]> 0)or (Tally[ftsMalformed]> 0) then
    FlagForManualReview(SourceFileName, Tally[ftsCycle], Tally[ftsMalformed]);
end;

مرزها، هزینه‌ها، و اینکه این کجا جا می‌افتد

جریان محتوای یک Form دقیقاً یک‌بار به‌ازای هر فراخوانی enumeration رمزگشایی و تجزیه می‌شود بدون توجه به اینکه Form چند بار فراخوانی می‌شود، چون PDFlibPas فهرست دستورالعمل تجزیه‌شده را در برابر شیء جریان زیرین کش می‌کند به‌جای دوباره تجزیه‌کردنش در هر فراخوانی خواهرانه — مهر سه‌گوشه‌ی مثال ابتدایی یک‌بار رمزگشایی و سه‌بار پیموده می‌شود، نه سه‌بار رمزگشایی. آنچه واقعاً در هر فراخوانی تکی بازساخته می‌شود هر چیزی است که به‌طور مشروع بین یک محل فراخوانی و بعدی فرق می‌کند: تراکر فرزند، CTM به‌هم‌پیوسته، کلیپ تقاطع‌یافته، و دامنه‌ی منبع. آن حسابداری CTM و کلیپ به‌ازای هر فراخوانی همان ماشین‌آلات پشت تراکر وضعیت CTM و کلیپ جریان محتوا در PDFlibPas است، ارزش خواندن در کنار این یکی برای هر پیمایش جریان محتوا که فراتر از خودِ بازگشت Form می‌رود

دو محدودیت ارزش تنظیم انتظارات دارند پیش از اینکه این API وارد یک خط لوله‌ی بزرگ‌تر شود. سقف عمق ۶۴سطحی یک دکمه‌ی تنظیم برای اسناد واقعاً عمیق مشروع نیست، چون فاکتورها، صورت‌حساب‌ها، و الگوهای گزارش واقعی اساساً هرگز Formها را بیش از سه یا چهار سطح تودرتو نمی‌کنند — سندی که واقعاً به ftsDepthLimit برخورد می‌کند بسیار محتمل‌تر است بدشکل یا خصمانه باشد تا غیرمعمول پیچیده، و ارزش دارد به‌عنوان یک سیگنال کیفیت-داده لاگ شود نه اینکه بی‌سروصدا با یک عدد بزرگ‌تر دوباره تلاش شود. EnumPageContentStatesEx همچنین یک API تحلیل سمت-خواندن است: گزارش می‌دهد یک جریان محتوا چه کاری انجام می‌دهد، نه اینکه آیا یک Form اصلاً باید قابل‌مشاهده باشد، که یک سؤال جداگانه است که با وضعیت قابل‌مشاهده‌بودن Optional Content Group پاسخ داده می‌شود وقتی یک Form مهر یا واترمارک پشت یک لایه‌ای می‌نشیند که یک نمایشگر ممکن است خاموشش کرده باشد. تشخیص چرخه‌ی زنجیره-فراخوانی، جداسازی به‌ازای هر فراخوانی، و دامنه‌بندی منبع با هم یک گوشه از سطح بازرسی جریان محتوا در کامپوننت PDFlibPas برای Delphi و C++Builder را می‌سازند