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アプリケーションを実行する

以下の手順に従って、Aspose.PDFを使用してPDFドキュメントを生成し、Amazon S3に保存するシンプルなLambda関数を作成します。

  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でプロジェクトを右クリックし、**Publish to AWS Lambda…**を選択します。ウィザードに従って関数を構成し、デプロイします。
  • デプロイが完了したら、Visual StudioのAWSエクスプローラーまたはAWS Management Consoleから関数を呼び出すことができます。任意の文字列を入力として渡します。
  • 生成されたPDFファイル(例:AP_out_... .pdf)がS3バケットにあるか確認します。

潜在的な問題:フォントの可用性

生成された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;
            }
        }
    }
}