diff --git a/QuickLook.Plugin/QuickLook.Plugin.Hdf5Viewer/Hdf5SummaryBuilder.cs b/QuickLook.Plugin/QuickLook.Plugin.Hdf5Viewer/Hdf5SummaryBuilder.cs new file mode 100644 index 000000000..ebe840b85 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.Hdf5Viewer/Hdf5SummaryBuilder.cs @@ -0,0 +1,160 @@ +using PureHDF; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace QuickLook.Plugin.Hdf5Viewer; + +internal static class Hdf5SummaryBuilder +{ + private const int MaxDepth = 16; + private const int MaxChildrenPerGroup = 250; + private const int MaxAttributesPerObject = 20; + + public static string Build(string path) + { + var sb = new StringBuilder(32 * 1024); + var file = H5File.OpenRead(path); + sb.AppendLine("HDF5 structure summary"); + sb.AppendLine(); + AppendObject(file, sb, depth: 0); + + return sb.ToString(); + } + + private static void AppendObject(object h5Object, StringBuilder sb, int depth) + { + if (depth > MaxDepth) + { + sb.AppendLine($"{Indent(depth)}... depth limit reached"); + return; + } + + switch (h5Object) + { + case IH5Group group: + AppendGroup(group, sb, depth); + return; + case IH5Dataset dataset: + AppendDataset(dataset, sb, depth); + return; + case IH5CommitedDatatype committedDatatype: + sb.AppendLine($"{Indent(depth)}[DATATYPE] {SafeName(committedDatatype.Name)}"); + return; + case IH5UnresolvedLink unresolvedLink: + sb.AppendLine($"{Indent(depth)}[UNRESOLVED] {SafeName(unresolvedLink.Name)}"); + return; + default: + sb.AppendLine($"{Indent(depth)}[{h5Object.GetType().Name}] {SafeName(TryGetName(h5Object))}"); + return; + } + } + + private static void AppendGroup(IH5Group group, StringBuilder sb, int depth) + { + sb.AppendLine($"{Indent(depth)}[GROUP] {SafeName(group.Name)}"); + AppendAttributes(group, sb, depth + 1); + + var children = SafeReadChildren(group).ToList(); + var visibleChildren = children.Take(MaxChildrenPerGroup).ToList(); + + foreach (var child in visibleChildren) + AppendObject(child, sb, depth + 1); + + if (children.Count > MaxChildrenPerGroup) + sb.AppendLine($"{Indent(depth + 1)}... {children.Count - MaxChildrenPerGroup} more children"); + } + + private static void AppendDataset(IH5Dataset dataset, StringBuilder sb, int depth) + { + var dimensions = string.Join(" x ", dataset.Space.Dimensions.Select(d => d.ToString())); + var shape = string.IsNullOrWhiteSpace(dimensions) ? "scalar" : dimensions; + + sb.AppendLine( + $"{Indent(depth)}[DATASET] {SafeName(dataset.Name)} | " + + $"shape={shape}, dtype={dataset.Type.Class}, itemSize={dataset.Type.Size}B, layout={dataset.Layout.Class}"); + + AppendAttributes(dataset, sb, depth + 1); + } + + private static void AppendAttributes(IH5Object attributable, StringBuilder sb, int depth) + { + var visible = new List(MaxAttributesPerObject); + var hasMoreAttributes = false; + + try + { + var count = 0; + + foreach (var attribute in attributable.Attributes()) + { + count++; + + if (count <= MaxAttributesPerObject) + { + visible.Add(attribute); + } + else + { + hasMoreAttributes = true; + break; + } + } + } + catch (Exception ex) + { + sb.AppendLine($"{Indent(depth)}@attributes: "); + return; + } + + if (visible.Count == 0) + return; + + foreach (var attribute in visible) + { + var dimensions = string.Join(" x ", attribute.Space.Dimensions.Select(d => d.ToString())); + var shape = string.IsNullOrWhiteSpace(dimensions) ? "scalar" : dimensions; + + sb.AppendLine( + $"{Indent(depth)}@{SafeName(attribute.Name)} " + + $"(type={attribute.Type.Class}, shape={shape}, itemSize={attribute.Type.Size}B)"); + } + + if (hasMoreAttributes) + sb.AppendLine($"{Indent(depth)}... more attributes"); + } + + private static IEnumerable SafeReadChildren(IH5Group group) + { + try + { + return group.Children(); + } + catch + { + return Array.Empty(); + } + } + + private static string SafeName(string name) + { + return string.IsNullOrEmpty(name) ? "/" : name; + } + + private static string Indent(int level) + { + return new string(' ', level * 2); + } + + private static string TryGetName(object h5Object) + { + if (h5Object is IH5Object namedObject) + return namedObject.Name; + + if (h5Object is IH5UnresolvedLink unresolvedLink) + return unresolvedLink.Name; + + return string.Empty; + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.Hdf5Viewer/Hdf5TextPanel.cs b/QuickLook.Plugin/QuickLook.Plugin.Hdf5Viewer/Hdf5TextPanel.cs new file mode 100644 index 000000000..ad5564a30 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.Hdf5Viewer/Hdf5TextPanel.cs @@ -0,0 +1,32 @@ +using System.Windows.Controls; +using System.Windows.Media; + +namespace QuickLook.Plugin.Hdf5Viewer; + +public sealed class Hdf5TextPanel : UserControl +{ + private readonly TextBox _textBox; + + public Hdf5TextPanel() + { + _textBox = new TextBox + { + IsReadOnly = true, + TextWrapping = System.Windows.TextWrapping.NoWrap, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + HorizontalScrollBarVisibility = ScrollBarVisibility.Auto, + FontFamily = new FontFamily("Consolas"), + FontSize = 13, + BorderThickness = new System.Windows.Thickness(0), + Padding = new System.Windows.Thickness(12, 8, 12, 8) + }; + + Content = _textBox; + } + + public void SetText(string text) + { + _textBox.Text = text ?? string.Empty; + _textBox.CaretIndex = 0; + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.Hdf5Viewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.Hdf5Viewer/Plugin.cs new file mode 100644 index 000000000..70ac4e10b --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.Hdf5Viewer/Plugin.cs @@ -0,0 +1,109 @@ +using QuickLook.Common.Plugin; +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace QuickLook.Plugin.Hdf5Viewer; + +public sealed class Plugin : IViewer +{ + private static readonly byte[] Hdf5Signature = { 0x89, 0x48, 0x44, 0x46, 0x0D, 0x0A, 0x1A, 0x0A }; + private static readonly string[] SupportedExtensions = { ".h5", ".hdf5", ".hdf", ".he5" }; + private static readonly long[] SignatureProbeOffsets = { 0, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288 }; + private Hdf5TextPanel _panel; + + public int Priority => 0; + + public void Init() + { + } + + public bool CanHandle(string path) + { + if (Directory.Exists(path)) + return false; + + var extension = Path.GetExtension(path); + if (!SupportedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + return false; + + return HasHdf5Signature(path); + } + + public void Prepare(string path, ContextObject context) + { + context.PreferredSize = new Size { Width = 1100, Height = 760 }; + } + + public void View(string path, ContextObject context) + { + _panel = new Hdf5TextPanel(); + context.ViewerContent = _panel; + context.Title = Path.GetFileName(path); + context.IsBusy = true; + + var panel = _panel; + + Task.Run(() => + { + try + { + return Hdf5SummaryBuilder.Build(path); + } + catch (Exception ex) + { + return + $"Failed to open HDF5 file.{Environment.NewLine}{Environment.NewLine}" + + $"{ex.GetType().Name}: {ex.Message}"; + } + }).ContinueWith(t => + { + if (panel is not null) + panel.SetText(t.Result); + + context.IsBusy = false; + }, TaskScheduler.FromCurrentSynchronizationContext()); + } + + public void Cleanup() + { + _panel = null; + } + + private static bool HasHdf5Signature(string path) + { + try + { + using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + if (stream.Length < Hdf5Signature.Length) + return false; + + var header = new byte[Hdf5Signature.Length]; + + foreach (var offset in SignatureProbeOffsets) + { + if (offset + Hdf5Signature.Length > stream.Length) + break; + + stream.Position = offset; + + var read = stream.Read(header, 0, header.Length); + if (read != header.Length) + continue; + + if (header.SequenceEqual(Hdf5Signature)) + return true; + } + } + } + catch + { + return false; + } + + return false; + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.Hdf5Viewer/QuickLook.Plugin.Hdf5Viewer.csproj b/QuickLook.Plugin/QuickLook.Plugin.Hdf5Viewer/QuickLook.Plugin.Hdf5Viewer.csproj new file mode 100644 index 000000000..bf448516a --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.Hdf5Viewer/QuickLook.Plugin.Hdf5Viewer.csproj @@ -0,0 +1,75 @@ + + + + Library + net462 + QuickLook.Plugin.Hdf5Viewer + QuickLook.Plugin.Hdf5Viewer + 512 + false + true + latest + false + false + false + MinimumRecommendedRules.ruleset + {69D60E22-9190-4433-9A6E-1D889CF5CA52} + + + + true + full + false + ..\..\Build\Debug\QuickLook.Plugin\QuickLook.Plugin.Hdf5Viewer\ + DEBUG;TRACE + AnyCPU + prompt + + + + pdbonly + true + ..\..\Build\Release\QuickLook.Plugin\QuickLook.Plugin.Hdf5Viewer\ + TRACE + AnyCPU + prompt + + + + true + full + false + ..\..\Build\Debug\QuickLook.Plugin\QuickLook.Plugin.Hdf5Viewer\ + DEBUG;TRACE + x86 + prompt + + + + pdbonly + true + ..\..\Build\Release\QuickLook.Plugin\QuickLook.Plugin.Hdf5Viewer\ + TRACE + x86 + prompt + + + + + + + + + {85FDD6BA-871D-46C8-BD64-F6BB0CB5EA95} + QuickLook.Common + False + + + + + + Properties\GitVersion.cs + + + + diff --git a/QuickLook.sln b/QuickLook.sln index a3cb7eaea..dd9b2ccfb 100644 --- a/QuickLook.sln +++ b/QuickLook.sln @@ -58,6 +58,7 @@ Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "QuickLook.Installer", "Quic {B4F7C88D-C79D-49E7-A1FB-FB69CF72585F} = {B4F7C88D-C79D-49E7-A1FB-FB69CF72585F} {311E6E78-3A5B-4E51-802A-5755BD5F9F97} = {311E6E78-3A5B-4E51-802A-5755BD5F9F97} {B0054A16-472E-44AC-BA40-349303E524FF} = {B0054A16-472E-44AC-BA40-349303E524FF} + {69D60E22-9190-4433-9A6E-1D889CF5CA52} = {69D60E22-9190-4433-9A6E-1D889CF5CA52} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "QuickLook.Native64", "QuickLook.Native\QuickLook.Native64\QuickLook.Native64.vcxproj", "{794E4DCF-F715-4836-9D30-ABD296586D23}" @@ -92,6 +93,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickLook.Plugin.MediaInfoV EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickLook.Plugin.InsvBlocker", "QuickLook.Plugin\QuickLook.Plugin.InsvBlocker\QuickLook.Plugin.InsvBlocker.csproj", "{A1B2C3D4-E5F6-4A5B-9C8D-7E6F5A4B3C2D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickLook.Plugin.Hdf5Viewer", "QuickLook.Plugin\QuickLook.Plugin.Hdf5Viewer\QuickLook.Plugin.Hdf5Viewer.csproj", "{69D60E22-9190-4433-9A6E-1D889CF5CA52}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -300,6 +303,14 @@ Global {A1B2C3D4-E5F6-4A5B-9C8D-7E6F5A4B3C2D}.Release|Any CPU.Build.0 = Release|Any CPU {A1B2C3D4-E5F6-4A5B-9C8D-7E6F5A4B3C2D}.Release|x64.ActiveCfg = Release|Any CPU {A1B2C3D4-E5F6-4A5B-9C8D-7E6F5A4B3C2D}.Release|x64.Build.0 = Release|Any CPU + {69D60E22-9190-4433-9A6E-1D889CF5CA52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69D60E22-9190-4433-9A6E-1D889CF5CA52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69D60E22-9190-4433-9A6E-1D889CF5CA52}.Debug|x64.ActiveCfg = Debug|Any CPU + {69D60E22-9190-4433-9A6E-1D889CF5CA52}.Debug|x64.Build.0 = Debug|Any CPU + {69D60E22-9190-4433-9A6E-1D889CF5CA52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69D60E22-9190-4433-9A6E-1D889CF5CA52}.Release|Any CPU.Build.0 = Release|Any CPU + {69D60E22-9190-4433-9A6E-1D889CF5CA52}.Release|x64.ActiveCfg = Release|Any CPU + {69D60E22-9190-4433-9A6E-1D889CF5CA52}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -327,6 +338,7 @@ Global {311E6E78-3A5B-4E51-802A-5755BD5F9F97} = {06EFDBE0-6408-4B37-BCF2-0CF9EBEA2E93} {B0054A16-472E-44AC-BA40-349303E524FF} = {06EFDBE0-6408-4B37-BCF2-0CF9EBEA2E93} {A1B2C3D4-E5F6-4A5B-9C8D-7E6F5A4B3C2D} = {06EFDBE0-6408-4B37-BCF2-0CF9EBEA2E93} + {69D60E22-9190-4433-9A6E-1D889CF5CA52} = {06EFDBE0-6408-4B37-BCF2-0CF9EBEA2E93} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D3761C32-8C5F-498A-892B-3B0882994B62} diff --git a/QuickLook.slnx b/QuickLook.slnx index cf08c3890..293e98b1b 100644 --- a/QuickLook.slnx +++ b/QuickLook.slnx @@ -17,6 +17,7 @@ + @@ -48,6 +49,7 @@ +