mirror of
https://github.com/QL-Win/QuickLook.git
synced 2026-02-27 01:00:11 +08:00
Add DICOM image support to ImageViewer plugin #1866
This is not a permanent support and will be adjusted to the plugin later.
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
// Copyright © 2017-2026 QL-Win Contributors
|
||||
//
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
using FellowOakDicom;
|
||||
using FellowOakDicom.Imaging;
|
||||
using QuickLook.Common.Helpers;
|
||||
using QuickLook.Common.Plugin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace QuickLook.Plugin.ImageViewer.AnimatedImage.Providers;
|
||||
|
||||
internal sealed class DicomProvider : AnimationProvider
|
||||
{
|
||||
private static readonly HashSet<DicomTransferSyntax> SupportedTransferSyntaxes =
|
||||
[
|
||||
DicomTransferSyntax.ImplicitVRLittleEndian,
|
||||
DicomTransferSyntax.ExplicitVRLittleEndian,
|
||||
DicomTransferSyntax.ExplicitVRBigEndian,
|
||||
];
|
||||
|
||||
private static readonly object ImageManagerLock = new();
|
||||
private static bool _imageManagerInitialized;
|
||||
|
||||
private readonly DicomDataset _dataset;
|
||||
private readonly DicomImage _dicomImage;
|
||||
private readonly object _renderLock = new();
|
||||
|
||||
public DicomProvider(Uri path, MetaProvider meta, ContextObject contextObject) : base(path, meta, contextObject)
|
||||
{
|
||||
EnsureWpfImageManager();
|
||||
|
||||
_dataset = LoadDicomDataset(path.LocalPath);
|
||||
if (_dataset == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_dicomImage = new DicomImage(_dataset);
|
||||
Animator = new Int32AnimationUsingKeyFrames();
|
||||
|
||||
var duration = GetFrameDuration(_dataset);
|
||||
var accumulator = TimeSpan.Zero;
|
||||
|
||||
for (var i = 0; i < _dicomImage.NumberOfFrames; i++)
|
||||
{
|
||||
Animator.KeyFrames.Add(new DiscreteInt32KeyFrame(i, KeyTime.FromTimeSpan(accumulator)));
|
||||
accumulator += duration;
|
||||
}
|
||||
|
||||
if (accumulator == TimeSpan.Zero && _dicomImage.NumberOfFrames > 0)
|
||||
accumulator = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
Animator.Duration = new Duration(accumulator);
|
||||
Animator.RepeatBehavior = RepeatBehavior.Forever;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ProcessHelper.WriteLog(e.ToString());
|
||||
_dicomImage = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override Task<BitmapSource> GetThumbnail(Size renderSize)
|
||||
{
|
||||
return new Task<BitmapSource>(() =>
|
||||
{
|
||||
if (_dicomImage == null)
|
||||
return null;
|
||||
|
||||
lock (_renderLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var rendered = _dicomImage.RenderImage(0);
|
||||
var image = TryGetBitmapSource(rendered);
|
||||
if (image == null)
|
||||
return null;
|
||||
|
||||
var scaled = ScaleToFit(image, renderSize);
|
||||
Helper.DpiHack(scaled);
|
||||
scaled.Freeze();
|
||||
|
||||
return scaled;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ProcessHelper.WriteLog(e.ToString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override Task<BitmapSource> GetRenderedFrame(int index)
|
||||
{
|
||||
return new Task<BitmapSource>(() =>
|
||||
{
|
||||
if (_dicomImage == null)
|
||||
return null;
|
||||
|
||||
lock (_renderLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (index < 0 || index >= _dicomImage.NumberOfFrames)
|
||||
return null;
|
||||
|
||||
using var rendered = _dicomImage.RenderImage(index);
|
||||
var image = TryGetBitmapSource(rendered);
|
||||
if (image == null)
|
||||
return null;
|
||||
|
||||
Helper.DpiHack(image);
|
||||
image.Freeze();
|
||||
|
||||
return image;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ProcessHelper.WriteLog(e.ToString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
// DicomImage logic does not require disposal, it holds the Dataset which is in memory.
|
||||
}
|
||||
|
||||
private DicomDataset LoadDicomDataset(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var file = DicomFile.Open(path);
|
||||
var transferSyntax = file.FileMetaInfo?.TransferSyntax ?? file.Dataset.InternalTransferSyntax;
|
||||
|
||||
// Strict check
|
||||
if (!SupportedTransferSyntaxes.Contains(transferSyntax))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var pixelData = DicomPixelData.Create(file.Dataset, false);
|
||||
if (!IsSupportedPixelData(pixelData))
|
||||
return null;
|
||||
|
||||
return file.Dataset;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ProcessHelper.WriteLog(e.ToString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static TimeSpan GetFrameDuration(DicomDataset dataset)
|
||||
{
|
||||
if (TryGetSingleValue(dataset, DicomTag.FrameTime, out double frameTime))
|
||||
{
|
||||
return TimeSpan.FromMilliseconds(frameTime);
|
||||
}
|
||||
|
||||
if (TryGetSingleValue(dataset, DicomTag.RecommendedDisplayFrameRate, out double frameRate) && frameRate > 0)
|
||||
{
|
||||
// FPS to duration
|
||||
return TimeSpan.FromSeconds(1.0 / frameRate);
|
||||
}
|
||||
|
||||
return TimeSpan.FromMilliseconds(100);
|
||||
}
|
||||
|
||||
private static bool TryGetSingleValue<T>(DicomDataset dataset, DicomTag tag, out T value)
|
||||
{
|
||||
value = default;
|
||||
try
|
||||
{
|
||||
if (dataset.Contains(tag))
|
||||
{
|
||||
value = dataset.GetValue<T>(tag, 0); // Use GetValue with index 0 which is safer or simple GetValue<T>(tag, 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static BitmapSource TryGetBitmapSource(IImage image)
|
||||
{
|
||||
try
|
||||
{
|
||||
var wpfBitmap = image.AsWriteableBitmap();
|
||||
return wpfBitmap;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ProcessHelper.WriteLog($"Bitmap conversion failed: {e}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureWpfImageManager()
|
||||
{
|
||||
if (_imageManagerInitialized)
|
||||
return;
|
||||
|
||||
lock (ImageManagerLock)
|
||||
{
|
||||
if (_imageManagerInitialized)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
new DicomSetupBuilder()
|
||||
.RegisterServices(s =>
|
||||
{
|
||||
s.AddFellowOakDicom();
|
||||
s.AddImageManager<WPFImageManager>();
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ProcessHelper.WriteLog($"DICOM Init Failed: {e}");
|
||||
}
|
||||
|
||||
_imageManagerInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSupportedPixelData(DicomPixelData pixelData)
|
||||
{
|
||||
var photometric = pixelData.PhotometricInterpretation;
|
||||
|
||||
if (photometric == PhotometricInterpretation.YbrFull422 ||
|
||||
photometric == PhotometricInterpretation.YbrPartial422)
|
||||
return false;
|
||||
|
||||
return photometric == PhotometricInterpretation.Monochrome1 ||
|
||||
photometric == PhotometricInterpretation.Monochrome2 ||
|
||||
photometric == PhotometricInterpretation.Rgb ||
|
||||
photometric == PhotometricInterpretation.YbrFull;
|
||||
}
|
||||
|
||||
private static BitmapSource ScaleToFit(BitmapSource image, Size renderSize)
|
||||
{
|
||||
if (image == null)
|
||||
return null;
|
||||
|
||||
if (renderSize.Width <= 0 || renderSize.Height <= 0)
|
||||
return image;
|
||||
|
||||
var scale = Math.Min(renderSize.Width / image.PixelWidth, renderSize.Height / image.PixelHeight);
|
||||
if (double.IsNaN(scale) || double.IsInfinity(scale) || scale <= 0 || scale >= 1)
|
||||
return image;
|
||||
|
||||
return new TransformedBitmap(image, new ScaleTransform(scale, scale));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright © 2017-2026 QL-Win Contributors
|
||||
//
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
using FellowOakDicom;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
|
||||
namespace QuickLook.Plugin.ImageViewer;
|
||||
|
||||
public partial class MetaProvider
|
||||
{
|
||||
private static bool IsDicomFile(string path)
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
return extension.Equals(".dcm", StringComparison.OrdinalIgnoreCase)
|
||||
|| extension.Equals(".dicom", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static Size TryGetDicomSize(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var file = DicomFile.Open(path);
|
||||
var rows = file.Dataset.GetSingleValueOrDefault(DicomTag.Rows, 0);
|
||||
var columns = file.Dataset.GetSingleValueOrDefault(DicomTag.Columns, 0);
|
||||
|
||||
if (rows > 0 && columns > 0)
|
||||
return new Size(columns, rows);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.WriteLine(e);
|
||||
}
|
||||
|
||||
return Size.Empty;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ using System.Xml;
|
||||
|
||||
namespace QuickLook.Plugin.ImageViewer;
|
||||
|
||||
public class MetaProvider
|
||||
public partial class MetaProvider
|
||||
{
|
||||
private readonly SortedDictionary<string, (string, string)> _cache = []; // [key, [label, value]]
|
||||
|
||||
@@ -79,6 +79,13 @@ public class MetaProvider
|
||||
if (int.TryParse(w_.Item2, out var w) && int.TryParse(h_.Item2, out var h))
|
||||
return new Size(w, h);
|
||||
|
||||
if (IsDicomFile(_path))
|
||||
{
|
||||
var dicomSize = TryGetDicomSize(_path);
|
||||
if (!dicomSize.IsEmpty)
|
||||
return dicomSize;
|
||||
}
|
||||
|
||||
// fallback
|
||||
try
|
||||
{
|
||||
|
||||
@@ -36,7 +36,7 @@ public sealed partial class Plugin : IViewer, IMoreMenu
|
||||
".apng", ".ari", ".arw", ".avif", ".ani",
|
||||
".bay", ".bmp",
|
||||
".cap", ".cr2", ".cr3", ".crw", ".cur",
|
||||
".dcr", ".dcs", ".dds", ".dng", ".drf",
|
||||
".dcr", ".dcs", ".dds", ".dng", ".drf", ".dcm", ".dicom",
|
||||
".eip", ".emf", ".erf", ".exr",
|
||||
".fff",
|
||||
".gif",
|
||||
@@ -107,6 +107,9 @@ public sealed partial class Plugin : IViewer, IMoreMenu
|
||||
new KeyValuePair<string[], Type>([".svg"],
|
||||
typeof(SvgProvider)));
|
||||
#endif
|
||||
AnimatedImage.AnimatedImage.Providers.Add(
|
||||
new KeyValuePair<string[], Type>([".dcm", ".dicom"],
|
||||
typeof(DicomProvider)));
|
||||
AnimatedImage.AnimatedImage.Providers.Add(
|
||||
new KeyValuePair<string[], Type>(["*"],
|
||||
typeof(ImageMagickProvider)));
|
||||
|
||||
@@ -55,6 +55,12 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="fo-dicom" Version="5.2.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="fo-dicom.Imaging.Desktop" Version="5.2.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.IO.Compression" Version="4.3.0" />
|
||||
<PackageReference Include="QuickLook.ImageGlass.WebP" Version="1.4.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
Reference in New Issue
Block a user