مقاله فنی

عملکرد استخراج صفحه HotPDF در Delphi

دو دقیقه برای کپی سه صفحه از یک PDF چهل صفحه‌ای یک مشکل تنظیم عملکرد نیست. این نشانه‌ای از استفاده از مسیر نادرست API است. وقتی برای اولین بار این زمان‌بندی را در یک نمونه کپی صفحه HotPDF Component دیدم، غریزه من این بود که ابتدا به ساختار سند و سپس به کد نگاه کنم. مشخص شد که این ترتیب مهم است

چه چیزی واقعاً کند بود

PDF مورد نظر یک سند مرجع ۴۰ صفحه‌ای با یک درخت صفحه غیرساده بود: چندین گره میانی /Pages به جای یک آرایه مسطح واحد. کد نمونه اصلی LoadFromFile را فراخوانی می‌کرد، سپس یک سند جدید با BeginDoc می‌ساخت، روی شماره صفحات انتخاب‌شده حلقه می‌زد و در هر تکرار سند منبع را دوباره از دیسک برای استخراج یک صفحه بارگیری می‌کرد. این هزینه کامل تجزیه ضرب در تعداد صفحاتی است که می‌خواهید. یک فایل ۱۲ مگابایتی برای یک استخراج سه صفحه‌ای شش بار به دیسک دسترسی پیدا کرد زیرا کسی بررسی نکرده بود که آیا فایل باید در طول تکرارها باز بماند یا خیر

عامل دوم در کد نامرئی بود: LoadFromFile در HotPDF کل جدول ارجاع متقاطع را حل می‌کند و هر جریان شیء را در هنگام بارگیری از حالت فشرده خارج می‌سازد. این رفتار مناسبی برای سندی است که قصد اصلاح آن را دارید، اما اگر فقط تعداد صفحات و زیرمجموعه‌ای از صفحات را بخواهید، کاری بیشتر از نیاز شما انجام می‌دهد. برای دسترسی فقط‌خواندنی به ساختار، DAOpenFileReadOnly از غیرسریال‌سازی کل درخت شیء جلوگیری می‌کند، که برای فایل‌های فشرده با منابع تصویر بزرگ اهمیت دارد

هیچ‌کدام از این‌ها باگ کتابخانه نیستند. هر دو مورد مربوط به فراخوانندگانی است که API طراحی‌شده برای یک کار را انتخاب کرده و آن را برای کار دیگری استفاده می‌کنند

استفاده از InsertPagesFromDocument برای استخراج صفحه

مسیر درست برای کپی کردن محدوده‌ای از صفحات از یک سند HotPDF به سندی دیگر، InsertPagesFromDocument است که پس از LoadFromFile روی منبع فراخوانی می‌شود. شما منبع را یک بار بارگیری می‌کنید، مقصد را یک بار ایجاد یا بارگیری می‌کنید، صفحات را انتقال می‌دهید و ذخیره می‌کنید. منبع در طول تمام درج‌های صفحه در حافظه باقی می‌ماند:

procedure ExtractPages(const SourceFile, DestFile: string;
  const PageRange: string);
var
  Source, Dest: THotPDF;
begin
  Source := THotPDF.Create(nil);
  Dest   := THotPDF.Create(nil);
  try
    // Load source once: full parse happens here and only here
    Source.LoadFromFile(SourceFile);

    // Build a minimal destination document
    Dest.FileName := DestFile;
    Dest.BeginDoc;

    // Copy the requested range; '1-3' inserts pages 1 through 3
    // starting at position 1 in the destination
    Dest.InsertPagesFromDocument(Source, PageRange, 1);

    Dest.EndDoc;
  finally
    Source.Free;
    Dest.Free;
  end;
end;

پارامتر PageRange همان فرمت نمونه خط فرمان را می‌پذیرد: فهرستی از شماره‌ها یا محدوده‌های صفحه که با کاما جدا شده‌اند مانند '1-3' یا '1,5,7-9'. صفحات از یک شروع می‌شوند (1-based). InsertPagesFromDocument جریان‌های محتوا، دیکشنری‌های منبع و هندسه صفحه را کپی می‌کند بدون اینکه متادیتا، نشانک‌ها یا پیوست‌های فایل تعبیه‌شده را تغییر دهد، مگر اینکه از صفحات کپی‌شده به آن‌ها ارجاع داده شده باشد. برای یک استخراج سه صفحه‌ای از یک سند ۴۰ صفحه‌ای، این یک مجموعه کاری کوچک است

زمان‌بندی روی همان فایل ۱۲ مگابایتی که قبلاً دو دقیقه طول می‌کشید: با این الگو زیر ۱.۵ ثانیه است. بیشتر این زمان مربوط به یک فراخوانی LoadFromFile است. ساختار سند پس از آنکه جدول شیء برای بار اول حل شود، نامربوط خواهد بود

وقتی LoadFromFile بیش از حد است: API مستقیم فایل (Direct File API)

اگر فقط نیاز به شمارش صفحات، بررسی اطلاعات سند، یا کپی یک فایل بدون دستکاری محتویات آن دارید، API مستقیم فایل به طور کامل از تجزیه کامل جلوگیری می‌کند. DAOpenFileReadOnly جدول ارجاع متقاطع را بدون خارج کردن جریان‌های شیء از حالت فشرده نگاشت می‌کند، بنابراین پیچیدگی شمارش صفحه O(xref size) است تا O(file size):

procedure InspectPDF(const FileName: string);
var
  Pdf: THotPDF;
  Handle, PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Handle := Pdf.DAOpenFileReadOnly(FileName, '');
    if Handle <= 0 then
      Exit;
    try
      PageCount := Pdf.DAGetPageCount(Handle);
      Writeln('Pages: ', PageCount);

      // DACopyFile is a byte-preserving copy, no re-serialization
      Pdf.DACopyFile(FileName, 'archive-copy.pdf');
    finally
      Pdf.DACloseFile(Handle);
    end;
  finally
    Pdf.Free;
  end;
end;

نکته مهم: DAOpenFileReadOnly یک پارامتر رمز عبور می‌پذیرد اما برای ورودی‌های رمزگذاری‌شده به یک تجزیه کامل بازمی‌گردد، زیرا رمزگشایی به درخت شیء برای حل دیکشنری رمزگذاری نیاز دارد. اگر فایل‌های منبع شما رمزگذاری شده‌اند، ابتدا آن‌ها را با DecryptFile رمزگشایی کنید تا یک کپی بدون رمزگذاری دریافت کنید، سپس آن را با API مستقیم فایل باز کنید. تابع سطح فایل DecryptFile یک مسیر بازنویسی مستقیم AES-256 را برای رمزگذاری استاندارد در پیش می‌گیرد و برای فایل‌های بزرگ سریع‌تر از LoadFromFile به همراه SaveLoadedDocument است، زیرا مدل کامل شیء درون‌حافظه‌ای را نمی‌سازد

حافظه در طول پردازش دسته‌ای بزرگ

کارهای دسته‌ای که ده‌ها فایل را در یک حلقه پردازش می‌کنند دارای الگویی هستند که درست به نظر می‌رسد اما باعث انباشت حافظه می‌شود: ایجاد THotPDF داخل حلقه، فراخوانی LoadFromFile، انجام کار، فراخوانی Free. این ساختاراً مشکلی ندارد. مشکل زمانی است که کار درونی اشیاء موقتی تخصیص می‌دهد، استثناها را می‌گیرد و آن اشیاء موقتی را در مسیرهای خطا زنده نگه می‌دارد. مدیر حافظه Delphi متراکم نمی‌کند، بنابراین صد نشت مسیر خطا در طول یک اجرای دسته‌ای می‌تواند حافظه را به قدری بالا ببرد که تخصیص برای همه چیزهای دیگر را کند کند

راه‌حل عجیب و غریب نیست. هر THotPDF و هر TStream یا TBitmap میانی که در کار PDF مشارکت می‌کند متعلق به یک بلوک try/finally است که در آن Free آخرین دستور است. نشانگرهای محلی را قبل از try روی nil تنظیم کنید تا شاخه finally بتواند در صورت شکست مقداردهی اولیه در میان راه، به طور ایمن از if Assigned(x) then x.Free استفاده کند. این نظم مالکیت استاندارد Delphi است و داستان کامل برای این کلاس از مشکلات است

یک چیز دیگر که باید در زمینه‌های دسته‌ای بررسی شود: AddImage تصاویر را در یک لیست داخلی ثبت می‌کند که برای طول عمر نمونه THotPDF باقی می‌ماند. اگر از یک نمونه واحد در چندین سند با فراخوانی مکرر LoadFromFile دوباره استفاده کنید، ثبت‌های تصویر از اسناد قبلی در لیست باقی می‌مانند. یا یک نمونه تازه برای هر سند ایجاد کنید یا مسیر پاکسازی لیست تصویر را بین اسناد فراخوانی کنید

اندازه‌گیری قبل از تغییر هر چیزی

قبل از دست بردن به هر یک از این الگوها، اندازه‌گیری کنید. TStopwatch دلفی از System.Diagnostics، ابزار QueryPerformanceCounter را دربرمی‌گیرد و برای پروفایل زمان واقعی ورودی/خروجی فایل به اندازه کافی دقیق است. به تنهایی LoadFromFile را در آن قرار دهید و ببینید چقدر زمان می‌برد. اگر ۹۰٪ از کل زمان باشد، راه‌حل API مستقیم فایل یا کاهش دفعاتی است که همان فایل را تجزیه می‌کنید. اگر زیر ۲۰٪ باشد، تنگنا جای دیگری است و شما در حال پیگیری چیز اشتباهی هستید

استخراج دو دقیقه‌ای که این پست را شروع کرد، مشخص شد که کاملاً الگوی بارگیری مکرر بوده است. ساختار سند هیچ تأثیری نداشت؛ یک درخت صفحه مسطح نیز به همین شکل اجرا می‌شد. تغییر به یک فراخوانی LoadFromFile و به دنبال آن یک فراخوانی InsertPagesFromDocument بدون تغییر هیچ چیز دیگری، آن را به ۱.۳ ثانیه در همان سخت‌افزار رساند

API دستکاری صفحه نشان داده شده در اینجا بخشی از HotPDF Component برای Delphi و C++Builder است