مقاله فنی

رندر کردن صفحات PDF به تصاویر JPEG در دلفی با PDFium Component

رندر کردن یک صفحه PDF به JPEG دو عملیات است که مردم تمایل دارند با هم اجرا کنند و سپس به طور جداگانه عیب‌یابی (debug) کنند. ابتدا صفحه را به یک بیت‌مپ (bitmap) پیکسلی در وضوحی (resolution) که انتخاب می‌کنید شطرنجی (rasterize) می‌کنید. سپس آن بیت‌مپ را به رمزگذار JPEG می‌دهید و کیفیتی را انتخاب می‌کنید. PDFium Component نیمی از کار اول را از طریق RenderPage در اختیار دارد؛ نیمه دوم VCL ساده، TJPEGImage از Vcl.Imaging.jpeg است. درز بین آنها جایی است که تصمیمات جالب در آن زندگی می‌کنند، زیرا وضوحی که در سمت رندر انتخاب می‌کنید و کیفیتی که در سمت کدگذاری انتخاب می‌کنید با یکدیگر و در برابر اندازه فایل به روش‌هایی مبادله (trade off) می‌شوند که به راحتی اشتباه می‌شوند

چیزی که باید قبل از هر کدی درونی‌سازی (internalize) شود: یک صفحه PDF هیچ پیکسلی ندارد. در نقاط (points) توضیح داده شده است، جایی که یک پوینت 1/72 اینچ است، و صفحه یک ترسیم برداری است که در آن نقاط اندازه‌گیری می‌شود. وقتی از PDFium می‌خواهید که رندر کند، انتخاب می‌کنید که آن نقاشی را روی چند پیکسل قرار دهید، و آن انتخاب DPI است. محاسبه حسابی را اشتباه بگیرید و یا زمانی که می‌خواهید یک مستر چاپی (print master) داشته باشید، یک تصویر بندانگشتی تار ارائه می‌دهید، یا یک بیت‌مپ 200-مگاپیکسلی را به چیزی اختصاص می‌دهید که قرار است پیش‌نمایش 120 پیکسلی باشد

از DPI تا ابعاد پیکسلی

RenderPage به پیکسل‌های صحیح Width و Height نیاز دارد، نه یک DPI. بنابراین اولین کار تبدیل است. یک صفحه اندازه خود را در پوینت‌ها از طریق PageWidth و PageHeight (هر دو Double) گزارش می‌دهد و تبدیل همان تبدیلی است که هر شطرنجی‌کننده (rasterizer) از آن استفاده می‌کند: پیکسل‌ها برابر با نقاط ضربدر DPI هدف تقسیم بر 72 هستند. صفحه Letter ایالات متحده 612 در 792 پوینت است. در 150 DPI این رقم به 1275 در 1650 پیکسل تبدیل می‌شود؛ در 72 DPI در 612 در 792 باقی می‌ماند، یک پیکسل در هر پوینت، که در این صورت افراد فراموش می‌کنند که این فقط شناسه (identity) است

// Pdf.PageNumber must already point at the page you want.
PixelW := Round(Pdf.PageWidth  * Dpi / 72);
PixelH := Round(Pdf.PageHeight * Dpi / 72);
Bitmap := Pdf.RenderPage(0, 0, PixelW, PixelH, ro0, [], clWhite);
// ... use Bitmap ...
Bitmap.Free;   // the function-form RenderPage hands you ownership

دو جزئیات در آن چهار خط تعیین می‌کنند که آیا کد صحیح است یا خیر. اول این است که فرم تابع RenderPage یک TBitmap را برمی‌گرداند که شما مالک آن هستید. PDFium آن را تخصیص داد و رفت؛ اگر در هر تکرار آن را Free نکنید، دسته‌ای بیش از چند صد صفحه، چند صد بیت‌مپ را به بیرون نشت می‌دهد و فرآیند متورم می‌شود تا زمانی که چیزی سقوط کند. دومی آرگومان Color است، در اینجا clWhite. صفحات PDF معمولاً با فرض یک بستر (substrate) مات سفید کشیده می‌شوند و صفحه‌ای با شفافیت (transparency) که در رنگ پس‌زمینه اشتباه رندر می‌شود، لبه‌های گل‌آلود یا هاله‌های تاریک سرگردان تولید می‌کند. سفید پیش‌فرض مناسبی برای تقریباً همه اسناد است؛ این پارامتر برای موارد نادری وجود دارد که چنین نیست

0, 0 افست‌های Left و Top در صفحه هستند، در فضای مختصات مقیاس‌بندی شده، و شما آنها را در صفر رها می‌کنید مگر اینکه در حال برش باشید. ro0 چرخش است: آن را در صفر بگذارید و PDFium به هر چرخشی که صفحه قبلاً در ورودی /Rotate خود اعلام کرده است، احترام می‌گذارد، بنابراین صفحه‌ای که در حالت افقی تالیف شده است بدون اینکه کاری انجام دهید، افقی ظاهر می‌شود

کدگذاری بیت‌مپ به صورت JPEG

هنگامی که بیت‌مپ وجود داشته باشد، JPEG بخش آسان آن است و دلفی خالص است. TJPEGImage.Assign بیت‌مپ را در داخل کپی می‌کند، CompressionQuality کیفیت را در مقیاس 1 تا 100 تنظیم می‌کند و SaveToFile فایل را می‌نویسد. تنها قانون ترتیب این است که کیفیت باید قبل از ذخیره‌سازی تعیین شود، زیرا رمزگذاری‌ای را کنترل می‌کند که SaveToFile راه‌اندازی (trigger) می‌کند

uses
  Vcl.Graphics, Vcl.Imaging.jpeg, PDFium;

procedure SavePageAsJpeg(Pdf: TPdf; PageNumber, Dpi, Quality: Integer;
  const FileName: string);
var
  Bitmap: TBitmap;
  Jpeg: TJPEGImage;
begin
  Pdf.PageNumber := PageNumber;
  Bitmap := Pdf.RenderPage(0, 0,
    Round(Pdf.PageWidth  * Dpi / 72),
    Round(Pdf.PageHeight * Dpi / 72),
    ro0, [], clWhite);
  try
    Jpeg := TJPEGImage.Create;
    try
      Jpeg.Assign(Bitmap);
      Jpeg.CompressionQuality := Quality;   // 1..100
      Jpeg.SaveToFile(FileName);
    finally
      Jpeg.Free;
    end;
  finally
    Bitmap.Free;
  end;
end;

این try/finally تودرتو برای یک کمک‌کننده (helper) یک‌صفحه‌ای مشکل‌پسند به نظر می‌رسد، و دقیقاً برای یک دسته درست است. بلوک داخلی رمزگذار را آزاد می‌کند، بلوک بیرونی بیت‌مپ را آزاد می‌کند و هر کدام که در یک استثنا فعال (firing) شوند، باز هم آنچه را که متعلق به خودشان است آزاد می‌کنند. آنها را در یکی جمع کنید و یک استثنا در حین رمزگذاری می‌تواند بیت‌مپ را گیر بیندازد. در یک اجرای طولانی، این تفاوت بین یک مبدل است که به پایان می‌رسد و مبدلی که در صفحه 300 با یک فایل خراب و دیالوگ خارج-از-حافظه می‌میرد

انتخاب DPI و کیفیت با هم

دو دستگیره (knobs) مستقل از هدف خروجی نیستند، و اشتباه رایج این است که هر دو را از روی احتیاط زیاد کنید. تصویر بندانگشتی وب که با سرعت 300 DPI رندر شده و با کیفیت 95 ذخیره شده است، چند صد کیلوبایت است که وانمود می‌کند یک تصویر 120 پیکسلی است؛ مرورگر تقریباً تمام آن را در مقیاس پایین دور می‌اندازد. وضوح (resolution) را با پیکسل‌هایی که خروجی در واقع به آن نیاز دارد مطابقت دهید، سپس کیفیتی را انتخاب کنید که از فشرده‌سازی با اتلاف (lossy compression) JPEG بدون مصنوعات (artifacts) قابل مشاهده جان سالم به در ببرد

خروجیDPIکیفیت JPEG
تصویر بندانگشتی لیست7260-70
پیش‌نمایش روی صفحه96-15080-85
مشاهده با جزئیات بالا200-30085-95
مستر چاپ300-60090-100

کیفیت JPEG به خودی خود ارزش یک کلمه احتیاط را دارد. این یک شماره‌گیر (dial) خطی نیست. پرش از 70 به 85 باعث بهبود بصری واقعی برای رشد متواضع فایل می‌شود؛ پرش از 95 به 100 فایل را تقریباً دو برابر می‌کند برای تفاوتی که تقریباً هیچ‌کس نمی‌تواند ببیند، زیرا کیفیت 100 هنوز بدون اتلاف (lossless) نیست، فقط دیگر موارد زیادی را دور نمی‌ریزد. برای صفحات پرمتن (text-heavy)، فشرده‌سازی مبتنی-بر-بلوک (block-based) JPEG لبه‌های تیز گلیف‌ها را در صدای زنگ ضعیف لکه‌دار می‌کند، به همین دلیل است که کیفیت زیر 80 متن‌های اسکن‌شده را روی خروجی واضح تولید می‌کند. اگر صفحات بیشتر متن هستند و می‌توانید قالب‌ها را تغییر دهید، PNG آن متن را بدون زنگ زدن (ringing) رندر می‌کند؛ JPEG جایگاه خود را در عکاسی و محتوای ترکیبی که فشرده‌سازی آن واقعاً کوچکتر است به دست می‌آورد

تصاویر بندانگشتی سریع‌تر و کوچک‌تر

زمانی که هدف به جای بازتولید وفادارانه یک تصویر بندانگشتی باشد، می‌توانید به رندرکننده (renderer) بگویید کار کمتری انجام دهد. پارامتر Options مجموعه‌ای از پرچم‌های TRenderOption را می‌گیرد، و تعداد کمی از آنها وفاداری (fidelity) را دقیقاً به شکلی که یک پیش‌نمایش کوچک می‌خواهد با سرعت مبادله می‌کنند. reGrayscale رنگ را کاهش می‌دهد که هم سریعتر رندر می‌شود و هم بیت‌مپ کوچکتری برای کدگذاری تولید می‌کند. reNoSmoothImage و reNoSmoothPath از anti-aliasing صرف نظر می‌کنند که به هر حال در مقیاس تصویر بندانگشتی نامرئی است

function RenderThumbnail(Pdf: TPdf; PageNumber, MaxW, MaxH: Integer): TBitmap;
var
  Scale: Double;
begin
  Pdf.PageNumber := PageNumber;
  // Fit the page inside MaxW x MaxH while preserving aspect ratio.
  Scale := Min(MaxW / Pdf.PageWidth, MaxH / Pdf.PageHeight);
  Result := Pdf.RenderPage(0, 0,
    Round(Pdf.PageWidth  * Scale),
    Round(Pdf.PageHeight * Scale),
    ro0, [reGrayscale, reNoSmoothImage], clWhite);
end;

مورد تصویر بندانگشتی همچنین راه تمیزتری را برای فکر کردن در مورد اندازه‌بندی (sizing) نشان می‌دهد. به جای گذراندن DPI، یک ضریب مقیاس (scale factor) را محاسبه کنید که در کادر حاشیه با حفظ نسبت ابعاد مطابقت داشته باشد، که کاری است که Min دو نسبت انجام می‌دهد. یک صفحه پرتره و یک صفحه منظره (landscape) هر دو بدون اعوجاج درون یک کادر قرار می‌گیرند، و شما هرگز نباید در مورد اینکه کدام DPI مربوط به "متناسب بودن در 200 در 280" است، استدلال کنید. یک نکته در مورد reGrayscale: محتوای تصویر شطرنجی (raster image content) را به خاکستری تبدیل می‌کند، اما پر کردن برداری (vector fills) و متن مقادیر رنگی خود را در موتور حفظ می‌کنند، بنابراین صفحه‌ای که بیشتر هنر برداری (vector art) است ممکن است کمتر از نام پرچم تک‌رنگ (monochrome) برگردد. برای یک نتیجه تمام خاکستری (full-grayscale) واقعی، تبدیل بیت‌مپ رندر شده با GrayscalePdfBitmap مسیر قابل اعتمادی است

دسته‌ای کردن کل یک سند

کنار هم قرار دادن آن برای یک سند کامل یک حلقه در PageCount است که در آن PageNumber هر بار یک صفحه جابجا می‌شود. صفحات مبتنی-بر-1 (1-based) هستند: صفحه یک PageNumber := 1 است و حلقه به PageCount شامل می‌شود (inclusive)، نه PageCount - 1. مورد دیگری که دسته باید به آن احترام بگذارد، قرارداد بارگیری-بی‌صدا (silent-load contract) است. تنظیم Active := True هرگز روی فایل آسیب دیده یا رمز عبور اشتباه خطایی ایجاد نمی‌کند؛ فقط Active را روی False رها می‌کند. آن را قبل از اینکه یک صفحه را رندر کنید، بررسی کنید، در غیر این صورت اولین RenderPage در برابر سندی که هرگز باز نشده است، کار می‌کند

procedure ExportAllPages(const PdfPath, OutDir: string; Dpi, Quality: Integer);
var
  Pdf: TPdf;
  I, Digits: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := PdfPath;
    Pdf.Active := True;
    if not Pdf.Active then
      raise Exception.Create('Could not open ' + PdfPath);

    Digits := Length(IntToStr(Pdf.PageCount));   // zero-pad so files sort right
    for I := 1 to Pdf.PageCount do
      SavePageAsJpeg(Pdf, I, Dpi, Quality,
        Format('%s\page_%.*d.jpg', [OutDir, Digits, I]));
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

صفرگذاری (zero-padding) از طریق Digits چیز کوچکی است که باعث صرفه‌جویی در بعدازظهرِ بعد می‌شود. فایل‌ها را page_1.jpg تا page_10.jpg نام‌گذاری کنید و هر ابزاری که آنها را به عنوان رشته‌ها (strings) دسته‌بندی می‌کند، page_10 را درست بعد از page_1 قرار می‌دهد و ترتیب را به هم می‌زند. لایه‌گذاری به اندازه عرض بالاترین شماره صفحه، به‌طوری‌که یک سند 300-صفحه‌ای page_001.jpg تولید می‌کند، ترتیب لغوی (lexical order) و ترتیب صفحه را در همه‌جا (در جریان پایین‌دست) یکسان نگه می‌دارد

برای اسنادی که آنقدر بزرگ هستند که تبدیل آن‌ها زمان قابل توجهی می‌طلبد، آن را خارج از رشته UI اجرا کنید یا پیام‌ها را بین صفحات پمپ کنید تا برنامه پاسخگو بماند و به کاربر راهی برای توقف بدهد. اگر در حال رندر کردن صفحات بسیار بزرگ هستید و می‌خواهید لغوی (cancellation) در میانه صفحه به جای بین صفحات انجام شود، PDFium Component یک مسیر رندر پیشرونده با نماد لغو دارد؛ این مکانیزم سنگین‌تری نسبت به اکثر صادرات‌های دسته‌ای است، اما زمانی وجود دارد که یک صفحه منفرد در 600 DPI به خودی خود برای مسدود کردن به اندازه کافی کند باشد

یک جفت‌شدگی نهایی که ارزش دانستن را دارد. شطرنجی کردن یک صفحه، لایه متنی آن را از بین می‌برد: JPEG دارای پیکسل است و کلمات موجود در آن دیگر قابل انتخاب یا جستجو نیستند. هنگامی که به تصویر و متن اصلی نیاز دارید، تصویر را رندر کنید و متن را جداگانه بکشید، که قطعه همراه در مورد استخراج متن از اسناد PDF با PDFium Component پوشش می‌دهد. افزارهای (overloads) RenderPage و گزینه‌های رندر نشان داده شده در اینجا بخشی از PDFium Component برای دلفی و سی‌پلاس‌پلاس‌بیلدر (C++Builder) هستند