Salvation: REST Print Service for ArcGIS Server 9.3
At version 9.3*, the non-ADF ArcGIS web clients (Flex, Javascript, Silverlight) don’t support high quality printing. By high quality I mean variable resolution (so as to support dpi greater than 96) and the ability to use the template authoring in ArcGIS Desktop for a richer, smoother cartographic experience. If you want to create a hard copy of a map in, say Flex, printing the screen is the only option out-of-the-box. If you want to create an A3-sized PDF map with legend, borders, scale bars, North Arrows etc then you are up a murky creek with nothing but a hole-ridden wooden spoon to paddle out with.
Domenico Ciavarella came up with a bravissimo solution to the problem, tailored to the ADF, which made use of a ServerObjectExtension, LayoutSOE. Basically it involved invoking the SOE to output the PageLayout from ArcObjects. I hacked that and found that it worked fine but with the following deficiencies for the REST world:-
- No support for transparency in graphics (because ArcMap doesn’t support that, doh!)
- Not REST based, you needed to have the layers ahead of time in an MXD. This was a configuration nightmare to make sure that the layers in the print service MXD were the same as the layers in each map service that the Flex client was using. And if you were consuming other people’s map services and they changed the symbology then you were SOL.
- Too slow, adding/removing layers dynamically was taking minutes of time (especially the rasters).
Anyway, as luck would have it, my homey introduced me to a clued-up chap called Vish at this year’s ESRI Developer Conference in Palm Springs and he pointed me in a radically different direction. Kind of like Skynet in Terminator 2 after they find the remnants of Arnie from T1. I was being so amateur. What would Christian Bale think?
REST to the RESCUE
REST printing with ExportMap is fast and furious. All I would need to do was:
- Export the images from each Map Service
- Deserialize and draw the graphics with GDI+
- Draw a legend, using the ESRI SOAP API
- Export Templates (PageLayout) via an ArcObjects SOE
- Fuse the whole lot into a single image
- Handle writing to a PDF
To continue the Bale theme, albeit in a Adam West kinda way, to the Batcave!
The Big Picture
From the client perspective, all this architecture (REST service) had to do was:
- Provide a list of templates (e.g. Facilities Map, Transportation Map)
- Output the Map with the given format (e.g. PDF/PNG) from the selected template, including current layers/graphics/extent (WYSIWYG Printing)
Two methods! How hard could that be? Well, it was more than an afternoon’s work, but I’ll spare the vanilla stuff.
SOE what?
I half-inched** the aforementioned LayoutSOE to provide a user-friendly way of publishing & listing templates via the black magic of a ServerObjectExtension.
This gives the user the ability to specify templates in Catalog:
To be honest, I was a little scared of the SOE thing. All this regasm/gaccing. DCOM is something I last did with VB6 a decade ago. To allay my cross-process “Unexpected Failure” fears, I made all types plain strings and arrays.
The SOE had but two functions:
- Return Array of Templates. Done! That’s my kinda coding
- Return PrintTemplateInfo. This was an object holding the PageLayout information e.g. map bounds, the legend bounds (if any). It was also responsible for generating the template as a PNG, for consumption by the REST service. More on that later
The second of these involved some reasonably involved ArcObjects, but I checked out everything in VBA before committing to .NET SOE. Interactively discovering ArcObjects is sooo much easier than try-compile-test in .NET. Once that PageLayout metadata was extracted, time to export the image with marginalia on it and an extent set so as to draw a scalebar. Again, the LayoutSOE helps with all that. Tante Grazie, Domenico!
Boom, baby! We’re ready to roll onto the sexy REST side. Time to let ArcObjects fade into obscurity, may it (and its retarded non-exception handling) never cross our paths again…
REST easy
The core of this architecture is a WCF-assisted REST service, which has no ties to ArcGIS Server except for connecting to the SOE. So all it needs is the Web ADF Runtime (free again at 9.3.1). The service exposes a GET service to GetTemplates, which is pretty much a passthrough to the SOE. The other service is a POST service to CreateMap, which is where the meat of the matter lies. Fasten your seatbelts, I’m going to hit the code button a few times…
Export Templates (PageLayout) via an ArcObjects SOE
The first thing that CreateMap had to do was generate the template with appropriate marginalia and scalebar. Not only was the template returned as a URL but also the extents of the map and legend within the pagelayout. From the map bounds the size was calculated (dpi * inches).
Export the images from each Map Service
CreateMap’s primary duty was to draw images from each map service, remembering the extent and the visible layers. This entailed nothing more than taking the incoming print request and converting it to a URL for executing ExportMap on the REST server. I used PNG so as not to lose quality.
So, having built a URL like “http://localhost/ArcGIS/rest/services/Basemaps/Street/MapServer/export?bbox=7659147.07493616,704961.792450929,7668305.86853904,711693.897150482&bboxSR=2913&imageSR=2913&layers=show:0,1&size=761,653&format=png24&dpi=96&f=json&transparent=false”
The extent you ask for is not the extent that you get when calling ExportMap, so we take that too. It will help with graphics transformation.
Here’s the down & dirty on getting an image from the ESRI REST service
WebRequest request = WebRequest.Create(url);
WebResponse response = request.GetResponse();
remoteStream = response.GetResponseStream();
readStream = new StreamReader(remoteStream);
System.Drawing.Image img = System.Drawing.Image.FromStream(remoteStream);
From this small acorn the might oak of REST printing will flourish…
Deserialize and draw the graphics with GDI+
Cycling through the graphics was a little daunting because it entailed diving into GDI+. But it turned out to be surprisingly straightforward, unArcObjects-esque.
The only snaggette was transforming coordinates to page coordinates, which I had to do myself. I didn’t get into GIS to sully my hands with this sort of thing, but anyway 6th grade Math(s) sufficed.
An example of drawing a square marker…
private void DrawSquare(Graphics g2d, float x, float y, float size)
{
g2d.FillRectangle(_symBrush, (float)(x - 0.5 * size), (float)(y - 0.5 * size), (float)size, (float)size);
g2d.DrawRectangle(_symPen, (float)(x - 0.5 * size), (float)(y - 0.5 * size), (float)size, (float)size);
}
Draw a legend, using the ESRI SOAP API
We had already dabbled with the Legend in Flex because the Flex API doesn’t bother drawing it for you.
So this was a case of calling that and drawing the results into a .NET image, again with GDI+.
MapServerProxy93 mapProxy = new MapServerProxy93(mapServiceURL.Replace("/rest/", "/"));
ImageType imgType = new ImageType();
imgType.ImageFormat = esriImageFormat.esriImagePNG24;
imgType.ImageReturnType = esriImageReturnType.esriImageReturnMimeData;
return mapProxy.GetLegendInfo(mapProxy.GetDefaultMapName(), null, null, imgType);
Fuse the whole lot into a single image
OK, OK, you have all of these images, how do you blend them into a single one fit for printing? Again, GDI+ to your salvation.
You grab a graphics context with the bottom image, which would be the first map service to draw (e.g. basemap aerial/street etc).
//create the graphic from the original base image
g2d = Graphics.FromImage(bottomImage);
Once you have that then Robert is your father’s brother.
static private void AddImageToGraphics(Graphics g2d, Image image, int x, int y, float imageOpacity)
{
System.Drawing.Imaging.ColorMatrix cm = new System.Drawing.Imaging.ColorMatrix();
cm.Matrix00 = cm.Matrix11 = cm.Matrix22 = cm.Matrix44 = 1;
cm.Matrix33 = imageOpacity;
System.Drawing.Imaging.ImageAttributes ia = new System.Drawing.Imaging.ImageAttributes();
ia.SetColorMatrix(cm);
g2d.DrawImage(image, new System.Drawing.Rectangle(x, y, image.Width, image.Height), 0, 0, image.Width, image.Height,
GraphicsUnit.Pixel, ia);
g2d.Save();
}
That’s it, you can keep doing that ad finitum. So, stack all your map service images, one on top of the other. Then add your graphics. Then your legend. Finally, put the template on top of it and you are done.
Handle writing to a PDF
Ah, but Imaging.Formats doesn’t include PDF. Nope, for that we’ll have to use iTextSharp — a free PDF writer for C#. It’s pretty simple to use.
Couple of lines of code to add the image to the PDF and finito.
iTextSharp.text.Image image = iTextSharp.text.Image.GetInstance(mapImage, null);
cb.AddImage(image);
Flex fool
Suffice to say that from the Flex client it was necessary to serialize the layers and the graphics to JSON. There were a few niggly parts to that, but mostly ESRI’s json serializer did the trick.
That’s about it, well, plenty more to tell but I can hear gongs in the background — I am out of time. At the end you have a blazing fast high quality cartographic map production engine for the web. As Arthur Daley once said, on ne peut pas dire plus juste que ca.
* All this will be remedied at 9.4 of ArcGIS. Why does it seem that half my gis life has been spent writing features that were just around the corner in the next version of the product?
** pinched, Cockney Rhyming slang, innit.