Post Reply 
PNG to Base64 string
12-22-2018, 03:24 AM
Post: #1
PNG to Base64 string
Considering that this:
Code:
AFiles("SomeName"):=G0;
Saves a PNG image in the current app files under the name "SomeName" of the G0 graphic, it is not a stretch to convert this image into a Base64 image string that could be included as a screen capture inside some HTML report for a nice presentable read when opened on a PC.

My question is, has anyone explored this on the Prime? I've used PNG<>Base64 converters online to be able to create HTML pages without external links to images. I'm about to research the method to convert between the formats, but maybe there's an easy method that someone is aware of?
Visit this user's website Find all posts by this user
Quote this message in a reply
12-22-2018, 08:30 PM
Post: #2
RE: PNG to Base64 string
I had some time this morning to investigate this PNG to Base64 business further.

Although I now have something that works, it is not pretty or fast, and certainly not optimized. Here it is for anyone interested, I will try to improve on it when I have more time.

Code:
BinaryString();
Fmt8Bin();

EXPORT base64img()
BEGIN
  LOCAL mylst,mystr:="",mylen,mysubstr,newlst,i,j,b64str:="";
  LOCAL fname:="b64img.txt",fsize,fiter,fcontent,padsz,cur_chunk;
  LOCAL dict:={"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","0","1","2","3","4","5","6","7","8","9","+","/"};
  AFiles(fname):=G0;
  fsize:=AFilesB(fname);
  IF fsize>10000 THEN // lists can not be longer than 10000 items so it's necessary to process in chunks if longer
    fiter:=IP(fsize/10000);
    FOR j FROM 1 TO fiter DO
      fcontent:=AFilesB(fname,(j-1)*10000,10000);
      mystr:=mystr+BinaryString(fcontent);
    END;
    IF FP(fsize/10000) THEN
      fcontent:=AFilesB(fname,fiter*10000,fsize-(fiter*10000));
      mystr:=mystr+BinaryString(fcontent);
    END;
  ELSE
    fcontent:=AFilesB(fname,0,fsize);
    mystr:=mystr+BinaryString(fcontent);
  END;
  mylen:=DIM(mystr);
  FOR i FROM 1 TO IP(mylen/24) DO  // iterate over 24-bit increments, construct the 6-bit integers as strings
    mysubstr:=MID(mystr,(i-1)*24+1,24);
    newlst:=EXPR("{#0"+MID(mysubstr,1,6)+"b,#0"+MID(mysubstr,7,6)+"b,#0"+MID(mysubstr,13,6)+"b,#0"+MID(mysubstr,19,6)+"b}");
    newlst:=SETBASE(newlst,3);
    b64str:=b64str+dict(newlst(1)+1)+dict(newlst(2)+1)+dict(newlst(3)+1)+dict(n​ewlst(4)+1);
  END;
  IF FP(mylen/24) THEN // deal with partial remainders of less than 24 bits
    mysubstr:=MID(mystr,(i-1)*24+1);
    padsz:=6-(DIM(mysubstr) MOD 6);
    IF padsz THEN
      FOR i FROM 1 TO padsz DO
        mysubstr:=mysubstr+"0";
      END;
    END;
    newlst:="{";
    FOR i FROM 1 TO DIM(mysubstr)/6 DO
      newlst:=newlst+IFTE(i>1,",","");
      newlst:=newlst+"#0"+MID(mysubstr,(i-1)*6+1,6)+"b";
    END;
    newlst:=EXPR(newlst+"}");
    newlst:=SETBASE(newlst,3);
    FOR i FROM 1 TO SIZE(newlst) DO
      b64str:=b64str+dict(newlst(i)+1);
    END;
    padsz:=4-(SIZE(newlst) MOD 4);
    IF padsz THEN // pad if necessary
      FOR i FROM 1 TO padsz DO
        b64str:=b64str+"=";
      END;
    END;
  END;
  DelAFiles(fname);
  fsize:=DIM(b64str);
  IF fsize>10000 THEN // again max of 10000
    fiter:=IP(fsize/10000);
    FOR j FROM 1 TO fiter DO
      cur_chunk:=ASC(MID(b64str,(j-1)*10000+1,10000));
      AFilesB(fname,(j-1)*10000):=cur_chunk;
    END;
    IF FP(fsize/10000) THEN
      cur_chunk:=ASC(MID(b64str,fiter*10000+1));
      AFilesB(fname,fiter*10000):=cur_chunk;
    END;
  ELSE
    AFilesB(fname,0):=ASC(b64str);
  END;
END;

// returns a string of sequential binary integers, 
BinaryString(declist)
BEGIN
  LOCAL binlist,k,binstr:="";
  binlist:=SETBASE(declist,1);
  FOR k FROM 1 TO SIZE(declist) DO
    binstr:=binstr+Fmt8Bin(STRING(binlist(k)));
  END;
  RETURN binstr;
END;

// removes # and b characters from the string of the binary integer
// left pads with 0's if not 8 characters
Fmt8Bin(str)
BEGIN
  LOCAL k,strdim;
  str:=MID(str,2,DIM(str)-2);
  strdim:=DIM(str);
  IF strdim<8 THEN
    FOR k FROM strdim+1 TO 8 DO
      str:="0"+str;
    END;
  END;
  RETURN str;
END;

I confirmed the results with https://codebeautify.org/base64-to-image-converter Pretty neat!

It appears that the bulk of the time is spent formatting the binary strings, padding with 0's, etc.
Visit this user's website Find all posts by this user
Quote this message in a reply
12-27-2018, 05:09 AM
Post: #3
RE: PNG to Base64 string
Here is a small sample program that actually creates the HTML file in the current app's AFiles. I've made the screen capture simple so the speed is pretty good for demonstration purposes. With more complex screen content of course it gets more involved and slower.
Code:
WriteBase64();
BinaryString();

EXPORT base64img()
BEGIN
  LOCAL fname:="b64img.html",htmlcontent,fsize,fiter,cur_chunk,j;
  RECT_P(G0,0,0,319,239,#000000h,#CCCCCCh);
  TEXTOUT_P("Sample Screen Capture",G0,5,10,3,#000000h);
  AFiles(fname):=G0;
  htmlcontent:="<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><title>Base64 Screen Capture</title></head><body><body><div><img style=\"display:block; margin:0 auto;\" src=\"data:image/png;base64,"+WriteBase64(fname)+"\" alt=\"Sample Screen Capture\" /></div></body></html>";
  DelAFiles(fname);
  fsize:=DIM(htmlcontent);
  IF fsize>10000 THEN // again max of 10000
    fiter:=IP(fsize/10000);
    FOR j FROM 1 TO fiter DO
      cur_chunk:=ASC(MID(htmlcontent,(j-1)*10000+1,10000));
      AFilesB(fname,(j-1)*10000):=cur_chunk;
    END;
    IF FP(fsize/10000) THEN
      cur_chunk:=ASC(MID(htmlcontent,fiter*10000+1));
      AFilesB(fname,fiter*10000):=cur_chunk;
    END;
  ELSE
    AFilesB(fname,0):=ASC(htmlcontent);
  END;
END;

WriteBase64(fname)
BEGIN
  LOCAL mylst,mystr:="",mylen,mysubstr,newlst,i,j,b64str:="";
  LOCAL fsize,fiter,fcontent,padsz,cur_chunk;
  LOCAL dict:={"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","0","1","2","3","4","5","6","7","8","9","+","/"};
  fsize:=AFilesB(fname);
  IF fsize>10000 THEN // lists can not be longer than 10000 items so it's necessary to process in chunks if longer
    fiter:=IP(fsize/10000);
    FOR j FROM 1 TO fiter DO
      fcontent:=AFilesB(fname,(j-1)*10000,10000);
      mystr:=mystr+BinaryString(fcontent);
    END;
    IF FP(fsize/10000) THEN
      fcontent:=AFilesB(fname,fiter*10000,fsize-(fiter*10000));
      mystr:=mystr+BinaryString(fcontent);
    END;
  ELSE
    fcontent:=AFilesB(fname,0,fsize);
    mystr:=mystr+BinaryString(fcontent);
  END;
  mylen:=DIM(mystr);
  FOR i FROM 1 TO IP(mylen/24) DO  // iterate over 24-bit increments, construct the 6-bit integers as strings
    mysubstr:=MID(mystr,(i-1)*24+1,24);
    newlst:=EXPR("{#"+MID(mysubstr,1,6)+"b,#"+MID(mysubstr,7,6)+"b,#"+MID(mysubstr,13,6)+"b,#"+MID(mysubstr,19,6)+"b}");
    newlst:=SETBASE(newlst,3);
    b64str:=b64str+dict(newlst(1)+1)+dict(newlst(2)+1)+dict(newlst(3)+1)+dict(n​ewlst(4)+1);
  END;
  IF FP(mylen/24) THEN // deal with partial remainders of less than 24 bits
    mysubstr:=MID(mystr,(i-1)*24+1);
    padsz:=6-(DIM(mysubstr) MOD 6);
    IF padsz THEN
      FOR i FROM 1 TO padsz DO
        mysubstr:=mysubstr+"0";
      END;
    END;
    newlst:="{";
    FOR i FROM 1 TO DIM(mysubstr)/6 DO
      newlst:=newlst+IFTE(i>1,",","");
      newlst:=newlst+"#"+MID(mysubstr,(i-1)*6+1,6)+"b";
    END;
    newlst:=EXPR(newlst+"}");
    newlst:=SETBASE(newlst,3);
    FOR i FROM 1 TO SIZE(newlst) DO
      b64str:=b64str+dict(newlst(i)+1);
    END;
    padsz:=4-(SIZE(newlst) MOD 4);
    IF padsz THEN // pad if necessary
      FOR i FROM 1 TO padsz DO
        b64str:=b64str+"=";
      END;
    END;
  END;
  RETURN b64str;
END;

// returns a string of sequential binary integers from a list of decimal integers
BinaryString(declist)
BEGIN
  LOCAL binlist,k,curstr,binstr:="";
  binlist:=SETBASE(declist,1);
  FOR k FROM 1 TO SIZE(declist) DO
    curstr:=STRING(binlist(k));
    binstr:=binstr+RIGHT("0000000"+MID(curstr,2,DIM(curstr)-2),8);
  END;
  RETURN binstr;
END;

I'll follow up in the next post about the challenge to make this program faster.
Visit this user's website Find all posts by this user
Quote this message in a reply
12-27-2018, 05:27 AM
Post: #4
RE: PNG to Base64 string
To demonstrate how a PNG to Base64 is decoded and then encoded, I'll use a basic block of 3 bytes of data expressed in decimal format as {137, 80, 78}.

Step 1: Convert these bytes to binary format using SETBASE({137,80,78},1). The result is {#10001001b,#1010000b,#1001110b}

Step 2: Format as string, making sure each string is 8 chars long, left-padding with zeroes where required, resulting in:
10001001 01010000 01001110

Step 3: Combine and then break into 6-bit chunks:
100010010101000001001110 to
100010 010101 000001 001110

Step 4: Format as binary integers to get:
{#100010b,#10101b,#1b,#1110b}

Step 5: Format as decimal integers SETBASE({#100010b,#10101b,#1b,#1110b},3) to get:
{#34d,#21d,#1d,#14d}

Step6: Look up the nth character in the Base64 alphabet (see https://en.wikipedia.org/wiki/Base64) for each integer to get:
iVBO

And so you iterate over each three byte block to compile the string, and in the end deal with any fractional remainders.

For anyone more familiar with these things; is there a quicker and simpler way to get from {137, 80, 78} to {34, 21, 1, 14} on the HP Prime? Any suggestions would be appreciated.
Visit this user's website Find all posts by this user
Quote this message in a reply
12-27-2018, 06:16 AM
Post: #5
RE: PNG to Base64 string
Do you actually need Step 5?

— Ian Abbott
Find all posts by this user
Quote this message in a reply
12-27-2018, 08:23 AM (This post was last modified: 12-27-2018 09:31 AM by Didier Lachieze.)
Post: #6
RE: PNG to Base64 string
(12-27-2018 05:27 AM)Jacob Wall Wrote:  For anyone more familiar with these things; is there a quicker and simpler way to get from {137, 80, 78} to {34, 21, 1, 14} on the HP Prime? Any suggestions would be appreciated.

Here is how I would do it :

Code:
L0:={137,80,78}; L1:={};
N:=L0(1)*256^2+L0(2)*256+L0(3);
FOR I FROM 3 DOWNTO 0 DO
  L1(0):=IP(N/64^I);
  N:=N-L1(0)*64^I;
END;

L1 will return {34,21,1,14}, but you can also do the lookup and build the Base64 string directly in the loop rather than building L1.
Find all posts by this user
Quote this message in a reply
12-27-2018, 06:35 PM
Post: #7
RE: PNG to Base64 string
(12-27-2018 06:16 AM)ijabbott Wrote:  Do you actually need Step 5?

I checked, Step 5 was not required. This step had little impact on performance, but certainly makes sense that it's not required.
Visit this user's website Find all posts by this user
Quote this message in a reply
12-27-2018, 06:47 PM (This post was last modified: 12-27-2018 09:09 PM by Jacob Wall.)
Post: #8
RE: PNG to Base64 string
(12-27-2018 08:23 AM)Didier Lachieze Wrote:  Here is how I would do it :

Code:
L0:={137,80,78}; L1:={};
N:=L0(1)*256^2+L0(2)*256+L0(3);
FOR I FROM 3 DOWNTO 0 DO
  L1(0):=IP(N/64^I);
  N:=N-L1(0)*64^I;
END;

L1 will return {34,21,1,14}, but you can also do the lookup and build the Base64 string directly in the loop rather than building L1.

Interesting, this makes sense. I don't work with binary enough to have figured that one out easily. I implemented this method and compared performance to original version, there is a significant improvement. Using a fairly typical screen resulting in a ~26KB file; my times were:
  • Previously posted version: ~14.8s
  • Previous version without Step 5: ~14.7s
  • New version using Didier's method: ~5.2s

I experimented reading 3 bytes at a time with AFilesB, but that proved to be much slower than reading in the maximum number of bytes into a local variable, then processing it 3 bytes per iteration.

Here's the latest version:
Code:
WriteBase64();
Base64String();

EXPORT base64img2()
BEGIN
  LOCAL fname:="b64img2.html",htmlcontent,fsize,fiter,cur_chunk,j;
  AFiles(fname):=G0;
  htmlcontent:="<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><title>Base64 Screen Capture</title></head><body><div><img style=\"display:block; margin:0 auto;\" src=\"data:image/png;base64,"+WriteBase64(fname)+"\" alt=\"Sample Screen Capture\" /></div></body></html>";
  DelAFiles(fname);
  fsize:=DIM(htmlcontent);
  IF fsize>10000 THEN // max of 10000 items in a list
    fiter:=IP(fsize/10000);
    FOR j FROM 1 TO fiter DO
      cur_chunk:=ASC(MID(htmlcontent,(j-1)*10000+1,10000));
      AFilesB(fname,(j-1)*10000):=cur_chunk;
    END;
    IF FP(fsize/10000) THEN
      cur_chunk:=ASC(MID(htmlcontent,fiter*10000+1));
      AFilesB(fname,fiter*10000):=cur_chunk;
    END;
  ELSE
    AFilesB(fname,0):=ASC(htmlcontent);
  END;
END;

WriteBase64(fname)
BEGIN
  LOCAL fcontent,j,b64str:="",fsize,fiter;
  fsize:=AFilesB(fname);
  IF fsize>9998 THEN // lists can not be longer than 10000, 9999 divides by 3 evenly
    fiter:=IP(fsize/9999);
    FOR j FROM 1 TO fiter DO
      fcontent:=AFilesB(fname,(j-1)*9999,9999);
      b64str:=b64str+Base64String(9999,fcontent);
    END;
    IF FP(fsize/9999) THEN
      fcontent:=AFilesB(fname,fiter*9999,fsize-(fiter*9999));
      b64str:=b64str+Base64String(SIZE(fcontent),fcontent);
    END;
  ELSE
    fcontent:=AFilesB(fname,0,fsize);
    b64str:=b64str+Base64String(SIZE(fcontent),fcontent);
  END;
  RETURN b64str;
END;

Base64String(fsize,mylst)
BEGIN
  LOCAL fiter:=IP(fsize/3),L1,N,j,i,mystr:="";
  LOCAL dict:={"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","0","1","2","3","4","5","6","7","8","9","+","/"};
  FOR j FROM 1 TO fiter DO
    N:=mylst(j*3-2)*256^2+mylst(j*3-1)*256+mylst(j*3);
    FOR i FROM 3 DOWNTO 0 DO
      L1:=IP(N/64^i);
      mystr:=mystr+dict(L1+1);
      N:=N-L1*64^i;
    END;
  END;
  IF fsize-fiter*3 THEN // fractional
    IF (fsize-fiter*3)==1 THEN
      N:=mylst(fsize)*256^2;
      FOR i FROM 3 DOWNTO 2 DO
        L1:=IP(N/64^i);
        mystr:=mystr+dict(L1+1);
        N:=N-L1*64^i;
      END;
      mystr:=mystr+"==";
    ELSE // must be 2
      N:=mylst(fsize-1)*256^2+mylst(fsize)*256;
      FOR i FROM 3 DOWNTO 1 DO
        L1:=IP(N/64^i);
        mystr:=mystr+dict(L1+1);
        N:=N-L1*64^i;
      END;
      mystr:=mystr+"=";
    END;
  END;
  RETURN mystr;
END;

EDIT: Removed duplicate <body> tag
Visit this user's website Find all posts by this user
Quote this message in a reply
12-27-2018, 08:41 PM
Post: #9
RE: PNG to Base64 string
I didn't try your latest version nor examined it thoroughly, but in htmlcontent:="..." you have <body> two times.

Geocaching Coords Solver, Number of triangles Puzzle, Snooker Counter
Find all posts by this user
Quote this message in a reply
Post Reply 




User(s) browsing this thread: