Index: trunk/grails-app/conf/Config.groovy
===================================================================
--- trunk/grails-app/conf/Config.groovy	(revision 634)
+++ trunk/grails-app/conf/Config.groovy	(revision 635)
@@ -64,5 +64,7 @@
 globalDirs.logDirectory = globalDirs.catalinaBase ? "${globalDirs.catalinaBase}${fs}logs${fs}" : globalDirs.targetDir
 globalDirs.workDirectory = globalDirs.catalinaBase ? "${globalDirs.catalinaBase}${fs}work${fs}" : globalDirs.targetDir
+globalDirs.tempDirectory = globalDirs.catalinaBase ? "${globalDirs.catalinaBase}${fs}temp${fs}${appName}${fs}" : globalDirs.targetDir
 globalDirs.searchableIndexDirectory = "${globalDirs.workDirectory}SearchableIndex${fs}${appName}${fs}"
+globalDirs.tempInventoryItemPicturesDirectory = "${globalDirs.tempDirectory}InventoryItemPictures${fs}"
 
 /**
Index: trunk/grails-app/controllers/InventoryItemDetailedController.groovy
===================================================================
--- trunk/grails-app/controllers/InventoryItemDetailedController.groovy	(revision 634)
+++ trunk/grails-app/controllers/InventoryItemDetailedController.groovy	(revision 635)
@@ -34,4 +34,27 @@
         }
         forward(action: 'search', params: params)
+    }
+
+    /**
+    * Display the import view.
+    */
+    def importInventoryItemPictures = {
+    }
+
+    /**
+    * Handle the import save.
+    */
+    def importInventoryItemPicturesSave = {
+        def result = inventoryItemService.importInventoryItemPictures(request)
+
+        if(!result.error) {
+            def logFileLink = g.link(controller: "appCore", action: "appLog") {"log"}
+            flash.message = g.message(code: "inventoryItemPictures.import.success", args: [logFileLink])
+            redirect(action:search)
+            return
+        }
+
+        flash.errorMessage = g.message(code: result.error.code, args: result.error.args)
+        redirect(action: importInventoryItemPictures)
     }
 
Index: trunk/grails-app/i18n/messages.properties
===================================================================
--- trunk/grails-app/i18n/messages.properties	(revision 634)
+++ trunk/grails-app/i18n/messages.properties	(revision 635)
@@ -11,4 +11,8 @@
 inventory.import.success=Inventory imported.
 inventory.import.failure=Could not create inventory from supplied file, failed on line {0}, see {1}.
+
+inventoryItemPictures.import.success=Inventory item pictures imported, see {0}.
+inventoryItemPictures.import.failure.no.directory=Import directory on server not found.
+inventoryItemPictures.import.failure.to.unzip=Failed to unzip supplied file.
 
 inventoryItemPurchase.import.success=Inventory item purchases imported.
Index: trunk/grails-app/services/InventoryItemService.groovy
===================================================================
--- trunk/grails-app/services/InventoryItemService.groovy	(revision 634)
+++ trunk/grails-app/services/InventoryItemService.groovy	(revision 635)
@@ -1,2 +1,4 @@
+import org.codehaus.groovy.grails.commons.ConfigurationHolder
+
 /**
 * Provides a service class for the InventoryItem domain class.
@@ -228,8 +230,10 @@
     /**
     * Save an inventory item picture.
+    * @param params An object or map containing at least the inventoryItem ID.
     * @param pictureSource A supported source to get the picture image from.
     * Supported sources:
     * HttpServletRequest e.g: 'request' var from controller to run getFile('file') against.
     * ServletContextResource e.g: grailsApplication.mainContext.getResource('images/logo.png')
+    * File e.g: new File('picture.jpg')
     */
     def savePicture(params, pictureSource) {
@@ -294,4 +298,16 @@
                 pictureInputStream = pictureSource.inputStream
             }
+            else if(pictureSource instanceof File) {
+                pictureFile = pictureSource
+                pictureFileName = pictureFile.name
+
+                if ( !pictureFile.isFile() || (pictureFile.length() == 0) )
+                    return fail(code:"default.file.not.supplied")
+
+                if (pictureFile.length() > Image.MAX_SIZE)
+                    return fail(code:"default.file.over.max.size", args: [Image.MAX_SIZE/kByteMultiplier, "kB"])
+
+                pictureInputStream = new FileInputStream(pictureSource)
+            }
             else {
                     return fail(code:"inventory.item.picture.source.not.supported")
@@ -329,6 +345,101 @@
             return result
 
-        } //end withTransaction
-    }
+        } // end withTransaction
+    } // savePicture
+
+    /**
+    * Import inventory pictures from an uploaded zip file.
+    * @param request The http request to run getFile against.
+    * Get file should return a zip format file containing the inventory item pictures.
+    */
+    def importInventoryItemPictures(request) {
+            def result = [:]
+
+            def kByteMultiplier = 1000
+            def mByteMultiplier = 1000 * kByteMultiplier
+            def fileMaxSize = 500 * mByteMultiplier
+
+            def fail = { Map m ->
+                result.error = [ code: m.code, args: m.args ]
+                return result
+            }
+
+            // Get file from request.
+            def multiPartFile = request.getFile('file')
+            def zipFileName = multiPartFile.originalFilename
+
+            if(!multiPartFile || multiPartFile.isEmpty())
+                return fail(code: "default.file.not.supplied")
+
+            if (multiPartFile.getSize() > fileMaxSize)
+                return fail(code: "default.file.over.max.size", args: [fileMaxSize/mByteMultiplier, "MB"])
+
+            // Check create import dir.
+            def dir = new File(ConfigurationHolder.config.globalDirs.tempInventoryItemPicturesDirectory)
+            def imageFiles = []
+
+            if(!dir.exists())
+                dir.mkdirs()
+
+            if(!dir.isDirectory()) {
+                return fail(code:'inventoryItemPictures.import.failure.no.directory')
+            }
+
+            // Write zip file to disk.
+            def zipOutputFile = new File(dir.absolutePath + File.separator + zipFileName)
+            multiPartFile.transferTo(zipOutputFile)
+
+            // Use ant to unzip.
+            def ant = new AntBuilder()
+            try {
+                ant.unzip(  src: zipOutputFile.absolutePath,
+                                    dest: dir.absolutePath,
+                                    overwrite:"true" )
+            }
+            catch(e) {
+                log.error e
+                return fail(code:'inventoryItemPictures.import.failure.to.unzip')
+            }
+
+            // Recurse through dir building list of imageFiles.
+            def imageFilePattern = ~/[^\s].+(\.(?i)(jpg|png|gif|bmp))$/
+
+            dir.eachFileMatch(imageFilePattern) {
+                imageFiles << it
+            }
+
+            dir.eachDirRecurse { subDir ->
+                subDir.eachFileMatch(imageFilePattern) {
+                    imageFiles << it
+                }
+            }
+
+            // Find inventoryItems by name of picture and call savePicture.
+            def inventoryItemInstance
+            def itemName
+            def savePictureResult
+
+            for(imageFile in imageFiles) {
+                itemName = imageFile.name[0..-5]
+                inventoryItemInstance = InventoryItem.findByName(itemName)
+                if(!inventoryItemInstance) {
+                    log.warn 'InventoryItem not found with name: ' + itemName
+                    continue
+                }
+                if(inventoryItemInstance.picture) {
+                    log.warn 'InventoryItem already has picture: ' + itemName
+                    continue
+                }
+                savePictureResult = savePicture(inventoryItemInstance, imageFile)
+                if(savePictureResult.error)
+                    log.error savePictureResult.error
+                else
+                    log.info 'InventoryItem picture saved: ' + itemName
+            }
+
+            // Success.
+            return result
+
+    } // importInventoryItemPictures
 
 } // end class
Index: trunk/grails-app/views/inventoryItemDetailed/importInventoryItemPictures.gsp
===================================================================
--- trunk/grails-app/views/inventoryItemDetailed/importInventoryItemPictures.gsp	(revision 635)
+++ trunk/grails-app/views/inventoryItemDetailed/importInventoryItemPictures.gsp	(revision 635)
@@ -0,0 +1,35 @@
+<html>
+    <head>
+        <meta name="layout" content="main" />
+        <title>Import Inventory</title>
+        <nav:resources override="true"/>
+        <g:render template="/shared/pictureHead" />
+    </head>
+    <body>
+        <div class="nav">
+            <h1>Import Inventory Pictures</h1>
+        </div>
+        <div class="body">
+            <g:render template="/shared/messages" />
+            <g:uploadForm action="importInventoryItemPicturesSave" onsubmit="return Lightbox.loading();">
+                <div class="dialog">
+                    <table>
+                        <tbody>
+                            <tr class="prop">
+                                <td valign="top" class="name">
+                                    <label for="file">Directory:</label>
+                                </td>
+                                <td valign="top" class="value">
+                                    <input type="file" id="file" name="file" size="40"/>
+                                </td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+                <div class="buttons">
+                    <span class="button"><input class="save" type="submit" value="Create" /></span>
+                </div>
+            </g:uploadForm>
+        </div>
+    </body>
+</html>
Index: trunk/grails-app/views/inventoryItemDetailed/search.gsp
===================================================================
--- trunk/grails-app/views/inventoryItemDetailed/search.gsp	(revision 634)
+++ trunk/grails-app/views/inventoryItemDetailed/search.gsp	(revision 635)
@@ -147,4 +147,8 @@
                                             Import Purchases
                                         </g:link>
+                                        /
+                                        <g:link action="importInventoryItemPictures">
+                                            Import Pictures
+                                        </g:link>
                                     </td>
                                 </tr>
