تحويل المستندات في AWS

إذا كنت تقوم ببناء تطبيقات سحابية أصلية على AWS وتحتاج إلى قدرات قوية لتوليد PDF أو التلاعب به أو تحويله، فإن دمج Aspose.PDF for .NET في وظائف AWS Lambda يوفر حلاً قويًا وقابلًا للتوسع. تتيح لك هذه الطريقة الاستفادة من الميزات الواسعة لـ Aspose.PDF ضمن بيئة AWS بدون خادم، مع إمكانية التكامل مع خدمات أخرى مثل S3 للتخزين.

توجهك هذه المقالة خلال إعداد وتشغيل Aspose.PDF for .NET في AWS Lambda، مع تغطية إنشاء PDF الأساسي ومعالجة التحديات الشائعة مثل إدارة الخطوط في السحابة.

المتطلبات الأساسية

  • حساب AWS نشط. مطلوب لإنشاء ونشر وظائف Lambda. إذا لم يكن لديك واحد، قم بالتسجيل في aws.amazon.com.
  • Visual Studio 2017 أو 2019 أو 2022 مع تثبيت AWS Toolkit for Visual Studio. هذا يبسط إنشاء المشاريع والنشر والاختبار.

تشغيل تطبيق Aspose.PDF for .NET في AWS Lambda

اتبع هذه الخطوات لإنشاء وظيفة Lambda بسيطة تقوم بإنشاء مستند PDF باستخدام Aspose.PDF وحفظه في Amazon S3:

  1. إنشاء مشروع AWS Lambda. في Visual Studio، أنشئ مشروعًا جديدًا باستخدام قالب AWS Lambda Project (.NET Core - C#). اختر نموذج Empty Function عند الطلب. يوفر هذا هيكل وظيفة أساسي.
  2. إضافة حزم NuGet. انقر بزر الماوس الأيمن على مشروعك في مستكشف الحلول، واختر “إدارة حزم NuGet…"، وقم بتثبيت ما يلي:
  • Aspose.PDF. المكتبة الأساسية للتلاعب بـ PDF.
  • AWSSDK.S3. مكتبة AWS SDK لـ .NET للتفاعل مع تخزين S3.
  1. تنفيذ وظيفة Lambda. استبدل محتويات ملف معالج الوظيفة الخاص بك (مثل Function.cs) بالكود التالي. هذا المثال ينشئ مستند PDF أساسي مع نص ويحفظه في دلو S3.
using System;
using System.IO;
using System.Threading.Tasks;
using Amazon.Lambda.Core;
using Amazon.S3;
using Amazon.S3.Model;
using Aspose.Pdf;
using Aspose.Pdf.Text;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace TestAsposePdfLambda
{
    public class Function
    {
        private IAmazonS3 S3Client { get; set; }
        private const string BucketName = "your-s3-bucket-name";

        /// <summary>
        /// Default constructor. Initializes the S3 client.
        /// </summary>
        public Function()
        {
            S3Client = new AmazonS3Client();
            // Consider setting the License here if needed, e.g., in a static constructor
            Aspose.Pdf.License lic = new Aspose.Pdf.License();
            // Assumes license file is an embedded resource
            lic.SetLicense("Aspose.PDF.lic");
        }

        /// <summary>
        /// Lambda function handler: Creates a PDF document and saves it to S3.
        /// </summary>
        public async Task<string> FunctionHandler(string input, ILambdaContext context)
        {
            context.Logger.LogLine($"Function processing input: {input}");

            // Create PDF document
            Document pdfDocument = new Document();

            // Add a page
            Page page = pdfDocument.Pages.Add();

            // Add text elements
            page.Paragraphs.Add(new TextFragment($"Hello {input} from Aspose.PDF!"));
            page.Paragraphs.Add(new TextFragment($"You are running on: {System.Environment.OSVersion.VersionString}"));

            // Save the PDF to a MemoryStream
            using (MemoryStream ms = new MemoryStream())
            {
                // Aspose.PDF saves directly to PDF format
                pdfDocument.Save(ms);
                // Reset stream position for reading
                ms.Position = 0;

                // Upload the stream to S3
                string outputKey = $"AP_out_{DateTime.UtcNow:yyyyMMddHHmmss}.pdf";
                context.Logger.LogLine($"Attempting to upload {outputKey} to bucket {BucketName}");
                bool putResult = await PutS3Object(BucketName, outputKey, ms, context);

                return putResult ? $"OK - PDF saved as s3://{BucketName}/{outputKey}" : "FAILED to upload PDF to S3";
            }
        }

        /// <summary>
        /// Helper method to upload a stream to an S3 bucket.
        /// </summary>
        private async Task<bool> PutS3Object(string bucket, string key, Stream content, ILambdaContext context)
        {
            try
            {
                PutObjectRequest request = new PutObjectRequest
                {
                    BucketName = bucket,
                    Key = key,
                    InputStream = content,
                    // Set appropriate content type
                    ContentType = "application/pdf"
                };
                var response = await S3Client.PutObjectAsync(request);
                context.Logger.LogLine($"S3 PutObject Response: {response.HttpStatusCode}");
                return response.HttpStatusCode == System.Net.HttpStatusCode.OK;
            }
            catch (AmazonS3Exception s3ex)
            {
                context.Logger.LogLine($"Error uploading to S3: {s3ex.Message} (AWS Request ID: {s3ex.RequestId}, Error Code: {s3ex.ErrorCode})");
                return false;
            }
            catch (Exception ex)
            {
                context.Logger.LogLine($"General error during S3 upload: {ex.Message}");
                return false;
            }
        }
    }
}
  1. نشر واختبار
  • استبدل "your-s3-bucket-name" في الكود باسم دلو S3 الذي لديك حق الوصول للكتابة إليه.
  • انقر بزر الماوس الأيمن على المشروع في Visual Studio واختر نشر إلى AWS Lambda…. اتبع المعالج لتكوين ونشر وظيفتك.
  • بمجرد النشر، يمكنك استدعاء الوظيفة من مستكشف AWS في Visual Studio أو من وحدة تحكم إدارة AWS. مرر أي سلسلة كمدخل.
  • تحقق من دلو S3 الخاص بك للملف PDF الذي تم إنشاؤه (مثل AP_out_... .pdf).

مشكلة محتملة: توفر الخطوط

عند فحص PDF الذي تم إنشاؤه، قد تلاحظ أن النص لا يستخدم الخطوط القياسية التي تتوقعها (مثل Arial أو Times New Roman). بدلاً من ذلك، قد تستخدم Aspose.PDF خطًا احتياطيًا. بيئات تنفيذ AWS Lambda هي حاويات Linux بسيطة. عادةً ما تفتقر إلى خطوط TrueType الشائعة الموجودة على Windows أو توزيعات Linux المكتبية. عندما لا تستطيع Aspose.PDF العثور على الخطوط المحددة أو الافتراضية، فإنها تستبدلها بخطوط احتياطية متاحة لضمان عرض النص. يمكن أن يؤثر ذلك على دقة الوثيقة المرئية.

كيفية استخدام خطوط مخصصة مخزنة في S3 مع Aspose.PDF for .NET

لضمان عرض ملفات PDF الخاصة بك مع الخطوط الصحيحة، تحتاج إلى توفيرها لـ Aspose.PDF ضمن بيئة Lambda. يعد تخزين الخطوط في دلو S3 نهجًا مرنًا وشائعًا للتطبيقات السحابية:

  • تحميل الخطوط إلى S3. أنشئ مجلدًا (مثل Fonts) في دلو S3 الخاص بك وقم بتحميل ملفات الخطوط TrueType (.ttf) أو OpenType (.otf) اللازمة. لأغراض العرض، يمكنك استخدام مجموعة متاحة مجانًا مثل “Noto Sans”.
  • تحميل الخطوط من S3 في Lambda. عدل وظيفة Lambda الخاصة بك لجلب هذه الملفات الخطية من S3 وتسجيلها مع FontRepository الخاص بـ Aspose.PDF.

إليك كيفية تعديل كود وظيفة Lambda السابقة:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Amazon.Lambda.Core;
using Amazon.S3;
using Amazon.S3.Model;
using Aspose.Pdf;
using Aspose.Pdf.Text;

// Assembly attribute
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace TestAsposePdfLambda
{
    public class Function
    {
        private IAmazonS3 S3Client { get; set; }
        private const string BucketName = "your-s3-bucket-name"; // Replace with your bucket name
        private const string FontsS3Folder = "Fonts/"; // Folder in your bucket containing .ttf/.otf files

        private static bool _fontsLoaded = false; // Flag to load fonts only once per container instance
        private static readonly object _fontLoadLock = new object(); // Lock for thread safety

        /// <summary>
        /// Static constructor: Ensures fonts are loaded when the class is first accessed
        /// within a Lambda execution environment instance.
        /// </summary>
        static Function()
        {
            // Consider setting the License here if needed
            Aspose.Pdf.License lic = new Aspose.Pdf.License();
            lic.SetLicense("Aspose.PDF.lic");
        }

        /// <summary>
        /// Default constructor. Initializes S3 client.
        /// </summary>
        public Function()
        {
            S3Client = new AmazonS3Client();
            // Ensure fonts are loaded
            EnsureFontsLoaded(S3Client, BucketName, FontsS3Folder);
        }

        /// <summary>
        /// Lambda function handler: Creates a PDF with custom fonts loaded from S3.
        /// </summary>
        public async Task<string> FunctionHandler(string input, ILambdaContext context)
        {
            context.Logger.LogLine($"Function processing input: {input}");
            // Ensure fonts loaded (important for warm starts)
            EnsureFontsLoaded(S3Client, BucketName, FontsS3Folder, context);

            // Create PDF document
            Document pdfDocument = new Document();

            // Add a page
            Page page = pdfDocument.Pages.Add();

            // Create TextFragment and specify the font
            TextFragment titleFragment = new TextFragment($"Hello {input} from Aspose.PDF!");

            // Attempt to find the font loaded from S3. Use the actual font name
            titleFragment.TextState.Font = FontRepository.FindFont("Noto Sans");
            // If the font wasn't found/loaded, FindFont might return a default/fallback font
            titleFragment.TextState.FontSize = 14;
            page.Paragraphs.Add(titleFragment);

            TextFragment infoFragment = new TextFragment($"Running on: {System.Environment.OSVersion.VersionString}");
            // Example using a specific style
            infoFragment.TextState.Font = FontRepository.FindFont("Noto Sans Regular");
            infoFragment.TextState.FontSize = 10;
            page.Paragraphs.Add(infoFragment);

            // Save PDF to stream
            using (MemoryStream ms = new MemoryStream())
            {
                pdfDocument.Save(ms);
                ms.Position = 0;

                // Upload to S3
                string outputKey = $"AP_Font_out_{DateTime.UtcNow:yyyyMMddHHmmss}.pdf";
                context.Logger.LogLine($"Attempting to upload {outputKey} to bucket {BucketName}");
                bool putResult = await PutS3Object(BucketName, outputKey, ms, context);
                return putResult ? $"OK - PDF saved as s3://{BucketName}/{outputKey}" : "FAILED to upload PDF to S3";
            }
        }

        /// <summary>
        /// Loads fonts from S3 into Aspose.PDF's FontRepository if not already loaded.
        /// </summary>
        private void EnsureFontsLoaded(IAmazonS3 s3Client, string bucketName, string fontsFolderKey, ILambdaContext context = null)
        {
            // Prevent multiple threads/invocations trying to load simultaneously
            lock (_fontLoadLock)
            {
                if (_fontsLoaded)
                {
                    return;
                }

                context?.Logger.LogLine("Attempting to load fonts from S3...");
                try
                {
                    // Get font sources from S3
                    var fontSources = Task.Run(async () => await GetS3FontSources(s3Client, bucketName, fontsFolderKey, context)).Result;

                    if (fontSources.Any())
                    {
                        // Clear existing default sources (optional, ensures only S3 fonts are primary)
                        // FontRepository.Sources.Clear();

                        // Add the sources loaded from S3
                        FontRepository.Sources.AddRange(fontSources);
                        context?.Logger.LogLine($"Successfully loaded {fontSources.Count()} font sources from S3.");
                        _fontsLoaded = true;
                    }
                    else
                    {
                        context?.Logger.LogLine("No font sources found in S3 folder.");
                        // Set _fontsLoaded to true anyway to avoid retrying every invocation if folder is empty/missing
                        _fontsLoaded = true;
                    }
                }
                catch (AggregateException aggEx) when (aggEx.InnerException is AmazonS3Exception s3Ex)
                {
                    context?.Logger.LogLine($"S3 Error loading fonts: {s3Ex.Message} (Request ID: {s3Ex.RequestId}, Error Code: {s3Ex.ErrorCode}) - Check bucket/folder name and permissions.");
                    // Avoid retrying constantly on permission errors
                    _fontsLoaded = true;
                }
                catch (Exception ex)
                {
                    context?.Logger.LogLine($"Error loading fonts from S3: {ex.ToString()}");
                    // Decide if you want to retry or not. Setting _fontsLoaded = true prevents retries.
                    _fontsLoaded = true;
                }
            }
        }

        /// <summary>
        /// Lists font files in an S3 folder and creates MemoryFontSource for each.
        /// </summary>
        private static async Task<List<MemoryFontSource>> GetS3FontSources(IAmazonS3 client, string bucketName, string fontsFolderKey, ILambdaContext context)
        {
            List<MemoryFontSource> fontSources = new List<MemoryFontSource>();
            ListObjectsV2Request request = new ListObjectsV2Request()
            {
                BucketName = bucketName,
                // e.g., "Fonts/"
                Prefix = fontsFolderKey,
            };

            context?.Logger.LogLine($"Listing objects in {bucketName}/{fontsFolderKey}");
            ListObjectsV2Response response;
            do
            {
                // Requires s3:ListBucket permission on the bucket
                response = await client.ListObjectsV2Async(request);

                foreach (S3Object entry in response.S3Objects)
                {
                    // Skip the folder itself and non-font files (simple check)
                    if (entry.Key.EndsWith("/") || !(entry.Key.EndsWith(".ttf", StringComparison.OrdinalIgnoreCase) || entry.Key.EndsWith(".otf", StringComparison.OrdinalIgnoreCase)))
                    {
                        continue;
                    }

                    context?.Logger.LogLine($"Found font file: {entry.Key}");
                    try
                    {
                        // Requires s3:GetObject permission on the font files
                        GetObjectRequest fontRequest = new GetObjectRequest
                        {
                            BucketName = bucketName,
                            Key = entry.Key
                        };
                        using (GetObjectResponse fontResponse = await client.GetObjectAsync(fontRequest))
                        {
                            using (MemoryStream ms = new MemoryStream())
                            {
                                await fontResponse.ResponseStream.CopyToAsync(ms);
                                // IMPORTANT: Aspose.PDF needs the raw byte array for MemoryFontSource.
                                // It manages the stream internally after this.
                                fontSources.Add(new MemoryFontSource(ms.ToArray()));
                                context?.Logger.LogLine($" -- Added MemoryFontSource for {entry.Key}");
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        context?.Logger.LogLine($" -- Failed to load font {entry.Key}: {ex.Message}");
                        // Decide how to handle failures - continue or stop?
                    }
                }
                request.ContinuationToken = response.NextContinuationToken;
            } while (response.IsTruncated);

            return fontSources;
        }

        /// <summary>
        /// Helper method to upload a stream to an S3 bucket.
        /// </summary>
        private async Task<bool> PutS3Object(string bucket, string key, Stream content, ILambdaContext context)
        {
            try
            {
                PutObjectRequest request = new PutObjectRequest
                {
                    BucketName = bucket,
                    Key = key,
                    InputStream = content,
                    // Set appropriate content type
                    ContentType = "application/pdf"
                };
                var response = await S3Client.PutObjectAsync(request);
                context.Logger.LogLine($"S3 PutObject Response: {response.HttpStatusCode}");
                return response.HttpStatusCode == System.Net.HttpStatusCode.OK;
            }
            catch (AmazonS3Exception s3ex)
            {
                context.Logger.LogLine($"Error uploading to S3: {s3ex.Message} (AWS Request ID: {s3ex.RequestId}, Error Code: {s3ex.ErrorCode})");
                return false;
            }
            catch (Exception ex)
            {
                context.Logger.LogLine($"General error during S3 upload: {ex.Message}");
                return false;
            }
        }
    }
}