/ src / modules / previewpane / PdfPreviewHandler / PdfPreviewHandlerControl.cs
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  }