fhessel / esp32_https_server

Alternative ESP32 Webserver implementation for the ESP32 Arduino Core, supporting HTTPS and HTTP.
MIT License
336 stars 124 forks source link

Canvas to JPEG, then file upload to ESP32 SPIFFS fails to decode, error 19 #131

Open frankcohen opened 3 years ago

frankcohen commented 3 years ago

Picojpeg is great. I found it embeded in Bodmer's JPEGDecoder https://github.com/Bodmer/JPEGDecoder/blob/master/src/picojpeg.h and used to display images on a 135x240 pixel OLED display.

I created a 10x10 pixel JPEG encoded image from a Javascript context using this code:

var mywide = video.videoWidth;
mywide = mywide / 6;
canvas.width = 135;
canvas.height = 240;

var image = canvas.getContext('2d').drawImage(video,0,0,10,10,0,0,10,10);
var base64png = canvas.toDataURL("image/jpeg", 1.0);
var dataURL = base64png.replace(/\s/g, '+').replace(/^data:image\/jpeg;base64,/, '');
var nobase = window.atob(dataURL);

var oAjaxReq = new XMLHttpRequest();
oAjaxReq.submittedData = nobase;

oAjaxReq.onreadystatechange = function() {
   console.log("Uploading image");
   console.log(oAjaxReq.responseText);
}

oAjaxReq.onload = function (oEvent)
{
   console.log("Uploaded");
}

oAjaxReq.open("post", "upload", true);

var bound = Date.now().toString(16);
var sBoundary = "----WebKitFormBoundary" + bound;

oAjaxReq.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + sBoundary );

var mydata = "--" + sBoundary + "\r\n";

mydata += "Content-Disposition: form-data; name=\"name\"; filename=\"" + "slice1.jpg" + "\"\r\n";
mydata += "Content-Type: image/png";
mydata += "\r\n";
mydata += "\r\n";

mydata += nobase;

mydata += "\r\n";

mydata += "--" + sBoundary + "--";
mydata += "\r\n";

oAjaxReq.send( mydata );

I am using FHessel's HTTPS Server on ESP32 to receive the form post from a Chrome browser.

https://github.com/fhessel/esp32_https_server

Here is my form post handler code for the server side:

void handle_upload(HTTPRequest * req, HTTPResponse * res)
{ 
  Serial.println("Handle_upload");

  HTTPBodyParser *parser;
  parser = new HTTPMultipartBodyParser(req);
  bool didwrite = false;

  Serial.println( "Handle_upload 2" );

  while(parser->nextField()) {

    Serial.println( "Handle_upload 3" );
    std::string name = parser->getFieldName();
    std::string filename = parser->getFieldFilename();
    std::string mimeType = parser->getFieldMimeType();
    Serial.printf("handleFormUpload: field name='%s', filename='%s', mimetype='%s'\n", name.c_str(), filename.c_str(), mimeType.c_str() );

    if ( ! (filename.rfind("/", 0) == 0) )
    {
      filename = "/" + filename;
    }

    Serial.print("handle_upload Name: "); 
    Serial.println(filename.c_str()  );

    fsUploadFile = LITTLEFS.open( filename.c_str(), "w");            // Open the file for writing in SPIFFS (create if it doesn't exist)

    size_t fileLength = 0;
    didwrite = true;

    while (!parser->endOfField()) {
      byte buf[512];
      size_t readLength = parser->read(buf, 512);
      fsUploadFile.write(buf, readLength);
      fileLength += readLength;
    }

    fsUploadFile.close();
    res->printf("<p>Saved %d bytes to %s</p>", (int)fileLength, filename.c_str() );
  }

  if (!didwrite) {
    res->println("<p>Did not write any file contents</p>");
  }

  delete parser;
  if(didwrite) 
   {                                    // If the file was successfully created
     res->setHeader("Location", "/success");
     res->setStatusCode(303);
   }
   else 
   {
     res->setStatusCode(500);
     res->setStatusText("Upload failed");
   }
 }

The upload works, stores the file in the ESP32 SPIFFS file system (using LittleFS) and here are the file contents (printed out as ASCII character and Hex value):

 =A, ⸮=C3, ⸮=BF, ⸮=C3, ⸮=84, =0, )=29, =10, =1, =0, =1, =3, =2, =5, =1,  =9, =0, =0, =0, =0, =0, =0, =0, =0, =0, =1, =2, =3, =5, =4, =11, =6, =7, =12, =13, 1=31, ⸮=C2, ⸮=81, =14, !=21, "=22, #=23, 2=32, Q=51, ⸮=C2, ⸮=85, ⸮=C3, ⸮=81, ⸮=C3, ⸮=94, ⸮=C3, ⸮=BF, ⸮=C3, ⸮=84, =0, =19, =1, =1, =0, =3, =1, =1, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =3, =5, =7, =4, =6, ⸮=C3, ⸮=BF, ⸮=C3, ⸮=84, =0, -=2D, =11, =1, =0, =1, =3, =2, =3, =3, 
 =D, =0, =0, =0, =0, =0, =0, =0, =0, =0, =1, =2, =3, =11, =4, =12, =5, =15, !=21, 1=31, ⸮=C2, ⸮=A3, ⸮=C3, ⸮=93, =14, "=22, #=23, A=41, Q=51, S=53, T=54, a=61, s=73, ⸮=C2, ⸮=A1, ⸮=C2, ⸮=B1, ⸮=C2, ⸮=B3, ⸮=C3, ⸮=B0, ⸮=C3, ⸮=BF, ⸮=C3, ⸮=9A, =0, =C, =3, =1, =0, =2, =11, =3, =11, =0, ?=3F, =0, ⸮=C3, ⸮=A3, z=7A, ⸮=C3, ⸮=8D, ⸮=C3, ⸮=89, ⸮=C2, ⸮=99, ⸮=C2, ⸮=99, ⸮=C2, ⸮=8F, 4=34, ⸮=C3, ⸮=87, ⸮=C2, ⸮=BA, ⸮=C2, ⸮=A9, ⸮=C3, ⸮=9B, i=69, ⸮=C2, ⸮=9F, ⸮=C3, ⸮=97, ⸮=C3, ⸮=9B, =7F, =1B, ⸮=C3, ⸮=8B, >=3E, ⸮=C2, ⸮=89, ⸮=C3, ⸮=8E, q=71, 3=33, ⸮=C3, ⸮=AC, ⸮=C3, ⸮=8F, ⸮=C3, ⸮=A7, ⸮=C3, ⸮=BB, ⸮=C3, ⸮=97, ⸮=C3, ⸮=B3, k=6B, 
 =D, [=5B, ⸮=C3, ⸮=A6, ]=5D, u=75, \=5C, ⸮=C3, ⸮=A3, l=6C, ⸮=C3, ⸮=95, U=55, }=7D, S=53, =18, ⸮=C3, ⸮=8D, ⸮=C3, ⸮=BD, 1=31, =1A, =8, ⸮=C2, ⸮=89, ⸮=C3, ⸮=B5, ⸮=C2, ⸮=88, ⸮=C2, ⸮=87, ⸮=C2, ⸮=B0, ⸮=C3, ⸮=A1, Y=59, ⸮=C3, ⸮=B2, 
 =D, >=3E, {=7B, }=7D, /=2F, ⸮=C3, ⸮=AE, ⸮=C2, ⸮=B8, ⸮=C3, ⸮=8F, ⸮=C2, ⸮=B8, ⸮=C3, ⸮=AC, ⸮=C3, ⸮=A7, ⸮=C2, ⸮=8A, ⸮=C3, ⸮=AA, ⸮=C2, ⸮=A7, ⸮=C3, ⸮=A8, }=7D, ⸮=C2, ⸮=B4, ⸮=C3, ⸮=96, P=50, ⸮=C2, ⸮=8B, =15, @=40, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, -=2D, ⸮=C3, ⸮=A3, ⸮=C2, ⸮=98, ⸮=C3, ⸮=BC, g=67, =1E, 3=33, ;=3B, ~=7E, ;=3B, =15, ⸮=C3, ⸮=BC, *=2A, ⸮=C3, ⸮=BE, U=55, ⸮=C2, ⸮=A0, ⸮=C3, ⸮=B7, =1D, ⸮=C3, ⸮=AD, ⸮=C3, ⸮=AF, =11, m=6D, ⸮=C3, ⸮=8F, 8=38, ⸮=C2, ⸮=A7, ⸮=C3, ⸮=85, w=77, =1A, =7F, =5, -=2D, ⸮=C2, ⸮=92, ⸮=C3, ⸮=89, k=6B, r=72, ⸮=C3, ⸮=BA, ⸮=C3, ⸮=9B, ⸮=C3, ⸮=99, =1C, ⸮=C2, ⸮=8D, ⸮=C3, ⸮=BF, =0, h=68, ⸮=C3, ⸮=96, j=6A, ;=3B, }=7D, ⸮=C3, ⸮=AB, ⸮=C3, ⸮=9D, ⸮=C2, ⸮=BB, V=56, ⸮=C2, ⸮=BA, ⸮=C3, ⸮=BB, 6=36, m=6D, ⸮=C3, ⸮=98, ⸮=C2, ⸮=B7, ⸮=C3, ⸮=B2, ⸮=C3, ⸮=AC, [=5B, ⸮=C2, ⸮=B5, j=6A, ⸮=C2, ⸮=9E, ⸮=C2, ⸮=9B, V=56, ⸮=C2, ⸮=A8, ⸮=C2, ⸮=A3, ⸮=C3, ⸮=A1, ⸮=C2, ⸮=A2, ⸮=C2, ⸮=9D, ⸮=C3, ⸮=BA, z=7A, ⸮=C2, ⸮=AA, ⸮=C3, ⸮=9E, ⸮=C2, ⸮=A9, ⸮=C2, ⸮=99, ⸮=C2, ⸮=9E, ⸮=C3, ⸮=8B, V=56, ⸮=C2, ⸮=AD, ⸮=C3, ⸮=98, ⸮=C2, ⸮=B7, M=4D, ⸮=C2, ⸮=AB, T=54, ⸮=C3, ⸮=AD, ⸮=C2, ⸮=A2, ⸮=C2, ⸮=9C, ⸮=C3, ⸮=AD, ⸮=C2, ⸮=A7, 5=35, U=55, ⸮=C2, ⸮=8D, ⸮=C3, ⸮=95, M=4D, S=53, ⸮=C3, ⸮=96, ⸮=C2, ⸮=A9, ⸮=C2, ⸮=99, ⸮=C2, ⸮=9C, ⸮=C3, ⸮=8C, ⸮=C3, ⸮=8C, ⸮=C3, ⸮=B5, ⸮=C2, ⸮=99, ⸮=C3, ⸮=AD, ⸮=C3, ⸮=84, t=74, W=57, ⸮=C3, ⸮=9F, ⸮=C2, ⸮=BF, w=77, S=53, v=76, ⸮=C2, ⸮=BB, ⸮=C3, ⸮=B7, ⸮=C3, ⸮=AB, ⸮=C3, ⸮=9F, v=76, ⸮=C2, ⸮=BD, ⸮=C2, ⸮=BB, ⸮=C2, ⸮=AA, ⸮=C3, ⸮=9B, M=4D, 9=39, ⸮=C3, ⸮=9B, L=4C, Q=51, O=4F, ⸮=C2, ⸮=9B, D=44, S=53, L=4C, b=62, ⸮=C2, ⸮=9A, b=62, :=3A, D=44, g=67, =19, ⸮=C2, ⸮=9E, ⸮=C2, ⸮=B9, ⸮=C2, ⸮=96, 
 =A, D=44,  =20, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, =0, ⸮=C3, ⸮=BF, ⸮=C3, ⸮=99, [HTTPS:I] Request: GET /favicon.ico (FID=62)

Picojpeg rejects the file and throws error 19.

I tested Picojpeg using a jpeg file I created in Pixelmator for Mac and Picojpeg works fine. I use the same HTTPS Server code to upload the file contents. I use this HTML:

<form action=upload method=post enctype=multipart/form-data>
<input type=file name=name>
<input class=button type=submit value=Upload>
</form>

This uploads the Jpeg file correctly. And Picojpeg decodes the jpeg image correctly and with no error.

I'm not sure where to go next? Any help would be appreciated.

-Frank

frankcohen commented 3 years ago

I found the Javascript XMLHttpRequest correctly sends the data to the servlet, but the servlet handling the Chrome html form as above also does a Base64 decode of the data before saving it to SPIFFS or an SD card.

    function uploadslice( dacanvas, disnum, slicenum ) {
        doneflag = false;

        var base64jpg = dacanvas.toDataURL("image/jpeg", 1.0);
        var dataURL = base64jpg.replace(/\s/g, '+').replace(/^data:image\/jpeg;base64,/, '');

        var oAjaxReq = new XMLHttpRequest();
    oAjaxReq.submittedData = dataURL;

        oAjaxReq.onload = function (oEvent)
        {
             var httprq = new XMLHttpRequest();
         httprq.onreadystatechange = function() {
             if (httprq.readyState == 4 && httprq.status == 200)
                         console.log("0");
             }

         httprq.open("GET", "https://192.168.1.1/showslice?filename=/slice" + slicenum + ".jpg&display=" + disnum, true); // true for asynchronou
         httprq.send(null);

             console.log( "slicenum = " + slicenum );

             if ( slicenum == 0 )
             {
                uploadslice( canvas1, 4, 1 );
             }
             if ( slicenum == 1 )
             {
                uploadslice( canvas2, 3, 2 );
             }
             if ( slicenum == 2 )
             {
                uploadslice( canvas3, 2, 3 );
             }
             if ( slicenum == 3 )
             {
                uploadslice( canvas4, 1, 4 );
             }
             if ( slicenum == 4 )
             {
                uploadslice( canvas5, 0, 5 );
             }
        }

        oAjaxReq.open("post", "upload", true);
        oAjaxReq.setRequestHeader("Decode64", "true" );

        var bound = Date.now().toString(16);
        var sBoundary = "----WebKitFormBoundary" + bound;

        oAjaxReq.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + sBoundary );

        var mydata = "--" + sBoundary + "\r\n";

        mydata += "Content-Disposition: form-data; name=\"name\"; filename=\"" + "slice" + slicenum + ".jpg" + "\"\r\n";
        mydata += "Content-Type: image/png";
        mydata += "\r\n";
        mydata += "\r\n";

        mydata += dataURL;

        mydata += "\r\n";

        mydata += "--" + sBoundary + "--";
        mydata += "\r\n";

        oAjaxReq.send( mydata );
    }

  // Set up our event listener to run the startup process
  // once loading is complete.
  window.addEventListener('load', startup, false);
})();

This looks like a bug in ESP32_HTTPS_Server to me.

i came up with a workaround... my Javascript code adds a special HTTP header value and the servlet does an extra step to do the Base64 decoding on the fly:

void handle_upload(HTTPRequest * req, HTTPResponse * res)
{ 
  Serial.println("Handle_upload");

  HTTPBodyParser *parser;
  parser = new HTTPMultipartBodyParser(req);
  bool didwrite = false;

  bool decode64 = false;
  std::string decval = req->getHeader( "Decode64" );
  String decstr = decval.c_str();
  if ( decstr == "true" )
  {
    decode64 = true;
    Serial.println( "Decode64 = true" );
  }

  while(parser->nextField()) {

    std::string name = parser->getFieldName();
    std::string filename = parser->getFieldFilename();
    std::string mimeType = parser->getFieldMimeType();
    Serial.printf("handleFormUpload: field name='%s', filename='%s', mimetype='%s'\n", name.c_str(), filename.c_str(), mimeType.c_str() );

    if ( ! (filename.rfind("/", 0) == 0) )
    {
      filename = "/" + filename;
    }

    Serial.print("handle_upload Name: "); 
    Serial.println(filename.c_str()  );

    fsUploadFile = LITTLEFS.open( filename.c_str(), "w");            // Open the file for writing in SPIFFS (create if it doesn't exist)

    size_t fileLength = 0;
    didwrite = true;

    while (!parser->endOfField()) {
      byte buf[512];
      size_t readLength = parser->read(buf, 512);

      if ( decode64 && ( readLength>0 ) )
      {
        size_t outlen;
        unsigned char output[ 512 ];

        mbedtls_base64_decode(output, 512, &outlen, buf, readLength);

        fsUploadFile.write(output, outlen);
        fileLength += outlen;
      }
      else
      {
        fsUploadFile.write(buf, readLength);
        fileLength += readLength;
      }

    }

    fsUploadFile.close();
    res->printf("<p>Saved %d bytes to %s</p>", (int)fileLength, filename.c_str() );
  }

  if (!didwrite) {
    res->println("<p>Did not write any file contents</p>");
  }

  delete parser;

  Serial.print( "LITTLEFS totalBytes=" );
  Serial.print( LITTLEFS.totalBytes() );
  Serial.print( ", usedBytes=" );
  Serial.print( LITTLEFS.usedBytes() );
  Serial.print( ", free bytes=" );
  Serial.println( LITTLEFS.totalBytes() - LITTLEFS.usedBytes() );

  if(didwrite) 
  {                                    // If the file was successfully created
    res->setHeader("Location", "/success");
    res->setStatusCode(303);
  }
  else 
  {
    res->setStatusCode(500);
    res->setStatusText("Upload failed");
  }
}

This is the decoding code. It uses a Base64 decoder built into the ESP32 operating environment:

      if ( decode64 && ( readLength>0 ) )
      {
        size_t outlen;
        unsigned char output[ 512 ];

        mbedtls_base64_decode(output, 512, &outlen, buf, readLength);

        fsUploadFile.write(output, outlen);
        fileLength += outlen;
      }
      else
      {
        fsUploadFile.write(buf, readLength);
        fileLength += readLength;
      }

-Frank