مقاله فنی

متن کهنه پس از ویرایش: کش FPDF_TEXTPAGE در PDFium

شما AddText را برای مهرزدن یک خط روی یک صفحه‌ی PDF با PDFiumPas فرا می‌خوانید، سپس بلافاصله FindFirst را فرا می‌خوانید تا تأیید کنید مهر فرود آمده، و جستجو خالی برمی‌گردد. متن روی صفحه است — Acrobat آن را نشان می‌دهد — اما کامپوننت TPdf در PDFiumPas یک ساختار جداگانه‌ی کش‌شده‌ی FPDF_TEXTPAGE نگه می‌دارد، یک‌بار از جریان محتوای صفحه تجزیه‌شده، و یک ویرایش خودش به‌طور گذشته‌نگر آن ساختار را به‌روز نمی‌کند. آن را پیش از اینکه refresh شود پرس‌وجو کنید و دقیقاً همان‌طور که پیش از تغییرتان به‌نظر می‌رسید صفحه را می‌خوانید، نه پس از آن

چرا PDFium بلافاصله پس از یک ویرایش متن کهنه برمی‌گرداند؟

PDFiumPas موتور رندر PDFium گوگل را برای Delphi و C++Builder می‌پیچد، و فراخوانی‌های متن و ویرایش آن به دو زیرسیستم متفاوت درون آن موتور دست دراز می‌کنند. FPDF_TEXTPAGE متعلق به سمت خواندن است: FPDFText_LoadPage یک‌بار جریان محتوای صفحه را می‌پیماید و صفحه‌ی متن را می‌سازد — کدهای نویسه، موقعیت‌ها، متریک فونت، مرزهای واژه — و PDFiumPas آن ساختار را تا زمانی که صفحه بارشده باقی می‌ماند کش نگه می‌دارد. فراخوانی‌های ویرایش مثل FPDFPage_InsertObject یا FPDFPage_GenerateContent روی نمایشی کاملاً متفاوت عمل می‌کنند، گراف شیء و جریان-محتوای صفحه، و PDFium آن تغییرات را خودش به یک صفحه‌ی متن از پیش بازشده هُل نمی‌دهد. بازسازی آن در هر ویرایش، ویرایش دسته‌ای را به‌شکل غیرقابل‌قبولی کند می‌کرد، پس طراحی به‌جای آن آن هزینه را با یک قاعده معامله می‌کند — هر کسی که handle را در اختیار دارد، پس از یک ویرایش محتوا-تغییردهنده آن را می‌بندد، و خواندن بعدی یکی تازه می‌سازد

درون کش متن TPdf: FTextPage، LoadTextPage، و UnloadTextPage

TPdf handle کش‌شده را در یک فیلد خصوصی تکی، FTextPage، پیگیری می‌کند و چرخه‌ی عمرش را در دو متد می‌پیچد. LoadTextPage بررسی می‌کند آیا FTextPage برابر nil است و، فقط در آن حالت، FPDFText_LoadPage را در برابر صفحه‌ی فعلی فرا می‌خواند؛ اگر یک handle از پیش وجود داشته باشد، LoadTextPage آن را دوباره استفاده می‌کند بدون پرسیدن اینکه آیا صفحه از زمان ساخته‌شدنش تغییر کرده. نیمه‌ی دیگر UnloadTextPage است: این handle بومی را با FPDFText_ClosePage می‌بندد، FTextPage را به nil برمی‌گرداند، و فهرست لینک-وب کش‌شده و هر نشست جستجوی در-حال-پیشرفت را هم رها می‌کند، چون هر دو از همان صفحه‌ی متن مشتق شده بودند و به همان دلیل کهنه می‌شوند

رفتار دوباره-استفاده-بدون-بررسی در LoadTextPage دقیقاً همان دلیلی است که ترتیب اهمیت دارد. هر پرس‌وجوی متن روی TPdfText، FindFirst، GetWebLinks — ابتدا از طریق LoadTextPage سرازیر می‌شود، پس تا زمانی که FTextPage همچنان handle پیش-از-ویرایش را نگه می‌دارد، هیچ‌کدام از آن فراخوانی‌ها هیچ راهی برای فهمیدن اینکه یک تغییر رخ داده ندارند. ناوبری صفحه هرگز ریسک اینجا نبوده: UnloadPage، که روی سوییچ‌های صفحه، دوباره‌بارگذاری‌ها، و بسته‌شدن سند اجرا می‌شود، همیشه صفحه‌ی متن را همراه با خودِ صفحه بسته است. سؤال باز همیشه درباره‌ی ویرایش‌های اعمال‌شده به صفحه‌ای بوده که همچنان روی آن نشسته‌اید

کدام متدهای PDFiumPas کش را به‌طور خودکار refresh می‌کنند؟

متدهای ویرایش-صفحه‌ی خودِ TPdfAddText، SetText، SetTextPositions، AddPath، RemoveObject، و InsertFormObjectFromXObject — هرکدام پیش از فراخوانی UpdatePage (‏FPDFPage_GenerateContent در PDFium) برای serialize‌کردن تغییر درون جریان محتوا، UnloadTextPage را فرا می‌خوانند. هرکدام از این‌ها را فراخوانی کنید و همان فراخوانی بعدی Text، FindFirst، یا GetWebLinks صفحه‌ی متن را از محتوایی که اکنون ایستاده دوباره می‌سازد، بدون هیچ فراخوانی اضافه‌ای از سمت شما

var
  Pdf: TPdf;
  Index: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    Pdf.AddText('Reviewed by J. Alvarez', 'Helvetica', 10, 72, 40, clBlack, 255, 0);
    // AddText already closed the cached text page, so this FindFirst
    // call rebuilds it fresh before it searches
    Index := Pdf.FindFirst('Reviewed by J. Alvarez');
    if Index >= 0 then
      ShowMessage('Stamp confirmed at character ' + IntToStr(Index));
  finally
    Pdf.Free;
  end;
end;

الگویی که همچنان می‌شکند: کش‌کردن handle خام TextPage

TPdf handle زنده را از طریق یک ویژگی فقط-خواندنی TextPage در معرض دید می‌گذارد، برای حالت نادری که نیاز دارید یک تابع FPDFText_* را که PDFiumPas نپیچیده فرا بخوانید. آن راه‌فرار دقیقاً همان‌جایی است که ابطال‌سازی خودکار نمی‌تواند کمک کند: به‌محض اینکه مقدار FPDF_TEXTPAGE را از ویژگی به یک متغیر محلی کپی کنید، PDFiumPas هیچ راهی برای فهمیدن اینکه همچنان آن را در اختیار دارید ندارد، و هیچ راهی برای به‌روزکردن کپی شما وقتی UnloadTextPage جای دیگری در کد شما اجرا می‌شود

var
  Pdf: TPdf;
  RawHandle: FPDF_TEXTPAGE;
  StaleCount: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    RawHandle := Pdf.TextPage;    // FPDFText_LoadPage handle, cached in FTextPage
    Pdf.SetText(0, 'Amended Clause 4.2');
    // SetText already closed RawHandle and set Pdf.TextPage back to nil.
    // Calling any FPDFText_* function against the old value now touches a
    // handle PDFium has already freed — undefined behavior, not a bug you
    // can catch with a nil check
    StaleCount := FPDFText_CountChars(RawHandle);
  finally
    Pdf.Free;
  end;
end;

استفاده از یک handle پس از اینکه FPDFText_ClosePage روی آن اجرا شده، در خودِ PDFium یک رفتار تعریف‌نشده است، نه یک قرارداد PDFiumPas که می‌توانید نادیده بگیرید — ممکن است آخرین-داده‌ی-شناخته‌شده را برگرداند، چیزی برنگرداند، یا فرآیند را کرش کند، و اینکه کدام‌یک روی یک ساخت مشخص اتفاق می‌افتد چیزی نیست که کد برنامه باید به آن بستگی داشته باشد. قاعده‌ی امن محدود است: Pdf.TextPage را تازه بخوانید، بلافاصله پیش از فراخوانی FPDFText_*ای که به آن نیاز دارد، و هرگز یک کپی را در سراسر یک عبارتی که ممکن است صفحه را ویرایش کند نگه ندارید

ویرایش‌های خود را دسته‌بندی کنید، سپس یک‌بار پرس‌وجو کنید

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

var
  Pdf: TPdf;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'watermarked.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    // Strip every text object that looks like a draft watermark. Each
    // RemoveObject call already invalidates the cache on its own, so
    // nothing needs refreshing by hand between iterations
    for I := Pdf.ObjectCount - 1 downto 0 do
      if (Pdf.ObjectType[I] = otText) and (Pdf.ObjectBounds[I].Top > 700) then
        Pdf.RemoveObject(I, True);

    // Query once, after the whole batch is done, not once per removal
    if Pdf.FindFirst('DRAFT') < 0 then
      ShowMessage('Watermark cleared');
  finally
    Pdf.Free;
  end;
end;

همان منطق دسته‌بندی به‌طور مشخص برای وضعیت جستجو هم اعمال می‌شود. FindNext و FindPrevious نشستی را که با FindFirst شروع شده ادامه می‌دهند، و آن نشست همراه با هر چیز دیگری با UnloadTextPage فروپاشیده می‌شود، پس فراخوانی دوباره‌ی FindNext پس از یک ویرایش — به‌جای فراخوانی دوباره‌ی FindFirst — به‌جای اینکه بی‌سروصدا یک جستجو را در برابر محتوایی که دیگر وجود ندارد از سر بگیرد، یک استثنا raise می‌کند. هر ویرایشی را به‌عنوان یک مرز سخت برای هم محتوای متن و هم موقعیت جستجو در نظر بگیرید، و اجازه دهید یک FindFirst تازه در سمت دیگر ویرایش‌هایتان جستجو را دوباره بردارد

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

استخراج متن ساده — خواندن متن یک صفحه بدون تغییردادن هیچ‌چیزی — هرگز با هیچ‌کدام از این‌ها روبه‌رو نمی‌شود، چون هیچ چیزی handleای را که هیچ ویرایشی لمسش نکرده باطل نمی‌کند. برای اینکه Text، مستطیل‌های نویسه، و مرزهای واژه روی یک صفحه‌ی تغییرنیافته چطور کار می‌کنند، مقاله‌ی همراه درباره‌ی استخراج متن با PDFiumPas آن زمینه را بدون چرخه‌ی عمر کش صفحه‌ی متنی که این مقاله بالای آن می‌افزاید پوشش می‌دهد

چرخه‌ی عمر کش بیشترین اهمیت را در گردش‌کارهایی دارد که ویرایش می‌کنند و سپس بلافاصله روی نتیجه عمل می‌کنند: مهرزدن یک تصحیح و جستجوی آن، سیاه‌کردن یک پاراگراف و تأیید اینکه رفته، یا یافتن یک عبارت برای لنگرزدن یک حاشیه‌نویسی markup درست پس از درج متن نزدیک آن. آن مورد آخر ارزش پرچم‌گذاری جداگانه دارد — حاشیه‌نویسی‌های markup با quad-point از مستطیل‌های نویسه‌ای که از صفحه‌ی متن خوانده شده موقعیت‌گذاری می‌شوند، پس یک حاشیه‌نویسی ساخته‌شده از مختصات ثبت‌شده پیش از یک ویرایش، به‌محض فرودآمدن ویرایش، جای اشتباهی را هایلایت می‌کند

APIهای ویرایش و متن TPdf بخشی از کامپوننت PDFium برای Delphi و C++Builder هستند، و صفحه‌ی محصول مرجع کامل متد را برای سطوح ویرایش، استخراج، و جستجوی پوشش‌داده‌شده در اینجا حمل می‌کند