11

When reading a file from the input element by the FileReader api it works, but my android device also allows sending files and it seems to send heic files anyway which then results in an empty image without any errors in the console. Also the orientation is wrong when getting an image directly from the camera. I just found heavy librarys to implement and i am looking for a smarter solution.

JavaScript

function previewFile( e ) {
    var preview = document.getElementById('usersProfilePicture');
    var file    = e.files[0];
    var reader  = new FileReader();

    reader.onloadend = function () {
        preview.src = reader.result;
    }
    if (file) {
        reader.readAsDataURL(file);
    } else {
        preview.src = "";
    }
}

HTML5

<form>
    <label>
        <input id="uploadProfilePicture" name=file type=file accept="image/jpg, image/jpeg, image/png, image/gif, image/bmp">
    </label>
</form>

There are no error messages at all. Firefox, Chrome both on desktop and android allow .heic files no matter what accept attribute i set.

worst solution: deny acceptance of .heic files best solution: make fileReader work with .heic files. in between solution: detect heic and convert it to jpeg, clientside.

Sascha Grindau
  • 418
  • 2
  • 6
  • 18
  • 1
    i figures out that the current stable android chrome app allows selecting of invisible .heic files and does not respect the accept attribute, however the new canary build hides .heic files. So this is getting fixed in future automatically. But heic is gaining popularity, i have to support this. So i still need a proper solution to this. – Sascha Grindau Jul 20 '19 at 18:48
  • 3
    Bumping this, I need a solution for heic as well. – douglasrcjames Dec 26 '19 at 21:30

3 Answers3

9

The answer above explains this very well but for anyone looking for another example I have slightly modified the example from Heic2Any (https://alexcorvi.github.io/heic2any/)

<input type="file" id="heic" onchange="convertHEIC(event)">
async function convertHEIC (event){
    let output = document.getElementById('output');

    //if HEIC file
    if(event.target.files[0] && event.target.files[0].name.includes(".HEIC")){
        // get image as blob url
        let blobURL = URL.createObjectURL(event.target.files[0]);
        
        // convert "fetch" the new blob url
        let blobRes = await fetch(blobURL)

        // convert response to blob
        let blob = await blobRes.blob()

        // convert to PNG - response is blob
        let conversionResult = await heic2any({ blob })

        // convert to blob url
        var url = URL.createObjectURL(conversionResult);
        document.getElementById("target").innerHTML = `<a target="_blank" href="${url}"><img src="${url}"></a>`;
    
    }       
};
Jordan Badger
  • 91
  • 1
  • 3
4

I have a workaround for this issue for now by using the library heic2any

(https://github.com/alexcorvi/heic2any)

check to see if file from input is .heic, then use library like so:

heic2any({
        // required: the HEIF blob file
        blob: file,
        // (optional) MIME type of the target file
        // it can be "image/jpeg", "image/png" or "image/gif"
        // defaults to "image/png"
        toType: "image/jpeg",
        // conversion quality
        // a number ranging from 0 to 1
        quality: 0.5
    })

I wrap this call in a promise and then pass the result to the file reader:

// uploadHEIC is a wrapper for heic2any
uploadHEIC(heicFile).then(function (heicToJpgResult) {
    var reader = new Filereader();
    reader.onload = function () {
    // Do what you want to file
    }
    reader.readAsArrayBuffer(heicToJpgResult);
}
2

A few things to note to get this working properly.

First, in windows the assigned mime type for heic and heif is blank. Not sure when this bug will get fixed, but for now you can't rely on mime type in your scripts or input tag. I needed to add the heic and heif file extensions in the accept parameter of my input tag:

<input type="file" accept="image/*,.heic,.heif" />

and in my script I created a function to check the file extension for heic and heif if mime type was blank.

function isHEIC(file) { // check file extension since windows returns blank mime for heic
    let x = file.type ? file.type.split('image/').pop() : file.name.split('.').pop().toLowerCase();
    return x == 'heic' || x == 'heif';
}

Also, heic2any is pretty large (even if minified and compressed). I decided to load it dynamically only when needed.

function loadScript(url, callback) {
    var script = document.querySelectorAll('script');
    for (var i = 0; i < script.length; i++) {
        if (script[i].src === url) {
            script = script[i];
            if (!script.readyState && !script.onload) {
                callback();
            } else { // script not loaded so wait up to 10 seconds
                var secs = 0, thisInterval = setInterval(function() {
                    secs++;
                    if (!script.readyState && !script.onload) {
                        clearInterval(thisInterval);
                        callback();
                    } else if (secs == 10) {
                        clearInterval(thisInterval);
                        console.log('could not load ' + url);
                    }
                }, 1000);
            }
            return;
        }
    }
    script = document.createElement('script');
    script.type = 'text/javascript';
    document.getElementsByTagName('head')[0].appendChild(script);
    if (script.readyState) {
        script.onreadystatechange = function() {
            if (script.readyState === 'loaded' || script.readyState === 'complete') {
                script.onreadystatechange = null;
                callback();
            }
        }
    } else {
        script.onload = function() {
            script.onload = null;
            callback();
        }
    }
    script.src = url;
}

I my use case I'm leveraging heic2any to prepare images for upload. If the image is heic I convert it to png (blob), then pass the result to another utility (image blob reduce) to resize and sharpen before converting to jpg in preparation for upload. However, for the sake of simplicity the example below uses heic2any to convert to jpg in 1 step before upload.

function convertHEIC(file) {
    return new Promise(function(resolve) {
        if (!isHEIC(file)) return resolve(file);
        loadScript('https://cdn.jsdelivr.net/npm/heic2any@0.0.3/dist/heic2any.min.js', function() {
            heic2any({
                blob: file,
                toType: "image/jpg"
            }).then(function (convertedFile) {
                convertedFile.name = file.name.substring(0, file.name.lastIndexOf('.')) + '.jpeg';
                resolve(convertedFile);
            });
        });
    });
}

// convert any heic (and do any other prep) before uploading the file
convertHEIC(file).then(function(file) {
    // code to upload (or do something else with file)
    .
    .
    .
}
Tom Davenport
  • 341
  • 4
  • 7
  • A couple issues... heic2any does not seem to support "image/jpg", needs to be "image/jpeg" (or else it defaults to "image/png"). Also script.readyState and script.onreadystatechange DONT seem to be a thing, so the loadScript just loads the script every time. – devlop Sep 02 '22 at 00:16
  • Thanks, for the feedback. I've corrected the jpeg typo. As far as dynamic script loading, is it not doing exactly what it should (load the script when you call the function)? I found that code here: https://stackoverflow.com/a/71209306/7522114 and it works fine for me. Can you provide more clarity on what the issue is? – Tom Davenport Sep 04 '22 at 10:28
  • Also regarding use of onreadystatechange with script, see here: https://stackoverflow.com/a/31374433/7522114 (note lots of upvotes with a link to further discussion so I think it should be fine to use) but again, if you're having an issue I'm curious to know what might not be working. – Tom Davenport Sep 04 '22 at 10:47
  • Debugging the code neither `readyState` nor `onreadystatechange` are properties of script (HtmlScriptElement) - compared to `onload` which is a property (null by default). Adding logging to the code, I see that the readyState condition always fails, and the else block always runs. I'm not an authority on the spec (at all) but I don't think script elements support these properties (and that other post you ref is really just using onload, the onreadystatechange is unused) - at least from my testing. – devlop Sep 05 '22 at 18:41
  • There is another bug in the code, the `script.find(function(script) { script.src === url; })` isn't returning anything, so the find will always return `undefined`, and so a new script element is added every invocation. The side effect of this is that onload will run each time. Fixing that bug, eg `{return script.src === url; }`, makes onload only run the first invocation (as desired) but subsequent calls will not run the callback (since the readyState/onreadystatechange properties are not doing anything). – devlop Sep 05 '22 at 18:45
  • Thanks for the heads up. That will teach me to use a function found on stackoverflow that I haven't fully tested! I updated the code so it should work now. FWIW, readyState is only there as a fallback to support older browsers (modern browsers will use onload). Hopefully this fixes your issues. – Tom Davenport Sep 07 '22 at 00:13
  • This worked great for me and the dynamic load of the script only when needed is perfect, no added weight to the app, well done. – OG Sean Aug 23 '23 at 21:05