angularcropperjsheic

Angular 9: How to convert HEIF file format to a known web format when uploading images


I am mantaining a webapp (PWA) written in Angular 9, where users upload images and crop, rotate etc. in cropperjs.

On iOS a new image format (HEIF) is becoming the standard and these users are complaining that they are not able to upload their photos. It seems that sometimes iOS will convert to jpg automatically, and sometimes it doesn't. Therefore we need to be able to receive images in HEIF format.

I tried adding the mimetype image/heif to the accept attribute but the *.heic images are still dimmed on iOS. I seems that many are just choosing to accept all files, but that is not an option for this web app.

Also cropperjs does not support the HEIF image format, so how do we convert to a know web format?


Solution

  • The solution for us, was to install heic2any:

    npm install heic2any
    

    Then import it in the component where it is needed:

    import heic2any from "heic2any";
    

    In the accept attribute add the mimetype and the file extension.

    image/jpeg, image/png, image/heif, .heic, .heif
    

    And if you need to support upload of any kind of image:

    image/*, .heic, .heif
    

    We are using ngfSelect, and it looks like this (simplified):

    <div ngfSelect
      [accept]  = "'image/*, .heic, .heif'"
      (filesChange) = "imageSelected($event)">
      
    

    Below is a simplified version of the imageSelected() function

    public imageSelected(event) {
      let f:File;
    
      //RECEIVE IMAGE
    
      if (event[0] && (event[0]['lastModified'] || event[0]['lastModifiedDate'])) {
        f = event[0];
        if (event.length > 1) {
          event.splice(0,event.length-1);
          f = event[0];
        }
      } else if (event.target && event.target.files && event.target.files[0]) {
        f = event.target.files[0];
      }
    
      if (!f) {
        //Handle error and exit
      }
    
      let blob:Blob = f;
      let file:File = f;
    
      let convProm:Promise<any>;
      
      //CONVERT HEIC TO JPG
    
      if (/image\/hei(c|f)/.test(f.type)) {
        convProm = heic2any({blob,toType:"image/jpeg",quality:0}).then((jpgBlob:Blob) => {
    
          //Change the name of the file according to the new format
          let newName = f.name.replace(/\.[^/.]+$/, ".jpg");
    
          //Convert blob back to file
          file = this.blobToFile(jpgBlob,newName);
    
        }).catch(err => {
          //Handle error
        });
      } else {
        //This is not a HEIC image so we can just resolve
        convProm = Promise.resolve(true);
      }
    
      //ADD IMAGE FILE TO CROPPERJS EDITOR
    
      convProm.then(() => {
        
        let reader = new FileReader();
        let _thisComp = this;
    
        //Add file to FileReader
        if (file) {
          reader.readAsDataURL(file);
        }
        //Listen for FileReader to get ready
        reader.onload = function () {
          
          //Set imageUrl. This will trigger initialization of cropper via imageLoaded() 
          //as configured in the html img tag:
          //<img #image id="image" [src]="imageUrl" (load)="imageLoaded($event)" class="cropper"> 
    
          _thisComp.imageUrl = reader.result;
        }
      });
    }
    
    
    private blobToFile = (theBlob: Blob, fileName:string): File => {
      let b: any = theBlob;
    
      //A Blob() is almost a File() - it's just missing the two properties below which we will add
      b.lastModified = new Date();
      b.name = fileName;
    
      //Cast to a File() type
      return <File>theBlob;
    }
    

    The above may not be the prettiest solution, but I hope it can be of help and inspiration to others facing the same challenge.