مقاله فنی

هایلایت غیرمخرب PDF در Delphi: لایه‌ی بازبینی HotPDF

مستطیلی که در حین بازبینی دور یک پاراگراف کشیده می‌شود، لازم نیست به یک نشانه درون PDF تبدیل شود. THPDFViewerModel در HotPDF، AddHighlightRegion را در معرض دید می‌گذارد، متدی که هر هایلایت را به‌عنوان یک رکورد درون‌حافظه‌ای نگه می‌دارد نه یک تغییر در سند بارشده، پس یک بازبین می‌تواند ده‌ها صفحه را نشانه‌گذاری کند درحالی‌که فایل روی دیسک بایت‌به‌بایت همان چیزی می‌ماند که بود. به ۶۴۰۰٪ زوم کنید، صفحه را ۹۰ درجه بچرخانید، از Fit Width به Fit Page تغییر دهید، همان مستطیل همچنان روی همان پاراگراف می‌نشیند، چون محاسبات مختصات از طریق هندسه‌ی واقعی رندر در همان لحظه‌ای که نشانه کشیده شده اجرا می‌شود

ابزارهای بازبینی ساخته‌شده حول یک نمایشگر PDF دائماً با این مسئله روبه‌رو می‌شوند. یک صفحه‌ی redlining، یک پاس QA روی فاکتورهای تولیدشده، یک گردش کار تأیید داخلی: همه‌ی این‌ها نیاز دارند به کسی اجازه دهند توجه را به یک ناحیه از صفحه جلب کند بدون اینکه هر نشانه‌ی پیش‌نویس به یک تغییر دائمی در فایل تبدیل شود، و بدون اینکه به یک زیرسیستم حاشیه‌نویسی کامل نیاز پیدا کنند فقط برای نشان‌دادن یک جعبه‌ی رنگی درحالی‌که کسی هنوز تصمیم می‌گیرد آیا نشانه به‌جا است. HotPDF این را با یک لایه‌ی هایلایت اختصاصی پاسخ می‌دهد که کاملاً روی سمت Model از تفکیکی می‌نشیند که در ساخت یک نمایشگر PDF سفارشی با معماری MVC در Delphi توصیف شده، و این هم دلیل آن است که چرا همان فهرست هایلایت را می‌توان از یک تست واحد بدون هیچ handle پنجره‌ای در میدان دید هدایت کرد

AddHighlightRegion در HotPDF واقعاً چه چیزی را ذخیره می‌کند؟

AddHighlightRegion دقیقاً سه چیز را برای هر نشانه ذخیره می‌کند: یک شماره‌ی صفحه‌ی صفر-پایه، یک THPDFRectangle در مختصات فضای کاربر PDF، و یک TColor، همگی به‌صورت یک رکورد THPDFViewerHighlight بسته‌بندی‌شده درون THPDFViewerModel. فراخوانی Viewer.HighlightRegion(PageIndex, PageRect, clYellow)، یا معادل آن Model.AddHighlightRegion، یکی از این رکوردها را به یک آرایه‌ی خصوصی می‌افزاید و اندیس آن را برمی‌گرداند، و آن اندیس تنها handleای است که یک فراخواننده پس می‌گیرد: هیچ شیء جداگانه‌ای، هیچ اینترفیس با شمارش مرجعی، هیچ چیزی برای آزادکردن وجود ندارد. هر قابلیت دیگری در این مقاله، رسم نشانه، نگاشت مجدد آن پس از تغییر زوم، حذف آن، روی همان یک رکورد کوچک ساخته شده است

هر مستطیل پیش از پذیرفته‌شدن نرمال‌سازی و کلیپ می‌شود. AddHighlightRegion اگر بازبینی از راست به چپ بکشد، لبه‌های چپ و راست را جابه‌جا می‌کند، برای یک کشیدن روبه‌بالا، بالا و پایین را جابه‌جا می‌کند، سپس نتیجه را در برابر MediaBox صفحه که از طریق GetLoadedPageBox بازیابی شده کلیپ می‌کند. مستطیلی که در نهایت عرض صفر، ارتفاع صفر، یا کاملاً خارج از صفحه داشته باشد، مستقیماً رد می‌شود: متد -1 برمی‌گرداند و هیچ‌چیزی به فهرست افزوده نمی‌شود. آن مقدار بازگشتی تزئینی نیست: یک دسته از هایلایت‌ها که از یک فایل بازبینی خارجی، یا از مختصات کهنه پس از جایگزینی یک صفحه بازسازی شده، می‌تواند بی‌سروصدا رکوردهایی را گم کند اگر فراخواننده آن را بررسی نکند

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

یک هایلایت هم‌تراز می‌ماند چون HotPDF آن را در فضای صفحه‌ی PDF ذخیره می‌کند و در هر رسم مجدد آن را دوباره به فضای صفحه‌نمایش تصویر می‌کند، به‌جای ذخیره‌ی یک مستطیل صفحه‌نمایشی که همان لحظه‌ای که سطح زوم تغییر کند کهنه می‌شود. THPDFViewerModel.PagePointToView و معکوس آن، ViewPointToPage، آن تصویرسازی را در دو مرحله انجام می‌دهند: ابتدا ورودی /Rotate خودِ صفحه، سپس ViewRotation مستقل نمایشگر، که هرگز به PDF بازنویسی نمی‌شود و فقط روی آنچه نمایشگر نشان می‌دهد اثر می‌گذارد. لغوکردن تبدیل هنگام رهاکردن ماوس همان دو مرحله را به‌ترتیب معکوس اجرا می‌کند، و این همان چیزی است که اجازه می‌دهد یک هایلایت کشیده‌شده در زوم بالا روی یک صفحه‌ی چرخیده‌ی ۲۷۰ درجه، دقیقاً در جای درست بنشیند پس از اینکه بازبین نما را به Fit Page بازنشاند

DPIای که برای آن تصویرسازی استفاده می‌شود به‌اندازه‌ی چرخش اهمیت دارد. نمایشگر HotPDF بلافاصله پس از هر رندر، DPI دقیق بیت‌مپ فعلی روی صفحه را در FRenderedDPI ثبت می‌کند، و ImageMouseUp همان مقدار را به ViewPointToPage می‌سپارد پس یک مختصات ماوس همیشه با استفاده از رزولوشنی که واقعاً با آن رسم شده تبدیل می‌شود، نه رزولوشنی که از ویژگی زوم فعلی دوباره محاسبه شده. CreatePageSnapshot و خویشاوندانش DPI را به بازه‌ی ۱۲ تا ۲۴۰۰ محدود می‌کنند، اما مسیر رندر تعاملی چنین سقفی ندارد: نردبان استاندارد زوم در ۶۴۰۰٪ به اوج می‌رسد، که در خط‌مبنای پیش‌فرض ۹۶ DPI به‌مراتب بیش از ۲۴۰۰ DPI محاسبه می‌شود، پس استفاده‌ی دوباره از یک محدودیت به‌سبک snapshot برای نگاشت مختصات، هر هایلایت را در اوج بازه‌ی زوم چند پیکسل جابه‌جا می‌کرد. دو پیش‌فرض کوچک‌تر این تعامل را کامل می‌کنند: کشیدنی کوتاه‌تر از دو پیکسل روی هر محور، به‌عنوان یک کلیک تلقی می‌شود و هیچ هایلایتی تولید نمی‌کند، و هایلایت‌کردن نمی‌تواند تا زمانی که حداقل یک صفحه واقعاً رندر شده باشد آغاز شود، چون FRenderedDPI از صفر شروع می‌شود

سیم‌کشی هایلایت تعاملی به یک صفحه‌ی بازبینی

روشن‌کردن هایلایت تعاملی یک کار سه-ویژگی روی خودِ کنترل THPDFViewer است: InteractionMode را به‌جای vimBrowse پیش‌فرض به vimHighlight تنظیم کنید، یک HighlightColor انتخاب کنید، که پیش‌فرضش clYellow است، و OnMarqueeSelect را برای فهمیدن اینکه بازبین همین الان چه چیزی کشیده مدیریت کنید. هر چیز دیگری، گرفتن ماوس، رسم مستطیل انتخاب نقطه‌چین درحالی‌که بازبین می‌کشد، تبدیل نقطه‌ی رهاسازی به فضای صفحه، فراخوانی AddHighlightRegion، پیش از شلیک آن رویداد درون کنترل رخ می‌دهد

type
  TReviewForm = class(TForm)
    Viewer: THPDFViewer;
    ReviewLog: TMemo;
    procedure FormCreate(Sender: TObject);
  private
    procedure ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
      PageIndex: Integer; const PageRect: THPDFRectangle;
      HighlightIndex: Integer);
  end;

// PdfDoc is a THotPDF already loaded elsewhere on the form
procedure TReviewForm.FormCreate(Sender: TObject);
begin
  Viewer.PDFDocument := PdfDoc;
  Viewer.InteractionMode := vimHighlight;
  Viewer.HighlightColor := clLime;
  Viewer.OnMarqueeSelect := ViewerMarqueeSelect;
end;

procedure TReviewForm.ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
  PageIndex: Integer; const PageRect: THPDFRectangle; HighlightIndex: Integer);
begin
  ReviewLog.Lines.Add(Format('page %d, mark #%d at (%.1f, %.1f)-(%.1f, %.1f)',
    [PageIndex + 1, HighlightIndex, PageRect.Left, PageRect.Bottom,
     PageRect.Right, PageRect.Top]));
end;

OnMarqueeSelect فقط برای کشیدنی که واقعاً یک هایلایت تولید کرده شلیک می‌شود: کلیکی که برای شمرده‌شدن به‌عنوان یک کشیدن خیلی کوچک است بلافاصله overlay انتخاب را پاک می‌کند، و کشیدنی که کاملاً خارج از صفحه فرود بیاید به AddHighlightRegion می‌رسد اما همان‌طور که یک فراخوانی برنامه‌ای رد می‌شد، آنجا هم رد می‌شود، پس رویداد در هر دو حالت خاموش می‌ماند. یک جزئیات پیاده‌سازی که اگر هایلایت‌کردن روزی در لبه‌های کنترل به پاسخ‌دادن قطع به نظر برسد ارزش دانستن دارد: مالکیت capture ماوس متعلق به خودِ THPDFViewer است، یک فرزند TScrollBox، نه به TImage داخلی‌ای که بیت‌مپ صفحه را نشان می‌دهد، و این همان چیزی است که اجازه می‌دهد بازبین از لبه‌ی صفحه‌ی رندرشده فراتر بکشد و همچنان یک رهاسازی تمیز دریافت کند

افزودن، حذف، و بازخوانی هایلایت‌ها از کد

هایلایت‌ها اصلاً لازم نیست از یک کشیدن ماوس بیایند. Viewer.HighlightRegion(PageIndex, PageRect, Color)، که به همان Model.AddHighlightRegionای که کشیدن تعاملی درون‌خود فراخوانی می‌کند سرازیر می‌شود، دقیقاً به این دلیل عمومی است که یک صفحه‌ی بازبینی بتواند هایلایت‌ها را از داده‌ای که از پیش دارد بازسازی کند: نظرات بارشده از یک پایگاه داده، نتایج از یک جستجوی متنی، یا نشانه‌های بازیابی‌شده از یک نشست پیشین. چون مختصات صرفاً اعداد ساده‌ی فضای کاربر PDF هستند، هیچ چیزی درباره‌ی این مسیر به رندرشدن اول یک صفحه بستگی ندارد، برخلاف کشیدن تعاملی که نیاز دارد FRenderedDPI از پیش یک مقدار واقعی داشته باشد

var
  I: Integer;
  Item: TPriorComment;    // your own record: PageIndex + PageRect
  NewIndex: Integer;
begin
  for I := 0 to PriorComments.Count - 1 do
  begin
    Item := TPriorComment(PriorComments[I]);
    NewIndex := Viewer.HighlightRegion(Item.PageIndex, Item.PageRect, clAqua);
    if NewIndex < 0 then
      LogWarning('comment %d fell outside the page and was dropped', [I]);
  end;
end;

حذف یک هایلایت تکی جایی است که ذخیره‌سازی مبتنی بر آرایه خودش را نشان می‌دهد. RemoveHighlightRegion یک رکورد را حذف می‌کند و هر رکورد بعدی را یک موقعیت به پایین جابه‌جا می‌کند تا شکاف را ببندد، که به این معناست که هر اندیسی که پیش‌تر گرفته شده، از یک رویداد OnMarqueeSelect یا از یک شمارش پیشین، به‌محض اینکه چیزی جلوتر از آن در فهرست حذف شود، دیگر قابل‌اعتماد نیست. OnHighlightChange در هر افزودن، حذف، و فراخوانی ClearHighlightRegions شلیک می‌شود، اما هیچ اطلاعاتی درباره‌ی اینکه چه چیزی تغییر کرده حمل نمی‌کند، پس الگوی امن این است که آن را نشانه‌ای برای بازسازی هر فهرستی که یک پنل بازبینی از HighlightCount و TryGetHighlightRegion نشان می‌دهد در نظر بگیرید، نه اینکه یک اندیس کش‌شده را در جا وصله کنید

procedure TReviewForm.ViewerHighlightChange(Sender: TObject);
var
  I: Integer;
  Mark: THPDFViewerHighlight;
begin
  MarkList.Items.Clear;
  for I := 0 to Viewer.Model.HighlightCount - 1 do
    if Viewer.Model.TryGetHighlightRegion(I, Mark) then
      MarkList.Items.AddObject(Format('page %d', [Mark.PageIndex + 1]),
        TObject(I));
end;

یک نشانه چه زمانی باید به‌جایش یک حاشیه‌نویسی Highlight واقعی شود؟

یک ناحیه‌ی هایلایت دقیقاً همان لحظه‌ای باید به یک حاشیه‌نویسی واقعی تبدیل شود که نیاز دارد بیرون از آن یک نمونه‌ی THPDFViewer دوام بیاورد. HotPDF همچنین AddHighlightAnnotation را برای یک صفحه‌ی جدید و AddLoadedHighlightAnnotation را برای یک سند از پیش بارشده در معرض دید می‌گذارد، و علی‌رغم نام تقریباً یکسان، این یک سازوکار کاملاً متفاوت است: هر دو یک حاشیه‌نویسی واقعی از نوع text-markup مطابق ISO 32000-1 §12.5.6.10 می‌نویسند، PDF از نوع /Subtype /Highlight، درون آرایه‌ی /Annots صفحه، با /QuadPoints که دقیقاً دنباله‌ی glyph را نشانه‌گذاری می‌کند، و هر نمایشگر PDF مطابق با استاندارد، به‌محض ذخیره‌شدن فایل آن را رندر می‌کند، نه فقط خودِ HotPDF. همان مرز سازوکار تصمیم می‌گیرد آیا یک نشانه از طریق XFDF رفت‌وبرگشت می‌کند: یک حاشیه‌نویسی ساخته‌شده با AddLoadedHighlightAnnotation یک شیء عادی PDF است که ExportLoadedAnnotationsToXFDF آن را برمی‌دارد و به‌عنوان نشانه‌گذاری ISO 19444-1 به Acrobat یا یک ابزار بازبینی دیگر می‌سپارد، که در وارد و صادرکردن حاشیه‌نویسی‌های PDF به‌عنوان XFDF در Delphi پوشش داده شده، درحالی‌که یک ناحیه‌ی افزوده‌شده از طریق AddHighlightRegion برای آن export نامرئی است چون اصلاً هرگز به گراف شیء نوشته نشده: فقط تا زمانی که THPDFViewerModelای که آن را ساخته وجود دارد، وجود دارد. خانواده‌ی کامل انواع حاشیه‌نویسی markup و هندسی موجود روی یک صفحه، و اینکه یک مستطیل چطور هرکدام را جای می‌دهد، در مقاله‌ی حاشیه‌نویسی‌های PDF در Delphi با HotPDF پوشش داده شده، و قاعده‌ی عملی ساده است: یک نشانه را قابل‌دورانداختن نگه دارید تا زمانی که یک سند هنوز در حال بحث است، و آن را زمانی که یک تصمیم نهایی شد، به یک حاشیه‌نویسی متعهد کنید

لایه‌ی هایلایت کجا متوقف می‌شود

لایه‌ی هایلایت، از سوی خودش، هیچ تلاشی نمی‌کند شبیه یک قلم هایلایتر نیمه‌شفاف به‌نظر برسد: RefreshDocument هر ناحیه را به‌عنوان یک مستطیل خط‌دور دو-پیکسلی با رنگ خودش روی بیت‌مپ کش‌شده‌ی صفحه رسم می‌کند، دقیقاً همان‌طور که نتایج جستجو را رسم می‌کند، به‌جای اینکه یک پرشدگی رنگی را روی متن زیرینش ترکیب کند، پس یک ظاهر کلاسیک شستشوی زرد باید در کد برنامه رسم شود یا به یک جریان ظاهری حاشیه‌نویسی‌ ارتقایافته واگذار شود. یک قابلیت که ارزش استفاده‌ی دوباره دارد به‌محض اینکه یک ناحیه وجود دارد CreateCurrentPageRegionSnapshot است، که همان THPDFRectangleای که یک هایلایت از پیش حمل می‌کند را می‌گیرد و فقط آن ناحیه را به یک بیت‌مپ رندر می‌کند، مفید برای پیوست‌کردن یک تصویر پیش‌نمایش کوچک به یک نظر بازبینی بدون export کردن کل صفحه. یک ساخت بازبینی مجبور نیست از قبل بین این دو سازوکار انتخاب کند: هر نشانه‌ی جدید را به‌طور پیش‌فرض به یک ناحیه‌ی قابل‌دورانداختن THPDFViewerHighlight نگه دارید تا زمانی که یک رشته‌ی نظر باز می‌ماند، و فقط زمانی که یک بازبین آن را حل کند AddLoadedHighlightAnnotation را فراخوانی کنید، که PDF بارشده را در طول رفت‌وبرگشتی که بیشترین تغییر را تولید می‌کند دست‌نخورده نگه می‌دارد. کنترل نمایشگر توصیف‌شده در اینجا بخشی از کامپوننت استاندارد HotPDF برای Delphi و C++Builder است، در کنار بقیه‌ی APIهای حاشیه‌نویسی و فرم که در بالا به آن‌ها ارجاع داده شد