diff --git a/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/CsvViewerPanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/CsvViewerPanel.xaml.cs
index 26e289cf2..1bf514988 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/CsvViewerPanel.xaml.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.CsvViewer/CsvViewerPanel.xaml.cs
@@ -69,10 +69,15 @@ public void LoadFile(string path)
searchPanel.SetMatchCount(0, 0);
searchPanel.Visibility = Visibility.Collapsed;
- var encoding = CharsetDetector.DetectFromFile(path).Detected?.Encoding ??
+ var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+
+ // Detect from a sample: only the first rows are shown, and scanning a whole large file
+ // here blocks the UI thread (and switching to the next file) for seconds on slow disks.
+ var encoding = CharsetDetector.DetectFromStream(stream, 1024 * 1024).Detected?.Encoding ??
Encoding.Default;
+ stream.Position = 0;
- using var sr = new StreamReader(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), encoding);
+ using var sr = new StreamReader(stream, encoding);
// Use fixed delimiters for known extensions to avoid mis-detection on small samples.
var extension = Path.GetExtension(path);
diff --git a/QuickLook.Plugin/QuickLook.Plugin.PDFViewer/PdfDocumentWrapper.cs b/QuickLook.Plugin/QuickLook.Plugin.PDFViewer/PdfDocumentWrapper.cs
index e1883d734..51fe9ecfa 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.PDFViewer/PdfDocumentWrapper.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.PDFViewer/PdfDocumentWrapper.cs
@@ -17,6 +17,7 @@
using System;
using System.IO;
+using System.Threading;
using PdfiumViewer;
namespace QuickLook.Plugin.PDFViewer;
@@ -24,11 +25,35 @@ namespace QuickLook.Plugin.PDFViewer;
public class PdfDocumentWrapper : IDisposable
{
public PdfDocumentWrapper(Stream stream, string password = null)
+ : this(stream, password, CancellationToken.None)
{
- PdfStream = new MemoryStream((int)stream.Length);
- stream.CopyTo(PdfStream);
+ }
+
+ ///
+ /// Copies the document into memory in chunks so a pending load can be abandoned
+ /// (e.g. when the user moves to another file) without reading the rest of the file.
+ ///
+ public PdfDocumentWrapper(Stream stream, string password, CancellationToken cancellationToken)
+ {
+ PdfStream = new MemoryStream(stream.Length <= int.MaxValue ? (int)stream.Length : 0);
+ try
+ {
+ var buffer = new byte[1024 * 1024];
+ int read;
+ while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ PdfStream.Write(buffer, 0, read);
+ }
- PdfDocument = PdfDocument.Load(PdfStream, password);
+ cancellationToken.ThrowIfCancellationRequested();
+ PdfDocument = PdfDocument.Load(PdfStream, password);
+ }
+ catch
+ {
+ PdfStream.Dispose();
+ throw;
+ }
}
public PdfDocument PdfDocument { get; private set; }
diff --git a/QuickLook.Plugin/QuickLook.Plugin.PDFViewer/PdfViewerControl.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.PDFViewer/PdfViewerControl.xaml.cs
index f684547a9..7a3a9f755 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.PDFViewer/PdfViewerControl.xaml.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.PDFViewer/PdfViewerControl.xaml.cs
@@ -212,31 +212,36 @@ private void UpdatePageViewWhenSelectionChanged(object sender, SelectionChangedE
public static Size GetDesiredControlSizeByFirstPage(string path, string password = null)
{
- Size size;
+ using var s = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ using var tempHandle = PdfDocument.Load(s, password);
- using (var s = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ return GetDesiredControlSize(tempHandle);
+ }
+
+ public static Size GetDesiredControlSize(PdfDocument document)
+ {
+ var size = new Size(0, 0);
+ document.PageSizes.Take(5).ForEach(p =>
{
- using (var tempHandle = PdfDocument.Load(s, password))
- {
- size = new Size(0, 0);
- tempHandle.PageSizes.Take(5).ForEach(p =>
- {
- size.Width = Math.Max(size.Width, p.Width);
- size.Height = Math.Max(size.Height, p.Height);
- });
-
- if (tempHandle.PageCount > 1)
- size.Width += /*listThumbnails.ActualWidth*/ 150;
- }
- }
+ size.Width = Math.Max(size.Width, p.Width);
+ size.Height = Math.Max(size.Height, p.Height);
+ });
+
+ if (document.PageCount > 1)
+ size.Width += /*listThumbnails.ActualWidth*/ 150;
return new Size(size.Width * 3, size.Height * 3);
}
public void LoadPdf(string path, string password = null)
{
- var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
- PdfDocumentWrapper = new PdfDocumentWrapper(stream, password);
+ using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ LoadPdf(new PdfDocumentWrapper(stream, password));
+ }
+
+ public void LoadPdf(PdfDocumentWrapper document)
+ {
+ PdfDocumentWrapper = document;
_pdfLoaded = true;
if (PdfDocumentWrapper.PdfDocument.PageCount < 2)
diff --git a/QuickLook.Plugin/QuickLook.Plugin.PDFViewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.PDFViewer/Plugin.cs
index 54733c2d7..d0584ed43 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.PDFViewer/Plugin.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.PDFViewer/Plugin.cs
@@ -22,7 +22,10 @@
using System.IO;
using System.Reflection;
using System.Runtime.ExceptionServices;
+using System.Threading;
+using System.Threading.Tasks;
using System.Windows;
+using System.Windows.Controls;
using System.Windows.Threading;
namespace QuickLook.Plugin.PDFViewer;
@@ -33,6 +36,11 @@ public sealed class Plugin : IViewer
private string _path;
private PdfViewerControl _pdfControl;
private PasswordControl _passwordControl;
+ private CancellationTokenSource _loadCts;
+ private Task _loadTask;
+
+ /// How long Prepare() waits for the document to get the exact window size.
+ private const int PrepareWaitMilliseconds = 200;
public int Priority => -1;
@@ -62,14 +70,31 @@ public void Prepare(string path, ContextObject context)
_context = context;
_path = path;
+ // Load on a background thread: a large PDF takes seconds to read, and blocking the UI
+ // thread here would also block switching to the next file until it finishes.
+ _loadCts = new CancellationTokenSource();
+ var token = _loadCts.Token;
+ _loadTask = Task.Run(() =>
+ {
+ using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ return new PdfDocumentWrapper(stream, null, token);
+ }, token);
+
try
{
- var desiredSize = PdfViewerControl.GetDesiredControlSizeByFirstPage(path);
- context.SetPreferredSizeFit(desiredSize, 0.9);
+ // Typical documents are ready within this wait, so the window still gets the page size.
+ if (_loadTask.Wait(PrepareWaitMilliseconds))
+ {
+ context.SetPreferredSizeFit(PdfViewerControl.GetDesiredControlSize(_loadTask.Result.PdfDocument), 0.9);
+ return;
+ }
+
+ // Still loading: assume an A4 portrait page
+ context.SetPreferredSizeFit(new Size(595 * 3, 842 * 3), 0.9);
}
- catch (PdfException ex) when (ex.Message == "Password required or incorrect password")
+ catch (AggregateException)
{
- // Fallback to a size to request the password
+ // Fallback to a size to request the password, or to show the error (handled in View)
context.PreferredSize = new Size { Width = 800, Height = 600 };
}
}
@@ -79,85 +104,135 @@ public void View(string path, ContextObject context)
_pdfControl = new PdfViewerControl();
context.ViewerContent = _pdfControl;
- Exception exception = null;
+ var control = _pdfControl;
+ var loadTask = _loadTask;
- _ = _pdfControl.Dispatcher.BeginInvoke(() =>
+ if (loadTask.IsCompleted)
{
- try
- {
- _pdfControl.LoadPdf(path);
+ // Keep the original behaviour: let the control be loaded first, and rethrow load
+ // errors so QuickLook falls back to another viewer.
+ Exception exception = null;
+ _ = control.Dispatcher.BeginInvoke(() => exception = OnDocumentLoaded(loadTask, control, path, context),
+ DispatcherPriority.Loaded).Wait();
+
+ if (exception != null)
+ ExceptionDispatchInfo.Capture(exception).Throw();
+ return;
+ }
- context.Title = $"1 / {_pdfControl.TotalPages}: {Path.GetFileName(path)}";
+ loadTask.ContinueWith(_ => control.Dispatcher.BeginInvoke(() =>
+ {
+ // Superseded by another file meanwhile: Cleanup() releases the document
+ if (_pdfControl != control)
+ return;
- _pdfControl.CurrentPageChanged += UpdateWindowCaption;
+ var exception = OnDocumentLoaded(loadTask, control, path, context);
+ if (exception != null)
+ {
+ context.ViewerContent = new TextBlock
+ {
+ Text = exception.Message,
+ TextWrapping = TextWrapping.Wrap,
+ Margin = new Thickness(24),
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ context.Title = Path.GetFileName(path);
context.IsBusy = false;
}
- catch (PdfException ex) when (ex.Message == "Password required or incorrect password")
+ }, DispatcherPriority.Loaded), TaskScheduler.Default);
+ }
+
+ ///
+ /// Shows the loaded document, or the password prompt. Returns any other load error.
+ ///
+ private Exception OnDocumentLoaded(Task loadTask, PdfViewerControl control, string path, ContextObject context)
+ {
+ try
+ {
+ control.LoadPdf(loadTask.GetAwaiter().GetResult());
+
+ context.Title = $"1 / {control.TotalPages}: {Path.GetFileName(path)}";
+
+ control.CurrentPageChanged += UpdateWindowCaption;
+ context.IsBusy = false;
+ }
+ catch (PdfException ex) when (ex.Message == "Password required or incorrect password")
+ {
+ // Fallback to request a password
+ _passwordControl = new PasswordControl();
+ _passwordControl.PasswordRequested += (string password) =>
{
- // Fallback to request a password
- _passwordControl = new PasswordControl();
- _passwordControl.PasswordRequested += (string password) =>
+ try
{
- try
+ var desiredSize = PdfViewerControl.GetDesiredControlSizeByFirstPage(path, password);
+ context.SetPreferredSizeFit(desiredSize, 0.9); // Actually it is no longer effective here
+ context.ViewerContent = _pdfControl;
+
+ // Call the viewer window private method using reflection
+ // QuickLook.ViewerWindow.ResizeAndCentreExistingWindow
+ if (Window.GetWindow(_pdfControl) is Window window)
{
- var desiredSize = PdfViewerControl.GetDesiredControlSizeByFirstPage(path, password);
- context.SetPreferredSizeFit(desiredSize, 0.9); // Actually it is no longer effective here
- context.ViewerContent = _pdfControl;
+ var ResizeAndCentreExistingWindow = window.GetType().GetMethod("ResizeAndCentreExistingWindow",
+ BindingFlags.NonPublic | BindingFlags.Instance);
- // Call the viewer window private method using reflection
- // QuickLook.ViewerWindow.ResizeAndCentreExistingWindow
- if (Window.GetWindow(_pdfControl) is Window window)
+ if (ResizeAndCentreExistingWindow != null)
{
- var ResizeAndCentreExistingWindow = window.GetType().GetMethod("ResizeAndCentreExistingWindow",
- BindingFlags.NonPublic | BindingFlags.Instance);
+ var newRect = (Rect)ResizeAndCentreExistingWindow.Invoke(window, [context.PreferredSize]);
- if (ResizeAndCentreExistingWindow != null)
- {
- var newRect = (Rect)ResizeAndCentreExistingWindow.Invoke(window, [context.PreferredSize]);
-
- window.MoveWindow(newRect.Left, newRect.Top, newRect.Width, newRect.Height);
- }
+ window.MoveWindow(newRect.Left, newRect.Top, newRect.Width, newRect.Height);
}
+ }
- context.IsBusy = true;
- _pdfControl.LoadPdf(path, password);
+ context.IsBusy = true;
+ _pdfControl.LoadPdf(path, password);
- context.Title = $"1 / {_pdfControl.TotalPages}: {Path.GetFileName(path)}";
+ context.Title = $"1 / {_pdfControl.TotalPages}: {Path.GetFileName(path)}";
- _pdfControl.CurrentPageChanged += UpdateWindowCaption;
- context.IsBusy = false;
- }
- catch (PdfException ex) when (ex.Message == "Password required or incorrect password")
- {
- // This password is not accepted
- return false;
- }
+ _pdfControl.CurrentPageChanged += UpdateWindowCaption;
+ context.IsBusy = false;
+ }
+ catch (PdfException ex) when (ex.Message == "Password required or incorrect password")
+ {
+ // This password is not accepted
+ return false;
+ }
- // This password is accepted
- return true;
- };
+ // This password is accepted
+ return true;
+ };
- context.ViewerContent = _passwordControl;
- context.Title = $"[PASSWORD PROTECTED] {Path.GetFileName(path)}";
- context.IsBusy = false;
- }
- catch (Exception ex)
- {
- exception = ex;
- }
- }, DispatcherPriority.Loaded).Wait();
+ context.ViewerContent = _passwordControl;
+ context.Title = $"[PASSWORD PROTECTED] {Path.GetFileName(path)}";
+ context.IsBusy = false;
+ }
+ catch (Exception ex)
+ {
+ return ex;
+ }
- if (exception != null)
- ExceptionDispatchInfo.Capture(exception).Throw();
+ return null;
}
public void Cleanup()
{
GC.SuppressFinalize(this);
+ // Stop reading the file if it is still loading
+ _loadCts?.Cancel();
+
+ var adopted = _pdfControl?.PdfDocumentWrapper;
_pdfControl?.Dispose();
_pdfControl = null;
+ // Release a document that finished loading but was never shown
+ _loadTask?.ContinueWith(t =>
+ {
+ if (t.Status == TaskStatus.RanToCompletion && !ReferenceEquals(t.Result, adopted))
+ t.Result.Dispose();
+ }, TaskScheduler.Default);
+ _loadTask = null;
+
_context = null;
}