3

I'm looking for a function that takes an image and which makes sure that the target file size is smaller than the given size, but as big as possible.

The reason is for creating a vcard the max. size of the photo is 224KB.
(Apple or vcard limitation: https://support.apple.com/en-us/HT202158)

I would like to have a function like this:

function (imageBase64, maxFileSize)
{
    // Check if image is smaller
    // If yes, return the old one.
    // If not, reduce image size proportional to fit in maxFileSize
    // return newImage
}

Do you have any hints how to solve it?

I would prefer to do this client side in js. But if it wouldn't be possible, I accept also server side php solutions.

1 Answer 1

6

One possible solution is to change the jpeg quality setting.

Rather than iterate many quality setting I have created a estimate of image sizes to quality settings.

I get the file size of the image at the best quality then use the estimates sizes to guess the quality setting to be under the file size. To get as close as possible I move the quality settings up and down and check the file size a few times.

There is a threshold size that if a quality setting is found that has the file size under the required size and above the threshold percentage of the required size then that quality setting is used.

If the threshold is not found it will use the best quality of quality setting that passed.

If it had trouble finding a quality setting then it will just give a very low setting.

If the quality setting is zero then it has failed.

Functions needed

// this function converts a image to a canvas image
function image2Canvas(image){
    var canvas = document.createElement("canvas");
    canvas.width = image.width;
    canvas.height = image.height;
    canvas.ctx = canvas.getContext("2d");
    canvas.ctx.drawImage(image,0,0);
    return canvas;
}
// warning try to limit calls to this function as it can cause problems on some systems
// as they try to keep up with GC
// This function gets the file size by counting the number of Base64 characters and 
// calculating the number of bytes encoded.
function getImageFileSize(image,quality){  // image must be a canvas
    return Math.floor(image.toDataURL("image/jpeg",quality).length * (3/4));
}

function qualityForSize(image,fileSize){
    // These are approximations only
    // and are the result of using a test image and finding the file size
    // at quality setting 1 to 0.1 in 0.1 steps
    const scalingFactors = [
        5638850/5638850,
        1706816/5638850,
        1257233/5638850,
        844268/5638850,
        685253/5638850,
        531014/5638850,
        474293/5638850,
        363686/5638850,
        243578/5638850,
        121475/5638850,
        0, // this is added to catch the stuff ups.
    ]
    var size = getImageFileSize(image,1); // get file size at best quality;
    if(size <= fileSize){ // best quality is a pass
        return 1;
    }
    // using size make a guess at the quality setting
    var index = 0;
    while(size * scalingFactors[index] > fileSize){ index += 1 }
    if(index === 10){  // Could not find a quality setting 
        return 0; // this is bad and should not be used as a quality setting
    }
    var sizeUpper = size * scalingFactors[index-1];  // get estimated size at upper quality
    var sizeLower = size * scalingFactors[index];  // get estimated size at lower quality
    // estimate quality via linear interpolation
    var quality = (1-(index/10)) + ((fileSize - sizeLower) / (sizeUpper-sizeLower)) * 0.1;
    var qualityStep = 0.02; // the change in quality (this value gets smaller each try)
    var numberTrys = 3;  //  number of trys to get as close as posible to the file size
    var passThreshold = 0.90; // be within 90% of desiered file size
    var passQualities = []; // array of quality settings that are under file size
    while(numberTrys--){
         var newSize = getImageFileSize(image,quality); // get the file size for quality guess
         if(newSize <= fileSize && newSize/fileSize > passThreshold ){ // does it pass?
             return quality;  // yes return quality
         }
         if(newSize > fileSize){  // file size too big 
            quality -= qualityStep;  // try lower quality
            qualityStep /= 2;        // reduce the quality step for next try
         }else{
            passQualities.push(quality);  // save this quality incase nothing get within the pass threashold
            quality += qualityStep;  // step the quality up.
            qualityStep /= 2;        // reduce the size of the next quality step     
         }
    }
    // could not find a quality setting so get the best we did find
    if(passQualities.length > 0){ //check we did get a pass
           passQualities.sort();  // sort to get best pass quality
           return passQualities.pop(); // return best quality that passed
    }
    // still no good result so just default to next 0.1 step down
    return 1-((index+1)/10);
}

How to use

// testImage is the image to set quality of
// the image must be loaded
var imgC = image2Canvas(testImage); // convert image to a canvas
var qualitySetting = qualityForSize(imgC,244000);   // find the image quality to be under file size 244000
// convert to data URL
var dataURL = imgC.toDataURL("image/jpeg",qualitySetting);
// the saved file will be under 244000
Sign up to request clarification or add additional context in comments.

1 Comment

Your solution works very well. One doubt, in IE the image obtained is png although in the code is specified "image/jpeg". Do you have any idea why this happens?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.