Draw interactively on a PDF page

Manipulate PDF, Shapes, UI, View
4/8/2014

Downloads

This code sample allows users to interactively draw lines on PDF pages and save the resulting document.

Running the sample.

To run the Draw.zip sample, please download and unzip it and open the project file in Visual Studio. After starting the application, you can open a PDF file via the file menu. Then, in the same menu click on "Toggle Pencil Mode". This will allow you to draw lines on the shown PDF page. While drawing you will not be able to navigate to other pages. When you click on "Toggle Pencil Mode" again, the lines will become slightly thinner. At that point they are part of the page, and if you save the document the lines will be saved as well.

PictureBox

In principle, one can draw anything on a PDF page by means of shapes. Shapes however, are somewhat inefficient when drawing interactively. This is because each shape needs to be converted into actual PDF graphics before it can be shown to the user, and this is relatively costly. The sample below will use a WinForms PictureBox to perform the interactive drawing, and when the user is done, it will convert this into PDF shapes.

This is implemented via a special PagesViewer instance that defines a PencilMode. When the PencilMode is set to true, the PictureBox becomes active and the user can draw lines. When the PencilMode is set to false, the lines in the PictureBox are converted to PDF shapes.

When the PictureBox gets activated, a number of things happen:

  • If no PictureBox exists yet, it will be created and added to the parent.
  • Events handlers are installed for the MouseDown, MouseMove, MouseUp and Paint event. These handlers implement the actual drawing. We will discuss these later
  • The PictureBox gets a copy of the PDF page image.
  • The PictureBox is made visible and put to the front. The PagesViewer on the other hand is made invisible and disabled. The latter makes sure that the PagesViewer stops handling events while the user is Painting. Amongst others this makes sure that the same page stays in view.

When the PictureBox gets deactivated the following happens:

  • The lines that were drawn in the PictureBox get converted to PDF shapes. This is done in the WriteToPDF method.
  • The PictureBox is made invisible and sent to the back. The PagesViewer is made visible, and enabled, so that it handles events again.
internal class StandardPagesViewerWithPencilTool : 
                       TallComponents.Interaction.WinForms.Controls.StandardPagesViewer
{
  public bool PencilMode
  {
    get { return _pencilMode; }
    set
    {
       _pencilMode = value;

       if (value)
       {
         if (_pictureBox == null)
         {
            _pictureBox = new PictureBox();
            (_pictureBox as ISupportInitialize).BeginInit();
            _pencilCursor = new Cursor(Properties.Resources.PencilTool.Handle);
            _pictureBox.Cursor = _pencilCursor;
            _pictureBox.Location = Location;
            _pictureBox.Name = "picPencil";
            _pictureBox.BorderStyle = BorderStyle.None;
            _pictureBox.TabStop = false;
            _pictureBox.BackColor = System.Drawing.Color.White;

            _pictureBox.MouseDown += picPencil_MouseDown;
            _pictureBox.MouseUp += picPencil_MouseUp;
            _pictureBox.MouseMove += picPencil_MouseMove;
            _pictureBox.Paint += picPencil_Paint;

            (_pictureBox as ISupportInitialize).EndInit();

            Parent.Controls.Add(_pictureBox);
          }
          _paintedPaths = new List<GraphicsPath>();

          Bitmap bmp = new Bitmap(ClientSize.Width, ClientSize.Height);
          DrawToBitmap(bmp, ClientRectangle);

          _pictureBox.Image = bmp;
          _pictureBox.Size = ClientSize;
          _pictureBox.Visible = true;
          _pictureBox.BringToFront();
        }
        else
        {
          if (_pictureBox == null)
            return;

          WriteToPDF();

          _pictureBox.Visible = false;
          _pictureBox.SendToBack();
        }

        // While drawing, do not enable PagesViewer, nor redrawing of the PagesViewer.

        Enabled = !value;
        Visible = !value;
      }
    }
  }
}

## Interactive Drawing

The actual drawing is implemented via a number of handlers that are defined on the PictureBox instance. The lines that are drawn are maintained in the _paintedPaths collection. This a collection of GraphicsPath instances.

The paint handler simply draws all paths that are present in the _paintedPaths collection. The mouse handlers add new lines to the collection and they call Invalidate so that the new lines are drawn by the paint handler.

``` csharp
private void picPencil_Paint(object sender, PaintEventArgs e)
{
  try
  {
    using (Pen p = new Pen(System.Drawing.Color.Black, 3))
    {
      foreach (GraphicsPath gp in _paintedPaths)
      {
        e.Graphics.DrawPath(p, gp);
      }
    }
  }
  catch
  {
    /* Ignore errors during painting. */
  }
}

private void picPencil_MouseDown(object sender, MouseEventArgs e)
{
  if (!PencilMode)
    return;

  if (e.Button != MouseButtons.Left)
    return;

  _paintedPaths.Add(new GraphicsPath(FillMode.Winding));
  _paintedPaths[_paintedPaths.Count - 1].StartFigure();
  _paintedPaths[_paintedPaths.Count - 1].AddLine(e.X, e.Y, e.X, e.Y);
}

private void picPencil_MouseMove(object sender, MouseEventArgs e)
{
  if (!PencilMode)
    return;

  if (e.Button != MouseButtons.Left)
    return;

  _paintedPaths[_paintedPaths.Count - 1].AddLine(e.X, e.Y, e.X, e.Y);
  _pictureBox.Invalidate();
}     

private void picPencil_MouseUp(object sender, MouseEventArgs e)
{
  if (!PencilMode)
    return;

  if (e.Button != MouseButtons.Left)
    return;

  _paintedPaths[_paintedPaths.Count - 1].AddLine(e.X, e.Y, e.X, e.Y);
}

Creating shapes

When the user turns off the PencilMode, the WriteToPDF method gets called. This method is shown below. The sample basically connects all shapes to be page that drawing started in. Basically, each path in the _paintedPaths collection gets converted to a FreeHandShape, and each line in a path gets converted to a FreeHandLineSegment.

For each such a line segment we need to transform the PictureBox coordinates to PDF page coordinates. This is needed because in WinForms the coordinate origin of each graphical element is at its top left, while in PDF it is at the bottom left of each page.

We add each FreeHandShape to the Overlay of the page, and finally, we invalidate the Overlay. The latter is needed to tell PDFControls.NET to redraw the Overlay.

private void WriteToPDF()
{
  if (_paintedPaths.Count <= 0)
    return;

  try
  {
    Application.UseWaitCursor = true;
    Application.DoEvents();
    Parent.Enabled = false;

    // We try to find the page that drawing started in.
    MatrixTransform docTransform = new MatrixTransform(DocumentToClientTransform).Inverse;
    TallComponents.Interaction.Point documentPoint =
      new TallComponents.Interaction.Point(_paintedPaths[0].PathPoints[0].X,
                                           _paintedPaths[0].PathPoints[0].Y, docTransform);

    TallComponents.PDF.Page page = FindPage(documentPoint);

    if (page != null)
    {
      PageInteractor pageInteractor = DocumentInteractor.FindPageInteractor(page);
      if (pageInteractor != null)
      {
        MatrixTransform pageTransform = new MatrixTransform(GetPageToDocumentTransform(page)).Inverse;
        FreeHandShape shape = null;
        foreach (GraphicsPath gp in _paintedPaths)
        {
          TallComponents.Interaction.Point point = new TallComponents.Interaction.Point(
          gp.PathPoints[0].X, gp.PathPoints[0].Y, docTransform);
          point = new TallComponents.Interaction.Point(point, pageTransform);
          point = new TallComponents.Interaction.Point(point, pageInteractor.PdfPageTransform);

          shape = new FreeHandShape();
          shape.Dock = TallComponents.PDF.Shapes.DockStyle.None;
          shape.BlendMode = BlendMode.Normal;
          shape.ID = "PencilTool_" + Guid.NewGuid();
          shape.Pen = new TallComponents.PDF.Pens.Pen(RgbColor.Black);
          shape.Pen.Width = 1;

          FreeHandPath path = new FreeHandPath();
          path.Segments.Add(new FreeHandStartSegment(point.X, point.Y));

          for (int i = 1; i < gp.PathPoints.Length; i++)
          {
            point = new TallComponents.Interaction.Point(gp.PathPoints[i].X, gp.PathPoints[i].Y,
                                                            docTransform);
            point = new TallComponents.Interaction.Point(point, pageTransform);
            point = new TallComponents.Interaction.Point(point, pageInteractor.PdfPageTransform);

            path.Segments.Add(new FreeHandLineSegment(point.X, point.Y));
          }
          shape.Paths.Add(path);
          page.Overlay.Add(shape);
        }

        // This will tell PDFControls.NET to draw the overlay.
        page.Overlay.Invalidate();
      }
    }
  }
  finally
  {
    Application.UseWaitCursor = false;
    Parent.Enabled = true;
  }
}

After conversion, one can write the PDF document to a file. The drawn lines will then be visible in any PDF viewer that shows this document.