مقاله فنی

به‌روزرسانی افزایشی PDF در دلفی: راهنمای AppendToStream

به‌روزرسانی‌های افزایشی PDF به یک برنامه دلفی اجازه می‌دهند سندی را با افزودن فقط اشیای تغییریافته تغییر دهد و هر بایت اصلی را دست‌نخورده بگذارد. losLab PDF Library این را از طریق AppendToStream پیاده‌سازی می‌کند، که فقط بخش افزایشی تعریف‌شده در ISO 32000-1 §7.5.6 را می‌نویسد، پس ویرایش یک نشانک در یک فایل ۲ GB به‌جای بازنویسی کامل، چند کیلوبایت خروجی هزینه دارد. همین مکانیزم دلیل آن است که سندهای امضاشده را می‌توان بدون بی‌اعتبارکردن امضاهایشان به‌روزرسانی کرد

دردی که این حل می‌کند ملموس است. یک ذخیره کامل کل فایل را بازنویسی می‌کند: هر شیء دوباره سریال می‌شود، هر افست ارجاع متقابل دوباره محاسبه می‌شود، و خروجی هیچ رابطه بایت‌به‌بایتی با ورودی ندارد. برای یک فاکتور ۴۰ KB این مشکلی نیست. برای یک آرشیو اسکن‌شده ۲ GB که فقط یک غلط تایپی را در عنوان سند اصلاح کرده‌اید، بازنویسی دو گیگابایت برای تغییر بیست بایت بی‌معنی است — و اگر فایل یک امضای دیجیتال داشت، بازنویسی همین حالا آن را نابود کرد

چرا ذخیره‌کردن یک PDF امضای دیجیتال آن را می‌شکند؟

یک امضای دیجیتال PDF محتوای منطقی سند را امضا نمی‌کند؛ بازه‌های بایتی فایل فیزیکی را امضا می‌کند. ورودی /ByteRange در دیکشنری امضا دقیقاً ثبت می‌کند که چکیده رمزنگاری کدام گستره‌های فایل را پوشش می‌دهد. هر عملیات ذخیره‌ای که آن بایت‌ها را دوباره سریال کند — حتی عملیاتی که سندی از نظر معنایی یکسان تولید کند — چکیده را تغییر می‌دهد، و هر اعتبارسنجی امضا را شکسته گزارش خواهد کرد. این عمدی است: امضا بایت‌هایی را که امضاکننده دیده گواهی می‌کند، نه یک مدل انتزاعی سند را

به‌روزرسانی‌های افزایشی راه خروجی هستند که مشخصات PDF فراهم می‌کند. چون یک ذخیره افزایشی داده جدید را پس از %%EOF اصلی می‌افزاید و هرگز به بازه‌های بایتی امضاشده دست نمی‌زند، امضای موجود همچنان در برابر بایت‌هایی که پوشش می‌دهد معتبر می‌ماند. اعتبارسنج‌ها سپس تغییرات افزوده‌شده را جداگانه دسته‌بندی می‌کنند — یک امضای دوم، پرکردن فرم، یک حاشیه‌نویسی — و تصمیم می‌گیرند که آیا تغییرات مجازی هستند یا نه. هر جریان‌کار چندامضایی به این وابسته است: هر امضاکننده یک بخش افزایشی روی بخش قبلی می‌افزاید. اگر خط‌لوله‌های امضا می‌سازید، مقاله همراه درباره امضا و اعتبارسنجی PAdES در دلفی به‌تفصیل پوشش می‌دهد که بازه‌های بایتی امضا و بخش‌های افزایشی چگونه با هم تعامل می‌کنند

PDF Library for Delphi: دیاگرام بازه بایتی که نشان می‌دهد چرا ذخیره کامل PDF امضاهای دیجیتال را بی‌اعتبار می‌کند در حالی که به‌روزرسانی‌های افزایشی AppendToStream آن‌ها را حفظ می‌کنند
چون چکیده بازه‌های بایتی ثابتی را پوشش می‌دهد، یک ذخیره کامل آن‌ها را به‌هم می‌ریزد و اعتبارسنجی را می‌شکند، در حالی که یک به‌روزرسانی افزایشی فراتر از ناحیه امضاشده می‌افزاید و هر امضای زنجیره‌ای زنده می‌ماند

به‌روزرسانی‌های افزایشی زیر ISO 32000-1 §7.5.6 چگونه کار می‌کنند

ISO 32000-1 §7.5.6 مدل را در سه قاعده تعریف می‌کند. اول، محتوای فایل اصلی کاملاً دست‌نخورده می‌ماند — حتی یک بایت جابه‌جا نمی‌شود. دوم، اشیای تغییریافته و تازه‌ساخته‌شده پس از آخرین %%EOF افزوده می‌شوند، هر کدام با همان شماره شیئی که پیش‌تر داشت (اشیای تغییریافته صرفاً تعریف جدیدتری می‌گیرند که تعریف قدیمی را می‌پوشاند). سوم، یک بخش ارجاع متقابل و trailer جدید افزوده می‌شود؛ ورودی /Prev در trailer به افست بایتی بخش ارجاع متقابل قبلی اشاره می‌کند و زنجیره‌ای می‌سازد که خواننده از جدیدترین به قدیمی‌ترین پیمایش می‌کند تا هر شیء را به تازه‌ترین تعریفش حل کند

دو ویژگی مفید از این ساختار نتیجه می‌شود. به‌روزرسانی‌ها به نسبت آنچه تغییر کرده ارزان‌اند، نه به نسبت اندازه سند — هزینه افزودن برابر اندازه اشیای تغییریافته به‌علاوه سربار کوچک xref/trailer است. و فایل تاریخچه نسخه‌های خودش می‌شود: هر بازبینی قبلی همچنان به‌طور فیزیکی حاضر است، پس یک ممیز می‌تواند فایل را در هر %%EOF قبلی برش بزند و دقیقاً سندی را که در آن نقطه وجود داشت بازیابی کند. برای جریان‌کارهای انطباق که باید ثابت کنند سند پیش از هر اصلاحیه چه شکلی بوده، این رد ممیزی داخلی اغلب استدلال تعیین‌کننده به نفع ذخیره‌های افزایشی است

نوشتن یک به‌روزرسانی افزایشی با AppendToStream

losLab PDF Library خروجی افزایشی را از طریق AppendToStream(AppendMode: Integer; OutStream: TStream): Integer در دسترس قرار می‌دهد که در صورت موفقیت 1 و در صورت شکست 0 برمی‌گرداند. پارامتر AppendMode انتخاب می‌کند چه چیزی در جریان مقصد فرود می‌آید. حالت 0 یک فایل کامل می‌نویسد: ابتدا بایت‌های منبع اصلی به جریان کپی می‌شوند، سپس بخش افزایشی افزوده می‌شود. حالت 1 فقط خودِ بخش افزایشی را می‌نویسد — دلتا — و بایت‌های منبع را کاملاً رد می‌کند. حالت 2 ابتدا پیشوندی را می‌نویسد که فراخواننده از طریق SetAppendInputFromString ثبت کرده، سپس بخش به‌روزرسانی را روی آن می‌افزاید

مقایسه حالت‌های 0، 1 و 2 در AppendToStream که بایت‌های منبع، پیشوند فراخواننده و بخش افزایشی را به یک جریان دلفی می‌نویسند
AppendMode انتخاب می‌کند جریان چه چیزی دریافت کند، از یک کپی کامل تا پیشوند فراخواننده به‌علاوه دلتا، و حالت 1 بخش افزایشی خالص و قابل‌حمل را تولید می‌کند
var
  Doc: TPDFlib;
  Delta: TMemoryStream;
begin
  Doc := TPDFlib.Create;
  try
    if Doc.LoadFromFile('contract.pdf', '') <= 0 then
      Exit;

    // ویرایش کوچک: از آن نوع تغییراتی که نباید
    // بازنویسی کل فایل را برانگیزد
    Doc.SetInformation(3, 'Amended 2026-07-04');  // کلید 3 = /Subject

    Delta := TMemoryStream.Create;
    try
      // AppendMode = 1: فقط بخش افزایشی را بنویس.
      // بایت‌های اصلی + Delta = یک PDF کامل و معتبر.
      if Doc.AppendToStream(1, Delta) = 1 then
        Delta.SaveToFile('contract.delta.bin');
    finally
      Delta.Free;
    end;
  finally
    Doc.Free;
  end;
end;

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

کتابخانه چگونه بدون کپی فایل اصلی افست‌های xref را محاسبه می‌کند؟

ورودی‌های ارجاع متقابل درون یک بخش افزایشی باید افست‌های بایتی مطلق داشته باشند — موقعیت‌هایی که از ابتدای فایل کامل اندازه‌گیری می‌شوند، نه از ابتدای دلتا. این برای حالت 1 معمایی می‌سازد: نویسنده هرگز بایت‌های اصلی را تولید نمی‌کند، با این حال هر افستی که ثبت می‌کند باید وانمود کند که آن‌ها آنجا هستند. losLab PDF Library این را با یک آداپتور جریان داخلی، TPDFAppendSectionStream، حل می‌کند که یک فضای مختصات مجازی به سریال‌ساز ارائه می‌دهد. آداپتور با طول بایتی فایل اصلی به‌عنوان افست پایه ساخته می‌شود، موقعیت و اندازه خود را به‌صورت آن پایه به‌علاوه هر چه تاکنون افزوده شده گزارش می‌دهد، و فقط بایت‌های تازه‌نوشته‌شده را به جریان مقصد فراخواننده ارسال می‌کند

پیامد این است که حالت 1 هرگز کپی‌ای از سند منبع را مادی نمی‌کند — نه روی دیسک، نه در حافظه. پیاده‌سازی ساده‌لوحانه (نوشتن فایل کامل در یک بافر موقت و سپس بریدن دنباله) یک کپی گذرا از کل PDF اصلی را حمل می‌کرد، که برای ورودی‌های مقیاس گیگابایتی دقیقاً همان هزینه‌ای است که به‌روزرسانی‌های افزایشی برای اجتناب از آن وجود دارند. این تکنیک مجازی‌سازی افست خویشاوند نزدیک جابه‌جایی ارجاع بایتی است که در جاهای دیگر کتابخانه به کار رفته؛ مقاله ادغام سریع PDF با جابه‌جایی ارجاع بایتی همان ایده را در ترکیب سندها نشان می‌دهد، و راهنمای ادغام و تقسیم PDF بزرگ با دسترسی مستقیم به فایل معماری I/O پیرامونی را برای فایل‌هایی که به‌راحتی در RAM جا نمی‌گیرند پوشش می‌دهد

ذخیره‌های کامل جریانی با SaveToStream

خروجی افزایشی نیمی از داستان جریان‌سازی است؛ نیمه دیگر آن چیزی است که در یک ذخیره کامل رخ می‌دهد. SaveToStream در losLab PDF Library سریال‌ساز سند را مستقیماً روی جریان مقصد می‌راند، به‌جای این‌که ابتدا کل سند را در یک AnsiString میانی رندر کند و سپس آن بافر را در یک فراخوانی بنویسد. رویکرد قدیمی‌تر کار می‌کرد، اما به این معنا بود که هر ذخیره کامل به‌طور گذرا یک کپی کامل دوم از خروجی را در حافظه نگه می‌داشت — در ۱۰ MB بی‌ضرر، در ۵۰۰ MB دردناک، و برای خروجی‌های چندگیگابایتی روی فرایندهای ۳۲ بیتی یک دیوار سخت. سریال‌سازی مستقیم باعث می‌شود اوج حافظه از ساختارهای شیئی سند پیروی کند نه از طول سریال‌شده آن

var
  Doc: TPDFlib;
  Output: TFileStream;
begin
  Doc := TPDFlib.Create;
  try
    if Doc.LoadFromFile('archive.pdf', '') <= 0 then
      Exit;

    // ... ویرایش‌هایی که بازنویسی کامل را توجیه می‌کنند ...

    Output := TFileStream.Create('archive-rewritten.pdf', fmCreate);
    try
      if Doc.SaveToStream(Output) = 0 then
        Writeln('Save failed, error ', Doc.LastErrorCode);
    finally
      Output.Free;
    end;
  finally
    Doc.Free;
  end;
end;

درسی درباره share mode: وقتی AppendToFile صفر برگرداند

یک رگرسیون در این حوزه ارزش بازگویی دارد چون الگوی خرابی آن تعمیم می‌یابد. AppendToFile(FileName) یک به‌روزرسانی افزایشی را مستقیماً به یک PDF موجود روی دیسک می‌افزاید — فراخوانی طبیعی برای یک جریان‌کار رد ممیزی درجا: فایلی را بارگذاری کنید، تغییری بدهید، به همان مسیر بیفزایید. در v3.71.2 دقیقاً همین توالی شروع کرد به برگرداندن 0. علت ریشه‌ای در بارگذار بود نه در نویسنده: برای پشتیبانی از خواندن درخواستی سندهای بزرگ، LoadFromFile هندل فایل منبع را برای طول عمر شیء سند باز نگه می‌دارد، و آن هندل با fmShareDenyWrite باز شده بود. وقتی AppendToFile سپس سعی می‌کرد همان فایل را برای نوشتن دوباره باز کند، share mode خودِ بارگذار آن را رد می‌کرد، و API پیش از نوشتن حتی یک بایت شکست می‌خورد

اصلاح، share mode بارگذار را به fmShareDenyNone شل کرد، که دقیقاً به دلیل ماهیت یک افزودن افزایشی ایمن است: بایت‌ها را اکیداً پس از انتهای فایل می‌افزاید و هرگز ناحیه‌ای را که هندل بلندعمر خواننده سرویس می‌دهد بازنویسی نمی‌کند. درس کلی برای هر کسی که این کتابخانه را می‌پیچد — یا بارگذارهای جریانی مشابه می‌سازد — این است که خواننده‌های تنبلِ هندل‌نگه‌دار و نویسنده‌های همان‌فایل با هم در تنش‌اند، و share mode که هنگام باز کردن انتخاب می‌کنید یک قرارداد API است، نه یک جزئیات پیاده‌سازی. اگر AppendToFile در کد شما زمانی 0 برگرداند، ابتدا بررسی کنید که آیا چیز دیگری در فرایند شما هنوز فایل مقصد را با یک share mode محدودکننده نگه داشته یا نه

هزینه‌های صادقانه: وقتی به‌روزرسانی افزایشی ابزار اشتباهی است

به‌روزرسانی‌های افزایشی اندازه فایل را با کارایی نوشتن معاوضه می‌کنند، و این معاوضه همیشه به نفع شما نیست. هر بازبینی اشیای تغییریافته خود را می‌افزاید در حالی که تعریف‌های جایگزین‌شده در فایل می‌مانند، پس سندی که صدها بار ویرایش شده اشیای مرده و یک زنجیره /Prev طولانی انباشته می‌کند که هر خواننده باید آن را پیمایش کند. بدتر این‌که محتوای «حذف‌شده» از بین نرفته است: متنی که در بازبینی پنجم حذف شده هنوز به‌طور فیزیکی در بایت‌های بازبینی چهارم حاضر است و هر کسی که فایل را برش بزند می‌تواند آن را بازیابی کند. بنابراین سانسور (redaction)، پاک‌سازی یا هر حذف محتوای حساس یک بازنویسی کامل می‌طلبد — ذخیره افزایشی یک سانسور، نشت داده با چند مرحله اضافی است

ذخیره کامل همچنین انتخاب درست است وقتی هدف فشرده‌سازی است (بیرون‌راندن افزایش‌های انباشته و اشیای استفاده‌نشده)، وقتی ویژگی‌های سراسری سند مانند رمزگذاری را تغییر می‌دهید — رمزگذاری دوباره به هر رشته و جریان دست می‌زند، پس چیزی «افزایشی» در این تغییر باقی نمی‌ماند — یا وقتی یک تحویلی تمیز تولید می‌کنید که تاریخچه ویرایش نباید همراه فایل سفر کند. یک قاعده معقول: تا زمانی که سند زنده و در حال تغییر است، به‌ویژه وقتی امضا دارد، از AppendToStream یا AppendToFile استفاده کنید؛ در مرزهای چرخه حیات، وقتی سند سیستم شما را ترک می‌کند یا تاریخچه‌اش باید مسطح شود، از یک بازنویسی کامل با SaveToStream استفاده کنید

راهنمای تصمیم PDF Library for Delphi برای انتخاب میان ذخیره‌های افزایشی AppendToStream و بازنویسی‌های کامل SaveToStream در طول چرخه حیات سند
تا زمانی که سند زنده و امضاشده است بیفزایید، و هر وقت بایت‌ها باید ناپدید شوند، تاریخچه باید مسطح شود یا رمزگذاری تغییر می‌کند به بازنویسی کامل سوئیچ کنید

به‌روزرسانی‌های افزایشی، خروجی دلتا با افست مجازی و سریال‌سازی مستقیم به جریان همگی بخشی از losLab PDF Library استاندارد برای Delphi، C# و VB.NET هستند؛ صفحه محصول سطح کامل API ذخیره و افزودن را در کنار ویژگی‌های امضا و فایل بزرگ که در بالا بحث شد فهرست می‌کند