diff --git a/NOTICE.md b/NOTICE.md
index 5649fced0a29..4d8a05c57d66 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -788,6 +788,34 @@ SOFTWARE.
## Utility: Peek
+### The Quite OK Image Format reference decoder
+
+**Source**: https://github.com/phoboslab/qoi
+
+**Note**: [@pedrolamas](https://github.com/pedrolamas) translated and adapted the reference decoder code to C# that is in PowerToys from the original C++ implementation.
+
+MIT License
+
+Copyright (c) 2022 Dominic Szablewski
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
### UTF Unknown
We use the UTF.Unknown NuGet package for detecting encoding in text/code files.
diff --git a/src/common/FilePreviewCommon/FilePreviewCommon.csproj b/src/common/FilePreviewCommon/FilePreviewCommon.csproj
index f10873559fd3..da6b7bdbdb85 100644
--- a/src/common/FilePreviewCommon/FilePreviewCommon.csproj
+++ b/src/common/FilePreviewCommon/FilePreviewCommon.csproj
@@ -4,12 +4,15 @@
net8.0-windows
- win-x64;win-arm64
+ win-x64;win-arm64
$(Version).0
Microsoft Corporation
PowerToys
PowerToys FilePreviewCommon
PowerToys.FilePreviewCommon
+ true
+ true
+ enable
@@ -32,7 +35,7 @@
Always
- Always
+ Always
Always
diff --git a/src/common/FilePreviewCommon/Formatters/XmlFormatter.cs b/src/common/FilePreviewCommon/Formatters/XmlFormatter.cs
index b282c802ae73..450e92a5b37e 100644
--- a/src/common/FilePreviewCommon/Formatters/XmlFormatter.cs
+++ b/src/common/FilePreviewCommon/Formatters/XmlFormatter.cs
@@ -26,7 +26,7 @@ public string Format(string value)
var stringBuilder = new StringBuilder();
var xmlWriterSettings = new XmlWriterSettings()
{
- OmitXmlDeclaration = xmlDocument.FirstChild.NodeType != XmlNodeType.XmlDeclaration,
+ OmitXmlDeclaration = xmlDocument.FirstChild?.NodeType != XmlNodeType.XmlDeclaration,
Indent = true,
};
diff --git a/src/modules/previewpane/common/Utilities/GcodeHelper.cs b/src/common/FilePreviewCommon/GcodeHelper.cs
similarity index 98%
rename from src/modules/previewpane/common/Utilities/GcodeHelper.cs
rename to src/common/FilePreviewCommon/GcodeHelper.cs
index f614a099e4e6..62dc29554e84 100644
--- a/src/modules/previewpane/common/Utilities/GcodeHelper.cs
+++ b/src/common/FilePreviewCommon/GcodeHelper.cs
@@ -8,7 +8,7 @@
using System.Linq;
using System.Text;
-namespace Common.Utilities
+namespace Microsoft.PowerToys.FilePreviewCommon
{
///
/// Gcode file helper class.
diff --git a/src/modules/previewpane/common/Utilities/GcodeThumbnail.cs b/src/common/FilePreviewCommon/GcodeThumbnail.cs
similarity index 97%
rename from src/modules/previewpane/common/Utilities/GcodeThumbnail.cs
rename to src/common/FilePreviewCommon/GcodeThumbnail.cs
index 7715c3f53f90..545d7aa04ab9 100644
--- a/src/modules/previewpane/common/Utilities/GcodeThumbnail.cs
+++ b/src/common/FilePreviewCommon/GcodeThumbnail.cs
@@ -5,9 +5,8 @@
using System;
using System.Drawing;
using System.IO;
-using PreviewHandlerCommon.Utilities;
-namespace Common.Utilities
+namespace Microsoft.PowerToys.FilePreviewCommon
{
///
/// Represents a gcode thumbnail.
diff --git a/src/modules/previewpane/common/Utilities/GcodeThumbnailFormat.cs b/src/common/FilePreviewCommon/GcodeThumbnailFormat.cs
similarity index 93%
rename from src/modules/previewpane/common/Utilities/GcodeThumbnailFormat.cs
rename to src/common/FilePreviewCommon/GcodeThumbnailFormat.cs
index bb4d84e0bce5..1e471ed8c50d 100644
--- a/src/modules/previewpane/common/Utilities/GcodeThumbnailFormat.cs
+++ b/src/common/FilePreviewCommon/GcodeThumbnailFormat.cs
@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-namespace Common.Utilities
+namespace Microsoft.PowerToys.FilePreviewCommon
{
///
/// The gcode thumbnail image format.
diff --git a/src/common/FilePreviewCommon/MarkdownHelper.cs b/src/common/FilePreviewCommon/MarkdownHelper.cs
index 31013419ed6f..bb8a98440ebe 100644
--- a/src/common/FilePreviewCommon/MarkdownHelper.cs
+++ b/src/common/FilePreviewCommon/MarkdownHelper.cs
@@ -30,7 +30,7 @@ public static string MarkdownHtml(string fileContent, string theme, string fileP
// Extension to modify markdown AST.
HTMLParsingExtension extension = new HTMLParsingExtension(imagesBlockedCallBack);
- extension.FilePath = Path.GetDirectoryName(filePath);
+ extension.FilePath = Path.GetDirectoryName(filePath) ?? string.Empty;
// if you have a string with double space, some people view it as a new line.
// while this is against spec, even GH supports this. Technically looks like GH just trims whitespace
diff --git a/src/common/FilePreviewCommon/MonacoHelper.cs b/src/common/FilePreviewCommon/MonacoHelper.cs
index eac3826426fc..54b0ac93af5b 100644
--- a/src/common/FilePreviewCommon/MonacoHelper.cs
+++ b/src/common/FilePreviewCommon/MonacoHelper.cs
@@ -28,12 +28,12 @@ public static class MonacoHelper
new XmlFormatter(),
}.AsReadOnly();
- private static string _monacoDirectory;
+ private static string? _monacoDirectory;
public static string GetRuntimeMonacoDirectory()
{
string codeBase = Assembly.GetExecutingAssembly().Location;
- string path = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(codeBase), "Assets", "Monaco"));
+ string path = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(codeBase) ?? string.Empty, "Assets", "Monaco"));
if (Path.Exists(path))
{
return path;
@@ -41,7 +41,7 @@ public static string GetRuntimeMonacoDirectory()
else
{
// We're likely in WinUI3Apps directory and need to go back to the base directory.
- return Path.GetFullPath(Path.Combine(Path.GetDirectoryName(codeBase), "..", "Assets", "Monaco"));
+ return Path.GetFullPath(Path.Combine(Path.GetDirectoryName(codeBase) ?? string.Empty, "..", "Assets", "Monaco"));
}
}
diff --git a/src/common/FilePreviewCommon/QoiImage.cs b/src/common/FilePreviewCommon/QoiImage.cs
new file mode 100644
index 000000000000..ac315eb8323e
--- /dev/null
+++ b/src/common/FilePreviewCommon/QoiImage.cs
@@ -0,0 +1,177 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Buffers.Binary;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Text;
+
+//// Based on https://github.com/phoboslab/qoi/blob/master/qoi.h
+
+namespace Microsoft.PowerToys.FilePreviewCommon
+{
+ ///
+ /// QOI Image helper.
+ ///
+ public static class QoiImage
+ {
+#pragma warning disable SA1310 // Field names should not contain underscore
+ private const byte QOI_OP_INDEX = 0x00; // 00xxxxxx
+ private const byte QOI_OP_DIFF = 0x40; // 01xxxxxx
+ private const byte QOI_OP_LUMA = 0x80; // 10xxxxxx
+ private const byte QOI_OP_RUN = 0xc0; // 11xxxxxx
+ private const byte QOI_OP_RGB = 0xfe; // 11111110
+ private const byte QOI_OP_RGBA = 0xff; // 11111111
+
+ private const byte QOI_MASK_2 = 0xc0; // 11000000
+
+ private const int QOI_MAGIC = 'q' << 24 | 'o' << 16 | 'i' << 8 | 'f';
+ private const int QOI_HEADER_SIZE = 14;
+
+ private const uint QOI_PIXELS_MAX = 400000000;
+
+ private const byte QOI_PADDING_LENGTH = 8;
+#pragma warning restore SA1310 // Field names should not contain underscore
+
+ private record struct QoiPixel(byte R, byte G, byte B, byte A)
+ {
+ public readonly int GetColorHash() => (R * 3) + (G * 5) + (B * 7) + (A * 11);
+ }
+
+ ///
+ /// Creates a from the specified QOI data stream.
+ ///
+ /// A that contains the QOI data.
+ /// The this method creates.
+ /// The stream does not have a valid QOI image format.
+ public static Bitmap FromStream(Stream stream)
+ {
+ var fileSize = stream.Length;
+
+ if (fileSize < QOI_HEADER_SIZE + QOI_PADDING_LENGTH)
+ {
+ throw new ArgumentException("Not enough data for a QOI file");
+ }
+
+ Bitmap? bitmap = null;
+
+ try
+ {
+ using var reader = new BinaryReader(stream, Encoding.UTF8, true);
+
+ var headerMagic = ReadUInt32BigEndian(reader);
+
+ if (headerMagic != QOI_MAGIC)
+ {
+ throw new ArgumentException("Invalid QOI file header");
+ }
+
+ var width = ReadUInt32BigEndian(reader);
+ var height = ReadUInt32BigEndian(reader);
+ var channels = reader.ReadByte();
+ var colorSpace = reader.ReadByte();
+
+ if (width == 0 || height == 0 || channels < 3 || channels > 4 || colorSpace > 1 || height >= QOI_PIXELS_MAX / width)
+ {
+ throw new ArgumentException("Invalid QOI file data");
+ }
+
+ var pixelFormat = channels == 4 ? PixelFormat.Format32bppArgb : PixelFormat.Format24bppRgb;
+
+ bitmap = new Bitmap((int)width, (int)height, pixelFormat);
+
+ var bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.WriteOnly, pixelFormat);
+ var dataLength = bitmapData.Height * bitmapData.Stride;
+
+ var index = new QoiPixel[64];
+ var pixel = new QoiPixel(0, 0, 0, 255);
+
+ var run = 0;
+ var chunksLen = fileSize - QOI_PADDING_LENGTH;
+
+ for (var dataIndex = 0; dataIndex < dataLength; dataIndex += channels)
+ {
+ if (run > 0)
+ {
+ run--;
+ }
+ else if (stream.Position < chunksLen)
+ {
+ var b1 = reader.ReadByte();
+
+ if (b1 == QOI_OP_RGB)
+ {
+ pixel.R = reader.ReadByte();
+ pixel.G = reader.ReadByte();
+ pixel.B = reader.ReadByte();
+ }
+ else if (b1 == QOI_OP_RGBA)
+ {
+ pixel.R = reader.ReadByte();
+ pixel.G = reader.ReadByte();
+ pixel.B = reader.ReadByte();
+ pixel.A = reader.ReadByte();
+ }
+ else if ((b1 & QOI_MASK_2) == QOI_OP_INDEX)
+ {
+ pixel = index[b1];
+ }
+ else if ((b1 & QOI_MASK_2) == QOI_OP_DIFF)
+ {
+ pixel.R += (byte)(((b1 >> 4) & 0x03) - 2);
+ pixel.G += (byte)(((b1 >> 2) & 0x03) - 2);
+ pixel.B += (byte)((b1 & 0x03) - 2);
+ }
+ else if ((b1 & QOI_MASK_2) == QOI_OP_LUMA)
+ {
+ var b2 = reader.ReadByte();
+ var vg = (b1 & 0x3f) - 32;
+ pixel.R += (byte)(vg - 8 + ((b2 >> 4) & 0x0f));
+ pixel.G += (byte)vg;
+ pixel.B += (byte)(vg - 8 + (b2 & 0x0f));
+ }
+ else if ((b1 & QOI_MASK_2) == QOI_OP_RUN)
+ {
+ run = b1 & 0x3f;
+ }
+
+ index[pixel.GetColorHash() % 64] = pixel;
+ }
+
+ unsafe
+ {
+ var bitmapPixel = (byte*)bitmapData.Scan0 + dataIndex;
+
+ bitmapPixel[0] = pixel.B;
+ bitmapPixel[1] = pixel.G;
+ bitmapPixel[2] = pixel.R;
+ if (channels == 4)
+ {
+ bitmapPixel[3] = pixel.A;
+ }
+ }
+ }
+
+ bitmap.UnlockBits(bitmapData);
+
+ return bitmap;
+ }
+ catch
+ {
+ bitmap?.Dispose();
+
+ throw;
+ }
+ }
+
+ private static uint ReadUInt32BigEndian(BinaryReader reader)
+ {
+ var buffer = reader.ReadBytes(4);
+
+ return BinaryPrimitives.ReadUInt32BigEndian(buffer);
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.Common/Extensions/IFileSystemItemExtensions.cs b/src/modules/peek/Peek.Common/Extensions/IFileSystemItemExtensions.cs
index 0d976fd28814..5717e97aa867 100644
--- a/src/modules/peek/Peek.Common/Extensions/IFileSystemItemExtensions.cs
+++ b/src/modules/peek/Peek.Common/Extensions/IFileSystemItemExtensions.cs
@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using System;
+using System.Buffers.Binary;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
@@ -77,6 +77,31 @@ public static class IFileSystemItemExtensions
return size;
}
+ public static Size? GetQoiSize(this IFileSystemItem item)
+ {
+ Size? size = null;
+ using (FileStream stream = new FileStream(item.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
+ {
+ if (stream.Length >= 12)
+ {
+ stream.Position = 4;
+
+ using (var reader = new BinaryReader(stream))
+ {
+ uint widthValue = BinaryPrimitives.ReadUInt32BigEndian(reader.ReadBytes(4));
+ uint heightValue = BinaryPrimitives.ReadUInt32BigEndian(reader.ReadBytes(4));
+
+ if (widthValue > 0 && heightValue > 0)
+ {
+ size = new Size(widthValue, heightValue);
+ }
+ }
+ }
+ }
+
+ return size;
+ }
+
public static ulong GetSizeInBytes(this IFileSystemItem item)
{
ulong sizeInBytes = 0;
diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/Helpers/BitmapHelper.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Helpers/BitmapHelper.cs
index 30cb971ee69c..a8699a443443 100644
--- a/src/modules/peek/Peek.FilePreviewer/Previewers/Helpers/BitmapHelper.cs
+++ b/src/modules/peek/Peek.FilePreviewer/Previewers/Helpers/BitmapHelper.cs
@@ -18,7 +18,6 @@ public static class BitmapHelper
public static async Task GetBitmapFromHBitmapAsync(IntPtr hbitmap, bool isSupportingTransparency, CancellationToken cancellationToken)
{
Bitmap? bitmap = null;
- Bitmap? tempBitmapForDeletion = null;
try
{
@@ -26,15 +25,29 @@ public static async Task GetBitmapFromHBitmapAsync(IntPtr hbitmap,
cancellationToken.ThrowIfCancellationRequested();
+ return await BitmapToImageSource(bitmap, isSupportingTransparency, cancellationToken);
+ }
+ finally
+ {
+ bitmap?.Dispose();
+
+ // delete HBitmap to avoid memory leaks
+ NativeMethods.DeleteObject(hbitmap);
+ }
+ }
+
+ public static async Task BitmapToImageSource(Bitmap bitmap, bool isSupportingTransparency, CancellationToken cancellationToken)
+ {
+ Bitmap? transparentBitmap = null;
+
+ try
+ {
if (isSupportingTransparency && bitmap.PixelFormat == PixelFormat.Format32bppRgb)
{
var bitmapRectangle = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
var bitmapData = bitmap.LockBits(bitmapRectangle, ImageLockMode.ReadOnly, bitmap.PixelFormat);
- var transparentBitmap = new Bitmap(bitmapData.Width, bitmapData.Height, bitmapData.Stride, PixelFormat.Format32bppArgb, bitmapData.Scan0);
-
- // Can't dispose of original bitmap yet as that causes crashes on png files. Saving it for later disposal after saving to stream.
- tempBitmapForDeletion = bitmap;
+ transparentBitmap = new Bitmap(bitmapData.Width, bitmapData.Height, bitmapData.Stride, PixelFormat.Format32bppArgb, bitmapData.Scan0);
bitmap = transparentBitmap;
}
@@ -54,11 +67,7 @@ public static async Task GetBitmapFromHBitmapAsync(IntPtr hbitmap,
}
finally
{
- bitmap?.Dispose();
- tempBitmapForDeletion?.Dispose();
-
- // delete HBitmap to avoid memory leaks
- NativeMethods.DeleteObject(hbitmap);
+ transparentBitmap?.Dispose();
}
}
diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/ImagePreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/ImagePreviewer.cs
index d01a60e56e34..ab42b5724cdb 100644
--- a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/ImagePreviewer.cs
+++ b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/ImagePreviewer.cs
@@ -9,6 +9,7 @@
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using ManagedCommon;
+using Microsoft.PowerToys.FilePreviewCommon;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
@@ -91,6 +92,14 @@ public async Task GetPreviewSizeAsync(CancellationToken cancellatio
ImageSize = size.Value;
}
}
+ else if (IsQoi(Item))
+ {
+ var size = await Task.Run(Item.GetQoiSize);
+ if (size != null)
+ {
+ ImageSize = size.Value;
+ }
+ }
else
{
ImageSize = await Task.Run(Item.GetImageSize);
@@ -257,6 +266,12 @@ await Dispatcher.RunOnUiThread(async () =>
Preview = source;
}
+ else if (IsQoi(Item))
+ {
+ using var bitmap = QoiImage.FromStream(stream);
+
+ Preview = await BitmapHelper.BitmapToImageSource(bitmap, true, cancellationToken);
+ }
else
{
var bitmap = new BitmapImage();
@@ -286,6 +301,11 @@ private bool IsSvg(IFileSystemItem item)
return item.Extension == ".svg";
}
+ private bool IsQoi(IFileSystemItem item)
+ {
+ return item.Extension == ".qoi";
+ }
+
private void Clear()
{
lowQualityThumbnailPreview = null;
@@ -367,6 +387,8 @@ private void Clear()
".cr3",
".svg",
+
+ ".qoi",
};
}
}
diff --git a/src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandler.csproj b/src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandler.csproj
index 72afb9e10da4..1f58af39db1f 100644
--- a/src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandler.csproj
+++ b/src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandler.csproj
@@ -61,6 +61,7 @@
+
diff --git a/src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandlerControl.cs b/src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandlerControl.cs
index 63d9828d3848..67c6d9f42e72 100644
--- a/src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandlerControl.cs
+++ b/src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandlerControl.cs
@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using Common;
-using Common.Utilities;
+using Microsoft.PowerToys.FilePreviewCommon;
using Microsoft.PowerToys.PreviewHandler.Gcode.Telemetry.Events;
using Microsoft.PowerToys.Telemetry;
diff --git a/src/modules/previewpane/GcodeThumbnailProvider/GcodeThumbnailProvider.cs b/src/modules/previewpane/GcodeThumbnailProvider/GcodeThumbnailProvider.cs
index 4afe553f3da2..d9b9026b8fa8 100644
--- a/src/modules/previewpane/GcodeThumbnailProvider/GcodeThumbnailProvider.cs
+++ b/src/modules/previewpane/GcodeThumbnailProvider/GcodeThumbnailProvider.cs
@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
-using Common.Utilities;
+using Microsoft.PowerToys.FilePreviewCommon;
namespace Microsoft.PowerToys.ThumbnailHandler.Gcode
{
diff --git a/src/modules/previewpane/GcodeThumbnailProvider/GcodeThumbnailProvider.csproj b/src/modules/previewpane/GcodeThumbnailProvider/GcodeThumbnailProvider.csproj
index 24ac171b7a6d..034e87d70e95 100644
--- a/src/modules/previewpane/GcodeThumbnailProvider/GcodeThumbnailProvider.csproj
+++ b/src/modules/previewpane/GcodeThumbnailProvider/GcodeThumbnailProvider.csproj
@@ -46,6 +46,7 @@
+
diff --git a/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandler.csproj b/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandler.csproj
index 462ba6cd7a4c..66936c38d3ca 100644
--- a/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandler.csproj
+++ b/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandler.csproj
@@ -61,6 +61,7 @@
+
diff --git a/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandlerControl.cs b/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandlerControl.cs
index 95d606ce82bc..047a3e614fa9 100644
--- a/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandlerControl.cs
+++ b/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandlerControl.cs
@@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.
using Common;
+using Microsoft.PowerToys.FilePreviewCommon;
using Microsoft.PowerToys.PreviewHandler.Qoi.Telemetry.Events;
using Microsoft.PowerToys.Telemetry;
-using PreviewHandlerCommon.Utilities;
namespace Microsoft.PowerToys.PreviewHandler.Qoi
{
@@ -63,7 +63,7 @@ public override void DoPreview(T dataSource)
throw new ArgumentException($"{nameof(dataSource)} for {nameof(QoiPreviewHandlerControl)} must be a string but was a '{typeof(T)}'");
}
- FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
+ using FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
thumbnail = QoiImage.FromStream(fs);
diff --git a/src/modules/previewpane/QoiThumbnailProvider/QoiThumbnailProvider.cs b/src/modules/previewpane/QoiThumbnailProvider/QoiThumbnailProvider.cs
index b055a26d0bcf..7c0e7ead1c97 100644
--- a/src/modules/previewpane/QoiThumbnailProvider/QoiThumbnailProvider.cs
+++ b/src/modules/previewpane/QoiThumbnailProvider/QoiThumbnailProvider.cs
@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
-using PreviewHandlerCommon.Utilities;
+using Microsoft.PowerToys.FilePreviewCommon;
namespace Microsoft.PowerToys.ThumbnailHandler.Qoi
{
diff --git a/src/modules/previewpane/QoiThumbnailProvider/QoiThumbnailProvider.csproj b/src/modules/previewpane/QoiThumbnailProvider/QoiThumbnailProvider.csproj
index 9ae42bb515f7..6ab6ce01b8a0 100644
--- a/src/modules/previewpane/QoiThumbnailProvider/QoiThumbnailProvider.csproj
+++ b/src/modules/previewpane/QoiThumbnailProvider/QoiThumbnailProvider.csproj
@@ -46,6 +46,7 @@
+
diff --git a/src/modules/previewpane/common/PreviewHandlerCommon.csproj b/src/modules/previewpane/common/PreviewHandlerCommon.csproj
index 99a6b2ed43eb..bb1f8043d0dd 100644
--- a/src/modules/previewpane/common/PreviewHandlerCommon.csproj
+++ b/src/modules/previewpane/common/PreviewHandlerCommon.csproj
@@ -10,7 +10,6 @@
false
true
enable
- true
diff --git a/src/modules/previewpane/common/Utilities/QoiImage.cs b/src/modules/previewpane/common/Utilities/QoiImage.cs
deleted file mode 100644
index 20a2ca14765a..000000000000
--- a/src/modules/previewpane/common/Utilities/QoiImage.cs
+++ /dev/null
@@ -1,177 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// The Microsoft Corporation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System;
-using System.Buffers.Binary;
-using System.Drawing;
-using System.Drawing.Imaging;
-using System.IO;
-
-//// Based on https://github.com/phoboslab/qoi/blob/master/qoi.h
-
-namespace PreviewHandlerCommon.Utilities
-{
- ///
- /// QOI Image helper.
- ///
- public static class QoiImage
- {
-#pragma warning disable SA1310 // Field names should not contain underscore
- private const byte QOI_OP_INDEX = 0x00; // 00xxxxxx
- private const byte QOI_OP_DIFF = 0x40; // 01xxxxxx
- private const byte QOI_OP_LUMA = 0x80; // 10xxxxxx
- private const byte QOI_OP_RUN = 0xc0; // 11xxxxxx
- private const byte QOI_OP_RGB = 0xfe; // 11111110
- private const byte QOI_OP_RGBA = 0xff; // 11111111
-
- private const byte QOI_MASK_2 = 0xc0; // 11000000
-
- private const int QOI_MAGIC = 'q' << 24 | 'o' << 16 | 'i' << 8 | 'f';
- private const int QOI_HEADER_SIZE = 14;
-
- private const uint QOI_PIXELS_MAX = 400000000;
-
- private const byte QOI_PADDING_LENGTH = 8;
-#pragma warning restore SA1310 // Field names should not contain underscore
-
- private record struct QoiPixel(byte R, byte G, byte B, byte A)
- {
- public readonly int GetColorHash() => (R * 3) + (G * 5) + (B * 7) + (A * 11);
- }
-
- ///
- /// Creates a from the specified QOI data stream.
- ///
- /// A that contains the QOI data.
- /// The this method creates.
- /// The stream does not have a valid QOI image format.
- public static Bitmap FromStream(Stream stream)
- {
- var fileSize = stream.Length;
-
- if (fileSize < QOI_HEADER_SIZE + QOI_PADDING_LENGTH)
- {
- throw new ArgumentException("Not enough data for a QOI file");
- }
-
- using var reader = new BinaryReader(stream);
-
- var headerMagic = ReadUInt32BigEndian(reader);
-
- if (headerMagic != QOI_MAGIC)
- {
- throw new ArgumentException("Invalid QOI file header");
- }
-
- var width = ReadUInt32BigEndian(reader);
- var height = ReadUInt32BigEndian(reader);
- var channels = reader.ReadByte();
- var colorSpace = reader.ReadByte();
-
- if (width == 0 || height == 0 || channels < 3 || channels > 4 || colorSpace > 1 || height >= QOI_PIXELS_MAX / width)
- {
- throw new ArgumentException("Invalid QOI file data");
- }
-
- var pixelsCount = width * height;
- var pixels = new QoiPixel[pixelsCount];
- var index = new QoiPixel[64];
-
- var pixel = new QoiPixel(0, 0, 0, 255);
-
- var run = 0;
- var chunksLen = fileSize - QOI_PADDING_LENGTH;
-
- for (var pixelIndex = 0; pixelIndex < pixelsCount; pixelIndex++)
- {
- if (run > 0)
- {
- run--;
- }
- else if (stream.Position < chunksLen)
- {
- var b1 = reader.ReadByte();
-
- if (b1 == QOI_OP_RGB)
- {
- pixel.R = reader.ReadByte();
- pixel.G = reader.ReadByte();
- pixel.B = reader.ReadByte();
- }
- else if (b1 == QOI_OP_RGBA)
- {
- pixel.R = reader.ReadByte();
- pixel.G = reader.ReadByte();
- pixel.B = reader.ReadByte();
- pixel.A = reader.ReadByte();
- }
- else if ((b1 & QOI_MASK_2) == QOI_OP_INDEX)
- {
- pixel = index[b1];
- }
- else if ((b1 & QOI_MASK_2) == QOI_OP_DIFF)
- {
- pixel.R += (byte)(((b1 >> 4) & 0x03) - 2);
- pixel.G += (byte)(((b1 >> 2) & 0x03) - 2);
- pixel.B += (byte)((b1 & 0x03) - 2);
- }
- else if ((b1 & QOI_MASK_2) == QOI_OP_LUMA)
- {
- var b2 = reader.ReadByte();
- var vg = (b1 & 0x3f) - 32;
- pixel.R += (byte)(vg - 8 + ((b2 >> 4) & 0x0f));
- pixel.G += (byte)vg;
- pixel.B += (byte)(vg - 8 + (b2 & 0x0f));
- }
- else if ((b1 & QOI_MASK_2) == QOI_OP_RUN)
- {
- run = b1 & 0x3f;
- }
-
- index[pixel.GetColorHash() % 64] = pixel;
- }
-
- pixels[pixelIndex] = pixel;
- }
-
- return ConvertToBitmap(width, height, channels, pixels);
- }
-
- private static Bitmap ConvertToBitmap(uint width, uint height, byte channels, QoiPixel[] pixels)
- {
- var pixelFormat = channels == 4 ? PixelFormat.Format32bppArgb : PixelFormat.Format24bppRgb;
- var bitmap = new Bitmap((int)width, (int)height, pixelFormat);
-
- var bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.WriteOnly, pixelFormat);
-
- unsafe
- {
- for (var pixelIndex = 0; pixelIndex < pixels.Length; pixelIndex++)
- {
- var pixel = pixels[pixelIndex];
- var bitmapPixel = (byte*)bitmapData.Scan0 + (pixelIndex * channels);
-
- bitmapPixel[0] = pixel.B;
- bitmapPixel[1] = pixel.G;
- bitmapPixel[2] = pixel.R;
- if (channels == 4)
- {
- bitmapPixel[3] = pixel.A;
- }
- }
- }
-
- bitmap.UnlockBits(bitmapData);
-
- return bitmap;
- }
-
- private static uint ReadUInt32BigEndian(BinaryReader reader)
- {
- var buffer = reader.ReadBytes(4);
-
- return BinaryPrimitives.ReadUInt32BigEndian(buffer);
- }
- }
-}