// Copyright © 2017-2026 QL-Win Contributors // // This file is part of QuickLook program. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . using QuickLook.Common.Helpers; using QuickLook.Common.Plugin; using QuickLook.Common.Plugin.MoreMenu; using QuickLook.Plugin.ImageViewer.AnimatedImage.Providers; using QuickLook.Plugin.ImageViewer.Webview; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Windows; using System.Windows.Input; namespace QuickLook.Plugin.ImageViewer; public sealed partial class Plugin : IViewer, IMoreMenu { private static readonly HashSet WellKnownExtensions = new( [ ".apng", ".ari", ".arw", ".avif", ".ani", ".bay", ".bmp", ".cap", ".cr2", ".cr3", ".crw", ".cur", ".dcr", ".dcs", ".dds", ".dng", ".drf", ".dcm", ".dicom", ".eip", ".emf", ".erf", ".exr", ".fff", ".gif", ".hdr", ".heic", ".heif", ".ico", ".icon", ".icns", ".iiq", ".jfif", ".jp2", ".jpeg", ".jpg", ".jxl", ".j2k", ".jpf", ".jpx", ".jpm", ".jxr", ".k25", ".kdc", ".mdc", ".mef", ".mos", ".mrw", ".mj2", ".miff", ".nef", ".nrw", ".obm", ".orf", ".pbm", ".pcx", ".pef", ".pgm", ".png", ".pnm", ".ppm", ".psb", ".psd", ".ptx", ".pxn", ".qoi", ".r3d", ".raf", ".raw", ".rw2", ".rwl", ".rwz", ".sr2", ".srf", ".srw", ".svg", ".svgz", ".tga", ".tif", ".tiff", ".wdp", ".webp", ".wmf", ".x3f", ".xcf", ".xbm", ".xpm", ]); private ImagePanel _ip; private string _currentPath; private MetaProvider _meta; private IWebImagePanel _ipWeb; private IWebMetaProvider _metaWeb; public int Priority => 0; public IEnumerable MenuItems => GetMenuItems(); public void Init() { // Option of UseColorProfile: // Default is False (disable color profile conversion) // Note that enabling this feature will slow down image previewing, especially on large images. var useColorProfile = SettingHelper.Get("UseColorProfile", false, "QuickLook.Plugin.ImageViewer"); // Option of UseNativeProvider: // Default is True (disable precise colors and choose faster response) // Note that disabling this feature may slightly slow down image previewing but you can get precise colors. var useNativeProvider = SettingHelper.Get("UseNativeProvider", true, "QuickLook.Plugin.ImageViewer"); AnimatedImage.AnimatedImage.Providers.Add( new KeyValuePair( useColorProfile ? [".apng"] : [".apng", ".png"], typeof(APngProvider))); AnimatedImage.AnimatedImage.Providers.Add( new KeyValuePair([".gif"], typeof(GifProvider))); AnimatedImage.AnimatedImage.Providers.Add( new KeyValuePair( useColorProfile ? [] : (useNativeProvider ? [".bmp", ".jpg", ".jpeg", ".jfif", ".tif", ".tiff"] : []), typeof(NativeProvider))); AnimatedImage.AnimatedImage.Providers.Add( new KeyValuePair([".jxr"], typeof(WmpProvider))); AnimatedImage.AnimatedImage.Providers.Add( new KeyValuePair([".icns"], typeof(IcnsProvider))); AnimatedImage.AnimatedImage.Providers.Add( new KeyValuePair([".webp"], typeof(WebPProvider))); AnimatedImage.AnimatedImage.Providers.Add( new KeyValuePair([".cur", ".ani"], typeof(CursorProvider))); #if USESVGSKIA AnimatedImage.AnimatedImage.Providers.Add( new KeyValuePair([".svg"], typeof(SvgProvider))); #endif AnimatedImage.AnimatedImage.Providers.Add( new KeyValuePair([".dcm", ".dicom"], typeof(DicomProvider))); AnimatedImage.AnimatedImage.Providers.Add( new KeyValuePair(["*"], typeof(ImageMagickProvider))); } public bool CanHandle(string path) { if (WebHandler.TryCanHandle(path)) return true; if (Directory.Exists(path)) return false; // Disabled due mishandling text file types e.g., "*.config". // Only check extension for well known image and animated image types. if (WellKnownExtensions.Any(ext => path.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return true; // For files without extensions, check magic numbers for common image formats if (!Path.HasExtension(path)) { return IsImageByMagicNumber(path); } return false; } private static bool IsImageByMagicNumber(string path) { try { if (!File.Exists(path)) return false; ReadOnlySpan pngSignature = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; ReadOnlySpan jpegSignature = [0xFF, 0xD8, 0xFF]; ReadOnlySpan gif87Signature = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]; ReadOnlySpan gif89Signature = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]; ReadOnlySpan bmpSignature = [0x42, 0x4D]; ReadOnlySpan webpRiffSignature = [0x52, 0x49, 0x46, 0x46]; ReadOnlySpan webpWebpSignature = [0x57, 0x45, 0x42, 0x50]; const int maxSignatureLength = 12; using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); if (fs.Length < bmpSignature.Length) return false; var buffer = new byte[maxSignatureLength]; var bytesRead = fs.Read(buffer, 0, buffer.Length); if (bytesRead < bmpSignature.Length) return false; // PNG: 89 50 4E 47 0D 0A 1A 0A if (bytesRead >= pngSignature.Length && buffer.AsSpan(0, pngSignature.Length).SequenceEqual(pngSignature)) { return true; } // JPEG: FF D8 FF if (bytesRead >= jpegSignature.Length && buffer.AsSpan(0, jpegSignature.Length).SequenceEqual(jpegSignature)) { return true; } // GIF: GIF87a or GIF89a if (bytesRead >= gif87Signature.Length && (buffer.AsSpan(0, gif87Signature.Length).SequenceEqual(gif87Signature) || buffer.AsSpan(0, gif89Signature.Length).SequenceEqual(gif89Signature))) { return true; } // BMP: BM if (bytesRead >= bmpSignature.Length && buffer.AsSpan(0, bmpSignature.Length).SequenceEqual(bmpSignature)) { return true; } // WebP: RIFF....WEBP if (bytesRead >= 12 && buffer.AsSpan(0, webpRiffSignature.Length).SequenceEqual(webpRiffSignature) && buffer.AsSpan(8, webpWebpSignature.Length).SequenceEqual(webpWebpSignature)) { return true; } return false; } catch (IOException ex) { ProcessHelper.WriteLog($"IO error while checking image magic number for {path}: {ex.Message}"); return false; } catch (UnauthorizedAccessException ex) { ProcessHelper.WriteLog($"Access denied while checking image magic number for {path}: {ex.Message}"); return false; } catch (Exception ex) { ProcessHelper.WriteLog($"Unexpected error while checking image magic number for {path}: {ex}"); return false; } } public void Prepare(string path, ContextObject context) { if (WebHandler.TryPrepare(path, context, out _metaWeb)) return; _meta = new MetaProvider(path); var size = _meta.GetSize(); if (!size.IsEmpty) context.SetPreferredSizeFit(size, 0.8d); else context.PreferredSize = new Size(800, 600); context.Theme = (Themes)SettingHelper.Get("LastTheme", 1, "QuickLook.Plugin.ImageViewer"); } public void View(string path, ContextObject context) { _currentPath = path; if (WebHandler.TryView(path, context, _metaWeb, out _ipWeb)) return; _ip = new ImagePanel(context, _meta); var size = _meta.GetSize(); context.ViewerContent = _ip; context.Title = size.IsEmpty ? $"{Path.GetFileName(path)}" : $"{size.Width}×{size.Height}: {Path.GetFileName(path)}"; _ip.ImageUriSource = Helper.FilePathToFileUrl(path); // Load the custom cursor into the preview panel if (new[] { ".cur", ".ani" }.Any(ext => path.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) { _ip.Cursor = CursorProvider.GetCursor(path) ?? Cursors.Arrow; } } public void Cleanup() { GC.SuppressFinalize(this); _ip?.Dispose(); _ip = null; _ipWeb?.Dispose(); _ipWeb = null; } }