wordpress hit counter
A Photo Slideshow Presentation Using the Open XML SDK 2.0 - OpenXML Developer - Blog - OpenXML Developer

A Photo Slideshow Presentation Using the Open XML SDK 2.0

Blog

Samples, Demos, and Reference Articles

A Photo Slideshow Presentation Using the Open XML SDK 2.0

  • Comments 1

By Phillip Wong

 

1    INTRODUCTION

The recent Open XML Format SDK 2.0 aims to give coders an easier life dealing with the Open XML document format, abstracting away the complexity of the XML, and giving coders a nicer, strongly-typed method of interacting with the innards of Open XML documents. It also gives access to some useful tools which we will be using. You can download the Open XML Format SDK here.

 

 

Slide generated by the PhotoParser application

I aimed to take advantage of these features when creating the (admittedly unimaginatively named) PhotoParser application, a small command-line tool for crawling a directory of images (e.g. holiday snaps from a digital camera) in JPEG format, and creating a slick PowerPoint slideshow to display each photo alongside a table of EXIF metadata displaying more information about the photo. In this article I show how using the Open XML Format SDK 2.0 (and its included tools) makes generating and manipulating the content of a PowerPoint document easy. The article focuses on the parts of the application that utilise the Open XML SDK. For the full code, please download the source.

 

 

2      CODE SAMPLE STRUCTURE

 

The complete Visual Studio 2008 solution (inside /PhotoParser-src) is included in this code sample, along with a pre-compiled sample binary (inside /PhotoParser-bin).

The VS2008 solution for the PhotoParser itself comprises of the following classes:

  • Program.cs: The command-line application.
  • OpenXML/Template.cs: The Document Reflector generated code of our created PowerPoint template.
  • OpenXML/PhotoSlideGenerator.cs: The code for creating and changing the content of the template’s slides.
  • Exif/ExifMetadata.cs: The code for extracting certain image EXIF metadata from the JPEG photos.

The DocumentFormat.OpenXml.dll assembly is also included with the binary version of the application, along with a directory of some photos you can use to test the application out. A Readme file is included detailing how the command-line application is executed.

 

 

3      USING A TEMPLATE

 

Rather than having to create an entire presentation document from scratch, we will use a PowerPoint document as the basis of our slideshow. This does mean you will need access to PowerPoint or a similar Open XML application in order to create the template.

 

3.1      Designing in PowerPoint

The first step is to fire up PowerPoint and create the basic template for the slides. For PhotoParser’s template, each slide merely consists of:

  • A single image placeholder for the photos (taking up most of the slide’s real-estate);
  • A table filled with a bunch of text placeholders (which I named #Exif1# - #Exif12#). These text placeholders will be replaced with extracted EXIF metadata from each photo.

Once you’ve created the template, save it somewhere as a .pptx. My template for PhotoParser is included in the download if you prefer to use that.

 

 

The template used for PhotoParser.

 

3.2      Document Reflector

One of the tools that the Open XML Format SDK v2.0 provides is the Document Reflector, which can be found in the Open XML Format SDK\V2.0\tools\ directory. This tool takes an existing Open XML document and automatically generates the C# code for creating and saving that document. The generated code can either be viewed on screen or exported to a file.

 

 

The Document Reflector tool.

 

 

If we run it on our template .pptx file, we get code that looks (in part) like the following:

 

private static Presentation GeneratePresentationPart1()

        {

            var element =

                new Presentation(

                    new SlideMasterIdList(

                        new SlideMasterId() { Id = (UInt32Value)2147483708U, RelationshipId = "rId1" }),

                    new SlideIdList(

                        new SlideId() { Id = (UInt32Value)256U, RelationshipId = "rId2" }),

                    new SlideSize() { Cx = 9144000, Cy = 6858000, Type = SlideSizeValues.Screen4x3 },

                    new NotesSize() { Cx = 6858000, Cy = 9144000 },

                    new DefaultTextStyle(

                        new a.DefaultParagraphProperties(

                            new a.DefaultRunProperties() { Language = "en-US" }),

                        new a.Level1ParagraphProperties(

                            new a.DefaultRunProperties(

                                new a.SolidFill(

                                    new a.SchemeColor() { Val = a.SchemeColorValues.Text1 }),

                                new a.LatinFont() { Typeface = "+mn-lt" },

                                new a.EastAsianFont() { Typeface = "+mn-ea" },

                                new a.ComplexScriptFont() { Typeface = "+mn-cs" }

                            ) { FontSize = 1800, Kerning = 1200 }

 

Admittedly, not the prettiest code to look at. However, this generated class provides a CreatePackage(string filePath) method that performs what would be, by far, the most time consuming part of the application to code: creating the presentation document and its structure. This is an incredibly helpful use for Document Reflector, and makes the whole process of Open XML document generation very straight-forward.

For PhotoParser, I exported the document generation code from Document Reflector into its own class file: Template.cs. Apart from changing the namespace (PhotoParser.OpenXML) and the class name (Template), there was no need to touch this code ever again!

Note: You may encounter an “Inconsistent Line Endings” dialog when importing the exported .cs file into Visual Studio. I found that this tends to happen when the generated code contains Base64 encoded data (e.g. images), though it may occur in other cases. Simply say ‘Yes’ to use default CRLF encoding.

 

 

4   GENERATING THE CONTENT

 

At this point we have a template from which we can create PowerPoint documents with our designed “look and feel”, just by using the following code:

 

 

//Load up and create the template presentation

Template template = new Template();

template.CreatePackage(args[1]);

//Load up the template presentation for alteration

PresentationDocument doc = PresentationDocument.Open(args[1], true);

 

This creates a copy of our template presentation and saves it to a specified file path. We then reopen up the file so we can make our modifications. Now we just need to make the new slides for the photos.

 

4.1      Creating New Slides

We can create these slides by cloning the existing slide within the template presentation and changing the placeholders.

public void AddNewSlides(List<string> imagePaths)

        {

            foreach (string im in imagePaths)

            {

                Console.WriteLine("Adding new slide for image: " + im);

                AddNewSlide(_document.PresentationPart, im);

            }

 

            //Need to remember to save parts after altering them!

            _document.PresentationPart.Presentation.Save();

        }

 Once we have the template document created, the List<string> of paths to each photo is passed into the PhotoSlideGenerator so a new slide can be created for each photo:

This simply calls the AddNewSlide method, which performs the actual slide creation, and then calls the Save() method on the presentation, which saves all the changes made to the presentation document file.

 

 

private void AddNewSlide(PresentationPart parent, string imagePath)

        {

            if (_slideTemplate == null) return;

 

The AddNewSlide method takes an image location and creates a slide for that image in the given presentation. To do this, we first use the template slide that exists in the template presentation and make a clone of its contents using the FeedData method to create the new slide. As we want all the slides to be using the same design/layout as the template slide, we simply attach the template’s SlideLayout to the new slide as well.

 

 

 

var newSlidePart = parent.AddNewPart<SlidePart>("newSlide" + _slideId);

 

            //copy the contents of the template slide to the new slide and attach the appropriate layout

            newSlidePart.FeedData(_slideTemplate.GetStream(FileMode.Open));

            newSlidePart.AddPart(_slideTemplate.SlideLayoutPart, _slideTemplate.GetIdOfPart(_slideTemplate.SlideLayoutPart));

 

4.2      Changing the Placeholders

Now we just need to swap out the template placeholders with some actual content. To do this, we create a new ImagePart of JPEG type and assign it a new reference id. We can again use FeedData to set the content of the new ImagePart to the filestream of a given image. The brilliant thing is that when this slide is then saved, the Open XML SDK will automatically write out that image into the package structure automatically, no need to write code to copy files around.

 

After changing the content, we also need to change all the references in the slide’s xml (which, if you recall, is simply a complete copy of the template slide’s xml), to point at our new ImagePart. To do this we can use Microsoft Language Integrated Query (LINQ) to query the slide’s descendant objects for a Blip object, a DocumentFormat.OpenXml.Drawing class which is used to store the embedded image reference. Once we have the Blip object, setting the Embed id reference to that of the ImagePart is straight-forward.

 

 

/*** Alter the new slide's Image contents ***/

            ImagePart imagePart = newSlidePart.AddImagePart(ImagePartType.Jpeg, "imgId" + _slideId);

 

            using (FileStream imageStream = new FileStream(imagePath, FileMode.Open))

            {

                imagePart.FeedData(imageStream);

            }

//change the reference in the slide xml to the new imagepart

            Blip blip = newSlidePart.Slide.Descendants<Blip>().First();

            blip.Embed = "imgId" + _slideId;

 

 

 

Finally, as the images tend to have different sizes, orientations, aspect ratios etc. it is probably necessary to re-centre the image so that the slide stays looking pretty. We can use LINQ here again to gain access to the various elements of the slide and use their offsets and sizes to calculate the new image position. The following code resizes the image, keeping the aspect ratio of the image intact and determining the new width to use. It then centres the image in the middle of the slide horizontally and between the top of the slide and the bottom table vertically:

           

 

//need to readjust the image container size and position depending on the image's aspect ratio

            Image image = Image.FromFile(imagePath);

            double aspectRatio = (double)image.Width / image.Height;

 

            SlideSize slideSize = parent.Presentation.Descendants<SlideSize>().First();

            Transform tableTransform = newSlidePart.Slide.Descendants<Transform>().First();

            Transform2D imageTransform = newSlidePart.Slide.Descendants<Transform2D>().First();

           

 

            //readjust the width while anchoring the height

            imageTransform.Extents.Cx = (Int64)(imageTransform.Extents.Cy * aspectRatio);

 

            //recentre the image to the middle of the slide and between the top and the EXIF data table

            imageTransform.Offset.X = (slideSize.Cx / 2) - (imageTransform.Extents.Cx / 2);

            imageTransform.Offset.Y = (tableTransform.Offset.Y / 2) - (imageTransform.Extents.Cy / 2);

 

 

The text placeholders within our templates table are even easier to replace, again using LINQ. We simply find a text field with the placeholder’s value and then replace that text with another string.

 

private static void SetPlaceholder(SlidePart slidePart, string placeholder, string value)

        {

            List<Text> textListExif1 = slidePart.Slide.Descendants<Text>().Where(t => t.Text.Equals(placeholder)).ToList();

            foreach (Text text in textListExif1) text.Text = value;

        }

 

 

We use the helper methods defined in the ExifMetadata class to extract the photo information and swap them with the placeholders in the template slides, as follows:

 

 

/*** Alter the new slide's Table contents ***/

            using (Stream stream = File.OpenRead(imagePath))

            {

                //using BitmapMetadata to read some of the exif metadata of the image

                BitmapDecoder decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.Default);

                BitmapFrame frame = decoder.Frames[0];

                BitmapMetadata metaData = (BitmapMetadata)frame.Metadata;

 

                //Replace the placeholder text in the slide with exif metadata values

                SetPlaceholder(newSlidePart, "#Exif1#", ExifMetadata.GetManufacturer(metaData));

                SetPlaceholder(newSlidePart, "#Exif2#", ExifMetadata.GetModel(metaData));

                SetPlaceholder(newSlidePart, "#Exif3#", ExifMetadata.GetDateTaken(metaData));

                SetPlaceholder(newSlidePart, "#Exif4#", ExifMetadata.GetDimensions(metaData, image));

                SetPlaceholder(newSlidePart, "#Exif5#", ExifMetadata.GetExposureTime(metaData));

                SetPlaceholder(newSlidePart, "#Exif6#", ExifMetadata.GetIsoSpeed(metaData)); 

                  

      ... etc. ...

 

            }

 

4.3      Sidetrack: Extracting EXIF Data

Here I sidetrack a little to talk about extracting EXIF data from an image in C# (feel free to skip past this section).
 
Windows Presentation Foundation provides the .NET Framework with the BitmapMetadata class (System.Windows.Media.Imaging in PresentationCore.dll), which makes it easy to read/write metadata to/from an image.

I had to make a number of helper methods for extracting a small amount of EXIF metadata from a BitmapMetadata object which are contained in ExifMetadata.cs. The BitmapMetadata class itself provides direct access to some EXIF (and non-EXIF) metadata through properties: ApplicationName, Author, CameraManufacturer, CameraModel, Comment, Copyright, DateTaken, Keywords, Location, Rating, Subject, and Title. Obtaining this information is pretty straight-forward, as shown in the GetDateTaken method:

 

 

public static string GetDateTaken(BitmapMetadata metaData)

        {

            if (!String.IsNullOrEmpty(metaData.DateTaken))

            {

                return metaData.DateTaken;

            }

 

            return DefaultValue;

        }

 

However, for any other EXIF metadata information, it is necessary to use the GetQuery method to retrieve it, and then handle the returned result appropriately. In PhotoParser, I use the following method to query metadata from an image and return it as an object:

 

private static object GetMetaDataInfo(BitmapMetadata metadata, string query)

        {

            if (metadata.ContainsQuery(query))

            {

                return metadata.GetQuery(query);

            }

 

            return null;

        }

 

Dealing with EXIF data can be troublesome, as it requires knowledge of specific tag numbers identifying each piece of information, knowledge of the construction of the query path (e.g. whether it exists in exif or in subifd subpaths), as well as the creation of several methods for handling the expected result type (e.g. rational). Even more methods are needed if you want to convert from the EXIF stored types to more “useful” units (e.g. APEX to f-stop).

 

I would highly recommend using an already existing EXIF library to handle all these nuances, and if you really want to do-it-yourself, the specification available at http://www.exif.org is probably the best reference.

 

An example of a helper method using a query is shown in the following GetDimensions method, which gets the Width (EXIF tag 40962) and Height (EXIF tag 40963) of an image and formats it into a string for display:

 

 

public static string GetDimensions(BitmapMetadata metaData, Image image)

        {

            if (GetMetaDataInfo(metaData, "/app1/ifd/exif/subifd:{uint=40962}") != null && GetMetaDataInfo(metaData, "/app1/ifd/exif/subifd:{uint=40963}") != null)

            {

                return GetMetaDataInfo(metaData, "/app1/ifd/exif/subifd:{uint=40962}") + " x " + GetMetaDataInfo(metaData, "/app1/ifd/exif/subifd:{uint=40963}") + " px";

            }

 

            return image.Width + " x " + image.Height + " px";

        }

 

4.4      Completing the Slide and Presentation

We’ve now changed all the content of the slide, and all that’s left is to save the changes and place the new slide into the presentations list of slides. Saving any part of a document writes its contents out to file.

 

//save the changes to the slide

            newSlidePart.Slide.Save();

 

            //need to assign an id to the new slide and add it to the slideIdList

            //first figure out the largest existing id

            SlideIdList slideIdList = parent.Presentation.SlideIdList;

            uint maxSlideId = 1;

 

            foreach (SlideId slideId in slideIdList.ChildElements)

            {

                if (slideId.Id > maxSlideId) maxSlideId = slideId.Id;

            }

 

            //assign an id and add the new slide at the end of the list

            SlideId newSlideId = new SlideId { Id = ++maxSlideId, RelationshipId = parent.GetIdOfPart(newSlidePart) };

            slideIdList.Append(newSlideId);

 

            _slideId++;

        }

 

Once we’ve done this process for each photo, we need to delete the template slide from the document. This is easy since we know the reference id for this slide from the Document Reflector generated code, and so it can be removed using the following code:

 

public void DeleteTemplateSlide()

        {

            //delete the template slide and any references

            SlideIdList slideIdList = _document.PresentationPart.Presentation.SlideIdList;

 

            foreach (SlideId slideId in slideIdList.ChildElements)

            {

                if (slideId.RelationshipId.Value.Equals("rId2")) slideIdList.RemoveChild(slideId);

            }

 

            _document.PresentationPart.DeletePart(_slideTemplate);

            _document.PresentationPart.Presentation.Save();

        }

 

 

...And we’re all done!

 

5    BONUS: MAKING AN ALTERNATIVE TEMPLATE 

One of the advantages of leaving the Document Reflector generated code unmodified is that it makes switching out the template with an alternate design really easy. All you need to do is rustle up a new PowerPoint document using the same basic elements (an Image and a table of EXIF placeholders), and export that to another class file using Document Reflector. In our PhotoParser application, a couple of changes to the image re-sizing and re-centring code are also needed.
 
 

Our alternate template for PhotoParser

 

I exported this template using Document Reflector to TemplateAlternate.cs, and made the following changes to the code:

·         Program.cs: To make the program use the alternate template.

 

TemplateAlternate template = new TemplateAlternate();

·         PhotoSlideGenerator: To change the image re-centring code in order to centre the photos to the middle of the slide vertically, and between the right-edges of the table and the slide horizontally.

 

//readjust the height while anchoring the width

imageTransform.Extents.Cy = (Int64)(imageTransform.Extents.Cx / aspectRatio);

 

imageTransform.Offset.X = ((slideSize.Cx - ((long)tableTransform.Offset.X + tableTransform.Extents.Cx)) / 2) - (imageTransform.Extents.Cx / 2) + ((long)tableTransform.Offset.X + tableTransform.Extents.Cx);

 

imageTransform.Offset.Y = (slideSize.Cy / 2) - (imageTransform.Extents.Cy / 2);

And that’s it! The app will now create a slideshow using the alternate template.

 

Slide generated by PhotoParser using the alternate template

 

6     Conclusion

 

In this code sample I set out to use the new Open XML SDK 2.0 to create an application for generating a PowerPoint document from a set of photos. I made ready use of the Document Reflector tool to convert a template document into C# code to do a lot of the grunt work. The generated code enabled the application to create new PowerPoint documents with the pre-designed look and feel of the template. Then it was just a matter of swapping the template placeholders with the photos and extracted EXIF information, all made easy using the Open XML SDK 2.0’s support for LINQ to XML. Overall, the supplied libraries and tools of the new Open XML SDK make the matter of building applications for document content generation much, much easier.
Attachment: OpenXmlPhotoParser.zip
  • The attachment provided is not the source code for the article.  Could you please upload the correct source code?  

    Thanks!
Page 1 of 1 (1 items)