Convert an XPS to JPEG, PNG, TIFF or BMP in A4 Pages


Introduction

This tale begins with DGML graphs in Visual Studio. If you want to share a DGML graph with others you have only a couple of alternatives:

  1. Save the Graph as an XPS (Microsoft XML Paper Specification: see Wikipedia on Open XML Paper Specification or the Microsoft XML Paper Specification) file, or
  2. Share the DGML Graph (XML content files, See How to: Edit and Customize Graph Documents or the XML Schema for DGML ).

Both of these options have pit falls waiting for the unwary.

  1. For the XPS path your pit falls include:
    • A dependence on the XPS Viewer (see: What is the XPS Viewer? ). Which is fine if you have the viewer installed. But if you don’t have the XPS Viewer installed or don’t have the privileges to install software on the machine, you’re snookered. XPS is a file format which potentially be a format which your clients cannot read.
    • If the graph is larger than the paper you printer accepts, you probably cannot print the diagram. You are now dependent on the printer having a “poster print” (tiles the large output image onto multiple pages) feature. This again may not be an easily resolved problem.
  2. For the DGML file path your pit falls include:
    • A dependence on Visual Studio. If you don’t have a version of Visual Studio, again you’re snookered. I’ve not looked to see if the Visual Studio Express (which is free – check the Microsoft Licencing Terms and Conditions to see if you can use this path) versions support DGML viewing. Again you’re relying on the clients being able to download and install a copy of Visual Studio, which may not be an option in many work environments.

In my case some of my clients are internal to the organisation, and some are external. In both cases I cannot be guaranteed of either set of clients be able to read DGML or XPS file. So, what alternatives are available? The only path available which will guarantee that the clients will be able to read the files, is to use a graphics file format (TIFF, JPEG, PNG or BMP), to and “chop up” the big images into A4 chunks.

Other Options:

I did looks for some other options. But, the Microsoft Office suite does not seem to like to play with XPS files. There were a couple of option I did try:

  • Using Work to read an XPS. I was hoping that the XPS would come in as an image which I could imbed into a Word document. I did try and suck an XPS files into Word (2007 and 2010 version), but they reported that the XPS file was illegal (and a Microsoft product wrote it!).
  • I did try Visio to read an XPS file. Again I was hoping that it would load the XPS as an image. But, Visio does not seem to have any idea about XPS files. File Open, Insert an Image, both do not accept XPS are a format to be processed.

Converting the XPS to a graphics File Format

My quest for a solution started here (How to convert xps documents to other formats, for example bmp ?) The core code from the solution in MSDN is below.

static public void SaveXpsPageToBitmap(string xpsFileName)
{
    XpsDocument xpsDoc = new XpsDocument(xpsFileName, System.IO.FileAccess.Read);
    FixedDocumentSequence docSeq = xpsDoc.GetFixedDocumentSequence();

    // You can get the total page count from docSeq.PageCount
    for (int pageNum = 0; pageNum < docSeq.DocumentPaginator.PageCount; ++pageNum)
    {
        DocumentPage docPage = docSeq.DocumentPaginator.GetPage(pageNum);
        BitmapImage bitmap = new BitmapImage();
        RenderTargetBitmap renderTarget =
            new RenderTargetBitmap((int)docPage.Size.Width,
                                    (int)docPage.Size.Height,
                                    96, // WPF (Avalon) units are 96dpi based
                                    96,
                                    System.Windows.Media.PixelFormats.Default);

        renderTarget.Render(docPage.Visual);

        BitmapEncoder encoder = new BmpBitmapEncoder();  // Choose type here ie: JpegBitmapEncoder, etc
        encoder.Frames.Add(BitmapFrame.Create(renderTarget));

        FileStream pageOutStream =
            new FileStream(xpsFileName + ".Page" + pageNum + ".bmp", FileMode.Create, FileAccess.Write);
        encoder.Save(pageOutStream);
        pageOutStream.Close();
    }
}

There are a number of things which one should note about the code.

  • There are a number of resources which should be “Disposed” which are not.
  • If you want to deal with large XPS images, then you will need to compile it as x64. If you run the code with out building it as x64, you may get an “” exception. VS_References_For_XPS
  • You will need the following References for the project to compile.
  • You will also need the following using statements.
    using System.Windows.Xps.Packaging;
    using System.Windows.Documents;
    using System.Windows.Media.Imaging;
    using System.IO;

Cleaning up the Code

The following is the shell of a solution which has some of the memory management, and file handling cleaned up.

For those who do not know, the using statement is making sure that the object is cleaned up correctly. the following is from:

The using statement allows the programmer to specify when objects that use resources should release them. The object provided to the using statement must implement the IDisposable interface. This interface provides the Dispose method, which should release the object’s resources.

MSDN: using Statement (C# Reference)

The resulting code is:

    static public void SaveXpsPageToBitmap(string xpsFileName)
    {
        using (XpsDocument xpsDoc = new XpsDocument(xpsFileName, System.IO.FileAccess.Read))
        {
            FixedDocumentSequence docSeq = xpsDoc.GetFixedDocumentSequence();
            // You can get the total page count from docSeq.PageCount
            for (int pageNum = 0; pageNum < docSeq.DocumentPaginator.PageCount; ++pageNum)
            {
                using (DocumentPage docPage = docSeq.DocumentPaginator.GetPage(pageNum))
                {
                    BitmapImage bitmap = new BitmapImage();
                    RenderTargetBitmap renderTarget =
                        new RenderTargetBitmap((int)docPage.Size.Width,
                                                (int)docPage.Size.Height,
                                                96, // WPF (Avalon) units are 96dpi based
                                                96,
                                                System.Windows.Media.PixelFormats.Default);
                    renderTarget.Render(docPage.Visual);
                    BitmapEncoder encoder = new BmpBitmapEncoder();  // Choose type here ie: JpegBitmapEncoder, etc
                    encoder.Frames.Add(BitmapFrame.Create(renderTarget));
                    using (FileStream pageOutStream =
                        new FileStream(xpsFileName + ".Page" + pageNum + ".bmp", FileMode.Create, FileAccess.Write))
                    {
                        encoder.Save(pageOutStream);
                        pageOutStream.Close();
                    }
                }
            }
            xpsDoc.Close();
        }
    }

A Minor Digression: Encoders and Options Supported in File Formats

The core of the above solution is the BitmapEncoder classes, and the classes derived from it. The encoders available are (straight from MSDN):

System.Windows.Media.Imaging.BmpBitmapEncoder
System.Windows.Media.Imaging.GifBitmapEncoder
System.Windows.Media.Imaging.JpegBitmapEncoder
System.Windows.Media.Imaging.PngBitmapEncoder
System.Windows.Media.Imaging.TiffBitmapEncoder
System.Windows.Media.Imaging.WmpBitmapEncoder

Of the different encodings, and various graphics file formats, I was interested in the following capabilities:

Format Class Metadata Multiple Frames (multiple pages in one file)
BPM System.Windows.Media.Imaging.BmpBitmapEncoder No No
GIF System.Windows.Media.Imaging.GifBitmapEncoder No Yes
JPEG System.Windows.Media.Imaging.JpegBitmapEncoder Frame Level not Global No
PNG System.Windows.Media.Imaging.PngBitmapEncoder Frame Level not Global No
TIFF System.Windows.Media.Imaging.TiffBitmapEncoder Frame Level not Global Yes
WMP System.Windows.Media.Imaging.WmpBitmapEncoder (not clear) No

Pagination, Tiling, or Cropping to a Paper Size (or any size you like)

The following are the core routines of my solution.  This one does the writing of the separate tile files.

/// <summary>
/// Produces a series of graphics files in the format specified from the
/// input XPS file.
/// </summary>
/// <param name="JPG_Path">Used to generate the output file name
/// fileNamePart">The filename without directory or extensions,
/// which is used to generate the output file
/// pageNum">Used to generate the output file name
/// renderTarget">The bit map representation of the page to
/// be tiled into multiple parts</param>
/// <param name="Paper">The size of the tiles (in pixels - image is 96 dpi)
/// of the image that will be created.
/// PaperType">A string which describes the tile size (e.g. A3).
/// Used in creation of the output file name</param>
/// <param name="extension">The type of graphics file created.
/// Used to determine the encoder used, and to create the metadata for the file.</param>
private void GeneratePagesAsFiles(string JPG_Path, string fileNamePart,
    int pageNum, RenderTargetBitmap renderTarget,
    Size Paper, string PaperType, string extension)
{
    int iHrozTiles = ((int)(renderTarget.Width / Paper.Width)) + 1;
    int iVertTiles = ((int)(renderTarget.Height / Paper.Height)) + 1;
    BitmapEncoder encoder;
    for (int i = 0; i < iHrozTiles; i++)
    {
        for (int j = 0; j < iVertTiles; j++)
        {
            string outputFileName = MakeFileName(JPG_Path, fileNamePart,
                i, j, iHrozTiles, iVertTiles, "." + extension, PaperType);
            if (File.Exists(outputFileName))
                continue;
            Int32Rect crop = new Int32Rect(
                (int)Math.Min(i * Paper.Width, renderTarget.Width),
                (int)Math.Min(j * Paper.Height, renderTarget.Height),
                (int)Math.Min(Paper.Width, renderTarget.Width - ((i) * Paper.Width)),
                (int)Math.Min(Paper.Height, renderTarget.Height - (j * Paper.Height)));
            if (crop.X == renderTarget.Width || crop.Y == renderTarget.Height)
                continue;
            CroppedBitmap cb1 = new CroppedBitmap(renderTarget, crop);
            BitmapMetadata metadata =
                MakeMetadata(extension, fileNamePart, i, j, iHrozTiles, iVertTiles);
            switch (extension)
            {
                case "png": encoder = new PngBitmapEncoder(); break;
                case "jpg": encoder = new JpegBitmapEncoder(); break;
                case "tiff": encoder = new TiffBitmapEncoder(); break;
                case "gif": encoder = new GifBitmapEncoder(); break;
                case "bmp": encoder = new BmpBitmapEncoder(); break;
                case "wdp": encoder = new WmpBitmapEncoder(); break;
                default: extension = "png"; encoder = new PngBitmapEncoder(); break;
            }
            encoder.Frames.Add(BitmapFrame.Create(cb1, null, metadata, null));
            using (FileStream pageOutStream =
                new FileStream(outputFileName, FileMode.Create, FileAccess.Write))
            {
                encoder.Save(pageOutStream);
                pageOutStream.Close();
            }
        }
    }
}

The paper size object which is passed in is one of the following (96 dpi * paper dimensions (in inches)):

        Size A4Paper = new Size(780, 1100); // rounded to make the checking the math simpler
        Size A3Paper = new Size(1560, 2200); // rounded to make the checking the math simpler

The following is the core routine which generates a multiple page TIFF file:

/// <summary>
/// Writes a multiple page TIFF file, tiled into pages
/// </summary>
/// <param name="JPG_Path">The path for the output file, used to make
/// the output file name
/// fileNamePart">The filename without path or extension,
/// used to make the output file name
/// pageNum">The page number in the XPS file,
/// used to make the output file name</param>
/// <param name="renderTarget">The bit map image of the XPS page</param>
/// <param name="Paper">The size of the output tiles required
/// PaperSize">The description of the tile size(e.g. A3),
/// used to make the output file name</param>
private void GeneratePagesAsMultiPageFile(string JPG_Path, string fileNamePart,
    int pageNum, RenderTargetBitmap renderTarget, Size Paper, string PaperSize)
{
    int iHrozTiles = ((int)(renderTarget.Width / Paper.Width)) + 1;
    int iVertTiles = ((int)(renderTarget.Height / Paper.Height)) + 1;
    BitmapMetadata metadata = MakeMetadata(fileNamePart, "tiff");
    TiffBitmapEncoder encoder = new TiffBitmapEncoder();
    using (FileStream pageOutStream = new FileStream(
        MakeFileName(JPG_Path, fileNamePart, pageNum,
        "MultiplePage", ".tiff", PaperSize),
        FileMode.Create, FileAccess.Write))
    {
        for (int i = 0; i < iHrozTiles; i++)
        {
            for (int j = 0; j < iVertTiles; j++)
            {
                Int32Rect crop = new Int32Rect(
                    (int)Math.Min(i * Paper.Width, renderTarget.Width),
                    (int)Math.Min(j * Paper.Height, renderTarget.Height),
                    (int)Math.Min(Paper.Width, renderTarget.Width - ((i) * Paper.Width)),
                    (int)Math.Min(Paper.Height, renderTarget.Height - (j * Paper.Height)));
                if (crop.X == renderTarget.Width || crop.Y == renderTarget.Height)
                    continue;
                CroppedBitmap cb1 = new CroppedBitmap(renderTarget, crop);
                encoder.Frames.Add(BitmapFrame.Create(cb1, null, metadata, null));
            }
        }
        encoder.Save(pageOutStream);
        pageOutStream.Close();
    }
}

Possible Enhancements

There are a couple of things which could be “sharpened” up in the solution presented. These include:

  • Not outputting blank pages. This is potentially possible, the Bit Map of the cropped image would have all locations with the same value. Maybe putting a “this page is blank” message onto the page.
  • Printing the page number, and or (x,y) location onto the outputs. This again is possible.
  • Support for more paper sizes. This is just a process of defining different paper sizes. The current A4 and A3 suite my requirements.

Conclusions

It took a bit of hunting to get to this solution, but it does work (most of the time – I’ll expand on that next).

I suspect that there is a limit on the size of an XPS diagram which can be handled by these API’s. I have one XPS file which “blows up”, when trying to process it. I’ve more investigating to do before I have completely diagnosed this. I suspect that it will end up being a bug report for Microsoft.

Advertisements

, , , , , , , , , , , , ,

  1. #1 by Georg on March 9, 2012 - 7:23 pm

    Hi Craig,
    thank You for this post.
    I see, how do you generate images/print from XPS,
    but how did you save/convert the DGML into XPS per C# code?
    The ~.GraphModel.Graph.Save method only generate *.dgml files.
    I only now the way to open the dgml-document in VS2010 and manualy “Export as XPS”.

    Thanks,
    Ciao:
    GG 😉

    • #2 by aussiecraig on March 10, 2012 - 3:13 pm

      Hi Georg,

      What I was doing was:
      1) Running a project in Visual Studio, which generated a series of DGML files.
      2) Loading those DGML files into Visual Studio, as rendered diagrams. Actually, I was keeping those diagrams open in Visual Studio and using the reload when changed function in Visual Studio.
      3) Saving the DGML rendered images to XPS files. I should have written a macro to do this, but never quite got around to doing that.
      4) The chopping the XPS files up, into image files, so that I could send them to someone who did not have the XPS viewer installed. The Corporate IT outsource provider there had locked things up so much, that it was not possible to install the viewer.

      I hope this helps.
      Craig

  2. #3 by Ulf on July 2, 2011 - 6:16 am

    Like David I found this post very interesting, this looks like a workable way to be able to crop XPS-file contents when outputting them to a printer.

    http://www.mediafire.com is usually ok when it comes to posting larger downloads for public consumption.

    /Ulf

    • #4 by aussiecraig on July 2, 2011 - 8:27 am

      Ulf,
      This was developed in a corporate environment, which was using Windows XP. This XP image did not include the XPS viewer, and was very heavily locked down (for the standard user). It has since been upgraded to Windows 7 which does include the XPS viewer.
      So, I needed a format for the XPS image files which could be viewed and printed with the ‘standard’ XP desktop. Hence, using graphics file formats, and chopping the images up into A4 and A3 size portions.
      Craig

    • #5 by David on July 4, 2011 - 11:52 pm

      Hi Ulf,

      I’ve created a small app with this code online at http://XPSConverter.codeplex.com you should just be able to download and install the app.

      David

      • #6 by aussiecraig on July 5, 2011 - 2:02 pm

        David,
        Thanks for that. I am fine with it going up there.
        I considered codeplex, but talked myself out of posting it up there. The main reason was because it really should have a UI, and I hate writing UI’s.
        Thanks again,
        Craig

  3. #7 by david on June 8, 2011 - 9:30 pm

    Excellent post,

    would be great to have a downloadable C# project or exe

    • #8 by aussiecraig on June 9, 2011 - 6:14 am

      David,
      The WordPress site this blog is on has a very limited range of file types which can be uploaded.
      At present I do not have another location on the web where I could load soource code and prrojecc files.
      Craig

      • #9 by David on July 4, 2011 - 11:51 pm

        Hi Craig,

        thanks once again for this blog post. I’ve tried to take the code snippets and create a little app out of them.
        I’ve posted this on CodePlex at http://XPSConverter.codeplex.com
        hope that you are ok with this?

        feel free to tidy up the code on codeplex – I’m happy to add you as an admin

        David

  4. #10 by aussiecraig on November 27, 2010 - 11:14 am

    Many thanks for the appreciation. I’m very glad you have found you visit to my blog worthwhile.
    I must admit that I continue to find IT a very fertile field of innovation.
    Taking development work from the professional environment (my work life) and converting it into worthwhile blog posts is a skill I continue to develop.
    Many thanks again.
    Craig

  1. Pagination of XPS files into Graphics format files in A3 or A4 Pages « Craig's Eclectic Blog
  2. Speed Math guy

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: