diff --git a/.DS_Store b/.DS_Store index c796adf1..08eac70e 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/Damselfly.Core/ImageProcessing/GraphicsMagicProcessor.cs b/Damselfly.Core/ImageProcessing/ImageMagickProcessor.cs similarity index 82% rename from Damselfly.Core/ImageProcessing/GraphicsMagicProcessor.cs rename to Damselfly.Core/ImageProcessing/ImageMagickProcessor.cs index a0ed1743..51c8b5ba 100644 --- a/Damselfly.Core/ImageProcessing/GraphicsMagicProcessor.cs +++ b/Damselfly.Core/ImageProcessing/ImageMagickProcessor.cs @@ -10,20 +10,18 @@ namespace Damselfly.Core.ImageProcessing { - public class GraphicsMagicProcessor : IImageProcessor + public class ImageMagickProcessor : IImageProcessor { - // TODO: Add check that GM exists - // SkiaSharp doesn't handle .heic files... yet - private static readonly string[] s_imageExtensions = { ".jpg", ".jpeg", ".png", /*".heic", */".webp" }; + private static readonly string[] s_imageExtensions = { ".jpg", ".jpeg", ".png", ".heic", ".tif", ".tiff", ".webp" }; - public ICollection SupportedFileExtensions { get { return s_imageExtensions; } } + public static ICollection SupportedFileExtensions { get { return s_imageExtensions; } } const string imageMagickExe = "convert"; const string graphicsMagickExe = "gm"; - private bool s_useGraphicsMagick = true; + private bool s_useGraphicsMagick = false; // GM doesn't support HEIC yet. - public GraphicsMagicProcessor() + public ImageMagickProcessor() { CheckToolStatus(); } @@ -33,28 +31,16 @@ public GraphicsMagicProcessor() /// private void CheckToolStatus() { - ProcessStarter gmprocess = new ProcessStarter(); - bool gmAvailable = gmprocess.StartProcess("gm", "-version"); + ProcessStarter improcess = new ProcessStarter(); + bool imAvailable = improcess.StartProcess("convert", "--version"); - if (!gmAvailable) + if (imAvailable) { - ProcessStarter improcess = new ProcessStarter(); - bool imAvailable = improcess.StartProcess("convert", "--version"); - - if (imAvailable) - { - var verString = gmprocess.OutputText?.Split('\n').FirstOrDefault(); - Logging.Log($"ImageMagick found: {verString}"); - } - else - throw new ApplicationException("Neither ImageMagick or GraphicsMagick were found."); + var verString = improcess.OutputText?.Split('\n').FirstOrDefault(); + Logging.Log($"ImageMagick found: {verString}"); } else - { - s_useGraphicsMagick = true; - var verString = gmprocess.OutputText?.Split('\n').FirstOrDefault(); - Logging.Log($"GraphicsMagick found: {verString}"); - } + Logging.LogError("ImageMagick not found."); } /// diff --git a/Damselfly.Core/ImageProcessing/ImageProcessorFactory.cs b/Damselfly.Core/ImageProcessing/ImageProcessorFactory.cs new file mode 100644 index 00000000..ae5737e1 --- /dev/null +++ b/Damselfly.Core/ImageProcessing/ImageProcessorFactory.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Damselfly.Core.Interfaces; + +namespace Damselfly.Core.ImageProcessing +{ + public class ImageProcessorFactory + { + private ImageMagickProcessor imProcessor; + private SkiaSharpProcessor skiaProcessor; + private ImageSharpProcessor isharpProcessor; + private string rootContentPath; + + public void SetContentPath(string path) + { + rootContentPath = path; + } + + /// + /// Takes a file extension, and returns an ImageProcessor that can generate + /// thumbnails for that file type. + /// + /// + /// + public IImageProcessor GetProcessor( string fileExtension ) + { + if( ! fileExtension.StartsWith( "." ) ) + { + fileExtension = $".{fileExtension}"; + } + + if( SkiaSharpProcessor.SupportedFileExtensions.Any( x => x.Equals( fileExtension, StringComparison.OrdinalIgnoreCase ) ) ) + { + if (skiaProcessor == null) + skiaProcessor = new SkiaSharpProcessor(); + + return skiaProcessor; + } + + if (ImageSharpProcessor.SupportedFileExtensions.Any(x => x.Equals(fileExtension, StringComparison.OrdinalIgnoreCase))) + { + if (isharpProcessor == null) + { + isharpProcessor = new ImageSharpProcessor(); + isharpProcessor.SetFontPath(Path.Combine(rootContentPath, "fonts")); + } + return isharpProcessor; + } + + if (ImageMagickProcessor.SupportedFileExtensions.Any(x => x.Equals(fileExtension, StringComparison.OrdinalIgnoreCase))) + { + if (imProcessor == null) + imProcessor = new ImageMagickProcessor(); + + return imProcessor; + } + + return null; + } + } +} diff --git a/Damselfly.Core/ImageProcessing/ImageSharpProcessor.cs b/Damselfly.Core/ImageProcessing/ImageSharpProcessor.cs index d3f10d67..0178cd45 100644 --- a/Damselfly.Core/ImageProcessing/ImageSharpProcessor.cs +++ b/Damselfly.Core/ImageProcessing/ImageSharpProcessor.cs @@ -21,9 +21,9 @@ namespace Damselfly.Core.ImageProcessing public class ImageSharpProcessor : IImageProcessor { private static FontCollection fontCollection; - private static readonly string[] s_imageExtensions = { ".jpg", ".jpeg", ".png", ".webp" }; + private static readonly string[] s_imageExtensions = { ".jpg", ".jpeg", ".png", ".webp", ".tga", ".gif", ".bmp" }; - public ICollection SupportedFileExtensions { get { return s_imageExtensions; } } + public static ICollection SupportedFileExtensions { get { return s_imageExtensions; } } public ImageSharpProcessor() { diff --git a/Damselfly.Core/ImageProcessing/SkiaSharpProcessor.cs b/Damselfly.Core/ImageProcessing/SkiaSharpProcessor.cs index 68675b74..bb4af57e 100644 --- a/Damselfly.Core/ImageProcessing/SkiaSharpProcessor.cs +++ b/Damselfly.Core/ImageProcessing/SkiaSharpProcessor.cs @@ -17,7 +17,7 @@ public class SkiaSharpProcessor : IImageProcessor // SkiaSharp doesn't handle .heic files... yet private static readonly string[] s_imageExtensions = { ".jpg", ".jpeg", ".png", /*".heic", */".webp", ".bmp", ".dng", ".cr2" }; - public ICollection SupportedFileExtensions { get { return s_imageExtensions; } } + public static ICollection SupportedFileExtensions { get { return s_imageExtensions; } } /// /// Create an SHA1 hash from the image data (pixels only) to allow us to find @@ -70,11 +70,11 @@ public Task CreateThumbs(FileInfo source, IDictionary x.Value.width); - load = new Stopwatch("LoadTHumb"); + load = new Stopwatch("LoadThumb"); using var sourceBitmap = LoadOrientedBitmap(source, desiredWidth); load.Stop(); @@ -132,7 +132,7 @@ public Task CreateThumbs(FileInfo source, IDictionary CreateThumbs(FileInfo source, IDictionary destFiles ); void TransformDownloadImage(string input, Stream output, ExportConfig config); - ICollection SupportedFileExtensions { get; } + static ICollection SupportedFileExtensions { get; } } } diff --git a/Damselfly.Core/Services/ConfigService.cs b/Damselfly.Core/Services/ConfigService.cs index 63f45b25..9c269a2c 100644 --- a/Damselfly.Core/Services/ConfigService.cs +++ b/Damselfly.Core/Services/ConfigService.cs @@ -11,7 +11,7 @@ namespace Damselfly.Core.Services /// public class ConfigService : IConfigService { - private IDictionary _cache; + private readonly IDictionary _cache = new Dictionary(StringComparer.OrdinalIgnoreCase); public ConfigService() { @@ -23,7 +23,8 @@ public void InitialiseCache( bool force = false ) if (_cache == null || force) { using var db = new ImageContext(); - _cache = db.ConfigSettings.ToDictionary(x => x.Name, x => x, StringComparer.OrdinalIgnoreCase); + foreach (var setting in db.ConfigSettings ) + _cache[setting.Name] = setting; } } diff --git a/Damselfly.Core/Services/ImageProcessService.cs b/Damselfly.Core/Services/ImageProcessService.cs index f8feb23e..6253e5ae 100644 --- a/Damselfly.Core/Services/ImageProcessService.cs +++ b/Damselfly.Core/Services/ImageProcessService.cs @@ -23,40 +23,65 @@ namespace Damselfly.Core.Services /// public class ImageProcessService : IImageProcessor { - private readonly IImageProcessor _processor; + private readonly ImageProcessorFactory _factory; - public ImageProcessService(IImageProcessor processor) + public ImageProcessService() { - _processor = processor; - - Logging.Log($"Initialised {processor.GetType().Name} for thumbnail processing."); + _factory = new ImageProcessorFactory(); } public void SetContentPath( string path ) { - if( _processor is ImageSharpProcessor imageSharp ) - imageSharp.SetFontPath(Path.Combine(path, "fonts")); + _factory.SetContentPath(path); } + /// + /// Creates a set of thumbs for an input image + /// + /// + /// + /// public async Task CreateThumbs(FileInfo source, IDictionary destFiles) { - return await _processor.CreateThumbs(source, destFiles); + var processor = _factory.GetProcessor(source.Extension); + + if( processor != null ) + return await processor.CreateThumbs(source, destFiles); + + return new ImageProcessResult { ThumbsGenerated = false }; } + /// + /// Convert an image, optionally watermarking. + /// + /// + /// + /// public void TransformDownloadImage(string input, Stream output, ExportConfig config) { - _processor.TransformDownloadImage(input, output, config); + var ext = Path.GetExtension(input); + + var processor = _factory.GetProcessor(ext); + + if (processor != null) + processor.TransformDownloadImage(input, output, config); } + /// + /// Returns true if the file is one that we consider to be an image - that is, + /// one that we have an image processor for, which will generate thumbs, etc. + /// + /// + /// public bool IsImageFileType(FileInfo filename) { if (filename.IsHidden()) return false; - return _processor.SupportedFileExtensions.Any(x => x.Equals(filename.Extension, StringComparison.OrdinalIgnoreCase)); - } - + var processor = _factory.GetProcessor(filename.Extension); - public ICollection SupportedFileExtensions { get{ return _processor.SupportedFileExtensions; } } + // If we have a valid processor, we're good. + return processor != null; + } } } diff --git a/Damselfly.Core/Services/ImageRecognitionService.cs b/Damselfly.Core/Services/ImageRecognitionService.cs index 80750c77..f224b23c 100644 --- a/Damselfly.Core/Services/ImageRecognitionService.cs +++ b/Damselfly.Core/Services/ImageRecognitionService.cs @@ -154,6 +154,21 @@ private bool UseAzureForRecogition(IList objects) return false; } + private System.Drawing.Bitmap SafeLoadBitmap(string fileName) + { + System.Drawing.Bitmap bmp = null; + try + { + // Load the bitmap once + bmp = new System.Drawing.Bitmap(fileName); + } + catch( Exception ex ) + { + Logging.LogError($"Error loading bitmap for {fileName}: {ex}"); + } + + return bmp; + } /// /// Detect objects in the image. /// @@ -161,18 +176,27 @@ private bool UseAzureForRecogition(IList objects) /// private async Task DetectObjects(Image image) { + var file = new FileInfo(image.FullPath); + var thumbSize = ThumbSize.Large; + var medThumb = new FileInfo(_thumbService.GetThumbPath(file, thumbSize)); + var fileName = Path.Combine(image.Folder.Path, image.FileName); + try { var foundObjects = new List(); var foundFaces = new List(); - var file = new FileInfo(image.FullPath); - var thumbSize = ThumbSize.Large; - var medThumb = new FileInfo(_thumbService.GetThumbPath(file, thumbSize)); - var fileName = Path.Combine(image.Folder.Path, image.FileName); Logging.Log($"Processing AI image detection for {file.Name}..."); - // Load the bitmap once - var bitmap = new System.Drawing.Bitmap(medThumb.FullName); + if (!File.Exists(medThumb.FullName) ) + { + // The thumb isn't ready yet. + return; + } + + var bitmap = SafeLoadBitmap(medThumb.FullName); + + if (bitmap == null) + return; // Next, look for faces. We need to determine if we: // a) Use only local (Accord.Net) detection @@ -351,7 +375,7 @@ private async Task DetectObjects(Image image) var allFound = foundObjects.Union(foundFaces).ToList(); // Write faces locally with rectangles - for debugging - DrawRects(image.FullPath, allFound); + DrawRects(medThumb.FullName, allFound); using var db = new ImageContext(); @@ -365,7 +389,7 @@ private async Task DetectObjects(Image image) } catch (Exception ex) { - Logging.LogError($"Exception during AI detection: {ex}"); + Logging.LogError($"Exception during AI detection for {fileName}: {ex}"); } } @@ -408,14 +432,21 @@ private void DrawRects(string fullPath, List imgObjs) { if (System.Diagnostics.Debugger.IsAttached) { - string outDir = "/Users/markotway/Desktop/Faces"; - if (!System.IO.Directory.Exists(outDir)) - System.IO.Directory.CreateDirectory(outDir); + try + { + string outDir = "/Users/markotway/Desktop/Faces"; + if (!System.IO.Directory.Exists(outDir)) + System.IO.Directory.CreateDirectory(outDir); - var output = Path.Combine(outDir, Path.GetFileName(fullPath)); + var output = Path.Combine(outDir, Path.GetFileName(fullPath)); - var rects = imgObjs.Select(x => new SixLabors.ImageSharp.Rectangle(x.RectX, x.RectY, x.RectWidth, x.RectHeight)).ToList(); - ImageSharpProcessor.DrawRects(fullPath, rects, output); + var rects = imgObjs.Select(x => new SixLabors.ImageSharp.Rectangle(x.RectX, x.RectY, x.RectWidth, x.RectHeight)).ToList(); + ImageSharpProcessor.DrawRects(fullPath, rects, output); + } + catch (Exception ex) + { + Logging.LogError($"Exception while drawing rects for {fullPath}: {ex}"); + } } } diff --git a/Damselfly.Core/Services/ThumbnailService.cs b/Damselfly.Core/Services/ThumbnailService.cs index 04350fd8..7ee2c00d 100644 --- a/Damselfly.Core/Services/ThumbnailService.cs +++ b/Damselfly.Core/Services/ThumbnailService.cs @@ -92,7 +92,7 @@ public string GetThumbPath(FileInfo imageFile, ThumbSize size) } else { - string extension = Path.GetExtension(imageFile.Name); + string extension = ".jpg";// Always use .jpg - we don't want to create .heic thumbs, etc string baseName = Path.GetFileNameWithoutExtension(imageFile.Name); string relativePath = imageFile.DirectoryName.MakePathRelativeTo(PicturesRoot); string thumbFileName = $"{baseName}_{GetSizePostFix(size)}{extension}"; @@ -169,7 +169,7 @@ private Dictionary GetThumbConfigs(FileInfo source, bool { var destFile = new FileInfo( GetThumbPath(source, thumbConfig.size) ); - if( ! destFile.Directory.Exists ) + if ( ! destFile.Directory.Exists ) { Logging.LogTrace("Creating directory: {0}", destFile.Directory.FullName); var newDir = System.IO.Directory.CreateDirectory( destFile.Directory.FullName ); diff --git a/Damselfly.Web/Startup.cs b/Damselfly.Web/Startup.cs index 714c5abf..0b486be8 100644 --- a/Damselfly.Web/Startup.cs +++ b/Damselfly.Web/Startup.cs @@ -53,7 +53,7 @@ public void ConfigureServices(IServiceCollection services) services.AddFileReaderService(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(x => x.GetRequiredService()); services.AddSingleton(x => x.GetRequiredService());