14

I'm letting users upload multiple images directly to Amazon-S3 using Multer-S3 and then displaying those images on the front end via a loop. All works perfectly.

However when the images are uploaded via mobile (image taken on an iPhone or Android) the orientation is correct on mobile but does NOT have correct orientation on desktops. Major problem.

This is due to the images EXIF data I believe.

Seems like ImageMagick or Kraken JS https://kraken.io/docs/storage-s3 might be a way to solve it but for the life of me I cannot figure out how to implement either with the way I'm uploading and showing images shown below.

How would I change my code below to auto-orient the images? Note: It must work for multiple images.

Thanks for any help!

Heres's how I'm letting users upload multiple images at a time directly to Amazon-S3:

aws.config.update({
    secretAccessKey: 'AccessKey',
    accessKeyId: 'KeyID',
    region: 'us-east-2'
});

var s3 = new aws.S3();

    var storage =  multerS3({
        limits : { files: 25 },
        s3: s3,
        bucket: 'files',
        key: function (req, file, cb) {
            var fileExtension = file.originalname.split(".")[1];
            var path = "uploads/" + req.user._id + Date.now() + "." + fileExtension;
            cb(null, path); 
        },
    })


var upload = multer({storage: storage}).any("images", 25);

router.post("/", middleware.isLoggedIn, function(req, res, next){

        upload(req,res,function(err) {
        if(err) {
        console.log(err);
        res.redirect('/')
        }




Listings.findById(req.params.id, function(err, foundListings){

    var allimages = []

            if(typeof req.files !== "undefined") {
            for(var i = 0; i < req.files.length; i++) {
                allimages.push(req.files[i].key);
            }
            }
 var currentimages = allimages;

 var newListings = {currentimages:currentimages}
 //Removed the other Model aspects
    Listings.create(newListings, function(err, newlyCreated){
        if(err){
            console.log(err);
        } else {

 res.redirect("/listings");
    }
    });
    });

How I'm displaying the images on the front end. Listings.currentimages is an array containing all image links.

app.locals.awspath = "https://s3.us-east-2.amazonaws.com/myfiles/";

// awspath is the file path to my Amazon-S3 path

<div id='allimages'>
<% for(var i = 0; i < listings.currentimages.length; i++ ) { %>
<div class='smallerImages'>

<%  var url2 = awspath + listings.currentimages[i] %>
<img class="small" src="<%= url2 %>">

</div>
<% } %>
</div>
AndrewLeonardi
  • 3,351
  • 9
  • 47
  • 100
  • Have a look at [this](https://stackoverflow.com/q/46135349/643039) – Mathieu de Lorimier Sep 19 '17 at 12:41
  • @MathieudeLorimier Thank you for sending. Very interesting. I think I'd rather fix this on the back-end rather than the front end. Any thoughts? :) – AndrewLeonardi Sep 19 '17 at 13:27
  • 1
    I agree, @mostafazh has sime great suggestions. – Mathieu de Lorimier Sep 20 '17 at 00:56
  • 1
    It's worth noting that ImageMagick (recent versions) decodes the Exif data and stores the orientation in "image->orientation". I don't believe it (yet) stores that value back into the Exif profile if it gets changed. – Glenn Randers-Pehrson Sep 21 '17 at 17:46
  • @GlennRanders-Pehrson Thank you very much for letting me know this Glenn! Any thoughts on how else to handle the multiple image aspect? :) – AndrewLeonardi Sep 21 '17 at 17:48
  • Sorry I haven't thought about orientation of multiple images. I notice that GraphicsMagick stores image->orientation while ImageMagick stores that plus image_info->orientation. Not sure of the implications of that. BTW, both IM and GM store image->orientation in PNG files, in an experimental "orNT" chunk. – Glenn Randers-Pehrson Sep 21 '17 at 21:09

1 Answers1

19

The problem is that iOS sets the image's EXIF metadata which causes this behavior. You can use a library that can read the EXIF metadata and rotate the image for you.

jpeg-autorotate (https://github.com/johansatge/jpeg-autorotate) is a very simple lib and has very nice documentation (you should check it out).

Example

var jo = require('jpeg-autorotate');
var fs = require('fs');

// var options = {quality: 85};
var options = {};
var path = '/tmp/Portrait_8.jpg'; // You can use a Buffer, too
jo.rotate(path, options, function(error, buffer, orientation) {
    if (error) {
        console.log('An error occurred when rotating the file: ' + error.message);
        return;
    }
    console.log('Orientation was: ' + orientation);

    // upload the buffer to s3, save to disk or more ...
    fs.writeFile("/tmp/output.jpg", buffer, function(err) {
        if(err) {
            return console.log(err);
        }

        console.log("The file was saved!");
    });
});

You can find some sample images with different EXIF rotation metadata from here

Converted as an AWS Lambda Function

// Name this file index.js and zip it + the node_modules then upload to AWS Lambda

console.log('Loading function');
var aws = require('aws-sdk');
var s3 = new aws.S3({apiVersion: '2006-03-01'});
var jo = require('jpeg-autorotate');

// Rotate an image given a buffer
var autorotateImage = function(data, callback) {
  jo.rotate(data, {}, function(error, buffer, orientation) {
      if (error) {
          console.log('An error occurred when rotating the file: ' + error.message);
          callback(error, null);
      } else {
        console.log('Orientation was: ' + orientation);
        callback(null, buffer);
      }
  });
};

// AWS Lambda runs this on every new file upload to s3
exports.handler = function(event, context, callback) {
    console.log('Received event:', JSON.stringify(event, null, 2));
    // Get the object from the event and show its content type
    var bucket = event.Records[0].s3.bucket.name;
    var key = event.Records[0].s3.object.key;
    s3.getObject({Bucket: bucket, Key: key}, function(err, data) {
        if (err) {
            console.log("Error getting object " + key + " from bucket " + bucket +
                ". Make sure they exist and your bucket is in the same region as this function.");
            callback("Error getting file: " + err, null);
        } else {
            // log the content type, should be an image
            console.log('CONTENT TYPE:', data.ContentType);
            // rotate the image
            autorotateImage(data.Body, function(error, image) {
              if (error) {
                callback("Error rotating image: " + error, null);
              }

              const params = {
                Bucket: bucket,
                  Key: 'rotated/' + key,
                  Body: image
              };
              // Upload new image, careful not to upload it in a path that will trigger the function again!
              s3.putObject(params, function (err, data) {
                if (error) {
                    callback("Error uploading rotated image: " + error, null);
                } else {
                  console.log("Successfully uploaded image on S3", data);
                  // call AWS Lambda's callback, function was successful!!!
                  callback(null, data);
                }
              });
            });
        }
    });
};

Notes This function upload the rotated images to the same bucket but you can easily change that. If you are just starting with AWS Lambda, I'd suggest you learn more about it (https://www.youtube.com/watch?v=eOBq__h4OJ4, https://www.youtube.com/watch?v=PEatXsXIkLc)

Make sure you've the right permissions (read and write), correct function trigger, correct "Handler" when creating the function! Make sure to checkout the function logs in CloudWatch too, makes debugging a lot easier. If it starts timing out, increase the function timeout and increase it's memory.

mostafazh
  • 4,144
  • 1
  • 20
  • 26
  • Hey @mostafazh thanks for the great answer! One of my troubles is that users are uploading multiple images at once. Up to 25 at a time. In both examples it looks like its rotating one image: 'fixed.jpg" & "IMG_0001.jpg." How would this work with more than one image? Thanks so much!!! – AndrewLeonardi Sep 19 '17 at 18:24
  • You will need to do for every image. You might also go "fancy" and create an AWS Lambda function that will listen to every image upload to S3 and have the Lambda function auto-rotate the images. One benefit of Lambda is that they execute in parallel and will keep your request light (just upload the files to S3). More about AWS Lambda http://docs.aws.amazon.com/lambda/latest/dg/lambda-introduction.html – mostafazh Sep 19 '17 at 18:31
  • thank you for sending that over! Any chance you could update your answer a bit showing an example of how that might be done? :) – AndrewLeonardi Sep 19 '17 at 18:38
  • another idea, given the event driven behavior of node, @AndrewLeonardi you can also have the server return the successful upload response to the request. The server will continue to work on processing the images regardless, thoughts? – mostafazh Sep 19 '17 at 19:05
  • hmm interesting thought no doubt! – AndrewLeonardi Sep 19 '17 at 19:10
  • @AndrewLeonardi I'm almost down with the AWS Lambda function, hope it helps you. Sorry for the delay! – mostafazh Sep 22 '17 at 01:07
  • @AndrewLeonardi thanks man for the bounty. Can you mark the answer as the best answer if you think it is :) Cheers! – mostafazh Sep 24 '17 at 15:50
  • I'm about to mark your answer as correct! :) One question. Could you update the jpeg-autorotate section to work with multiple images? I still don't understand how the "Path" option works with multiple images with links coming from S3. – AndrewLeonardi Sep 29 '17 at 20:03
  • thanks Andrew :) Let me try to explain it a little more. Both code works for 1 file at a time. You can call the first code n times to work with multiple files. However, in the 2nd code snippet, I'm using AWS Lambda which (with the right configuration, beyond the scope of this question) but you should differently take a look at it (it's very cheap and very very scaleable). What it does is it is "triggered" on every new file upload to s3, so what you need to do in your node server is push all X number of files to s3 and once they hit s3, an X Lambda function will execute for each file ;) – mostafazh Sep 29 '17 at 20:09
  • 1
    Thank you so much for all of the help! Amazing answer. – AndrewLeonardi Sep 29 '17 at 20:14
  • Will this work for video? – zero_cool May 27 '23 at 00:11