User image download from in webgl app

Looking to let a user export a screenshot or other image from within a webgl app so I want to give a user the contents of a Texture2D in effect.
As much a js/php question as Unity really

Now I can save image to a server and then direct them to the URL but seems very wasteful when the data is to hand so can it be done another way?

I use megagrab for this. Aviable in the asset store. Works perfect for HiRes screenshot downloads, or upload them direct to a defined location.

Yes but does not handle download through webgl right? It is not hard to do the grab part with a Render Texture, its offering to download.
Guess it may have to be uploaded to a URL and the user pointed to that

my solution for webGL: Megagrab to create the screenshot, and following code to download it:

    [DllImport("__Internal")]
    private static extern void ImageDownloader(string str, string fn);

    public static byte[] ssData = null;
    public static string imageFilename = "";
    void DownloadScreenshot ()
    {
        if(ssData != null)
        {
            Debug.Log("Downloading..."+imageFilename);
            ImageDownloader(System.Convert.ToBase64String(imageData), imageFilename);
        }
    }

.jslib in the Assets/Plugins folder: (ImageDownloader.jslib)

var ImageDownloaderPlugin = {
  ImageDownloader: function(str, fn) {
      var msg = Pointer_stringify(str);
      var fname = Pointer_stringify(fn);
      var contentType = 'image/jpeg';
      function fixBinary (bin)
      {
        var length = bin.length;
        var buf = new ArrayBuffer(length);
        var arr = new Uint8Array(buf);
        for (var i = 0; i < length; i++)
        {
              arr[i] = bin.charCodeAt(i);
        }
        return buf;
      }
      var binary = fixBinary(atob(msg));
      var data = new Blob([binary], {type: contentType});
      var link = document.createElement('a');
    link.download = fname;
    link.innerHTML = 'DownloadFile';
    link.setAttribute('id', 'ImageDownloaderLink');
    if(window.webkitURL != null)
    {
        link.href = window.webkitURL.createObjectURL(data);
    }
    else
    {
        link.href = window.URL.createObjectURL(data);
        link.onclick = function()
        {
            var child = document.getElementById('ImageDownloaderLink');
            child.parentNode.removeChild(child);
        };
        link.style.display = 'none';
        document.body.appendChild(link);
    }
    link.click();
  }
};
mergeInto(LibraryManager.library, ImageDownloaderPlugin);
2 Likes

thanks was having trouble finding something like that, will give it a try

Thanks A LOT for the heads up, sumpfkraut, you just saved me a lot of headaches !

Thanks for the solution. Does anyone have an idea to convert/expand this to a generic file downloader to cover other content types like doc, pdf…etc.? I’m not a Javascript coder but when I skimmed the code I couldn’t find anything image specific except the content type. Any ideas?

This is the one I use personally for different file types. You simply pass it a byte[ ] array, the length of the byte array and the desired file name.

The mouse event is a little bit of a hack to get it to download but otherwise it’s fairly simple code.

DownloadFile : function(array, size, fileNamePtr)
{
    var fileName = UTF8ToString(fileNamePtr);
 
    var bytes = new Uint8Array(size);
    for (var i = 0; i < size; i++)
    {
       bytes[i] = HEAPU8[array + i];
    }
 
    var blob = new Blob([bytes]);
    var link = document.createElement('a');
    link.href = window.URL.createObjectURL(blob);
    link.download = fileName;
 
    var event = document.createEvent("MouseEvents");
    event.initMouseEvent("click");
    link.dispatchEvent(event);
    window.URL.revokeObjectURL(link.href);
}

C# example in case of a screenshot (CaptureScreenshotAsTexture introduced in 2018.1 function I believe):

[DllImport("__Internal")]
private static extern void DownloadFile(byte[] array, int byteLength, string fileName);

var texture = ScreenCapture.CaptureScreenshotAsTexture();
byte[] textureBytes = texture.EncodeToPNG();
DownloadFile(textureBytes, textureBytes.Length, "screenshot.png");
Destroy(texture);

I used a PNG here but it works for any file type. No base64 string conversion needed.

10 Likes

I just tested JJJohan’s solution, and I give it high marks. I was replacing a hacky no-longer-working solution from 2015, and this simplified my code substantially.

I tried both solutions, even in an empty project using 2018.3.5f1 and my images comes up pitch black (I believe transparent if it’s png). sumpfkraut , JJJohan do you have any ideas what might be wrong?
I tried a lot of stuff past few hours but couldn’t get it to work so had to ask at this point :frowning:

both examples are still working. you are sure your byte array is correct?

I just tried again using exact same js you posted and;

public void DownloadScreenshot()
        {
            var texture = ScreenCapture.CaptureScreenshotAsTexture();
            byte[] textureBytes = texture.EncodeToJPG();
            ImageDownloader(System.Convert.ToBase64String(textureBytes), imageFilename);
        }

byte array looks good in editor, I mean I see some values which shouldn’t be the case with pitch black image. I’m using .net 4.x runtime and api compatibility level. managed stripping level is low. tried using both asm and web assembly.

edit: this is an empty project so there is no other scripts, post processing or fancy camera stuff.

I see that you are encoding the data as base64 before sending it to the jslib. If you’re using the jslib snippet I posted then that won’t work, as it expects you to just pass the byte array as such:

[DllImport("__Internal")]
private static extern void DownloadFile(byte[] array, int byteLength, string fileName);

private void DownloadScreenshot()
{
    Texture2D texture = ScreenCapture.CaptureScreenshotAsTexture();
    byte[] textureBytes = texture.EncodeToJPG();
    DownloadFile(textureBytes, textureBytes.Length, "screenshot.jpg");
    Destroy(texture);
}

I’ve slightly modified my jslib snippet a few posts above so it cleans up better and uses Utf8ToString since it looks like Pointer_Stringify is deprecated in newer Emscripten versions.

I tried your version before and again today but I’m getting same results. At this point I’m pretty sure it’s not code related but there’s something else (locale? unity/build settings? version?). Thanks a lot to you both for responding guys, I really appreciate it. I’ll post again if I can find the issue :frowning:

Edit: actually I just tried it on a totally different machine with EN-US locale (I’m saying this as I had problems with that before) and had the same result. I’m totally confused now.

Edit: It all works now, I think it was because of my wrong wait till end of frame part I never checked. Can’t believe how many hours I wasted on that :frowning: Thanks again for all the help!

and if you want a upscaled screenshot, you can try this:

    IEnumerator RecordUpscaledFrame(int screenshotUpscale)
    {
        yield return new WaitForEndOfFrame();
        int resWidthN = Camera.main.pixelWidth * screenshotUpscale;
        int resHeightN = Camera.main.pixelHeight * screenshotUpscale;
        string dateFormat = "dd-MM-yyyy-HH-mm-ss";

        RenderTexture rt = new RenderTexture(resWidthN, resHeightN, 24);
        Camera.main.targetTexture = rt;
        TextureFormat tFormat = TextureFormat.RGB24;
        Texture2D screenShot = new Texture2D(resWidthN, resHeightN, tFormat, false);
        Camera.main.Render();
        RenderTexture.active = rt;
        screenShot.ReadPixels(new Rect(0, 0, resWidthN, resHeightN), 0, 0);
        Camera.main.targetTexture = null;
        RenderTexture.active = null;
        byte[] bytes = screenShot.EncodeToJPG();
        string filename = resWidthN.ToString() + "x" + resHeightN.ToString() + "px_" + System.DateTime.Now.ToString(dateFormat);
        Object.Destroy(rt);
        Object.Destroy(screenShot);
    }

Alternatively, Screenshot.CaptureScreenshotAsTexture has an integer parameter overload to set the resolution scale ;).

the CaptureScreenshotAsTexture parameter scale the rendered image. the result is a really bad quality.

Interesting, I figured it’d be a bit more useful than simply scaling the existing output. Good to know, thanks.

boa noite pessoal alguem tem um passo a passo de como fazer isso, estou começando