Fix #106: display thumbnails

This commit is contained in:
Paddy Xu
2017-11-05 02:15:00 +02:00
parent edc073a0ea
commit 24fafc5746
17 changed files with 304 additions and 68 deletions

View File

@@ -16,10 +16,16 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Media.Animation; using System.Windows.Media.Animation;
using System.Windows.Threading;
using QuickLook.Helpers;
using QuickLook.Plugin.ImageViewer.Exiv2;
namespace QuickLook.Plugin.ImageViewer.AnimatedImage namespace QuickLook.Plugin.ImageViewer.AnimatedImage
{ {
@@ -29,7 +35,11 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage
DependencyProperty.Register("AnimationUri", typeof(Uri), typeof(AnimatedImage), DependencyProperty.Register("AnimationUri", typeof(Uri), typeof(AnimatedImage),
new UIPropertyMetadata(null, LoadImage)); new UIPropertyMetadata(null, LoadImage));
private readonly ObjectAnimationUsingKeyFrames _animator = new ObjectAnimationUsingKeyFrames(); public static readonly DependencyProperty MetaProperty =
DependencyProperty.Register("Meta", typeof(Meta), typeof(AnimatedImage));
private ObjectAnimationUsingKeyFrames _animator = new ObjectAnimationUsingKeyFrames();
private bool _disposed;
public Uri AnimationUri public Uri AnimationUri
{ {
@@ -37,11 +47,19 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage
set => SetValue(AnimationUriProperty, value); set => SetValue(AnimationUriProperty, value);
} }
public Meta Meta
{
private get => (Meta) GetValue(MetaProperty);
set => SetValue(MetaProperty, value);
}
public void Dispose() public void Dispose()
{ {
BeginAnimation(SourceProperty, null); BeginAnimation(SourceProperty, null);
Source = null; Source = null;
_animator.KeyFrames.Clear(); _animator.KeyFrames.Clear();
_disposed = true;
} }
private static void LoadImage(DependencyObject obj, DependencyPropertyChangedEventArgs ev) private static void LoadImage(DependencyObject obj, DependencyPropertyChangedEventArgs ev)
@@ -50,8 +68,55 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage
if (instance == null) if (instance == null)
return; return;
var path = ((Uri) ev.NewValue).LocalPath; var thumbnail = instance.Meta?.GetThumbnail(true);
var ext = Path.GetExtension(path).ToLower(); instance.Source = thumbnail;
if (thumbnail != null)
LoadFullImageAsync(obj, ev);
else
LoadFullImage(obj, ev);
}
private static void LoadFullImage(DependencyObject obj, DependencyPropertyChangedEventArgs ev)
{
var instance = obj as AnimatedImage;
if (instance == null)
return;
instance._animator = LoadFullImageCore((Uri) ev.NewValue);
instance.BeginAnimation(SourceProperty, instance._animator);
}
private static void LoadFullImageAsync(DependencyObject obj, DependencyPropertyChangedEventArgs ev)
{
Task.Run(() =>
{
var instance = obj as AnimatedImage;
if (instance == null)
return;
var animator = LoadFullImageCore((Uri) ev.NewValue);
instance.Dispatcher.Invoke(DispatcherPriority.Render,
new Action(() =>
{
if (instance._disposed)
{
ProcessHelper.PerformAggressiveGC();
return;
}
instance._animator = animator;
instance.BeginAnimation(SourceProperty, instance._animator);
Debug.WriteLine($"LoadFullImageAsync {Thread.CurrentThread.ManagedThreadId}");
}));
});
}
private static ObjectAnimationUsingKeyFrames LoadFullImageCore(Uri path)
{
var ext = Path.GetExtension(path.LocalPath).ToLower();
IAnimationProvider provider; IAnimationProvider provider;
@@ -67,10 +132,11 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage
provider = new ImageMagickProvider(); provider = new ImageMagickProvider();
break; break;
} }
var animator = new ObjectAnimationUsingKeyFrames();
provider.GetAnimator(animator, path.LocalPath);
animator.Freeze();
provider.GetAnimator(instance._animator, path); return animator;
instance.BeginAnimation(SourceProperty, instance._animator);
} }
} }
} }

View File

@@ -28,9 +28,6 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage
{ {
public void GetAnimator(ObjectAnimationUsingKeyFrames animator, string path) public void GetAnimator(ObjectAnimationUsingKeyFrames animator, string path)
{ {
// set dcraw.exe for Magick.NET
Directory.SetCurrentDirectory(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
using (var image = new MagickImage(path)) using (var image = new MagickImage(path))
{ {
image.Density = new Density(Math.Floor(image.Density.X), Math.Floor(image.Density.Y)); image.Density = new Density(Math.Floor(image.Density.X), Math.Floor(image.Density.Y));
@@ -39,8 +36,6 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage
animator.KeyFrames.Add(new DiscreteObjectKeyFrame(image.ToBitmapSource(), TimeSpan.Zero)); animator.KeyFrames.Add(new DiscreteObjectKeyFrame(image.ToBitmapSource(), TimeSpan.Zero));
animator.Duration = Duration.Forever; animator.Duration = Duration.Forever;
} }
Directory.SetCurrentDirectory(App.AppPath);
} }
} }
} }

View File

@@ -15,50 +15,33 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
using System;
using System.Windows; using System.Windows;
using ExifLib;
using ImageMagick; using ImageMagick;
using QuickLook.Plugin.ImageViewer.Exiv2;
namespace QuickLook.Plugin.ImageViewer namespace QuickLook.Plugin.ImageViewer
{ {
internal static class ImageFileHelper internal static class ImageFileHelper
{ {
internal static Size? GetImageSize(string path) internal static Size GetImageSize(string path, Meta meta)
{ {
var ori = GetOrientationFromExif(path); var size = meta.GetSize();
if (!size.IsEmpty)
return size;
try try
{ {
var info = new MagickImageInfo(path); var info = new MagickImageInfo(path);
if (ori == OrientationType.RightTop || ori == OrientationType.LeftBotom) if (meta.GetOrientation() == OrientationType.RightTop ||
meta.GetOrientation() == OrientationType.LeftBotom)
return new Size {Width = info.Height, Height = info.Width}; return new Size {Width = info.Height, Height = info.Width};
return new Size {Width = info.Width, Height = info.Height}; return new Size {Width = info.Width, Height = info.Height};
} }
catch (MagickException) catch (MagickException)
{ {
return null; return Size.Empty;
}
}
private static OrientationType GetOrientationFromExif(string path)
{
try
{
using (var re = new ExifReader(path))
{
re.GetTagValue(ExifTags.Orientation, out ushort orientation);
if (orientation == 0)
return OrientationType.Undefined;
return (OrientationType) orientation;
}
}
catch (Exception)
{
return OrientationType.Undefined;
} }
} }
} }

View File

@@ -18,7 +18,7 @@
</Rectangle> </Rectangle>
<ScrollViewer x:Name="viewPanel" BorderThickness="0" HorizontalScrollBarVisibility="Auto" <ScrollViewer x:Name="viewPanel" BorderThickness="0" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto" Focusable="False" IsManipulationEnabled="True"> VerticalScrollBarVisibility="Auto" Focusable="False" IsManipulationEnabled="True">
<animatedImage:AnimatedImage x:Name="viewPanelImage" Stretch="None" Margin="0,32,0,0" <animatedImage:AnimatedImage x:Name="viewPanelImage" Stretch="None" Meta="{Binding Meta, ElementName=imagePanel}"
RenderOptions.BitmapScalingMode="{Binding RenderMode, ElementName=imagePanel}" RenderOptions.BitmapScalingMode="{Binding RenderMode, ElementName=imagePanel}"
AnimationUri="{Binding ImageUriSource, ElementName=imagePanel}" /> AnimationUri="{Binding ImageUriSource, ElementName=imagePanel}" />
</ScrollViewer> </ScrollViewer>

View File

@@ -29,6 +29,7 @@ using System.Windows.Media.Imaging;
using System.Windows.Threading; using System.Windows.Threading;
using QuickLook.Annotations; using QuickLook.Annotations;
using QuickLook.Helpers; using QuickLook.Helpers;
using QuickLook.Plugin.ImageViewer.Exiv2;
namespace QuickLook.Plugin.ImageViewer namespace QuickLook.Plugin.ImageViewer
{ {
@@ -42,6 +43,7 @@ namespace QuickLook.Plugin.ImageViewer
private Uri _imageSource; private Uri _imageSource;
private DateTime _lastZoomTime = DateTime.MinValue; private DateTime _lastZoomTime = DateTime.MinValue;
private double _maxZoomFactor = 3d; private double _maxZoomFactor = 3d;
private Meta _meta;
private double _minZoomFactor = 0.1d; private double _minZoomFactor = 0.1d;
private BitmapScalingMode _renderMode = BitmapScalingMode.HighQuality; private BitmapScalingMode _renderMode = BitmapScalingMode.HighQuality;
private BitmapSource _source; private BitmapSource _source;
@@ -70,6 +72,11 @@ namespace QuickLook.Plugin.ImageViewer
viewPanel.ManipulationDelta += ViewPanel_ManipulationDelta; viewPanel.ManipulationDelta += ViewPanel_ManipulationDelta;
} }
internal ImagePanel(Meta meta) : this()
{
Meta = meta;
}
public BitmapScalingMode RenderMode public BitmapScalingMode RenderMode
{ {
get => _renderMode; get => _renderMode;
@@ -163,6 +170,17 @@ namespace QuickLook.Plugin.ImageViewer
} }
} }
public Meta Meta
{
get => _meta;
set
{
if (Equals(value, _meta)) return;
_meta = value;
OnPropertyChanged();
}
}
public void Dispose() public void Dispose()
{ {
viewPanelImage?.Dispose(); viewPanelImage?.Dispose();

View File

@@ -20,6 +20,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Windows; using System.Windows;
using ImageMagick; using ImageMagick;
using QuickLook.Plugin.ImageViewer.Exiv2;
namespace QuickLook.Plugin.ImageViewer namespace QuickLook.Plugin.ImageViewer
{ {
@@ -28,18 +29,18 @@ namespace QuickLook.Plugin.ImageViewer
private static readonly string[] Formats = private static readonly string[] Formats =
{ {
// camera raw // camera raw
".3fr", ".ari", ".arw", ".bay", ".crw", ".cr2", ".cap", ".data", ".dcs", ".dcr", ".dng", ".drf", ".eip", ".ari", ".arw", ".bay", ".crw", ".cr2", ".cap", ".dcs", ".dcr", ".dng", ".drf", ".eip", ".erf", ".fff",
".erf", ".fff", ".gpr", ".iiq", ".k25", ".kdc", ".mdc", ".mef", ".mos", ".mrw", ".nef", ".nrw", ".obm", ".iiq", ".k25", ".kdc", ".mdc", ".mef", ".mos", ".mrw", ".nef", ".nrw", ".obm", ".orf", ".pef", ".ptx",
".orf", ".pef", ".ptx", ".pxn", ".r3d", ".raf", ".raw", ".rwl", ".rw2", ".rwz", ".sr2", ".srf", ".srw", ".pxn", ".r3d", ".raf", ".raw", ".rwl", ".rw2", ".rwz", ".sr2", ".srf", ".srw", ".x3f",
".tif", ".x3f",
// normal // normal
".bmp", ".ico", ".icon", ".jpg", ".jpeg", ".psd", ".svg", ".wdp", ".tif", ".tiff", ".tga", ".bmp", ".ico", ".icon", ".jpg", ".jpeg", ".psd", ".svg", ".wdp", ".tif", ".tiff", ".tga", ".webp", ".pbm",
".webp", ".pbm", ".pgm", ".ppm", ".pnm", ".pgm", ".ppm", ".pnm",
// animated // animated
".png", ".apng", ".gif" ".png", ".apng", ".gif"
}; };
private Size _imageSize;
private ImagePanel _ip; private ImagePanel _ip;
private Meta _meta;
private Size _imageSize;
public int Priority => int.MaxValue; public int Priority => int.MaxValue;
@@ -55,26 +56,19 @@ namespace QuickLook.Plugin.ImageViewer
public void Prepare(string path, ContextObject context) public void Prepare(string path, ContextObject context)
{ {
_imageSize = ImageFileHelper.GetImageSize(path) ?? Size.Empty; _imageSize = ImageFileHelper.GetImageSize(path, _meta = new Meta(path));
if (!_imageSize.IsEmpty) if (!_imageSize.IsEmpty)
context.SetPreferredSizeFit(_imageSize, 0.6); context.SetPreferredSizeFit(_imageSize, 0.8);
else else
context.PreferredSize = new Size(800, 600); context.PreferredSize = new Size(800, 600);
context.PreferredSize = new Size(context.PreferredSize.Width, context.PreferredSize.Height + 32);
Directory.SetCurrentDirectory(App.AppPath);
context.TitlebarBlurVisibility = true;
context.TitlebarOverlap = true;
context.TitlebarAutoHide = false;
context.UseDarkTheme = true; context.UseDarkTheme = true;
} }
public void View(string path, ContextObject context) public void View(string path, ContextObject context)
{ {
_ip = new ImagePanel(); _ip = new ImagePanel(_meta);
context.ViewerContent = _ip; context.ViewerContent = _ip;
context.Title = _imageSize.IsEmpty context.Title = _imageSize.IsEmpty

View File

@@ -58,9 +58,6 @@
<AssemblyOriginatorKeyFile>key.snk</AssemblyOriginatorKeyFile> <AssemblyOriginatorKeyFile>key.snk</AssemblyOriginatorKeyFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="ExifLib, Version=1.7.0.0, Culture=neutral, PublicKeyToken=30284005913968db, processorArchitecture=MSIL">
<HintPath>..\..\packages\ExifLib.1.7.0.0\lib\net45\ExifLib.dll</HintPath>
</Reference>
<Reference Include="LibAPNG"> <Reference Include="LibAPNG">
<HintPath>.\LibAPNG.dll</HintPath> <HintPath>.\LibAPNG.dll</HintPath>
</Reference> </Reference>
@@ -84,6 +81,7 @@
<Compile Include="AnimatedImage\GIFAnimationProvider.cs" /> <Compile Include="AnimatedImage\GIFAnimationProvider.cs" />
<Compile Include="AnimatedImage\IAnimationProvider.cs" /> <Compile Include="AnimatedImage\IAnimationProvider.cs" />
<Compile Include="AnimatedImage\ImageMagickProvider.cs" /> <Compile Include="AnimatedImage\ImageMagickProvider.cs" />
<Compile Include="exiv2\Meta.cs" />
<Compile Include="ImageFileHelper.cs" /> <Compile Include="ImageFileHelper.cs" />
<Compile Include="ImagePanel.xaml.cs"> <Compile Include="ImagePanel.xaml.cs">
<DependentUpon>ImagePanel.xaml</DependentUpon> <DependentUpon>ImagePanel.xaml</DependentUpon>
@@ -109,11 +107,22 @@
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="dcraw.exe">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Resource Include="Resources\background-b.png" /> <Resource Include="Resources\background-b.png" />
<Resource Include="Resources\background.png" /> <Resource Include="Resources\background.png" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Include="exiv2\exiv2.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="exiv2\exiv2.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="exiv2\expat.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="exiv2\zlib.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project> </Project>

View File

@@ -0,0 +1,176 @@
// Copyright © 2017 Paddy Xu
//
// 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 System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using ImageMagick;
using QuickLook.ExtensionMethods;
namespace QuickLook.Plugin.ImageViewer.Exiv2
{
public class Meta
{
private static readonly string ExivPath =
Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "exiv2\\exiv2.exe");
private readonly string _path;
private OrientationType _orientation = OrientationType.Undefined;
private Dictionary<string, string> _summary;
public Meta(string path)
{
_path = path;
}
private Dictionary<string, string> GetSummary()
{
if (_summary != null)
return _summary;
return _summary = Run($"\"{_path}\"", ":");
}
public BitmapSource GetThumbnail(bool autoZoom = false)
{
GetOrientation();
var count = Run($"-pp \"{_path}\"", ",").Count;
if (count == 0)
return null;
var suc = Run($"-f -ep{count} -l \"{Path.GetTempPath().TrimEnd('\\')}\" \"{_path}\"", ",");
if (suc.Count != 0)
return null;
try
{
using (var image = new MagickImage(Path.Combine(Path.GetTempPath(),
$"{Path.GetFileNameWithoutExtension(_path)}-preview{count}.jpg")))
{
File.Delete(image.FileName);
if (_orientation == OrientationType.RightTop)
image.Rotate(90);
else if (_orientation == OrientationType.BottomRight)
image.Rotate(180);
else if (_orientation == OrientationType.LeftBotom)
image.Rotate(270);
if (!autoZoom)
return image.ToBitmapSource();
var size = GetSize();
return new TransformedBitmap(image.ToBitmapSource(),
new ScaleTransform(size.Width / image.Width, size.Height / image.Height));
}
}
catch (Exception)
{
return null;
}
}
public Size GetSize()
{
if (_summary == null)
GetSummary();
if (!_summary.ContainsKey("Image size"))
return Size.Empty;
var width = int.Parse(_summary["Image size"].Split('x')[0].Trim());
var height = int.Parse(_summary["Image size"].Split('x')[1].Trim());
switch (GetOrientation())
{
case OrientationType.RightTop:
case OrientationType.LeftBotom:
return new Size(height, width);
default:
return new Size(width, height);
}
}
public OrientationType GetOrientation()
{
if (_orientation != OrientationType.Undefined)
return _orientation;
try
{
var ori = Run($"-g Exif.Image.Orientation -Pkv \"{_path}\"", "\\s");
if (ori?.ContainsKey("Exif.Image.Orientation") == true)
_orientation = (OrientationType) int.Parse(ori["Exif.Image.Orientation"]);
else
_orientation = OrientationType.TopLeft;
}
catch (Exception)
{
_orientation = OrientationType.TopLeft;
}
return _orientation;
}
private Dictionary<string, string> Run(string arg, string regexSplit)
{
string result;
using (var p = new Process())
{
p.StartInfo.UseShellExecute = false;
p.StartInfo.CreateNoWindow = true;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.FileName = ExivPath;
p.StartInfo.Arguments = arg;
p.StartInfo.StandardOutputEncoding = Encoding.UTF8;
p.Start();
p.WaitForExit();
result = p.StandardOutput.ReadToEnd();
}
return string.IsNullOrWhiteSpace(result)
? new Dictionary<string, string>()
: ParseResult(result, regexSplit);
}
private Dictionary<string, string> ParseResult(string result, string regexSplit)
{
var res = new Dictionary<string, string>();
result.Replace("\r\n", "\n").Split('\n').ForEach(l =>
{
if (string.IsNullOrWhiteSpace(l))
return;
var eles = Regex.Split(l, regexSplit + "{1,}");
res.Add(eles[0].Trim(), eles[1].Trim());
});
return res;
}
}
}

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="ExifLib" version="1.7.0.0" targetFramework="net462" />
<package id="Magick.NET-Q8-AnyCPU" version="7.0.7.900" targetFramework="net462" /> <package id="Magick.NET-Q8-AnyCPU" version="7.0.7.900" targetFramework="net462" />
</packages> </packages>

View File

@@ -27,7 +27,6 @@
VirtualizingPanel.IsVirtualizing="True" Width="150" VirtualizingPanel.IsVirtualizing="True" Width="150"
SelectedIndex="0" SelectedIndex="0"
Focusable="False" Focusable="False"
Margin="0,32,0,0"
Background="#00FFFFFF" Background="#00FFFFFF"
ItemsSource="{Binding PageIds, ElementName=thisPdfViewer}" ItemsSource="{Binding PageIds, ElementName=thisPdfViewer}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" BorderThickness="0,0,1,0" ScrollViewer.HorizontalScrollBarVisibility="Disabled" BorderThickness="0,0,1,0"

View File

@@ -60,9 +60,6 @@ namespace QuickLook.Plugin.PDFViewer
context.SetPreferredSizeFit(desiredSize, 0.6); context.SetPreferredSizeFit(desiredSize, 0.6);
context.PreferredSize = new Size(context.PreferredSize.Width, context.PreferredSize.Height + 32); context.PreferredSize = new Size(context.PreferredSize.Width, context.PreferredSize.Height + 32);
context.TitlebarBlurVisibility = true;
context.TitlebarOverlap = true;
} }
public void View(string path, ContextObject context) public void View(string path, ContextObject context)

View File

@@ -22,7 +22,7 @@ using QuickLook.NativeMethods;
namespace QuickLook.Helpers namespace QuickLook.Helpers
{ {
internal class ProcessHelper public class ProcessHelper
{ {
private const int ErrorInsufficientBuffer = 0x7A; private const int ErrorInsufficientBuffer = 0x7A;