6

Main Goal : Load images from an online url then save the image (whatever type) locally in a /Repo/{VenueName} dir on a mobile. This way it will hopefully save on future mobile data, when scene loads checks for local image first then calls a www request if it doesn't exist on the mobile already.

I've got images online, I've pulled the url from a json file and now I want to store them locally on mobile devices to save data transfer for the end user.

I've gone round in circles with persistent data paths and IO.directories and keep hitting problems.

At the moment I've got a function that saves text from online and successfully stores it on a device but if I use it for images it won't work due to the string argument shown below, I tried to convert it to bytes editing the function too rather than passing it www.text and got an image corrupt error.

Here's the old function I use for text saving files.

public void writeStringToFile( string str, string filename ){
        #if !WEB_BUILD
            string path = pathForDocumentsFile( filename );
            FileStream file = new FileStream (path, FileMode.Create, FileAccess.Write);

            StreamWriter sw = new StreamWriter( file );
            sw.WriteLine( str );

            sw.Close();
            file.Close();
        #endif  
    }


public string pathForDocumentsFile( string filename ){ 
        if (Application.platform == RuntimePlatform.IPhonePlayer)
        {
            string path = Application.dataPath.Substring( 0, Application.dataPath.Length - 5 );
            path = path.Substring( 0, path.LastIndexOf( '/' ) );
            return Path.Combine( Path.Combine( path, "Documents" ), filename );
        }else if(Application.platform == RuntimePlatform.Android){
        string path = Application.persistentDataPath;   
            path = path.Substring(0, path.LastIndexOf( '/' ) ); 
            return Path.Combine (path, filename);
        }else {
            string path = Application.dataPath; 
            path = path.Substring(0, path.LastIndexOf( '/' ) );
            return Path.Combine (path, filename);
        }
    }

This works well for text as it expect a string but I can't get it working on images no matter how much I edit it.

I ended up going down a different route but have unauthorised access issues with the following code and don't think it will work on mobiles but..

IEnumerator loadPic(WWW www, string thefile, string folder)
 {
    yield return www;
     string venue = Application.persistentDataPath + folder;
     string path = Application.persistentDataPath + folder + "/" + thefile;

    if (!System.IO.Directory.Exists(venue))
     {
        System.IO.Directory.CreateDirectory(venue);
     }

    if (!System.IO.Directory.Exists(path))
     {
        System.IO.Directory.CreateDirectory(path);
     }

    System.IO.File.WriteAllBytes(path, www.bytes);
 }

Urgh, it's 3am here and I can't figure it out, can you wizards help me out? Thanks in advance.

Diego
  • 371
  • 1
  • 3
  • 13

2 Answers2

12

I tried to convert it to bytes editing the function too rather than passing it www.text and got an image corrupt error

That's probably the cause of 90% of your problem. WWW.text is used for non binary data such as simple text.

1.Download images or files with WWW.bytes not WWW.text.

2.Save image with File.WriteAllBytes.

3.Read image with File.ReadAllBytes.

4.Load image to Texture with Texture2D.LoadImage(yourImageByteArray);

5.Your path must be Application.persistentDataPath/yourfolderName/thenFileName if you want this to be compatible with every platform. It shouldn't be Application.persistentDataPath/yourFileName or Application.dataPath.

6.Finally, use Debug.Log to see what's going on in your code. You must or at-least use the debugger. You need to know exactly where your code is failing.

You still need to perform some error checking stuff.

public void downloadImage(string url, string pathToSaveImage)
{
    WWW www = new WWW(url);
    StartCoroutine(_downloadImage(www, pathToSaveImage));
}

private IEnumerator _downloadImage(WWW www, string savePath)
{
    yield return www;

    //Check if we failed to send
    if (string.IsNullOrEmpty(www.error))
    {
        UnityEngine.Debug.Log("Success");

        //Save Image
        saveImage(savePath, www.bytes);
    }
    else
    {
        UnityEngine.Debug.Log("Error: " + www.error);
    }
}

void saveImage(string path, byte[] imageBytes)
{
    //Create Directory if it does not exist
    if (!Directory.Exists(Path.GetDirectoryName(path)))
    {
        Directory.CreateDirectory(Path.GetDirectoryName(path));
    }

    try
    {
        File.WriteAllBytes(path, imageBytes);
        Debug.Log("Saved Data to: " + path.Replace("/", "\\"));
    }
    catch (Exception e)
    {
        Debug.LogWarning("Failed To Save Data to: " + path.Replace("/", "\\"));
        Debug.LogWarning("Error: " + e.Message);
    }
}

byte[] loadImage(string path)
{
    byte[] dataByte = null;

    //Exit if Directory or File does not exist
    if (!Directory.Exists(Path.GetDirectoryName(path)))
    {
        Debug.LogWarning("Directory does not exist");
        return null;
    }

    if (!File.Exists(path))
    {
        Debug.Log("File does not exist");
        return null;
    }

    try
    {
        dataByte = File.ReadAllBytes(path);
        Debug.Log("Loaded Data from: " + path.Replace("/", "\\"));
    }
    catch (Exception e)
    {
        Debug.LogWarning("Failed To Load Data from: " + path.Replace("/", "\\"));
        Debug.LogWarning("Error: " + e.Message);
    }

    return dataByte;
}

Usage:

Prepare url to download image from and save to:

//File url
string url = "http://www.wallpapereast.com/static/images/Cool-Wallpaper-11C4.jpg";
//Save Path
string savePath = Path.Combine(Application.persistentDataPath, "data");
savePath = Path.Combine(savePath, "Images");
savePath = Path.Combine(savePath, "logo");

As you can see, there is no need to add the image extension(png, jpg) to the savePath and you shouldn't add the image extension in the save path. This will make it easier to load later on if you don't know the extension. It should work as long as the image is a png or jpg image format.

Dowload file:

downloadImage(url, savePath);

Load Image from file:

byte[] imageBytes = loadImage(savePath);

Put image to Texture2D:

Texture2D texture;
texture = new Texture2D(2, 2);
texture.LoadImage(imageBytes);
Programmer
  • 121,791
  • 22
  • 236
  • 328
  • Thanks Programmer, it was definitely one of those times I was going round in circles and not thinking clearly at all in the end. Thanks for the function. – Diego Apr 15 '17 at 09:35
  • You are welcome. You should be fine as long as you understand what I explained in my answer. – Programmer Apr 15 '17 at 09:40
  • `Debug.Log` is right. The file is likely stored at *\Users\Diego\Library\Application Support\DefaultCompany\3D Interactive App\data\Images* and the file name is *logo*. I don't use Mac but you can Google how to view hidden folders in Library folder. Look [here](http://osxdaily.com/2013/10/28/show-user-library-folder-os-x-mavericks) – Programmer Apr 15 '17 at 11:09
  • Also, check the answer I left properly. You can check if file exist with `Directory.Exists(Path.GetDirectoryName(path))`. If you are looking for a way to save bunch of data except for images, videos and sound then use Json. I made a simple helper class for that [here](http://stackoverflow.com/questions/40965645/what-is-the-best-way-to-save-game-state/40966346#40966346) – Programmer Apr 15 '17 at 11:13
  • The other function will come in handy for a later stage thank you. One final thing if you have the time to help, on first load I need to somehow alter the loadImage function to be a coroutine so the part where where I assign the texture2d gives it change to read the file otherwise I'm getting an exception thrown. I've currently duplicated the loadimage function renaming it so if the local file isn't found it calls my coroutine rather than the loadimage function. I'm having trouble finding a way to wait until the image bytes has finished in a coroutine before assigning. Whats the best way? – Diego Apr 15 '17 at 13:34
  • This is what I currently have.. I called it like this : StartCoroutine (loadImageFirstTime(loadImage(savePath))); then private IEnumerator loadImageFirstTime(byte[] databyte) { yield return databyte; Texture2D texture; texture = new Texture2D(750, 346); texture.LoadImage(databyte); Image im = imagePanelItem.GetComponent(); im.sprite = Sprite.Create (texture, new Rect (0, 0, 750, 346), new Vector2 ());//set the Rect with position and dimensions as you need } – Diego Apr 15 '17 at 13:42
  • Hi, I am away from my computer for few hours and will be back. I will answer your follow up questions when I come back. Although, it will be helpful if you ask a new question and explain what exactly you are doing. Maybe you are trying to check if the file exist on the device. If it does not, download it...Maybe I am wrong.... Will reply in hours when I come back – Programmer Apr 15 '17 at 13:48
  • Thanks for getting back to me, What I have is a scrolling list which is filled with prefabs dynamically, when the scene loads it uses json to load a list of items, one of the json array items is the image for the list item background (a picture of the venue). Your code allowed me to download the image and store it. The problem is, when its a first run and the local files don't exist yet the www is called and tries to apply the list item bg image before the www has finished downloading saving and reading the data, this causes an exception when it tries to assign the image before its done. – Diego Apr 15 '17 at 18:37
  • I get it. There are many ways to fix this. Call `loadImage` function at the end of the `saveImage` function. – Programmer Apr 15 '17 at 22:50
  • Hi again, that worked but it only applies the image to the last item in the for loop, I've made a new question here [link]http://stackoverflow.com/questions/43437005/convert-unity-panel-into-class-to-give-www-request-time-to-load – Diego Apr 16 '17 at 12:02
7

@Programmer 's answer is correct but it's obsolete I just update it:

WWW is obsolete

The UnityWebRequest is a replacement for Unity’s original WWW object. It provides a modular system for composing HTTP requests and handling HTTP responses. The primary goal of the UnityWebRequest system is to permit Unity games to interact with modern Web backends. It also supports high-demand features such as chunked HTTP requests, streaming POST/PUT operations and full control over HTTP headers and verbs. https://docs.huihoo.com/unity/5.4/Documentation/en/Manual/UnityWebRequest.html

UnityWebRequest.GetTexture is obsolete

Note: UnityWebRequest.GetTexture is obsolete. Use UnityWebRequestTexture.GetTexture instead.

Note: Only JPG and PNG formats are supported.

UnityWebRequest properly configured to download an image and convert it to a Texture.

https://docs.unity3d.com/ScriptReference/Networking.UnityWebRequestTexture.GetTexture.html

using System;
using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;

public class ImageDownloader : MonoBehaviour
{

    private void Start()
    {
        //File url
        string url = "https://www.stickpng.com/assets/images/58482b92cef1014c0b5e4a2d.png";
        //Save Path
        string savePath = Path.Combine(Application.persistentDataPath, "data");
        savePath = Path.Combine(savePath, "Images");
        savePath = Path.Combine(savePath, "logo.png");
        downloadImage(url,savePath);
    }
    public void downloadImage(string url, string pathToSaveImage)
    {
        StartCoroutine(_downloadImage(url,pathToSaveImage));
    }

    private IEnumerator _downloadImage(string url, string savePath)
    {
        using (UnityWebRequest uwr = UnityWebRequestTexture.GetTexture(url))
        {
            yield return uwr.SendWebRequest();

            if (uwr.isNetworkError || uwr.isHttpError)
            {
                Debug.LogError(uwr.error);
            }
            else
            {
                Debug.Log("Success");
                Texture myTexture = DownloadHandlerTexture.GetContent(uwr);
                byte[] results = uwr.downloadHandler.data;
                saveImage(savePath, results);

            }
        }
    }

    void saveImage(string path, byte[] imageBytes)
    {
        //Create Directory if it does not exist
        if (!Directory.Exists(Path.GetDirectoryName(path)))
        {
            Directory.CreateDirectory(Path.GetDirectoryName(path));
        }

        try
        {
            File.WriteAllBytes(path, imageBytes);
            Debug.Log("Saved Data to: " + path.Replace("/", "\\"));
        }
        catch (Exception e)
        {
            Debug.LogWarning("Failed To Save Data to: " + path.Replace("/", "\\"));
            Debug.LogWarning("Error: " + e.Message);
        }
    }

    byte[] loadImage(string path)
    {
        byte[] dataByte = null;

        //Exit if Directory or File does not exist
        if (!Directory.Exists(Path.GetDirectoryName(path)))
        {
            Debug.LogWarning("Directory does not exist");
            return null;
        }

        if (!File.Exists(path))
        {
            Debug.Log("File does not exist");
            return null;
        }

        try
        {
            dataByte = File.ReadAllBytes(path);
            Debug.Log("Loaded Data from: " + path.Replace("/", "\\"));
        }
        catch (Exception e)
        {
            Debug.LogWarning("Failed To Load Data from: " + path.Replace("/", "\\"));
            Debug.LogWarning("Error: " + e.Message);
        }

        return dataByte;
    }
}
Seyed Morteza Kamali
  • 806
  • 1
  • 10
  • 22