diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/DicomProvider.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/DicomProvider.cs
new file mode 100644
index 0000000..86ec346
--- /dev/null
+++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/DicomProvider.cs
@@ -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 .
+
+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 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 GetThumbnail(Size renderSize)
+ {
+ return new Task(() =>
+ {
+ 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 GetRenderedFrame(int index)
+ {
+ return new Task(() =>
+ {
+ 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(DicomDataset dataset, DicomTag tag, out T value)
+ {
+ value = default;
+ try
+ {
+ if (dataset.Contains(tag))
+ {
+ value = dataset.GetValue(tag, 0); // Use GetValue with index 0 which is safer or simple GetValue(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();
+ })
+ .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));
+ }
+}
diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/MetaProvider.Discom.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/MetaProvider.Discom.cs
new file mode 100644
index 0000000..981b6fc
--- /dev/null
+++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/MetaProvider.Discom.cs
@@ -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 .
+
+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;
+ }
+}
diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/MetaProvider.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/MetaProvider.cs
index 5be0723..1c39a91 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/MetaProvider.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/MetaProvider.cs
@@ -26,7 +26,7 @@ using System.Xml;
namespace QuickLook.Plugin.ImageViewer;
-public class MetaProvider
+public partial class MetaProvider
{
private readonly SortedDictionary _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
{
diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs
index e1f001b..04020e9 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs
@@ -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([".svg"],
typeof(SvgProvider)));
#endif
+ AnimatedImage.AnimatedImage.Providers.Add(
+ new KeyValuePair([".dcm", ".dicom"],
+ typeof(DicomProvider)));
AnimatedImage.AnimatedImage.Providers.Add(
new KeyValuePair(["*"],
typeof(ImageMagickProvider)));
diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj
index 2f6dc3f..1cefa30 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj
+++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj
@@ -55,6 +55,12 @@
+
+ all
+
+
+ all
+
all
diff --git a/SUPPORTED_FORMATS.md b/SUPPORTED_FORMATS.md
index 2322708..c0ce515 100644
--- a/SUPPORTED_FORMATS.md
+++ b/SUPPORTED_FORMATS.md
@@ -143,6 +143,7 @@ Update not completed yet...
- `.cur` (Windows cursor)
- `.dcr`, `.dcs`, `.drf` (Kodak RAW image)
- `.dds` (DirectDraw Surface)
+- `.dcm`, `.dicom` (DICOM medical image, preview)
- `.dng` (Digital Negative RAW)
- `.eip` (Capture One Enhanced Image Package)
- `.emf` (Enhanced Metafile)