Display Inline Pictures From a Rich Text Field on a Web Page - Greg Collins
in

InfoPath Dev

This Blog

Syndication

Greg Collins

Display Inline Pictures From a Rich Text Field on a Web Page

To display your InfoPath form data in a Web page, you can write XSL to translate your form into HTML. With a little knowledge of XSL this is a straightforward task. To display the contents of a rich text field you must use xsl:copy-of instead of xsl:value-of. This allows the XHTML content to be displayed in the Web page.

But you will quickly discover that this works for everything in a rich text field except pictures. This is because InfoPath stores pictures using base64 encoding, and then embeds the resulting string directly into the XML content—specifically into the xd:inline attribute of the img element. The XHTML for a picture in a rich text field will look something like the following:

<img style='WIDTH: 100px; HEIGHT: 122px' tabIndex='-1' src='msoinline/a0d1eb893d744127' xmlns:xd='http://schemas.microsoft.com/office/infopath/2003' xd:inline='/9j/4AAQSkZJRgABAQEASABIAAD...'/>

The src attribute tells InfoPath that the encoded picture is stored inline in the xd:inline attribute. But Internet Explorer does not understand the contents of the src attribute and ignores the xd:inline attribute, thus failing to display the picture. Because xsl:copy-of performs a single operation copy of the contents of the node, there is no way to handle the pictures independently. For this we must write our own "copy-of" code. Displaying inline pictures in your Web page requires extra work, but the results are worth it.

In this task we will create a simple InfoPath form with a rich text field that contains inline pictures, and then display the decoded pictures in a Web page. We will use XSL to transform our XML form into HTML, and then use an ASPX page to make the base64 encoded pictures displayable in the Web page. This task requires that you have available to you an installation of Internet Information Services (IIS), version 6.0 or later, and will assume that you have it installed on your local machine. Let's start by designing a new blank form, and then we will create the several support files needed to complete this task.


THE FORM TEMPLATE

Create the test form template:

  1. Open the Controls task pane.
  2. Insert a Rich Text Box into the view.
  3. Double-click the rich text field.
  4. In the Rich Text Box Properties dialog box, change the field name to RichTextField, and then click OK.
  5. Save the form template as InlinePictures2Html.xsn, and then close InfoPath.


THE MYFORM.XML FILE

Create the test form:

  1. Launch the InlinePictures2Html.xsn form.
  2. Insert several pictures into the rich text field.
  3. Save the form as C:\Inetpub\wwwroot\form\myForm.xml, and then close InfoPath.


THE DEFAULT.ASPX FILE

The default.aspx file is the landing page for the Web user. This page initiates the XSL transformation our InfoPath form into an HTML Web page. This is the first of three key files required to display inline pictures from a rich text field in a Web page. Copy the following code into a text editor, and then save the file as C:\Inetpub\wwwroot\form\default.aspx. Note that this is the same folder that you saved the myForm.xml file to.

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Xml" %>
<%@ Import Namespace="System.Xml.Xsl" %>

<!-- Source code developed by Greg Collins -->

<script language="C#" runat="server">
void Page_Load(Object sender, EventArgs e)
{
    // Create an XSL argument to provide the absolute URL to the XML file being transformed.
    // This argument will be availabe as an xsl:param named URL.
    XsltArgumentList oArgs = new XsltArgumentList();
    string sPath = Request.Path.ToString();
    sPath = sPath.Substring(0, sPath.LastIndexOf("/")+1) + "myForm.xml";
    oArgs.AddParam("URL", "", sPath);
    xmlFile.TransformArgumentList = oArgs;
}
</script>

<asp:Xml id="xmlFile" runat="server" DocumentSource="myForm.xml" TransformSource="/transform.xsl" />

Let's examine what the default.aspx file is actually doing. If we were not attempting to decode inline pictures from a rich text field the entire script block would be unnecessary. The purpose of the script block is to get the path from the root folder to the XML file to be transformed, and then pass it as a parameter to the XSL. We are using code to identify this path but you could just as easily hardcode it.

Because Request.Path actually returns the path to the default.aspx file, we must remove the filename portion of the path and replace it with the filename of the XML file to be transformed. We could use Server.MapPath("myForm.xml") here, but this might pose a security issue due to the physical directory structure it reveals; instead we will save that for the decodePicture.aspx file. The modified path becomes the value of a parameter named URL, which is passed to and used in the transform.xsl file.

Values from two attribute on the asp:Xml element must be referenced in the script block: the id attribute and the DocumentSource attribute.  The value of the id attribute is the name of the object whose TransformArgumentList property is being set at the end of the script block. If these two do not match an error will occur. The value of the DocumentSource attribute is the name of the XML file to be transformed and is appended to the path after removing the name of this ASPX file. If these two do not match you will receive undesirable results in your Web page.


THE TRANSFORM.XSL FILE

The transform.xsl file is what transforms our InfoPath form into the HTML that will be displayed in a Web page. This is the second of three key files required to display inline pictures from a rich text field in a Web page. It employs custom "copy-of" functionality to process img elements independently of other XHTML elements within the rich text field. This separation is important in order to specify that the source of the picture will be supplied by an ASPX page.

You must ensure that any namespaces used in your form are also included in the namespace declarations on the xsl:stylesheet element. If you fail to include a namespace the XSL will not be processed.

Copy the following code into a text editor, and then save the file as C:\Inetpub\wwwroot\transform.xsl. This file is saved to the root folder so that it can be easily accessed from anywhere in your Web site.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:cs="C#" xmlns:my="http://schemas.microsoft.com/office/infopath/2003/myXSD/2004-11-03T18:01:22">
<xsl:strip-space elements="*" />
<xsl:output method="html" indent="no" />

<!-- Source code developed by Greg Collins -->

<!-- Parameter specifying the absolute URL of the XML file being transformed -->
<xsl:param name="URL"/>

<xsl:template match="/">
    <h1>Rich Text Pictures Displayed in a Web Page:</h1>
    <xsl:apply-templates />
</xsl:template>

<xsl:template match="my:RichTextField">
    <xsl:apply-templates />
</xsl:template>

<!-- Template for every element except pictures -->
<xsl:template match="my:RichTextField//*[name(.) != 'img']">
    <xsl:element name="{name(.)}">
        <xsl:for-each select="@*">
            <xsl:attribute name="{name(.)}"><xsl:value-of select="."/></xsl:attribute>
        </xsl:for-each>
        <xsl:apply-templates />
    </xsl:element>
</xsl:template>

<!-- Template specifically for pictures -->
<xsl:template match="my:RichTextField//*[name(.) = 'img']">
    <xsl:element name="img">

        <!-- Copy every attribute except for 'src' and 'xd:inline' -->
        <xsl:for-each select="@*[name(.) != 'src' and name(.) != 'xd:inline']">
            <xsl:attribute name="{name(.)}"><xsl:value-of select="."/></xsl:attribute>
        </xsl:for-each>

        <!-- Generate the 'src' attribute to decode the base64 picture -->
        <xsl:attribute name="src">/decodePicture.aspx?u=<xsl:value-of select="$URL"/>&amp;n=<xsl:value-of select="count(preceding::*[name(.) = 'img'])"/></xsl:attribute>
        <xsl:apply-templates />
    </xsl:element>
</xsl:template>

</xsl:stylesheet>

Let's examine what the transform.xsl file is actually doing. The xsl:param element receives its value from the parameter set by the default.aspx page that initiated the transformation. This value is the path from the root folder to the XML file being transformed. This parameter is essential because it allows the code in decodePicture.aspx to load that XML file and decode each of its base64 encoded pictures.

The script block is used to create an index of the inline pictures. Each picture is assigned an incrementally larger number starting from zero. The code in decodePicture.aspx uses these numbers to know which picture is being requested.

Because our sample form is so simplistic, the main template only places a header on the page. The real work is done when the template for the rich text field is processed. If pictures were not saved inline, you would simply be able to use an xsl:copy-of element here and be done with it; but because we are dealing with inline pictures, we must write custom "copy-of" functionality. This requires two templates: one for the img element, and one for everything else.

The first of our custom "copy-of" templates matches the XPath my:RichTextField//*[name(.) != 'img'], or in other words, any element inside the rich text field that is not named img. In this template we copy the element, giving it the same name, copy each of its attributes, and then continue applying templates.

The second of our custom "copy-of" templates matches the XPath my:RichTextField//*[name(.) = 'img'], or in other words, any element inside the rich text field, as long as it is named img. This template is very similar to the previous one, creating an element of the same name, and then copying the attributes; only this time we specifically do not copy  two of the attributes: src and xd:inline. The src attribute will be constructed later and the xd:inline attribute will be left for the decodePicture.aspx page to work with. It is this latter attribute that actually contains the base64 encoded picture.

The reason we wrote custom "copy-of" functionality was to set up the src attribute of the img element so that it will call the decodePicture.aspx page to request the decoded picture. To do this we provide two parameters representing the URL to the XML file we are currently transforming and an index to the picture we are currently working on. The resulting img element, transformed for our HTML Web page, will look something like the following:

<img style='WIDTH: 100px; HEIGHT: 122px' tabIndex='-1' src='/decodePicture.aspx?u=/myForm.xml&amp;n=0'>

The contents of this transform.xsl file are the barebones requirements to ensure that your rich text pictures will be displayed. Beyond this you would write your XSL to transform the remainder of your InfoPath form into HTML for your Web page.


THE DECODEPICTURE.ASPX FILE

The decodePicture.aspx file is where we decode the base64 encoded inline pictures. This is the third of three key files required to display inline pictures from a rich text field in a Web page. This file gets called by the Web browser when it encounters the src attribute of the img element that we created as part of the XSL transformation.

Copy the following code into a text editor, and then save the file as C:\Inetpub\wwwroot\decodePicture.aspx. This file is saved to the root folder so that it can be easily accessed from anywhere in your Web site.

<%@ Page Language="C#" Debug="true" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Xml" %>
<%@ Import Namespace="System.Collections" %>

<!-- Source code developed by Greg Collins -->

<script language="C#" runat="server">
void Page_Load(Object sender, EventArgs e)   
{
    try
    {
        // Get the parameters.
        string sXmlFile = Request.QueryString["u"];
        int nPictureNumber = Convert.ToInt32(Request.QueryString["n"]) * 2;
        if(null == sXmlFile || 0 > nPictureNumber)
            return;
   
        // Get the server path to the XML file.
        sXmlFile = Server.MapPath(sXmlFile);
   
        // Attempt to get the decoded pictures from the cache.
        ArrayList arrPicture = (ArrayList)Cache[sXmlFile];
        System.Drawing.Image oPicture;
   
        // If not already there, decode the pictures and add them to the cache.
        if(null == arrPicture)
        {
            // Load the XML file that was transformed.
            XmlDocument oXml = new XmlDocument();
            oXml.Load(sXmlFile);
       
            // Add the appropriate namespaces so the selection will work.
            XmlNamespaceManager oNsm = new XmlNamespaceManager(oXml.NameTable);
            oNsm.AddNamespace("rt", "http://www.w3.org/1999/xhtml");
            oNsm.AddNamespace("xd", "http://schemas.microsoft.com/office/infopath/2003");
       
            // Get the full list of xd:inline attributes on img elements.
            XmlNodeList oPictures = oXml.SelectNodes("//rt:img/@xd:inline", oNsm);
            arrPicture = new ArrayList();
            for(int i = 0; i < oPictures.Count; i++)
            {
                // Convert each base64 encoded string into an actual picture.
                string sBase64Picture = oPictures.Item(i).Value;
                byte[] bPicture = Convert.FromBase64String(sBase64Picture);
                MemoryStream oMemStr = new MemoryStream();
                oMemStr.Write(bPicture, 0, bPicture.Length);
                oPicture = System.Drawing.Image.FromStream(oMemStr);
               
                // Add each decoded picture and it's format to the array.
                arrPicture.Add(oPicture);
                arrPicture.Add(sBase64Picture.Substring(0, 4));
            }
       
            // Store the picture array in the Application Cache.
            Cache.Insert(sXmlFile, arrPicture, new CacheDependency(sXmlFile));
        }
   
        // Ignore request if the picture number excedes what's in the array.
        if(arrPicture.Count <= nPictureNumber)
            return;
   
        // Obtain the specified picture and format.
        oPicture = (System.Drawing.Image)arrPicture[nPictureNumber];
        string sPictureFormat = (string)arrPicture[nPictureNumber+1];
   
        // Send the decoded picture back to the Web page to be displayed.
        // R0lG = Uppercase R + Number Zero + Lowercase L + Uppercase G
        if("R0lG" == sPictureFormat)
            oPicture.Save(Response.OutputStream, System.Drawing.Imaging.ImageFormat.Gif);
        else
            oPicture.Save(Response.OutputStream, System.Drawing.Imaging.ImageFormat.Jpeg);
    }
    catch(Exception ex)
    {
        // If anything goes wrong do not display an error, just return with no picture.
        return;
    }
}
</script>

Let's examine what the decodePicture.aspx file is actually doing. Two query string parameters are retrieved from the src attribute of the img element produced by our custom XSL transform: the path from the root folder to the XML file containing the inline pictures, and the index of the particular picture to decode. The index is immediately multiplied by two because the array created to store the decoded pictures also stores a sibling piece of information that identifies the picture format. If either parameter is not present, we just return, which results in a broken picture in the Web page.

The sXmlFile variable is immediately converted to the absolute server path using Server.MapPath(sXmlFile). This variable is used to load the XML file, and is also used as the name of the object we store in the cache.

The first thing we do is discover whether this particular XML file has already been parsed and stored in the cache; if so we use the cached version. Caching our pictures significantly improves performance because we do not need to load the XML file each time a decoded picture is requested. If not already in the cache, we must take the time decode the pictures, and then add them to the cache.

To do this we must load the XML file, and then add the namespaces required to be able to reach the pictures in the rich text fields. Two namespaces in particular need to be added: the first is for the rich text field, and the second is for the xd:inline attribute of the img elements. After we've added the namespaces, we get the list base64 encoded pictures, and then one-by-one, decode them.

Each decoded picture is stored in an array along with the four characters at the beginning of the base64 encoded picture. These four characters identify the picture format, whether it is GIF or JPEG. We must determine the picture format because JPEG pictures displayed in GIF format do not look as good, and GIF pictures displayed in JPEG format do not allow for animated pictures. The four characters representing a GIF picture are R0lG. The four characters representing a JPEG picture are /9j/.

Although InfoPath supports more picture formats than GIF and JPEG, for our purposes, these are the only formats that exist. This is because all the other picture formats display correctly in one of these two formats.

With all of these files in place you will be able to correctly display inline pictures from a rich text field in your Web pages. You can test this by launching your Web browser and navigating to http://localhost/form/default.aspx.

One thing to note is that decoded inline pictures will not render as quickly as those loaded from actual picture files stored on a server; but this is a minor issue compared to the convenience of being able to display the complete contents of your rich text fields in a Web page, including the inline pictures.

©2005 Greg Collins. All rights reserved. Licensed to Autonomy Systems, LLC for display on InfoPathDev.com.

Comments

 

Nick Tran said:

Without any coding how do I cut and paste pictures and documents (doc & xls) inot Rich Text Box in Web Browser?

May 12, 2009 10:24 AM
 

sandyleo said:

Can you please let me know what all the changes would be required if I use Apache web server instead of IIS ? I can not host DECODEPICTURE.ASPX file on IIS as I am using Apache in my project.

December 16, 2011 2:23 PM
Copyright © 2003-2019 Qdabra Software. All rights reserved.
View our Terms of Use.