Aws-lambda-dotnet: Поддерживает ли AWS Lambda вызовы System.Drawing с помощью libgdiplus?

Созданный на 18 апр. 2018  ·  7Комментарии  ·  Источник: aws/aws-lambda-dotnet

Привет,

Недавно я реализовал создание Captcha в своем API AWS Lambda .NET Core 2. Я мог бы поклясться, что когда-то он работал, но сейчас он просто выходит из строя. Я изучил текущую реализацию .NET Core System.Drawing; однако из того, что я читаю, явные зависимости от GDI + делают эту библиотеку несовместимой с разными платформами. При дальнейшем исследовании обнаруживается CompatCore.System.Drawing, который изначально был разработан командой Mono. Предположительно, это кроссплатформенная версия System.Drawing, способная использовать реализацию GDI + на базе Linux; однако попытка его использования просто вызывает следующую ошибку / трассировку стека:

System.TypeInitializationException: The type initializer for 'System.Drawing.GDIPlus' threw an exception. ---> System.DllNotFoundException: Unable to load DLL 'gdiplus': The specified module or one of its dependencies could not be found.
 (Exception from HRESULT: 0x8007007E)
   at System.Drawing.GDIPlus.GdiplusStartup(UInt64& token, GdiplusStartupInput& input, GdiplusStartupOutput& output)
   at System.Drawing.GDIPlus..cctor()
   --- End of inner exception stack trace ---
   at System.Drawing.GDIPlus.GdipCreateBitmapFromScan0(Int32 width, Int32 height, Int32 stride, PixelFormat format, IntPtr scan0, IntPtr& bmp)
   at System.Drawing.Bitmap..ctor(Int32 width, Int32 height, PixelFormat format)
   at Ivy.Captcha.Services.CaptchaGenerationService.GenerateCaptchaImage(Int32 captchaCharLength, Int32 width, Int32 height)
   at IAGE.Api.Data.Services.CaptchaApiService.GenerateCaptcha() in D:\Workspaces\iam-global-education\src\Api\IAGE.Api.Data\Services\CaptchaApiService.cs:line 63
   at lambda_method.lambda_method(Closure , Object , Object[] )
   at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.d__12.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.d__10.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.d__14.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.d__22.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.d__17.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.d__15.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Builder.RouterMiddleware.d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.d__6.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware.d__7.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Elmah.Io.AspNetCore.ElmahIoMiddleware.d__6.MoveNext()

Согласно следующему потоку: https://github.com/CoreCompat/CoreCompat/issues/3

Я должен был иметь возможность включить libgdiplus DLL ref, добавив NuGet-пакет runtime.linux-x64.CoreCompat.System.Drawing; однако для меня это было бесполезно. Я пробовал различные комбинации библиотечных ссылок, надеясь, что это будет аналогичная проблема, которую многие люди видели с System.Cryptography.x509Certificates; но мне еще предстоит найти комбинацию, которая позволила бы этому работать.

Удалось ли кому-нибудь еще запустить функциональность System.Drawing на AWS Lambda?

guidance

Самый полезный комментарий

Да, Magick.NET был обновлен через пару месяцев, чтобы сразу перейти к работе с кросс-платформой. Все, что мне нужно было сделать, это добавить его в проект. Ниже приведена лямбда-функция, которую я написал для создания с ее помощью эскизов изображений.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
    <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Amazon.Lambda.Core" Version="1.0.0" />
    <PackageReference Include="Amazon.Lambda.Serialization.Json" Version="1.1.0" />
    <PackageReference Include="Amazon.Lambda.S3Events" Version="1.0.2" />
    <PackageReference Include="AWSSDK.S3" Version="3.3.16.2" />
    <PackageReference Include="AWSXRayRecorder.Handlers.AwsSdk" Version="2.1.0-beta" />
    <PackageReference Include="Magick.NET-Q16-AnyCPU" Version="7.4.2" />
  </ItemGroup>

  <ItemGroup>
    <DotNetCliToolReference Include="Amazon.Lambda.Tools" Version="2.1.1" />
  </ItemGroup>

</Project>
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using Amazon.Lambda.Core;
using Amazon.Lambda.S3Events;
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.XRay.Recorder.Core;
using ImageMagick;

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

namespace ImageResizer
{
    public class Function
    {
        IAmazonS3 S3Client { get; set; }

        /// <summary>
        /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment
        /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the
        /// region the Lambda function is executed in.
        /// </summary>


        public Function()
        {
            Amazon.XRay.Recorder.Handlers.AwsSdk.AWSSDKHandler.RegisterXRayForAllServices();
            S3Client = new AmazonS3Client();
        }

        /// <summary>
        /// Constructs an instance with a preconfigured S3 client. This can be used for testing the outside of the Lambda environment.
        /// </summary>
        /// <param name="s3Client"></param>
        public Function(IAmazonS3 s3Client)
        {
            this.S3Client = s3Client;
        }

        HashSet<string> SupportedImageTypes { get; } = new HashSet<string> { ".png", ".jpg", ".jpeg" };

        /// <summary>
        /// This method is called for every Lambda invocation. This method takes in an S3 event object and can be used 
        /// to respond to S3 notifications.
        /// </summary>
        /// <param name="evnt"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public async Task FunctionHandlerAsync(S3Event evnt, ILambdaContext context)
        {
            foreach (var record in evnt.Records)
            {
                var bucket = record.S3.Bucket.Name;
                var key = record.S3.Object.Key;

                if (key.StartsWith("thumbnails"))
                {
                    context.Logger.LogLine($"Object s3://{bucket}/{key} is already a thumbnail");
                    continue;
                }
                if (!SupportedImageTypes.Contains(Path.GetExtension(key.ToLower())))
                {
                    context.Logger.LogLine($"Object s3://{bucket}/{key} is not a supported image type");
                    continue;
                }

                context.Logger.LogLine($"Processing s3://{bucket}/{key}");

                MemoryStream resizedImageStream;
                using (var response = await this.S3Client.GetObjectAsync(bucket, key))
                {
                    AWSXRayRecorder.Instance.BeginSubsegment("Magick Image Resize");
                    try
                    {
                        using (MagickImageCollection collection = new MagickImageCollection(response.ResponseStream))
                        {
                            foreach (MagickImage image in collection)
                            {
                                image.Resize(200, 200);
                            }
                            context.Logger.LogLine($"   Image resized");

                            resizedImageStream = new MemoryStream();
                            collection.Write(resizedImageStream);
                            resizedImageStream.Position = 0;
                        }
                    }
                    catch(Exception e)
                    {
                        AWSXRayRecorder.Instance.AddException(e);
                        throw;
                    }
                    finally
                    {
                        AWSXRayRecorder.Instance.EndSubsegment();
                    }
                }

                var index = key.LastIndexOf('/');
                var thumbnailKey = "thumbnails/" + (index != -1 ? key.Substring(index + 1) : key);
                await this.S3Client.PutObjectAsync(new PutObjectRequest
                {
                    BucketName = bucket,
                    Key = thumbnailKey,
                    InputStream = resizedImageStream
                });

                context.Logger.LogLine($"   Thumbnail saved to s3://{bucket}/{thumbnailKey}");
            }
        }
    }
}

Все 7 Комментарий

Я слышал об использовании libgdiplus для System.Drawing в Linux, но не пробовал. Для обработки изображений в Lambda я использовал последнюю версию Magick.NET-Q16-AnyCPU . Я не рисовал с его помощью, который, как я полагаю, вам понадобится для Captcha, но я вижу, что у него есть некоторые возможности рисования .

Моя проблема заключается в том, что libgdiplus не является установкой по умолчанию в экземпляре Lambda Linux. Предположительно, runtime.linux-x64.CoreCompat.System.Drawing был пакетом NuGet, который просто выполнял сценарии для установки этой библиотеки через apt-get при запуске приложения; однако, покопавшись в коде этого проекта по приведенной ниже ссылке, я не смог точно понять, как это работает и почему у меня может возникнуть сбой.

https://github.com/CoreCompat/CoreCompat/tree/master/native/runtime.linux-x64.CoreCompat.System.Drawing

Я продолжу искать решение пока; но эта ссылка Magick.NET, по-видимому, является допустимым методом создания некоторых изображений Captcha, поэтому я обязательно сохраню ее в качестве запасного варианта. Я рассмотрел возможность использования собственного System.Drawing, поскольку, похоже, он был разработан для лучшей кросс-платформенной совместимости, но все же кажется, что они каким-то образом ссылаются на установленную библиотеку GDI +, хотя,

Вы видели, как этот пакет Magick.NET успешно используется на Lambda?

Да, Magick.NET был обновлен через пару месяцев, чтобы сразу перейти к работе с кросс-платформой. Все, что мне нужно было сделать, это добавить его в проект. Ниже приведена лямбда-функция, которую я написал для создания с ее помощью эскизов изображений.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
    <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Amazon.Lambda.Core" Version="1.0.0" />
    <PackageReference Include="Amazon.Lambda.Serialization.Json" Version="1.1.0" />
    <PackageReference Include="Amazon.Lambda.S3Events" Version="1.0.2" />
    <PackageReference Include="AWSSDK.S3" Version="3.3.16.2" />
    <PackageReference Include="AWSXRayRecorder.Handlers.AwsSdk" Version="2.1.0-beta" />
    <PackageReference Include="Magick.NET-Q16-AnyCPU" Version="7.4.2" />
  </ItemGroup>

  <ItemGroup>
    <DotNetCliToolReference Include="Amazon.Lambda.Tools" Version="2.1.1" />
  </ItemGroup>

</Project>
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using Amazon.Lambda.Core;
using Amazon.Lambda.S3Events;
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.XRay.Recorder.Core;
using ImageMagick;

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

namespace ImageResizer
{
    public class Function
    {
        IAmazonS3 S3Client { get; set; }

        /// <summary>
        /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment
        /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the
        /// region the Lambda function is executed in.
        /// </summary>


        public Function()
        {
            Amazon.XRay.Recorder.Handlers.AwsSdk.AWSSDKHandler.RegisterXRayForAllServices();
            S3Client = new AmazonS3Client();
        }

        /// <summary>
        /// Constructs an instance with a preconfigured S3 client. This can be used for testing the outside of the Lambda environment.
        /// </summary>
        /// <param name="s3Client"></param>
        public Function(IAmazonS3 s3Client)
        {
            this.S3Client = s3Client;
        }

        HashSet<string> SupportedImageTypes { get; } = new HashSet<string> { ".png", ".jpg", ".jpeg" };

        /// <summary>
        /// This method is called for every Lambda invocation. This method takes in an S3 event object and can be used 
        /// to respond to S3 notifications.
        /// </summary>
        /// <param name="evnt"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public async Task FunctionHandlerAsync(S3Event evnt, ILambdaContext context)
        {
            foreach (var record in evnt.Records)
            {
                var bucket = record.S3.Bucket.Name;
                var key = record.S3.Object.Key;

                if (key.StartsWith("thumbnails"))
                {
                    context.Logger.LogLine($"Object s3://{bucket}/{key} is already a thumbnail");
                    continue;
                }
                if (!SupportedImageTypes.Contains(Path.GetExtension(key.ToLower())))
                {
                    context.Logger.LogLine($"Object s3://{bucket}/{key} is not a supported image type");
                    continue;
                }

                context.Logger.LogLine($"Processing s3://{bucket}/{key}");

                MemoryStream resizedImageStream;
                using (var response = await this.S3Client.GetObjectAsync(bucket, key))
                {
                    AWSXRayRecorder.Instance.BeginSubsegment("Magick Image Resize");
                    try
                    {
                        using (MagickImageCollection collection = new MagickImageCollection(response.ResponseStream))
                        {
                            foreach (MagickImage image in collection)
                            {
                                image.Resize(200, 200);
                            }
                            context.Logger.LogLine($"   Image resized");

                            resizedImageStream = new MemoryStream();
                            collection.Write(resizedImageStream);
                            resizedImageStream.Position = 0;
                        }
                    }
                    catch(Exception e)
                    {
                        AWSXRayRecorder.Instance.AddException(e);
                        throw;
                    }
                    finally
                    {
                        AWSXRayRecorder.Instance.EndSubsegment();
                    }
                }

                var index = key.LastIndexOf('/');
                var thumbnailKey = "thumbnails/" + (index != -1 ? key.Substring(index + 1) : key);
                await this.S3Client.PutObjectAsync(new PutObjectRequest
                {
                    BucketName = bucket,
                    Key = thumbnailKey,
                    InputStream = resizedImageStream
                });

                context.Logger.LogLine($"   Thumbnail saved to s3://{bucket}/{thumbnailKey}");
            }
        }
    }
}

Я только начинаю заниматься магией и думаю, что это прекрасно сработает. Мало того, что у вас есть доступ к относительно разнообразному API для работы с изображениями, он также хорошо поддерживается кросс-платформенным. Большое спасибо за предложение, еще немного усилий по разработке, и я смогу преодолеть это препятствие без проблем!

Мне все еще любопытно, как некоторые люди смогли запустить и запустить реализацию GDI +. Я не вижу способа, которым можно было бы установить соответствующую библиотеку libgdiplus в Lambda до выполнения во время выполнения.

Похоже, что API ImageMagick Drawables может некорректно быть кроссплатформенным с AWS Lambda из того, что я вижу. После реализации библиотеки и выполнения функции Draw () возникает следующая ошибка:

ImageMagick.MagickDrawErrorException: NonconformingDrawingPrimitiveDefinition `text' @ error/draw.c/DrawImage/3402
   at ImageMagick.MagickExceptionHelper.Check(IntPtr exception)
   at ImageMagick.DrawingWand.NativeDrawingWand.Render()
   at ImageMagick.MagickImage.Draw(IEnumerable`1 drawables)
   at Ivy.Captcha.Magick.Services.MagickCaptchaGenerationService.GenerateCaptchaImage(Int32 captchaCharLength, Int32 width, Int32 height)
   at IAGE.Api.Data.Services.CaptchaApiService.GenerateCaptcha() in D:\Workspaces\iam-global-education\src\Api\IAGE.Api.Data\Services\CaptchaApiService.cs:line 63
   at lambda_method.lambda_method(Closure , Object , Object[] )
   at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.d__12.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.d__10.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.d__14.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.d__22.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.d__17.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.d__15.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Builder.RouterMiddleware.d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.d__6.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware.d__7.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Elmah.Io.AspNetCore.ElmahIoMiddleware.d__6.MoveNext()

Независимо от того, что я делаю, мне кажется, что никакие функции рисования не работают в Lambda. Я пытался использовать System.Drawing (Microsoft), CoreCompat.System.Drawing (Mono) и ImageMagick (3rd Party), но все, казалось, безрезультатно. Кажется, что каждая библиотека генерации изображений имеет некоторые основные системные зависимости, которые мне трудно обойти с помощью AWS Lambda. Я продолжу экспериментировать и сообщу, если добьюсь успеха.

Есть ли шанс, что мы когда-нибудь сможем настроить нашу среду выполнения Lambda в будущем, подобно тому, как мы можем настраивать контейнеры Docker? Все это можно решить, просто установив libgdiplus в мой экземпляр Lambda.

Я идиот, и мне никогда не следовало создавать изображения Captcha на моем сервере AWS Lambda, когда я мог бы просто использовать Google ReCaptcha. Таким образом, моя AWS Lambda обрабатывает только проверочный код и никогда не ссылается на какие-либо функции рисования.

Я все еще хотел бы увидеть метод реализации какой-либо формы функции рисования на AWS Lambda в будущем; однако я собираюсь отложить эту проблему на второй план и списать это на ужасное дизайнерское решение с моей стороны.

Возможно, уровни развертывания Lambda могут решить эту проблему до тех пор, пока в какой-то момент в .Net Core не появится надлежащая поддержка.

https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html

Была ли эта страница полезной?
0 / 5 - 0 рейтинги