Go Home and BLOB an Image Man: How to Import Image Files into the Retail Image Table
Hello again everyone! All of my obsessive Tomamaniacs may have noticed that this entry is on the retail blog instead of the normal ArcherPoint blog. Officially, I’ve joined the ArcherPoint Retail team to do development. In practice, that means that I deal with the retail customers and the specifics of LS Retail's LS Nav product instead of dealing with whatever customer happens to need something when I’m available.
Today, I’ll be writing about doing a bulk import of image files into LS Nav's Retail Image table. This table is used to assign images to an Item or Item Variant record, and it can be accessed by the regular NAV client or the POS client. (You could probably even set up some sort of web service something or other to make it accessible to a web site, although I’d suggest that you do a bulk export of the images to a file system or a content management service instead.) Some of this stuff could also probably be used to import images into Microsoft Dynamics NAV’s item picture field, if you’re using that instead.
I wrote a whole codeunit to do a bulk import of images for a customer, and I’ll go through all of the functions and try to explain what everything is doing so you can do it yourself. You could also probably do some modifications so a user could just vacuum in the contents of a directory somehow, assuming they were good with the naming conventions. (Note that all of this code was written in NAV 2015, so things may be a little different in other versions of NAV. The same general concepts should still apply, though.)
Global variables for the codeunit are as follows:
· FailureCount, integer
· SuccessCount, integer
· FileRecord, record, subtype File
Of those, FailureCount and SuccessCount are used to track what got pulled in successfully or what failed, and FileRecord is used to access out file system.
The first chunk of code is in the OnRun function for the codeunit. Local variables are as follows:
· Window, dialog
· ImagePath, text
· Counter, integer
The code looks like this:
ImagePath := 'C:\Temp\Images’;
IF NOT CONFIRM('Ready to start?',FALSE) THEN BEGIN
ERROR('Did not run import.');
Window.OPEN('Processing #1########## of #2##########\#3#############################\Successfully Imported: #4##########\Failed to Import: #5##########');
Window.UPDATE(2,FileRecord.COUNT - 2);
IF NOT (FileRecord.Name IN ['.','..']) THEN BEGIN
Counter += 1;
IF ImportImage THEN BEGIN
SuccessCount += 1;
FILE.ERASE(ImagePath + '\' + FileRecord.Name);
END ELSE BEGIN
FailureCount += 1;
UNTIL FileRecord.NEXT = 0;
MESSAGE('Processing complete. %1 images imported successfully. %2 failed.',SuccessCount,FailureCount);
That’s the first function, and some stuff is going on there. I wrote this codeunit for a one-time import of images, which is why there are so many hard-coded text strings. If I’d intended it for user consumption, I’d put the value loaded into ImagePath into a setup table somewhere, the value passed to Window.OPEN would be a text constant, and the MESSAGE at the end would also be a text constant. I’m often too lazy to create text constants if I’m just writing a codeunit for one-time use, though. (Don’t tell the other developers. They’ll make fun of me.)
FileRecord is the global Record variable with a Type value of File. That’s how you access the local file system on your computer via NAV code. By doing the SETRANGE on Path, we can tell NAV where to look for our images. You’ll also note that I’m examining FileRecord.Name to be sure the Name isn’t “.” or “..” because those two values will appear if you try to read the contents of a directory. This is similar to how you can go to a command prompt and run the dir command to get the contents of a directory, and the first two entries are a single period and then two periods. The “IF NOT (FileRecord.Name . . .” statement there ignores them. I feel like I tried doing a SETFILTER on FileRecord.Name to get around this and it didn’t work, although your mileage may vary.
The “IF ImportImage” statement there calls a function that we’ll discuss in a few more lines; it’s where the rest of the lifting is done. The one thing I want to denote is that FILE.ERASE function call. It will delete the picture from the directory that we’re importing from. Note that FILE.ERASE doesn’t get undone if you hit an error condition, so you might not want to use it yourself. In this case, I was importing pictures from a temporary directory, and the master images were stored somewhere else, so I wasn’t worried if they got erased; I could just copy them back to my temporary directory.
The ImportImage function has a Boolean return value that I named SuccessfulImport, because I find that clearly-labeled variable names are more important than creative variable names. (I am boring. My favorite character on Sesame Street is Bert.) The local variables are as follows:
- FileExtension, code 10
- ItemNo, code 20
- RetailImageName, code 20
- FileMgt, codeunit, subtype File Management
- ImageFile, file
- TheInstream, instream
- FileNameLen, integer
- RetailImageCounter, integer
- TheOutstream, outstream
- Item, record, subtype Item
- RetailImage, record, subtype Retail Image
- RetailImageLink, record, subtype Retail Image Link
- TempBlob, record, subtype TempBlob (note that although it says “TempBlob” and it’s a record, this is NOT a Temporary record)
- RecRef, a recordref
And the code looks like this:
LOCAL ImportImage() SuccessfulImport : Boolean
SuccessfulImport := FALSE;
FileNameLen := STRLEN(FileRecord.Name);
IF (FileNameLen < 4) OR (FileNameLen > 24) THEN BEGIN
FileExtension := COPYSTR(FileRecord.Name,STRLEN(FileRecord.Name) - 3);
ItemNo := COPYSTR(FileRecord.Name,1,STRLEN(FileRecord.Name) - 4);
IF NOT (FileExtension IN ['.JPG','.PNG']) THEN BEGIN
IF NOT Item.GET(ItemNo) THEN BEGIN
RetailImageName := ItemNo;
RetailImageCounter := 0;
WHILE RetailImage.GET(RetailImageName) DO BEGIN
RetailImageName := FORMAT(RetailImageCounter);
RetailImageName := '0' + RetailImageName;
UNTIL STRLEN(ItemNo) + 1 + STRLEN(RetailImageName) = MAXSTRLEN(RetailImageName);
RetailImageName := ItemNo + '_' + RetailImageName;
RetailImageCounter := RetailImageCounter + 1;
RetailImage.Code := RetailImageName;
FileMgt.BLOBImportFromServerFile(TempBlob,FileRecord.Path + '\' + FileRecord.Name);
// imports without showing the dialog; also imports relative to the server,
// not the client
RetailImage."Image Blob" := TempBlob.Blob;
RetailImageLink."Record Id" := FORMAT(RecRef.RECORDID);
RetailImageLink."Image Id" := RetailImage.Code;
RetailImageLink."Display Order" := 1;
RetailImageLink.TableName := Item.TABLENAME;
RetailImageLink.KeyValue := Item."No.";
SuccessfulImport := TRUE;
There’s a healthy chunk of code there, so let’s break things down a little.
The first IF statement checks that we’ve got a file name that’s long enough that we can parse out the item number without exceeding the maximum length of an item number. I was fortunate here that my customer had well-named files where I could just pull the item number out of the file name without having to try to parse things out. Also, I didn’t have to worry about variants; trying to handle variants would have made this more complex.
The next two IF statements make sure we have a valid file extension and that we’re importing an image for an item that actually exists. I’m just skipping files that don’t have a match, but you might want to do something fancier by throwing an ERROR or moving them into a special directory for problematic images.
The next block of code (containing the WHILE loop) goes through and makes sure that we’ve got a unique image name. Sometimes companies will update item images with new pictures to reflect a changing of the seasons or a fancy rebranding or updated packaging, so we try to assume that there might already be a Retail Image record for the item. We just add this new image in addition to any images that already exist by building a unique name.
After that, we create a Retail Image record and import the file into a BLOB by using the File Management codeunit’s BLOBImportFromServerFile function. I was doing my import from the NAV server, but you might want to do things a little differently if you’re working on a client computer.
Finally, we set our record ref variable to use the Item record we’re targeting, and then we create a Retail Image Link record to tie the Retail Image record (and picture we’ve imported) to the Item record that we need.
If everything goes well, we set our SuccessfulImport Boolean to TRUE and then move on to the next record.
And those are the basics of importing retail images and assigning them to items. You could probably use some similar techniques to import picture files into the regular NAV table for item images as well.
SPECIAL STREET FIGHTER UPDATE: I can’t let a blog update go by without talking about where I am playing Street Fighter. The good news: I got to go to Evo this year and compete in the tournament. The bad news: I went 0-2. Maybe I’ll do better next year. At least I got to eat at the amazing and delicious Monta Ramen; the fried rice I had was the culinary highlight of my trip.
If you have any questions about this blog, contact the Retail experts at ArcherPoint.