-2

I have service that pulls an object from an API. Some of that object may contain image URLs. The backend currently scans for these, and processes them, (in PHP) by get_file_contents() and translating them to inline data. This is heavily loading the throughput on my server. The reason I am doing this is because I want to cache the images for being offline later, but in a way that I can still just use regular angular to render the object.

I can't do the processing in Javascript in the browser with $http.get() because the site hosting the images is blocking the cross-site request. What I thought to do, then, was to create an <IMG> element in the browser, that called the service back once it was loaded so I can extract the data and process the object with it.

I can't control the service worker to store the get from inside the app, and the URL's are not known by the app at any time before it downloads the API object anyway.

I did think about redoing the service worker to store gets from off my site as well, but that seemed a little bit wrong, and I'm not sure how well it would work anyway, plus, while developing, I switch off the service worker as it means I have to let the entire site load twice for it to refresh completely.

Can anyone help me with a way to get image data via the browser into my service?

Nigel Johnson
  • 522
  • 1
  • 5
  • 21

1 Answers1

-1

If I had found a CORS supportive image host in the first place I may not have needed this and could probably have just used the $http call.

A directive, service and controller are required, as well as a host that supports CORS (Imgur for example). I also used this base64 canvas code.

Here is the javascript code:

// Using this from https://stackoverflow.com/questions/934012/get-image-data-in-javascript
function getBase64Image(img) {
    // Create an empty canvas element
    var canvas = document.createElement("canvas");
    canvas.width = img.width;
    canvas.height = img.height;

    // Copy the image contents to the canvas
    var ctx = canvas.getContext("2d");
    ctx.drawImage(img, 0, 0);

    // Get the data-URL formatted image
    // Firefox supports PNG and JPEG. You could check img.src to
    // guess the original format, but be aware the using "image/jpg"
    // will re-encode the image.
    var dataURL = canvas.toDataURL("image/png");

    return dataURL;
    //  return dataURL.replace(/^data:image\/(png|jpg);base64,/, "");
}

// Used on the img tag to handle the DOM notification feeding into the service
app.directive('notifyimgsvc', function() {
    return {restrict : 'A', link : function(scope, element, attrs) {
        element.bind('load', function() {
            console.log('imgSvc::notify() image is loaded');
            console.log("imgSvc::notify(): " + this.src);
            imgSvc.notifyLoad(this.src, getBase64Image(this));

        });
        element.bind('error', function() {
            console.log('imgSvc::notify() image could not be loaded');
            console.log("imgSvc::notify(): " + this.src);
        });
    }};
});

// A core service to handle the comms in both directions from requests to data
app.service('imgSvc', [function(netSvc) {
    imgSvc = this; // to avoid ambiguoity in some inner function calls

    imgSvc.images = {}; // a cache of images
    imgSvc.requests = []; // the requests and their callbacks
    imgSvc.handlers = []; // handlers that will render images

    console.log("imgSvc::init()");

    // Allows a controller to be notified of a request for an image and
    // a callback to call when an image is added. There should only ever
    // be one of these so an array is probaby not needed and any further
    // requests should probably throw an error.
    imgSvc.registerHandler = function(callback) {
        console.log("imgSvc::registerHandler()");
        if (imgSvc.requests.length) {
            // Already have image requests, so tell the new handler about them
            for ( var i in imgSvc.requests) {
                callback(imgSvc.requests[i].url);
            }
        }
        // Add the new handler to the stack
        imgSvc.handlers.push(callback);
    };

    // The usage function from your code, provide a callback to get notified
    // of the data when it loads.
    imgSvc.getImg = function(url, callback) {
        console.log("imgSvc::getImg('" + url + "')");

        // If we have pre-cached it, send it back immediately.
        if (imgSvc.images[url] != undefined) {
            console.log("imgSvc::getImg('" + url + "'): Already have data for this one");
            callback(url, imgSvc.images[url]);
            return;
        }

        // push an object into the request queue so we can process returned data later.
        // Doing it this way als means you can have multiple requests before any data
        // is returned and they all get notified easily just by looping through the array.
        var obj = {"url" : url, "callback" : callback};
        if (imgSvc.handlers.length) {
            console.log("imgSvc::getImg('" + url + "'): informing handler");
            for ( var i in imgSvc.handlers) {
                imgSvc.handlers[i](obj.url);
            }
        }
        imgSvc.requests.push(obj);
    };

    // Notification of a successful load (or fail if src == null).
    imgSvc.notifyLoad = function(url, src) {
        console.log("imgSvc.notifyLoad()");
        // Save the data to the cache so any further calls can be handled
        // immediately without a request being created.
        imgSvc.images[url] = src;

        // Go though the requests list and call any callbacks that are registered.
        if (imgSvc.requests.length) {
            console.log("imgSvc.notifyLoadCallback('" + url + "'): scanning requests");
            for (var i = 0; i < imgSvc.requests.length; i++) {
                if (imgSvc.requests[i].url == url) {
                    console.log("imgSvc.notifyLoadCallback('" + url + "'): found request");
                    // found the request so remove it from the request list and call it
                    var req = imgSvc.requests.splice(i, 1)[0];
                    i = i - 1;
                    console.log("imgSvc.notifyLoadCallback('" + url + "')");
                    req.callback(url, src);
                } else {
                    console.log("imgSvc.notifyLoadCallback('" + url + "'): skipping request for '" + imgSvc.requests[i].url + "'");
                }
            }
        } else {
            console.log("imgSvc.notifyLoadCallback('" + url + "'): No requests present??");
        }
    };

    // The notifiy fail is just a logging wrapper around the failure.
    imgSvc.notifyFail = function(url) {
        console.log("imgSvc.notifyFail()");
        imgSvc.notifyLoad(url, null);
    };
}]);

// A simple controller to handle the browser loading of images.
// Could probably generate the HTML, but just doing simply here.
app.controller('ImageSvcCtrl', ["$scope", function($scope) {
    $scope.images = [];
    console.log("imgSvcCtrl::init()");

    // Register this handler so as images are pushed to the service,
    // this controller can render them using regular angular.
    imgSvc.registerHandler(function(url) {
        console.log("imgSvcCtrl::addUrlHandler('" + url + "')");
        // Only add it if we don't hqve it already. The caching in the
        // service will handle multiple request for the same URL, and
        // all associated malarkey
        if ($scope.images.indexOf(url) == -1) {
            $scope.images.push(url);
        }
    });

}]);

The HTML you need for this is very simple:

<div data-ng-controller="ImageSvcCtrl" style="display:none;">
    <img data-ng-repeat="img in images" data-ng-src="{{img}}" alt="loading image" crossorigin="anonymous" notifyimgsvc />
</div>

And you call it within your controllers like this:

var req_url = "https://i.imgur.com/lsRhmIp.jpg";
imgSvc.getImg(req_url, function(url, data) {
    if(data) {
        logger("MyCtrl.notify('" + url + "')");
    } else {
        logger("MyCtrl.notifyFailed('" + url + "')");
    }
});
Nigel Johnson
  • 522
  • 1
  • 5
  • 21