Posts Tagged JPEG

Pagination of XPS files into Graphics format files in A3 or A4 Pages


Introduction

This is a continuation of the previous post (Craig’s Eclectic Blog » Convert an XPS to JPEG, PNG, TIFF or BMP in A4 Pages ) in which I presented a solution to producing A3 of A4 pages from a large XPS file. There were a number of things in that solution which I was not entirely happy with, which this post addresses.

Things which were “not to my satisfaction”

There were a couple of problems I had with the solution presented. These included:

  • The Paper size occurred twice in the functions arguments. It was there once as a size, and once as a string for encoding into the file name. This looked like a classic case of a class which should have existed which was overlooked.
  • There were a number of places where the string for the encoding was passed into API class, or used as part of the file name generation. Again, this looked like a case of an enumeration which should have existed in the solution which was missing. One could also argue that the .Net Framework API’s should be using a similar enumeration.
  • There was a better way to handle the encoding of page parts. This solution is a “bit” more efficient.

Also there were a couple of things which should have been included in the previous post which I forgot to put in. These included:

  • The main needs to be [STAThreadAttribute]. The attribute (see the following on MSDN for an explanation of attributes in C#: Attributes (C# and Visual Basic) ) on the main STAThread is vital (the program will throw an exception without it). The main also give a clue as to why I cleaned up some of the memory management in the source code which this started from. I had over 500 XPS files to tile into workable, and all user consumable, files.
  • There was an optimisation of the tiling process which I should have included. This optimisation is to align the longest edge of the paper with the longest edge of the bitmap to be tiled. This, should and I’ve only my empirical “feel” for the subject which suggests it should be so, should minimise the number of tiles produces.

The Classes in the Final Solution

The  following diagram is the class call structure, generated by Visual Studio

XPS_to_Graphic_Structure

The following the class diagram for the final solution (final being loosely used term – in that it is final only until I thinks of another improvement, or a new requirement, for the solution).

ClassDiagram1

The Main Program

There are a couple of key point to note here:

  • The STAThread attribute is necessary. The underlying API’s used by the program require the STA (Single Threaded Apartment)Threading model.
  • The redesign of the interface into the pagination process has yielded a far more flexible API. The ability too specify multiple:
    • Encodings
    • Paper sizes
    • Output file types (one big one, multiple pages in one file and multiple encoded by paper size files)
    • The “skip done” provides a rudimentary restart facility.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace XpsConverter
{
    /// <summary>
    /// Program for the conversion of XPS files to graphics files in:
    ///     Multiple output formats
    ///     Multiple tilings of the image (A3 or A4 currently)
    /// </summary>
    class Program
    {
        /// <summary>
        /// Where the XPS files are found
        /// </summary>
        private static string XPS_Path =
            @"H:\Visual Studio 2010\Projects\ProduceDGML_from_XML\DGML_From_LINQ\XPS";
        /// <summary>
        /// Where the graphics files are written
        /// </summary>
        private static string JPG_Path =
            @"H:\Visual Studio 2010\Projects\Convert_XPS_to_BPM\JPG_Files\";

        /// <summary>
        /// Main for the execution of the program.
        /// 
        /// args">
        [STAThread]
        static void Main(string[] args)
        {
            List<PaperSize> papers = new List<PaperSize>()
            {
                new PaperSize(PaperSizes.A3),
                new PaperSize(PaperSizes.A4)
            };

            List<EncoderTypes> singleFileOuptut = new List<EncoderTypes>
            {
                EncoderTypes.gif, EncoderTypes.png, EncoderTypes.jpg,
                EncoderTypes.bmp, EncoderTypes.tiff, EncoderTypes.wdp
                //EncoderTypes.gif
            };
            var filesList = Directory.GetFiles(XPS_Path, "*.xps");
            foreach (string fileName in filesList)
            {
                //XPS_Outputs_Producer producer = new XPS_Outputs_Producer();
                XPS_Outputs_Producer.XPS_To_Pages(fileName, JPG_Path, false, true,
                    papers, singleFileOuptut, true,
                    EncoderTypes.tiff, papers);
            }
        }
    }
}

The Encoder Encapsulation

This is simply a very thin wrapper class around the Framework’s Encoder classes. Simply it allows the creation of a specific encoder on the basis of the Encoder Types enumeration. My API design philosophy would have the Framework working in this manner, through a factory class. One day, I may convert this into an extension method, but for now it does the job.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Media.Imaging;

namespace XpsConverter
{
    /// <summary>
    /// A very thing wrapper class over the native framework bit map
    /// encoding classes.
    /// </summary>
    public class EncoderEncapsulation
    {
        private EncoderTypes _encoderType;
        private BitmapEncoder _encoder;
        public EncoderEncapsulation(EncoderTypes requiredType)
        {
            this._encoderType = requiredType;
            this._encoder = null;
        }
        public BitmapEncoder Encoder
        {
            get
            {
                if (this._encoder != null)
                    return this._encoder;
                switch (this._encoderType)
                {
                    case EncoderTypes.png:
                        this._encoder = new PngBitmapEncoder();
                        return this._encoder;
                    case EncoderTypes.jpg:
                        this._encoder = new JpegBitmapEncoder();
                        return this._encoder;
                    case EncoderTypes.tiff:
                        this._encoder = new TiffBitmapEncoder();
                        return this._encoder;
                    case EncoderTypes.gif:
                        this._encoder = new GifBitmapEncoder();
                        return this._encoder;
                    case EncoderTypes.bmp:
                        this._encoder = new BmpBitmapEncoder();
                        return this._encoder;
                    case EncoderTypes.wdp:
                        this._encoder = new WmpBitmapEncoder();
                        return this._encoder;
                    default:
                        this._encoder = new BmpBitmapEncoder();
                        return this._encoder;
                }
            }
        }
        public EncoderTypes EncoderType
        {
            get
            {
                return this._encoderType;
            }
        }
    }
}

The File Namer Class

This is simply a file name generator class. It works with the pages in the XPS file and the tiles being generated.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace XpsConverter
{
    /// <summary>
    /// This class is just a file name generating object.
    /// There is a degree of structure in the input names,
    /// and the generated names embellish on that with extra metadata.
    /// </summary>
    public class FileNamer
    {
        private string _filename;

        private FileNamer()
        {
            this._filename = null;
        }

        public FileNamer(string FilePath, string fileNamePart,
            int pageNo, int iTotalPages, EncoderTypes FileType):this()
        {
            StringBuilder res = new StringBuilder(FilePath);
            if(FilePath[FilePath.Length -1] != '\\')
                res.Append('\\');
            int iPage = pageNo + 1;
            int iPageTotal = iTotalPages;
            res.AppendFormat("{0}_Page_{1}_of_{3}.{2}",
                fileNamePart, iPage,
                Enum.GetName(typeof(EncoderTypes), FileType), iPageTotal);
            this._filename = res.ToString();
        }

        public FileNamer(string RootOutputPath, string fileNamePart,
            int pageNum, int iTotalPages,
            int iHrozTiles, int iVertTiles, int i, int j,
            PaperSize paperSize, EncoderTypes encoderType) : this()
        {
            int iPart = ((i * (iVertTiles)) + j) + 1;
            int iTotal = (iHrozTiles) * (iVertTiles);
            int iPage = pageNum + 1;

            StringBuilder result = new StringBuilder(RootOutputPath);
            if (RootOutputPath[RootOutputPath.Length - 1] != '\\')
                result.Append('\\');
            result.AppendFormat("{0}_Page_{1}_of_{6}_Part_{2}_of_{3}_{5}.{4}",
                fileNamePart, iPage, iPart, iTotal,
                Enum.GetName(typeof(EncoderTypes), encoderType),
                Enum.GetName(typeof(PaperSizes), paperSize.PaperSizeEnum),
                iTotalPages);
            this._filename = result.ToString();
        }

        public    FileNamer(string rootOutputPath,
            string fileNamePart,EncoderTypes MultiPageEncoder,PaperSize paper)
        {
            StringBuilder result = new StringBuilder(rootOutputPath);
            if (rootOutputPath[rootOutputPath.Length - 1] != '\\')
                result.Append('\\');
            result.AppendFormat("{0}_{2}.{1}", fileNamePart,
                Enum.GetName(typeof(EncoderTypes), MultiPageEncoder),
                Enum.GetName(typeof(PaperSizes), paper.PaperSizeEnum));
            this._filename = result.ToString();
        }

        public string FileName
        {
            get
            {
                return this._filename;
            }
        }
    }
}

Bitmap Metadata Generation

The following is major function in my bitmap metadata generation method. There was a bit of “trial and exception” to determine which metadata elements are valid for each of the encoding types.

public BitmapMetadata MakeMetadata(EncoderTypes encoderType)
{
    string encoder = System.Enum.GetName(typeof(EncoderTypes), encoderType);
    if (encoderType == EncoderTypes.wdp || encoderType == EncoderTypes.gif || encoderType == EncoderTypes.bmp)
        return null;
    BitmapMetadata metadata = new BitmapMetadata(encoder);
    switch (encoderType)
    {
        case EncoderTypes.png:
            metadata.DateTaken = this.Taken;
            break;
        case EncoderTypes.jpg:
            metadata.ApplicationName = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
            metadata.Author = this.Author;
            metadata.Comment = this.Comment;
            metadata.Copyright = this.Copyright;
            metadata.DateTaken = this.Taken;
            metadata.Keywords = this.Keywords;
            metadata.Subject = this.Subject;
            metadata.Title = this.Title;
            break;
        case EncoderTypes.tiff:
            metadata.ApplicationName = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
            metadata.Author = this.Author;
            metadata.Comment = this.Comment;
            metadata.Copyright = this.Copyright;
            metadata.DateTaken = this.Taken;
            metadata.Keywords = this.Keywords;
            metadata.Subject = this.Subject;
            metadata.Title = this.Title;
            break;
        case EncoderTypes.gif:
            Debug.WriteLine("GIF files do not appear to support metadata. Should not get here");
            break;
        case EncoderTypes.bmp:
            Debug.WriteLine("BMP files do not appear to support metadata. Should not get here!");
            break;
        case EncoderTypes.wdp:
            Debug.WriteLine("WDP files do not appear to support metadata. Should not get here!");
            break;
        default:
            Debug.WriteLine("Should not get here");
            break;
    }
    return metadata;
}

Paper Size Encapsulation

The class, and enumeration, wrap the details of paper size into a usable class. Also, the class includes overrides of Equals and GetHashCode.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;

namespace XpsConverter
{
    /// <summary>
    /// Enumeration which contains the paper types for which size information is kept
    /// </summary>
    public enum PaperSizes { A4, A3 }

    /// <summary>
    /// Encapsulation of the Paper size information.
    /// </summary>
    public class PaperSize
    {
        private Size _paperSize;
        private PaperSizes _paperSizeEnum;
        private PaperSize()
        {

        }

        public PaperSize(PaperSizes paperSize)
        {
            this._paperSizeEnum = paperSize;
            switch (paperSize)
            {
                case PaperSizes.A4:
                    this._paperSize = new Size(780, 1100);
                    break;
                case PaperSizes.A3:
                    this._paperSize = new Size(1560, 2200);
                    break;
                default:
                    this._paperSize = new Size(780, 1100);
                    break;
            }
        }
        public PaperSizes PaperSizeEnum
        {
            get
            {
                return this._paperSizeEnum;
            }
        }
        public Size CurrentSize
        {
            get
            {
                return this._paperSize;
            }
        }
        public int Height
        {
            get
            {
                return (int)this._paperSize.Height;
            }
        }
        public int Width
        {
            get
            {
                return (int) this._paperSize.Width;
            }
        }

        public override bool Equals(object obj)
        {
            if (obj == null)
                return false;
            PaperSize temp = obj as PaperSize;
            if (temp == null)
                return false;
            if (temp._paperSizeEnum == this._paperSizeEnum)
                return true;
            return false;
        }

        public override int GetHashCode()
        {
            return this._paperSizeEnum.GetHashCode();
        }
    }
}

The Pagination Process

This is where most of the work happens.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Windows.Xps.Packaging;
using System.Windows.Documents;
using System.Windows.Media.Imaging;
using System.Diagnostics;
using System.Windows;

namespace XpsConverter
{
    public static class XPS_Outputs_Producer
    {
        /// <summary>
        /// This is the main entry point in to the process of converting an
        /// XPS file into graphics files
        /// </summary>
        /// <param name="fileName">The name of the file to be processed.
        /// It should include all of the path information to find the file
        /// RootOutputPath">The path to the output root
        /// SkipDone">Skip files which have already been created
        /// OneBigOne">Generate a graphics file which is not tiled into parts</param>
        /// <param name="papers">The list of paper sizes to tile the XPS file into
        /// singleFileOuptut">The list of encodings which the XPS file
        /// is to be converted into
        /// multiplePageOutput">The flag used to indicate that one file
        /// with multiple pages (frames in graphics file terms) is to be created
        /// MultiPageEncoder">The list of encodings which are to be used
        /// for the multiple page output
        /// multiplePapers">The list of papers which the XPS is to be tiled into.</param>
        internal static void XPS_To_Pages(string fileName, string RootOutputPath,
            bool SkipDone ,
            bool OneBigOne,
            List<PaperSize> papers,
            List<EncoderTypes> singleFileOuptut,
            bool multiplePageOutput ,
            EncoderTypes MultiPageEncoder,
            List<PaperSize> multiplePapers)
        {
            string fileNamePart = Path.GetFileNameWithoutExtension(fileName);
            FileNameDecoder decoder = new FileNameDecoder(fileNamePart);
            string DirectoryPart = decoder.MakeDirectoryPartsFromName(RootOutputPath);
            RootOutputPath = DirectoryPart;
            FileNameDecoder.copyXPSFile(fileName, RootOutputPath, fileNamePart);
            if (SkipDone &&
                AlreadyDone(new FileNamer(RootOutputPath, fileNamePart, 0, 0, singleFileOuptut[0])))
                return;

            MultiplePageFilePaperAndEncoder multiPageOutput = null;
            if(multiplePageOutput)
            {
                multiPageOutput = new MultiplePageFilePaperAndEncoder();
                foreach(PaperSize paper in multiplePapers)
                {
                    multiPageOutput.Add(new EncoderEncapsulation(MultiPageEncoder),  paper,
                        new FileNamer(RootOutputPath, fileNamePart, MultiPageEncoder, paper));
                }
            }
            XpsDocument xpsDoc = null;
            try
            {
                xpsDoc = ProcessXPSDocument(fileName, RootOutputPath, OneBigOne, papers,
                    singleFileOuptut, multiplePageOutput, fileNamePart, decoder,
                    multiPageOutput, xpsDoc);
                xpsDoc = null;
            }
            finally
            {
                if (xpsDoc != null)
                    xpsDoc.Close();
            }
        }

        /// <summary>
        /// This method processes the XPS document and extracts the pages of that document.
        /// 
        /// 
        /// RootOutputPath">
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        private static XpsDocument ProcessXPSDocument(string fileName, string RootOutputPath,
            bool OneBigOne, List<PaperSize> papers, List<EncoderTypes> singleFileOuptut,
            bool multiplePageOutput, string fileNamePart, FileNameDecoder decoder,
            MultiplePageFilePaperAndEncoder multiPageOutput, XpsDocument xpsDoc)
        {
            using (xpsDoc = new XpsDocument(fileName, System.IO.FileAccess.Read))
            {
                FixedDocumentSequence docSeq = xpsDoc.GetFixedDocumentSequence();
                int iTotalPages = docSeq.DocumentPaginator.PageCount;
                for (int pageNum = 0; pageNum < iTotalPages; ++pageNum)
                {
                    DocumentPage docPage = null;
                    try
                    {
                        docPage = ProcessBitMapImage(RootOutputPath, OneBigOne,
                            papers, singleFileOuptut, fileNamePart, decoder,
                            multiPageOutput, docSeq, iTotalPages, pageNum, docPage);
                        docPage = null;
                    }
                    finally
                    {
                        if (docPage != null)
                            docPage.Dispose();
                    }
                }
                if (multiplePageOutput)
                {
                    GenerateMultpliePageOutputs(multiPageOutput);
                }
            }
            return xpsDoc;
        }

        /// <summary>
        /// This method renders the page into a BitMap and then calls the required
        /// pagination routines.
        /// 
        /// RootOutputPath">
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        private static DocumentPage ProcessBitMapImage(string RootOutputPath, bool OneBigOne,
            List<PaperSize> papers, List<EncoderTypes> singleFileOuptut, string fileNamePart,
            FileNameDecoder decoder, MultiplePageFilePaperAndEncoder multiPageOutput,
            FixedDocumentSequence docSeq, int iTotalPages, int pageNum, DocumentPage docPage)
        {
            using (docPage = docSeq.DocumentPaginator.GetPage(pageNum))
            {
                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);
                if (OneBigOne)
                {
                    MakeOutputFile(RootOutputPath, fileNamePart, pageNum, iTotalPages, singleFileOuptut, renderTarget, decoder);
                }
                MakeOutputFiles(RootOutputPath, fileNamePart, pageNum, iTotalPages, singleFileOuptut, papers, renderTarget, decoder, multiPageOutput);
            }
            return docPage;
        }

        /// <summary>
        /// Produces the output files for each of the multiple page outputs
        /// 
        /// MultiplePageEncoders">
        private static void GenerateMultpliePageOutputs(
            MultiplePageFilePaperAndEncoder MultiplePageEncoders)
        {
            foreach(Tuple<EncoderEncapsulation, FileNamer> output in MultiplePageEncoders.GetOutputDetails())
            {
                ProcudeOutputFile(output);
            }
        }

        /// <summary>
        /// Does the writing of the encoded bitmap to output file.
        /// </summary>
        /// <param name="output"></param>
        private static void ProcudeOutputFile(Tuple<EncoderEncapsulation, FileNamer> output)
        {
            FileStream pageOutStream = null;
            using (pageOutStream = new FileStream(output.Item2.FileName, FileMode.Create, FileAccess.Write))
            {
                try
                {
                    output.Item1.Encoder.Save(pageOutStream);
                    pageOutStream.Close();
                    pageOutStream = null;
                }
                catch (AccessViolationException ex)
                {
                    Debug.WriteLine(ex);
                    Debug.WriteLine(
                        "{0} File {1} is broken trying next", DateTime.Now.ToLongTimeString(), output.Item2.FileName);
                    pageOutStream.Close();
                    File.Delete(output.Item2.FileName);
                }
                finally
                {
                    if (pageOutStream != null)
                    {
                        pageOutStream.Dispose();
                    }
                }
            }
        }

        /// <summary>
        /// Crops the Image to each of the Paper sized tiles, and then produces the output file.
        /// 
        /// 
        /// fileNamePart">
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        private static void MakeOutputFiles(string RootOutputPath,
            string fileNamePart,
            int pageNum, int iTotalPages,
            List<EncoderTypes> singleFileOuptut,
            List<PaperSize> papers,
            RenderTargetBitmap renderTarget,
            FileNameDecoder decoder,
            MultiplePageFilePaperAndEncoder MultiEncoders)
        {
            foreach (PaperSize PaperSize in papers)
            {
                int iHorzTiles;
                int iVertTiles;
                Size Paper = CalculateTiles(renderTarget, PaperSize.CurrentSize, out iHorzTiles, out iVertTiles);
                for (int i = 0; i < iHorzTiles; i++)
                {
                    for (int j = 0; j < iVertTiles; j++)
                    {
                        Int32Rect crop = CalculateCropRectangle(renderTarget, Paper, i, j);
                        if (crop.X == renderTarget.Width || crop.Y == renderTarget.Height)
                            continue;
                        CroppedBitmap croppedBitmap = new CroppedBitmap(renderTarget, crop);
                        AddToMultiPageEncoders(MultiEncoders, croppedBitmap, decoder, iHorzTiles, iVertTiles, i, j, PaperSize, pageNum, iTotalPages);
                        foreach (EncoderTypes encoderType in singleFileOuptut)
                        {
                            FileNamer namer = new FileNamer(
                                RootOutputPath, fileNamePart, pageNum, iTotalPages,
                                iHorzTiles, iVertTiles, i, j,
                                PaperSize, encoderType);
                            EncoderEncapsulation encoder = new EncoderEncapsulation(encoderType);
                            BitmapEncoder enc = encoder.Encoder;
                            enc.Frames.Add(BitmapFrame.Create(croppedBitmap, null,
                                decoder.MakeMetadata(encoderType, pageNum,  iTotalPages, i,j, iHorzTiles, iVertTiles), null));
                            ProduceOutputFile(namer, enc);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Adds the copped bitmap to the correct paper sizes multiple page output encoder
        /// 
        /// 
        /// croppedBitmap">
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        private static void AddToMultiPageEncoders(
            MultiplePageFilePaperAndEncoder MultiplePageEncoders,
            CroppedBitmap croppedBitmap,
            FileNameDecoder Decoder,
            int iHorzTiles, int iVertTiles,
            int i, int j, PaperSize paper,
            int iPage, int iPages)
        {
            if (MultiplePageEncoders == null)
                return;
            BitmapEncoder encoder = MultiplePageEncoders.getEncoder(paper);
            encoder.Frames.Add(BitmapFrame.Create(croppedBitmap, null,
                Decoder.MakeMetadata(MultiplePageEncoders.getEncoderType(paper), iPage, iPages, i, j, iHorzTiles, iVertTiles),
                null));
        }

        private static Int32Rect CalculateCropRectangle(RenderTargetBitmap renderTarget, Size Paper, int i, int 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)));
            return crop;
        }

        /// <summary>
        /// This routine tries and optimises the way the tiles are carved off the base image.
        /// It is more efficient (in terms of number of tiles required) to carve the image
        /// up with the tile longest edge, matching the image longest edge.
        /// (An unproven assertions - the math is probably quite long winded to prove).
        /// </summary>
        /// <param name="renderTarget">The bitmap which will have tiles produced</param>
        /// <param name="Paper">The size of the tile to create
        /// iHrozTiles">The number of horizontal tiles
        /// iVertTiles">The number of vertical tiles</param>
        /// <returns></returns>
        private static Size CalculateTiles(RenderTargetBitmap renderTarget, Size Paper, out int iHrozTiles, out int iVertTiles)
        {
            if (renderTarget.Height > renderTarget.Width)
            {
                iHrozTiles = ((int)(renderTarget.Width / Paper.Width)) + 1;
                iVertTiles = ((int)(renderTarget.Height / Paper.Height)) + 1;
            }
            else
            {
                Size switched = new Size(Paper.Height, Paper.Width);
                iHrozTiles = ((int)(renderTarget.Width / Paper.Width)) + 1;
                iVertTiles = ((int)(renderTarget.Height / Paper.Height)) + 1;
            }
            return Paper;
        }

        /// <summary>
        /// Encodes the full size (same as the original) bit map to required encoding.
        /// </summary>
        /// <param name="RootOutputPath"></param>
        /// <param name="fileNamePart"></param>
        /// <param name="pageNum"></param>
        /// <param name="iTotalPages"></param>
        /// <param name="singleFileOuptut"></param>
        /// <param name="renderTarget"></param>
        /// <param name="decoder"></param>
        private static void MakeOutputFile(string RootOutputPath, string fileNamePart, int pageNum, int iTotalPages,
            List<EncoderTypes> singleFileOuptut,
            RenderTargetBitmap renderTarget,
            FileNameDecoder decoder)
        {
            foreach (EncoderTypes encoding in singleFileOuptut)
            {
                FileNamer namer = new FileNamer(RootOutputPath, fileNamePart, pageNum, iTotalPages, encoding);
                try
                {
                    EncoderEncapsulation encoderType = new EncoderEncapsulation(encoding);
                    BitmapEncoder encoder = encoderType.Encoder;
                    //BitmapFrame framed = BitmapFrame.Create(renderTarget);
                    encoder.Frames.Add(BitmapFrame.Create(renderTarget, null,
                        decoder.MakeMetadata(encoding), null));
                    ProduceOutputFile(namer, encoder);

                }
                catch (AccessViolationException ex)
                {
                    Debug.WriteLine(ex);
                }
            }
        }

        /// <summary>
        /// Does the writing of the encoded bitmap to the output file.
        /// 
        /// namer">
        /// 
        private static void ProduceOutputFile(FileNamer namer, BitmapEncoder encoder)
        {
            FileStream pageOutStream = null;
            try
            {
                using (pageOutStream = new FileStream(namer.FileName, FileMode.Create, FileAccess.Write))
                {
                    encoder.Save(pageOutStream);
                    pageOutStream.Close();
                    pageOutStream = null;
                }
            }
            finally
            {
                if (pageOutStream != null)
                    pageOutStream.Dispose();
            }
        }

        /// <summary>
        /// Checks to see if the file exists
        /// </summary>
        /// <param name="namer"></param>
        /// <returns></returns>
        private static bool AlreadyDone(FileNamer namer)
        {
            return File.Exists(namer.FileName);
        }

    }
}

, , , , , , , ,

Leave a comment

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.

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

12 Comments

%d bloggers like this: