PdfPreviewHandlerControl.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 using System; 5 using System.Drawing; 6 using System.IO; 7 using System.Runtime.InteropServices.ComTypes; 8 using System.Windows.Forms; 9 10 using Common; 11 using Common.Utilities; 12 using Microsoft.PowerToys.PreviewHandler.Pdf.Properties; 13 using Microsoft.PowerToys.PreviewHandler.Pdf.Telemetry.Events; 14 using Microsoft.PowerToys.Telemetry; 15 using Windows.Data.Pdf; 16 using Windows.Storage.Streams; 17 using Windows.UI.ViewManagement; 18 19 namespace Microsoft.PowerToys.PreviewHandler.Pdf 20 { 21 /// <summary> 22 /// Win Form Implementation for Pdf Preview Handler. 23 /// </summary> 24 public class PdfPreviewHandlerControl : FormHandlerControl 25 { 26 /// <summary> 27 /// RichTextBox control to display error message. 28 /// </summary> 29 private RichTextBox _infoBar; 30 31 /// <summary> 32 /// FlowLayoutPanel control to display the image of the pdf. 33 /// </summary> 34 private FlowLayoutPanel _flowLayoutPanel; 35 36 /// <summary> 37 /// Use UISettings to get system colors and scroll bar size. 38 /// </summary> 39 private static readonly UISettings _uISettings = new(); 40 41 /// <summary> 42 /// Initializes a new instance of the <see cref="PdfPreviewHandlerControl"/> class. 43 /// </summary> 44 public PdfPreviewHandlerControl() 45 { 46 SetBackgroundColor(GetBackgroundColor()); 47 } 48 49 /// <summary> 50 /// Start the preview on the Control. 51 /// </summary> 52 /// <param name="dataSource">Stream reference to access source file.</param> 53 public override void DoPreview<T>(T dataSource) 54 { 55 if (global::PowerToys.GPOWrapper.GPOWrapper.GetConfiguredPdfPreviewEnabledValue() == global::PowerToys.GPOWrapper.GpoRuleConfigured.Disabled) 56 { 57 // GPO is disabling this utility. Show an error message instead. 58 _infoBar = GetTextBoxControl(Resources.GpoDisabledErrorText); 59 Controls.Add(_infoBar); 60 base.DoPreview(dataSource); 61 62 return; 63 } 64 65 this.SuspendLayout(); 66 67 try 68 { 69 if (dataSource is not string filePath) 70 { 71 throw new ArgumentException($"{nameof(dataSource)} for {nameof(PdfPreviewHandlerControl)} must be a string but was a '{typeof(T)}'"); 72 } 73 74 using (var dataStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) 75 { 76 var memStream = new MemoryStream(); 77 dataStream.CopyTo(memStream); 78 memStream.Position = 0; 79 80 try 81 { 82 // AsRandomAccessStream() extension method from System.Runtime.WindowsRuntime 83 var pdf = PdfDocument.LoadFromStreamAsync(memStream.AsRandomAccessStream()).GetAwaiter().GetResult(); 84 85 if (pdf.PageCount > 0) 86 { 87 _flowLayoutPanel = new FlowLayoutPanel 88 { 89 AutoScroll = true, 90 AutoSize = true, 91 Dock = DockStyle.Fill, 92 FlowDirection = FlowDirection.TopDown, 93 WrapContents = false, 94 }; 95 _flowLayoutPanel.Resize += FlowLayoutPanel_Resize; 96 97 // Only show first 10 pages. 98 for (uint i = 0; i < pdf.PageCount && i < 10; i++) 99 { 100 using var page = pdf.GetPage(i); 101 var image = PageToImage(page); 102 103 var picturePanel = new Panel() 104 { 105 Name = "picturePanel", 106 Margin = new Padding(6, 6, 6, 0), 107 Size = CalculateSize(image), 108 BorderStyle = BorderStyle.FixedSingle, 109 }; 110 111 var picture = new PictureBox 112 { 113 Dock = DockStyle.Fill, 114 Image = image, 115 SizeMode = PictureBoxSizeMode.Zoom, 116 }; 117 118 picturePanel.Controls.Add(picture); 119 _flowLayoutPanel.Controls.Add(picturePanel); 120 } 121 122 if (pdf.PageCount > 10) 123 { 124 var messageBox = new RichTextBox 125 { 126 Name = "messageBox", 127 Text = Resources.PdfMorePagesMessage, 128 BackColor = Color.LightYellow, 129 Dock = DockStyle.Fill, 130 Multiline = true, 131 ReadOnly = true, 132 ScrollBars = RichTextBoxScrollBars.None, 133 BorderStyle = BorderStyle.None, 134 }; 135 messageBox.ContentsResized += RTBContentsResized; 136 137 _flowLayoutPanel.Controls.Add(messageBox); 138 } 139 140 Controls.Add(_flowLayoutPanel); 141 } 142 } 143 catch (Exception ex) 144 { 145 if (ex.Message.Contains("Unable to update the password. The value provided as the current password is incorrect.", StringComparison.Ordinal)) 146 { 147 Controls.Clear(); 148 _infoBar = GetTextBoxControl(Resources.PdfPasswordProtectedError); 149 Controls.Add(_infoBar); 150 } 151 else 152 { 153 throw; 154 } 155 } 156 finally 157 { 158 memStream.Dispose(); 159 } 160 } 161 162 try 163 { 164 PowerToysTelemetry.Log.WriteEvent(new PdfFilePreviewed()); 165 } 166 catch 167 { // Should not crash if sending telemetry is failing. Ignore the exception. 168 } 169 } 170 catch (Exception ex) 171 { 172 try 173 { 174 PowerToysTelemetry.Log.WriteEvent(new PdfFilePreviewError { Message = ex.Message }); 175 } 176 catch 177 { // Should not crash if sending telemetry is failing. Ignore the exception. 178 } 179 180 Controls.Clear(); 181 _infoBar = GetTextBoxControl(Resources.PdfNotPreviewedError); 182 Controls.Add(_infoBar); 183 } 184 finally 185 { 186 base.DoPreview(dataSource); 187 } 188 189 this.ResumeLayout(false); 190 this.PerformLayout(); 191 } 192 193 /// <summary> 194 /// Resize the Panels on FlowLayoutPanel resize based on the size of the image. 195 /// </summary> 196 /// <param name="sender">sender (not used)</param> 197 /// <param name="e">args (not used)</param> 198 private void FlowLayoutPanel_Resize(object sender, EventArgs e) 199 { 200 this.SuspendLayout(); 201 _flowLayoutPanel.SuspendLayout(); 202 203 foreach (Panel panel in _flowLayoutPanel.Controls.Find("picturePanel", false)) 204 { 205 var pictureBox = panel.Controls[0] as PictureBox; 206 var image = pictureBox.Image; 207 208 panel.Size = CalculateSize(image); 209 } 210 211 _flowLayoutPanel.ResumeLayout(false); 212 this.ResumeLayout(false); 213 } 214 215 /// <summary> 216 /// Transform the PdfPage to an Image. 217 /// </summary> 218 /// <param name="page">The page to transform to an Image.</param> 219 /// <returns>An object of type <see cref="Image"/></returns> 220 private Image PageToImage(PdfPage page) 221 { 222 Image imageOfPage = null; 223 224 using (var stream = new InMemoryRandomAccessStream()) 225 { 226 page.RenderToStreamAsync(stream, new PdfPageRenderOptions() 227 { 228 DestinationWidth = (uint)this.ClientSize.Width, 229 }).GetAwaiter().GetResult(); 230 231 stream.Seek(0); // Reset the stream position to the beginning before reading. 232 233 imageOfPage = Image.FromStream(stream.AsStream()); 234 } 235 236 return imageOfPage; 237 } 238 239 /// <summary> 240 /// Calculate the size of the control based on the size of the image/pdf page. 241 /// </summary> 242 /// <param name="pdfImage">Image of pdf page.</param> 243 /// <returns>New size off the panel.</returns> 244 private Size CalculateSize(Image pdfImage) 245 { 246 var hasScrollBar = _flowLayoutPanel.VerticalScroll.Visible; 247 248 // Add 12px margin to the image by making it 12px smaller. 249 int width = this.ClientSize.Width - 12; 250 251 // If the vertical scroll bar is visible, make the image smaller. 252 var scrollBarSizeWidth = (int)_uISettings.ScrollBarSize.Width; 253 if (hasScrollBar && width > scrollBarSizeWidth) 254 { 255 width -= scrollBarSizeWidth; 256 } 257 258 int originalWidth = pdfImage.Width; 259 int originalHeight = pdfImage.Height; 260 float percentWidth = (float)width / originalWidth; 261 262 int newHeight = (int)(originalHeight * percentWidth); 263 264 return new Size(width, newHeight); 265 } 266 267 /// <summary> 268 /// Get the system background color, based on the selected theme. 269 /// </summary> 270 /// <returns>An object of type <see cref="Color"/>.</returns> 271 private static Color GetBackgroundColor() 272 { 273 var systemBackgroundColor = _uISettings.GetColorValue(UIColorType.Background); 274 275 return Color.FromArgb(systemBackgroundColor.A, systemBackgroundColor.R, systemBackgroundColor.G, systemBackgroundColor.B); 276 } 277 278 /// <summary> 279 /// Gets a textbox control. 280 /// </summary> 281 /// <param name="message">Message to be displayed in textbox.</param> 282 /// <returns>An object of type <see cref="RichTextBox"/>.</returns> 283 private RichTextBox GetTextBoxControl(string message) 284 { 285 var textBox = new RichTextBox 286 { 287 Text = message, 288 BackColor = Color.LightYellow, 289 Multiline = true, 290 Dock = DockStyle.Top, 291 ReadOnly = true, 292 ScrollBars = RichTextBoxScrollBars.None, 293 BorderStyle = BorderStyle.None, 294 }; 295 textBox.ContentsResized += RTBContentsResized; 296 297 return textBox; 298 } 299 300 /// <summary> 301 /// Callback when RichTextBox is resized. 302 /// </summary> 303 /// <param name="sender">Reference to resized control.</param> 304 /// <param name="e">Provides data for the resize event.</param> 305 private void RTBContentsResized(object sender, ContentsResizedEventArgs e) 306 { 307 var richTextBox = (RichTextBox)sender; 308 309 // Add 5px extra height to the textbox. 310 richTextBox.Height = e.NewRectangle.Height + 5; 311 } 312 } 313 }