0

This is a follow up question to this question.

I'm trying to simplify the way we embed images into our HTML results. The idea for this was inspired by this other question .

Basically what I am trying to do is to write a function-style macro (called %html_embed_image()) that takes an image, and converts it into a base64 format suitable for use in an HTML <img src=""> block.

Given an image such as this:

enter image description here

The usage would be:

data _null_;
  file _webout;
  put "<img src=""%html_embed_image(iFileName=hi.png)"" />";
run;

And the final output would be:

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABaSURBVDhP5YxbCsAgDAS9/6XTvJTWNUSIX3ZAYXcdGxW4QW6Khw42Axne81LG0shlRvVVLyeTI2aZ2fcPyXwPdBI8B999NK/gKTaGyxaMX8gTJRkpyREFmegBTt8lFJjOey0AAAAASUVORK5CYII=" />

The question linked above shows how to do this in regular datastep code, but I am having issues getting this working in a function style macro. I posted a simplified problem I was having earlier and Tom was able to solve that simplified issue, but it doesn't seem to be working in the greater context of the function style macro.

Here is my code so far (the line causing issues is wrapped with two put statements indicating that it is the problem):

option mprint symbolgen;

%macro html_embed_image(iFileName=);

  %local rc fid rc2 str str_length format_length format_mod base64_format base64_string;

  /* ONLY READ IN 16K CHUNKS AS CONVERTING TO BASE64  */
  /* INCREASES SIZE AND DONT WANT TO EXCEED 32K. */
  %let rc  = %sysfunc(filename(filrf, &iFileName, , lrecl=16000));
  %let fid = %sysfunc(fopen(&filrf, i, 16000, b));

  %if &fid > 0 %then %do;

    %let rc = %sysfunc(fread(&fid));
    %do %while(&rc eq 0);  
      %let rc2 = %sysfunc(fget(&fid,str,16000));
      %let str = %superq(str);

      /* FORMAT LENGTH NEEDS TO BE 4n/3 ROUNDED UP TO NEAREST MULTIPLE OF 4 */
      %let str_length = %length(&str);
      %let format_length = %sysevalf(4*(&str_length/3));
      %let format_mod = %sysfunc(mod(&format_length,4));
      %if &format_mod ne 0 %then %do;
        %let format_length = %sysevalf(&format_length - &format_mod + 4);
      %end;
      %let base64_format = %sysfunc(cats($base64x,&format_length,.));
      %put &=base64_format;

      /* CONVERT THE BINARY DATA TO BASE64 USING THE CALCULATED FORMAT */
      %put PROBLEM START;
      %let base64_string = %sysfunc(putc(&str,&base64_format));
      %put PROBLEM END;


      %put &=base64_string;
      /*&base64_string*/ /* RETURN RESULT HERE - COMMENTED OUT UNTIL WORKING */

      %let rc = %sysfunc(fread(&fid)); 
    %end;

  %end;
  %else %do;
    %put %sysfunc(sysmsg());
  %end;

  %let rc=%sysfunc(fclose(&fid));
  %let rc=%sysfunc(filename(filrf));
%mend;

Test the code:

%put %html_embed_image(iFileName=hi.png);

Results in:

ERROR: Expected close parenthesis after macro function invocation not found.

Any tips on how to fix this, or suggestions for workarounds would be great.

Community
  • 1
  • 1
Robert Penridge
  • 8,424
  • 2
  • 34
  • 55
  • have you considered writing a FCMP function for this? This could bridge the gap between Data Step code and Macro. You could use `%sysfunc()` to make your call. – DomPazz Jan 18 '16 at 18:58
  • 1
    @DomPazz I've thought about it but I can't figure out how I would get around the 32k limitation. – Robert Penridge Jan 18 '16 at 21:45
  • If you are calling it within a data step then why does it need to be function style macro? Also within a data step you will have problem with 32K string limit. – Tom Jan 18 '16 at 22:48
  • @Tom I thought that literals could exceed the 32k limit but I just tested it out and they get truncated. – Robert Penridge Jan 18 '16 at 23:30

1 Answers1

0

Just write the text using a data step.

%let fname=hi.png;
data _null_;
  file _webout recfm=n;
  if _n_=1 then put '<img src="data:image/png;base64,';
  length str $60 coded $80 ;
  infile "&fname" recfm=n eof=eof;
  do len=1 to 60;
    input ch $char1.;
    substr(str,len,1)=ch;
  end;
  put str $base64x80.;
return;
eof:
  len=len-1;
  clen=4*ceil(len/3);
  coded = putc(substr(str,1,len),cats('$base64x',clen,'.'));
  put coded $varying80. clen ;
  put '" />';
run;

If you really want to generate text in-line it might be best to add quotes so that you could call the macro in the middle of a PUT statement and not worry about hitting maximum string length.

%macro base64(file);
%local filerc fileref rc fid text len ;

%*----------------------------------------------------------------------
Assign fileref and open the file.
-----------------------------------------------------------------------;
%let fileref = _fread;
%let filerc = %sysfunc(filename(fileref,&file));
%let fid = %sysfunc(fopen(&fileref,s,60,b));

%*----------------------------------------------------------------------
Read file and dump as quoted BASE64 text.
-----------------------------------------------------------------------;
%if (&fid > 0) %then %do;
  %do %while(%sysfunc(fread(&fid)) = 0);
    %do %while(not %sysfunc(fget(&fid,text,60)));
      %let len = %eval(4*%sysfunc(ceil(%length(%superq(text))/3)));
      %if (&len) %then "%sysfunc(putc(%superq(text),$base64x&len..))" ;
    %end;
  %end;
  %let rc = %sysfunc(fclose(&fid));
%end;

%*----------------------------------------------------------------------
Clear fileref assigned by macro,
-----------------------------------------------------------------------;
%if ^(&filerc) %then %let rc = %sysfunc(filename(fileref));

%mend base64;

So then your example data step becomes something like this:

%let fname=hi.png;
data _null_;
  file _webout recfm=n;
  put '<img src="data:image/png;base64,' %base64(&fname) '" />' ;
run;
Tom
  • 47,574
  • 2
  • 16
  • 29
  • Thanks Tom. Yes this will work but it doesn't exactly make the process simple. Ideally I'm looking for some kind of construct that can be called like a function. – Robert Penridge Jan 19 '16 at 15:26
  • I see two issues. One is if you want to embed it in a PUT statement then have the macro function emit quoted strings. Second is that you need to read in multiples of 3 bytes, but most data is blocked in multiples of 512 bytes. You might need to concatenate the results of two FGET() calls over two FREAD() calls so that you can make sure to generate BASE64 strings based on strings that are multiples of 3. – Tom Jan 19 '16 at 15:45
  • @RobertPenridge - why not embed javascript into your document which downloads the base64 image (using the above approach) on the document load event? – Allan Bowe Jan 23 '16 at 15:46
  • @RawFocus It's a dynamic image (chart produced by SAS) and the SAS server is a different machine to the webserver so I would have to SFTP the temporary file(s) to it (and then have something scheduled to clean them up). While possible it adds so many more moving parts and is a very undesirable way of doing things to me. – Robert Penridge Jan 25 '16 at 15:14
  • The different machine isn't an issue (this is a regular setup, the midtier is often physically different to the application server). So long as your application server can stream to _webout then you can definitely call it directly from Javascript. I'll see if I can post an example in the next few days.. – Allan Bowe Jan 25 '16 at 16:58