مقاله فنی

بررسی حاشیه‌نویسی PDF در دلفی با کامپوننت PDFium

حاشیه‌نویسی در یک PDF یک دیکشنری متصل به یک صفحه است، نه علامتی که روی آن رسم شده باشد. استاندارد ISO 32000-1 §12.5 تقریباً دو جین نوع فرعی (subtype) را تعریف می‌کند، و هر کدام دارای یک /Subtype، یک مستطیل در مختصات صفحه، مجموعه‌ای از پرچم‌ها، و معمولاً یک استریم ظاهری (appearance stream) هستند که تصمیم می‌گیرد یک نمایشگر واقعاً چه چیزی را رسم کند. انواع فرعی برای فردی که یک سند را بررسی می‌کند، به یک معنا نیستند. یک برجسته‌سازی (Highlight) و یک خط جوهر (Ink stroke) نظر و بازخورد (comment) هستند؛ یک پیوند (Link) برای ناوبری است؛ یک Popup پنجره کوچکی است که وقتی روی یک یادداشت چسبان (sticky note) کلیک می‌کنید باز می‌شود، به عنوان شیء مستقل ذخیره می‌شود و توسط یک شیء والد به آن اشاره می‌شود. پاسخ‌ها (Replies) حاشیه‌نویسی‌های متنی کاملی هستند که از طریق یک ورودی in-reply-to به نظری که به آن پاسخ می‌دهند ارجاع می‌دهند. بنابراین آرایه حاشیه‌نویسی در سطح صفحه لیست نظرات بررسی‌کننده نیست. این یک کیسه مسطح حاوی نظرات، اتصالاتی که آن‌ها را به هم وصل می‌کند، و چندین چیزی است که هیچ بررسی‌کننده‌ای هرگز آن‌ها را یک نظر نمی‌نامد. پنلی که این آرایه را به عنوان لیست نظرات در نظر بگیرد، با هر نمایشگر دیگری که مشتری اجرا می‌کند، مغایرت خواهد داشت

ساخت یک جریان کاریِ بررسیِ حاشیه‌نویسی بر روی کامپوننت PDFium، کامپوننت VCL/LCL مبتنی بر PDFium برای دلفی، C++Builder و لازاروس، به معنای تمرکز بر نقاطی است که شکاف بین آرایه خام و دید انسانی باعث مشکل می‌شود: شمارش، ایندکس‌گذاری، تغییر رنگ علائمی که موتور از قبل ثابت (فریز) کرده است، حذف بدون به جا گذاشتن آثار (ghosts) و افزودن علائم خاص خودتان

چرا شمارش شما هرگز با پنل نظرات اکروبات (Acrobat) مطابقت ندارد

یک قرارداد علامت‌گذاری شده را به طور همزمان در نمایشگر خود و اکروبات باز کنید، خواهید دید که مجموع آن‌ها به ندرت با هم تطابق دارند. اکروبات یک نمای مدیریت‌شده (curated view) را نشان می‌دهد: علائم گروه‌بندی شده در رشته‌های پاسخ (reply threads)، پنجره‌های بازشو که در یادداشت‌های مربوطه پنهان شده‌اند، و لینک‌ها و ابزارک‌های فرم که کنار گذاشته شده‌اند. آرایه خام همه این‌ها را بدون تمایز در خود نگه می‌دارد، بنابراین یک شمارش ساده در برخی جهات مقدار بالا و در جهات دیگر به طور همزمان مقدار پایین نشان می‌دهد

پنجره‌های بازشو (Popups) مجموع را افزایش می‌دهند، زیرا هر یادداشت چسبان با یک شیء Popup جداگانه همراه است و شمارش هر دو، تعداد یادداشت‌ها را دو برابر می‌کند. اگر روی علائم قابل مشاهده فیلتر کنید، پاسخ‌ها مجموع را کاهش می‌دهند، زیرا یک پاسخ یک حاشیه‌نویسی متنی است که تا زمانی که کسی رشته را باز نکند، چیزی برای رسم ندارد، و حذف آن باعث از دست رفتن بحث می‌شود. پرچم‌های Hidden و NoView یک حاشیه‌نویسی را بدون اینکه آن را از آرایه خارج کنند از صفحه نمایش محو می‌کنند، بنابراین یک شمارش بدون توجه به پرچم، علائمی را که کاربر نمی‌تواند ببیند شامل می‌شود. حاشیه‌نویسی‌های پیوند (Link) در همان آرایه نظرات قرار می‌گیرند و نه به شمارش و نه به لیست تعلق دارند. پیش از نوشتن حلقه در مورد قانون شمارش تصمیم بگیرید و آن تصمیم را یادداشت کنید، زیرا "چرا پنل شما عددی متفاوت از اکروبات را نشان می‌دهد" اولین تیکتی (درخواست پشتیبانی) است که یک ویژگی بررسی دریافت می‌کند

همه چیز را یک بار ایندکس کنید، و سپس هرگز یک صفحه را دوباره پردازش نکنید

یک قانون طراحی هدایتگر تمام مواردی است که در ادامه می‌آید: فیلتر کردن بر اساس نویسنده، نوع یا صفحه هرگز نباید اشیاء صفحه را دوباره پردازش (re-parse) کند. در یک سند 300 صفحه‌ای با نشانه‌گذاری‌های سنگین، پردازش مجدد در هر تغییر منوی کشویی، پنل را به چیزی تبدیل می‌کند که هر بار برای چند ثانیه دچار لکنت (هنگ کردن) می‌شود. این کامپوننت، ویژگی AnnotationCount و ویژگی ایندکس‌شده Annotation[] را در دسترس قرار می‌دهد، که هر دو به صفحه بارگذاری‌شده فعلی محدود می‌شوند و رکورد TPdfAnnotation که آن‌ها برمی‌گردانند، شامل تمام چیزهایی است که یک نمای لیست به آن نیاز دارد: Subtype، Flags، Color، Rectangle، ContentsText و AuthorText. حرکت درست این است که هنگام باز کردن سند، تمام صفحات را یک بار پیمایش (sweep) کنید و ایندکس مسطح خود را نگه‌دارید:

procedure TReviewPanel.BuildIndex;
var
  PageNo, i: Integer;
  A: TPdfAnnotation;
begin
  FItems.Clear;
  for PageNo := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := PageNo;
    for i := 0 to Pdf.AnnotationCount - 1 do
    begin
      A := Pdf.Annotation[i];
      // Keep reviewer-relevant subtypes only; record the page and
      // index pair because all later edits are addressed by it
      if A.Subtype in [anText, anHighlight, anInk] then
        FItems.Add(TReviewItem.Create(PageNo, i,
          A.AuthorText, A.ContentsText, A.Rectangle, A.Color));
    end;
  end;
end;

جفتی که ارزش تأکید دارد (PageNo, i) است. هر تغییر بعدی، چه تغییر رنگ و چه حذف، با شماره صفحه به علاوه ایندکس حاشیه‌نویسی آدرس‌دهی می‌شود، و این ایندکس شکننده است: حذف یک حاشیه‌نویسی تمام موارد پس از آن در همان صفحه را مجدداً شماره‌گذاری می‌کند. بنابراین برنامه‌ریزی کنید که پس از هر حذفی، ورودی‌های صفحه متأثر را به جای اصلاح شماره‌های ایندکس در همانجا، از نو بسازید. این بازسازی یک میلی‌ثانیه زمان می‌برد. در مقابل، یک ایندکس کهنه نظر اشتباه بررسی‌کننده را حذف می‌کند، که این همان باگی است که اعتماد به کل ویژگی را از بین می‌برد

نخ‌بندی (Threading) حتی اگر اولین نسخه انتشار یافته شما تنها به جای نمایش دادن، پاسخ‌ها را بشمارد، شایسته جایگاهی در ایندکس است. آیتم‌ها را بر اساس مرجع والدشان در حین باز بودن صفحه گروه‌بندی کنید، تا پنل بعداً بتواند مانند اکروبات یک رشته گفتگو را جمع کند. بازسازی این گروه‌بندی با روش تنبل (lazy) در حین اسکرول کردن، کل هدفِ یک‌بار ایندکس کردن را از بین می‌برد، زیرا صفحاتی را که قبلاً برای پردازش آن‌ها هزینه داده‌اید، دوباره باز می‌کند. هندسه نیز همان نظم را می‌طلبد. مشخصه Rectangle در هر رکورد، در مختصات صفحه (page-space) است و تبدیل آن به مختصات نما متعلق به یک کلاس کمکی مشترک است، نه اینکه در سراسر کد پخش شود. پنل‌ها باگ‌های مختصاتی را زمانی ایجاد می‌کنند که انتخاب، تست برخورد (hit-testing) و رسم، هر کدام ریاضیات زوم و چرخش خاص خود را اختراع می‌کنند؛ هر سه مورد را از طریق یک تبدیل واحد مسیردهی کنید، و یک برجسته‌سازی (highlight)، سطر آن در لیست، و هدف کلیک آن دقیقاً روی همان جوهر متصل باقی می‌ماند

تغییر رنگ علائم و وتوی استریم ظاهری

تغییر رنگ یک برجسته‌سازی (highlight) از زرد به کهربایی شبیه به یک کد تک‌خطی به نظر می‌رسد و گاهی اوقات نیز همین‌طور است. اما نکته مهم در استاندارد ISO 32000-1 §12.5.5 است. زمانی که یک حاشیه‌نویسی دارای استریم ظاهری /AP است، یک نمایشگر سازگار، آن استریم از پیش ساخته شده را رسم می‌کند و با ورودی رنگ در دیکشنری مانند متا‌دیتای مرده رفتار می‌کند. اکروبات برای همه چیزهایی که ایجاد می‌کند استریم‌های ظاهری می‌نویسد، بنابراین اکثر حاشیه‌نویسی‌هایی که از مشتریان دریافت می‌کنید از قبل در این وضعیت هستند و رنگی که با اطمینان کامل تنظیم می‌کنید هرگز به صفحه نمایش نمی‌رسد. تغییر رنگ یک عملیات خواندن-اصلاح-نوشتن (read-modify-write) از طریق ویژگی Annotation[] است و این کامپوننت در مورد این تضاد صادق است: وقتی موتور اجازه نمی‌دهد که یک رنگ دیکشنری بر یک ظاهر تثبیت‌شده غلبه کند، عمل نوشتن خطای EPdfError را صادر می‌کند

A := Pdf.Annotation[Item.Index];
A.HasColor := True;
A.Color := $0000B0FF;       // amber
A.ColorAlpha := 160;
try
  Pdf.Annotation[Item.Index] := A;
except
  on EPdfError do
  begin
    // The annotation owns a pre-rendered /AP stream; the dictionary
    // color alone cannot change what viewers paint
    Item.AppearanceLocked := True;
    StatusBar.SimpleText := 'Color is fixed by the annotation appearance';
  end;
end;

این استثنا را هر بار بگیرید (catch کنید) و با آن به عنوان اطلاعات برخورد کنید، نه به عنوان شکست. اگر این حفاظ را نادیده بگیرید، پنل شما با خوشحالی رنگ کهربایی را در لیست خود نشان می‌دهد در حالی که صفحه به رنگ زرد رسم می‌شود؛ هفته‌ها بعد کاربر گزارش می‌دهد که "نمایشگر شما ویرایش‌های مرا نادیده می‌گیرد"، و شما یک بعدازظهر را صرف این می‌کنید که نمی‌توانید این مشکل را در فایلی که اتفاقاً استریم ظاهری ندارد بازتولید کنید. به محض اینکه بدانید ظاهر قفل شده است، دو واکنش صادقانه دارید: به جای حاشیه‌نویسی، رنگ لایه انتخاب (selection overlay) خود را تغییر دهید تا بررسی‌کننده حداقل برجسته‌سازی را که انتخاب کرده ببیند، یا آن سطر را به عنوان 'با ظاهر قفل‌شده' علامت‌گذاری کنید تا هیچ‌کس انتظار اعمال تغییرات را نداشته باشد

حذف حاشیه‌نویسی‌ها بدون به جا گذاشتن آثار (ghosts)

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

Pdf.PageNumber := Item.PageNo;
Pdf.DeleteAnnotation(Item.Index);   // raises EPdfError on failure
Bmp := Pdf.RenderPage(0, 0, ViewWidth, ViewHeight, ro0, [reAnnotations]);
try
  PaintPageBitmap(Bmp);
finally
  Bmp.Free;  // RenderPage hands bitmap ownership to the caller
end;
RebuildPageEntries(Item.PageNo);  // indices after Item.Index shifted

در آن بلوک دو جزئیات وجود دارد که امکان اشتباه در آن‌ها زیاد است. گزینه reAnnotations باید وجود داشته باشد، در غیر این صورت شبکه جدید هر حاشیه‌نویسی باقی‌مانده را حذف می‌کند و صفحه به گونه‌ای به نظر می‌رسد که گویی به جای یک علامت، کل مجموعه نظرات را پاک کرده‌اید. و فراخوانی Bmp.Free اختیاری نیست: سربارگذاریِ (overload) تابع RenderPage مالکیت بیت‌مپ را به فراخوانی‌کننده واگذار می‌کند، بنابراین فراموش کردنِ آزادسازی باعث می‌شود که در هر حذف یک شبکه تمام‌صفحه نشت کند، که یک بررسی‌کننده که روی یک سند طولانی کار می‌کند در عرض چند دقیقه آن را به یک فشار حافظه واقعی تبدیل می‌کند

افزودن علائم بررسی‌کننده از رابط کاربری خودتان

ایجاد حاشیه‌نویسی‌ها از طریق CreateAnnotation انجام می‌شود، که یک رکورد TPdfAnnotation پر شده (subtype، rectangle، color، contents، author) را دریافت کرده و آن را به صفحه فعلی متصل می‌کند. یادداشت چسبان، زیرنوع (subtype) anText، حالت آسان ماجرا است: موقعیت، محتوا و نویسنده را تنظیم کنید و کار تمام است. اما حاشیه‌نویسی‌های جوهر (Ink annotations) جایی است که افراد در آن گیر می‌افتند. مستطیلِ رکورد فقط طراحی را محدود می‌کند؛ خود خطوط، آرایه‌هایی از نقاط هستند که باید به طور جداگانه از طریق فراخوانی ضربه‌قلم (ink-stroke) موتور پیوست شوند، یعنی داده‌های FS_POINTF که از طریق ورودی ماوس یا قلم ثبت شده‌اند، هر بار یک ضربه، به FPDFAnnot_AddInkStroke خورانده می‌شوند. ساختن یک حاشیه‌نویسی جوهر از یک مستطیل و نه هیچ چیز دیگری، منجر به ایجاد خط‌خطی‌های خالی می‌شود که به عنوان یک فضای سفید رندر می‌گردد، و شبیه به باگی در موتور به نظر می‌رسد که در واقع یک حاشیه‌نویسی نیمه‌کاره است

هم‌زمان سیاست نویسندگی (authorship) را نیز مشخص کنید. هر علامتی که رابط کاربری شما ایجاد می‌کند باید دارای یک AuthorText سازگار باشد، زیرا فیلتر بررسی‌کننده‌ای که ماه آینده می‌سازید، به همان اندازه نام‌هایی که امروز روی نظرات ثبت می‌کنید، خوب خواهد بود. رشته‌های نویسنده خالی یا متناقض بدون باز کردن مجدد تک تک فایل‌ها قابل تعمیر به صورت عطف به ماسبق نیستند

استخراج نظرات بررسی از نمایشگر

ارزش واقعی داده‌های بررسی زمانی مشخص می‌شود که بتوانند از نمایشگر خارج شوند، مانند خلاصه‌ای که مدیر پروژه بدون باز کردن فایل می‌خواند یا یک فایل CSV که شیت پیگیری را تغذیه می‌کند. اطلاعات را از ایندکسی که قبلاً ساخته‌اید استخراج کنید، و هرگز از پردازش (parse) مجدد استفاده نکنید، و یک روش پایدار برای ارجاع به هر علامت انتخاب کنید. یک شماره صفحه همراه با مستطیل حاشیه‌نویسی از رفت‌وبرگشت‌ها (round-trips) جان سالم به در می‌برد اما ایندکس آرایه چنین نیست، زیرا حذف بعدی بی‌سروصدا ایندکس‌ها را مجدداً شماره‌گذاری می‌کند و فایل CSV شما به نظرات اشتباهی اشاره خواهد کرد

یک ردیف باارزش برای نگه‌داری، شامل صفحه، زیرنوع (subtype)، نویسنده، مُهر زمان ایجاد در صورت وجود در فایل، متن محتوا، و ستون وضعیتی است که شما در اختیار دارید نه آنچه که PDF ارائه می‌دهد. همان مرحله ایندکس‌گذاری در مراحل ابتدایی و هنگام دریافت (intake) نیز مفید است، یعنی زمانی که سندی از خارج تیم می‌رسد و می‌خواهید قبل از اینکه کسی آن را بررسی کند بدانید که داخل آن چیست. مقاله محیط کار دریافت PDF آن تریاژ را توضیح می‌دهد، و ناوبری فیلدهای فرم مشکل متضاد آن را پوشش می‌دهد: بررسی اسنادی که برای جمع‌آوری داده‌ها ساخته شده‌اند به جای جمع‌آوری نظرات

موردی که آرایه به شما نشان نخواهد داد

یک حالت شکست مستحق بررسی و توجه است زیرا مانند یک نقص در کد شما به نظر می‌رسد در حالی که اینطور نیست. یک مشتری وجود برجسته‌سازی‌های (highlights) مشهودی را در سراسر یک صفحه گزارش می‌دهد، اما پنل شما چیزی لیست نمی‌کند، و AnnotationCount نیز مقدار صفر را برمی‌گرداند. توضیح معمول این است که علائم در یک مرحله قبلی (upstream) مسطح (flatten) شده‌اند. مسطح کردن (Flattening) ظواهر حاشیه‌نویسی را با محتوای معمولی صفحه ترکیب می‌کند، بنابراین برجسته‌سازی‌ها به بخشی از گرافیک صفحه تبدیل می‌شوند و به طور کامل از حالت اشیاء حاشیه‌نویسی خارج می‌شوند. در نتیجه چیزی برای API حاشیه‌نویسی باقی نمی‌ماند که بخواهد آن را بشمرد، تغییر رنگ دهد یا حذف کند. زمانی که یک نشانه‌گذاری رسم شده با شمارش صفر را می‌بینید، به جای جستجوی باگ در حلقه شمارش، بپرسید که فایل چگونه تولید شده است

سطح حاشیه‌نویسی (annotation surface) استفاده شده در اینجا، از شمارش و ایجاد تا تغییر رنگ، حذف و گزینه‌های رندر که نمایش را صادقانه نگه می‌دارد، با کامپوننت PDFium برای دلفی، C++Builder و Lazarus/FPC عرضه می‌شود