imazen / imageflow-server

A super-fast image server to speed up your site - deploy as a microservice, serverless, or embeddable.
https://docs.imageflow.io
GNU Affero General Public License v3.0
254 stars 33 forks source link

Imageflow throws 'wrong input color space' error on jpg handled successfully by ImageResizer #51

Closed peaeater closed 3 years ago

peaeater commented 3 years ago

I'm using Imageflow.Server 0.5.10 and Imageflow.Server.HybridCache 0.5.10, and getting the following error: Imageflow.Bindings.ImageflowException: ImageMalformed: Error 9: Wrong input color space on transform (ObjectCreationError).

The image, a black and white jpeg, is attached. The same image is successfully handled by Image Resizer 4.0.5.94.

P4207

Exception stack:

2021-09-21 09:33:03.4583|1|ERROR|Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware|An unhandled exception has occurred while executing the request. Imageflow.Bindings.ImageflowException: ImageMalformed: Error 9: Wrong input color space on transform (ObjectCreationError) at
imageflow_core\src\codecs\color_transform_cache.rs:126:197
https://github.com/imazen/imageflow/blob/1f6dc67eef41132d5d3300676daa5d42ae7f8941/imageflow_core\src\codecs\color_transform_cache.rs#L126
imageflow_core\src\codecs\color_transform_cache.rs:196:152
https://github.com/imazen/imageflow/blob/1f6dc67eef41132d5d3300676daa5d42ae7f8941/imageflow_core\src\codecs\color_transform_cache.rs#L196
imageflow_core\src\codecs\mozjpeg_decoder.rs:367:35
https://github.com/imazen/imageflow/blob/1f6dc67eef41132d5d3300676daa5d42ae7f8941/imageflow_core\src\codecs\mozjpeg_decoder.rs#L367
imageflow_core\src\flow\nodes\codecs_and_pointer.rs:222:65
https://github.com/imazen/imageflow/blob/1f6dc67eef41132d5d3300676daa5d42ae7f8941/imageflow_core\src\flow\nodes\codecs_and_pointer.rs#L222
imageflow_core\src\flow\execution_engine.rs:477:114
https://github.com/imazen/imageflow/blob/1f6dc67eef41132d5d3300676daa5d42ae7f8941/imageflow_core\src\flow\execution_engine.rs#L477
imageflow_core\src\context.rs:419:59
https://github.com/imazen/imageflow/blob/1f6dc67eef41132d5d3300676daa5d42ae7f8941/imageflow_core\src\context.rs#L419
imageflow_core\src\context_methods.rs:50:68
https://github.com/imazen/imageflow/blob/1f6dc67eef41132d5d3300676daa5d42ae7f8941/imageflow_core\src\context_methods.rs#L50
Active node:
NodeDebugInfo {
    stable_id: 2,
    params: Json(
        Decode {
            io_id: 0,
            commands: None,
        },
    ),
    index: NodeIndex(1),
}

   at Imageflow.Bindings.JobContext.AssertReady()
   at Imageflow.Bindings.JobContext.SendJsonBytes(String method, Byte[] utf8Json)
   at Imageflow.Bindings.JobContext.Execute[T](T message)
   at Imageflow.Fluent.ImageJob.FinishAsync(JobExecutionOptions executionOptions, SecurityOptions securityOptions, CancellationToken cancellationToken)
   at Imageflow.Server.ImageJobInfo.ProcessUncached()
   at Imageflow.Server.ImageflowMiddleware.<>c__DisplayClass15_0.<<ProcessWithStreamCache>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Imazen.HybridCache.AsyncCache.<>c__DisplayClass35_0.<<GetOrCreateBytes>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Imazen.Common.Concurrency.AsyncLockProvider.TryExecuteAsync(String key, Int32 timeoutMs, CancellationToken cancellationToken, Func`1 success)
   at Imazen.HybridCache.AsyncCache.GetOrCreateBytes(Byte[] key, AsyncBytesResult dataProviderCallback, CancellationToken cancellationToken, Boolean retrieveContentType)
   at Imazen.HybridCache.HybridCache.GetOrCreateBytes(Byte[] key, AsyncBytesResult dataProviderCallback, CancellationToken cancellationToken, Boolean retrieveContentType)
   at Imageflow.Server.ImageflowMiddleware.ProcessWithStreamCache(HttpContext context, String cacheKey, ImageJobInfo info)
   at Imageflow.Server.ImageflowMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)|url: https://bvm.andornot.com/media/All%20Numbered%20Photographs/P4200%20-%20P4299/P4207.jpg|action: |Microsoft.AspNetCore.Diagnostics.DiagnosticsLoggerExtensions.UnhandledException| body: 

Startup.cs:

using System;
using System.Globalization;
using System.IO;
using Andi.Web.Authentication;
using Andi.Web.Heart.Domain;
using Andi.Web.Heart.Search;
using Andi.Web.Options;
using Andi.Web.Schema;
using Andi.Web.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Imageflow.Fluent;
using Imageflow.Server;
using Imageflow.Server.HybridCache;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Rotativa.AspNetCore;
using SolrNet;
using System.Collections.Generic;
using System.Linq;

namespace Andi.Web
{
  public class Startup
  {
    public Startup(IConfiguration configuration, IWebHostEnvironment env)
    {
      Configuration = configuration;
      Env = env;
    }

    public IConfiguration Configuration { get; }
    public IWebHostEnvironment Env { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
      services.Configure<CookiePolicyOptions>(options =>
      {
        // This lambda determines whether user consent for non-essential cookies is needed for a given request.
        options.CheckConsentNeeded = context => false;
        options.MinimumSameSitePolicy = SameSiteMode.Lax;
      });

      // localization
      services.AddLocalization(opt => opt.ResourcesPath = "Resources/resx");

      // in-memory cache
      services.AddMemoryCache();

      // options/config
      services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
      var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();
      services.Configure<IpWhitelistSettings>(Configuration.GetSection("IpWhitelist"));
      services.Configure<SimpleAuthenticationSettings>(Configuration.GetSection("SimpleAuthentication"));
      services.Configure<PdfViewerSettings>(Configuration.GetSection("PdfViewer"));

      // used for http context current, getting route data
      services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
      services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();

      // solrnet
      services.AddSolrNet(appSettings.SolrUrl);
      services.AddSolrNet<Document>(new Uri(new Uri($"{appSettings.SolrUrl.TrimEnd('/')}/"), appSettings.SolrCore).AbsoluteUri);

      // imageflow
      services.AddImageflowHybridCache(new HybridCacheOptions(Path.Combine(Env.ContentRootPath, "imagecache")));

      // file providers: e.g. wwwroot, media, any other 'virtual directory'
      var providers = new List<IFileProvider>
      {
        Env.WebRootFileProvider
      };
      foreach (var vdir in appSettings.VirtualDirectories)
      {
        var provider = new PhysicalFileProvider(
          Path.IsPathFullyQualified(vdir.PhysicalPath)
          ? vdir.PhysicalPath
          : Path.Join(Env.ContentRootPath, vdir.PhysicalPath));
        providers.Add(provider);
      }
      var compositeFileProvider = new CompositeFileProvider(providers);
      services.AddSingleton<IFileProvider>(compositeFileProvider);

      // internal services
      services.AddScoped<ISearchService, SearchService>();
      services.AddScoped<ISearchParameterMapper, SolrSearchParameterMapper>();
      services.AddSingleton<IDocumentMappingManager, SolrDocumentMappingManager>();
      services.AddTransient<IQueryParser, SolrQueryParser>();
      services.AddScoped<ISearchOperations, SolrSearchOperations>();
      services.AddScoped<INavService, NavService>();
      services.AddScoped<IOpenUrlService, OpenUrlService>();
      services.AddTransient<IFreetextFactory, FreetextFactory>();
      services.AddTransient<IResourceService, ResourceService>();
      services.AddScoped<IMailService, MailService>();
      services.AddTransient<IViewRenderingService, ViewRenderingService>();
      services.AddScoped<IAuthenticatorService, SimpleAuthenticatorService>();
      services.AddScoped<IPdfViewerService, PdfViewerService>();
      services.AddScoped<IDisqusService, DisqusService>();
      services.AddScoped<IPdfImageService, PdfImageService>();

      // mvc
      services.AddControllersWithViews(o =>
        {
          if (appSettings.EnableAuthentication)
          {
            // Require authorized user Set default [Authorize] filter.
            // Override on controllers/actions with [AllowAnonymous] or [Authorize(PolicyName="SomePolicy")].
            var policy = new AuthorizationPolicyBuilder()
              .RequireAuthenticatedUser()
              .Build();
            o.Filters.Add(new AuthorizeFilter(policy));
          }
        })
        .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
        .AddDataAnnotationsLocalization();

      // Cookie-based authentication settings
      if (appSettings.EnableAuthentication)
      {
        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
          .AddCookie(options =>
          {
            options.AccessDeniedPath = "/error/403";
            options.LoginPath = "/auth/login";
          });
      }

    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
      var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();
      var media = appSettings.VirtualDirectories.Single(vd => vd.Name.Equals("media", StringComparison.OrdinalIgnoreCase));

      if (env.IsDevelopment())
      {
        app.UseDeveloperExceptionPage();
      }
      else
      {
        // handle 5xx error codes
        app.UseExceptionHandler("/error");
        app.UseHsts();
      }

      // handle 4xx error codes
      app.UseStatusCodePagesWithReExecute("/error/{0}");

      app.UseHttpsRedirection();
      app.UseDefaultFiles();

      // imageflow
      app.UseImageflow(new ImageflowMiddlewareOptions()
        .SetMapWebRoot(true)
        .SetLicenseKey(EnforceLicenseWith.RedDotWatermark, "obfuscated")
        .SetDiagnosticsPageAccess(env.IsDevelopment() ? AccessDiagnosticsFrom.AnyHost : AccessDiagnosticsFrom.LocalHost)
        // map 'media' to file location
        .MapPath(media.RequestPath, Path.IsPathFullyQualified(media.PhysicalPath) ? media.PhysicalPath : Path.Join(Env.ContentRootPath, media.PhysicalPath))
        .SetAllowCaching(true)
        // set client cache header to 30 days
        .SetDefaultCacheControlString("public, max-age=2592000")
        // limit image sizes
        .SetJobSecurityOptions(new SecurityOptions()
          .SetMaxDecodeSize(new FrameSizeLimit(8000, 8000, 30))
          .SetMaxEncodeSize(new FrameSizeLimit(8000, 8000, 30))
          .SetMaxFrameSize(new FrameSizeLimit(8000, 8000, 30)))
        // configure a watermark
        .AddWatermark(new NamedWatermark("andornot", "/img/bvmuseum-logo.png",
          new WatermarkOptions()
            .SetFitBoxLayout(new WatermarkFitBox(WatermarkAlign.Image, 10, 10, 90, 90), WatermarkConstraintMode.Within, new ConstraintGravity(100, 100))
            .SetOpacity(0.7f)
            .SetHints(new ResampleHints()
              .SetResampleFilters(InterpolationFilter.Robidoux_Sharp, null)
              .SetSharpen(7, SharpenWhen.Downscaling))
            .SetMinCanvasSize(500, 500)))
        // force watermark on all media images
        .AddRewriteHandler(media.RequestPath, args =>
        {
          args.Query["watermark"] = "andornot";
        })
      );

      // serve static files from wwwroot (must come after imageflow)
      app.UseStaticFiles();

      // serve static files from media folder
      // and add mime mappings
      var extProvider = new FileExtensionContentTypeProvider();
      extProvider.Mappings.Add(".dzi", "text/xml");
      app.UseStaticFiles(new StaticFileOptions
      {
        ContentTypeProvider = extProvider,
        FileProvider = new PhysicalFileProvider(Path.IsPathFullyQualified(media.PhysicalPath) ? media.PhysicalPath : Path.Join(env.ContentRootPath, media.PhysicalPath)),
        RequestPath = media.RequestPath
      });

      // localization cultures
      var cultures = new[]
      {
        new CultureInfo("en-CA"),
        new CultureInfo("fr-CA")
      };
      app.UseRequestLocalization(new RequestLocalizationOptions
      {
        DefaultRequestCulture = new RequestCulture("en-CA", "en-CA"),
        SupportedCultures = cultures,
        SupportedUICultures = cultures
      });

      app.UseRouting();

      // authentication
      if (appSettings.EnableAuthentication)
      {
        if (appSettings.EnableIpWhitelist)
        {
          // before .UseAuth to avoid unnecessary login redirects
          app.UseMiddleware<IpWhitelistAuthenticator>();
        }
        // .UseAuth must come after routing but before endpoints
        app.UseAuthentication();
        app.UseAuthorization();
      }

      app.UseEndpoints(endpoints =>
        {
          endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
        });

      // rotativa wkhtmltopdf
      RotativaConfiguration.Setup(env.ContentRootPath, "Rotativa");

      // nreco poppler
      NReco.PdfRenderer.License.SetLicenseKey("obfuscated", "obfuscated");
    }
  }
}
lilith commented 3 years ago

&ignore_icc_errors=true controls this behavior, and you can set it as a default command for all images. ImageResizer silently ignores this, which can be a bad thing.

peaeater commented 3 years ago

Ah, thank you. I've set it as a default command in Startup because for me it's only a bad thing if the image can't be viewed.

app.UseImageflow(new ImageflowMiddlewareOptions()
.AddCommandDefault("ignore_icc_errors", "true")
);