PNG to Base64 string - Jacob Wall - 12-22-2018 03:24 AM
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?
RE: PNG to Base64 string - Jacob Wall - 12-22-2018 08:30 PM
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(newlst(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.
RE: PNG to Base64 string - Jacob Wall - 12-27-2018 05:09 AM
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(newlst(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.
RE: PNG to Base64 string - Jacob Wall - 12-27-2018 05:27 AM
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.
RE: PNG to Base64 string - ijabbott - 12-27-2018 06:16 AM
Do you actually need Step 5?
RE: PNG to Base64 string - Didier Lachieze - 12-27-2018 08:23 AM
(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.
RE: PNG to Base64 string - Jacob Wall - 12-27-2018 06:35 PM
(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.
RE: PNG to Base64 string - Jacob Wall - 12-27-2018 06:47 PM
(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
RE: PNG to Base64 string - chromos - 12-27-2018 08:41 PM
I didn't try your latest version nor examined it thoroughly, but in htmlcontent:="..." you have <body> two times.
|