مقاله فنی

ویرایش فراداده PDF بارگذاری‌شده در Delphi بدون بازنویسی

شما ده‌هزار PDF قراردادی از دوازده مولد مختلف دارید و تیم حقوقی می‌خواهد هر کدامشان عنوان درست Author، یک رشته‌ی Producer اصلاح‌شده و یک حالت خواندن داشته باشد که در شروع پنل نشانک‌ها را باز کند. راه‌حل ساده‌لوحانه این است که هر فایل را بارگذاری کنید، صفحه‌ها را دوباره بچینید و یک سند تازه بنویسید. اگر این کار را بکنید، تمام شماره‌های شیء موجود، تاریخچه به‌روزرسانی افزایشی، هر امضای دیجیتال، و xref دقیق و تنظیم‌شده‌ای را که ابزار اصلی ساخته بود از دست داده‌اید. صفحه‌ها یکسان به نظر می‌رسند، اما فایل از نظر ساختاری دیگر همان فایل نیست. برای یک ویرایش فراداده، این از هر نظر معامله‌ای نادرست است

راه درست این است که سند بارگذاری‌شده را مثل یک گراف شیء در نظر بگیرید که درجا آن را تغییر می‌دهید: به دیکشنری Info، /Metadata stream، و Catalog دسترسی پیدا کنید، چند ورودی موردنظرتان را عوض کنید و نتیجه را برگردانید. HotPDF، مؤلفه بومی VCL برای Delphi و C++Builder، دقیقاً همین سطح را از طریق API نوشتن سند بارگذاری‌شده در اختیار می‌گذارد. این مقاله درباره استفاده درست از آن است، و درباره تنها اشتباهی که تقریباً همه مرتکب می‌شوند: ویرایش دیکشنری Info و فراموش کردن اینکه یک نسخه دوم از همان فراداده در XMP زندگی می‌کند

دو جا همان فراداده را نگه می‌دارند، و با هم ناسازگارند

PDF اطلاعات سند را در دو محل موازی نگه می‌دارد، و همین ریشه بیشتر تیکت‌های «عنوان را عوض کردم اما Acrobat هنوز قدیمی را نشان می‌دهد» است. اولی دیکشنری اطلاعات سند است، همان /Info شیء کلاسیک با /Title، /Author، /Subject، /Keywords، /Creator، /Producer کلیدها، که در ISO 32000-1 §14.3.3 تعریف شده‌اند. دومی یک بسته XMP است، یک سند XML که به‌صورت یک stream از Catalog و زیر /Metadata آویزان است، در §14.3.2 تعریف شده و بر پایه مدل داده Adobe XMP ساخته شده است

هر دو می‌توانند عنوان را نگه دارند. هیچ چیز در مشخصات آن‌ها را مجبور نمی‌کند با هم موافق باشند. نمایشگرهای مدرن و بیشتر اعتبارسنج‌های PDF/A وقتی XMP موجود باشد آن را ترجیح می‌دهند و وقتی نباشد به دیکشنری Info برمی‌گردند. پس اگر فقط /Info را به‌روزرسانی کنید ـ که دقیقاً همان کاری است که بیشتر کدهای «set PDF metadata» انجام می‌دهند ـ خواننده‌ای که به XMP اعتماد می‌کند همچنان مقدار قدیمی را نشان می‌دهد و بررسی‌کننده PDF/A ناسازگاری را علامت می‌زند. عملیات درست روی هر فایلی که از قبل یک بسته XMP دارد، نوشتنِ دوگانه است: ورودی Info را تغییر دهید و XMP را دوباره تولید کنید تا این دو هم‌ساز بمانند. HotPDF هر دو نیمه را در اختیار شما می‌گذارد؛ انضباط استفاده هم‌زمان از آن‌ها با شماست

ویرایش دیکشنری Info

کمک‌برنامه‌های سمت Info ساده و قابل‌پیش‌بینی‌اند. SetLoadedTitle، SetLoadedAuthor، SetLoadedSubject، SetLoadedKeywords، SetLoadedCreator و SetLoadedProducer هر کدام یک AnsiString رشته را می‌گیرند و کلید متناظر را در دیکشنری Info بارگذاری‌شده می‌نویسند، و اگر کلید وجود داشته باشد مقدارش را جایگزین می‌کنند و اگر نه، آن را اضافه می‌کنند. برای حذف کامل یک کلید - مثلاً یک /Creatorکلید نشت‌دارِ RemoveLoadedInfoKey که نام ابزار داخلی شما را لو می‌دهد - با نام خام کلید فرا بخوانید. هیچ‌کدام از این‌ها به XMP دست نمی‌زنند؛ آن‌ها فقط روی /Info شیء LoadFromFile که هنگام تجزیه فایل پیدا کرد

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract-in.pdf', '') > 0 then
    begin
      Pdf.SetLoadedTitle('Master Services Agreement 2026');
      Pdf.SetLoadedAuthor('Legal Department');
      Pdf.SetLoadedSubject('Executed contract, retention 7 years');
      Pdf.SetLoadedKeywords('contract; MSA; 2026; executed');
      Pdf.SetLoadedProducer('Acme Document Pipeline');
      Pdf.RemoveLoadedInfoKey('Creator');  // drop the originating tool name
      Pdf.SaveLoadedDocument('contract-out.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

یک نکته را دقیق نگه دارید: این‌ها AnsiStringرشته می‌گیرند. برای عنوان‌های ASCII این مسئله اهمیتی ندارد، اما رشته‌های متنی PDF که به نویسه‌های غیرلاتین نیاز دارند باید مطابق مشخصات کدگذاری شوند - UTF-16BE با نشانگر ترتیب بایت، یا PDFDocEncoding - پیش از آنکه به آن‌ها بدهید. کتابخانه بایت‌هایی را که تحویلش می‌دهید داخل یک شیء رشته می‌نویسد؛ برای شما حدس نمی‌زند که کدام کدگذاری درست است. اگر عنوان‌های شما ساده و انگلیسی‌اند، این را نادیده بگیرید. اگر شامل نویسه‌های دارای لهجه یا CJK هستند، با دقت کدگذاری کنید و در یک نمایشگر واقعی آزمایش بگیرید

بازنویسی بسته XMP

SetLoadedXMPMetadata ادامهٔ نیمهٔ دیگرِ نوشتنِ دوگانه است. کل بسته XMP را به‌صورت یک AnsiString بدهید و یکی از دو کار را انجام می‌دهد: اگر Catalog از قبل به یک /Metadata stream ارجاع داده باشد، محتوای همان stream را درجا جایگزین می‌کند و همان شماره شیء را نگه می‌دارد؛ اگر هیچ stream فراداده‌ای وجود نداشته باشد، یکی می‌سازد، آن را /Type /Metadata و /Subtype /XML علامت می‌زند، یک شماره شیء اختصاص می‌دهد و آن را از Catalog پیوند می‌دهد. در هر صورت، در نهایت یک شیء فراداده معتبر دارید که نمایشگرها آن را می‌خوانند

شما XML را می‌دهید، یعنی schema را خودتان کنترل می‌کنید - dc:title, dc:creator, xmp:CreatorTool, و همین‌طور ادامه. این هم قدرت است و هم مسئولیت: کتابخانه بسته شما را parse یا validate نمی‌کند، و بایت‌ها را بدون فشرده‌سازی و بدون اعمال هیچ stream filterی می‌نویسد. یک بسته معیوب از این فراخوانی رد می‌شود و بعدتر به شکل شکایتِ فراداده خراب بیرون می‌زند. XML را با دقت بسازید، و دقیقاً همان مقادیری را که در دیکشنری Info نوشته‌اید در آن بازتاب دهید تا این دو نما هرگز با هم تناقض نداشته باشند

const
  XMP_TEMPLATE =
    '<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>' +
    '<x:xmpmeta xmlns:x="adobe:ns:meta/">' +
    '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">' +
    '<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">' +
    '<dc:title><rdf:Alt><rdf:li xml:lang="x-default">%s</rdf:li></rdf:Alt></dc:title>' +
    '<dc:creator><rdf:Seq><rdf:li>%s</rdf:li></rdf:Seq></dc:creator>' +
    '</rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end="w"?>';
begin
  // After setting the Info dictionary, mirror the same values into XMP:
  Pdf.SetLoadedTitle('Master Services Agreement 2026');
  Pdf.SetLoadedAuthor('Legal Department');
  Pdf.SetLoadedXMPMetadata(
    AnsiString(Format(XMP_TEMPLATE,
      ['Master Services Agreement 2026', 'Legal Department'])));
  Pdf.SaveLoadedDocument('contract-out.pdf');
end;

آن ترتیب - اول Info، دوم XMP، بعد ذخیره - الگویی است که باید در ذهن بماند. این دو فراخوان مستقل‌اند؛ سازگاری فقط به این دلیل وجود دارد که همان رشته‌ها را به هر دو داده‌اید. اگر در فایلی که بسته XMP دارد فراخوان XMP را حذف کنید، به همان باگ سکوتِ قدیمی برمی‌گردید که این بخش برای جلوگیری از آن نوشته شده است

Diagram showing a PDF Info dictionary and an XMP metadata stream both holding title and author, edited in place alongside the bookmark outline tree
فراداده در دو جا زندگی می‌کند - دیکشنری Info و stream XMP - به‌علاوه اشاره‌های سطح Catalog برای نحوه باز شدن و درخت outline. ویرایش درجا هر کدام را لمس می‌کند بدون آنکه سند را از نو بسازد.

هدایتِ نحوه باز شدن فایل توسط نمایشگر

سه ورودی Catalog تعیین می‌کنند خواننده درست در لحظه باز شدن سند چه می‌بیند، و هر سه ویرایش یک‌خطی روی گراف بارگذاری‌شده‌اند. SetLoadedPageMode می‌نویسد /PageMode را به‌صورت یک نام‌شیء: 'UseOutlines' را بدهید تا پنل نشانک‌ها باز شود، 'UseThumbs' برای نوار بندانگشتی‌ها، 'FullScreen' برای حالت ارائه، یا 'UseAttachments' برای نشان دادن پنل پیوست‌ها (ISO 32000-1 §7.7.3.1، جدول 28). SetLoadedPageLayoutمی‌نویسد /PageLayout به همان شکل - 'SinglePage', 'OneColumn', 'TwoColumnLeft', و بقیه. هر دو، نام را بدون اسلش پیشرو می‌گیرند؛ کتابخانه آن را هنگام خروج اضافه می‌کند

SetLoadedLanguageمی‌نویسد ورودی Catalog /Lang، برچسب زبان طبیعی برای کل سند - 'en-US', 'de-DE', برچسب BCP 47. به تفاوت نوعی که معمولاً آدم‌ها را به اشتباه می‌اندازد دقت کنید: /PageMode و /PageLayout شیءهای PDF نام هستند، در حالی که /Lang یک رشته است. HotPDF این را در داخل درست انجام می‌دهد، اما اگر خروجی را نگاه کنید، /PageMode /UseOutlines را در برابر /Lang (en-US) خواهید دید، و حالا می‌دانید چرا. ورودی /Lang از چیزی که به نظر می‌رسد مهم‌تر است: این همان چیزی است که فناوری کمکی برای انتخاب تلفظ می‌خواند و یک الزام سخت برای انطباق دسترسی‌پذیری PDF/UA است

if Pdf.LoadFromFile('handbook.pdf', '') > 0 then
begin
  Pdf.SetLoadedPageMode('UseOutlines');     // /PageMode, a name
  Pdf.SetLoadedPageLayout('TwoColumnLeft'); // /PageLayout, a name
  Pdf.SetLoadedLanguage('en-US');           // /Lang, a string
  Pdf.SaveLoadedDocument('handbook-tagged.pdf');
end;

تغییر نام نشانک‌ها بدون برهم زدن درخت

عنوان نشانک‌ها از کارهای معمول نظافت است - یک غلط املایی در یک سرفصل، یا شماره‌گذاری دوباره یک فصل بعد از ساخت outline. SetLoadedOutlineTitle یک شاخص صفرمبنا را در سطح بالا ورودی‌های outline و یک عنوان جدید می‌گیرد، زنجیره Catalog → /Outlines/First/Next رشتهٔ /Title را عوض می‌کند. فقط عنوان را تغییر می‌دهد؛ مقصد، حالت باز/بسته بودن و ساختار فرزندان دست‌نخورده می‌مانند

if Pdf.LoadFromFile('report.pdf', '') > 0 then
begin
  Pdf.SetLoadedOutlineTitle(0, 'Executive Summary');
  Pdf.SetLoadedOutlineTitle(1, 'Financial Results');
  Pdf.SaveLoadedDocument('report-renamed.pdf');
end;

تغییر نام دقیقاً از این جهت امن است که هرگز به شمارنده‌های ساختاری دست نمی‌زند. حذف یک ورودی outline همان جایی است که مشکل‌ساز می‌شود، و حتی وقتی فقط دارید تغییر نام می‌دهید، فهمیدن آن ارزش دارد، چون به شما می‌گوید چه چیزی را نباید دستی ویرایش کنید. هر گره outline یک /Count دارد، و - طبق ISO 32000-1 §12.3.3 - آن شمارش، تعداد فرزندان مستقیم نیست. بلکه تعداد کل نوادگان قابل‌نمایش یک /Count مثبت برابر N یعنی اکنون N نواده در معرض دید هستند، و مقدار منفی یعنی گره نوادگانی دارد اما جمع شده است. وقتی یک ورودی سطح بالا حذف می‌شود، شمارش ریشهٔ /Outlinesنمی‌تواند صرفاً یک واحد کم شود؛ باید برای هر گره سطح بالای باقی‌مانده دوباره محاسبه شود: «یک برای خود گره به‌علاوه شمارش مثبت /Count»، با نادیده گرفتن نوادگان هر گره جمع‌شده (شمارش منفی). اگر این را غلط انجام دهید، جمع کل نشانک‌ها که خواننده نمایش می‌دهد دچار لغزش می‌شود - با هر حذف، بیش از یک واحد تغییر می‌کند. تغییر نام همه این‌ها را دور می‌زند، و همین یک دلیل دیگر است برای ترجیح دادن کمک‌برنامه هدفمند به دستکاری مستقیم دیکشنری

چگونه ذخیره در همان‌جا می‌ماند

هر ویرایش بالا اشیا را در حافظه تغییر می‌دهد؛ هیچ چیزی تا SaveLoadedDocument اجرا نشود به دیسک نمی‌رسد. دلیل ارزان بودن این رویکرد این است که ذخیره سند را از نو نمی‌سازد - شماره‌های شیء موجود و ساختاری را که HotPDF هنگام بارگذاری تجزیه کرده بود حفظ می‌کند و همان گراف را با چند شیء تغییرکرده و تازه‌اختصاص‌یافته برمی‌گرداند. همین است که نمی‌گذارد یک گذر فراداده کل فایل را از نو بنویسد، و همین سازوکار به‌روزرسانی درجا است که جریان‌های شیء و به‌روزرسانی‌های افزایشی را ممکن می‌کند. اگر فایل‌های مبدأ شما از Word یا یک مجموعه اداری دیگر می‌آیند، چیدمان شیءهای آن‌ها ریزه‌کاری‌های خودش را دارد که پیش از ویرایش باید بدانید؛ مقاله مربوط به جریان‌های ارجاع متقاطع با ارجاع هیبریدی در PDFهای Office توضیح می‌دهد این فایل‌ها چگونه ساخته شده‌اند و در رفت‌وبرگشت چه چیزی باقی می‌ماند

دو مرز را باید رعایت کرد. اول، این یک مدل ویرایش درجا است، نه ابزار redaction یا sanitization: حذف یک کلید Info همان کلید را حذف می‌کند، اما مقادیر قدیمی‌ای را که ممکن است در نسل پیشینِ به‌روزرسانی افزایشیِ همان فایل باقی مانده باشند پاک نمی‌کند. اگر نیاز شما حذف واقعی فراداده حساس است، آن کار دیگری است و سنگین‌تر. دوم، نوشتن XMP لفظی است - کتابخانه به XML شما اعتماد می‌کند و آن را validate نمی‌کند - بنابراین برای هر چیزی که قرار است به PDF/A یا یک اعتبارسنج سخت‌گیر برسد، بسته را از یک الگوی مطمئن تولید کنید و خروجی را بررسی کنید. اگر در این حدود استفاده شود، ویرایش درجا فراداده ابزار مناسبی با اندازه درست است: چند بایت غلط را درست می‌کند و نود و نه درصد فایلی را که از قبل درست بود دقیقاً همان‌طور که سازنده اصلی نوشته است رها می‌کند

API نوشتن سند بارگذاری‌شده‌ای که اینجا نشان داده شده، همراه با بسته استاندارد HotPDF Component برای Delphi و C++Builder عرضه می‌شود، در کنار مجموعه کامل روش‌های ویرایش فراداده، outline و Catalog