From 7cafc18b129261af8d1d3c6f741ebc060d2242f6 Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Wed, 11 Sep 2024 14:29:19 -0400 Subject: [PATCH] fix: Dynamically load in-place skiasharp --- .github/workflows/ci.yml | 142 +++++-- src/.nuspec/Uno.Resizetizer.targets | 62 +--- .../src/SkiaSharpTools.Initializer.cs | 215 +++++++++++ src/Resizetizer/src/SkiaSharpTools.cs | 351 +++++++++--------- 4 files changed, 507 insertions(+), 263 deletions(-) create mode 100644 src/Resizetizer/src/SkiaSharpTools.Initializer.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82c6a1ec..ebad6788 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ on: - legacy/** env: - UnoCheck_Version: '1.21.1' + UnoCheck_Version: '1.26.0-dev.28' jobs: build_tool: @@ -32,10 +32,10 @@ jobs: with: dotnet-version: '5.0.408' - - name: Setup .NET 7 + - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '7.0.100' + dotnet-version: '8.0.401' - name: Setup GitVersion uses: gittools/actions/gitversion/setup@v1.1.1 @@ -55,8 +55,8 @@ jobs: gci -r -File -Include *.cs,*.targets,*.props,*.csproj | foreach-object { $a = $_.fullname; ( get-content $a ) | foreach-object { $_ -replace "v0","${{steps.gitversion.outputs.sha}}" } | set-content $a } - run: | - & dotnet tool update --global uno.check --version ${{ env.UnoCheck_Version }} --add-source https://api.nuget.org/v3/index.json - & uno-check -v --ci --non-interactive --fix --skip xcode --skip androidemulator --skip gtk3 --skip vswin --skip vsmac + & dotnet tool update --global uno.check --version ${{ env.UnoCheck_Version }} --add-source https://api.nuget.org/v3/index.json + & uno-check -v --ci --non-interactive --fix --skip xcode --skip androidemulator --skip gtk3 --skip vswin --skip vsmac name: Install .NET Workloads - name: Build - CI @@ -74,15 +74,113 @@ jobs: run: | dotnet test src/Resizetizer/test/UnitTests/Resizetizer.UnitTests.csproj -c Release -p:PackageVersion=$adjustedPackageVersion -p:Version=${{ steps.gitversion.outputs.assemblySemVer }} --logger GitHubActions --blame-crash --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover - validation_5_2: - name: Validate 5.2 Samples + validation_5_2_nix: + name: Validate 5.2 needs: build_tool + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + config: [Debug, Release] + + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download Artifact + uses: actions/download-artifact@v4 + with: + name: NuGet + path: samples/packages + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.401' + + - run: | + ubuntu_release=`lsb_release -rs` + wget https://packages.microsoft.com/config/ubuntu/${ubuntu_release}/packages-microsoft-prod.deb -O packages-microsoft-prod.deb + sudo dpkg -i packages-microsoft-prod.deb + sudo apt-get install apt-transport-https + sudo apt-get update + sudo apt-get install -y msopenjdk-11 + sudo update-alternatives --list java + echo "JAVA_HOME=/usr/lib/jvm/msopenjdk-11-amd64" >> "$GITHUB_ENV" + if: runner.os == 'Linux' + name: Install OpenJDK 11 + + - run: | + & dotnet tool update --global uno.check --version ${{ env.UnoCheck_Version }} --add-source https://api.nuget.org/v3/index.json + & uno-check -v --ci --non-interactive --fix --skip xcode --skip androidemulator --skip gtk3 --skip vswin --skip vsmac + shell: pwsh + name: Install .NET Workloads + + - name: Restore 5.2 Sample App (${{ matrix.config }}) + shell: pwsh + run: | + cd samples/5.2/Uno52ResizetizerTests + & dotnet restore + + - name: Delete nupkg files + run: | + Remove-Item -Recurse -Path $env:USERPROFILE\.nuget\packages\*.nupkg -Force + if: runner.os == 'Windows' + shell: pwsh + + - name: Delete nupkg files + run: | + Remove-Item -Recurse -Path $env:HOME\.nuget\packages\*.nupkg -Force + if: runner.os != 'Windows' + shell: pwsh + + - name: Validate 5.2 Samples app (${{ matrix.config }}) + shell: pwsh + + # Skip the macOS release build, the agent does not have + # enough disk space to handle it. + if: matrix.os == 'macos-latest' && matrix.config != 'Release' + + run: | + & dotnet build -c ${{ matrix.config }} -p:RunAOTCompilation=false -p:WasmShellILLinkerEnabled=false /p:WindowsAppSDKSelfContained=false /p:WindowsPackageType=None -bl:./logs/sample-5.2-${{ matrix.config }}.binlog samples/5.2/Uno52ResizetizerTests/Uno52ResizetizerTests.sln + + - name: Validate 5.2 Samples app (${{ matrix.config }} Incremental) + + # Skip the macOS release build, the agent does not have + # enough disk space to handle it. + if: matrix.os == 'macos-latest' && matrix.config != 'Release' + + shell: pwsh + run: | + & dotnet build -c ${{ matrix.config }} /p:RunAOTCompilation=false /p:WasmShellILLinkerEnabled=false /p:WindowsAppSDKSelfContained=false /p:WindowsPackageType=None /bl:.\logs\sample-5.2-${{ matrix.config }}-incremental.binlog samples\5.2\Uno52ResizetizerTests\Uno52ResizetizerTests.sln + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: logs_5_2_${{ matrix.config }}_${{ matrix.os }} + path: .\logs + + + validation_5_2_win: + name: Validate 5.2 Samples (msbuild) + needs: build_tool runs-on: windows-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.401' + - name: Download Artifact uses: actions/download-artifact@v4 with: @@ -105,22 +203,6 @@ jobs: & $msbuild $sampleSolution /p:TargetFramework=net8.0-android /p:Configuration=Debug /p:AndroidInstallAfterBuild=False /p:BuildingInsideVisualStudio=true /p:DefineExplicitDefaults=true /p:RunConfiguration=Default /t:GetAndroidDependencies & $msbuild $sampleSolution /p:TargetFramework= /p:Configuration=Debug /p:AndroidInstallAfterBuild=False /p:BuildingInsideVisualStudio=true /p:UseHostCompilerIfAvailable=false /p:DefineExplicitDefaults=true /p:RunConfiguration=Default /t:rebuild - - name: Validate 5.2 Samples app (Debug) - run: | - & dotnet build -c Debug /p:RunAOTCompilation=false /p:WasmShellILLinkerEnabled=false /bl:.\logs\sample-5.2-Debug.binlog samples\5.2\Uno52ResizetizerTests\Uno52ResizetizerTests.sln - - - name: Validate 5.2 Samples app (Debug Incremental) - run: | - & dotnet build -c Debug /p:RunAOTCompilation=false /p:WasmShellILLinkerEnabled=false /bl:.\logs\sample-5.2-Debug-incremental.binlog samples\5.2\Uno52ResizetizerTests\Uno52ResizetizerTests.sln - - - name: Validate 5.2 Samples app (Release) - run: | - & dotnet build -c Release /p:WindowsAppSDKSelfContained=false /p:WindowsPackageType=None /p:RunAOTCompilation=false /p:WasmShellILLinkerEnabled=false /bl:.\logs\sample-5.2-Release.binlog samples\5.2\Uno52ResizetizerTests\Uno52ResizetizerTests.sln - - - name: Validate 5.2 Samples app (Release Incremental) - run: | - & dotnet build -c Release /p:WindowsAppSDKSelfContained=false /p:WindowsPackageType=None /p:RunAOTCompilation=false /p:WasmShellILLinkerEnabled=false /bl:.\logs\sample-5.2-Release-incremental.binlog samples\5.2\Uno52ResizetizerTests\Uno52ResizetizerTests.sln - - name: Upload Artifacts uses: actions/upload-artifact@v4 if: always() @@ -142,7 +224,12 @@ jobs: with: name: NuGet path: samples/packages - + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.401' + - run: | & dotnet tool update --global uno.check --version ${{ env.UnoCheck_Version }} --add-source https://api.nuget.org/v3/index.json & uno-check -v --ci --non-interactive --fix --skip xcode --skip androidemulator --skip gtk3 --skip vswin --skip vsmac @@ -151,12 +238,12 @@ jobs: - name: Validate 5.1 Samples app run: | $msbuild = vswhere -latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe - & $msbuild /r /bl:.\logs\samples.binlog samples\NewTemplate\build.slnf /p:AotAssemblies=false /p:WasmShellILLinkerEnabled=false + & $msbuild /r /bl:.\logs\samples.binlog samples\NewTemplate\build.slnf /p:AotAssemblies=false /p:WindowsPackageType=None /p:WasmShellILLinkerEnabled=false - name: Validate 5.1 Samples (incremental) run: | $msbuild = vswhere -latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe - & $msbuild /r /bl:.\logs\samples-incremental.binlog samples\NewTemplate\build.slnf /p:AotAssemblies=false /p:WasmShellILLinkerEnabled=false + & $msbuild /r /bl:.\logs\samples-incremental.binlog samples\NewTemplate\build.slnf /p:AotAssemblies=false /p:WindowsPackageType=None /p:WasmShellILLinkerEnabled=false - name: Upload Artifacts uses: actions/upload-artifact@v4 @@ -172,7 +259,8 @@ jobs: needs: - build_tool - validation_5_1 - - validation_5_2 + - validation_5_2_win + - validation_5_2_nix steps: - name: Checkout uses: actions/checkout@v4 diff --git a/src/.nuspec/Uno.Resizetizer.targets b/src/.nuspec/Uno.Resizetizer.targets index 9cd40b93..97ec3978 100644 --- a/src/.nuspec/Uno.Resizetizer.targets +++ b/src/.nuspec/Uno.Resizetizer.targets @@ -9,7 +9,7 @@ - <_UnoResizetizerTaskAssemblyName>$(MSBuildThisFileDirectory)netstandard2.0\Uno.Resizetizer_v0.dll + <_UnoResizetizerTaskAssemblyName>$(MSBuildThisFileDirectory)netstandard2.0\Uno.Resizetizer_v0.dll + BeforeTargets="UnoResizetizeCollectItems"> - - - - <_ResizetizerRuntimeIdentifier>$(NETCoreSdkPortableRuntimeIdentifier) - - <_ResizetizerRuntimeIdentifier Condition=" $(_ResizetizerRuntimeIdentifier.Contains('osx')) ">osx - <_ResizetizerRuntimeIdentifierDirectory>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)', 'netstandard2.0', 'runtimes', '$(_ResizetizerRuntimeIdentifier)')) - <_ResizetizerRuntimeAssetsOutput>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)', 'netstandard2.0')) - - - - <_ResiztizerRuntimeAssets Include="$(_ResizetizerRuntimeIdentifierDirectory)\**\*" - OutputDirectory="$(_ResizetizerRuntimeAssetsOutput)" /> - - - - - - - - - - - - - diff --git a/src/Resizetizer/src/SkiaSharpTools.Initializer.cs b/src/Resizetizer/src/SkiaSharpTools.Initializer.cs new file mode 100644 index 00000000..1214a018 --- /dev/null +++ b/src/Resizetizer/src/SkiaSharpTools.Initializer.cs @@ -0,0 +1,215 @@ +using SkiaSharp; +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Uno.Resizetizer; + +internal abstract partial class SkiaSharpTools +{ + private delegate void SetDllImportResolverDelegate(Assembly assembly, Func resolver); + private delegate bool TryLoadDelegate(string libraryName, Assembly assembly, DllImportSearchPath? searchPath, out IntPtr libHandle); + + private static bool _initialized; + private static MethodInfo _setDllImportResolver; + private static TryLoadDelegate _tryLoad; + + static SkiaSharpTools() + { + Initialize(); + } + + public static void Initialize() + { + if (!_initialized) + { + _initialized = true; + + InitializeWindowsSearchPaths(); + + var isNetCore = Type.GetType("System.Runtime.Loader.AssemblyLoadContext") != null; + + if (isNetCore) + { + SetupResolver(); + } + } + } + + /// + /// Initializes SkiaSharp in a netcore environment, where the assemblies are loaded + /// through the System.Runtime.InteropServices.NativeLibrary, but uses the system dll search paths. + /// + private static void InitializeWindowsSearchPaths() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + foreach (var runtimePath in GetRuntimesFolder()) + { + _ = AddDllDirectory(runtimePath); + } + } + } + + private static void SetupResolver() + { + var dllImportResolverDelegateType = Type.GetType("System.Runtime.InteropServices.DllImportResolver")!; + + // We're building with netstandard 2.0 which does not provide those APIs, but we + // know we're running on netcore. Let's use them through reflection. + _setDllImportResolver = Type + .GetType("System.Runtime.InteropServices.NativeLibrary") + ?.GetMethod("SetDllImportResolver", [typeof(Assembly), dllImportResolverDelegateType]); + + _tryLoad = (TryLoadDelegate)Type + .GetType("System.Runtime.InteropServices.NativeLibrary") + ?.GetMethod("TryLoad", [typeof(string), typeof(Assembly), typeof(DllImportSearchPath?), typeof(IntPtr).MakeByRefType()]) + ?.CreateDelegate(typeof(TryLoadDelegate)); + + if (_setDllImportResolver is not null && _tryLoad is not null) + { + var importResolverMethod = typeof(SkiaSharpTools).GetMethod(nameof(ImportResolver), BindingFlags.Static | BindingFlags.NonPublic); + var importResolverDelegate = Delegate.CreateDelegate(dllImportResolverDelegateType, null, importResolverMethod!); + + _setDllImportResolver.Invoke(null, [typeof(SkiaSharp.SKAlphaType).Assembly, importResolverDelegate]); + } + else + { + throw new InvalidOperationException($"Unable to find System.Runtime.InteropServices.NativeLibrary.SetDllImportResolver or TryLoad"); + } + } + + private static string[] GetRuntimesFolder() + { + if (typeof(SkiaSharpTools).Assembly.Location is { } location + && Path.GetDirectoryName(location) is { } directory) + { + var bitness = Environment.Is64BitProcess ? "x64" : "x86"; + var arch = RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "arm64" : bitness; + var ridOS = GetRidOS(); + + return + [ + Path.Combine(directory, "runtimes", ridOS + "-" + arch, "native"), + Path.Combine(directory, "runtimes", ridOS, "native") + ]; + } + else + { + throw new InvalidOperationException("Unable to get the tools assembly location"); + } + } + + private static string GetRidOS() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "win"; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return "linux"; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return "osx"; + } + + throw new NotSupportedException("This operating system is not supported"); + } + + static IntPtr ImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + IntPtr libHandle = IntPtr.Zero; + var searchFlags = DllImportSearchPath.SafeDirectories + | DllImportSearchPath.UserDirectories; + + if (libraryName.Equals("libHarfBuzzSharp", StringComparison.OrdinalIgnoreCase) + || libraryName.Equals("libSkiaSharp", StringComparison.OrdinalIgnoreCase)) + { + if (!NixTryLoad(libraryName, out libHandle) + && !_tryLoad(libraryName + ".dll", typeof(SkiaSharpTools).Assembly, searchFlags, out libHandle)) + { + throw new InvalidOperationException($"Failed to load {libraryName}"); + } + } + + bool NixTryLoad(string library, out IntPtr handle) + { + var extension = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "dylib" : "so"; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + foreach (var runtimePath in GetRuntimesFolder()) + { + IntPtr localDlOpen(string fileName) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return Linux.dlopen(fileName, false); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return dlopen_macos(fileName, 0); + } + else + { + throw new NotSupportedException("This operating system is not supported"); + } + } + + handle = localDlOpen(Path.Combine(runtimePath, library + "." + extension)); + + if (handle != IntPtr.Zero) + { + return true; + } + } + } + + handle = IntPtr.Zero; + return false; + } + + return libHandle; + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern int AddDllDirectory(string NewDirectory); + + [DllImport("libSystem.dylib", EntryPoint = "dlopen")] + public static extern IntPtr dlopen_macos(string fileName, int flags); + + // Imported from https://github.com/mono/SkiaSharp/blob/482e6ee2913a08a7cad76520ccf5fbce97c7c23b/binding/Binding.Shared/LibraryLoader.cs + private static class Linux + { + private const string SystemLibrary = "libdl.so"; + private const string SystemLibrary2 = "libdl.so.2"; // newer Linux distros use this + + private const int RTLD_LAZY = 1; + private const int RTLD_NOW = 2; + private const int RTLD_DEEPBIND = 8; + + public static IntPtr dlopen(string path, bool lazy = true) + { + try + { + return dlopen2(path, (lazy ? RTLD_LAZY : RTLD_NOW) | RTLD_DEEPBIND); + } + catch (DllNotFoundException) + { + return dlopen1(path, (lazy ? RTLD_LAZY : RTLD_NOW) | RTLD_DEEPBIND); + } + } + + [DllImport(SystemLibrary, EntryPoint = "dlopen")] + private static extern IntPtr dlopen1(string path, int mode); + [DllImport(SystemLibrary2, EntryPoint = "dlopen")] + private static extern IntPtr dlopen2(string path, int mode); + } + +} diff --git a/src/Resizetizer/src/SkiaSharpTools.cs b/src/Resizetizer/src/SkiaSharpTools.cs index 0ba683a1..cdd46d74 100644 --- a/src/Resizetizer/src/SkiaSharpTools.cs +++ b/src/Resizetizer/src/SkiaSharpTools.cs @@ -1,181 +1,180 @@ -using System; +using SkiaSharp; +using System; using System.Diagnostics; using System.IO; -using SkiaSharp; -namespace Uno.Resizetizer +namespace Uno.Resizetizer; + +internal abstract partial class SkiaSharpTools { - internal abstract class SkiaSharpTools - { - public static SkiaSharpTools Create(bool isVector, string filename, SKSize? baseSize, SKColor? backgroundColor, SKColor? tintColor, ILogger logger) - => isVector - ? new SkiaSharpSvgTools(filename, baseSize, backgroundColor, tintColor, logger) as SkiaSharpTools - : new SkiaSharpBitmapTools(filename, baseSize, backgroundColor, tintColor, logger); - - public static SkiaSharpTools CreateImaginary(SKColor? backgroundColor, ILogger logger) - => new SkiaSharpImaginaryTools(backgroundColor, logger); - - public SkiaSharpTools(ResizeImageInfo info, ILogger logger) - : this(info.Filename, info.BaseSize, info.Color, info.TintColor, logger) - { - } - - public SkiaSharpTools(string filename, SKSize? baseSize, SKColor? backgroundColor, SKColor? tintColor, ILogger logger) - { - Logger = logger; - Filename = filename; - BaseSize = baseSize; - BackgroundColor = backgroundColor; - - if (tintColor is SKColor tint) - { - Logger?.Log($"Detected a tint color of {tint}"); - - Paint = new SKPaint - { - ColorFilter = SKColorFilter.CreateBlendMode(tint, SKBlendMode.SrcIn) - }; - } - } - - public string Filename { get; } - - public SKSize? BaseSize { get; } - - public SKColor? BackgroundColor { get; } - - public ILogger Logger { get; } - - public SKPaint Paint { get; } - - public void Resize(DpiPath dpi, string destination, double additionalScale = 1.0, bool dpiSizeIsAbsolute = false) - { - var sw = new Stopwatch(); - sw.Start(); - - var originalSize = GetOriginalSize(); - var absoluteSize = dpiSizeIsAbsolute ? dpi.Size : null; - var (scaledSize, scale) = GetScaledSize(originalSize, dpi, absoluteSize); - var (canvasSize, _) = GetCanvasSize(dpi, null, this); - - using (var tempBitmap = new SKBitmap(canvasSize.Width, canvasSize.Height)) - { - Draw(tempBitmap, additionalScale, originalSize, scale, scaledSize); - Save(destination, tempBitmap); - } - - sw.Stop(); - Logger?.Log($"Save Image took {sw.ElapsedMilliseconds}ms ({destination})"); - } - - public static (SKSizeI Scaled, SKSize Unscaled) GetCanvasSize(DpiPath dpi, SKSize? baseSize = null, SkiaSharpTools baseTools = null) - { - // if an explicit size was given by the type of image, use that - if (dpi.Size is SKSize size) - { - var scale = (float)dpi.Scale; - var scaled = new SKSizeI( - (int)(size.Width * scale), - (int)(size.Height * scale)); - return (scaled, size); - } - - // if an explicit size was given in the csproj, use that - if (baseSize is SKSize bs) - { - var scale = (float)dpi.Scale; - var scaled = new SKSizeI( - (int)(bs.Width * scale), - (int)(bs.Height * scale)); - return (scaled, bs); - } - - // try determine the best size based on the loaded image - if (baseTools is not null) - { - var baseOriginalSize = baseTools.GetOriginalSize(); - var (baseScaledSize, _) = baseTools.GetScaledSize(baseOriginalSize, dpi.Scale); - return (baseScaledSize, baseOriginalSize); - } - - throw new InvalidOperationException("The canvas size cannot be calculated if there is no size to start from (DPI size, BaseSize or image size)."); - } - - void Draw(SKBitmap tempBitmap, double additionalScale, SKSize originalSize, float scale, SKSizeI scaledSize) - { - using var canvas = new SKCanvas(tempBitmap); - - var canvasSize = tempBitmap.Info.Size; - - // clear - canvas.Clear(BackgroundColor ?? SKColors.Transparent); - - // center the drawing - canvas.Translate( - (canvasSize.Width - scaledSize.Width) / 2, - (canvasSize.Height - scaledSize.Height) / 2); - - // apply initial scale to size the image to fit the canvas - canvas.Scale(scale, scale); - - // apply additional user scaling - if (additionalScale != 1.0) - { - var userFgScale = (float)additionalScale; - - // add the user scale to the main scale - scale *= userFgScale; - - // work out the center as if the canvas was exactly the same size as the foreground - var fgCenterX = originalSize.Width / 2; - var fgCenterY = originalSize.Height / 2; - - // scale to the user scale, centering - canvas.Scale(userFgScale, userFgScale, fgCenterX, fgCenterY); - } - - // draw - DrawUnscaled(canvas, scale); - } - - void Save(string destination, SKBitmap tempBitmap) - { - using var stream = File.Create(destination); - tempBitmap.Encode(stream, SKEncodedImageFormat.Png, 100); - } - - public abstract SKSize GetOriginalSize(); - - public abstract void DrawUnscaled(SKCanvas canvas, float scale); - - public (SKSizeI, float) GetScaledSize(SKSize originalSize, DpiPath dpi, SKSize? absoluteSize = null) => - GetScaledSize(originalSize, dpi.Scale, absoluteSize ?? dpi.Size); - - public (SKSizeI, float) GetScaledSize(SKSize originalSize, decimal resizeRatio, SKSize? absoluteSize = null) - { - var sourceNominalWidth = (int)(absoluteSize?.Width ?? BaseSize?.Width ?? originalSize.Width); - var sourceNominalHeight = (int)(absoluteSize?.Height ?? BaseSize?.Height ?? originalSize.Height); - - // Find the actual size of the image - var sourceActualWidth = (double)originalSize.Width; - var sourceActualHeight = (double)originalSize.Height; - - // Figure out what the ratio to convert the actual image size to the nominal size is - var nominalRatio = Math.Min( - sourceNominalWidth / sourceActualWidth, - sourceNominalHeight / sourceActualHeight); - - // Multiply nominal ratio by the resize ratio to get our final ratio we actually adjust by - var adjustRatio = nominalRatio * (double)resizeRatio; - - // Figure out our scaled width and height to make a new canvas for - var scaledWidth = sourceActualWidth * adjustRatio; - var scaledHeight = sourceActualHeight * adjustRatio; - var scaledSize = new SKSizeI( - (int)Math.Round(scaledWidth), - (int)Math.Round(scaledHeight)); - - return (scaledSize, (float)adjustRatio); - } - } + public static SkiaSharpTools Create(bool isVector, string filename, SKSize? baseSize, SKColor? backgroundColor, SKColor? tintColor, ILogger logger) + => isVector + ? new SkiaSharpSvgTools(filename, baseSize, backgroundColor, tintColor, logger) as SkiaSharpTools + : new SkiaSharpBitmapTools(filename, baseSize, backgroundColor, tintColor, logger); + + public static SkiaSharpTools CreateImaginary(SKColor? backgroundColor, ILogger logger) + => new SkiaSharpImaginaryTools(backgroundColor, logger); + + public SkiaSharpTools(ResizeImageInfo info, ILogger logger) + : this(info.Filename, info.BaseSize, info.Color, info.TintColor, logger) + { + } + + public SkiaSharpTools(string filename, SKSize? baseSize, SKColor? backgroundColor, SKColor? tintColor, ILogger logger) + { + Logger = logger; + Filename = filename; + BaseSize = baseSize; + BackgroundColor = backgroundColor; + + if (tintColor is SKColor tint) + { + Logger?.Log($"Detected a tint color of {tint}"); + + Paint = new SKPaint + { + ColorFilter = SKColorFilter.CreateBlendMode(tint, SKBlendMode.SrcIn) + }; + } + } + + public string Filename { get; } + + public SKSize? BaseSize { get; } + + public SKColor? BackgroundColor { get; } + + public ILogger Logger { get; } + + public SKPaint Paint { get; } + + public void Resize(DpiPath dpi, string destination, double additionalScale = 1.0, bool dpiSizeIsAbsolute = false) + { + var sw = new Stopwatch(); + sw.Start(); + + var originalSize = GetOriginalSize(); + var absoluteSize = dpiSizeIsAbsolute ? dpi.Size : null; + var (scaledSize, scale) = GetScaledSize(originalSize, dpi, absoluteSize); + var (canvasSize, _) = GetCanvasSize(dpi, null, this); + + using (var tempBitmap = new SKBitmap(canvasSize.Width, canvasSize.Height)) + { + Draw(tempBitmap, additionalScale, originalSize, scale, scaledSize); + Save(destination, tempBitmap); + } + + sw.Stop(); + Logger?.Log($"Save Image took {sw.ElapsedMilliseconds}ms ({destination})"); + } + + public static (SKSizeI Scaled, SKSize Unscaled) GetCanvasSize(DpiPath dpi, SKSize? baseSize = null, SkiaSharpTools baseTools = null) + { + // if an explicit size was given by the type of image, use that + if (dpi.Size is SKSize size) + { + var scale = (float)dpi.Scale; + var scaled = new SKSizeI( + (int)(size.Width * scale), + (int)(size.Height * scale)); + return (scaled, size); + } + + // if an explicit size was given in the csproj, use that + if (baseSize is SKSize bs) + { + var scale = (float)dpi.Scale; + var scaled = new SKSizeI( + (int)(bs.Width * scale), + (int)(bs.Height * scale)); + return (scaled, bs); + } + + // try determine the best size based on the loaded image + if (baseTools is not null) + { + var baseOriginalSize = baseTools.GetOriginalSize(); + var (baseScaledSize, _) = baseTools.GetScaledSize(baseOriginalSize, dpi.Scale); + return (baseScaledSize, baseOriginalSize); + } + + throw new InvalidOperationException("The canvas size cannot be calculated if there is no size to start from (DPI size, BaseSize or image size)."); + } + + void Draw(SKBitmap tempBitmap, double additionalScale, SKSize originalSize, float scale, SKSizeI scaledSize) + { + using var canvas = new SKCanvas(tempBitmap); + + var canvasSize = tempBitmap.Info.Size; + + // clear + canvas.Clear(BackgroundColor ?? SKColors.Transparent); + + // center the drawing + canvas.Translate( + (canvasSize.Width - scaledSize.Width) / 2, + (canvasSize.Height - scaledSize.Height) / 2); + + // apply initial scale to size the image to fit the canvas + canvas.Scale(scale, scale); + + // apply additional user scaling + if (additionalScale != 1.0) + { + var userFgScale = (float)additionalScale; + + // add the user scale to the main scale + scale *= userFgScale; + + // work out the center as if the canvas was exactly the same size as the foreground + var fgCenterX = originalSize.Width / 2; + var fgCenterY = originalSize.Height / 2; + + // scale to the user scale, centering + canvas.Scale(userFgScale, userFgScale, fgCenterX, fgCenterY); + } + + // draw + DrawUnscaled(canvas, scale); + } + + void Save(string destination, SKBitmap tempBitmap) + { + using var stream = File.Create(destination); + tempBitmap.Encode(stream, SKEncodedImageFormat.Png, 100); + } + + public abstract SKSize GetOriginalSize(); + + public abstract void DrawUnscaled(SKCanvas canvas, float scale); + + public (SKSizeI, float) GetScaledSize(SKSize originalSize, DpiPath dpi, SKSize? absoluteSize = null) => + GetScaledSize(originalSize, dpi.Scale, absoluteSize ?? dpi.Size); + + public (SKSizeI, float) GetScaledSize(SKSize originalSize, decimal resizeRatio, SKSize? absoluteSize = null) + { + var sourceNominalWidth = (int)(absoluteSize?.Width ?? BaseSize?.Width ?? originalSize.Width); + var sourceNominalHeight = (int)(absoluteSize?.Height ?? BaseSize?.Height ?? originalSize.Height); + + // Find the actual size of the image + var sourceActualWidth = (double)originalSize.Width; + var sourceActualHeight = (double)originalSize.Height; + + // Figure out what the ratio to convert the actual image size to the nominal size is + var nominalRatio = Math.Min( + sourceNominalWidth / sourceActualWidth, + sourceNominalHeight / sourceActualHeight); + + // Multiply nominal ratio by the resize ratio to get our final ratio we actually adjust by + var adjustRatio = nominalRatio * (double)resizeRatio; + + // Figure out our scaled width and height to make a new canvas for + var scaledWidth = sourceActualWidth * adjustRatio; + var scaledHeight = sourceActualHeight * adjustRatio; + var scaledSize = new SKSizeI( + (int)Math.Round(scaledWidth), + (int)Math.Round(scaledHeight)); + + return (scaledSize, (float)adjustRatio); + } }