Ei,
Implementei recentemente a geração de Captcha em meu AWS Lambda .NET Core 2 API. Eu poderia jurar que já estava funcionando, mas agora está simplesmente falhando. Eu examinei a implementação atual de System.Drawing do .NET Core; no entanto, pelo que estou lendo, as dependências explícitas do GDI + tornam essa biblioteca não compatível com várias plataformas. Após uma investigação mais aprofundada, existe um CompatCore.System.Drawing que foi originalmente desenvolvido pela equipe Mono. Supostamente, esta é uma versão de plataforma cruzada de System.Drawing que é capaz de usar a implementação baseada em Linux do GDI +; no entanto, a tentativa de usá-lo simplesmente gera o seguinte erro / rastreamento de pilha:
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()
De acordo com o seguinte tópico: https://github.com/CoreCompat/CoreCompat/issues/3
Eu deveria ter sido capaz de incluir a referência da DLL libgdiplus adicionando o pacote NuGet runtime.linux-x64.CoreCompat.System.Drawing; no entanto, isso foi em vão para mim. Tenho tentado várias combinações de referências de biblioteca na esperança de que esse seja um problema semelhante que muitas pessoas já viram com o System.Cryptography.x509Certificates; mas ainda não encontrei uma combinação que permita que isso funcione.
Alguém mais conseguiu colocar a funcionalidade System.Drawing em execução no AWS Lambda?
Eu ouvi falar sobre o uso de libgdiplus para System.Drawing no Linux, mas ainda não tentei. Para manipulação de imagens no Lambda, tenho usado a última versão do Magick.NET-Q16-AnyCPU . Não fiz nenhum desenho com ele, suponho que você precisaria para o Captcha, mas vejo que tem alguns recursos de desenho .
Meu problema parece ser que libgdiplus não é uma instalação padrão na instância do Lambda Linux. Supostamente, o runtime.linux-x64.CoreCompat.System.Drawing era um pacote NuGet que simplesmente executava scripts para instalar esta biblioteca via apt-get na inicialização do aplicativo; no entanto, depois de examinar o código deste projeto no link abaixo, não consegui entender exatamente como isso funciona ou por que pode estar falhando para mim.
Vou continuar procurando por uma solução por enquanto; mas, aquele link do Magick.NET parece ser um método válido de gerar algumas imagens Captcha, então vou ter certeza de manter isso como um fallback. Eu pesquisei o uso do System.Drawing nativo, pois parece que foi desenvolvido para uma melhor compatibilidade entre plataformas, mas ainda parece que eles estão de alguma forma fazendo referência à biblioteca GDI + instalada,
Você viu este pacote Magick.NET usado com sucesso no Lambda?
Sim, o Magick.NET foi atualizado alguns meses antes de começar a funcionar em várias plataformas. Tudo que eu precisava fazer era adicioná-lo ao projeto. Abaixo está uma função Lambda que escrevi para criar miniaturas de imagens com ela.
<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}");
}
}
}
}
Estou apenas entrando no Magick e acho que funcionará lindamente. Você não só tem acesso a uma API de manipulação de imagens relativamente diversa, como também parece ser uma plataforma cruzada bem suportada. Muito obrigado pela sugestão, um pouco mais de esforço de desenvolvimento e eu devo ser capaz de superar esse obstáculo sem problemas!
Ainda estou curioso para saber como algumas pessoas parecem ter conseguido colocar a implementação GDI + em funcionamento. Não vejo nenhuma maneira de obter a biblioteca libgdiplus apropriada instalada no Lambda antes da execução em tempo de execução.
Parece que a API ImageMagick Drawables pode não ser compatível com várias plataformas com AWS Lambda, pelo que estou vendo. Ao implementar a biblioteca e executar a função Draw (), estou gerando o seguinte erro:
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()
Não importa o que eu faça, não consigo fazer nenhuma funcionalidade de desenho funcionar no Lambda. Tentei usar System.Drawing (Microsoft), CoreCompat.System.Drawing (Mono) e ImageMagick (terceiros), mas todos aparentemente sem sucesso. Cada biblioteca de geração de imagem parece ter algumas dependências de sistema importantes que estou tendo dificuldade em lidar com o AWS Lambda. Vou continuar a experimentar e relatar se tiver sucesso.
Existe alguma chance de que possamos personalizar nosso tempo de execução Lambda no futuro de forma semelhante a como podemos personalizar os contêineres do Docker? Tudo isso poderia ser resolvido com uma simples execução para instalar libgdiplus em minha instância Lambda.
Sou um idiota e nunca deveria ter gerado imagens Captcha em meu servidor AWS Lambda quando poderia simplesmente ter usado o Google ReCaptcha. Dessa forma, meu AWS Lambda lida apenas com o código de verificação e nunca precisa fazer referência a qualquer forma de funcionalidade de desenho.
Eu ainda gostaria de ver um método de implementação de alguma forma de funcionalidade de desenho no AWS Lambda no futuro; no entanto, vou colocar esse problema em banho-maria e atribuir isso a uma péssima decisão de design da minha parte por enquanto.
Talvez as camadas de implantação do Lambda possam remediar esse problema até que o suporte adequado esteja disponível no .Net Core em algum ponto.
https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html
Comentários muito úteis
Sim, o Magick.NET foi atualizado alguns meses antes de começar a funcionar em várias plataformas. Tudo que eu precisava fazer era adicioná-lo ao projeto. Abaixo está uma função Lambda que escrevi para criar miniaturas de imagens com ela.