Inserting an Image into a Bookmark in an OpenXML WordprocessingML Document

Bookmarks provide a convenient way in WordprocessingML to provide insertion points for various items, such as text, images, etc.  Previously, I have outlined how to programmatically retrieve and replace text within a bookmark.  In a recent project, a client wanted to use bookmarks as insertion points for one or more images.  Using with the replace bookmark text sample code as a starting point, I extended it to include image insertion.  There are a number of additional steps to create an Open XML
package that contains a properly inserted image. The main points are described below and sample code is attached.

In a recent screencast, I discuss the mechanics of how images are handled in OpenXml.  At the end of that screencast, I show how to manually insert an image into a word document package.  In order to implement this in code, the following steps are performed:

  1. Flatten the paragraphs
  2. Create a resource ID for the image
  3. Create the image XElement
  4. Insert the image element into the bookmark
  5. Move the bookmark
  6. Create a new image part and stream the image into that part

Flattening the paragraphs

Flattening the paragraphs is a technique that I use in order to simplify XML operations.  It creates temporarily invalid WordprocessingML, but is useful as an intermediate step.  For example, it is valid for bookmarks to span different levels in the XML hierarchy.  However, for inserting an image in between bookmark start and end tags, our job is significantly easier if the bookmarks are at the same level of hierarchy in the XML.   

Create a Resource ID for the Image

The resource ID links the image part to the main document part. The only constraint for the image resource ID is that it must be unique.  To create a resource ID, the code uses a GUID, strips out the dashes and prepends the number with “r”.

 

string imageRId = "r" + Guid.NewGuid().ToString().Replace("-", String.Empty);  

 

Create the Image XElement

There is no easy way to create the markup for the image other than hand-coding the image element tree.  In order to see this markup, I created a Word document, inserted an image, saved the document, opened it in the Open
XML Package Editor Power Tool for Visual Studio 2010
, and then examined the markup for the image.  A fragment of the markup is shown below.  We can see that the markup for the image is below a w:r element and begins with the w:drawing element:

 

<w:r>
 
<w:drawing>
   
<wp:inline distT="0"
               
distB="0"
               
distL="0"
               
distR="0">
     
<wp:extent cx="2314575"
                 
cy="1647825" />
     
<wp:effectExtent l="0"
                       
t="0"
                       
r="0"
                       
b="0" />
     
<wp:docPr id="1"
               
name="Image1" />
     
<wp:cNvGraphicFramePr>
       
<a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
                             
noChangeAspect="1" />
     
</wp:cNvGraphicFramePr>

Using the PowerTools for OpenXml utility PtOpenXmlUtil.cs, which contains the intialized XName objects for WordprocessingML, I created an image element that produces similar image markup, as shown below.

XElement imageElement =
   
new XElement(W.drawing,
       
new XElement(WP.inline,
           
new XAttribute("distT", "0"),
           
new XAttribute("distB", "0"),
           
new XAttribute("distL", "0"),
           
new XAttribute("distR", "0"),
           
new XElement(WP.extent,
               
new XAttribute("cx", imageWidthExtent),
               
new XAttribute("cy", imageHeightExtent)
           
),
           
new XElement(WP.effectExtent,
               
new XAttribute("l", "0"),
               
new XAttribute("t", "0"),
               
new XAttribute("r", "0"),
               
new XAttribute("b", "0")
           
),
           
new XElement(WP.docPr,
               
new XAttribute("id", "1"),  
               
new XAttribute("name", name)                                        
           
),
           
new XElement(WP.cNvGraphicFramePr,
               
new XElement(A.graphicFrameLocks,
                   
new XAttribute(XNamespace.Xmlns + "a", A.a),
                   
new XAttribute("noChangeAspect", "1"))
           
),
           
new XElement(A.graphic,
                   
new XAttribute(XNamespace.Xmlns + "a", A.a),
                   
new XElement(A.graphicData,
                       
new XAttribute("uri", Pic.pic),
                       
new XElement(Pic._pic,
                           
new XAttribute(XNamespace.Xmlns + "pic", Pic.pic),
                           
new XElement(Pic.nvPicPr,
                               
new XElement(Pic.cNvPr,
                                   
new XAttribute("id", "0"),
                                   
new XAttribute("name", "")
                               
),
                               
new XElement(Pic.cNvPicPr)
                           
),
                           
new XElement(Pic.blipFill,
                               
new XElement(A.blip,
                                   
new XAttribute(R.r + "embed", imageRId)
                               
),
                               
new XElement(A.stretch,
                                   
new XElement(A.fillRect)
                               
)
                           
),
                           
new XElement(Pic.spPr,
                               
new XElement(A.xfrm,
                                   
new XElement(A.off,
                                       
new XAttribute("x", "0"),
                                       
new XAttribute("y", "0")
                                   
),
                                   
new XElement(A.ext,
                                       
new XAttribute("cx", imageWidth),
                                       
new XAttribute("cy", imageHeight)
                                   
)
                               
),
                           
new XElement(A.prstGeom,
                               
new XAttribute("prst", "rect"),
                               
new XElement(A.avLst)
                           
)
                       
)
                   
)
               
)
           
)
       
)
);

There are several input parameters for this image element: the image dimensions, the dimensions of the space reserved for the image (extent), and the resource ID.  The image name is not critical as the image part is referenced by the resource ID, not the image name.

The height and width of the image and the dimensions reserved for it are defined by the attributes in the A:ext and wp:extent elements, respectively.  These values are in EMUs, English Metric Units, where 1 EMU = 1/914400 inch = 1/360000 cm.  In this sample code, the image to be inserted is passed in as a Bitmap object.  The methods that return the width and height for a Bitmap object return the values in a different unit, where 1 unit = 1/96 inch.  Thus, the Bitmap image width and height are first converted into EMUs and then assigned as attribute values in the A:ext and wp:extent elements.   

To understand how these dimensions effect the layout, see the examples below:

Image Size, Extent

Example 1

Example 2

Example 3

<a:ext    

cx=2400000

cy=1600000 />

cx=2400000

cy=800000 />

cx=2400000

cy=1600000 />

<wp:extent

cx=2400000
cy=1600000 />

cx=2400000
cy=1600000 />

cx=1200000
cy=800000 />

Notice in Example 2 that when the image size is smaller than
the space reserved for it (extent), then the space is left unfilled.  And when
the extent dimensions are smaller than the image size, the image is squeezed
into the extent space, as shown in Example 3.

After this image element has been instantiated, it is
inserted between the bookmark start and end elements.

Move the Bookmark

The client requested to be able to insert multiple images at
the same bookmark.  Because the image is inserted between w:bookmarkStart and
w:bookmarkEnd, one of these needed to be moved adjacent to the other. 
Otherwise the current image would be replaced by the new one.  By moving the w:bookmarkEnd
to after the w:bookmarkStart and before the image markup, the next image added
via the method call to the same bookmark moves the existing image(s) to the right.

One important point about bookmark names in Word is that
bookmark names are treated as case-insensitive within Word.  That is, you
cannot create bookmarks by name “A” and “a”; only one will be accepted.   So
when we work with bookmarks within code, we convert the bookmark name to
uppercase to eliminate case-sensitivity.  For example:

XElement bookmark = xDoc.Descendants(W.bookmarkStart)
   
.FirstOrDefault(d =>
       
((string)d.Attribute(W.name)).ToUpper() == bookmarkName.ToUpper());

Create a New Part and Stream the Image into that Part

After adding the image element, the image part is created
and then the image is streamed into that part.  In this example, the image to
be inserted at the bookmark is passed into the method as a Bitmap object: image
The variable imageFormatForSave specifies the format to save the image
part as: e.g., ImageFormat.Png, ImageFormat.Jpeg, etc. 

Lastly, the document’s root element is replaced with the new
root element with the image markup, and then that is put into the
MainDocumentPart.

MainDocumentPart mainPart = doc.MainDocumentPart;
ImagePart imagePart = mainPart.AddImagePart(ImageElement.GetImagePartType (imageFormatForSave), imageRId);
           
using (MemoryStream ms = new MemoryStream())
{
    image
.Save(ms, imageFormatForSave);
   
byte[] ba = ms.ToArray();
   
using (Stream s = imagePart.GetStream(FileMode.Create, FileAccess.ReadWrite))
        s
.Write(ba, 0, ba.GetUpperBound(0) + 1);
}
               
xDoc
.Elements().First().ReplaceWith(newRoot);
doc
.MainDocumentPart.PutXDocument();

The sample code is attached.  It provides several test cases to show the functionality of the image insertion utility.

Download – Example Code