مقاله فنی

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

یک تیم پردازش مطالبات (claims-processing) سی سال پرونده کاغذی داشت که از طریق یک اسکنر تغذیه‌گر-ورق (sheet-fed) عبور می‌کرد. اسکنر یک JPEG در هر صفحه را به پوشه‌ای به نام‌های 0001.jpg، 0002.jpg و غیره تف (spit out) می‌کرد. آنچه بایگانی در واقع به آن نیاز داشت یک PDF برای هر فایل پرونده (case file) بود، با صفحات مرتب شده، به طوری که یک بازبین بتواند به جای کلیک کردن روی صدها تصویر کوچک تصویر، یک سند واحد را باز کند. این آخرین مرحله، تبدیل یک توده شماره‌گذاری شده از اسکن‌ها به یک PDF مرتب شده واحد، کار ما در اینجا است

PDFium Component مستقیماً آن را مدیریت می‌کند. فراتر از رندرینگ و استخراج متن، این کامپوننت می‌تواند یک PDF را از ابتدا بسازد: یک سند خالی ایجاد کند، یک صفحه خالی با هر اندازه‌ای که دوست دارید اضافه کند، یک تصویر را در مختصات فضای-کاربر (user-space coordinates) در آن صفحه بیندازد، و سپس ذخیره کند. کل خط لوله (pipeline) روی کامپوننت TPdf قرار دارد، بنابراین یک مبدل دسته‌ای (batch converter) یک حلقه روی نام فایل‌ها به اضافه مشتی فراخوانی است

خط لولهٔ دسته‌ای Delphi با فراخوانی‌های AddPage و PageNumber و AddImage و SaveAs در PDFium Component که پوشه‌ای از اسکن‌های شماره‌دار را به یک PDF مرتب بدل می‌کند
هر اسکن به یک صفحه تبدیل می‌شود که با AddPage ساخته و پیش از آنکه AddImage بکشدش با PageNumber نشانه‌گیری می‌شود؛ SaveAs سند آماده را یک بار می‌نویسد

شکل تبدیل

برای هر اسکن باید سه اتفاق بیفتد. شما اندازه صفحه را تعیین می‌کنید، تصویر را در داخل صفحه با در نظر گرفتن یک حاشیه قرار می‌دهید، و به صفحه بعدی می‌روید. PDFium Component برای هر کدام یک متد به شما می‌دهد: AddPage یک صفحه خالی با یک اندازه مشخص ایجاد می‌کند، AddImage (یا AddPicture اگر از قبل یک TPicture در دست دارید) بیت‌مپ (bitmap) را به صفحه فعلی رسم می‌کند، و PageNumber به کامپوننت می‌گوید که فراخوانی‌های بعدی برای رسم کردن، کدام صفحه را هدف قرار می‌دهند

جزئیاتی که افراد را به اشتباه می‌اندازد سیستم مختصات است. فضای کاربری PDF مبدا را در گوشه سمت چپ پایین صفحه قرار می‌دهد و Y به سمت بالا افزایش می‌یابد، برعکس مختصات صفحه‌نمایش که توسعه‌دهندگان دلفی بر اساس بازتاب (reflex) به آن‌ها می‌رسند. X, Y که به AddImage می‌دهید، گوشه پایین سمت چپ مستطیل تصویر است، و Width, Height اندازه مکان‌گیری در پوینت است، نه اندازه پیکسلی فایل منبع. این را برعکس دریافت کنید تا اسکن‌های شما خارج از صفحه قرار گیرند یا نسبت به جایی که انتظار داشتید وارونه شوند

ایجاد سند و یک صفحه به ازای هر اسکن

با یک سند خالی شروع کنید. CreateDocument یک PDF تازه را تخصیص می‌دهد و کامپوننت را فعال نگه می‌دارد، بنابراین هیچ مرحله باز کردن جداگانه‌ای وجود ندارد. از آنجا لیست فایل‌های اسکن شده را می‌پیمایید و برای هر کدام یک صفحه اضافه می‌کنید، آن را به صفحه فعلی تبدیل می‌کنید و تصویر را قرار می‌دهید. ابعاد صفحه در اینجا A4 در پوینت (595 × 842 عمودی) است، اندازه ورق استاندارد برای مکاتبات بایگانی شده

procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
  PageW = 595.0;   // عرض A4 به پوینت
  PageH = 842.0;   // ارتفاع A4 به پوینت
  Margin = 36.0;   // حاشیه نیم‌اینچی دور هر اسکن
var
  I: Integer;
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                       // تازه، خالی، از پیش فعال
    for I := 0 to Files.Count - 1 do
    begin
      Pdf.AddPage(I + 1, PageW, PageH);       // نمایه صفحه یک‌پایه است
      Pdf.PageNumber := I + 1;                // صفحه جدید را جاری کن
      PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
    end;
    Pdf.SaveAs(OutputPath);
  finally
    Pdf.Free;
  end;
end;

هر تکرار یک صفحه ایجاد می‌کند و بلافاصله PageNumber را روی آن تنظیم می‌کند. آن خط دوم مهم است: AddPage صفحه را درج می‌کند اما متدهای ترسیم (draw methods) روی هر صفحه‌ای که جاری باشد عمل می‌کنند، بنابراین تنظیم PageNumber همان چیزی است که AddImage را به صفحه‌ای که تازه ساخته‌اید هدف می‌گیرد. از آن بگذرید تا تصاویر شما در هر صفحه‌ای که قبلاً بارگذاری شده بود، روی هم قرار گیرند

یک فرض در آن حلقه پنهان است: ترتیب Files. یک اسکنر صفحات را 0001.jpg تا 0100.jpg نام‌گذاری می‌کند، اما شمارش دایرکتوری همیشه آن‌ها را مرتب‌شده برنمی‌گرداند، و لحظه‌ای که به page9.jpg در کنار page10.jpg برخورد می‌کنید، مرتب‌سازی رشته‌ای ساده صفحه 10 را قبل از صفحه 9 قرار می‌دهد. لیست را صراحتاً قبل از حلقه مرتب کنید و در زمان اسکن به نام‌های صفر-گذاری شده (zero-padded) ترجیح دهید تا ترتیب لغوی (lexical order) با ترتیب صفحه مطابقت داشته باشد. توالی صفحه تنها چیزی است که بازبین فوراً متوجه آن می‌شود و پیشگیری از آن کم‌هزینه‌ترین اشتباه است

قرار دادن یک اسکن و حفظ نسبت ابعاد آن

اسکن به ندرت هم‌شکل صفحه است. اگر آن را برای پر کردن ورق بکشید متن را مخدوش می‌کنید؛ اگر آن را در اندازه پیکسلی کامل قرار دهید سرریز می‌شود. راه‌حل این است که با ضریب کوچکتر از دو نسبت (width-fit یا height-fit) مقیاس‌بندی کنید و آنچه باقی مانده را متمرکز (center) کنید. از آنجایی که مبدا در پایین سمت چپ قرار دارد، متمرکز کردن به معنای تقسیم فضای باقیمانده به طور مساوی و افزودن آن به هر دو X و Y است

دیاگرام صفحهٔ A4 نشان می‌دهد AddImage در PDFium Component چگونه با کد Delphi و مبدأ پایین-چپ، اسکن مقیاس‌شده را درون حاشیه‌ها می‌نشاند
AddImage گوشه پایین-چپ مستطیل جای‌گذاری را می‌گیرد، پس جا شدن و وسط‌چین‌کردن به نقطه صفحه از مبدأ محاسبه می‌شوند
procedure TArchiveForm.PlaceScan(Pdf: TPdf; const FileName: string;
  PageW, PageH, Margin: Double);
var
  Pic: TPicture;
  AvailW, AvailH, Scale, DrawW, DrawH, X, Y: Double;
begin
  Pic := TPicture.Create;
  try
    Pic.LoadFromFile(FileName);              // BMP، JPG، PNG و غیره از طریق واحدهای گرافیکی VCL

    AvailW := PageW - 2 * Margin;
    AvailH := PageH - 2 * Margin;

    // بدون مخدوش کردن اسکن، داخل حاشیه‌ها جا بده
    Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
    DrawW := Pic.Width * Scale;
    DrawH := Pic.Height * Scale;

    // متمرکز کن: فضای باقیمانده به‌طور مساوی تقسیم می‌شود. Y از پایین صفحه اندازه‌گیری می‌شود
    X := (PageW - DrawW) / 2;
    Y := (PageH - DrawH) / 2;

    Pdf.AddImage(FileName, X, Y, DrawW, DrawH);
  finally
    Pic.Free;
  end;
end;

این کار فایل را یک بار بارگیری می‌کند تا ابعاد پیکسلی آن را بخواند، یک مقیاس یکنواخت منفرد را محاسبه می‌کند و مستطیل مکان‌گیری را به AddImage ارسال می‌کند. AddImage مسیر فایل را به طور مستقیم می‌پذیرد و آن را از طریق همان خط لوله تصویری مانند AddPicture مسیریابی می‌کند، بنابراین هر فرمتی که واحدهای گرافیکی VCL تشخیص می‌دهند بدون نیاز به حالت-ویژه (special-casing) کار می‌کند. اگر از قبل تصویر را در TPicture از یک صفحه پیش‌نمایش رمزگشایی کرده‌اید، AddPicture(Pic, X, Y, DrawW, DrawH) را با همان مستطیل فراخوانی کنید و از خواندن دومین فایل بگذرید

پرش از رمزگشایی برای اسکن‌های JPEG

اسکنرها تقریباً همیشه JPEG ساطع (emit) می‌کنند. بارگیری یک JPEG در TPicture آن را به یک بیت‌مپ (bitmap) رمزگشایی می‌کند، و سپس PDFium در هنگام ذخیره مجدداً آن را رمزگذاری می‌کند، دو رفت و برگشت با اتلاف (lossy round trips) که به آنها نیازی ندارید. AddJpegImage بایت‌های فشرده اصلی را مستقیماً از یک جریان (stream) به داخل صفحه تعبیه می‌کند که هم سریع‌تر است و هم برای یک دسته با حجم بالا، از نظر بصری تمیزتر است

AddJpegImage بایت‌های اصلی JPEG را در صفحهٔ PDFium Component تعبیه می‌کند، درحالی‌که AddImage رمزگشایی و SaveAs بازکدگذاری پیکسل‌ها را در Delphi انجام می‌دهد
AddJpegImage بایت‌های فشرده اسکنر را عیناً درون‌سازی می‌کند و از گذرهای دیکود و بازکدگذاری که AddImage و SaveAs انجام می‌دهند می‌پرهیزد
var
  Stream: TFileStream;
begin
  // ... پس از AddPage + PageNumber برای صفحه فعلی ...
  Stream := TFileStream.Create(FileName, fmOpenRead);
  try
    // بایت‌های JPEG را همان‌طور که هستند تعبیه می‌کند؛ بدون چرخه decode/re-encode
    Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
  finally
    Stream.Free;
  end;
end;

شما همچنان X، Y، DrawW و DrawH را به همان روش محاسبه می‌کنید، زیرا برای مقیاس‌بندی به ابعاد پیکسل نیاز دارید. آن ها را از روی فایل یا تجزیه هدر سریع بخوانید، سپس جریان خام را به AddJpegImage تحویل دهید. برای اسکن‌های PNG یا TIFF، مسیر AddImage مناسب است؛ میانبر JPEG را برای فرمتی رزرو کنید که واقعاً در مورد آن صدق می‌کند

برچسب زدن هر صفحه

اسکن‌های بایگانی شده زمانی که هر صفحه نام فایل منبع خود را به همراه داشته باشد، راحت‌تر حسابرسی می‌شوند. AddText یک رشته را در یک مختصات فضای کاربر (user-space coordinate) رسم می‌کند، بنابراین یک عنوان (caption) درست زیر تصویر قرار می‌گیرد. به یاد داشته باشید که محور Y وارونه است: برای قرار دادن برچسب در زیر اسکن، به جای افزودن به آن، از لبه پایینی تصویر کم می‌کنید

// عنوان زیر اسکن: Y به سمت پایین صفحه کاهش می‌یابد
Pdf.AddText('File: ' + ExtractFileName(FileName), 'Helvetica', 9,
  X, Y - 14, clGray);

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

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

PDFium Component از loslab.com، APIهای ساخت سند، رندرینگ و متنی را که در سراسر این سری استفاده می‌شوند در یک بسته ارائه می‌دهد