在 AWS 中转换文档

如果您正在 AWS 上构建云原生应用程序,并需要强大的 PDF 生成、处理或转换功能,将 Aspose.PDF for .NET 集成到 AWS Lambda 函数中提供了一种强大且可扩展的解决方案。这种方法允许您在 AWS 的无服务器环境中利用 Aspose.PDF 的广泛功能,可能与其他服务如 S3 进行集成以进行存储。

本文将指导您在 AWS Lambda 中设置和运行 Aspose.PDF for .NET,涵盖基本的 PDF 创建并解决云中字体管理等常见挑战。

先决条件

  • 活跃的 AWS 账户。创建和部署 Lambda 函数所需。如果您没有账户,请在 aws.amazon.com 注册。
  • 安装了 AWS Toolkit for Visual Studio 的 Visual Studio 2017、2019 或 2022。这简化了项目创建、部署和测试。

在 AWS Lambda 中运行 Aspose.PDF for .NET 应用程序

按照以下步骤创建一个简单的 Lambda 函数,该函数使用 Aspose.PDF 生成 PDF 文档并将其保存到 Amazon S3:

  1. 创建 AWS Lambda 项目。在 Visual Studio 中,使用 AWS Lambda Project (.NET Core - C#) 模板创建一个新项目。在提示时选择 Empty Function 蓝图。这提供了一个基本的函数结构。
  2. 添加 NuGet 包。在解决方案资源管理器中右键单击您的项目,选择“管理 NuGet 包…”,并安装以下内容:
  • Aspose.PDF。用于 PDF 操作的核心库。
  • AWSSDK.S3。与 S3 存储交互的 AWS SDK for .NET 库。
  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…。按照向导配置并部署您的函数。
  • 部署后,您可以从 Visual Studio 的 AWS Explorer 或 AWS 管理控制台调用该函数。传递任何字符串作为输入。
  • 检查您的 S3 存储桶以获取生成的 PDF 文件(例如,AP_out_... .pdf)。

潜在问题:字体可用性

当您检查生成的 PDF 时,您可能会注意到文本没有使用您期望的标准字体(如 Arial 或 Times New Roman)。相反,Aspose.PDF 可能会使用后备字体。AWS Lambda 执行环境是最小的 Linux 容器。它们通常缺少 Windows 或桌面 Linux 发行版中常见的 TrueType 字体。当 Aspose.PDF 找不到指定或默认字体时,它会用可用的后备字体替代,以确保文本仍然被渲染。这可能会影响文档的视觉保真度。

如何使用存储在 S3 中的自定义字体与 Aspose.PDF for .NET

为了确保您的 PDF 使用正确的字体进行渲染,您需要在 Lambda 环境中将它们提供给 Aspose.PDF。将字体存储在 S3 存储桶中是云应用程序的一种灵活且常见的方法:

  • 上传字体到 S3。在您的 S3 存储桶中创建一个文件夹(例如,Fonts),并上传所需的 TrueType(.ttf)或 OpenType(.otf)字体文件。为了演示,您可以使用像“Noto Sans”这样的免费字体集。
  • 在 Lambda 中从 S3 加载字体。修改您的 Lambda 函数以从 S3 获取这些字体文件并将它们注册到 Aspose.PDF 的 FontRepository 中。

以下是您如何调整之前的 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;
            }
        }
    }
}