mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-05 10:46:33 +02:00
[FileExplorerAddOns]Improves handling of Gcode Thumbnails (#27947)
* Improves handling of Gcode Thumbnails * Remove Peek support * Moves GcodeHelper to PreviewHandlerCommon * Reverts minor change * Skip unknown data on GcodeHelper.GetBestThumbnail * Replaces QOI.Core with QoiImage * Fixes spellchecker * Reverts changes to NOTICE.md * Minor QoiImage improvements * Use custom QoiPixel struct * Add MIT notice for the QOI reference code * Fix spellcheck for the MIT notice * Update NOTICE.md tweaked notice a bit
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<AssemblyTitle>PowerToys.PreviewHandlerCommon</AssemblyTitle>
|
||||
@@ -9,6 +9,8 @@
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
81
src/modules/previewpane/common/Utilities/GcodeHelper.cs
Normal file
81
src/modules/previewpane/common/Utilities/GcodeHelper.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Common.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Gcode file helper class.
|
||||
/// </summary>
|
||||
public static class GcodeHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets any thumbnails found in a gcode file.
|
||||
/// </summary>
|
||||
/// <param name="reader">The <see cref="TextReader"/> instance to the gcode file.</param>
|
||||
/// <returns>The thumbnails found in a gcode file.</returns>
|
||||
public static IEnumerable<GcodeThumbnail> GetThumbnails(TextReader reader)
|
||||
{
|
||||
string? line;
|
||||
var format = GcodeThumbnailFormat.Unknown;
|
||||
StringBuilder? capturedText = null;
|
||||
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
if (line.StartsWith("; thumbnail", StringComparison.InvariantCulture))
|
||||
{
|
||||
var parts = line[11..].Split(" ");
|
||||
|
||||
switch (parts[1])
|
||||
{
|
||||
case "begin":
|
||||
format = parts[0].ToUpperInvariant() switch
|
||||
{
|
||||
"" => GcodeThumbnailFormat.PNG,
|
||||
"_JPG" => GcodeThumbnailFormat.JPG,
|
||||
"_QOI" => GcodeThumbnailFormat.QOI,
|
||||
_ => GcodeThumbnailFormat.Unknown,
|
||||
};
|
||||
capturedText = new StringBuilder();
|
||||
|
||||
break;
|
||||
|
||||
case "end":
|
||||
if (capturedText != null)
|
||||
{
|
||||
yield return new GcodeThumbnail(format, capturedText.ToString());
|
||||
|
||||
capturedText = null;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
capturedText?.Append(line[2..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the best thumbnail available in a gcode file.
|
||||
/// </summary>
|
||||
/// <param name="reader">The <see cref="TextReader"/> instance to the gcode file.</param>
|
||||
/// <returns>The best thumbnail available in the gcode file.</returns>
|
||||
public static GcodeThumbnail? GetBestThumbnail(TextReader reader)
|
||||
{
|
||||
return GetThumbnails(reader)
|
||||
.Where(x => x.Format != GcodeThumbnailFormat.Unknown)
|
||||
.OrderByDescending(x => (int)x.Format)
|
||||
.ThenByDescending(x => x.Data.Length)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/modules/previewpane/common/Utilities/GcodeThumbnail.cs
Normal file
72
src/modules/previewpane/common/Utilities/GcodeThumbnail.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
// 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.Drawing;
|
||||
using System.IO;
|
||||
using PreviewHandlerCommon.Utilities;
|
||||
|
||||
namespace Common.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a gcode thumbnail.
|
||||
/// </summary>
|
||||
public class GcodeThumbnail
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the gcode thumbnail image format.
|
||||
/// </summary>
|
||||
public GcodeThumbnailFormat Format { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the gcode thumbnail image data in base64.
|
||||
/// </summary>
|
||||
public string Data { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GcodeThumbnail"/> class.
|
||||
/// </summary>
|
||||
/// <param name="format">The gcode thumbnail image format.</param>
|
||||
/// <param name="data">The gcode thumbnail image data in base64.</param>
|
||||
public GcodeThumbnail(GcodeThumbnailFormat format, string data)
|
||||
{
|
||||
Format = format;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="Bitmap"/> representing this thumbnail.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Bitmap"/> representing this thumbnail.</returns>
|
||||
public Bitmap? GetBitmap()
|
||||
{
|
||||
switch (Format)
|
||||
{
|
||||
case GcodeThumbnailFormat.JPG:
|
||||
case GcodeThumbnailFormat.PNG:
|
||||
return BitmapFromBase64String();
|
||||
|
||||
case GcodeThumbnailFormat.QOI:
|
||||
return BitmapFromQoiBase64String();
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap BitmapFromBase64String()
|
||||
{
|
||||
var bitmapBytes = Convert.FromBase64String(Data);
|
||||
|
||||
return new Bitmap(new MemoryStream(bitmapBytes));
|
||||
}
|
||||
|
||||
private Bitmap BitmapFromQoiBase64String()
|
||||
{
|
||||
var bitmapBytes = Convert.FromBase64String(Data);
|
||||
|
||||
return QoiImage.FromStream(new MemoryStream(bitmapBytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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.
|
||||
|
||||
namespace Common.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// The gcode thumbnail image format.
|
||||
/// </summary>
|
||||
public enum GcodeThumbnailFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// Unknown image format.
|
||||
/// </summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// JPG image format.
|
||||
/// </summary>
|
||||
JPG,
|
||||
|
||||
/// <summary>
|
||||
/// QOI image format.
|
||||
/// </summary>
|
||||
QOI,
|
||||
|
||||
/// <summary>
|
||||
/// PNG image format.
|
||||
/// </summary>
|
||||
PNG,
|
||||
}
|
||||
}
|
||||
177
src/modules/previewpane/common/Utilities/QoiImage.cs
Normal file
177
src/modules/previewpane/common/Utilities/QoiImage.cs
Normal file
@@ -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;
|
||||
|
||||
//// Based on https://github.com/phoboslab/qoi/blob/master/qoi.h
|
||||
|
||||
namespace PreviewHandlerCommon.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// QOI Image helper.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Bitmap"/> from the specified QOI data stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">A <see cref="Stream"/> that contains the QOI data.</param>
|
||||
/// <returns>The <see cref="Bitmap"/> this method creates.</returns>
|
||||
/// <exception cref="ArgumentException">The stream does not have a valid QOI image format.</exception>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.ComTypes;
|
||||
@@ -17,7 +18,7 @@ namespace Common.Utilities
|
||||
/// </remarks>
|
||||
public class ReadonlyStream : Stream
|
||||
{
|
||||
private IStream _stream;
|
||||
private IStream? _stream;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ReadonlyStream"/> class.
|
||||
@@ -238,6 +239,7 @@ namespace Common.Utilities
|
||||
}
|
||||
}
|
||||
|
||||
[MemberNotNull(nameof(_stream))]
|
||||
private void CheckDisposed()
|
||||
{
|
||||
if (_stream == null)
|
||||
|
||||
@@ -20,12 +20,12 @@ namespace Common
|
||||
/// <summary>
|
||||
/// WebView2 Control to display Svg.
|
||||
/// </summary>
|
||||
private WebView2 _browser;
|
||||
private WebView2? _browser;
|
||||
|
||||
/// <summary>
|
||||
/// WebView2 Environment
|
||||
/// </summary>
|
||||
private CoreWebView2Environment _webView2Environment;
|
||||
private CoreWebView2Environment? _webView2Environment;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the virtual host
|
||||
@@ -38,7 +38,7 @@ namespace Common
|
||||
/// <remarks>
|
||||
/// Source: https://stackoverflow.com/a/283917/14774889
|
||||
/// </remarks>
|
||||
public static string AssemblyDirectory
|
||||
public static string? AssemblyDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -60,7 +60,7 @@ namespace Common
|
||||
_browser = new WebView2();
|
||||
_browser.Dock = DockStyle.Fill;
|
||||
_browser.Visible = true;
|
||||
_browser.NavigationCompleted += (object sender, CoreWebView2NavigationCompletedEventArgs args) =>
|
||||
_browser.NavigationCompleted += (object? sender, CoreWebView2NavigationCompletedEventArgs args) =>
|
||||
{
|
||||
// Put here logic needed after WebView2 control is done navigating to url/page
|
||||
};
|
||||
@@ -83,7 +83,7 @@ namespace Common
|
||||
_browser.NavigateToString("Test");
|
||||
|
||||
// Or navigate to Uri
|
||||
_browser.Source = new Uri(filePath);
|
||||
_browser.Source = new Uri(filePath!);
|
||||
}
|
||||
catch (NullReferenceException)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user