9

Just for testing I downloaded a PNG file via http (in this case from a JIRA server via API)

For the http request I have a quite "standart" class HttpFileManager I just add for completeness:

public static class HttpFileManager
{

    public void DownloadImage(string url, Action<Texture> successCallback = null, Credentials credentials = null, Action<UnityWebRequest> errorCallback = null)
    {
        StartCoroutine(DownloadImageProcess(url, successCallback, credentials, errorCallback));
    }

    private static IEnumerator DownloadImageProcess(string url, Action<Texture> successCallback, Credentials credentials, Action<UnityWebRequest> errorCallback)
    {
        var www = UnityWebRequestTexture.GetTexture(url);

        if (credentials != null)
        {
            // This simply adds some headers to the request required for JIRA api
            // it is not relevant for this question
            AddCredentials(www, credentials);
        }

        yield return www.SendWebRequest();

        if (www.isNetworkError || www.isHttpError)
        {
            Debug.LogErrorFormat("Download from {0} failed with {1}", url, www.error);
            errorCallback?.Invoke(www);
        }
        else
        {
            Debug.LogFormat("Download from {0} complete!", url);
            successCallback?.Invoke(((DownloadHandlerTexture) www.downloadHandler).texture);
        }
    }

    public static void UploadFile(byte[] rawData, string url, Action<UnityWebRequest> successcallback, Credentials credentials, Action<UnityWebRequest> errorCallback)

    private static IEnumerator UploadFileProcess(byte[] rawData, string url, Action<UnityWebRequest> successCallback, Credentials credentials, Action<UnityWebRequest> errorCallback)
    {
        var form = new WWWForm();
        form.AddBinaryData("file",rawData,"Test.png");

        var www = UnityWebRequest.Post(url, form);

        www.SetRequestHeader("Accept", "application/json");

        if (credentials != null)
        {
            // This simply adds some headers to the request required for JIRA api
            // it is not relevant for this question
            AddCredentials(www, credentials);
        }

        yield return www.SendWebRequest();

        if (www.isNetworkError || www.isHttpError)
        {
            Debug.LogErrorFormat("Upload to {0} failed with code {1} {2}", url, www.responseCode, www.error);
            errorCallback?.Invoke(www);
        }
        else
        {
            Debug.LogFormat("Upload to {0} complete!", url);
            successCallback?.Invoke(www);
        }
    }
}

Later in my script I do

public Texture TestTexture;

// Begin the download
public void DownloadTestImage()
{
    _httpFileManager.DownloadImage(ImageGetURL, DownloadImageSuccessCallback, _credentials);
}

// After Download store the Texture
private void DownloadImageSuccessCallback(Texture newTexture)
{
    TestTexture = newTexture;
}

// Start the upload
public void UploadTestImage()
{
    var data = ((Texture2D) TestTexture).EncodeToPNG();

    _httpFileManager.UploadFile(data, ImagePostUrl, UploadSuccessCallback, _credentials);
}

// After Uploading
private static void UploadSuccessCallback(UnityWebRequest www)
{
    Debug.Log("Upload worked!");
}

In resume the problem lies in the for and back converting in

(DownloadHandlerTexture) www.downloadHandler).texture

and

((Texture2D) TestTexture).EncodeToPNG();

The result looks like this

On the top the original image; on the bottom the re-uploaded one.

As you can see it grows from 40kb to 59kb so by the factor 1,475. The same applies to larger files so that a 844kb growed to 1,02Mb.

So my question is

Why is the uploaded image bigger after EncodeToPNG() than the original image?

and

Is there any compression that could/should be used on the PNG data in order to archive the same compression level (if compression is the issue at all)?

First I thought maybe different Color depths but both images are RGBA-32bit

enter image description here


Update

Here are the two images

original (40kb) (taken from here)

enter image description here

re-uploaded (59kb)

enter image description here


Update 2

I repeated the test with a JPG file and EncodeToJPG() and the result seems to be even worse:

enter image description here

On the top the original image; on the bottom the re-uploaded one.

This time it went from 27kb to 98kb so factor 2,63. Strangely the filesize also was constant 98kb no matter what I put as quality parameter for EncodeToJPG().

derHugo
  • 83,094
  • 9
  • 75
  • 115
  • do both images have an alpha channel? can you link both images here? – Ruzihm Nov 08 '18 at 21:21
  • @Ruzihm yes they look basicaly identical including the alpha channel. I added the images – derHugo Nov 08 '18 at 21:29
  • Can you do the-same experiment with jpeg and see what happens? – Programmer Nov 08 '18 at 23:17
  • Looks like the second one was done at a slightly lower compression ratio. They're pixel for pixel identical, but the underlying bytes are vastly different. – Draco18s no longer trusts SE Nov 09 '18 at 02:58
  • @Programmer I didn't try JPG -> JPG yet. I'll add that. But in `EncodeToJPG` you at least have a parameter for `quality` but for `EncodeToPNG` there are no further options. And well, ofcourse JPG is smaller in general (only know the data currently for 4Mb PNG -> ca 200kb JPG - without quality parameter) – derHugo Nov 09 '18 at 05:57
  • @Draco18s yes my current guess would be that Unity might by default export RGBA32 PNGs while the original might e.g. have only RGBA24 or something like that.. -> ca factor 1,34 larger – derHugo Nov 09 '18 at 06:05
  • @Programmer I did the test for JPG -> JPG see `Update 2` – derHugo Nov 09 '18 at 10:23
  • 2
    Still don't know. I will put bounty in 3 days if no answer – Programmer Nov 09 '18 at 18:01
  • Unity always use their own way to do importing image indeed. The difference is only due to the compression ratio which should be chosen internally by unity. You may try simply re-save those image by other program, like photoshop or PIL in python, you will get back same size. I will see if any documentation stated that (currently I can't access unity docs). – MT-FreeHK Nov 12 '18 at 04:59

2 Answers2

13

If you precisely inspect both PNG files, you will notice the difference. Both of them have same resolution, same bit depth, some number of channels, and both are not interlaced.

However, the original image contains only one IDAT section which holds 41370 bytes of encoded data.

The image originating from Unity contains 8 IDAT sections: 7 x 8192 bytes and one 2860 bytes, 60204 bytes altogether.

In the PNG specification, there is a note:

Multiple IDAT chunks are allowed so that encoders can work in a fixed amount of memory; typically the chunk size will correspond to the encoder's buffer size.

Furthermore, the data contained in those IDAT sections is not necessarily exact the same for the same source images. Those IDAT sections hold raw byte data which was first pre-filtered and then encoded using the zlib compression.

So, the PNG encoder can choose the pre-filtering algorithm from 5 available ones:

Type    Name

0       None
1       Sub
2       Up
3       Average
4       Paeth

Additionally, the zlib compression can be configured for compression window size, which can also be chosen by the PNG encoder.

Inspecting the zlib streams gives following results:

  • both files use "deflate" compression with same window size 32k
  • the compression flags are, however, different - original file has compression level 1 (fast algorithm) whereas the Unity encoded file has compression level 0 (fastest algorithm).

This explains the differences in the binary data and data size.

It seems that you have no control over Unity's PNG encoder, so unfortunately you cannot force it to choose another zlib algorithm.

I suppose, the same happens with the JPEG files - the encoder just chooses a faster algorithm that produces a larger file.

If you want to be in full control of PNG-encoding in Unity, you need to implement your own PNG encoder. E.g. here on Unity forum, there is a sample of such a PNG encoder that uses the zlib.net library. You can fine tune the encoding e.g. by specifying the zlib compression algorithm.

dymanoid
  • 14,771
  • 4
  • 36
  • 64
  • This indeed could be the why this is happening. – Programmer Nov 12 '18 at 15:01
  • Wow thanks for taking the time to investigate and explain that! Makes totally sense now. I guess we don't really have control than but it is good to now what is happening there. I thought it might be my mistake on how I receive and convert the bytes and the other way round. – derHugo Nov 18 '18 at 17:44
  • In old times we were using PNGCrush to generate the smallest PNG. It was beating all the other PNG exporters. It was a command shell utility though – Aleksandr Stepanov Jan 22 '20 at 07:04
0

read here it seems that while you can import a png, what you actually have in your project becomes a texture, or sprite file depending, and this added data increases its size. the additional bytes you are seeing come from information attached to the png by the editor, in order to streamline unity;s loading process, and allow you to set different options for the import. your file size can change drastically based on which settings you choose.

Technivorous
  • 1,682
  • 2
  • 16
  • 22
  • Thanks for your answer but the question was about writing it back to a png/jpg file not about the memory size of the texture while it is loaded in Unity. – derHugo Nov 18 '18 at 17:46