Fix #342: better async thumbnail loading

This commit is contained in:
Paddy Xu
2018-09-07 21:33:49 +03:00
parent 89f1bb46b6
commit 08ccedece4
10 changed files with 287 additions and 84 deletions

View File

@@ -0,0 +1,62 @@
// Copyright © 2018 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.Globalization;
using System.Threading.Tasks;
using System.Windows.Data;
using System.Windows.Media.Imaging;
namespace QuickLook.Plugin.PDFViewer
{
internal class AsyncPageToThumbnailConverter : IMultiValueConverter
{
private static readonly BitmapImage Loading =
new BitmapImage(
new Uri("pack://application:,,,/QuickLook.Plugin.PdfViewer;component/Resources/loading.png"));
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length < 2)
throw new Exception("PageIdToImageConverter");
if (!(values[0] is PdfDocumentWrapper handle)) return null;
var pageId = (int) values[1];
if (pageId < 0) return null;
var task = Task.Run(() =>
{
try
{
return handle.RenderThumbnail(pageId);
}
catch (Exception)
{
return Loading;
}
});
return new NotifyTaskCompletion<BitmapSource>(task, Loading);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,89 @@
// Copyright © 2018 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.ComponentModel;
using System.Threading.Tasks;
namespace QuickLook.Plugin.PDFViewer
{
public sealed class NotifyTaskCompletion<TResult> : INotifyPropertyChanged
{
private readonly TResult _loading;
public NotifyTaskCompletion(Task<TResult> task, TResult loading = default(TResult))
{
Task = task;
_loading = loading;
if (!task.IsCompleted)
{
var _ = WatchTaskAsync(task);
}
}
public Task<TResult> Task { get; }
public TResult Result => Task.Status == TaskStatus.RanToCompletion ? Task.Result : _loading;
public TaskStatus Status => Task.Status;
public bool IsCompleted => Task.IsCompleted;
public bool IsNotCompleted => !Task.IsCompleted;
public bool IsSuccessfullyCompleted => Task.Status ==
TaskStatus.RanToCompletion;
public bool IsCanceled => Task.IsCanceled;
public bool IsFaulted => Task.IsFaulted;
public AggregateException Exception => Task.Exception;
public Exception InnerException => Exception?.InnerException;
public string ErrorMessage => InnerException?.Message;
public event PropertyChangedEventHandler PropertyChanged;
private async Task WatchTaskAsync(Task task)
{
try
{
await task;
}
catch
{
// ignored
}
var propertyChanged = PropertyChanged;
if (propertyChanged == null)
return;
propertyChanged(this, new PropertyChangedEventArgs("Status"));
propertyChanged(this, new PropertyChangedEventArgs("IsCompleted"));
propertyChanged(this, new PropertyChangedEventArgs("IsNotCompleted"));
if (task.IsCanceled)
{
propertyChanged(this, new PropertyChangedEventArgs("IsCanceled"));
}
else if (task.IsFaulted)
{
propertyChanged(this, new PropertyChangedEventArgs("IsFaulted"));
propertyChanged(this, new PropertyChangedEventArgs("Exception"));
propertyChanged(this,
new PropertyChangedEventArgs("InnerException"));
propertyChanged(this, new PropertyChangedEventArgs("ErrorMessage"));
}
else
{
propertyChanged(this,
new PropertyChangedEventArgs("IsSuccessfullyCompleted"));
propertyChanged(this, new PropertyChangedEventArgs("Result"));
}
}
}
}

View File

@@ -0,0 +1,58 @@
// Copyright © 2018 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.IO;
using PdfiumViewer;
namespace QuickLook.Plugin.PDFViewer
{
public class PdfDocumentWrapper : IDisposable
{
public PdfDocumentWrapper(Stream stream)
{
PdfStream = new MemoryStream((int) stream.Length);
stream.CopyTo(PdfStream);
PdfDocument = PdfDocument.Load(PdfStream);
}
public PdfDocument PdfDocument { get; private set; }
public MemoryStream PdfStream { get; private set; }
public void Dispose()
{
PdfDocument.Dispose();
PdfDocument = null;
PdfStream.Dispose();
PdfStream = null;
}
public void Refresh()
{
var oldD = PdfDocument;
PdfStream.Position = 0;
var newObj = new PdfDocumentWrapper(PdfStream);
PdfDocument = newObj.PdfDocument;
PdfStream = newObj.PdfStream;
oldD.Dispose();
}
}
}

View File

@@ -26,27 +26,45 @@ namespace QuickLook.Plugin.PDFViewer
{
internal static class PdfPageExtension
{
public static BitmapSource RenderThumbnail(this PdfDocument doc, int page)
private static int _renderCount;
private static readonly object LockObj = new object();
public static BitmapSource RenderThumbnail(this PdfDocumentWrapper doc, int page)
{
var size = doc.PageSizes[page];
var factorX = 130d / size.Width;
var factorY = 210d / size.Height;
lock (LockObj)
{
if (_renderCount++ == 50)
{
doc.Refresh();
_renderCount = 0;
}
}
var size = doc.PdfDocument.PageSizes[page];
var factorX = 60d / size.Width;
var factorY = 120d / size.Height;
return doc.Render(page, Math.Min(factorX, factorY), false);
}
public static BitmapSource Render(this PdfDocument doc, int page, double factor, bool fixDpi = true)
public static BitmapSource Render(this PdfDocumentWrapper doc, int page, double factor, bool fixDpi = true)
{
var size = doc.PageSizes[page];
var scale = DpiHelper.GetCurrentScaleFactor();
var dpiX = fixDpi ? scale.Horizontal * DpiHelper.DefaultDpi : 96;
var dpiY = fixDpi ? scale.Vertical * DpiHelper.DefaultDpi : 96;
Bitmap bitmap;
lock (LockObj)
{
var size = doc.PdfDocument.PageSizes[page];
var realWidth = (int) Math.Round(size.Width * scale.Horizontal * factor);
var realHeight = (int) Math.Round(size.Height * scale.Vertical * factor);
var bitmap = doc.Render(page, realWidth, realHeight, dpiX, dpiY,
PdfRenderFlags.LimitImageCacheSize | PdfRenderFlags.LcdText | PdfRenderFlags.Annotations|PdfRenderFlags.ForPrinting) as Bitmap;
bitmap = doc.PdfDocument.Render(page, realWidth, realHeight, dpiX, dpiY,
PdfRenderFlags.LimitImageCacheSize | PdfRenderFlags.LcdText | PdfRenderFlags.Annotations |
PdfRenderFlags.ForPrinting) as Bitmap;
}
var bs = bitmap?.ToBitmapSource();
bitmap?.Dispose();

View File

@@ -12,6 +12,7 @@
d:DesignWidth="720.29">
<UserControl.Resources>
<ResourceDictionary>
<local:AsyncPageToThumbnailConverter x:Key="AsyncPageToThumbnailConverter" />
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/QuickLook.Common;component/Styles/MainWindowStyles.xaml" />
<ResourceDictionary Source="ListBoxItemStyleNoFocusedBorder.xaml" />
@@ -23,17 +24,20 @@
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox x:Name="listThumbnails" Grid.Column="0" VirtualizingPanel.ScrollUnit="Pixel"
VirtualizingPanel.IsVirtualizing="True" Width="150"
<ListBox x:Name="listThumbnails" Grid.Column="0" Width="150"
SelectedIndex="0"
Focusable="False"
HorizontalContentAlignment="Center"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
Background="Transparent" BorderThickness="0"
ItemsSource="{Binding PageThumbnails, ElementName=thisPdfViewer}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ItemsSource="{Binding ElementName=thisPdfViewer, Path=PageThumbnails}"
ItemContainerStyle="{Binding Mode=OneWay, Source={StaticResource ListBoxItemStyleNoFocusedBorder}}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid MaxHeight="150"
<Grid Height="150"
MinWidth="30"
MaxWidth="{Binding ViewportWidth, Mode=Default, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ScrollViewer}}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10" />
@@ -45,8 +49,15 @@
<RowDefinition Height="*" />
<RowDefinition Height="10" />
</Grid.RowDefinitions>
<Border x:Name="bbd" Grid.Row="1" Grid.Column="1" BorderThickness="1" BorderBrush="#33FFFFFF">
<Image Source="{Binding}" />
<Border x:Name="bbd" Grid.Row="1" Grid.Column="1" BorderThickness="1" BorderBrush="Gray">
<Image Source="{Binding Path=Result}">
<Image.DataContext>
<MultiBinding Converter="{StaticResource AsyncPageToThumbnailConverter}">
<Binding Path="PdfDocumentWrapper" ElementName="thisPdfViewer" />
<Binding />
</MultiBinding>
</Image.DataContext>
</Image>
</Border>
<!--
<Label Grid.Row="1" Grid.Column="1" Content="{Binding Mode=OneWay, Converter={StaticResource MathConverter}, ConverterParameter=@VALUE+1}" FontSize="14" />

View File

@@ -22,10 +22,8 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
using PdfiumViewer;
using QuickLook.Common.ExtensionMethods;
@@ -41,9 +39,7 @@ namespace QuickLook.Plugin.PDFViewer
private double _maxZoomFactor = double.NaN;
private double _minZoomFactor = double.NaN;
private PdfDocument _pdfHandle;
private bool _pdfLoaded;
private Stream _pdfStream;
private double _viewRenderFactor = double.NaN;
public PdfViewerControl()
@@ -59,10 +55,9 @@ namespace QuickLook.Plugin.PDFViewer
pagePanel.ImageScrolled += NavigatePage;
}
public ObservableCollection<BitmapSource> PageThumbnails { get; set; } =
new ObservableCollection<BitmapSource>();
public ObservableCollection<int> PageThumbnails { get; set; } = new ObservableCollection<int>();
public int TotalPages => _pdfHandle.PageCount;
public int TotalPages => PdfDocumentWrapper.PdfDocument.PageCount;
public int CurrentPage
{
@@ -77,6 +72,8 @@ namespace QuickLook.Plugin.PDFViewer
}
}
public PdfDocumentWrapper PdfDocumentWrapper { get; private set; }
public void Dispose()
{
GC.SuppressFinalize(this);
@@ -88,9 +85,8 @@ namespace QuickLook.Plugin.PDFViewer
}
_pdfLoaded = false;
_pdfHandle?.Dispose();
_pdfHandle = null;
_pdfStream.Close();
PdfDocumentWrapper?.Dispose();
PdfDocumentWrapper = null;
}
public event PropertyChangedEventHandler PropertyChanged;
@@ -154,7 +150,10 @@ namespace QuickLook.Plugin.PDFViewer
if (!_pdfLoaded)
return;
Debug.WriteLine($"Renrendering page {CurrentPage}");
if (CurrentPage < 0 || CurrentPage >= TotalPages)
return;
Debug.WriteLine($"Re-rendering page {CurrentPage}");
var pos = pagePanel.GetScrollPosition();
@@ -163,16 +162,16 @@ namespace QuickLook.Plugin.PDFViewer
// First time showing. Set thresholds here.
if (double.IsNaN(_minZoomFactor) || double.IsNaN(_maxZoomFactor))
{
factor = Math.Min(pagePanel.ActualHeight / _pdfHandle.PageSizes[CurrentPage].Height,
pagePanel.ActualWidth / _pdfHandle.PageSizes[CurrentPage].Width);
factor = Math.Min(pagePanel.ActualHeight / PdfDocumentWrapper.PdfDocument.PageSizes[CurrentPage].Height,
pagePanel.ActualWidth / PdfDocumentWrapper.PdfDocument.PageSizes[CurrentPage].Width);
_viewRenderFactor = factor;
_minZoomFactor = 0.1 * factor;
_maxZoomFactor = 5 * factor;
}
else if (pagePanel.ZoomToFit)
{
factor = Math.Min(pagePanel.ActualHeight / _pdfHandle.PageSizes[CurrentPage].Height,
pagePanel.ActualWidth / _pdfHandle.PageSizes[CurrentPage].Width);
factor = Math.Min(pagePanel.ActualHeight / PdfDocumentWrapper.PdfDocument.PageSizes[CurrentPage].Height,
pagePanel.ActualWidth / PdfDocumentWrapper.PdfDocument.PageSizes[CurrentPage].Width);
}
else
{
@@ -183,7 +182,7 @@ namespace QuickLook.Plugin.PDFViewer
pagePanel.MaxZoomFactor = _maxZoomFactor / factor;
}
var image = _pdfHandle.Render(CurrentPage, factor);
var image = PdfDocumentWrapper.Render(CurrentPage, factor);
pagePanel.Source = image;
pagePanel.ResetZoom();
@@ -239,72 +238,31 @@ namespace QuickLook.Plugin.PDFViewer
public void LoadPdf(string path)
{
_pdfStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
_pdfHandle = PdfDocument.Load(_pdfStream);
var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
PdfDocumentWrapper = new PdfDocumentWrapper(stream);
_pdfLoaded = true;
BeginLoadThumbnails(path);
BeginLoadThumbnails();
if (_pdfHandle.PageCount < 2)
if (PdfDocumentWrapper.PdfDocument.PageCount < 2)
listThumbnails.Visibility = Visibility.Collapsed;
}
public void LoadPdf(MemoryStream stream)
{
_pdfStream = new MemoryStream();
stream.WriteTo(_pdfStream);
_pdfStream.Position = 0;
_pdfHandle = PdfDocument.Load(_pdfStream);
stream.Position = 0;
PdfDocumentWrapper = new PdfDocumentWrapper(stream);
_pdfLoaded = true;
BeginLoadThumbnails(stream);
BeginLoadThumbnails();
if (_pdfHandle.PageCount < 2)
if (PdfDocumentWrapper.PdfDocument.PageCount < 2)
listThumbnails.Visibility = Visibility.Collapsed;
}
private void BeginLoadThumbnails(string path, string password = null)
private void BeginLoadThumbnails()
{
new Task(() =>
{
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using (var handle = PdfDocument.Load(stream, password))
{
for (var p = 0; p < handle.PageCount; p++)
{
var bs = handle.RenderThumbnail(p);
Dispatcher.BeginInvoke(new Action(() => PageThumbnails.Add(bs)));
}
handle.Dispose();
}
}
}).Start();
}
private void BeginLoadThumbnails(MemoryStream stream, string password = null)
{
var localStream = new MemoryStream();
stream.WriteTo(localStream);
localStream.Position = 0;
new Task(() =>
{
using (var handle = PdfDocument.Load(localStream, password))
{
for (var p = 0; p < handle.PageCount; p++)
{
var bs = handle.RenderThumbnail(p);
Dispatcher.BeginInvoke(new Action(() => PageThumbnails.Add(bs)));
}
handle.Dispose();
}
}).Start();
Enumerable.Range(0, PdfDocumentWrapper.PdfDocument.PageCount).ForEach(PageThumbnails.Add);
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)

View File

@@ -81,6 +81,9 @@
<Compile Include="..\..\GitVersion.cs">
<Link>Properties\GitVersion.cs</Link>
</Compile>
<Compile Include="AsyncPageToThumbnailConverter.cs" />
<Compile Include="NotifyTaskCompletion.cs" />
<Compile Include="PdfDocumentWrapper.cs" />
<Compile Include="PdfViewerControl.xaml.cs">
<DependentUpon>PdfViewerControl.xaml</DependentUpon>
</Compile>
@@ -113,6 +116,9 @@
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\loading.png" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="PdfiumViewer" version="2.13.0.0" targetFramework="net462" />
<package id="PdfiumViewer.Native.x86.v8-xfa" version="2018.4.8.256" targetFramework="net462" />