Avatar uploads with Meteor

I haven’t I’m afraid, though there’s a cool package raix:ui-dropped-event that adds a ‘dropped’ event to your Blaze Events helper that should make it trivial to implement a custom version very quickly. (I haven’t done so myself before but it looks like it should just be a case of calling your Slingshot upload method from within the ‘dropped’ event handler).

That was the package I was looking at - will give it a shot soon!

Currently looking at the best way to handle resizing of images before dropping them into S3 - due to the way in which slingshot works it’s not totally straightforward - but http://stackoverflow.com/a/30683815/2050403 looks promising!

For the resize features take a look at this repo: https://github.com/timbrandin/autoform-slingshot

If you need to make it work on mobile, you can use the cordova camera plugin with the allowEdit option. It also works with Android (from 4.4 only I think). It will only be a crop and not really a resize but it is still quite a nice feature.

1 Like

I’ve got jamgold:cropuploader integration on my immediate todo list (it combines slingshot with the jquery cropper plugin) so I’ll let you know how that goes.

1 Like

Oooh, looks interesting, I’ll check that out too - look forward to hearing how you get on with it!

Ok, I didn’t get on so well with jamgold:cropuploader, I got the first bit working easy enough (the uploading a derivative), but couldn’t really get the Cropper functionality to work (I’m working in a React app so YMMV). If you do have a look at it yourself I’d recommend (after reading the docs) jumping straight to the code in the example app in the github repo rather than manually work through the code in the docs, it’ll probably save you a bunch of time.

In the end I’ve decided I’ve dropped enough time on it without enough solid results that I’m going to go in another direction (I wasn’t that keen on the Cropper plugin to begin with). So I’m going to try using JCrop (which I’ve used before with great results) and hopefully using the approach outlined in the StackOverflow post you linked to can get client side cropping working that then just uploads the cropped/resized image over the wire direct to S3. (That’ll be the best solution for the user as they can crop a bit of a local hi-res photo and use that as their avatar with minimal upload bandwidth needed and good for me, no server involvement). Don’t have time to get to all that today though, but if it works I’ll let you know.

1 Like

Awesome, thanks for the update, much appreciated! I may still give it a go, my app’s pretty straight forward at the moment, no react or other front-end frameworks involved (and unlikely to be).

Sorry @elgusto I managed to miss this - will check that out to, cheers!

Edit: This was very simple to set up - assuming no problems down the line I may stick with this one. Shame about the mobile support - but given the context of my app that may fall into ‘nice to have’ - hopefully something that’s resolved at some point though…


@firegoby I’ve just successfully implemented the solution on stackoverflow (I don’t need user cropping so it’s a complete solution for me) and after some fiddling It’s working really nicely actually - thought I’d give it a shot before settling. Because it also deals with image quality as well as resizing I can crunch user supplied images into nice small jpegs which is always a bonus when working with something like S3!

Given that I don’t use Coffeescript OR the ES6 Promises package I did have to do some significant fiddling - but through some fit of chance what I’ve done works. In case you’re in a similar position here’s my event (everything else is standard slingshot):

Template.AccountSettings.events({
  "change #fieldImage": function(event, template) {

    b64ToBlob = function(b64Data, contentType, sliceSize) {
      var byteArray, byteArrays, byteCharacters, byteNumbers, i, offset, slice;
      sliceSize = sliceSize || 512;
      byteCharacters = atob(b64Data);
      byteArrays = [];
      offset = 0;
      while (offset < byteCharacters.length) {
        slice = byteCharacters.slice(offset, offset + sliceSize);
        byteNumbers = (function() {
          var j, ref, results;
          results = [];
          for (i = j = 0, ref = slice.length; 0 <= ref ? j < ref : j > ref; i = 0 <= ref ? ++j : --j) {
            results.push(slice.charCodeAt(i));
          }
          return results;
        })();
        byteArray = new Uint8Array(byteNumbers);
        byteArrays.push(byteArray);
        offset += sliceSize;
      }
      return new Blob(byteArrays, {
        type: contentType
      });
    };

    var uploader = new Slingshot.Upload("avatars");

    var data;

    var file = event.currentTarget.files[0];

    var data = processImage(file, 300, 300, 0.75, function(data) {
      var match = /^data:([^;]+);base64,(.+)$/.exec(data);
      var name = file.name;
      var type = match[1];
      var b64 = match[2];
      var blob = b64ToBlob(b64, type);
      blob.name = name;
      uploader.send(blob, function (error, downloadUrl) {
        if (error) {
          console.error('Error uploading: ', uploader.xhr.response);
          alert(error);
          // Handle the error
        }
        else {
          // Put this in a method
          Meteor.users.update( {_id: Meteor.userId()} , {$set: {"profile.image": downloadUrl}});
          alert('Image uploaded and saved');
        }
      });
    });

  }
});

Could do with some modularisation but it works. If you spot anything suspicious do let me know though, I have a feeling I’m missing a callback somewhere around the match from converting it from the Promise syntax - but as nothing is throwing an error and it’s working I’m not gonna rock that boat…

Edit: Gah, I hadn’t noticed originally but Clientside Image Manipulation only resizes to one dimension, it doesn’t actually crop to a specific set of dimensions.

Seems there isn’t one package that ticks all the boxes!

1 Like

Many thanks for translating that from Coffeescript! I came back from lunch with the same thing on my mind and you’d already done it, excellent :slight_smile: Worked perfectly, plug-n-play, thanks so much!

After a bit of to-and-fro I managed to get mine working exactly as hoped too - I’m cropping with Jcrop, writing to a Canvas preview, b64ToBlob()'ing that canvas, sending blob to S3. See a little movie, it’s working great! It’s really cool from a user’s perspective to be able to choose a 12MP 5Mb JPG locally, select a crop and only have to upload 150Kb over the wire (and direct to S3 with no server middleman). All with a nice interactive UI!

Yeah, mine needs refactoring too, the code isn’t going to win an awards but like you say, it works (and smoothly too). And there definitely doesn’t seem to be one package that does it all - I just ran through my ‘stack’ for this feature: Meteor, JQuery, Semantic UI, Jcrop, Slingshot, b64ToBlob() And this is still running on Blaze, haven’t even bothered making it a React component – Ah, modern web dev, it’s so straight-forward! :stuck_out_tongue:

1 Like

Good thread guys… would be neat to see a package version of a working solution ; )

1 Like

@firegoby id love to see your finished code if you don’t mind sharing it? I was thinking about it and giving the user control over the crop is better than square cropping it anyway, and i was have issues with getting the progress bar to work…

All it needs now is a droppable interface!

Edit: meant to say great job by the way, the video sold me on it! :slight_smile: and yea it’s a lovely technique doing it all client side.

2 Likes

@nathanhornby @spherop - I can’t share my finished code (it wouldn’t really help anyway, plus it’s not finished yet!) BUT I did go through and stripped out all my app-specific stuff to create a bare-bones implementation with all the same functionality.

Here’s the gist

This code wasn’t great to start with, it’ll be even less so now I’ve gone through it and manually converted from ES6 back to ES5 (never thought I’d have to do that! :stuck_out_tongue: ) and quickly stripped out all my app-specific code. This version works on my machine here but obviously haven’t tested it more, so be warned! Also renamed all the DOM references to IDs with the form #jcrop-<element>

There are numerous places I’ll be going through and rewriting this but my own code is only going to become more app-specific from here on out so this seems like the best point to strip things down to their bare-basics, hence this less than perfect release. But to be fair though, there should be everything you need here, and it makes a reasonable place for people to develop from in the future.

What you should get…

Pre-requisites

  1. You need an existing working Slingshot setup.

  2. You need to install the package mrt:jquery-jcrop with meteor add mrt:jquery-jcrop

  3. Go through jcrop.js searching for TODO: comments and that’s where to update with your own settings (there aren’t very many).

What happens…

  1. User selects file

  2. Change in file element triggers event handler 'change #jcrop-file'

  3. If all goes ok that un-hides #jcrop-step2 with the Jcrop instance initialized inside

  4. User crops image

  5. onChange handler for Jcrop updates the #jcrop-canvas element

  6. User clicks Upload

  7. Canvas is turned into a base64URL, that gets converted into a Blob, blob gets uploaded by Slingshot to S3, reporting progress as it goes via uploader ReactiveVar

Notes

  1. I don’t do semi-colons, deal with it :smiley:

  2. #jcrop-canvas is 150px x 150px on page, but 300 x 300 canvas (and upload) because I wanted @2x retina support. Tweak for your own needs.

  3. You’ll need to implement your own progress bar CSS (mine came from Semantic UI) the code is all there to power one, just needs some styles to actually show it. The text percentage readout does work out of the box though.

  4. Don’t even get me started on the mega switch statement in label() template helper, lol :smiley:

Best of luck with it, let me know how you get on

P.S. - Oh and a droppable interface shouldn’t be hard at all, I haven’t done it but a raix:ui-dropped-event wired up to feed its first dropped file into the first few lines of the 'change #jcrop-file' event handler should do it.

4 Likes

Thanks for sharing all the deets …

1 Like

Hugely helpful @firegoby - look forward to going over this to tomorrow, thanks!

Edit: Darn, looks like I won’t have a chance to play with it today - will update when I get a chance!

In case it’s of use to anyone finding this thread in the future, I’ve refactored and improved this into a React component available here.

I’ve cleaned up the code a lot and removed the b64toBlob() stuff and replaced it with a Polyfill for HTML5’s canvas.toBlob() function that will eventually have native browser support. Oh and added proper support of PNG, JPG & WEBP.

Also made it customisable with props, only slingshot is required, they are: -

slingshot: slingshot Directive Name *required
width: upload width
height: upload height
imageType: image MIME type, compatible with canvas.toBlob()
imageQuality: image quality for JPG or WEBP, number 0.0 to 1.0
options: Jcrop options
onError: on upload error callback(error, downloadURL)
onSuccess: on upload success callback(downloadURL)
setFilename: set upload filename callback(fileEvent)

So just call it wherever you need a cropper with: -

<Cropper slingshot="userUploads" />

As before you need an existing Slingshot setup and to install mrt:jquery-jcrop. Also this uses Semantic UI for its styling so if you don’t want to use it, you can go into the render() function and tweak the markup however you need.

Anyhoo, might be useful to someone.

4 Likes

Sweet!

I’ll be looking at your solution today (finally!) - haven’t touched this code (or meteor) for a while so am likely to use most of the afternoon refamiliarising myself - but I’ll update you once I’ve got it working!

Edit: @firegoby Worked first time - lovely stuff mate. Barely have that success with a supported plugin! Now to go through your React component and port over some of the improvements, as I’m sure you did a better job than I’d do :smile:

Next stop: Drag and drop. I’ll let you know how I get on, hopefully it’ll be soon!

Thanks again

1 Like

@firegoby Not getting very far with the file drop functionality actually. The package you linked to, raix:ui-dropped-event has basically no documentation with only an example for FS - which I don’t totally ‘get’ to be honest (the pertinant file variable appears to be what I need, but it doesn’t come from anywhere). I’ve been messing around with it for the past half hour but all it seems to add is the event, I have no idea how to get a file from it.

All the other packages I can find are complete solutions with the upload functionality (i.e. dropzone, which needs a destination URL) :frowning:

Am I just being dense with the ui-dropped-event?

Edit: As is typical I found the solution shortly after asking the question. Kind of by accident. I stumbled across this tutorial for building a droppable file upload in jquery, and noticed the pertinant reference: e.originalEvent.dataTransfer.files.

So all I needed to do was swap:

var oFile = $('#jcrop-file')[0].files[0];

For:

var files = event.originalEvent.dataTransfer.files;
var oFile = files[0];

And obviously swap out the change #jcrop-file event for the dropped #dropzone event.

Job done :thumbsup:

1 Like

It would be nice if somebody could create a package for: aviary.com

There’s an NPM package and one for meteor (but it’s 3 years old) https://atmospherejs.com/belisarius222/aviary

Time to start using react:

Sorry to revive such an old thread but I had a couple comments for future readers. I am switching from a CFS graphicsmagick workflow to Slingshot with JCrop:

@firegoby Your code samples are awesome. Helped me out a ton. Thank you for posting. I used your second pollyfill version in a Blaze template. Works great. FYI to future readers: file.lastModifiedDate is deprecated in browsers (and removed from Safari), use file.lastModified instead

I love the Slingshot workflow, but I’m finding two limitations that I didn’t have using CFS and graphicsmagick:

  1. I could support TIF file uploads with CFS and convert them to JPG (can’t do with Slingshot)
  2. I used to support GIF uploading. You can with Slingshot, but if you crop/resize with JCrop, when you convert the canvas to a blob it kills the animation. :frowning:

These are fine for now, but in the future I may have to have a separate uploading route for GIFs because that was really cool in my app to see animations. GIFs are like the “little file format that could”. They’ve managed to be quite useful and prominent.