Stack Overflow when uploading image in Webgl

I have a WebGL application that allows users to upload arbitrary images. I understand that there will have to be some limit to the file size they are allowed to upload, but currently a 4MB image is causing a stack overflow.

To get the image, I have some javascript on the page that uses a file picker html element, and onLoadEnd it sends a message to Unity containing the base64 encoded string of that image. Unity decodes that string to a byte[ ] and creates a Texture2D to display in the menu. Unfortunately, a 4MB image is allocating something like 30MB on the stack, and in WebGL that simply crashes it.

Is there a way to alloc less memory when creating the texture2d and sprite that I need?
If not, is there a way to tell Unity to give me a bigger stack size?

Relevant code snippets :
In my html:

function readFile()
{
  // Get the list of files from the input tag
  var files = document.getElementById('files').files;
  if(!files.length)
  {
    return;
  }
  // Get the first file form the list
   var file = files[0];
  // Instantiate the FileReader
  var reader = new FileReader();
  reader.onloadstart = function(evt)
  {
    if(evt.target.readyState == FileReader.LOADING)
    {
      //ShowImageLoader();
    }
  };
  // Wait until the reader is ready
  reader.onloadend = function(evt)
  {
    // Check the state of the reader to understand when it si done
    if (evt.target.readyState == FileReader.DONE)
    {
      var obj = {
        "name": file.name,
        "bytes": window.btoa(evt.target.result),
        "index": 0,
        "max":0
      };
     
      console.log("pushing image with " + obj.bytes.length + " bytes");
   
      PushImage(obj.bytes); //this is simply a SendMessage helper method. Unity indeed receives the bytes
    }
  };
  // Read the actual byte of the file
    reader.readAsBinaryString(file);
}

In my C# receiver object:

public void UpdateImage(string obj)
  {
      if (!Application.isEditor)
      {
          byte[] image = System.Convert.FromBase64String(obj);//this is the line that crashes it
      
          Texture2D tex = new Texture2D(1, 1);
          tex.LoadImage(image);

          fileCallback("No Name", tex, image);
          Destroy(gameObject);

      }
  }

you could do it in multible steps, just cut the string into multible and do one after another, make sure the string length is always a multible of 4 , i assume you have read the official Microsoft doc about System.Convert.FromBase64String(obj) especially the part about “possible exceptions”

Thanks, I considered splitting it up but originally was thinking that it wouldn’t work. Good to know it needs to be a multiple of 4, and I’ll check the docs again before I begin this endeavor. I’ll post back with results.

Alright I got it working and it works really well actually.

function SendFileToUnity(file) {
    // Instantiate the FileReader
    var reader = new FileReader();

    reader.onloadstart = function (evt) {
        if (evt.target.readyState == FileReader.LOADING) {
            //ShowImageLoader();
        }
    };

    // Wait until the reader is ready
    reader.onloadend = function (evt) {
        // Check the state of the reader to understand when it si done
        if (evt.target.readyState == FileReader.DONE) {
            var bytes = window.btoa(evt.target.result);
            var name = file.name;

            console.log("pushing image with " + bytes.length + " bytes");
            var maxChunkSize = 100000;
            var numChunks = Math.ceil(bytes.length / maxChunkSize);
            console.log("pushing " + numChunks + " chunks");
            for (var i = 0; i < numChunks ; i++) {
                var chunk = bytes.slice(i * maxChunkSize, (i + 1) * maxChunkSize);
                SendMessage(receiver, "UploadChunk", chunk));

            }

            SendMessage(receiver, "FinishImage", name);
        }
    };

    // Read the actual byte of the file
    reader.readAsBinaryString(file);
}

Then the two functions in my c# reciever:

    private List<byte> bytes = new List<byte>();
    public void UploadChunk(string obj)
    {
        UIManager.instance.OpenProgressModal("Processing Image");
        UnityEngine.Debug.Log("Update image");
        byte[] chunk = System.Convert.FromBase64String(obj);
        UnityEngine.Debug.Log("bytes concat");
        for (int i = 0; i < chunk.Length; i++)
        {
            bytes.Add(chunk[i]);
        }
        //bytes = bytes.Concat(chunk).ToList();
        //bytes = bytes.Concat(chunk).ToArray();
        UnityEngine.Debug.Log("got chunk");


    }

    public void FinishImage(string name)
    {
        UnityEngine.Debug.Log("finish image");

        if (!Application.isEditor)
        {
            //byte[] image = System.Convert.FromBase64String(bytes);

            Texture2D tex = new Texture2D(1, 1);
            byte[] byteArray = bytes.ToArray();
            tex.LoadImage(byteArray);

            fileCallback(name, tex, byteArray);
            Destroy(gameObject);

        }
        UnityEngine.Debug.Log("completely done");

        UIManager.instance.CloseProgressModal();

    }

Basically I upload chunks of the image, and for each image, once all the chunks for that image are pushed to unity, I send a message saying all the chunks are done so unity builds the image from the chunks.

2 Likes

With less hassle, you can use a FileManager package from the Unity Asset store.