From 32a1d709a59bb85d6269d76680e30d00071b49b7 Mon Sep 17 00:00:00 2001 From: ema Date: Sun, 15 Dec 2024 05:34:50 +0800 Subject: [PATCH] Support .icns image #1480 --- .../AnimatedImage/Providers/IcnsProvider.cs | 855 ++++++++++++++++++ .../MetaProvider.cs | 17 +- .../QuickLook.Plugin.ImageViewer/Plugin.cs | 5 +- .../QuickLook.Plugin.ImageViewer.csproj | 1 + 4 files changed, 873 insertions(+), 5 deletions(-) create mode 100644 QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/IcnsProvider.cs diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/IcnsProvider.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/IcnsProvider.cs new file mode 100644 index 0000000..71ee1cb --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/IcnsProvider.cs @@ -0,0 +1,855 @@ +// Copyright © 2024 ema +// +// 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.Plugin; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; +using Rectangle = System.Drawing.Rectangle; +using Size = System.Windows.Size; + +namespace QuickLook.Plugin.ImageViewer.AnimatedImage.Providers; + +/// +/// https://github.com/BrokenEvent/CsIcnsReader +/// Note: Not support the j2k format compression +/// +internal class IcnsProvider : AnimationProvider +{ + private IcnsImage[] _images; + + public IcnsProvider(Uri path, MetaProvider meta, ContextObject contextObject) : base(path, meta, contextObject) + { + _images = IcnsImageParser.GetImages(Path.LocalPath); + } + + public override void Dispose() + { + if (_images != null) + { + try + { + foreach (var image in _images) + { + image.Bitmap?.Dispose(); + } + } + catch + { + // Nothing is important + } + + _images = null; + } + } + + public override Task GetRenderedFrame(int index) + { + if (_images == null || _images.Length <= 0) + { + return new Task(() => null); + } + + IcnsImage image = _images + .Where(image => image.Bitmap != null) + .OrderByDescending(image => image.Bitmap.Width) + .FirstOrDefault(); + + if (image == null) + { + return new Task(() => null); + } + + using (var memoryStream = new MemoryStream()) + { + image.Bitmap.Save(memoryStream, ImageFormat.Png); + memoryStream.Seek(0, SeekOrigin.Begin); + + var bs = new BitmapImage(); + bs.BeginInit(); + bs.StreamSource = memoryStream; + bs.CacheOption = BitmapCacheOption.OnLoad; + bs.EndInit(); + bs.Freeze(); + + return new Task(() => bs); + } + } + + public override Task GetThumbnail(Size renderSize) + { + // Not implementing thumbnail method + return GetRenderedFrame(0); + } +} + +internal static class IcnsDecoder +{ + private static readonly uint[] PALETE_4BPP = + [ + 0xffffffff, 0xfffcf305, 0xffff6402, 0xffdd0806, + 0xfff20884, 0xff4600a5, 0xff0000d4, 0xff02abea, + 0xff1fb714, 0xff006411, 0xff562c05, 0xff90713a, + 0xffc0c0c0, 0xff808080, 0xff404040, 0xff000000 + ]; + + private static readonly uint[] PALETTE_8BPP = + [ + 0xFFFFFFFF, 0xFFFFFFCC, 0xFFFFFF99, 0xFFFFFF66, + 0xFFFFFF33, 0xFFFFFF00, 0xFFFFCCFF, 0xFFFFCCCC, + 0xFFFFCC99, 0xFFFFCC66, 0xFFFFCC33, 0xFFFFCC00, + 0xFFFF99FF, 0xFFFF99CC, 0xFFFF9999, 0xFFFF9966, + 0xFFFF9933, 0xFFFF9900, 0xFFFF66FF, 0xFFFF66CC, + 0xFFFF6699, 0xFFFF6666, 0xFFFF6633, 0xFFFF6600, + 0xFFFF33FF, 0xFFFF33CC, 0xFFFF3399, 0xFFFF3366, + 0xFFFF3333, 0xFFFF3300, 0xFFFF00FF, 0xFFFF00CC, + 0xFFFF0099, 0xFFFF0066, 0xFFFF0033, 0xFFFF0000, + 0xFFCCFFFF, 0xFFCCFFCC, 0xFFCCFF99, 0xFFCCFF66, + 0xFFCCFF33, 0xFFCCFF00, 0xFFCCCCFF, 0xFFCCCCCC, + 0xFFCCCC99, 0xFFCCCC66, 0xFFCCCC33, 0xFFCCCC00, + 0xFFCC99FF, 0xFFCC99CC, 0xFFCC9999, 0xFFCC9966, + 0xFFCC9933, 0xFFCC9900, 0xFFCC66FF, 0xFFCC66CC, + 0xFFCC6699, 0xFFCC6666, 0xFFCC6633, 0xFFCC6600, + 0xFFCC33FF, 0xFFCC33CC, 0xFFCC3399, 0xFFCC3366, + 0xFFCC3333, 0xFFCC3300, 0xFFCC00FF, 0xFFCC00CC, + 0xFFCC0099, 0xFFCC0066, 0xFFCC0033, 0xFFCC0000, + 0xFF99FFFF, 0xFF99FFCC, 0xFF99FF99, 0xFF99FF66, + 0xFF99FF33, 0xFF99FF00, 0xFF99CCFF, 0xFF99CCCC, + 0xFF99CC99, 0xFF99CC66, 0xFF99CC33, 0xFF99CC00, + 0xFF9999FF, 0xFF9999CC, 0xFF999999, 0xFF999966, + 0xFF999933, 0xFF999900, 0xFF9966FF, 0xFF9966CC, + 0xFF996699, 0xFF996666, 0xFF996633, 0xFF996600, + 0xFF9933FF, 0xFF9933CC, 0xFF993399, 0xFF993366, + 0xFF993333, 0xFF993300, 0xFF9900FF, 0xFF9900CC, + 0xFF990099, 0xFF990066, 0xFF990033, 0xFF990000, + 0xFF66FFFF, 0xFF66FFCC, 0xFF66FF99, 0xFF66FF66, + 0xFF66FF33, 0xFF66FF00, 0xFF66CCFF, 0xFF66CCCC, + 0xFF66CC99, 0xFF66CC66, 0xFF66CC33, 0xFF66CC00, + 0xFF6699FF, 0xFF6699CC, 0xFF669999, 0xFF669966, + 0xFF669933, 0xFF669900, 0xFF6666FF, 0xFF6666CC, + 0xFF666699, 0xFF666666, 0xFF666633, 0xFF666600, + 0xFF6633FF, 0xFF6633CC, 0xFF663399, 0xFF663366, + 0xFF663333, 0xFF663300, 0xFF6600FF, 0xFF6600CC, + 0xFF660099, 0xFF660066, 0xFF660033, 0xFF660000, + 0xFF33FFFF, 0xFF33FFCC, 0xFF33FF99, 0xFF33FF66, + 0xFF33FF33, 0xFF33FF00, 0xFF33CCFF, 0xFF33CCCC, + 0xFF33CC99, 0xFF33CC66, 0xFF33CC33, 0xFF33CC00, + 0xFF3399FF, 0xFF3399CC, 0xFF339999, 0xFF339966, + 0xFF339933, 0xFF339900, 0xFF3366FF, 0xFF3366CC, + 0xFF336699, 0xFF336666, 0xFF336633, 0xFF336600, + 0xFF3333FF, 0xFF3333CC, 0xFF333399, 0xFF333366, + 0xFF333333, 0xFF333300, 0xFF3300FF, 0xFF3300CC, + 0xFF330099, 0xFF330066, 0xFF330033, 0xFF330000, + 0xFF00FFFF, 0xFF00FFCC, 0xFF00FF99, 0xFF00FF66, + 0xFF00FF33, 0xFF00FF00, 0xFF00CCFF, 0xFF00CCCC, + 0xFF00CC99, 0xFF00CC66, 0xFF00CC33, 0xFF00CC00, + 0xFF0099FF, 0xFF0099CC, 0xFF009999, 0xFF009966, + 0xFF009933, 0xFF009900, 0xFF0066FF, 0xFF0066CC, + 0xFF006699, 0xFF006666, 0xFF006633, 0xFF006600, + 0xFF0033FF, 0xFF0033CC, 0xFF003399, 0xFF003366, + 0xFF003333, 0xFF003300, 0xFF0000FF, 0xFF0000CC, + 0xFF000099, 0xFF000066, 0xFF000033, 0xFFEE0000, + 0xFFDD0000, 0xFFBB0000, 0xFFAA0000, 0xFF880000, + 0xFF770000, 0xFF550000, 0xFF440000, 0xFF220000, + 0xFF110000, 0xFF00EE00, 0xFF00DD00, 0xFF00BB00, + 0xFF00AA00, 0xFF008800, 0xFF007700, 0xFF005500, + 0xFF004400, 0xFF002200, 0xFF001100, 0xFF0000EE, + 0xFF0000DD, 0xFF0000BB, 0xFF0000AA, 0xFF000088, + 0xFF000077, 0xFF000055, 0xFF000044, 0xFF000022, + 0xFF000011, 0xFFEEEEEE, 0xFFDDDDDD, 0xFFBBBBBB, + 0xFFAAAAAA, 0xFF888888, 0xFF777777, 0xFF555555, + 0xFF444444, 0xFF222222, 0xFF111111, 0xFF000000 + ]; + + // http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html + private static readonly byte[] PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10]; + + private static IcnsImage TryDecodingPng(IcnsImageParser.IcnsElement element, IcnsType imageType) + { + if (element.data.Length < PNG_SIGNATURE.Length) + return null; // definitely not a valid png + + for (int i = 0; i < PNG_SIGNATURE.Length; i++) + if (element.data[i] != PNG_SIGNATURE[i]) + return null; // not a png + + using (var ms = new MemoryStream(element.data)) + return new IcnsImage((Bitmap)Image.FromStream(ms), imageType); // cast is valid, for PNG it will be Bitmap + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static unsafe void SetPixel(BitmapData data, int x, int y, uint color) + { + *(((uint*)data.Scan0) + y * data.Width + x) = color; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static unsafe uint GetPixel(BitmapData data, int x, int y) + { + return *(((uint*)data.Scan0) + y * data.Width + x); + } + + private static void Decode1BPPImage(IcnsType imageType, byte[] imageData, BitmapData image) + { + int position = 0; + int bitsLeft = 0; + int value = 0; + for (int y = 0; y < imageType.Height; y++) + for (int x = 0; x < imageType.Width; x++) + { + if (bitsLeft == 0) + { + value = 0xff & imageData[position++]; + bitsLeft = 8; + } + + uint argb; + argb = (value & 0x80u) != 0 ? 0xff000000u : 0xffffffffu; + value <<= 1; + bitsLeft--; + + SetPixel(image, x, y, argb); + } + } + + private static void Decode4BPPImage(IcnsType imageType, byte[] imageData, BitmapData image) + { + int i = 0; + bool visited = false; + for (int y = 0; y < imageType.Height; y++) + for (int x = 0; x < imageType.Width; x++) + { + int index; + if (!visited) + index = 0xf & (imageData[i] >> 4); + else + index = 0xf & imageData[i++]; + visited = !visited; + + SetPixel(image, x, y, PALETE_4BPP[index]); + } + } + + private static void Decode8BPPImage(IcnsType imageType, byte[] imageData, BitmapData image) + { + for (int y = 0; y < imageType.Height; y++) + for (int x = 0; x < imageType.Width; x++) + { + int index = 0xff & imageData[y * imageType.Width + x]; + + SetPixel(image, x, y, PALETTE_8BPP[index]); + } + } + + private static void Decode32BPPImage(IcnsType imageType, byte[] imageData, BitmapData image) + { + for (int y = 0; y < imageType.Height; y++) + for (int x = 0; x < imageType.Width; x++) + { + uint argb = (0xff000000u /* the "alpha" is ignored */| + ((0xffu & imageData[4 * (y * imageType.Width + x) + 1]) << 16) | + ((0xffu & imageData[4 * (y * imageType.Width + x) + 2]) << 8) | + (0xffu & imageData[4 * (y * imageType.Width + x) + 3])); + + SetPixel(image, x, y, argb); + } + } + + private static void Decode32BPPImageARGB(IcnsType imageType, byte[] imageData, BitmapData image) + { + for (int y = 0; y < imageType.Height; y++) + for (int x = 0; x < imageType.Width; x++) + { + uint argb = (((0xffu & imageData[4 * (y * imageType.Width + x) + 0]) << 24) | + ((0xffu & imageData[4 * (y * imageType.Width + x) + 1]) << 16) | + ((0xffu & imageData[4 * (y * imageType.Width + x) + 2]) << 8) | + (0xffu & imageData[4 * (y * imageType.Width + x) + 3])); + + SetPixel(image, x, y, argb); + } + } + + private static void Apply1BPPMask(byte[] maskData, BitmapData image) + { + int position; + int bitsLeft = 0; + int value = 0; + + // 1 bit icon types have image data followed by mask data in the same entry + int totalBytes = (image.Width * image.Height + 7) / 8; + + if (maskData.Length >= 2 * totalBytes) + position = totalBytes; + else + throw new ArgumentException("1 BPP mask underrun parsing ICNS file"); + + for (int y = 0; y < image.Height; y++) + for (int x = 0; x < image.Width; x++) + { + if (bitsLeft == 0) + { + value = 0xff & maskData[position++]; + bitsLeft = 8; + } + + uint alpha; + alpha = (value & 0x80u) != 0 ? 0xffu : 0x00u; + value <<= 1; + bitsLeft--; + + SetPixel(image, x, y, (alpha << 24) | (0xffffffu & GetPixel(image, x, y))); + } + } + + private static void Apply8BPPMask(byte[] maskData, BitmapData image) + { + for (int y = 0; y < image.Height; y++) + for (int x = 0; x < image.Width; x++) + { + uint alpha = 0xffu & maskData[y * image.Width + x]; + SetPixel(image, x, y, alpha << 24 | (0xffffffu & GetPixel(image, x, y))); + } + } + + private static IcnsImageParser.IcnsElement FindElement(IEnumerable elements, int targetType) + { + foreach (IcnsImageParser.IcnsElement element in elements) + if (element.type == targetType) + return element; + + return null; + } + + private static IcnsImage DecodeImage(IcnsImageParser.IcnsElement imageElement, IcnsImageParser.IcnsElement[] icnsElements) + { + IcnsType imageType = IcnsType.FindType(imageElement.type, IcnsType.TypeDetails.Mask); + if (imageType == null) + return null; + + IcnsType maskType = null; + IcnsImageParser.IcnsElement maskElement = null; + + if (imageType.Details == IcnsType.TypeDetails.HasMask) + { + maskType = imageType; + maskElement = imageElement; + } + else if (imageType.Details == IcnsType.TypeDetails.None) + { + maskType = IcnsType.FindType(imageType.Width, imageType.Height, 8, IcnsType.TypeDetails.Mask); + if (maskType != null) + maskElement = FindElement(icnsElements, maskType.Type); + + if (maskElement == null) + { + maskType = IcnsType.FindType(imageType.Width, imageType.Height, 1, IcnsType.TypeDetails.Mask); + if (maskType != null) + maskElement = FindElement(icnsElements, maskType.Type); + } + } + + if (imageType.Details == IcnsType.TypeDetails.Compressed || + imageType.Details == IcnsType.TypeDetails.Retina) + { + IcnsImage result = TryDecodingPng(imageElement, imageType); + if (result != null) + return result; // png + + // Should try decoding using the j2k library + + return null; // couldn't be loaded + } + + int expectedSize = (imageType.Width * imageType.Height * imageType.BitsPerPixel + 7) / 8; + byte[] imageData; + + if (imageElement.data.Length < expectedSize) + { + if (imageType.BitsPerPixel == 32) + imageData = Rle24Compression.Decompress(imageType.Width, imageType.Height, imageElement.data); + else + throw new Exception("Short image data but not a 32 bit compressed type"); + } + else + imageData = imageElement.data; + + Bitmap image = new(imageType.Width, imageType.Height, PixelFormat.Format32bppArgb); + BitmapData bitmapData = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); + + switch (imageType.BitsPerPixel) + { + case 1: + Decode1BPPImage(imageType, imageData, bitmapData); + break; + + case 4: + Decode4BPPImage(imageType, imageData, bitmapData); + break; + + case 8: + Decode8BPPImage(imageType, imageData, bitmapData); + break; + + case 32: + if (imageType.Details == IcnsType.TypeDetails.ARGB) + Decode32BPPImageARGB(imageType, imageData, bitmapData); + else + Decode32BPPImage(imageType, imageData, bitmapData); + break; + + default: + image.UnlockBits(bitmapData); + image.Dispose(); + throw new NotSupportedException("Unsupported bit depth " + imageType.BitsPerPixel); + } + + if (maskElement != null) + { + switch (maskType.BitsPerPixel) + { + case 1: + Apply1BPPMask(maskElement.data, bitmapData); + break; + + case 8: + Apply8BPPMask(maskElement.data, bitmapData); + break; + + default: + image.UnlockBits(bitmapData); + image.Dispose(); + throw new NotSupportedException("Unsupport mask bit depth " + maskType.BitsPerPixel); + } + } + + image.UnlockBits(bitmapData); + return new IcnsImage(image, imageType); + } + + public static IcnsImage[] DecodeAllImages(IcnsImageParser.IcnsElement[] icnsElements) + { + List result = []; + + for (int i = 0; i < icnsElements.Length; i++) + { + IcnsImage image = DecodeImage(icnsElements[i], icnsElements); + if (image != null) + result.Add(image); + } + return [.. result]; + } +} + +internal class IcnsImage(Bitmap bitmap, IcnsType type) +{ + public Bitmap Bitmap => bitmap; + + public IcnsType Type => type; +} + +internal static class IcnsImageParser +{ + public static int ICNS_MAGIC = IcnsType.TypeAsInt("icns"); + + private static int Read4Bytes(Stream stream) + { + byte byte0 = (byte)stream.ReadByte(); + byte byte1 = (byte)stream.ReadByte(); + byte byte2 = (byte)stream.ReadByte(); + byte byte3 = (byte)stream.ReadByte(); + + return ((0xff & byte0) << 24) | + ((0xff & byte1) << 16) | + ((0xff & byte2) << 8) | + ((0xff & byte3) << 0); + } + + private static void Write4Bytes(Stream stream, int value) + { + stream.WriteByte((byte)((value & 0xff000000) >> 24)); + stream.WriteByte((byte)((value & 0x00ff0000) >> 16)); + stream.WriteByte((byte)((value & 0x0000ff00) >> 8)); + stream.WriteByte((byte)((value & 0x000000ff) >> 0)); + } + + private class IcnsHeader(int magic, int fileSize) + { + public int magic = magic; // Magic literal (4 bytes), always "icns" + public int fileSize = fileSize; // Length of file (4 bytes), in bytes. + } + + private static IcnsHeader ReadIcnsHeader(Stream stream) + { + int Magic = Read4Bytes(stream); + int FileSize = Read4Bytes(stream); + + if (Magic != ICNS_MAGIC) + throw new Exception("Wrong ICNS magic"); + + return new IcnsHeader(Magic, FileSize); + } + + public class IcnsElement(int type, int elementSize, byte[] data) + { + public int type = type; + public int elementSize = elementSize; + public byte[] data = data; + } + + private static IcnsElement ReadIcnsElement(Stream stream) + { + int type = Read4Bytes(stream); // Icon type (4 bytes) + int elementSize = Read4Bytes(stream); // Length of data (4 bytes), in bytes, including this header + byte[] data = new byte[elementSize - 8]; + stream.Read(data, 0, data.Length); + + return new IcnsElement(type, elementSize, data); + } + + private static IcnsElement[] ReadImage(Stream stream) + { + IcnsHeader icnsHeader = ReadIcnsHeader(stream); + + List icnsElementList = []; + for (int remainingSize = icnsHeader.fileSize - 8; remainingSize > 0;) + { + IcnsElement icnsElement = ReadIcnsElement(stream); + icnsElementList.Add(icnsElement); + remainingSize -= icnsElement.elementSize; + } + + IcnsElement[] icnsElements = new IcnsElement[icnsElementList.Count]; + for (int i = 0; i < icnsElements.Length; i++) + icnsElements[i] = icnsElementList[i]; + + return icnsElements; + } + + public static IcnsImage GetImage(string filename) + { + using (var stream = new FileStream(filename, FileMode.Open)) + return GetImage(stream); + } + + public static IcnsImage GetImage(Stream stream) + { + IcnsElement[] icnsContents = ReadImage(stream); + IcnsImage[] result = IcnsDecoder.DecodeAllImages(icnsContents); + if (result.Length <= 0) + throw new NotSupportedException("No icons in ICNS file"); + + IcnsImage max = null; + foreach (IcnsImage bitmap in result) + if (bitmap.Bitmap != null && (max == null || (bitmap.Bitmap.Width > bitmap.Bitmap.Height))) + max = bitmap; + + return max; + } + + public static IcnsImage[] GetImages(string filename) + { + using (var stream = new FileStream(filename, FileMode.Open)) + return GetImages(stream); + } + + public static IcnsImage[] GetImages(Stream stream) + { + IcnsElement[] icnsContents = ReadImage(stream); + return IcnsDecoder.DecodeAllImages(icnsContents); + } + + public static void WriteImage(Bitmap src, Stream stream) + { + IcnsType imageType = IcnsType.FindType(src.Width, src.Height, 32, IcnsType.TypeDetails.None); + if (imageType == null) + throw new NotSupportedException($"Invalid/unsupported source: {src.Width}x{src.Height}"); + + Write4Bytes(stream, ICNS_MAGIC); + Write4Bytes(stream, 4 + 4 + 4 + 4 + 4 * imageType.Width * imageType.Height + 4 + 4 + imageType.Width * imageType.Height); + + Write4Bytes(stream, imageType.Type); + Write4Bytes(stream, 4 + 4 + 4 * imageType.Width * imageType.Height); + + BitmapData bitmapData = src.LockBits(new Rectangle(0, 0, src.Width, src.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + + // the image + for (int y = 0; y < src.Height; y++) + { + for (int x = 0; x < src.Width; x++) + { + uint argb = IcnsDecoder.GetPixel(bitmapData, x, y); + stream.WriteByte(0); + stream.WriteByte((byte)((argb & 0x00ff0000) >> 16)); + stream.WriteByte((byte)((argb & 0x0000ff00) >> 8)); + stream.WriteByte((byte)((argb & 0x000000ff) >> 0)); + } + } + + // mask + IcnsType maskType = IcnsType.FindType(src.Width, src.Height, 8, IcnsType.TypeDetails.Mask); + Write4Bytes(stream, maskType.Type); + Write4Bytes(stream, 4 + 4 + imageType.Width * imageType.Width); + + for (int y = 0; y < src.Height; y++) + { + for (int x = 0; x < src.Width; x++) + { + uint argb = IcnsDecoder.GetPixel(bitmapData, x, y); + stream.WriteByte((byte)((argb & 0xff000000) >> 24)); + } + } + } + + public static void WriteImage(Bitmap src, string filename) + { + using (var stream = new FileStream(filename, FileMode.Create)) + WriteImage(src, stream); + } +} + +internal class IcnsType +{ + private readonly int type; + private readonly int width; + private readonly int height; + private readonly int bitsPerPixel; + private readonly TypeDetails details; + + // https://en.wikipedia.org/wiki/Apple_Icon_Image_format + public static readonly IcnsType[] ALL_TYPES = + [ + // 16x12 + new IcnsType("icm#", 16, 12, 1, TypeDetails.HasMask), + new IcnsType("icm4", 16, 12, 4, TypeDetails.None), + new IcnsType("icm8", 16, 12, 8, TypeDetails.None), + // 16x16 + new IcnsType("ics#", 16, 16, 1, TypeDetails.Mask), + new IcnsType("ics4", 16, 16, 4, TypeDetails.None), + new IcnsType("ics8", 16, 16, 8, TypeDetails.None), + new IcnsType("is32", 16, 16, 32, TypeDetails.None), + new IcnsType("s8mk", 16, 16, 8, TypeDetails.Mask), + new IcnsType("icp4", 16, 16, 32, TypeDetails.Compressed), + new IcnsType("ic04", 16, 16, 32, TypeDetails.ARGB), + // 18x18 + new IcnsType("icsb", 18, 18, 32, TypeDetails.ARGB), // not tested + // 32x32 + new IcnsType("ICON", 32, 32, 1, TypeDetails.None), + new IcnsType("ICN#", 32, 32, 1, TypeDetails.HasMask), + new IcnsType("icl4", 32, 32, 4, TypeDetails.None), + new IcnsType("icl8", 32, 32, 8, TypeDetails.None), + new IcnsType("il32", 32, 32, 32, TypeDetails.None), + new IcnsType("l8mk", 32, 32, 8, TypeDetails.Mask), + new IcnsType("icp5", 32, 32, 32, TypeDetails.Compressed), + new IcnsType("ic11", 32, 32, 32, TypeDetails.Retina), + new IcnsType("ic05", 32, 32, 32, TypeDetails.ARGB), + // 36x36 + new IcnsType("icsB", 36, 36, 32, TypeDetails.ARGB), // not tested + // 48x48 + new IcnsType("ich#", 48, 48, 1, TypeDetails.Mask), + new IcnsType("ich4", 48, 48, 4, TypeDetails.None), + new IcnsType("ich8", 48, 48, 8, TypeDetails.None), + new IcnsType("ih32", 48, 48, 32, TypeDetails.None), + new IcnsType("h8mk", 48, 48, 8, TypeDetails.Mask), + // 64x64 + new IcnsType("icp6", 64, 64, 32, TypeDetails.Compressed), + new IcnsType("ic12", 64, 64, 32, TypeDetails.Retina), + // 128x128 + new IcnsType("it32", 128, 128, 32, TypeDetails.None), + new IcnsType("t8mk", 128, 128, 8, TypeDetails.Mask), + new IcnsType("ic07", 128, 128, 32, TypeDetails.Compressed), + // other + new IcnsType("ic08", 256, 256, 32, TypeDetails.Compressed), + new IcnsType("ic13", 256, 256, 32, TypeDetails.Retina), + new IcnsType("ic09", 512, 512, 32, TypeDetails.Compressed), + new IcnsType("ic14", 512, 512, 32, TypeDetails.Retina), + new IcnsType("ic10", 1024, 1024, 32, TypeDetails.Retina), + ]; + + private IcnsType(string type, int width, int height, int bitsPerPixel, TypeDetails details) + { + this.type = TypeAsInt(type); + this.width = width; + this.height = height; + this.bitsPerPixel = bitsPerPixel; + this.details = details; + } + + public int Type => type; + + public int Width => width; + + public int Height => height; + + public int BitsPerPixel => bitsPerPixel; + + public TypeDetails Details => details; + + public static IcnsType FindType(int type, TypeDetails ignoreDetails) + { + for (int i = 0; i < ALL_TYPES.Length; i++) + { + if (ALL_TYPES[i].type != type) + continue; + + if (ignoreDetails != 0 && ALL_TYPES[i].Details == ignoreDetails) + continue; + + return ALL_TYPES[i]; + } + return null; + } + + public static IcnsType FindType(int width, int height, int bpp, TypeDetails details) + { + for (int i = 0; i < ALL_TYPES.Length; i++) + { + IcnsType type = ALL_TYPES[i]; + if (type.width == width && + type.height == height && + type.bitsPerPixel == bpp && + type.details == details) + return type; + } + + return null; + } + + public static int TypeAsInt(string type) + { + byte[] bytes = Encoding.ASCII.GetBytes(type); + + if (bytes.Length != 4) + throw new Exception("Invalid ICNS type"); + + return ((0xff & bytes[0]) << 24) | + ((0xff & bytes[1]) << 16) | + ((0xff & bytes[2]) << 8) | + (0xff & bytes[3]); + } + + public static string DescribeType(int type) + { + byte[] bytes = + [ + (byte)(0xff & (type >> 24)), + (byte)(0xff & (type >> 16)), + (byte)(0xff & (type >> 8)), + (byte)(0xff & type), + ]; + return Encoding.ASCII.GetString(bytes); + } + + public enum TypeDetails + { + /// + /// The default image with no detils. + /// + None, + + /// + /// The image is alpha mask. + /// + Mask, + + /// + /// Has alpha mask. + /// + HasMask, + + /// + /// Whole 4 channels are used. + /// + ARGB, + + /// + /// Compressed, j2k or PNG codec is used. + /// + Compressed, + + /// + /// Retina (2x) image. j2k or PNG is used. + /// + Retina, + } +} + +internal class Rle24Compression +{ + public static byte[] Decompress(int width, int height, byte[] data) + { + int pixelCount = width * height; + byte[] result = new byte[4 * pixelCount]; + + // Several ICNS parsers advance by 4 bytes here: + // http://code.google.com/p/icns2png/ - when the width is >= 128 + // http://icns.sourceforge.net/ - when those 4 bytes are all zero + // + // A scan of all .icns files on MacOS shows that + // all 128x128 images indeed start with 4 zeroes, + // while all smaller images don't. + // However it is dangerous to assume + // that 4 initial zeroes always need to be skipped, + // because they could encode valid pixels on smaller images. + // So always skip on 128x128, and never skip on anything else. + int dataPos = 0; + if (width >= 128 && height >= 128) + dataPos = 4; + + // argb, band by band in 3 passes, with no alpha + for (int band = 1; band <= 3; band++) + { + int remaining = pixelCount; + int resultPos = 0; + while (remaining > 0) + { + if ((data[dataPos] & 0x80) != 0) + { + int count = (0xff & data[dataPos]) - 125; + for (int i = 0; i < count; i++) + { + int idx = band + 4 * (resultPos++); + if (idx < result.Length) + result[idx] = data[dataPos + 1]; + } + dataPos += 2; + remaining -= count; + } + else + { + int count = (0xff & data[dataPos]) + 1; + dataPos++; + for (int i = 0; i < count; i++) + { + byte value = data[dataPos++]; + int idx = band + 4 * (resultPos++); + if (idx < result.Length) + result[idx] = value; + } + remaining -= count; + } + } + } + return result; + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/MetaProvider.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/MetaProvider.cs index ac551c0..4bc2aaf 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/MetaProvider.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/MetaProvider.cs @@ -82,11 +82,20 @@ public class MetaProvider // fallback - using (var mi = new MagickImage()) + try { - mi.Ping(_path); - w = (int)mi.Width; - h = (int)mi.Height; + using (var mi = new MagickImage()) + { + mi.Ping(_path); + w = (int)mi.Width; + h = (int)mi.Height; + } + } + catch + { + // There are always formats that MagickImage does not support + // TODO: Use MediaInfo to detect it? + return Size.Empty; } return w + h == 0 ? new Size(800, 600) : new Size(w, h); diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs index 116e49b..f97622e 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs @@ -38,7 +38,7 @@ public class Plugin : IViewer ".fff", ".gif", ".hdr", ".heic", ".heif", - ".ico", ".icon", ".iiq", + ".ico", ".icon", ".icns", ".iiq", ".jfif", ".jp2", ".jpeg", ".jpg", ".jxl", ".k25", ".kdc", ".mdc", ".mef", ".mos", ".mrw", @@ -73,6 +73,9 @@ public class Plugin : IViewer new KeyValuePair( useColorProfile ? [] : [".bmp", ".jpg", ".jpeg", ".jfif", ".tif", ".tiff"], typeof(NativeProvider))); + AnimatedImage.AnimatedImage.Providers.Add( + new KeyValuePair([".icns"], + typeof(IcnsProvider))); AnimatedImage.AnimatedImage.Providers.Add( new KeyValuePair(["*"], typeof(ImageMagickProvider))); diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj index 220ef2e..31afaaa 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj @@ -10,6 +10,7 @@ true latest false + true false false MinimumRecommendedRules.ruleset