New Component For Automatic Offline Image Caching

Hi everyone,
I wanted to showcase a new open source component that I have built for Ionic. It is called Imagenie and what it does is handle image caching for you automatically.

You simple add the Imagenie attribute to your IMG and it will automatically grab the ng-src or src for the image and cache it. Next time you view the image after you relaunch the cached version will be resolved. This is great if you are building something that uses a lot of images like I do. It also works with DIVs and you can even decide to pass the image src as a parameter to the Imagenie attribute itself like Imagenie=“my.image.soource.jpg”.

It uses local forage to store your images for you using using the best available storage for your device so you wont have to worry about anything. Please go an download today and fork and feel free to recommend new features.

##https://github.com/saniyusuf/imagenie

9 Likes

hi @saniyusuf, 10x for your great project. but I’ve a problem with my iphone 6 plus.
It works fine with a desktop browser but inside a cordova environment I see a black screen and nothing else.
do you know why?
10x

why inventing things that are actually there:

works fine.

But i miss a feature to set multiple caching folders.
Like -> i want to store images logical like user images in one folder to say “okay clear only the user images”.

This is not implemented there and i have no time to do this on my own.

Maybe you can put this feature in your directive.
And suggestion:
do not store many data in locastorage (like localForage does).
LocalStorage is limited to 2.5 till max 5 mb … it is not there for caching data.

Images should be store directly to the filesystem on nothing else (maybe as blob in a sqlite db).

And do not forget to provide the option to clear up cached images.

1 Like

Hi @bengtler if you have a look at the code youd realise that Imagenie does not use local storage but uses websql and Index DB depending on platform. Also it does not cache the image but actually converts it to a base 64 string so that next time no requests are actually sent and all you have is a string representation of your image gotten asynchronously from your Indexed DB storage.

I dont agree that all images must be stored in the file stream. The file system is Synchronous from my understanding while Indexed DB on the other hand is asynchronous. So for simple image caching I belive, base 63 with canvas is a good case.

@massimiliano_regis thank you for your kind words. As you know this is still very early so I welcome bug discovery. Can you explain your use case better so I understand the problem. If it works in the browser, it should work on the phone.

1 Like

It would be awesome if you do the same with css background images :smile:

@jesusbotella there is support for background image actually. It works on both IMG and other elems like DiV only catch is for now, you specify the src as the value of the attribute. In the next few iterations, I will make it automaticlly detect src for div supplied elsewhere if possible

yeah but also indexed and websql are limited through the browsers limit i think default is 10mb in chrome and base64 produces in long image lists with large images a huge dom.

Writing to the filesystem is therefore a better solution i think. no browser limits (only device storage). And loading an image from local filesystem is fast enough.

Im not trying to start an argument but @bengtler maybe you should research more before you state limits for storage. 10mb is for local storage only. Indexed DB has access for up to quota limits. They might ask a user permission for increased storage.

The aim of this component is not to full up your space but to provide easy cache storage which in feature can be managed with expiry times. How you use it it totally up to you but if your offline data is easting up much space its probably a sign of need to re architect.

And like I said, you can use file API but just the same it is possible to use indexed DB as well which provides an async API and for me that is the main reason I decided to go with that performance gain

1 Like

It is not starting argumenting, but i only want to show up alternatives and i want other people think before using other directives and so on. We are at a state where you can find anything through google in 2 minutes. But you can also get what you maybe not expected ;).

I know many people who include the first lib they found and yell how awesome it is but they do not know what they are doing or what the thing does they use.
Maybe this causes side effects and so on.

Yep and you are right i mixed up limitations, sorry.
The last time i informed me about indexeddb it was not really well supported and websql is not a standard.

My posts are also not critics for your work… i respect everyone who try to push such things forward.
But like i said if you want to handle huge images and many of them on one page base64 can slow down the whole thing and you get an overhead of round about 30% of the original image sizes.

I only wanted to provide another opinion… if someone wants to store the images as base64 they should do it for normal pages with a few images (optimized so you do not have >5MB per image) it is really fast approach… but there are always up- and down sizes.

Greets and keep on rollin’.

Hi,

as i understand there are some issues with indexedDB and iOS. There is an 10 mb limit on storing things in the app container, which is where indexedDB stores its database. There should be ways to store the database outside the app container but i’m not entirely sure about that. If thats possible, your directive would be a great benefit.

I think the 30% overhead of base64 is not that much of a deal.

Thanks for your hard work you guys.

Besides that, local storage is in fact limited to 2.5 mb on iOS which is a total joke.

1 Like

@dsac Very true about buggy IOS support. This is why I have a revert to use the WebSql support on IOS. And the limit for Mobile Indexed DB on IOS for example is unlimited but might request permission when you go over 200mb (http://www.html5rocks.com/en/tutorials/offline/quota-research/) which to be honest no app should go over I mean not even my FB goes over that limit.

And I agree i think the 30% over head is widely over emphasised when you consider that images are around 30kb. For me the Aync feature wins it for me anyday and with plans to use a web worker, things are about to get even more interesting.

I saw this on the blog post about the updated collection-repeat directive

Whenever you set the src of an img on iOS to a non-cached value, there is a freeze of anywhere from 50-150ms--even on an iPhone 6. In our tests, an Android 4.1 device with images in collection repeat outperforms an iPhone 6.
...
We tried creating a web worker that fetches the image, converts it, and sends its base64 representation back to the UI thread. The image is then set to this base64 representation as a data-uri. This fixes half of the problem. If you set an img src to a data-uri that has been set before, it instantly gets the rendered image from the cache and shows it without lag. However, the first time a unique data-uri is set, there is a similar delay to that of a a normal src.

I’m switched my app to use DATA_URLs for 640px JPGs and it seems to be “a little faster” But can you tell if there is any overlap between your offline caching and whatever they do internally in collection-repeat? I know I get a QuotaExceededError: DOM Exception 22 if I try to save all the DATA_URLs to local-storage.

@mixofia this is because you have already exhausted the local storage limit whihc is a minor 5mb. I will stay away from storing heavy things on localstorage instead use it for minor things like tokens. My component uses indexed DB which can have have almost unlimited storage.

Have you done any tests to see if your component will help get better performance with the new collection-repeat directive? I’m trying to avoid the 50-150ms delay on the iPhone, but I can’t tell if i’ve already done it with FILE_URIs.

Nope not at the moment to be honest. Still on Beta 14 and I am on the verge of upgrading to RC1.0.1 . Id keep you posted

Your component is amazing @saniyusuf!
Thank you so much!
You saved my life!

I’ve optimized a little your code. Can you check?

(function () {

angular.module('imagenie', ['LocalForageModule'])

    .constant('IMAGENIE_LOCAL_FORAGE_CONFIG', {
        name        : 'imagenie_db',
        storeName   : 'image',
        description : 'The database to hold base 64 versions of all your images so they are available offline'
    })

    .factory('ImagenieUtil', ['$q', ImagenieUtil])

    .directive('imagenie', ['ImagenieUtil', '$localForage', 'IMAGENIE_LOCAL_FORAGE_CONFIG', DirectiveFunction]);

function ImagenieUtil ($q){
    var ImagenieUtil = {};

    ImagenieUtil.getImageBase64String = function (url, outputFormat) {
        
        outputFormat = typeof outputFormat !== 'undefined' ? outputFormat : "image/jpeg";

        var imageBase64StringPromise = $q.defer();

        var canvas = document.createElement('CANVAS'),
            ctx = canvas.getContext('2d'),
            img = new Image;
        img.crossOrigin = 'Anonymous';
        img.onload = function(){
            var dataURL;
            canvas.height = img.height;
            canvas.width = img.width;
            ctx.drawImage(img, 0, 0);
            dataURL = canvas.toDataURL(outputFormat, 0.8); //quality 0.8
            canvas = null;
            imageBase64StringPromise.resolve(dataURL);
        };
        img.onerror = function () {
          imageBase64StringPromise.reject(url);
        };
        img.src = url;

        return imageBase64StringPromise.promise;
    };

    ImagenieUtil.isUndefined = function (value) {
        return typeof value === 'undefined';
    };

    ImagenieUtil.isEmpty = function (value) {
        return this.isUndefined(value) || value === '' || value === null;
    };

    ImagenieUtil.getImageSrc = function (elementAttributes) {
        if(!this.isEmpty(elementAttributes.imagenie)){
            return elementAttributes.imagenie;
        }else if(!this.isEmpty(elementAttributes.ngSrc)){
            return elementAttributes.ngSrc;
        }else if (!this.isEmpty(elementAttributes.src)){
            return elementAttributes.src;
        }else{
            console.warn('Image Src Undefined');
            return null;
        }
    };

    ImagenieUtil.setImageToElement = function (element, imageBase64String) {
        if(element[0].nodeName === 'IMG'){
            element.attr('src', imageBase64String);
        }else{
            element.css('background-image', 'url(' + imageBase64String + ')');
        }
    };

    ImagenieUtil.isUriAbsolute = function (uri) {
        //****************
        //http://stackoverflow.com/questions/10687099/how-to-test-if-a-url-string-is-absolute-or-relative
        //****************
        var uriTypeRegex = new RegExp('^(?:[a-z]+:)?//', 'i');
        return uriTypeRegex.test(uri);
    };

    return ImagenieUtil;
}

function DirectiveFunction(ImagenieUtil, $localForage, IMAGENIE_LOCAL_FORAGE_CONFIG) {
    return {
        restrict: 'A',
        link: function (scope, element, attrs) {

            attrs.$observe('imagenie', function () {
                var imageSrc = ImagenieUtil.getImageSrc(attrs);
                
                /* No custom instance is faster
                var imagenieLocalForageInstance = {};

                try {
                    imagenieLocalForageInstance = $localForage.instance(IMAGENIE_LOCAL_FORAGE_CONFIG.name);
                }catch (error) {
                    if(ionic.Platform.isIOS()){
                        //Force WebSQL on IOS as IndexedDB is not stable on IOS
                        var iosConfig = angular.extend(IMAGENIE_LOCAL_FORAGE_CONFIG, {driver : 'webSQLStorage'});
                        imagenieLocalForageInstance = $localForage.createInstance(iosConfig);
                    }else{
                        imagenieLocalForageInstance = $localForage.createInstance(IMAGENIE_LOCAL_FORAGE_CONFIG);
                    }
                }
                */

                if(ImagenieUtil.isUriAbsolute((imageSrc))){

                    attrs.ngSrc = '';
                    $localForage.getItem(encodeURIComponent(imageSrc))
                        .then(function (localImageSuccessData) {
                            if(!ImagenieUtil.isEmpty(localImageSuccessData)){

                                ImagenieUtil.setImageToElement(element, localImageSuccessData);

                            }else{

                                ImagenieUtil.getImageBase64String(imageSrc)
                                .then(function (imageBase64String) {
                                    $localForage.setItem(encodeURIComponent(imageSrc), imageBase64String);
                                    ImagenieUtil.setImageToElement(element, imageBase64String);

                                });
                                
                                /* You don't need to create a new img
                                var newImage = angular.element('<img />');
                                newImage.bind('load', function () {
                                    ImagenieUtil.getImageBase64String(imageSrc)
                                        .then(function (imageBase64String) {
                                            imagenieLocalForageInstance.setItem(encodeURIComponent(imageSrc), imageBase64String);
                                            ImagenieUtil.setImageToElement(element, imageBase64String);

                                        });
                                });

                                newImage.attr('src', imageSrc);*/
                            }

                        }, function () {

                            ImagenieUtil.getImageBase64String(imageSrc)
                            .then(function (imageBase64String) {
                                $localForage.setItem(encodeURIComponent(imageSrc), imageBase64String);
                                ImagenieUtil.setImageToElement(element, imageBase64String);
                            });

                            /*
                            var newImage = angular.element('<img />');
                            newImage.bind('load', function () {
                                ImagenieUtil.getImageBase64String(imageSrc)
                                    .then(function (imageBase64String) {
                                        imagenieLocalForageInstance.setItem(encodeURIComponent(imageSrc), imageBase64String);
                                        ImagenieUtil.setImageToElement(element, imageBase64String);
                                    });
                            });

                            newImage.attr('src', imageSrc);
                            */
                        });
                }
            });

        }
    }
}
})();

Thanks for using. can you please submit a github pull request with your changes?

Yes, no problem!

Done!

Thank you @saniyusuf!

I tried to use imgcache put it seems that it is not working in ionic-view. Is there anything special that needs to be set to make it work?