mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-06-08 01:52:52 +08:00

## Summary of the Pull Request This PR enables user to preview PDF files in the Explorer preview pane and in Outlook. **What is this about:** Windows does not support out of the box experience for previewing PDF files in the preview pane. Users need to install third-party software like Adobe Acrobat reader. The PdfPreviewHandler module enbales the user to preview PDF files. **How does someone test / validate:** Run the installer, open Explorer and select a PDF file, enable the preview pane. Maybe need to remove third-party PDF software. ## Quality Checklist - [X] **Linked issue:** #3548 - [ ] **Communication:** I've discussed this with core contributors in the issue. - [X] **Tests:** Added/updated and all pass - [X] **Installer:** Added/updated and all pass - [X] **Localization:** All end user facing strings can be localized - [ ] **Docs:** Added/ updated - [x] **Binaries:** Any new files are added to WXS / YML - [ ] No new binaries - [x] YML for signing - [x] WXS for installer
300 lines
12 KiB
C#
300 lines
12 KiB
C#
// Copyright (c) Microsoft Corporation
|
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
|
// See the LICENSE file in the project root for more information.
|
|
using System;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices.ComTypes;
|
|
using System.Windows.Forms;
|
|
|
|
using Common;
|
|
using Common.Utilities;
|
|
using Microsoft.PowerToys.PreviewHandler.Pdf.Properties;
|
|
using Microsoft.PowerToys.PreviewHandler.Pdf.Telemetry.Events;
|
|
using Microsoft.PowerToys.Telemetry;
|
|
using Windows.Data.Pdf;
|
|
using Windows.Storage.Streams;
|
|
using Windows.UI.ViewManagement;
|
|
|
|
namespace Microsoft.PowerToys.PreviewHandler.Pdf
|
|
{
|
|
/// <summary>
|
|
/// Win Form Implementation for Pdf Preview Handler.
|
|
/// </summary>
|
|
public class PdfPreviewHandlerControl : FormHandlerControl
|
|
{
|
|
/// <summary>
|
|
/// RichTextBox control to display error message.
|
|
/// </summary>
|
|
private RichTextBox _infoBar;
|
|
|
|
/// <summary>
|
|
/// FlowLayoutPanel control to display the image of the pdf.
|
|
/// </summary>
|
|
private FlowLayoutPanel _flowLayoutPanel;
|
|
|
|
/// <summary>
|
|
/// Use UISettings to get system colors and scroll bar size.
|
|
/// </summary>
|
|
private static UISettings _uISettings = new UISettings();
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="PdfPreviewHandlerControl"/> class.
|
|
/// </summary>
|
|
public PdfPreviewHandlerControl()
|
|
{
|
|
SetBackgroundColor(GetBackgroundColor());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start the preview on the Control.
|
|
/// </summary>
|
|
/// <param name="dataSource">Stream reference to access source file.</param>
|
|
public override void DoPreview<T>(T dataSource)
|
|
{
|
|
this.SuspendLayout();
|
|
|
|
try
|
|
{
|
|
using (var dataStream = new ReadonlyStream(dataSource as IStream))
|
|
{
|
|
var memStream = new MemoryStream();
|
|
dataStream.CopyTo(memStream);
|
|
memStream.Position = 0;
|
|
|
|
try
|
|
{
|
|
// AsRandomAccessStream() extension method from System.Runtime.WindowsRuntime
|
|
var pdf = PdfDocument.LoadFromStreamAsync(memStream.AsRandomAccessStream()).GetAwaiter().GetResult();
|
|
|
|
if (pdf.PageCount > 0)
|
|
{
|
|
InvokeOnControlThread(() =>
|
|
{
|
|
_flowLayoutPanel = new FlowLayoutPanel
|
|
{
|
|
AutoScroll = true,
|
|
AutoSize = true,
|
|
Dock = DockStyle.Fill,
|
|
FlowDirection = FlowDirection.TopDown,
|
|
WrapContents = false,
|
|
};
|
|
_flowLayoutPanel.Resize += FlowLayoutPanel_Resize;
|
|
|
|
// Only show first 10 pages.
|
|
for (uint i = 0; i < pdf.PageCount && i < 10; i++)
|
|
{
|
|
using (var page = pdf.GetPage(i))
|
|
{
|
|
var image = PageToImage(page);
|
|
|
|
var picturePanel = new Panel()
|
|
{
|
|
Name = "picturePanel",
|
|
Margin = new Padding(6, 6, 6, 0),
|
|
Size = CalculateSize(image),
|
|
BorderStyle = BorderStyle.FixedSingle,
|
|
};
|
|
|
|
var picture = new PictureBox
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
Image = image,
|
|
SizeMode = PictureBoxSizeMode.Zoom,
|
|
};
|
|
|
|
picturePanel.Controls.Add(picture);
|
|
_flowLayoutPanel.Controls.Add(picturePanel);
|
|
}
|
|
}
|
|
|
|
if (pdf.PageCount > 10)
|
|
{
|
|
var messageBox = new RichTextBox
|
|
{
|
|
Name = "messageBox",
|
|
Text = Resources.PdfMorePagesMessage,
|
|
BackColor = Color.LightYellow,
|
|
Dock = DockStyle.Fill,
|
|
Multiline = true,
|
|
ReadOnly = true,
|
|
ScrollBars = RichTextBoxScrollBars.None,
|
|
BorderStyle = BorderStyle.None,
|
|
};
|
|
messageBox.ContentsResized += RTBContentsResized;
|
|
|
|
_flowLayoutPanel.Controls.Add(messageBox);
|
|
}
|
|
|
|
Controls.Add(_flowLayoutPanel);
|
|
});
|
|
}
|
|
}
|
|
#pragma warning disable CA1031 // Password protected files throws an generic Exception
|
|
catch (Exception ex)
|
|
#pragma warning restore CA1031
|
|
{
|
|
if (ex.Message.Contains("Unable to update the password. The value provided as the current password is incorrect.", StringComparison.Ordinal))
|
|
{
|
|
InvokeOnControlThread(() =>
|
|
{
|
|
Controls.Clear();
|
|
_infoBar = GetTextBoxControl(Resources.PdfPasswordProtectedError);
|
|
Controls.Add(_infoBar);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
memStream.Dispose();
|
|
}
|
|
}
|
|
|
|
PowerToysTelemetry.Log.WriteEvent(new PdfFilePreviewed());
|
|
}
|
|
#pragma warning disable CA1031 // Do not catch general exception types
|
|
catch (Exception ex)
|
|
#pragma warning restore CA1031 // Do not catch general exception types
|
|
{
|
|
PowerToysTelemetry.Log.WriteEvent(new PdfFilePreviewError { Message = ex.Message });
|
|
|
|
InvokeOnControlThread(() =>
|
|
{
|
|
Controls.Clear();
|
|
_infoBar = GetTextBoxControl(Resources.PdfNotPreviewedError);
|
|
Controls.Add(_infoBar);
|
|
});
|
|
}
|
|
finally
|
|
{
|
|
base.DoPreview(dataSource);
|
|
}
|
|
|
|
this.ResumeLayout(false);
|
|
this.PerformLayout();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resize the Panels on FlowLayoutPanel resize based on the size of the image.
|
|
/// </summary>
|
|
/// <param name="sender">sender (not used)</param>
|
|
/// <param name="e">args (not used)</param>
|
|
private void FlowLayoutPanel_Resize(object sender, EventArgs e)
|
|
{
|
|
this.SuspendLayout();
|
|
_flowLayoutPanel.SuspendLayout();
|
|
|
|
foreach (Panel panel in _flowLayoutPanel.Controls.Find("picturePanel", false))
|
|
{
|
|
var pictureBox = panel.Controls[0] as PictureBox;
|
|
var image = pictureBox.Image;
|
|
|
|
panel.Size = CalculateSize(image);
|
|
}
|
|
|
|
_flowLayoutPanel.ResumeLayout(false);
|
|
this.ResumeLayout(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Transform the PdfPage to an Image.
|
|
/// </summary>
|
|
/// <param name="page">The page to transform to an Image.</param>
|
|
/// <returns>An object of type <see cref="Image"/></returns>
|
|
private Image PageToImage(PdfPage page)
|
|
{
|
|
Image imageOfPage;
|
|
|
|
using (var stream = new InMemoryRandomAccessStream())
|
|
{
|
|
page.RenderToStreamAsync(stream, new PdfPageRenderOptions()
|
|
{
|
|
DestinationWidth = (uint)this.ClientSize.Width,
|
|
}).GetAwaiter().GetResult();
|
|
|
|
imageOfPage = Image.FromStream(stream.AsStream());
|
|
}
|
|
|
|
return imageOfPage;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate the size of the control based on the size of the image/pdf page.
|
|
/// </summary>
|
|
/// <param name="pdfImage">Image of pdf page.</param>
|
|
/// <returns>New size off the panel.</returns>
|
|
private Size CalculateSize(Image pdfImage)
|
|
{
|
|
var hasScrollBar = _flowLayoutPanel.VerticalScroll.Visible;
|
|
|
|
// Add 12px margin to the image by making it 12px smaller.
|
|
int width = this.ClientSize.Width - 12;
|
|
|
|
// If the vertical scroll bar is visible, make the image smaller.
|
|
var scrollBarSizeWidth = (int)_uISettings.ScrollBarSize.Width;
|
|
if (hasScrollBar && width > scrollBarSizeWidth)
|
|
{
|
|
width -= scrollBarSizeWidth;
|
|
}
|
|
|
|
int originalWidth = pdfImage.Width;
|
|
int originalHeight = pdfImage.Height;
|
|
float percentWidth = (float)width / originalWidth;
|
|
|
|
int newHeight = (int)(originalHeight * percentWidth);
|
|
|
|
return new Size(width, newHeight);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the system background color, based on the selected theme.
|
|
/// </summary>
|
|
/// <returns>An object of type <see cref="Color"/>.</returns>
|
|
private static Color GetBackgroundColor()
|
|
{
|
|
var systemBackgroundColor = _uISettings.GetColorValue(UIColorType.Background);
|
|
|
|
return Color.FromArgb(systemBackgroundColor.A, systemBackgroundColor.R, systemBackgroundColor.G, systemBackgroundColor.B);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a textbox control.
|
|
/// </summary>
|
|
/// <param name="message">Message to be displayed in textbox.</param>
|
|
/// <returns>An object of type <see cref="RichTextBox"/>.</returns>
|
|
private RichTextBox GetTextBoxControl(string message)
|
|
{
|
|
var textBox = new RichTextBox
|
|
{
|
|
Text = message,
|
|
BackColor = Color.LightYellow,
|
|
Multiline = true,
|
|
Dock = DockStyle.Top,
|
|
ReadOnly = true,
|
|
ScrollBars = RichTextBoxScrollBars.None,
|
|
BorderStyle = BorderStyle.None,
|
|
};
|
|
textBox.ContentsResized += RTBContentsResized;
|
|
|
|
return textBox;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Callback when RichTextBox is resized.
|
|
/// </summary>
|
|
/// <param name="sender">Reference to resized control.</param>
|
|
/// <param name="e">Provides data for the resize event.</param>
|
|
private void RTBContentsResized(object sender, ContentsResizedEventArgs e)
|
|
{
|
|
var richTextBox = (RichTextBox)sender;
|
|
|
|
// Add 5px extra height to the textbox.
|
|
richTextBox.Height = e.NewRectangle.Height + 5;
|
|
}
|
|
}
|
|
}
|