مقاله فنی

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

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

مشکلی که این ویژگی حل می‌کند کاملاً ملموس است. یک ذخیره‌سازی کامل کل فایل را بازنویسی می‌کند: هر شیء دوباره سریال‌سازی می‌شود، هر آفست ارجاع متقابل (cross-reference) دوباره محاسبه می‌شود و خروجی هیچ رابطه بایتی با ورودی نخواهد داشت. برای یک فاکتور 40 کیلوبایتی این موضوع مشکلی نیست. اما برای یک آرشیو اسکن شده 2 گیگابایتی که فقط یک غلط املایی را در عنوان سند اصلاح کرده‌اید، بازنویسی دو گیگابایت برای تغییر بیست بایت مضحک است — و اگر فایل دارای امضای دیجیتال باشد، این بازنویسی آن را نابود می‌کند

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

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

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

نحوه عملکرد به‌روزرسانی‌های افزایشی تحت استاندارد ISO 32000-1 §7.5.6

نحوه عملکرد به‌روزرسانی‌های افزایشی تحت استاندارد ISO 32000-1 §7.5.6

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

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

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

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

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');  // key 3 = /Subject

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

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

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

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

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

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

خروجی افزایشی نیمی از داستان جریان‌سازی است؛ نیمه دیگر مربوط به اتفاقی است که در یک ذخیره کامل رخ می‌دهد. متد SaveToStream در کتابخانه losLab PDF سریال‌ساز سند را مستقیماً بر روی جریان هدف هدایت می‌کند، به جای اینکه ابتدا کل سند را در یک AnsiString واسط رندر کند و سپس آن بافر را در یک فراخوانی بنویسد. روش قدیمی‌تر کار می‌کرد، اما به این معنی بود که هر ذخیره کامل به طور گذرا کپی کامل دومی از خروجی را در حافظه نگه می‌داشت — در 10 مگابایت بی‌ضرر، در 500 مگابایت دردناک و یک دیوار سخت برای خروجی‌های چند گیگابایتی در فرآیندهای 32 بیتی. سریال‌سازی مستقیم باعث می‌شود که اوج مصرف حافظه به جای طول سریال‌سازی‌شده، ساختار اشیاء سند را دنبال کند

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;

درس حالت اشتراک‌گذاری: زمانی که AppendToFile مقدار 0 را برگرداند

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

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

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

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

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

جاسازی لازم است، اما کافی نیست

اصلاح فونت‌ها خطای 00030 را برطرف می‌کند و نه چیز دیگری. سندی که در PDF/A به دلیل رمزگذاری، فقدان متادیتای XMP، فضای رنگی وابسته به دستگاه بدون OutputIntent یا عدم وجود نقشه‌های ToUnicode رد می‌شود، حتی پس از جاسازی تمام فونت‌ها همچنان با شکست مواجه خواهد شد، به همین دلیل است که اصلاح باید درون یک حلقه مبتنی بر preflight قرار گیرد تا اینکه جایگزین آن شود. گزارش کامل را اجرا کنید، مواردی را که نام برده اصلاح کنید و اجازه دهید گزارش به شما بگوید چه زمانی کار تمام شده است. بعد هزینه نیز وجود دارد: یک برنامه فونت کامل CJK به چندین مگابایت می‌رسد، بنابراین جاسازی چندین مورد از آن‌ها می‌تواند یک سند کوچک را به طرز چشمگیری حجیم کند. راهکار متقابل، زیرمجموعه‌سازی (subsetting) است که در مقاله بهینه‌سازی حجم فایل PDF و زیرمجموعه‌سازی فونت پوشش داده شده است که برنامه جاسازی‌شده را فقط به گلیف‌هایی که سند واقعاً رندر می‌کند کاهش می‌دهد

جلوگیری از پسرفت اسناد جدید

متد SetEmbedAllFonts بخش پیشگیری از همین ویژگی است: یک محافظ در سمت نویسنده که مانع از تولید اسنادی توسط کد شما می‌شود که این مقاله آن‌ها را اصلاح می‌کند. با فعال بودن SetEmbedAllFonts(1)، هر فراخوانی بعدی AddTrueTypeFont که درخواست Embed=0 کند به یک ارجاع جاسازی‌شده ارتقا می‌یابد، که این امر تضمینی را که حالت PDF/A از قبل اعمال می‌کند به هر سند تعمیم می‌دهد. این متد بر فونت‌های اضافه‌شده پس از فراخوانی تاثیر می‌گذارد، نه فونت‌هایی که از قبل در فایل بارگذاری شده وجود دارند، بنابراین تقسیم وظایف مشخص است: SetEmbedAllFonts برای اسنادی که ایجاد می‌کنید، EmbedMissingFonts برای اسنادی که به ارث می‌برید

PDF.NewDocument;
PDF.SetEmbedAllFonts(1);
// از این به بعد، AddTrueTypeFont(Name, 0) مانند
// AddTrueTypeFont(Name, 1) رفتار می‌کند: هیچ ارجاع
// غیرجاسازی‌شدی نمی‌تواند به فایل خروجی راه یابد

به‌روزرسانی‌های افزایشی، خروجی دلتا با آفست مجازی و سریال‌سازی مستقیم در جریان همگی بخشی از استاندارد کتابخانه PDF losLab برای دلفی، سی‌شارپ و VB.NET هستند؛ صفحه محصول لیست کامل save و append API را در کنار ویژگی‌های امضا و فایل‌های بزرگ که در بالا بحث شد، نشان می‌دهد