Friday, November 19, 2010

How to export a map to PDF (client-side)

 How to export a map to PDF (client-side)

This post will describe a technique for exporting a map created with the ArcGIS API for Silverlight to a PDF document completely client-side (no server-side computing).

You will need:

  1. Microsoft Visual Studio 2010 (link)
  2. Microsoft Silverlight 4 Tools for Visual Studio 2010 (link)
  3. ESRI ArcGIS API for Silverlight (link)
  4. silverPDF from codeplex (link)

How to build a map that can be saved to PDF:

Step 1 – In VS2010, create a new Silverlight Application called “SilverlightApplication1”, accept all the default options.

Step 2 – Add a reference to the ESRI’s ArcGIS API for Silverlight and silverPDF from codeplex.

Step 3 – Add the following to MainPage.xaml

<UserControl x:Class="SilverlightApplication1.MainPage"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
   xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:esri="http://schemas.esri.com/arcgis/client/2009"
   mc:Ignorable="d"
   d:DesignHeight="300" d:DesignWidth="400">
    <Grid x:Name="LayoutRoot" Background="White">
        <esri:Map x:Name="Map">
            <esri:ArcGISTiledMapServiceLayer
Url="http://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer" />
        </esri:Map>
        <Button x:Name=
"ButtonPdf"
HorizontalAlignment=
"Left"
VerticalAlignment=
"Bottom"
Margin=
"10,0,0,10"
IsEnabled="False"
Content="Create PDF"/
>
    </Grid>
</UserControl>

Step 4 – Add the following to MainPage.xaml.cs (code behind)

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Resources;
using ESRI.ArcGIS.Client;
using ESRI.ArcGIS.Client.Geometry;
using PdfSharp.Drawing;
using PdfSharp.Pdf;

namespace SilverlightApplication1 {
    public partial class MainPage : UserControl {
        public MainPage() {
            InitializeComponent();

            // Listen to "Create PDF" button click
            this.ButtonPdf.Click += new RoutedEventHandler(this.Button_Click);

            // Enable "Create PDF" button only after the Imagery layer has loaded
            this.Map.Layers[0].Initialized += (s, e) => {
                this.ButtonPdf.IsEnabled = true;

                // Also, zoom to an adjusted full extent that fills the map canvas
                this.Map.ZoomTo(this.Map.Layers[0].FullExtent(this.Map));
            };
        }
        private void Button_Click(object sender, RoutedEventArgs e) {
            // Create a Save As dialog
            SaveFileDialog dialog = new SaveFileDialog();
            dialog.Filter = "PDF|*.pdf";

            // Show dialog. Exit if use closes dialog.
            if (!dialog.ShowDialog().Value) { return; }

            // Get the current tiled layer
            ArcGISTiledMapServiceLayer tiled = this.Map.Layers[0]
as ArcGISTiledMapServiceLayer;

            // Create a new dynamic layer from the same map service
            ArcGISDynamicMapServiceLayer dynamic = new ArcGISDynamicMapServiceLayer() {
                Url = tiled.Url,
                ImageFormat = ArcGISDynamicMapServiceLayer.RestImageFormat.JPG
            };

            // When the dynamic layer has initialized create the in-memory PDF document
            dynamic.Initialized += (a, b) => {
                dynamic.GetUrl(
                    this.Map.Extent,
                    (int)this.Map.ActualWidth,
                    (int)this.Map.ActualHeight,
                    delegate(string url, int width, int height, Envelope extent) {

                        // Download a new image of identical to what is currently
// displayed in the map

                        WebClient webClient = new WebClient();
                        webClient.OpenReadCompleted += (c, f) => {

                            // Use the dispatcher to force execution in the UI thread
                            Dispatcher.BeginInvoke(delegate() {
                                // Create the PDF document.
// Set document information properties.

                                PdfDocument document = new PdfDocument();
                                document.Info.Title = "World Imagery";

                                // Create a new page with the same dimensions as
// the browser map

                                PdfPage page = new PdfPage(document) {
                                    Height = new XUnit(this.Map.ActualHeight,
XGraphicsUnit.Presentation),
                                    Width = new XUnit(this.Map.ActualWidth,
XGraphicsUnit.Presentation)
                                };
                                document.Pages.Add(page);

                                // Create a graphics object for writing to the page
                                XGraphics graphics = XGraphics.FromPdfPage(page);

                                // Add the map image to the page
                                XImage map = XImage.FromStream(f.Result);
                                graphics.DrawImage(map, 0d, 0d);

                                // Save the PDF document to the user specified filename
                                document.Save(dialog.OpenFile());

                                // Inform the user that the PDF creation is complete
                                MessageBox.Show("PDF Creation Complete");
                            });
                        };
                        webClient.OpenReadAsync(new Uri(url));
                    }
                );
            };
            dynamic.Initialize();
        }
    }
    /// <summary>
    /// Extends the Esri Layer class with an alternative FullExtent property.
    /// Returns an adjusted extent conceptually similar to Image.Stretch==UniformToFill
    /// </summary>
    public static class LayerExtension {
        public static Envelope FullExtent(this Layer layer, FrameworkElement parent) {
            Envelope extent = layer.FullExtent;
            double ratioMap = parent.ActualHeight / parent.ActualWidth;
            double ratioLay = extent.Height / extent.Width;
            if (ratioMap < ratioLay) {
                return new Envelope() {
                    XMin = extent.XMin,
                    YMin = extent.GetCenter().Y - 0.5d * ratioMap * extent.Width,
                    XMax = extent.XMax,
                    YMax = extent.GetCenter().Y + 0.5d * ratioMap * extent.Width
                };
            }
            return new Envelope() {
                XMin = extent.GetCenter().X - 0.5d * extent.Height / ratioMap,
                YMin = extent.YMin,
                XMax = extent.GetCenter().X + 0.5d * extent.Height / ratioMap,
                YMax = extent.YMax
            };
        }
    }
}

The web application is now ready to run in debug.  When the application loads, use your mouse (and mousewheel) to navigate to your favorite location on the planet.  Click the “Create PDF” button to commence the PDF export.  You will first be prompted for the output location of the PDF document and then again when the export is complete.

6 comments:

  1. How are you referencing PDFSharp?
    The way this is written won't build

    ReplyDelete
  2. Thank you for the example. What do you do if I have multiple layers in a map? Thanks,

    ReplyDelete
  3. @Anonymous. To support maps with multiple layers you would have to export each layer and combine with something like WriteableBitmap.

    ReplyDelete
  4. Can I ask you how to convert pdf to image file(jpg, png... etc)??

    Thank you

    ReplyDelete
  5. Seems like with the new 3.0 ESRI Silverlight API we can do away with the WebClient stuff. Now the map control does not cause any security exceptions when you call new WriteableBitmap(mapControl, null).

    ReplyDelete