Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions QuickLook.Plugin/QuickLook.Plugin.Hdf5Viewer/Hdf5SummaryBuilder.cs
Original file line number Diff line number Diff line change
@@ -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<IH5Attribute>(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: <error: {ex.Message}>");
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<object> SafeReadChildren(IH5Group group)
{
try
{
return group.Children();
}
catch
{
return Array.Empty<object>();
}
}

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;
}
}
32 changes: 32 additions & 0 deletions QuickLook.Plugin/QuickLook.Plugin.Hdf5Viewer/Hdf5TextPanel.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
109 changes: 109 additions & 0 deletions QuickLook.Plugin/QuickLook.Plugin.Hdf5Viewer/Plugin.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net462</TargetFramework>
<RootNamespace>QuickLook.Plugin.Hdf5Viewer</RootNamespace>
<AssemblyName>QuickLook.Plugin.Hdf5Viewer</AssemblyName>
<FileAlignment>512</FileAlignment>
<SignAssembly>false</SignAssembly>
<UseWPF>true</UseWPF>
<LangVersion>latest</LangVersion>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<ProjectGuid>{69D60E22-9190-4433-9A6E-1D889CF5CA52}</ProjectGuid>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|AnyCPU'">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\..\Build\Debug\QuickLook.Plugin\QuickLook.Plugin.Hdf5Viewer\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|AnyCPU'">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>..\..\Build\Release\QuickLook.Plugin\QuickLook.Plugin.Hdf5Viewer\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\..\Build\Debug\QuickLook.Plugin\QuickLook.Plugin.Hdf5Viewer\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>..\..\Build\Release\QuickLook.Plugin\QuickLook.Plugin.Hdf5Viewer\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="PureHDF" Version="1.0.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\QuickLook.Common\QuickLook.Common.csproj">
<Project>{85FDD6BA-871D-46C8-BD64-F6BB0CB5EA95}</Project>
<Name>QuickLook.Common</Name>
<Private>False</Private>
</ProjectReference>
</ItemGroup>

<ItemGroup>
<Compile Include="..\..\GitVersion.cs">
<Link>Properties\GitVersion.cs</Link>
</Compile>
</ItemGroup>

</Project>
Loading