كيفية تحويل HTML إلى PDF باستخدام Azure Functions وwkhtmltopdf

دقائق القراءة: 12

مقدمة: لماذا نحتاج إلى تحويل HTML إلى PDF؟

يُعد إنشاء ملفات PDF تلقائياً من صفحات HTML من المهام الشائعة في الأنظمة الحديثة، خاصة عند بناء تطبيقات الأعمال والخدمات السحابية. فغالباً ما تحتاج الشركات إلى إنتاج فواتير، وتقارير طبية، ونماذج تأمين، وعروض أسعار، ومستندات رسمية قابلة للطباعة والأرشفة.

هناك أكثر من طريقة لتحقيق ذلك، لكن ليست كلها عملية على مستوى الإنتاج:

  • استخدام أدوات تحرير يدوية مثل أدوات التعبئة والتوقيع، وهي مناسبة للحالات الفردية فقط.
  • إنشاء ملف PDF مباشرة من التطبيق، وهي طريقة مقبولة للمستندات البسيطة جداً.
  • الاعتماد على أداة متخصصة مثل wkhtmltopdf لتحويل HTML إلى PDF بدقة ومرونة أعلى.

تُعد wkhtmltopdf خياراً ممتازاً لأنها مجانية، مفتوحة المصدر، وتعمل على منصات متعددة. وعند دمجها مع Azure Functions يمكننا بناء حل سحابي مرن وقابل للتوسع دون تحميل الخادم الرئيسي عبئاً إضافياً.

تحويل HTML إلى PDF باستخدام Azure Functions وأداة wkhtmltopdf في بيئة سحابية

المتطلبات الأساسية قبل البدء

قبل تنفيذ المشروع، تأكد من توفر العناصر التالية:

  • محرر VS Code مثبت على جهازك.
  • حساب فعّال على Azure Portal.
  • خطة Linux Basic (B1) App Service Plan، ويمكن أيضاً استخدام Windows Basic (B1) إن كانت متوفرة.
  • حساب Azure Storage لتخزين ملفات PDF الناتجة.
  • معرفة أساسية بنظام Linux وأوامر الطرفية.

لماذا نستخدم Azure Functions لهذه المهمة؟

عملية تحويل HTML إلى PDF ليست لحظية دائماً، بل قد تستهلك وقتاً وموارد، خاصة إذا كان المستند كبيراً أو يحتوي على تنسيقات معقدة. لذلك لا يُنصح بتنفيذها على الخادم الرئيسي المسؤول عن استقبال طلبات المستخدمين، لأن ذلك قد يسبب بطئاً في الاستجابة أو يؤثر في العمليات الأهم.

هنا تظهر قيمة Azure Functions، إذ تسمح لك بعزل هذه المهمة داخل وظيفة مستقلة تعمل عند الطلب عبر HTTP Trigger أو أي نوع آخر من المشغلات. هذا الأسلوب يوفر:

  • قابلية توسع أفضل.
  • عزلاً للمهام الثقيلة عن التطبيق الرئيسي.
  • خفضاً في التكلفة مقارنة ببناء خدمة منفصلة كاملة.
  • مرونة في الربط مع Azure Storage وخدمات المراقبة.

إنشاء مشروع Azure Functions محلياً

ابدأ أولاً بتثبيت أدوات Azure Functions Core Tools بحسب نظام التشغيل لديك. بعد الانتهاء، افتح سطر الأوامر ونفّذ الأمر التالي:

func init html2pdf

اسم المشروع هنا هو html2pdf، ويمكنك تغييره إلى أي اسم مناسب.

بعد تشغيل الأمر سيُطلب منك اختيار بيئة التشغيل worker runtime. اختر dotnet لأنه مناسب لهذا المشروع ويوفّر تكاملاً ممتازاً مع تقنيات مايكروسوفت.

سينشئ ذلك مجلداً باسم html2pdf داخل المسار الحالي. بعد ذلك افتح المشروع باستخدام VS Code، لأننا سنستفيد منه لاحقاً في النشر المباشر إلى Azure Functions.

إنشاء ملف الوظيفة Html2Pdf.cs

داخل المشروع، أنشئ ملفاً جديداً باسم Html2Pdf.cs. سنبدأ ببناء هيكل أولي لوظيفة تعمل عبر HTTP POST.

using System;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;

namespace Html2Pdf
{
    public class Html2Pdf
    {
        // The name of the function
        [FunctionName("Html2Pdf")]
        // The first arugment tells that the functions can be triggerd by a POST HTTP request.
        // The second argument is mainly used for logging information, warnings or errors
        public void Run([HttpTrigger(AuthorizationLevel.Function, "POST")] Html2PdfRequest Request, ILogger Log)
        {
        }
    }
}

في هذا الهيكل:

  • تحدد السمة [FunctionName("Html2Pdf")] اسم الوظيفة داخل Azure.
  • المعامل [HttpTrigger(AuthorizationLevel.Function, "POST")] يعني أن الوظيفة تُستدعى عبر طلب POST.
  • الكائن ILogger مفيد لتسجيل الأحداث والأخطاء أثناء التشغيل.

إنشاء نموذج الطلب Html2PdfRequest

بما أن الوظيفة تستقبل محتوى HTML واسم الملف، نحتاج إلى إنشاء نموذج بيانات يمثل الطلب. أضف ملفاً باسم Html2PdfRequest.cs وضع فيه الكود التالي:

namespace Html2Pdf
{
    public class Html2PdfRequest
    {
        // The HTML content that needs to be converted.
        public string HtmlContent { get; set; }

        // The name of the PDF file to be generated
        public string PDFFileName { get; set; }
    }
}

هذا النموذج بسيط لكنه مهم، لأنه يحدد البيانات المطلوبة لإنشاء الملف:

  • HtmlContent: المحتوى المراد تحويله.
  • PDFFileName: اسم ملف PDF الناتج.

إضافة مكتبة DinkToPdf إلى المشروع

حتى نتمكن من استدعاء wkhtmltopdf من كود .NET المُدار، نستخدم غلافاً برمجياً مناسباً. من أفضل الخيارات هنا مكتبة DinkToPdf، وهي تعتمد على P/Invoke للوصول إلى المكتبات غير المُدارة.

يمكنك إضافتها عبر NuGet باستخدام الأمر التالي من جذر المشروع:

dotnet add package DinkToPdf --version 1.0.8

ميزة هذه المكتبة أنها تخفي كثيراً من التفاصيل التقنية المعقدة، وتمنحك واجهة أبسط لتحويل المحتوى إلى PDF.

بناء دالة تحويل HTML إلى PDF

الآن أضف داخل الفئة Html2Pdf الكود التالي لإنشاء دالة مسؤولة عن تحويل المحتوى:

// Read more about converter on: https://github.com/rdvojmoc/DinkToPdf
// For our purposes we are going to use SynchronizedConverter
IPdfConverter pdfConverter = new SynchronizedConverter(new PdfTools());

// A function to convert html content to pdf based on the configuration passed as arguments
// Arguments:
// HtmlContent: the html content to be converted
// Width: the width of the pdf to be created. e.g. "8.5in", "21.59cm" etc.
// Height: the height of the pdf to be created. e.g. "11in", "27.94cm" etc.
// Margins: the margis around the content
// DPI: The dpi is very important when you want to print the pdf.
// Returns a byte array of the pdf which can be stored as a file
private byte[] BuildPdf(string HtmlContent, string Width, string Height, MarginSettings Margins, int? DPI = 180)
{
    // Call the Convert method of SynchronizedConverter "pdfConverter"
    return pdfConverter.Convert(
        new HtmlToPdfDocument()
        {
            // Set the html content
            Objects = { new ObjectSettings { HtmlContent = HtmlContent } },
            // Set the configurations
            GlobalSettings = new GlobalSettings
            {
                // PaperKind.A4 can also be used instead
                PaperSize = new PechkinPaperSize(Width, Height),
                DPI = DPI,
                Margins = Margins
            }
        });
}

تعيد هذه الدالة مصفوفة من النوع byte[] تمثل ملف PDF في الذاكرة، وهو ما يسهّل رفعه مباشرة إلى التخزين السحابي أو إرساله إلى خدمة أخرى.

أهم الإعدادات التي يمكنك تعديلها هنا:

  • أبعاد الصفحة مثل 8.5in و11in.
  • الدقة DPI، وهي مؤثرة جداً في جودة الطباعة.
  • الهوامش عبر MarginSettings.

استدعاء دالة التحويل داخل Run

بعد بناء دالة التحويل، يمكن استدعاؤها من داخل الوظيفة الرئيسية:

// PDFByteArray is a byte array of pdf generated from the HtmlContent
var PDFByteArray = BuildPdf(Request.HtmlContent, "8.5in", "11in", new MarginSettings(0, 0, 0, 0));

هنا يتم استخدام محتوى HTML القادم في الطلب وتحويله إلى ملف PDF بمقاس مناسب ودون هوامش.

رفع ملف PDF إلى Azure Storage

بعد إنشاء الملف في الذاكرة، تأتي الخطوة التالية وهي حفظه داخل حاوية Blob في Azure Storage. تأكد أولاً من إنشاء container مناسب، ثم أضف الكود التالي:

// The connection string of the Storage Account to which our PDF file will be uploaded
// Make sure to replace with your connection string.
var StorageConnectionString = "DefaultEndpointsProtocol=https;AccountName=<YOUR ACCOUNT NAME>;AccountKey=<YOUR ACCOUNT KEY>;EndpointSuffix=core.windows.net";

// Generate an instance of CloudStorageAccount by parsing the connection string
var StorageAccount = CloudStorageAccount.Parse(StorageConnectionString);

// Create an instance of CloudBlobClient to connect to our storage account
CloudBlobClient BlobClient = StorageAccount.CreateCloudBlobClient();

// Get the instance of CloudBlobContainer which points to a container name "pdf"
// Replace your own container name
CloudBlobContainer BlobContainer = BlobClient.GetContainerReference("pdf");

// Get the instance of the CloudBlockBlob to which the PDFByteArray will be uploaded
CloudBlockBlob Blob = BlobContainer.GetBlockBlobReference(Request.PDFFileName);

// Upload the pdf blob
await Blob.UploadFromByteArrayAsync(PDFByteArray, 0, PDFByteArray.Length);

هذه الخطوة تجعل الوظيفة أكثر عملية، لأن المستند الناتج لن يبقى في الذاكرة فقط، بل سيُحفظ في التخزين السحابي ليكون جاهزاً للتنزيل أو الأرشفة أو الإرسال لاحقاً.

بعد إضافة هذا الجزء ستلاحظ أنك بحاجة إلى:

  • إضافة تعليمات using الناقصة.
  • تغيير نوع إرجاع الدالة Run من void إلى async Task.

النسخة النهائية من ملف Html2Pdf.cs

using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using DinkToPdf;
using IPdfConverter = DinkToPdf.Contracts.IConverter;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using System.Threading.Tasks;

namespace Html2Pdf
{
    public class Html2Pdf
    {
        // Read more about converter on: https://github.com/rdvojmoc/DinkToPdf
        // For our purposes we are going to use SynchronizedConverter
        IPdfConverter pdfConverter = new SynchronizedConverter(new PdfTools());

        // A function to convert html content to pdf based on the configuration passed as arguments
        // Arguments:
        // HtmlContent: the html content to be converted
        // Width: the width of the pdf to be created. e.g. "8.5in", "21.59cm" etc.
        // Height: the height of the pdf to be created. e.g. "11in", "27.94cm" etc.
        // Margins: the margis around the content
        // DPI: The dpi is very important when you want to print the pdf.
        // Returns a byte array of the pdf which can be stored as a file
        private byte[] BuildPdf(string HtmlContent, string Width, string Height, MarginSettings Margins, int? DPI = 180)
        {
            // Call the Convert method of SynchronizedConverter "pdfConverter"
            return pdfConverter.Convert(
                new HtmlToPdfDocument()
                {
                    // Set the html content
                    Objects = { new ObjectSettings { HtmlContent = HtmlContent } },
                    // Set the configurations
                    GlobalSettings = new GlobalSettings
                    {
                        // PaperKind.A4 can also be used instead of width & height
                        PaperSize = new PechkinPaperSize(Width, Height),
                        DPI = DPI,
                        Margins = Margins
                    }
                });
        }

        // The name of the function
        [FunctionName("Html2Pdf")]
        // The first arugment tells that the functions can be triggerd by a POST HTTP request.
        // The second argument is mainly used for logging information, warnings or errors
        public async Task Run([HttpTrigger(AuthorizationLevel.Function, "POST")] Html2PdfRequest Request, ILogger Log)
        {
            // PDFByteArray is a byte array of pdf generated from the HtmlContent
            var PDFByteArray = BuildPdf(Request.HtmlContent, "8.5in", "11in", new MarginSettings(0, 0, 0, 0));

            // The connection string of the Storage Account to which our PDF file will be uploaded
            var StorageConnectionString = "DefaultEndpointsProtocol=https;AccountName=<YOUR ACCOUNT NAME>;AccountKey=<YOUR ACCOUNT KEY>;EndpointSuffix=core.windows.net";

            // Generate an instance of CloudStorageAccount by parsing the connection string
            var StorageAccount = CloudStorageAccount.Parse(StorageConnectionString);

            // Create an instance of CloudBlobClient to connect to our storage account
            CloudBlobClient BlobClient = StorageAccount.CreateCloudBlobClient();

            // Get the instance of CloudBlobContainer which points to a container name "pdf"
            // Replace your own container name
            CloudBlobContainer BlobContainer = BlobClient.GetContainerReference("pdf");

            // Get the instance of the CloudBlockBlob to which the PDFByteArray will be uploaded
            CloudBlockBlob Blob = BlobContainer.GetBlockBlobReference(Request.PDFFileName);

            // Upload the pdf blob
            await Blob.UploadFromByteArrayAsync(PDFByteArray, 0, PDFByteArray.Length);
        }
    }
}

إضافة مكتبة wkhtmltopdf إلى المشروع

حتى تعمل مكتبة DinkToPdf فعلياً، لا بد من توفير المكتبة الثنائية الخاصة بـ wkhtmltopdf داخل المشروع. وهنا يجب الانتباه إلى نوع النظام المستهدف في بيئة Azure App Service Plan.

في هذا السيناريو تم اختيار Linux Basic (B1) لأنه أوفر تكلفة من الخطة المكافئة على Windows. أثناء إعداد هذا الدليل كانت بيئة Azure App Service تعمل على Debian 10 بمعمارية amd64.

توفر DinkToPdf مكتبات مُجمّعة مسبقاً لأنظمة متعددة:

  • libwkhtmltox.so لنظام Linux.
  • libwkhtmltox.dll لنظام Windows.
  • libwkhtmltox.dylib لنظام MacOS.

بالنسبة لخطة Linux، حمّل الملف libwkhtmltox.so وضعه في جذر المشروع.

بنية مشروع Azure Functions بعد إضافة ملف libwkhtmltox.so إلى جذر المشروع

تحديث ملف csproj لنسخ المكتبة عند البناء والنشر

للتأكد من تضمين ملف .so ضمن مخرجات البناء والنشر، افتح ملف csproj وأضف العنصر التالي داخل ItemGroup:

<None Update="./libwkhtmltox.so">
  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  <CopyToPublishDirectory>Always</CopyToPublishDirectory>
</None>

وإذا أردت رؤية الملف كاملاً بعد التحديث، فسيكون كالتالي:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <AzureFunctionsVersion>v3</AzureFunctionsVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="DinkToPdf" Version="1.0.8" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.11" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
    <None Update="./libwkhtmltox.so">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Always</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>

إنشاء مورد Azure Functions App من البوابة

قبل النشر، عليك إنشاء تطبيق الوظائف من خلال Azure Portal. أثناء الإعداد، احرص على اختيار خطة لا تقل عن Basic، مع تحديد نظام التشغيل Linux.

تفاصيل إنشاء Azure Functions App داخل Azure Portalاختيار خطة Basic ونظام Linux عند إعداد Azure Functions App

من الأفضل أيضاً تفعيل Application Insights لأنه يساعدك على تتبع السجلات والأداء واكتشاف الأخطاء بسرعة، وتكلفته غالباً منخفضة جداً.

تفعيل Application Insights أثناء إنشاء Azure Functions App للمراقبة وتحليل السجلات

بعد ذلك تابع إلى Tags ثم أنشئ المورد. قد تستغرق العملية بضع دقائق.

نشر المشروع إلى Azure Functions عبر VS Code

بعد إنشاء المورد، يمكنك نشر المشروع مباشرة من VS Code. ثبّت إضافة Azure Functions من سوق الإضافات أولاً.

إضافة Azure Functions في VS Code من Marketplace

بعد التثبيت، سيظهر رمز Azure في الشريط الجانبي. انقر عليه ثم سجّل الدخول إلى حسابك.

واجهة تسجيل الدخول إلى Azure من خلال إضافة Azure Functions في VS Code

بعد تسجيل الدخول ستظهر لك قائمة تطبيقات الوظائف المتاحة:

قائمة تطبيقات Azure Functions داخل VS Code بعد تسجيل الدخول

للنشر:

  1. اضغط F1.
  2. اختر Azure Functions: Deploy to Function App....
  3. حدد التطبيق الذي أنشأته حديثاً.
  4. وافق على رسالة التأكيد وابدأ النشر.

قد تستغرق عملية النشر عدة دقائق حسب حجم المشروع والاتصال.

تهيئة wkhtmltopdf داخل بيئة Azure

بعد النشر، تبقى خطوة أخيرة مهمة: نقل ملف libwkhtmltox.so إلى الموقع الصحيح داخل تطبيق الوظائف.

انتقل إلى Azure Portal ثم افتح تطبيق الوظائف، وابحث في الشريط الجانبي عن SSH وافتحه.

فتح جلسة SSH داخل Azure Functions App لإدارة الملفات على الخادم

بعد فتح نافذة SSH، انتقل إلى مجلد bin بالأمر التالي:

cd /home/site/wwwroot/bin

عند تنفيذ الأمر ls قد لا تجد الملف libwkhtmltox.so داخل هذا المجلد، لأنه غالباً موجود في المسار /home/site/wwwroot. لذا انسخه إلى bin باستخدام الأمر التالي:

cp ../libwkhtmltox.so libwkhtmltox.so

بعد هذه الخطوة تصبح المكتبة في الموقع الذي تحتاجه الوظيفة أثناء التشغيل.

كيفية استدعاء الوظيفة واختبارها

قبل اختبار الوظيفة، تحتاج إلى الحصول على المفتاح السري Code الخاص بها، وهو مطلوب لاستدعائها بشكل آمن.

للحصول عليه:

  1. افتح Azure Portal.
  2. انتقل إلى تطبيق الوظائف الخاص بك.
  3. من القائمة الجانبية ابحث عن Functions.
  4. اختر الوظيفة Html2Pdf.
  5. افتح قسم Function Keys.
  6. انسخ المفتاح الافتراضي.

البحث عن Functions داخل Azure Portal للوصول إلى وظيفة Html2Pdfالوصول إلى Function Keys ونسخ كود الاستدعاء السري لوظيفة Azure

الآن يمكنك اختبار الوظيفة من خلال تطبيق Console بسيط في .NET باستخدام الكود التالي:

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace Demo.ConsoleApp
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            string AzureFunctionsUrl = "https://<Your Base Url>/api/Html2Pdf?code=<Replace with your Code>";

            using (HttpClient client = new HttpClient())
            {
                var Request = new Html2PdfRequest
                {
                    HtmlContent = "<h1>Hello World</h1>",
                    PDFFileName = "hello-world.pdf"
                };

                string json = JsonConvert.SerializeObject(Request);
                var buffer = System.Text.Encoding.UTF8.GetBytes(json);
                var byteContent = new ByteArrayContent(buffer);
                byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");

                using (HttpResponseMessage res = await client.PostAsync(AzureFunctionsUrl, byteContent))
                {
                    if (res.StatusCode != HttpStatusCode.NoContent)
                    {
                        throw new Exception("There was an error uploading the pdf");
                    }
                }
            }
        }
    }

    public class Html2PdfRequest
    {
        // The HTML content that needs to be converted.
        public string HtmlContent { get; set; }

        // The name of the PDF file to be generated
        public string PDFFileName { get; set; }
    }
}

بعد استبدال عنوان الخدمة والمفتاح الصحيحين وتشغيل التطبيق، سيتم إنشاء ملف باسم hello-world.pdf داخل الحاوية pdf في Azure Storage.

ملاحظات تقنية مهمة لتحسين الاعتمادية

رغم أن المثال السابق عملي وواضح، فإن بيئة الإنتاج تتطلب بعض التحسينات الإضافية، مثل:

  • التحقق من صحة المدخلات قبل بدء التحويل، خاصة محتوى HtmlContent واسم الملف.
  • إضافة معالجة أخطاء باستخدام كتل try/catch.
  • تخزين Storage Connection String داخل إعدادات التطبيق بدلاً من تضمينه في الكود.
  • تسجيل الأحداث عبر Application Insights لتسهيل التشخيص.
  • تقييد حجم المحتوى المرسل للوظيفة لتفادي الاستهلاك المفرط للموارد.

هذه الممارسات تعزز الأمان والاستقرار، وتساعد على تشغيل الخدمة بكفاءة في البيئات الحقيقية.

مقارنة سريعة بين الخيارات المتاحة

الطريقة المزايا القيود
الأدوات اليدوية سهلة للحالات الفردية غير قابلة للتوسع
إنشاء PDF مباشرة من التطبيق مناسب للمستندات البسيطة مرونة أقل في التنسيق المعقد
wkhtmltopdf مع Azure Functions مرن، اقتصادي، وقابل للتوسع يتطلب إعداداً أولياً أدق

الخاتمة

يوفر الدمج بين Azure Functions وwkhtmltopdf حلاً قوياً لتحويل HTML إلى PDF في البيئات السحابية. صحيح أن الإعداد الأولي يحتاج إلى بعض العناية، خاصة فيما يتعلق بالمكتبات الثنائية ومسارات النشر، لكنه في المقابل يمنحك حلاً منخفض التكلفة ومرناً وقابلاً للتشغيل دون خادم تقليدي دائم.

إذا كنت تبني نظاماً يحتاج إلى إنشاء مستندات تلقائياً، فهذه المقاربة مناسبة جداً، خصوصاً عندما تريد فصل المهام الثقيلة عن التطبيق الأساسي مع الاستفادة من خدمات Azure الأخرى مثل التخزين والمراقبة.

الخلاصة التقنية

من الناحية التقنية، يُعد استخدام Azure Functions مع DinkToPdf وwkhtmltopdf خياراً عملياً لإنشاء مستندات PDF عند الطلب دون الحاجة إلى خادم مخصص دائم التشغيل. التحدي الأكبر ليس في كتابة الكود، بل في ضبط بيئة التشغيل وموضع المكتبات الأصلية بشكل صحيح. وعند تنفيذ هذا الإعداد بعناية، تحصل على حل سحابي اقتصادي ومرن يصلح لتطبيقات الفوترة والتقارير والأرشفة الرقمية على نطاق جيد.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *