saucecontrol / PhotoSauce

MagicScaler high-performance, high-quality image processing pipeline for .NET
http://photosauce.net/
MIT License
569 stars 49 forks source link

Issue with WebP support #107

Closed indy-singh closed 1 year ago

indy-singh commented 1 year ago
var fromBase64String = Convert.FromBase64String("UklGRsACAABXRUJQVlA4ILQCAADQFACdASogACAAPgQBQAAACJZgCdMoRwN67+Ev6q3gc8l87/Jn+q9DN6gH6V7wD9d/2A4QD9M+sA9AD9VfSZ/VX4Hf2H/ZL4AP00uzv4p+FfUDdevUbJcvDPwx/JbUGfyX8gP1V/uWcBfFv6N+MH4zbID+Af3r8ldd7/YPxm/oHwt5kXmH/T+4F/D/5H/Vfxy/vX/n5Uv9VQ7UDRfbyfP1PPTq52tzX06Y9zudfwUAAP7//rtpve1v1ky90qXpBgB4bus9vIL83KeFODqf4yUU3dFCg7cpPgl4CoP0eeD10Ftln5z6s/X91zc8dD/+bkoO9fdPp/d2eCb38BYpPTAFj7/oYCH+XcVa8sdWHvIGzrPbbXWf1XUlpHva//yG3kDP7YmnGKaFRTUwFTUKxF/r/3Rdt/Yl8P/+dsLM0ltaduY0Ehm/o9kxzW+OmBxf/f+49m+Tt03lHkL/BRkAyGrr+cUQIVaHdEskP3sXCf0NNPxE8fttsaS8eJtEPu18qWantFIuCZ3E9LiWG69lqPcRotHVUw/VAcKsBqfW7UMRU6D+kThFE0EoHWUc+oX5S5JtSx+RvxkBA1fUYyI8MUJX6hEih6Wk5OJi2Bsd258iyTKyMqpDO4TSdJN//7Lj9AJGif3//R4TxeL/U0c5xUgklQ90yWBTFgdWn7c5asm+vIWFzvyisqziwZHlAOb53da9fPrdu6O2h3M2nAu6e+DMY0+Cmak7zOBYuApaayrELZwoqTo1L1JDSardQEN9qyzIYRAoWC7UQ2bEewV5EuPRJFqW6DWDlCffGP1OT9N7tdn2g0I9Z/DJi8c0v//fa8qhD/45DRmdzj46hftRgmeQLfFk+5tNiqbcViSRMqMaGL6siXavzqvDx6oPJN5MQoh8781530v4T1a0Ghw4BSf24AAAAA==");

using (var processingPipeline = MagicImageProcessor.BuildPipeline(new MemoryStream(fromBase64String), new ProcessImageSettings()))
{
    var imageWidth = processingPipeline.Settings.Width;
    var imageHeight = processingPipeline.Settings.Height;

    Console.WriteLine(imageWidth);
    Console.WriteLine(imageHeight);
}

Blows up with:-

Unhandled Exception: System.NotSupportedException: The WIC decoder with CLSID '7693e886-51c9-4070-8419-9f70738ec8fa' for MIME type 'image/webp' could not be instantiated.  This codec should be unregistered.
   at PhotoSauce.MagicScaler.WicImageDecoder.TryLoad(Guid clsid, String mime, Stream stream, IDecoderOptions options)
   at PhotoSauce.MagicScaler.WindowsCodecExtensions.<>c__DisplayClass1_0.<getWicCodecs>b__1(Stream stm, IDecoderOptions opt)
   at PhotoSauce.MagicScaler.CodecCollection.GetDecoderForStream(Stream stm, IDecoderOptions options)
   at PhotoSauce.MagicScaler.MagicImageProcessor.BuildPipeline(Stream imgStream, ProcessImageSettings settings)

So I tried https://www.nuget.org/packages/PhotoSauce.NativeCodecs.Libwebp as per https://github.com/saucecontrol/PhotoSauce/discussions/92#discussioncomment-3562947 and updated sample:-

var fromBase64String = Convert.FromBase64String("UklGRsACAABXRUJQVlA4ILQCAADQFACdASogACAAPgQBQAAACJZgCdMoRwN67+Ev6q3gc8l87/Jn+q9DN6gH6V7wD9d/2A4QD9M+sA9AD9VfSZ/VX4Hf2H/ZL4AP00uzv4p+FfUDdevUbJcvDPwx/JbUGfyX8gP1V/uWcBfFv6N+MH4zbID+Af3r8ldd7/YPxm/oHwt5kXmH/T+4F/D/5H/Vfxy/vX/n5Uv9VQ7UDRfbyfP1PPTq52tzX06Y9zudfwUAAP7//rtpve1v1ky90qXpBgB4bus9vIL83KeFODqf4yUU3dFCg7cpPgl4CoP0eeD10Ftln5z6s/X91zc8dD/+bkoO9fdPp/d2eCb38BYpPTAFj7/oYCH+XcVa8sdWHvIGzrPbbXWf1XUlpHva//yG3kDP7YmnGKaFRTUwFTUKxF/r/3Rdt/Yl8P/+dsLM0ltaduY0Ehm/o9kxzW+OmBxf/f+49m+Tt03lHkL/BRkAyGrr+cUQIVaHdEskP3sXCf0NNPxE8fttsaS8eJtEPu18qWantFIuCZ3E9LiWG69lqPcRotHVUw/VAcKsBqfW7UMRU6D+kThFE0EoHWUc+oX5S5JtSx+RvxkBA1fUYyI8MUJX6hEih6Wk5OJi2Bsd258iyTKyMqpDO4TSdJN//7Lj9AJGif3//R4TxeL/U0c5xUgklQ90yWBTFgdWn7c5asm+vIWFzvyisqziwZHlAOb53da9fPrdu6O2h3M2nAu6e+DMY0+Cmak7zOBYuApaayrELZwoqTo1L1JDSardQEN9qyzIYRAoWC7UQ2bEewV5EuPRJFqW6DWDlCffGP1OT9N7tdn2g0I9Z/DJi8c0v//fa8qhD/45DRmdzj46hftRgmeQLfFk+5tNiqbcViSRMqMaGL6siXavzqvDx6oPJN5MQoh8781530v4T1a0Ghw4BSf24AAAAA==");

CodecManager.Configure(codecs => {
    codecs.UseLibwebp();
});

using (var processingPipeline = MagicImageProcessor.BuildPipeline(new MemoryStream(fromBase64String), new ProcessImageSettings()))
{
    var imageWidth = processingPipeline.Settings.Width;
    var imageHeight = processingPipeline.Settings.Height;

    Console.WriteLine(imageWidth);
    Console.WriteLine(imageHeight);
}

Then the exception changes to:-

Unhandled Exception: System.DllNotFoundException: Unable to load DLL 'webpdemux': The specified module could not be found. (Exception from HRESULT: 0x8007007E)
   at PhotoSauce.Interop.Libwebp.Libwebpdemux.WebPDemuxInternal(WebPData* param0, Int32 param1, WebPDemuxState* param2, Int32 param3)
   at PhotoSauce.NativeCodecs.Libwebp.WebpContainer.TryLoad(Stream imgStream, IDecoderOptions options)
   at PhotoSauce.MagicScaler.CodecCollection.GetDecoderForStream(Stream stm, IDecoderOptions options)
   at PhotoSauce.MagicScaler.MagicImageProcessor.BuildPipeline(Stream imgStream, ProcessImageSettings settings)

Environment:-

OS Name:                   Microsoft Windows Server 2019 Standard
OS Version:                10.0.17763 N/A Build 17763

Unfortunately, we are using net framework 472 (we are in the processing of slowly moving to dotnet-6.0).

I get the same error with dotnet-6.0 until I install PhotoSauce.NativeCodecs.Libwebp.

Not sure what (if anything) can be done here?

Cheers, Indy

indy-singh commented 1 year ago

Hmm, I thought this was a OS thing. I just tried https://github.com/SixLabors/ImageSharp out of interest and it works without issue on 472.

var fromBase64String = Convert.FromBase64String("UklGRsACAABXRUJQVlA4ILQCAADQFACdASogACAAPgQBQAAACJZgCdMoRwN67+Ev6q3gc8l87/Jn+q9DN6gH6V7wD9d/2A4QD9M+sA9AD9VfSZ/VX4Hf2H/ZL4AP00uzv4p+FfUDdevUbJcvDPwx/JbUGfyX8gP1V/uWcBfFv6N+MH4zbID+Af3r8ldd7/YPxm/oHwt5kXmH/T+4F/D/5H/Vfxy/vX/n5Uv9VQ7UDRfbyfP1PPTq52tzX06Y9zudfwUAAP7//rtpve1v1ky90qXpBgB4bus9vIL83KeFODqf4yUU3dFCg7cpPgl4CoP0eeD10Ftln5z6s/X91zc8dD/+bkoO9fdPp/d2eCb38BYpPTAFj7/oYCH+XcVa8sdWHvIGzrPbbXWf1XUlpHva//yG3kDP7YmnGKaFRTUwFTUKxF/r/3Rdt/Yl8P/+dsLM0ltaduY0Ehm/o9kxzW+OmBxf/f+49m+Tt03lHkL/BRkAyGrr+cUQIVaHdEskP3sXCf0NNPxE8fttsaS8eJtEPu18qWantFIuCZ3E9LiWG69lqPcRotHVUw/VAcKsBqfW7UMRU6D+kThFE0EoHWUc+oX5S5JtSx+RvxkBA1fUYyI8MUJX6hEih6Wk5OJi2Bsd258iyTKyMqpDO4TSdJN//7Lj9AJGif3//R4TxeL/U0c5xUgklQ90yWBTFgdWn7c5asm+vIWFzvyisqziwZHlAOb53da9fPrdu6O2h3M2nAu6e+DMY0+Cmak7zOBYuApaayrELZwoqTo1L1JDSardQEN9qyzIYRAoWC7UQ2bEewV5EuPRJFqW6DWDlCffGP1OT9N7tdn2g0I9Z/DJi8c0v//fa8qhD/45DRmdzj46hftRgmeQLfFk+5tNiqbcViSRMqMaGL6siXavzqvDx6oPJN5MQoh8781530v4T1a0Ghw4BSf24AAAAA==");
var imageInfo = SixLabors.ImageSharp.Image.Identify(fromBase64String);
Console.WriteLine(imageInfo.Width);
Console.WriteLine(imageInfo.Height);
saucecontrol commented 1 year ago

I'm guessing your app is either ASP.NET (though I don't know why you'd have Console.WriteLine in ASP.NET) or some kind of service?

The first error you listed is a result of the fact the Windows WebP codec is installed as a Microsoft Store app so it's not available under all user accounts and may not be available on Server installations.

The second error, with the WebP plugin installed, is a simple DLL resolution issue. Modern .NET has support for native binary packaging all the way through the SDK, so when you publish an app, all the correct native binaries go with it. That's why net6.0 targets work with no hassle. .NET Framework didn't have the same, so the best I can do is put a step in the package definition that copies the binaries to a known location and then try to find them there at runtime.

The current release version will attempt to load the libwebp DLLs (webp.dll, webpdemux.dll, and webpmux.dll) from the proper architecture folder (probably x64) under your app's binary location. In ASP.NET, this may not be the correct location due to Shadow Copying. You may be able to resolve the issue by copying the required DLLs directly into your bin folder or the shadow folder.

I have also improved the runtime DLL resolution logic for .NET Framework recently, so you can try the latest version from the CI NuGet feed which should work in more cases. That updated version still relies on the DLLs being in an architecture folder alongside the managed assembly, but it uses the CodeBase location of the managed assembly to work around the Shadow Copy issue. If you do try that, let me know if it works for you.

Finally, ImageSharp has its own WebP implementation written in C#, so it doesn't depend on any external codec libraries. Its WebP implementation is significantly slower than libwebp and doesn't support all WebP features, but if it meets your needs, it may simplify your deployment.

indy-singh commented 1 year ago

I'm guessing your app is either ASP.NET (though I don't know why you'd have Console.WriteLine in ASP.NET) or some kind of service?

It's a standalone app, so I can replicate the problem in isolation [1] [2]. The actual app is hosted on Server2019 in IIS :)

The first error you listed is a result of the fact the Windows WebP codec is installed as a Microsoft Store app so it's not available under all user accounts and may not be available on Server installations. Ah that's what I thought, but I couldn't find anything for Server2019.

On my home machine (Windows 10) this works:-

PS C:\Users\Indy> Get-AppxPackage Microsoft.WebpImageExtension

Name              : Microsoft.WebpImageExtension
Publisher         : CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US
Architecture      : X64
ResourceId        :
Version           : 1.0.52351.0
PackageFullName   : Microsoft.WebpImageExtension_1.0.52351.0_x64__8wekyb3d8bbwe
InstallLocation   : C:\Program Files\WindowsApps\Microsoft.WebpImageExtension_1.0.52351.0_x64__8wekyb3d8bbwe
IsFramework       : False
PackageFamilyName : Microsoft.WebpImageExtension_8wekyb3d8bbwe
PublisherId       : 8wekyb3d8bbwe
IsResourcePackage : False
IsBundle          : False
IsDevelopmentMode : False
NonRemovable      : False
IsPartiallyStaged : False
SignatureKind     : Store
Status            : Ok

At work (Server2019) the same command returns nothing. So I presume that WebP is not supported on Server2019.

That's why net6.0 targets work with no hassle. .NET Framework didn't have the same, so the best I can do is put a step in the package definition that copies the binaries to a known location and then try to find them there at runtime.

Makes total sense, I appreciate the best effort. In reality we need to move off NET Framework and get onto dotnet-new.

Finally, ImageSharp has its own WebP implementation written in C#, so it doesn't depend on any external codec libraries. Its WebP implementation is significantly slower than libwebp and doesn't support all WebP features, but if it meets your needs, it may simplify your deployment.

Ah, good to know! Right now we have settled on PhotoSauce as it is blazing fast, and we can ignore WebP for now.

Cheers, Indy

[1] http://sscce.org/ [2] https://en.wikipedia.org/wiki/Minimal_reproducible_example

toddsay commented 1 year ago

Thanks Indy and Saucecontrol for those helpful details! I'm facing this exact issue in our .NET 4.8 project. Like Indy, I received the WIC decoder could not be instantiated error when running under Azure Windows Server, so I installed your native WEBP codec and then received the Unable to load DLL 'webpdemux' error. The dll's are present in the x86 and x64 subdirectories (under bin/Debug/net48 in this example) but do not appear to be located by the run time logic. The project architecture is defined as Any, but it can be x86 or x64 depending on the specific project (and whether Prefer 32-bit was checked in the project build settings).

I tried installing the latest CI versions (MagicScaler 0.14.0-ci230591 and Libwebp 1.3.0-ci230591), but unfortunately I still receive the same "unable to load DLL" error. If I use procmon to view the files opened by the process, I see it following the PATH and checking many locations for "webpdemux.DLL", but it never checks the x86/x64 subdirectory locations.

I know I can copy the dll up a directory, but that doesn't work very well for mixed x86/x64 projects that share a calling dll, and it also isn't handled automatically when unstalling NuGet packages.

Can you think of anything else I can try?

saucecontrol commented 1 year ago

Interesting. Thanks for letting me know.

The updated code in the CI builds uses Assembly.CodeBase to find the managed codec wrapper assembly location and then appends the ProcessArchitecture to that, so it should work even with Any targets. If you're able to check those in your environment, that may help track down why it's not working for you.

https://github.com/saucecontrol/PhotoSauce/blob/7953b64e2f044af1111b198a23698dacb8827e98/src/MagicScaler/Utilities/MiscExtensions.cs#L121-L128

Worst case, you can do the resolution yourself by P/Invoking LoadLibrary, which is what the wrapper is doing after guessing the location using the logic above.

toddsay commented 1 year ago

That's interesting. In my test, ProcessArchitecture is X64, and if I run code like yours above, I receive the expected subdirectory. I also tried a quick LoadLibrary test using that resulting location, and was able to load webp/webpdemux/webpmux.

So, just trying to guess why it doesn't work as expected already... I notice that the logic in WebpCodec.cs has the logic as #if NETFRAMEWORK. Is there any chance that the NuGet package I'm using doesn't have that section enabled?

saucecontrol commented 1 year ago

I suppose it's possible you've got the NetStandard binaries from the package instead, but that shouldn't be the case if you're using a current SDK to build your app. Are you using PackageReference or do you have an old packages.config setup?

You can, of course, check the code in your deployed app with ILSpy or the like to be absolutely sure you've got the binary you think you do.

toddsay commented 1 year ago

Good suggestion. I do see the relevant code in my PhotoSauce.NativeCodecs.Libwebp (in WebpFactory). And I'm using PackageReference, currently testing as:

    <PackageReference Include="PhotoSauce.MagicScaler" Version="0.14.0-ci230591" />
    <PackageReference Include="PhotoSauce.NativeCodecs.Libwebp" Version="1.3.0-ci230591" />
saucecontrol commented 1 year ago

Hmmm... Yeah, if the code is there and the code works independently, I'm out of ideas. If you do find something to explain it, please let me know.

toddsay commented 1 year ago

Possibly there's an issue with order of operations? I'm not sure if I'm on the right track, as I'm just trying to hit some breakpoints within disassembled optimized code, but it looks to me like WebpContainer.TryLoad is making some Libwebpdemux calls prior to constructing a WebpContainer. If I'm following the code properly (doubtful), it is within the WebpContainer constructor that we use WebpFactory.CreateDemuxer to load the library (within dependencyValid). Am I right that this flow seems to be using the demux library prior to loading it?

Also in case it is relevant to the expected order of things my calling code looks like: using (ProcessingPipeline pipeline = MagicImageProcessor.BuildPipeline(imageStream, processImageSettings))

saucecontrol commented 1 year ago

dependencyValid is the gatekeeper that ensures the LoadLibrary calls have occurred before attempting to use any of the libwebp functions on .NET Framework. All of the WebpFactory static methods check that Lazy<T> value before calling into libwebp so that they either see the true value from the Lazy<T> initializer and allow the call or re-throw the same exception that occurred during the native library version check.

Your usage looks fine, and if you're hitting a breakpoint inside the dependencyValid initializer, that part is working as expected.

saucecontrol commented 1 year ago

I'm looking at the documentation for LoadLibrary again, and it's not clear whether it's supposed to be automatically appending the .dll extension to the file name if the given path is not relative. I swear that's been working as is in my netfx tests, but if you get a chance to look at that before I do, it's worth verifying to be sure.

saucecontrol commented 1 year ago

it looks to me like WebpContainer.TryLoad is making some Libwebpdemux calls prior to constructing a WebpContainer

Aw jeez, I misread that. Yes, you're absolutely right. I've pushed an update that checks the dependencies from TryLoad as well. (And confirmed the .dll extension is appended as expected)

Turns out my tests were passing because they do a WebP encode before attempting to decode. Since the encoder was properly checking/loading the dependencies, it was getting taken care of there before reaching the unprotected calls.

Thanks for the investigation!

toddsay commented 1 year ago

Awesome, thanks for the insanely fast support! I waited for the successful build and confirmed that this fix is working for me. 👍

And yeah I hadn't ever hit a breakpoint in dependencyValid which was part of my suspicions there. And not that it matters now, but a LoadLibrary test worked for me too without specifying the extension.

For my planning, any projections on timeline for the next formal NuGet release?

saucecontrol commented 1 year ago

Great, thanks for the quick feedback!

Next official release will come when I complete work on my BMP (managed) and TIFF (libtiff) codec plugins. I had to pivot to some other projects the last few months and ended up paging most of that stuff out of my brain, but I've started ramping back up on it recently. I hope to have them wrapped up sometime in the next month or two, but no promises.