Encode images for offline usage with HTML5 Canvas

Tuesday, May 15, 2012

This is the additional part to the previous tutorial Create a self caching website ready for offline usage with HTML5 and jQuery . Our script is able to cache and restore markup, stylesheets and javascripts. Basically everything you need to make a kick-ass website. All of these assets are simple text and can be cached/restored as is . But how could we cache non-text based content, such as images, in HTML5's localStorage ?

How does it basically work?

Images can be represented as Base64 encoded data URI scheme . This technique has been around for quite a while and is supported by all modern browsers.

  1. <img alt="Image represented as data URI" src="data:image/png;base64,..." />

To generate this data URI scheme we use another feature of HTML5, the Canvas element . To be more specific, we only use two functions: drawImage() to get the image onto the Canvas element, and toDataURL() to generate the Base64 encoded data URI.

Preload images when online

We need to make sure that every image we want to cache is fully loaded. In that case we create a dummy image object and apply the jQuery load() event.

  1. encodeImageBase64: (img) -> # use canvas to get a base64 encoded string of the image
  2. canvas = document.createElement('canvas') # create a temporary canvas element
  3. canvas.width = img.width # set width and..
  4. canvas.height = img.height # ..height of the canvas to fit the complete image
  5. context = canvas.getContext('2d') # get the context for the canvas element
  6. context.drawImage img, 0, 0 # draw the image into the context
  7. canvas.toDataURL 'image/jpg' # and return the base64 encoded data url of the complete canvas

Store the data URI scheme in localStorage

We already have a function to store data in the localStorage. Encode the image and store it.

  1. loadImageCallback: (imgEl) ->
  2. encoded = @encodeImageBase64(imgEl.get(0)) # encode the image to a base64 data URI
  3. @loadAssetCallback imgEl.attr('src'), encoded # store encoded image in local storage

Restore images when offline

Restoring images from the localStorage cache is pretty simple. We replace the src attribute of the original image with the data URI scheme. That's it!

  1. restoreImages: -> # restore all images from cache if possible
  2. self = @ # keep a reference to this
  3. images = $ 'img', @bodyEl # get all image elements, use 

The complete script

  1. class SCOW
  2. constructor: ->
  3.   @headEl = $ 'head' # snapshot of the head element
  4.   @bodyEl = $ 'body' # snapshot of the body element
  5.   @curFileName = @getFileName() # get the requested path
  6.   @assets = @getStorage('assets') # initialize assets index
  7.     if @assets is null # if there is no assets index yet
  8.       @assets = ['js/jquery.js', 'js/scow.js'] # create it, these two assets are cached by appcache
  9.       @setStorage 'assets', @assets # and store it
  10.       applicationCache.addEventListener 'updateready', -> # if the manifest files are newly cached
  11.       localStorage.clear() # clear also local store
  12.       if navigator.onLine # check if there is a internet connection
  13.         @cacheCurrentFile() # if so cache the current file
  14.       else
  15.         @restoreFromCache() # try to restore the requested file from cache
  16.  
  17.       if location.host.indexOf('localhost') isnt -1 # check if we are in dev mode
  18.         $('#new-cache').show() # if so show the link to trigger a new cache via /new_cache
  19.  
  20.       @updateAssetsIndex() # output the current asset index
  21.  
  22.       getFileName: ->
  23.         path = location.pathname.split('/') # get current pathname and split it by /
  24.         filename = path[path.length - 1] # last part in array is filename
  25.  
  26.         if filename.length is 0 # check if the root path / is requested
  27.           filename = 'index.html' # fallback to index.html
  28.           filename # return filename
  29.  
  30.         getStorage: (name) -> # helper function to read/decode JSON from local storage
  31.           item = localStorage.getItem(name) # try to read from local storage
  32.  
  33.         if item isnt null # item was found in storage
  34.           item = JSON.parse(item) # json encoded object
  35.           item # return the object or null if not found
  36.  
  37.         setStorage: (name, value) -> # helper function to write/encode JSON to local storage
  38.           item = JSON.stringify(value) # create json string
  39.           localStorage.setItem name, item # write json string to local storage
  40.  
  41.         updateAssetsIndex: -> # output cached files in a list for the demo
  42.           listEl = $ '#cached-files' # we dont cache this list element globally because it could change in $body
  43.           listEl.children().remove() # remove all previously added list items
  44.  
  45.           for asset in @assets # for every path in the assets index
  46.             listEl.append "
  • #{asset}
  • " # append a list item containing the path isAssetCached: (path) -> # check if a asset is already cached - in the assets index $.inArray(path, @assets) isnt -1 # return true if already cached otherwise false loadAssetCallback: (path, content) -> # is called when a file is fully loaded unless @isAssetCached(path) # check if asset already cached @assets.push path # add path to assets index @setStorage path, content # cache the file with the path as key @setStorage 'assets', @assets # store the new assets index array @updateAssetsIndex() # update the demo list of the assets index loadAsset: (path, callback = ->) -> # load a asset with $.get, allow a additional callback $.get path, (content) => # get the file content @loadAssetCallback path, content # content loaded, invoke the callback callback path, content # invoke the additional callback loadAssets: (paths) -> # cache either css or scripts for path in paths # go through all paths unless @isAssetCached(path) # check if asset already cached @loadAsset path # start loading the asset getAssets: (selector, src) -> # extract the assets from the header and return array retArr = [] # array to return for el in @headEl.find(selector) # all elements matching the selector retArr.push $(el).attr(src) # read the attribute containing the source path and store it in array retArr # return a array of the file paths encodeImageBase64: (img) -> # use canvas to get a base64 encoded string of the image canvas = document.createElement('canvas') # create a temporary canvas element canvas.width = img.width # set width and.. canvas.height = img.height # ..height of the canvas to fit the complete image context = canvas.getContext('2d') # get the context for the canvas element context.drawImage img, 0, 0 # draw the image into the context canvas.toDataURL 'image/jpg' # and return the base64 encoded data url of the complete canvas loadImageCallback: (imgEl) -> encoded = @encodeImageBase64(imgEl.get(0)) # encode the image to a base64 data URI @loadAssetCallback imgEl.attr('src'), encoded # store encoded image in local storage loadImages: -> # get all images from the content and cache them encoded in base64 self = @ #keep a reference to 'this' images = $ 'img', @bodyEl # get all image elements, use

as context images.each -> # process every image $(new Image()) # create a dummy image object .attr('src', $(@).attr('src')) # load from the same path as found in the image element .load -> # add a load event for the dummy image self.loadImageCallback $(@) # trigger the callback and pass the jquery object for the image cacheCurrentFile: -> # cache the requested .html file cssAssets = @getAssets('link[rel="stylesheet"]', 'href') # get array of stylesheets jsAssets = @getAssets('script', 'src') # get array of javascripts cacheObj = # this will hold the cached page and all assets references bodyHtml : @bodyEl.html() # cache the content of the current file title : document.title # cache the title stylesheets: cssAssets # array with all stlesheets javascripts: jsAssets # array with all javascripts @loadAssetCallback(@curFileName, cacheObj) # manually invoke the callback and save the object @loadAssets cssAssets # begin to load all the css assets @loadAssets jsAssets # same with the javascripts @loadImages() # begin to load, encode and cache images restoreHeader: (paths, wrapper) -> # reassemble and include the cached files combined = '' # include all in one string for path in paths # go through all cached assets content = @getStorage(path) # try to get the content from local storage if content isnt null # check if the requested file is cached combined += content # add the cached content $(wrapper) # create a jquery object from the wrapper markup .text(combined) # set the content .appendTo @headEl # and append it to the head restoreImages: -> # restore all images from cache if possible self = @ #keep a reference to 'this' images = $ 'img', @bodyEl # get all image elements, use

Comments

Add new comment

Filtered HTML

  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
CAPTCHA
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.
Target Image