مقاله فنی

استخراج تصاویر از PDF با PDFium Component در دلفی

قالب PDF تصاویر را به شکل اشیای درجه‌یک درون استریم‌های محتوای خود ذخیره می‌کند. وقتی صفحه‌ای به یک عکس، اسکن یا نمودار ارجاع می‌دهد، دادهٔ پیکسلی در دیکشنری XObject در کنار هندسهٔ صفحه زندگی می‌کند. PDFium Component آن را از راه دو ویژگی روی TPdf عرضه می‌کند: BitmapCount که برمی‌گرداند صفحهٔ جاری چند بیت‌مپ توکار دارد، و Bitmap[Index] که یکی از آن‌ها را در قالب یک TBitmap رمزگشایی می‌کند که مالکش شمایید و باید آزادش کنید. کل مدل استخراج همین است. حلقه چهار خط می‌شود؛ آنچه قضاوت می‌طلبد لوله‌کشی پیرامون آن است

باز کردن سند

نخستین چیزی که باید دربارهٔ TPdf بدانید این است که Active := True هرگز استثنا نمی‌دهد. شکست بارگذاری، رمز عبور نادرست، فایل خراب: همه درون خود بلعیده می‌شوند و کامپوننت صرفاً غیرفعال می‌ماند. باید خودتان پس از انتساب پرچم را بررسی کنید، وگرنه با PageCount برابر صفر وارد حلقهٔ صفحه‌ها می‌شوید و در شگفت می‌مانید که چرا چیزی استخراج نشد

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
    begin
      Writeln('Failed to open: ', Pdf.FileName);
      Exit;
    end;
    Writeln(Pdf.PageCount, ' pages');
    // ادامه به مرحلهٔ استخراج
  finally
    Pdf.Free;
  end;
end;

فایل‌های محافظت‌شده با رمز عبور همان الگو را دنبال می‌کنند: Pdf.Password را پیش از تنظیم Active := True نسبت دهید. اگر رمز عبور نادرست باشد، Active روی False می‌ماند و هیچ استثنایی برای گرفتن نمی‌گیرید. در ابزاری دسته‌ای که صدها فایل را پردازش می‌کند، آن رفتار خاموش در واقع سودمند است: شکست‌ها را در یک فهرست انباشته می‌کنید به‌جای اینکه برای هر کدام پشتهٔ فراخوانی را باز کنید

پیمایش صفحه‌ها و بیرون کشیدن بیت‌مپ‌ها

ویژگی BitmapCount به‌ازای هر صفحه است، پس پیش از خواندنش Pdf.PageNumber را تنظیم می‌کنید. شمارهٔ صفحه‌ها از 1 آغاز می‌شود؛ پیش‌فرض 0 است، یعنی هیچ صفحه‌ای بارگذاری نشده. ویژگی Bitmap[Index] از 0 آغاز می‌شود و یک TBitmap برمی‌گرداند که مالکیتش با فراخواننده است. باید آزادش کنید. آزادسازی را درون حلقه‌ای بلند روی سندی بزرگ فراموش کنید و حافظه به‌سرعت بالا می‌رود، چون هر بیت‌مپ می‌تواند پیش از هر فشرده‌سازی چند مگابایت دادهٔ پیکسلی خام باشد

حلقهٔ استخراج تصویر PDF در دلفی با PDFium که پرچم خاموش Active را بررسی می‌کند، PageNumber را برای هر صفحه تنظیم می‌کند، BitmapCount را می‌خواند، بیت‌مپ‌های nil را رد می‌کند و هر TBitmap را پس از ذخیره آزاد می‌کند
حلقه شمارش را تنها پس از حرکت مکان‌نمای صفحه می‌خواند، بیت‌مپ‌های nil ناشی از XObjectهای بدشکل را رد می‌کند و هر TBitmap متعلق به فراخواننده را پیش از واکشی بعدی آزاد می‌کند
procedure ExtractAllImages(Pdf: TPdf; const OutputDir: string);
var
  Page, Idx: Integer;
  Bmp: TBitmap;
  OutPath: string;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    for Idx := 0 to Pdf.BitmapCount - 1 do
    begin
      Bmp := Pdf.Bitmap[Idx];
      if not Assigned(Bmp) then
        Continue;
      try
        OutPath := Format('%s\p%d_img%d.bmp', [OutputDir, Page, Idx + 1]);
        Bmp.SaveToFile(OutPath);
      finally
        Bmp.Free;
      end;
    end;
  end;
end;

پاسبان Assigned اهمیت دارد. شمار اندکی از مولدهای PDF تصویرهای XObject را با ابعاد پیکسلی صفر یا دادهٔ بدشکل به شکلی دیگر می‌نویسند؛ در آن موارد کامپوننت به‌جای بیت‌مپ خالی مقدار nil برمی‌گرداند. تلقی کردن بازگشت nil به‌عنوان خطا و متوقف کردن استخراج بازتاب نادرستی است: از آن بگذرید، اگر رد ممیزی می‌خواهید صفحه و ایندکس را ثبت کنید، و ادامه دهید. باقی صفحه هنوز می‌تواند تصویرهای معتبر بدهد

دقت کنید که حلقهٔ بیرونی در هر تکرار Pdf.PageNumber را تنظیم می‌کند. همان انتساب است که صفحه را در وضعیت درونی کامپوننت بارگذاری می‌کند و BitmapCount را معنادار می‌سازد. از آن بگذرید و شمارش همان یک صفحه را بارها می‌خوانید. این الگو هنگام نوشتن زائد به نظر می‌رسد، اما API این‌گونه طراحی شده است: صفحه یک مکان‌نماست، نه یک مجموعه

برگزیدن قالب خروجی

قالب BMP بدون اتلاف است و همیشه بدون واحدهای اضافی در دسترس، و همین آن را پیش‌فرض معقولی می‌کند وقتی هنوز نمی‌دانید تصویر چه دارد. وقتی اندازهٔ فایل مهم است، قالب پیکسلی TBitmap بازگشتی به شما می‌گوید کدام کدک مناسب است. بیت‌مپ 32 بیتی کانال آلفا حمل می‌کند؛ PNG آن را بدون اتلاف نگه می‌دارد. تصویر 24 بیتی بزرگ با تُن پیوسته نامزد JPEG است. تصویرهای کوچک‌تر یا آن‌ها که با پالت محدود کشیده شده‌اند عموماً بهتر است BMP بمانند تا از JPEG بگذرند، که در تنظیمات کیفیت پایین آرتیفکت بلوکی می‌افزاید و در کیفیت بالا چیز زیادی صرفه‌جویی نمی‌کند

procedure SaveBitmap(Bmp: TBitmap; const FileName: string);
var
  Jpg: TJPEGImage;
begin
  case UpperCase(ExtractFileExt(FileName)) of
    '.JPG', '.JPEG':
      begin
        Jpg := TJPEGImage.Create;
        try
          Jpg.Assign(Bmp);
          Jpg.CompressionQuality := 85;
          Jpg.SaveToFile(FileName);
        finally
          Jpg.Free;
        end;
      end;
  else
    Bmp.SaveToFile(FileName);  // BMP: بدون اتلاف، بدون واحد اضافی
  end;
end;

در عمل، انتخاب قالب را Bmp.PixelFormat و ابعاد هدایت می‌کنند. اگر PixelFormat = pf32bit باشد به قالبی نیاز دارید که آلفا را حمل کند؛ PNG انتخاب بدیهی است، هرچند در نسخه‌های قدیمی‌تر دلفی به واحد PNGImage نیاز دارد. برای تصویرهای 24 بیتی پهن‌تر از تقریباً 300 پیکسل، JPEG با کیفیت 85 نسبت به BMP کاهش حجمی سه‌به‌یک می‌دهد بی‌آنکه در بیشتر محتوای عکاسی اتلاف محسوسی داشته باشد. زیر آن آستانه، BMP از نظر حجم قابل مقایسه است و اصلاً هیچ تصمیم کیفیتی را لازم نمی‌کند

BitmapCount چه چیزی را می‌شمارد و چه چیزی را نه

قالب PDF میان تصویرهای XObject و گرافیک برداری کشیده‌شده با عملگرهای مسیر تمایز می‌گذارد. صفحه‌ای که از نظر بصری پیچیده به نظر می‌رسد می‌تواند BitmapCount صفر برگرداند اگر همهٔ عناصرش برداری باشند. صفحه‌های اسکن‌شده تقریباً همیشه دقیقاً یک تا برمی‌گردانند: اسکنر کل اسکن را به شکل یک تصویر XObject تمام‌صفحه با هر تفکیکی که روی اسکنر تنظیم بوده می‌نویسد. صفحه‌هایی که متن حروف‌چینی‌شده را با عکس‌های توکار می‌آمیزند به‌ازای هر عکس یک مدخل برمی‌گردانند. خط‌های تزئینی، پس‌زمینه‌های سایه‌دار و حاشیهٔ جدول‌ها معمولاً اصلاً در شمارش بیت‌مپ‌ها ظاهر نمی‌شوند

آنچه BitmapCount روی یک صفحهٔ PDF در PDFium پوشش می‌دهد، شمردن تصویرهای XObject مانند اسکن‌ها و عکس‌ها در حالی که متن برداری، خط‌های تزئینی، پرکردن‌های سایه‌دار و تصویرهای درون‌خطی بیرون از شمارش می‌مانند
صفحهٔ اسکن‌شده دقیقاً یک تصویر XObject تمام‌صفحه برمی‌گرداند، در حالی که صفحه‌ای کاملاً برداری می‌تواند شلوغ به نظر برسد و باز شمارش صفر بدهد

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

یک جزئیات ارزش به‌خاطر سپردن دارد: مقدار BitmapCount که می‌خوانید برای صفحهٔ جاری در لحظهٔ آخرین انتساب PageNumber است. اگر کد شما میان شمردن و واکشی شاخه بزند یا تابعی را صدا کند که PageNumber را تغییر دهد، ممکن است کمتر از فضایی که تخصیص داده‌اید تصویر بخوانید، یا فراتر از انتها ایندکس بزنید. خواندن شمارش و حلقهٔ Bitmap[] را روی یک صفحه نگه دارید و در این میان به PageNumber دست نزنید

استفاده از TPdfView در یک برنامهٔ فرمی

کامپوننت TPdfView همان ویژگی‌های BitmapCount و Bitmap[] را عرضه می‌کند، اما صفحه‌ای که از آن می‌خواند همان صفحهٔ نمایش‌داده‌شدهٔ جاری نما است، نه TPdf.PageNumber. این دو اشاره‌گر صفحه مستقل‌اند؛ تنظیم یکی دیگری را تکان نمی‌دهد. در برنامهٔ فرمی VCL با یک نمایشگر زنده، می‌توانید Pdf.PageNumber := N را صدا کنید تا استخراج را از راه TPdf پیش ببرید در حالی که نمایشگر روی هر جایی که کاربر آخرین بار پیمایش کرده می‌ماند. آن جدایی عمدی است و وضعیت نمایش نمایشگر را حین اجرای استخراج در پس‌زمینه تمیز نگه می‌دارد

حافظه و کارایی در کارهای دسته‌ای

در سراسر یک بایگانی بزرگ، بودجهٔ حافظه چیزی است که باید بیش از همه پاییدش. هر فراخوانی Bitmap[] یک TBitmap تازه روی هیپ تخصیص می‌دهد، و روی صفحهٔ اسکن‌شدهٔ 300 نقطه بر اینچ این به‌راحتی 25 مگابایت دادهٔ پیکسلی خام پیش از هر کدگذاری است. اگر صفحه‌ها را در حلقه‌ای فشرده و بدون آزادسازی میان تکرارها پردازش کنید، مجموعهٔ کاری خطی با شمار تصویرها رشد می‌کند. شکل درست همیشه این است: یک بیت‌مپ بگیر، کاری را که لازم داری بکن، آزادش کن، بعدی را بگیر. اگر برای گامی مقایسه‌ای نیاز دارید همزمان ارجاع به چند بیت‌مپ نگه دارید، نخست آن‌ها را با BitmapCount بشمارید و ظرفتان را متناسب تخصیص دهید، سپس هر کدام را به‌محض تمام شدن کارتان آزاد کنید نه اینکه به پاک‌سازی پایان سند موکول کنید. روی سندی با 500 صفحهٔ اسکن‌شده، همین تمایز می‌تواند فرق میان 25 مگابایت و 12 گیگابایت اوج حافظهٔ مقیم باشد

ویژگی‌های BitmapCount و Bitmap[] که اینجا نشان داده شدند بخشی از PDFium Component برای دلفی و C++Builder هستند