API
@-Formulas
JavaScript
LotusScript
Reg Exp
Web Design
Notes Client
XPages
 
Converting Web Attachment to Image Resource
While working on a web only project, the customer needed the ability to upload images to their web site for use in other pages. I could have chosen to store the attchments in documents, but there are a couple of disadvantages with that method: (1) the refrences to the image would have to be of the format of "/<db path>/0/<document unid>/$File/<image name>", which is not generally intuitive, and (2) the images, referenced in this manner, are not cached, which can be a performance bottleneck.

I decided that image resources would be a lot better way to go. The references are more straightforward - "/<db path>/<image name>" and the images are cached for better performance. So, the question becomes, how do you convert a file attachment into an image resource?

Before I get into the code, let me give you some warnings:
  1. This agent interacts with the file system on the server. So the agent itself needs to be marked as level 2 on the security tab, and the agent signer needs the authority to perform the restricted operations of interacting with the file system.
  2. Creating an image resource requires designer or higher access to the database. So even though we're creating an image resource programmatically, we cannot bypass the Notes security, so the agent signer must be at least a designer of the database.
  3. This agent uses Domino XML (DXL) to bring in the image resource. This is not an API call. But this tip is in the API category. So, if you want to get picky, this tip is in the wrong category on the web site. But working with DXL more closely resembles performing API routines. So I felt this category was more appropriate than the LotusScript category or another category.
  4. This code only works with GIF and JPG (or JPEG) images. There are other image types that work on the web, but after talking with the customer it was decided that limiting the number of image types was a preferred method. However, there is nothing in the code that validates the file extension. You may want to add that.
  5. There is nothing in the code that checks for duplicate images. If you do submit a second image with the same name, you'll end up with 2 image resources with the same name in the database. I don't know if there's a certain way to tell which image will be used when it's referenced, so it would be a good idea to check for duplicate images.
  6. I stripped out the error checking (to shorten the code) and made some other modifications to the agent shown here. You will WANT to put error checking in your agent. There are lots of things that can go wrong when you're working with the file system and DXL (file/path errors, DXL import errors, etc), so you will need to be aware of what error happened and where it happened.
In order to figure out how to do this conversion, I worked backwards. Using the DXL Utilities built into Domino Designer, I exported a GIF image resource and a JPG image resource to see the XML that was created. This was the XML that my code would need to create to bring in the image. I noticed in the XML that there is a field called $MIMEType that is part of the output. That gave me a clue that I'd need to use MIME to work with a text representation of the image. From there, I was able to start building the form and agent.

The form is a very basic web form. There's a file upload control on the form so the user can attach the file. There's a field called SaveOptions set to "0" so the submitted form is never actually saved. And the Web Query Save on the agent calls the agent shown below. Obviously, you can spruce up the form all you want and put things like a $$Return on it, but the form is not the purpose of this tip.

The query save agent is a LotusScript agent. I set all my web agents to be run manually from the agent list so they don't accidentally get kicked off from the Notes client. The agent must be set to target "none" (otherwise the agent won't run) and must be set to security level 2 (see warnings above). In order to make the agent more generic (allow for multiple file upload controls instead of just the one I used), I set up the a loop to go through all the attachments on the submitted form:

Sub Initialize
   Dim session As New NotesSession
   Dim context As NotesDocument
   Dim attachments As Variant
   Dim i As Integer
   
   Set context = session.DocumentContext
   attachments = Evaluate("@AttachmentNames", context)
   For i = 0 To Ubound(attachments)
      Call ProcessAttachment(session, context, Cstr(attachments(i)))
   Next
End Sub

The "ProcessAttachment" subroutine processes one attachment at a time. This subroutine is a "manager" that goes through the steps needed to accomplish the task of moving from an attachment on a web page to an image resource. I won't talk too much about this routine, but instead go into more detail on the subroutines that this "manager" calls.

Sub ProcessAttachments(session As NotesSession, context As NotesDocument, imageName As String)
   Dim directory As String
   Dim db As NotesDatabase
   Dim tempDoc As NotesDocument
   Dim entity As NotesMimeEntity
   Dim imageSize As Long
   
   Set db = session.CurrentDatabase
   directory = session.GetEnvironmentString("Directory", True)
   If Instr(directory, "\") <> 0 And Left(directory, 1) <> "\" Then directory = directory & "\"
   If Instr(directory, "/") <> 0 And Left(directory, 1) <> "/" Then directory = directory & "/"
   directory = directory & "temp"
   If Instr(directory, "\") <> 0 Then directory = directory & "\" Else directory = directory & "/"
   
   Call ExtractFile(context, directory, imageName)
   
   Set tempDoc = db.CreateDocument
   Call CreateMIMEEntity(session, directory & imageName, tempDoc, entity, imageSize)
   
   Call CreateDXLFile(session, directory & imageName, entity, imageSize)
   
   Call ImportDXLFile(session, directory & imageName, db)
   
   Kill directory & imageName
   Kill directory & Strleftback(imageName, ".") & ".dxl"
End Sub

The only thing I'll point out with the code is that the server's notes.ini file is scanned to find out the full path to the data directory. (That's another restricted operation, by the way). The "temp" directory off the standard data directory is used as the directory to temporarily hold the image and the XML file. Note that this directory doesn't normally exist, so you'll have to create it or use another directory. I didn't want to use the data directory itself because there are already images in the data directory (binary.gif, error.gif, etc.) and I didn't want to overwrite/delete a standard Domino image.

As you can tell from the code, there are 5 total steps in the process (after the directory is computed). Step 1 is to take the attachment on the submitted document and put it on the server. Step 2 is to convert that attachment, now on the server, into a MIME entity. This will be a text representation of the attachment. Step 3 is to put the XML around that text representation of the attachment into a DXL file on the server. Step 4 is to bring in the DXL file into the database, which actually creates the image resource design element. Step 5 is to clean up our temporary files.

Let's look at step 1 - detach the attachment to the server:

Sub ExtractFile(context As NotesDocument, directory As String, imageName As String)
   Dim attachment As NotesEmbeddedObject
   
   Set attachment = context.GetAttachment(imageName)
   Call attachment.ExtractFile(directory & imageName)
End Sub

This a pretty straightforward subroutine. The calling function knows the name of the attachment and passes it in. The code gets a hold of that attachment, and then detaches it to the server. The directory is the "temp" directory computed in the calling function.

Step 2 creates a MIME entity out of the image that is now on disk:

Sub CreateMIMEEntity(session As NotesSession, imagePath As String, tempDoc As NotesDocument, entity As NotesMimeEntity, imageSize As Long)
   Dim stream As NotesStream
   
   Set stream = session.CreateStream
   If Not stream.Open(imagePath) Then
      Error 4000, "Cannot open file " & imagePath & " for processing."
   End If
   
   imageSize = stream.Bytes
   
   Call tempDoc.ReplaceItemValue("Form", "Temporary Document")
   Set entity = tempDoc.CreateMIMEEntity
   If Right(Lcase(imagePath), 4) = ".gif" Then
      Call entity.SetContentFromBytes(stream, "image/gif", ENC_NONE)
   Else
      Call entity.SetContentFromBytes(stream, "image/jpeg", ENC_NONE)
   End If
   Call entity.EncodeContent(ENC_BASE64)
   Call stream.Close
End Sub

Note that the calling subroutine created the tempDoc document for us. That document is never saved - it is only used in memory. The last two parameters to the subroutine are actually return values - the variables are set by this subroutine and then used by other subroutines.

The first step in this subroutine is to create a stream that is used to read in the image as a binary file. That stream is used to populate a NotesMIMEEntity on the passed in document. A MIME Entity cannot be created on an empty document. So some kind of field needs to be created. So I create a "Form" field on the document. Then I'm able to populate the return variable pointing to the NotesMIMEEntity. The content of the entity is set from the stream (pointing to the image on disk) and the mime type of the entity is set to either image/gif or image/jpeg. The entity is then encoded and the stream is no longer needed because the entity has a representation of the image in memory. Technically, at this point, the on disk image could be deleted. But I wait until Step 5 to do all the cleanup.

Step 3 builds the XML file that will be used. It is by far the longest subroutine, but that is only because of all the hard-coded text that needs to be written to the XML file. A more generic solution would be to have a profile document in the database with 2 fields - one with the XML before the image and the other with the XML after the image. That would shorten this subroutine quite a bit.

Sub CreateDXLFile(session As NotesSession, imagePath As String, entity As NotesMimeEntity, imageSize As Long)
   Dim stream As NotesStream
   Dim directory As String
   Dim imageName As String
   
   If Instr(imagePath, "\") <> 0 Then
      directory = Strleftback(imagePath, "\") & "\"
      imageName = Strrightback(imagePath, "\")
   Else
      directory = Strleftback(imagePath, "/") & "/"
      imageName = Strrightback(imagePath, "/")
   End If
   
   Set stream = session.CreateStream
   IfNot stream.Open(directory & Strleftback(imageName, ".") & ".dxl", "ISO-8859-1") Then
      Error 4001, "Cannot create file " & directory & Strleftback(imageName, ".") & ".dxl on the server."
   End If
   Call stream.WriteText({<?xml version='1.0' encoding='utf-8'?>})
   Call stream.WriteText({<imageresource name='} & imageName & {'})
   Call stream.WriteText({ noreplace='true' publicaccess='false' designerversion='7'>})
   If Right(Lcase(imageName), 4) = ".gif" Then
      Call stream.WriteText({<gif>})
      Call stream.WriteText(entity.ContentAsText)
      Call stream.WriteText({</gif>})
   Else
      Call stream.WriteText({<jpeg>})
      Call stream.WriteText(entity.ContentAsText)
      Call stream.WriteText({</jpeg>})
   End If
   Call stream.WriteText({<item name='$FileSize' sign='true'><numberlist><number>})
   Call stream.WriteText(Cstr(imageSize) & {</number></numberlist></item>})
   Call stream.WriteText({<item name='$MimeType' sign='true'><textlist><text>})
   If Right(Lcase(imageName), 4) = ".gif" Then
      Call stream.WriteText({image/gif})
   Else
      Call stream.WriteText({image/jpeg})
   End If
   Call stream.WriteText({</text></textlist></item>})
   Call stream.WriteText({<item name='$FileModDT' sign='true'><datetimelist><datetime>})
   Call stream.WriteText(Format$(Now, "YYYYMMDD") & "T" & Format$(Now, "HHMMSS") & ",00-00")
   Call stream.WriteText({</datetime></datetimelist></item>})
   Call stream.WriteText({</imageresource>})
   Call stream.Close
End Sub

The first thing this agent does is build the file path to the DXL file. The path will be the same as the image (in the "temp" directory) except the extension will be ".dxl" instead of ".gif" or ".jpg". Next, the file is opened (which creates the file) and the character set is established. Then some text is written to the file. You should export an image using the DXL Utilites built into Domino Designer to get a feel for the XML. The setting "noreplace=true" set the flag "do not allow design refresh/replace to modify" on the resulting image resource. There is a different XML tag for gif files and jpeg files. That will be a warning if you want to expand this to other file types - you will need to use the DXL Utilities to see what Domino generates for the other file types and mimic what Domino does.

After the <gif> or <jpeg> XML tag, the text representation of the image is brought in. That's done by taking the image stored in the MIME Entity and writing out the text of the image. Then the <gif> or <jpeg> tag is closed.

After the image comes a few fields that end up on the design element. One is the file size, which was passed in as a parameter (the actual value came from step 2). Another is the MIME Type. And the last is the modified date/time of the file. This really could be anything - you could hard-code it to 01/01/1980 at midnight if you want. My code uses "right now" as the modified date/time. But it has to convert it to a standardized format. The ",00-00" at the end of the time is fractions of a second.

The stream is closed at the end, which actually finishes writing the text file to disk. Some of the DXL generated by Domino when you do an export of an image design element is not needed - the creation date of the design element for example. I only included stuff that I really thought was necessary. (In fact, the file size isn't truly needed, but it's nice to show that value in the view of image resources in designer).

Step 4 brings in the DXL file, which creates the actual image resouce design element.

Sub ImportDXLFile(session As NotesSession, imagePath As String, db As NotesDatabase)
   Dim stream As NotesStream
   Dim importer As NotesDXLImporter
   Dim dxlPath As String
   
   dxlPath = Strleftback(imagePath, ".") & ".dxl"
   Set stream = session.CreateStream
   If Not stream.Open(dxlPath, "ISO-8859-1") Then
      Error 4002, "Cannot open file " & dxlPath & " after it was created."
   End If
   Set importer = session.CreateDXLImporter(stream, db)
   importer.ReplaceDBProperties = False
   importer.ReplicaRequiredForReplaceOrUpdate = False
   importer.DesignImportOption = DXLIMPORTOPTION_CREATE
   Call importer.Process
   Call stream.Close
End Sub

The path to the DXL file can be computed just like in step 4 (it's based on the image name). A DXL importer object is created. The input of the importer is the XML file on disk. The output is the current database. The importer is not going to update any database properties, and it also shouldn't check the database replica information. (That replica information was actually not written to the XML, so the import would fail if the replica was required). The importer is going to create new design elements (1 design element, to be specific). Then the importer is processed and the DXL file is closed.

That's it. Step 5 was back in the "manager" function and it does the cleanup of deleting the two files. You can now proceed to convert an image, attached to a document on the web, into an image resource design element.