Browse Source

Improved checking if all native dependencies are available (#687)

Marcin Ziąbek 2 years ago
parent
commit
993321781a

+ 5 - 0
Source/QuestPDF/Drawing/DocumentGenerator.cs

@@ -17,6 +17,11 @@ namespace QuestPDF.Drawing
 {
     static class DocumentGenerator
     {
+        static DocumentGenerator()
+        {
+            NativeDependencyCompatibilityChecker.Test();
+        }
+        
         internal static void GeneratePdf(Stream stream, IDocument document)
         {
             ValidateLicense();

+ 1 - 25
Source/QuestPDF/Drawing/Exceptions/InitializationException.cs

@@ -4,33 +4,9 @@ namespace QuestPDF.Drawing.Exceptions
 {
     public class InitializationException : Exception
     {
-        internal InitializationException(string documentType, Exception innerException) : base(CreateMessage(documentType, innerException.Message), innerException)
+        internal InitializationException(string message, Exception inner) : base(message, inner)
         {
             
         }
-
-        private static string CreateMessage(string documentType, string innerExceptionMessage)
-        {
-            var (libraryName, nugetConvention) = GetLibraryName();
-            
-            return $"Cannot create the {documentType} document using the {libraryName} library. " +
-                   $"This exception usually means that, on your operating system where you run the application, {libraryName} requires installing additional dependencies. " +
-                   $"Such dependencies are available as additional nuget packages, for example {nugetConvention}.Linux.NoDependencies. " +
-                   $"Some operating systems may require installing multiple nugets, e.g. MacOS may need both {nugetConvention}.macOS.NoDependencies and {nugetConvention}.Linux.NoDependencies." +
-                   $"Please refer to the {libraryName} documentation for more details. " +
-                   $"Also, please consult the inner exception that has been originally thrown by the dependency library.";
-
-            (string GetLibraryName, string nugetConvention) GetLibraryName()
-            {
-                if (innerExceptionMessage.Contains("libSkiaSharp"))
-                    return ("SkiaSharp", "SkiaSharp.NativeAssets");
-                
-                if (innerExceptionMessage.Contains("libHarfBuzzSharp"))
-                    return ("HarfBuzzSharp", "HarfBuzzSharp.NativeAssets");
-                
-                // default
-                return ("SkiaSharp-related", "*.NativeAssets");
-            }
-        }
     }
 }

+ 2 - 0
Source/QuestPDF/Drawing/FontManager.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using System.Reflection;
 using HarfBuzzSharp;
 using QuestPDF.Fluent;
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using SkiaSharp;
 using SkiaSharp.HarfBuzz;
@@ -29,6 +30,7 @@ namespace QuestPDF.Drawing
 
         static FontManager()
         {
+            NativeDependencyCompatibilityChecker.Test();
             RegisterLibraryDefaultFonts();
         }
         

+ 5 - 0
Source/QuestPDF/Fluent/ElementExtensions.cs

@@ -8,6 +8,11 @@ namespace QuestPDF.Fluent
 {
     public static class ElementExtensions
     {
+        static ElementExtensions()
+        {
+            NativeDependencyCompatibilityChecker.Test();
+        }
+        
         internal static Container Create(Action<IContainer> factory)
         {
             var container = new Container();

+ 6 - 1
Source/QuestPDF/Fluent/MinimalApi.cs

@@ -1,12 +1,17 @@
 using System;
 using System.Collections.Generic;
-using QuestPDF.Drawing;
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 
 namespace QuestPDF.Fluent
 {
     public class Document : IDocument
     {
+        static Document()
+        {
+            NativeDependencyCompatibilityChecker.Test();
+        }
+        
         private Action<IDocumentContainer> ContentSource { get; }
         private DocumentMetadata Metadata { get; set; } = DocumentMetadata.Default;
         private DocumentSettings Settings { get; set; } = DocumentSettings.Default;

+ 5 - 0
Source/QuestPDF/Helpers/ColorValidator.cs

@@ -6,6 +6,11 @@ namespace QuestPDF.Helpers
 {
     internal static class ColorValidator
     {
+        static ColorValidator()
+        {
+            NativeDependencyCompatibilityChecker.Test();
+        }
+        
         private static readonly ConcurrentDictionary<string, bool> Colors = new();
         
         public static void Validate(string? color)

+ 5 - 0
Source/QuestPDF/Helpers/Helpers.cs

@@ -12,6 +12,11 @@ namespace QuestPDF.Helpers
 {
     internal static class Helpers
     {
+        static Helpers()
+        {
+            NativeDependencyCompatibilityChecker.Test();
+        }
+        
         internal static byte[] LoadEmbeddedResource(string resourceName)
         {
             using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName);

+ 134 - 0
Source/QuestPDF/Helpers/NativeDependencyCompatibilityChecker.cs

@@ -0,0 +1,134 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using QuestPDF.Drawing.Exceptions;
+
+namespace QuestPDF.Helpers
+{
+    internal static class NativeDependencyCompatibilityChecker
+    {
+        private static bool IsCompatibilityChecked = false;
+        
+        public static void Test()
+        {
+            if (IsCompatibilityChecked)
+                return;
+            
+            var innerException = CheckIfExceptionIsThrownWhenLoadingNativeDependencies();
+
+            if (innerException == null)
+            {
+                IsCompatibilityChecked = true;
+                return;
+            }
+
+            var newLine = Environment.NewLine;
+            var paragraph = newLine + newLine;
+
+            var initializationExceptionMessage = 
+                $"The QuestPDF library has encountered an issue while loading one of its dependencies. " +
+                $"This type of error often occurs when the current runtime is missing necessary NuGet packages. {paragraph}" +
+                $"Please ensure the following NuGet packages are added to your project and has matching versions:";
+
+            foreach (var nuget in GetRecommendedNugetDependencies())
+                initializationExceptionMessage += $"{newLine}- {nuget}";
+
+            initializationExceptionMessage += $"{paragraph}For a detailed understanding, please examine the inner exception and consult the official SkiaSharp library documentation.";
+            
+            throw new InitializationException(initializationExceptionMessage, innerException);
+        }
+
+        private static Exception? CheckIfExceptionIsThrownWhenLoadingNativeDependencies()
+        {
+            try
+            {
+                // accessing any SkiaSharp object triggers loading of SkiaSharp-related DLL dependency
+                var typeface = SkiaSharp.SKTypeface.Default;
+
+                // accessing any HarfBuzzSharp object triggers loading of HarfBuzz-related DLL dependency
+                using var shaper = new SkiaSharp.HarfBuzz.SKShaper(typeface);
+
+                // everything loads and works correctly
+                return null;
+            }
+            catch (Exception exception)
+            {
+                return exception;
+            }
+        }
+        
+        private static IEnumerable<string> GetRecommendedNugetDependencies()
+        {
+            const string skiaSharp = "SkiaSharp.NativeAssets";
+            const string harfBuzzSharp = "HarfBuzzSharp.NativeAssets";
+            
+            #if NET5_0_OR_GREATER
+            
+            if (OperatingSystem.IsMacOS())
+            {
+                yield return $"{skiaSharp}.macOS";
+                yield return $"{harfBuzzSharp}.macOS";
+            }
+            else if (OperatingSystem.IsMacCatalyst())
+            {
+                yield return $"{skiaSharp}.MacCatalyst";
+                yield return $"{harfBuzzSharp}.MacCatalyst";
+            }
+            else if (OperatingSystem.IsIOS())
+            {
+                yield return $"{skiaSharp}.iOS";
+                yield return $"{harfBuzzSharp}.iOS";
+            }
+            else if (OperatingSystem.IsWatchOS())
+            {
+                yield return $"{skiaSharp}.watchOS";
+                yield return $"{harfBuzzSharp}.watchOS";
+            }
+            else if (OperatingSystem.IsTvOS())
+            {
+                yield return $"{skiaSharp}.tvOS";
+                yield return $"{harfBuzzSharp}.tvOS";
+            }
+            else if (OperatingSystem.IsAndroid())
+            {
+                yield return $"{skiaSharp}.Android";
+                yield return $"{harfBuzzSharp}.Android";
+            }
+            else if (OperatingSystem.IsBrowser())
+            {
+                yield return $"{skiaSharp}.WebAssembly";
+                yield return $"{harfBuzzSharp}.WebAssembly";
+            }
+            else if (OperatingSystem.IsLinux())
+            {
+                yield return $"{skiaSharp}.Linux.NoDependencies";
+                yield return $"{harfBuzzSharp}.Linux";
+            }
+            else if (OperatingSystem.IsWindows())
+            {
+                yield return $"{skiaSharp}.Win32";
+                yield return $"{harfBuzzSharp}.Win32";
+            }
+            
+            #else
+            
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+            {
+                yield return $"{skiaSharp}.Linux.NoDependencies";
+                yield return $"{harfBuzzSharp}.Linux";
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+            {
+                yield return $"{skiaSharp}.macOS";
+                yield return $"{harfBuzzSharp}.macOS";
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                yield return $"{skiaSharp}.Win32";
+                yield return $"{harfBuzzSharp}.Win32";
+            }
+            
+            #endif
+        }
+    }
+}

+ 5 - 0
Source/QuestPDF/Helpers/Placeholders.cs

@@ -7,6 +7,11 @@ namespace QuestPDF.Helpers
 {
     public static class Placeholders
     {
+        static Placeholders()
+        {
+            NativeDependencyCompatibilityChecker.Test();
+        }
+        
         public static readonly Random Random = new Random();
         
         #region Word Cache

+ 5 - 0
Source/QuestPDF/Infrastructure/Image.cs

@@ -25,6 +25,11 @@ namespace QuestPDF.Infrastructure
     /// </remarks>
     public class Image
     {
+        static Image()
+        {
+            NativeDependencyCompatibilityChecker.Test();
+        }
+        
         internal SKImage SkImage { get; }
         internal ImageSize Size { get; }
 

+ 6 - 0
Source/QuestPDF/Settings.cs

@@ -1,4 +1,5 @@
 using System;
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 
 namespace QuestPDF
@@ -38,5 +39,10 @@ namespace QuestPDF
         /// </summary>
         /// <remarks>By default, this flag is enabled only when the debugger IS attached.</remarks>
         public static bool CheckIfAllTextGlyphsAreAvailable { get; set; } = System.Diagnostics.Debugger.IsAttached;
+
+        static Settings()
+        {
+            NativeDependencyCompatibilityChecker.Test();
+        }
     }
 }