How can I get HEIC Image format working in s3

2.5k Views Asked by At

I'm working with saving and displaying images in an AWS S3 bucket. I am on a mac and the images show fine on the mac.

I am able to upload many images to the bucket and then I can display them using a pre-signed URL. All good...

But then I have some other varied images such as .jpg that I see fine on the mac and seem to upload OK however do not display from s3 using pre-signed URL. When viewed in Mac Safari or chrome or Firefox I get the broken image symbol. Firefox also says:

The image "https://xxxxxxxxxx" cannot be displayed because it contains errors"

Someone suggested that possibly the original file creation might have been strange in someway and the Mac might be able to interpret the image however S3 cannot do this successfully. Possibly this might be a cross platform Windows / Mac / Linux image issue?

Test: I took one of the .jpg images that did not show up from S3 - and I opened it in preview on the Mac and exported it also as .jpg under a different name. Then I uploaded this new Version add this did seem to fix the problem because it now she displays correctly from s3.

However for what I'm doing I do not want to have to export every image and resave it - in order to go to S3.

Q: Does anybody have any solutions as to why I am getting some Errors when trying to display images from S3? Any ideas how to fix this?

-- possible clue : - in Mac terminal I tried :

 file -I ~/Desktop/test.jpg 

and surprisingly it came back as = image/heic even though the file had .jpg suffix.... Any idea how to get s3 to read "heic files" or just get this working?

Thanks Dave

1

There are 1 best solutions below

0
On

I ran into this issue too. For me, there was some issues that were brought on by an automatic "conversion" to jpg that was being performed when a file was shared via iCloud between an iPhone and an iPad or MacOS device. The conversion to jpg often just renamed the file extension to .jpg without changing the file to a jpg. Very confusing and frustrating, and not very on brand for Apple.

There's a few nuances that you should be aware of when combating this "fake jpg" issue.

The first is that you need to be able to tell if you have HEIC files with the wrong extension. To determine the actual type of the file, you can use the file's actual data instead of the extension. The first few bytes of the file contain the file type (aka the file's "magic number").

You could create a Lambda function that checks for every known magic number but it's a lot easier to let an node.js package handle that for you. I used the file-type-ext NPM package in this code, which I hosted on AWS Lambda. I send an HTTP request to the API gateway with the bucket name and key of the file I want to check, and it returns the actual file extension (or an error).

const AWS = require('aws-sdk');
const fileType = require('file-type-ext');

exports.handler = async (event) => {
  const s3 = new AWS.S3();

  // Retrieve the bucket and key from the event
  console.log('event.body:');
  console.log(event.body);
  let payload = JSON.parse(event.body);
  const bucket = payload.bucket; console.log('bucket: ' + bucket );
  const key = payload.key; console.log('key: ' + key );

  try {
    // Retrieve the file from S3
    const params = {
      Bucket: bucket,
      Key: key
    };

    const { Body } = await s3.getObject(params).promise();

    // Determine the file extension based on the magic number
    const fileBuffer = Buffer.from(Body);
    const { ext } = fileType(fileBuffer);

    if (ext) {
      return {
        statusCode: 200,
        body: ext
      };
    } else {
      return {
        statusCode: 500,
        body: 'File extension not found'
      };
    }
  } catch (error) {
    return {
      statusCode: 500,
      body: error.message
    };
  }
};

Once you know the file's actual type, my recommendation would be to use a separate Lambda function to convert the file to jpg. This would make it readable by any modern browser. My issue was entirely with HEICs masquerading as jpgs, so I only needed a function to deal with HEIC conversions. I tried a few different node.js packages and eventually went with heic-convert. Here's the Lambda function I ended up with. It ingests the a poorly named HEIC file, converts it to a JPG, then saves it as a randomly named jpg in the same bucket.

const { promisify } = require('util');
const fs = require('fs');
const convert = require('heic-convert');
const axios = require('axios');
const AWS = require('aws-sdk');
var payload = {};
var fileURL;
const BUCKET = process.env.BUCKET;

const s3 = new AWS.S3();

exports.handler = async (event, context) => {
  
  console.log('Event recieved.');
  console.log(event.body);
  payload = JSON.parse(event.body);
  fileURL = payload.URL;
  console.log('fileURL: ' + fileURL );
  
  try {
    const response = await axios.get(fileURL, {
      responseType: 'arraybuffer',
    });
    console.log('File downloaded successfully.', response.data);

    const inputBuffer = Buffer.from(response.data, 'binary');

    const outputBuffer = await convert({
      buffer: inputBuffer,
      format: 'JPEG',
      quality: 1,
    });
    console.log('File converted successfully.', outputBuffer);

    let rando = generateRandomString(16);

    const s3Params = {
      Bucket: BUCKET,
      Key: rando + '.jpg',
      Body: outputBuffer,
      ACL: 'public-read',
      ContentType: 'image/jpg'
    };

    const uploadResult = await s3.upload(s3Params).promise();
    console.log('File uploaded successfully:', uploadResult.Location);

    return {
      statusCode: 200,
      body: JSON.stringify({message: 'Conversion and upload completed successfully.', jpgLocation:  uploadResult.Location})
    };
  } catch (error) {
    console.error('Error converting HEIC to JPG:', error);
    return {
      statusCode: 500,
      body: 'An error occurred during conversion and upload.',
    };
  }
};


function generateRandomString(length) {
  let result = '';
  const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  
  for (let i = 0; i < length; i++) {
    const randomIndex = Math.floor(Math.random() * characters.length);
    result += characters.charAt(randomIndex);
  }

  return result;
}

When you set up your Lambda functions, don't forget that you'll have to give them IAM permissions to read/write the relevant S3 buckets. You'll also need to adjust the amount of available execution time and memory allowances for the functions, as well as setting up enviornment variables for any sensitive data, like your bucket names.