مقاله فنی

کاهش حجم فایل PDF در دلفی: فونت‌ها، تصاویر، LZW

برای کاهش حجم فایل PDF در دلفی، کتابخانه losLab PDF سه API ارائه می‌دهد که به سه منبع بزرگ افزایش حجم حمله می‌کنند: SubsetEmbeddedFonts هر برنامه فونت TrueType جاسازی‌شده را به گلیف‌هایی که سند واقعاً رندر می‌کند کاهش می‌دهد، DownsampleImages تصاویر رستر که از DPI هدف فراتر می‌روند را دوباره نمونه‌برداری می‌کند، و NormalizeLZWStreams فشرده‌سازی قدیمی LZWDecode را با FlateDecode جایگزین می‌کند. هر کدام تعداد اشیاء تغییر یافته را برمی‌گردانند، بنابراین مقدار صفر به شما می‌گوید که این مرحله بدون اثر بوده است تا اینکه یک شکست خاموش باشد

چرا PDF ادغام شده من از فایل‌های منبع آن بزرگتر است؟

یک PDF ادغام شده یا تولید شده با برنامه معمولاً به یکی از سه دلیل زیر بیش از حد بزرگ است: فونت‌های کاملاً جاسازی‌شده، تصاویری که با وضوحی بسیار بالاتر از وضوح نمایش نمونه‌برداری شده‌اند، و جریان‌هایی که هنوز با فیلتر قدیمی LZW فشرده شده‌اند. استاندارد ISO 32000-1 §9.9 به تولیدکننده اجازه می‌دهد برنامه فونت کامل را جاسازی کند و اکثر تولیدکنندگان دقیقاً همین کار را انجام می‌دهند زیرا این یک پیش‌فرض ایمن است. یک FontFile2 کامل Arial به صدها کیلوبایت می‌رسد؛ آن را در ده‌ها فایل منبع جاسازی کنید، آن‌ها را ادغام کنید، و در این صورت ده‌ها کپی از خطوط گلیف را برای کاراکترهایی که هیچ‌کس تایپ نکرده حمل خواهید کرد. ادغام خود زباله ایجاد نمی‌کند، بلکه فقط آن را در یک فایل واحد متمرکز می‌کند تا کل آن در نهایت نمایان شود

تصاویر دومین مقصر هستند. یک اسکن با پهنای 4800 پیکسل که در یک قاب یک‌چهارم صفحه قرار می‌گیرد، تقریباً 40 برابر داده‌های پیکسلی بیشتری نسبت به آنچه یک خط لوله چاپ 300 DPI می‌تواند استفاده کند، ارسال می‌کند. مقصر سوم آرام‌تر است: جریان‌های فیلتر شده با LZWDecode. استاندارد ISO 32000-1 §7.4.4 هر دو فیلتر LZWDecode و FlateDecode را مشخص می‌کند و اشاره می‌کند که Flate معمولاً حداقل به همان اندازه خوب فشرده می‌کند؛ در عمل، خروجی Flate به طور مداوم روی داده‌های یکسان کوچکتر است، و LZW بیشتر در فایل‌هایی زنده می‌ماند که در نقطه‌ای از تاریخچه خود از ابزارهای دهه 1990 عبور کرده‌اند. ادامه این مقاله سه مرحله کتابخانه losLab PDF را که هر مشکل را حل می‌کند، بررسی کرده و سپس آن‌ها را در یک خط لوله ترکیب می‌کند

زیرمجموعه‌سازی فونت با SubsetEmbeddedFonts

متد SubsetEmbeddedFonts هر فونت TrueType جاسازی‌شده در یک سند بارگذاری‌شده را به کاراکترهایی که سند واقعاً استفاده می‌کند کاهش می‌دهد و نیازی به آرگومان ندارد زیرا لیست نگهداری را از خود جریان‌های محتوا استخراج می‌کند. به طور داخلی، این مرحله جریان محتوای هر صفحه را با GetTextRuns پیمایش می‌کند، کدهای کاراکتر ارجاع‌داده‌شده تحت هر منبع فونت را جمع‌آوری کرده، یک لیست نگهداری می‌سازد و برنامه فونت اصلی را به موتور FontSub ویندوز (CreateFontPackage) می‌سپارد تا یک زیرمجموعه تولید کند. برنامه بازنویسی شده جایگزین جریان FontFile2 در محل می‌شود و نام BaseFont یک تگ LOSABC+ دریافت می‌کند، که کنوانسیون شش حرف بزرگ به علاوه علامت مثبت است که استاندارد ISO 32000-1 §9.6.4 برای فونت‌های زیرمجموعه تعریف می‌کند. این پیشوند همچنین همان چیزی است که فراخوانی را بی‌اثر (idempotent) می‌کند: این مرحله را دو بار اجرا کنید تا فونت‌های از قبل زیرمجموعه‌سازی‌شده شناسایی و نادیده گرفته شوند، بنابراین استفاده از آن در یک کار دسته‌ای که ممکن است فایل‌ها را مجدداً بررسی کند، ایمن است

var
  Lib: TPDFlib;
  Fonts: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('merged-report.pdf', '') = 1 then
    begin
      Fonts := Lib.SubsetEmbeddedFonts;
      // Fonts = تعداد برنامه‌های FontFile2 بازنویسی شده؛
      // 0 یعنی هیچ چیزی جاسازی نشده یا همه چیز از قبل زیرمجموعه‌سازی شده است
      Lib.SaveToFile('merged-report-subset.pdf');
    end;
  finally
    Lib.Free;
  end;
end;

دانستن دو جزئیات پیاده‌سازی ارزشمند است زیرا مرزهای API را توضیح می‌دهند. اول اینکه، این مرحله FontFile2 را هدف قرار می‌دهد، بنابراین برنامه‌های TrueType جاسازی‌شده را پوشش می‌دهد؛ فونت‌های جاسازی‌شده به عنوان Type 1 یا CFF دست‌نخورده باقی می‌مانند تا خطری ایجاد نشود. دوم اینکه، این متد به FontSub متکی است، که باعث می‌شود SubsetEmbeddedFonts فقط مخصوص ویندوز باشد. نکته ظریف‌تری از پیاده‌سازی: صلاحیت یک فونت با حل واقعی زنجیره ارجاع FontDescriptorFontFile2 تعیین می‌شود، نه با اعتماد به یک پرچم اکتشافی جاسازی‌شده, زیرا فونت‌های یک سند بارگذاری‌شده هرگز از ثبت اطلاعات سمت ایجاد که چنین پرچم‌هایی را تنظیم می‌کند، عبور نکرده‌اند. اگر جریان حل‌شده وجود داشته باشد، فونت یک کاندید است؛ در غیر این صورت، بدون خطا نادیده گرفته می‌شود

معامله صادقانه: یک فونت زیرمجموعه فقط شامل گلیف‌هایی است که در زمان زیرمجموعه‌سازی وجود داشته‌اند. اگر یک ابزار پایین‌دستی، یا کد خودتان، بعداً متنی با همان فونت اضافه کند، هر کاراکتری خارج از زیرمجموعه فاقد طرح کلی خواهد بود و به عنوان یک گلیف مفقود رندر می‌شود. زیرمجموعه‌سازی را به عنوان آخرین مرحله تغییر محتوا انجام دهید، هرگز قبل از مرحله ویرایش. همین احتیاط در صورتی اعمال می‌شود که قصد داشته باشید بعداً فونت را برای استفاده مجدد استخراج کنید؛ مقاله مربوط به استخراج متن، تصویر و فونت با PDFlibPas آنچه را که یک برنامه زیرمجموعه استخراج‌شده می‌تواند و نمی‌تواند به شما بدهد، پوشش می‌دهد

چگونه DownsampleImages تصمیم می‌گیرد کدام تصاویر را کوچک کند؟

متد DownsampleImages(MaxDPI, Quality, Filter) با استفاده از تخمین عمداً محافظه‌کارانه DPI، فقط تصاویری را دوباره نمونه‌برداری می‌کند که با اطمینان بتوان آن‌ها را بیش از حد نمونه‌برداری شده نامید. یک شیء تصویری PDF XObject ابعاد پیکسلی را ذخیره می‌کند اما وضوح فیزیکی قابل اعتمادی ندارد و تگ DPI تصویر منبع به ندرت از یک چرخه بارگذاری-ویرایش-ذخیره جان سالم به در می‌برد. بنابراین، این مرحله SrcDPI = PixelWidth / 8.5 را تخمین می‌زند، در واقع می‌پرسد: اگر این تصویر تمام پهنای یک صفحه Letter را بپوشاند، وضوح آن چقدر خواهد بود؟ فقط تصاویری که تخمین آن‌ها از MaxDPI فراتر رود، دستکاری می‌شوند. این سوگیری عمدی است: تصویری که به صورت کوچک در صفحه قرار گرفته است، DPI واقعی بالاتری نسبت به تخمین دارد، بنابراین این مرحله به جای از بین بردن یک دارایی با کیفیت چاپ که نمی‌تواند اندازه‌گیری کند، کمتر از حد نیاز عمل می‌کند

مقدار Quality از 1 تا 100 کیفیت کدگذاری مجدد JPEG را انتخاب می‌کند، در حالی که 0 خروجی را به عنوان Flate بدون اتلاف سبک PNG نگه می‌دارد؛ پارامتر Filter هسته نمونه‌برداری مجدد را انتخاب می‌کند، 0 برای میانگین جعبه و 1 برای دوخطی (bilinear). برای کاغذبازی‌های اداری اسکن شده، DownsampleImages(150, 75, 1) یک نقطه شروع معقول است؛ برای هر چیزی که ممکن است دوباره چاپ شود، MaxDPI را به 300 افزایش دهید یا این مرحله را کاملاً نادیده بگیرید. کاهش نمونه‌برداری تنها مرحله دارای اتلاف در میان این سه مرحله است، بنابراین باید در پشت تنظیمی قرار گیرد که کاربران شما بتوانند آن را غیرفعال کنند

تبدیل جریان‌های قدیمی LZW با NormalizeLZWStreams

متد NormalizeLZWStreams یک برد رایگان است: هر جریان LZWDecode را بدون اتلاف از حالت فشرده خارج کرده و دوباره با FlateDecode در همان محل فشرده می‌کند و تعداد جریان‌های تبدیل‌شده را برمی‌گرداند. این متد هم یک ورودی تکی /Filter /LZWDecode و هم ظاهر شدن LZW در داخل یک آرایه زنجیره فیلتر را مدیریت می‌کند، جایی که فقط پیوند LZW جایگزین شده و بقیه زنجیره حفظ می‌شود. پارامترهای پیش‌بینی‌کننده (Predictor، Columns، Colors، BitsPerComponent) از DecodeParms جریان خوانده شده و به مفسر منتقل می‌شوند، بنابراین داده‌های تصویری کدگذاری‌شده با پیش‌بینی‌کننده به درستی بازخوانی می‌شوند. از آنجا که هر دو فیلتر کدک‌های دقیق بیتی هستند، بایت‌های رمزگشایی شده قبل و بعد یکسان هستند؛ فقط فشرده‌سازی کانتینر تغییر می‌کند، به همین دلیل است که اجرای بدون قید و شرط این مرحله روی هر فایلی ایمن است

در سندی که فاقد جریان‌های LZW است، این فراخوانی به سادگی مقدار 0 را برمی‌گرداند و به چیزی دست نمی‌زند، که مجموعه رگرسیون کتابخانه به طور صریح این کار را انجام می‌دهد: یک فایل تازه ایجاد شده Flate-only باید صفر تبدیل گزارش کند. این تضمین بدون اثر (no-op) زمانی اهمیت پیدا می‌کند که این مرحله در یک خط لوله قرار گیرد که هزاران فایل ناهمگون را پردازش می‌کند، که برخی از آن‌ها متعلق به سال 2024 و برخی دیگر مربوط به سال 1998 هستند

خط لوله کامل بهینه‌سازی حجم در دلفی

این سه مرحله در یک تابع واحد بارگذاری-بهینه‌سازی-ذخیره ترکیب می‌شوند و ترتیب آن‌ها کمتر از آنچه انتظار دارید اهمیت دارد زیرا روی انواع اشیاء مجزا کار می‌کنند: فونت‌ها، اشیاء تصویری XObjects و فیلترهای جریان. اجرای زیرمجموعه‌سازی در ابتدا همچنان انتخاب تمیزتری است، زیرا این مرحله دارای محدودیت ترتیب ویرایش است

function OptimizePDF(const Src, Dst: string): Boolean;
var
  Lib: TPDFlib;
  Fonts, Images, Streams: Integer;
begin
  Result := False;
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(Src, '') <> 1 then
      Exit;
    Fonts   := Lib.SubsetEmbeddedFonts;        // TrueType FontFile2 -> زیرمجموعه
    Images  := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, دوخطی
    Streams := Lib.NormalizeLZWStreams;        // LZWDecode -> FlateDecode
    Result := Lib.SaveToFile(Dst) = 1;
    // ثبت لاگ Fonts/Images/Streams: سه صفر یعنی فایل از قبل بهینه بوده است
  finally
    Lib.Free;
  end;
end;

صحت عملکرد خط لوله را همان‌طور که کتابخانه صحت عملکرد خود را تایید می‌کند بررسی کنید: بررسی رفت و برگشت. آزمون‌های رگرسیون نسخه v3.130 یک سند ایجاد می‌کنند، آن را ذخیره می‌کنند، مجدداً بارگذاری می‌کنند، بهینه‌سازی را اجرا می‌کنند، دوباره ذخیره می‌کنند و سپس سه مورد را بررسی می‌کنند: خروجی کوچکتر است، تعداد تغییرات گزارش‌شده با انتظارات مطابقت دارد، و بارگذاری مجدد فایل بهینه‌شده همچنان بدون مشکل تجزیه و رندر می‌شود. بازتولید این چرخه ایجاد-بهینه‌سازی-بارگذاری مجدد بر روی نمونه‌ای از فایل‌های تولیدی خودتان و مقایسه متن استخراج‌شده قبل و بعد، سرمایه‌گذاری یک ساعته‌ای است که اشتباهات ادغام را مدت‌ها قبل از اینکه مشتری یک فاکتور خراب را باز کند، شناسایی می‌کند

// بررسی رفت و برگشت: فایل بهینه‌شده همچنان باید به طور تمیز بارگذاری شود
Lib := TPDFlib.Create;
try
  Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
  Assert(Lib.GetPageCount > 0);
finally
  Lib.Free;
end;

این خط لوله در کجای جریان کاری ادغام قرار می‌گیرد؟ بعد از ادغام، نه در طول آن. ادغام در ابتدا و بهینه‌سازی نتیجه واحد به این معنی است که هر فونت جاسازی‌شده یک بار در برابر اتحادیه تمام کاراکترهای استفاده شده زیرمجموعه‌سازی می‌شود، به جای اینکه برای هر فایل منبع به طور جداگانه انجام شود. اگر توان عملیاتی ادغام گلوگاه باشد، PDFlibPas یک مسیر سریع در سطح بایت ارائه می‌دهد که از تجزیه کامل اشیاء جلوگیری می‌کند، که در مقاله ادغام سریع PDF با تغییر مرجع بایت توضیح داده شده است؛ و برای ورودی‌های بیش از حد بزرگ که نمی‌توانند کاملاً در حافظه قرار گیرند، ادغام و تقسیم با دسترسی مستقیم برای PDFهای بزرگ مسیر جریان‌سازی را پوشش می‌دهد. هر دو به طور طبیعی با یک مرحله بهینه‌سازی نهایی روی خروجی ادغام شده جفت می‌شوند

کارهایی که این سه مرحله انجام نخواهند داد

سه‌گانه بهینه‌سازی کتابخانه losLab PDF عمداً از هر چیزی که معنای سند را تغییر دهد خودداری می‌کند. SubsetEmbeddedFonts فونت‌های تکراری را در منابع ادغام شده در یک برنامه واحد ادغام نمی‌کند، بلکه هر کدام را به طور مستقل کوچک می‌کند؛ حذف فونت‌های تکراری یک تبدیل متفاوت و پرخطرتر است. DownsampleImages از تصویری که تخمین محافظه‌کارانه DPI آن زیر آستانه باقی می‌ماند عبور می‌کند، حتی زمانی که یک انسان می‌تواند تشخیص دهد که برای قاب خود بیش از حد بزرگ است. و هیچ‌کدام از مراحل به ساختار سند دست نمی‌زند، بنابراین فایلی که با هزاران شیء یتیم (orphaned) متورم شده است، به ذخیره‌سازی با سبک بازنویسی نیاز دارد تا این مراحل در سطح جریان. در این محدودیت‌ها، ترکیب زیرمجموعه‌سازی فونت، کاهش وضوح تصویر و نرمال‌سازی LZW به Flate، سه منبع کلاسیک متورم شدن PDF را با یک فراخوانی قابل پیش‌بینی API برطرف می‌کند. این سه تابع به عنوان بخشی از کتابخانه PDF losLab برای دلفی، سی‌شارپ و VB.NET در کنار APIهای ادغام، استخراج و رندر که در بالا بحث شد، ارائه می‌شوند