PowerToys/src/modules/previewpane/PdfPreviewHandler/PdfPreviewHandlerControl.cs
R. de Veen 4177708e49
Enable PDF files in preview pane (#9088)
## 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
2021-08-26 16:43:26 -05:00

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;
}
}
}