This post is a follow up to some of my previous posts about the Send to Word/Excel features of Dynamics NAV. As you know, it uses a stylesheet (xslt file), that is being read into memory and the codeunit 403 inserts values into the document.
This is definitely one of the cool demo features, when presenting NAV. At this point you dont realize that the Cronus logo is hardcoded into the stylesheets, but think it is just using the logo in the Company Information setup.
My previous post gave you some steps on how to change this. But this can be pretty cumbersome to say it at least. So when having to do it for the I dont know what time again, i decided to create a function that could automatically insert the picture from the Company Information.
In order to do this, there are a few things you need to take into consideration:
- The picture must be encoded to Base64
- The size/resolution of the picture in Company Information might be too big to show at regular size in Word
- We need to calculate the width/height of the picture to maintain the aspect ratio when resizing it
Luckily most of these issues have already been solved, and by doing a quick search on either MIBUSO or Dynamicsuser, you will find the solutions i have used to accomplish this.
Lets start by creating the function for inserting the picture into the stylesheet, and look at its supporting functions later on. In this example i have tried to work with streams only, so we dont have to rely on any write access to filesystems etc. Here we go:
Create a new function in Codeunit 403 Application Launch Management, call it InsertCompanyLogo:
InsertCompanyLogo(StyleSheet : Record "Style Sheet") CompanyInfo.GET; CompanyInfo.CALCFIELDS(Picture); CompanyInfo.Picture.CREATEINSTREAM(InStr); Width := GetBitmapWidth(InStr); CompanyInfo.Picture.CREATEINSTREAM(InStr); Height := GetBitmapHeight(InStr); IF Width > 300 THEN BEGIN Height := ROUND(Height / (Width / 300),1); Width := 300; END;
First we get the Company Information and do a CALCFIELDS to retrieve the Picture BLOB. Next we want to read the content into a InStream variable, which we will use to calculate the dimensions of the picture. As you can see we are using the CREATEINSTREAM twice. This is to reset the pointer, as it would otherwise continue reading from our last read operation. The documentation on streams says that all read operations are done sequantially, so that explains it.
You can see i adjust the size of the logo if it is too big, this might or might not work with your logo.
The 2 functions to get the dimensions are just for reading some specific bytes (see BMP definition here http://en.wikipedia.org/wiki/BMP_file_format#Bitmap_data):
GetBitmapWidth(inStr : InStream) Width : Integer inStr.READ(value); Width := value[19]; Width += value[20] * 256; Width += value[21] * 256 * 256; Width += value[22] * 256 * 256 * 256; GetBitmapHeight(inStr : InStream) Height : Integer inStr.READ(value); Height := value[23]; Height += value[24] * 256; Height += value[25] * 256 * 256; Height += value[26] * 256 * 256 * 256;
So far so good. We have the Picture BLOB available, and we have figured out the dimensions of the picture. Next step, is to convert the picture data to Base64. I found a simple function for this on MIBUSO. Performance wise it is not very good, but it is simple and done in pure C/AL, so i choose it instead of the automation that is available in Commerce Gateway. You can find the original thread here: BASE64 Coding. The code continued in InsertCompanyLogo() is:
CompanyInfo.Picture.CREATEINSTREAM(InStr); TempCompanyInfo.Picture.CREATEOUTSTREAM(OutStr); Base64EncodeStream(InStr,OutStr); TempCompanyInfo.INSERT;
We need 2 variables pointing to our Company Information, one of them just a temporary to store our Base64 encoded value.
Next we need to open the actual stylesheet, and start reading it line by line. We are reading it line by line, since we are searching for specific tags in the file, and need to replace the Base64 encoded lines, which are stored with a certain size and linebreaks. There is a supporting function to read a textline from the Stylesheet called ReadTextLineFromStream, which will be shown later.
The tags we are looking for is the start and end of the w:binData tag.
StyleSheet.CALCFIELDS("Style Sheet"); StyleSheet."Style Sheet".CREATEINSTREAM(InStr); StyleSheet2.COPY(StyleSheet); StyleSheet2."Style Sheet".CREATEOUTSTREAM(OutStr); WHILE (NOT InStr.EOS) DO BEGIN TextLine := ReadTextLineFromStream(InStr); IF (STRPOS(TextLine,'<w:binData') <> 0) THEN StartFound := TRUE; IF (STRPOS(TextLine,'<v:shape id') <> 0) THEN EndFound := TRUE; IF NOT (StartFound OR EndFound) THEN OutStr.WRITETEXT(TextLine);
We have some control variables to figure out where we are in the original stylesheet, and to control whether or not we should transfer the line to the “new” stylesheet.
When we found the end of our Base64 encoded data, we have to write our own Base64 encoded image with its tags in there, and also the tag that actually displays it. This tag also need the dimensions of the picture.
IF EndFound THEN BEGIN OutStr.WRITETEXT('<w:binData w:name="wordml://03000001.png">'); TempCompanyInfo.GET; TempCompanyInfo.CALCFIELDS(Picture); TempCompanyInfo.Picture.CREATEINSTREAM(Base64InStream); REPEAT OutStr.WRITETEXT(ReadTextLineFromStream(Base64InStream)); UNTIL Base64InStream.EOS; OutStr.WRITETEXT('</w:binData>'); OutStr.WRITETEXT( STRSUBSTNO('<v:shape id="_x0000_i1025" type="#_x0000_t75" style="width:%1px;height:%2px">',Width,Height)); StartFound := FALSE; EndFound := FALSE; END; END; StyleSheet2.MODIFY;
Then just modify the new Stylesheet record, and we have saved our new stylesheet with the picture from Company Information.
Here is the 2 other supporting functions:
ReadTextLineFromStream(VAR inStr : InStream) TextLine : Text[1024] c10 := 10; c13 := 13; Stop := FALSE; REPEAT inStr.READ(Char); IF (Char = c10) AND (LastChar = c13) THEN Stop := TRUE; TextLine += FORMAT(Char); LastChar := Char; UNTIL Stop OR inStr.EOS; Base64EncodeStream(VAR inStr : InStream;VAR outStr : OutStream) txt64Chars := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; txt64Chars += 'abcdefghijklmnopqrstuvwxyz'; txt64Chars += '0123456789+/'; WHILE NOT inStr.EOS DO BEGIN nRead := inStr.READ(b); IF nRead < 3 THEN b[3] := 0; IF nRead < 2 THEN b[2] := 0; c[1] := b[1] DIV 4; c[2] := ((b[1] MOD 4) * 16) + (b[2] DIV 16); c[3] := ((b[2] MOD 16) * 4) + (b[3] DIV 64); c[4] := b[3] MOD 64; c[1] := txt64Chars + 1]; c[2] := txt64Chars + 1]; IF nRead > 1 THEN c[3] := txt64Chars + 1] ELSE c[3] := '='; IF nRead > 2 THEN c[4] := txt64Chars + 1] ELSE c[4] := '='; outStr.WRITE(c[1]); outStr.WRITE(c[2]); outStr.WRITE(c[3]); outStr.WRITE(c[4]); nWritten += 1; IF nWritten = 18 THEN BEGIN c[1] := 13; outStr.WRITE(c[1]); c[1] := 10; outStr.WRITE(c[1]); nWritten := 0; END; END;
To implement it i have just placed this function in the routine for InsertStyleSheet() (v5) and InsertFormStyleSheet()/InsertPageStyleSheet() (Version 2009):
InsertStyleSheet(ProgID : Text[38];Name : Text[250];FileName2 : Text[100];FormID : Integer) FileName := APPLICATIONPATH + 'StyleSheets\' + FileName2; IF EXISTS(FileName) THEN BEGIN StyleSheet.INIT; StyleSheet."Style Sheet ID" := CREATEGUID; StyleSheet."Object ID" := FormID; StyleSheet."Object Type" := StyleSheet."Object Type"::Form; StyleSheet."Program ID" := ProgID; StyleSheet.Name := Name; StyleSheet.Date := TODAY; StyleSheet."Style Sheet".IMPORT(FileName); StyleSheet.INSERT; // >> GOTCAL.SN InsertCompanyLogo(StyleSheet); // << GOTCAL.SN END;
Hope you enjoy this little customization, nothing rocket science, but a great time saver when setting up the standard stylesheets. Not to forget the introduction to Base64 encoding and reading bitmap information. Notice, if you have very large bitmaps, the Base64 encoding might take a while, just wait for it :). And again, this is a process that only needs to be run once to update the stylesheets.
For some great information about getting Stylesheets to work in 2009, you might want to visit this thread on MIBUSO: http://www.mibuso.com/forum/viewtopic.php?f=32&t=36267. Does anyone know if this has been changed with the R2 release?
I will add the files to download section shortly, if anyone is interested.
That’s cool.
I’m trying to follow the post, but…
Maybe it’s stupid question, but I have to ask it. Which datatype are variables “value” and “b” (from inStr.READ(value);).
value : Binary100
b : Binary3
c : Char[4]
Hi,
you wrote inyour posting:
…
GetBitmapHeight(inStr : InStream) Height : Integer
inStr.READ(value);
Height := value[23];
Height += value[24] * 256;
Height += value[25] * 256 * 256;
Height += value[26] * 256 * 256 * 256;
…
My questions:
In both functions GetBitmapHeight and GetBitmapWidth:
Is value an array variable? With 22 rsp. 26 dimensions?
Or how else is it defined?
value : Binary100
value : Binary100
Hi,
Did you prepare downloads with the code/fobs?
THANKS!
Thank you very much for this clever tutorial!
I’ve extended my Excel Buffer to store picture etc, but the scaling needed to determine the pictures height and width dynamically.
Without your code this wouldn’t be possible ๐