This is my approach. The most glaring inefficiency is calling convertToHex on the image blob. It only knows about PNG, JPEG, and GIF. This is code I've pieced together by reading file format specs, so it's quite possible that I've over-simplified things a bit and it may fail for certain images (it works on the few I tested). JPEG in particular is much harder to parse than PNG or GIF... it's likely that there are some JPEG encoders that will write an image that my code can't parse.
I left in imgtest() to show examples on how it may be used.
global class ImageSizer {
class ImageInfo {
public boolean success;
public string format;
public integer width;
public integer height;
}
public static void imgtest() {
Case caserec = [select id, description_rt__c from case where id = '5000j000004NowzAAC'];
string descrt = caserec.description_rt__c;
pattern imgpatt = pattern.compile('<img .*?src="(.+?)"');
matcher imgmatch = imgpatt.matcher(descrt);
while(imgmatch.find()) {
string url = imgmatch.group(1);
url = url.unescapeHtml4();
system.debug('foung img src: ' + url);
ImageInfo imginfo = getImageSize(url);
if(imginfo.success) {
system.debug('format: ' + imginfo.format);
system.debug('size: ' + imginfo.width + 'x' + imginfo.height);
} else {
system.debug('failed to get image metadata');
}
}
ImageInfo imginfo = getImageSize('https://upload.wikimedia.org/wikipedia/en/thumb/c/c1/The_Matrix_Poster.jpg/220px-The_Matrix_Poster.jpg');
system.debug('Jpeg test1: ' + imginfo);
imginfo = getImageSize('https://upload.wikimedia.org/wikipedia/commons/thumb/a/a4/Anatomy_of_a_Sunset-2.jpg/1280px-Anatomy_of_a_Sunset-2.jpg');
system.debug('Jpeg test2: ' + imginfo);
imginfo = getImageSize('https://methode--amigo--c.documentforce.com/sfc/servlet.shepherd/version/renditionDownload?rendition=ORIGINAL_Jpg&versionId=0680j000000yGrO&operationContext=CHATTER&contentId=05T0j000002oIQF');
system.debug('Jpeg test3: ' + imginfo);
imginfo = getImageSize('https://upload.wikimedia.org/wikipedia/commons/b/bb/Quilt_design_as_46x46_uncompressed_GIF.gif');
system.debug('GIF test: ' + imginfo);
imginfo = getImageSize('https://upload.wikimedia.org/wikipedia/commons/1/11/B%C3%B6dele_Bregenzerwald_Panorama.jpg');
system.debug('Pano test (2.5MB): ' + imginfo);
// 29.5MB image give heap overflow error
//imginfo = getImageSize('https://upload.wikimedia.org/wikipedia/commons/e/ea/Panorama_from_poonhill-2019-BJ.jpg');
//system.debug('Big jpeg test: ' + imginfo);
}
public static ImageInfo getImageSize(string url) {
pagereference page = new pagereference(url);
blob imgdata = page.getContent();
// Can't substring a blob, have to do the ugly and double the size as a hex string
// When parsing the hex string, all offsets must be doubled, requiring *2 below
string imgstring = EncodingUtil.convertToHex(imgdata);
ImageInfo imginfo = new ImageInfo();
imginfo.success = false;
imginfo.format = 'UNKNOWN "' + imgstring.substring(0,16) + '"';
imginfo.width = 0;
imginfo.height = 0;
if (imgstring.substring(0,8) == '89504e47') {
// PNG
imginfo.success = true;
imginfo.format = 'PNG';
imginfo.width = hexToInt(imgstring.substring(16*2,20*2));
imginfo.height = hexToInt(imgstring.substring(20*2,24*2));
} else if (imgstring.substring(0,4) == 'ffd8') {
// JPEG
// Might need a lot of the imgstring, but don't want to pass it by value
// due to limited heap space. Instead, just parse it all here.
imginfo.format = 'JPEG';
imginfo.width = 0;
imginfo.height = 0;
imginfo.success = false;
integer offset = 2 *2; // Skip past the SOI marker that we already found
while (offset < imgstring.length()) {
if (imgstring.substring(offset, offset+3) == 'ffc') { // SOFn
imginfo.success = true;
imginfo.height = hexToInt(imgstring.substring(offset+5*2, offset+7*2));
imginfo.width = hexToInt(imgstring.substring(offset+7*2, offset+9*2));
break;
} else if (imgstring.substring(offset, offset+2*2) == 'ffd8' // SOI (may appear after thumbnail)
|| imgstring.substring(offset, offset+2*2) == 'ffd9' // EOF (maybe end of thumbnail, keep going)
) {
offset += 2 *2;
} else if (imgstring.substring(offset, offset+2) == 'ff') {
// Most blocks are variable length, find the length and skip it
integer blocklen = hexToInt(imgstring.substring(offset+4, offset+8));
offset += (blocklen+2) * 2;
} else { // Not a header, either bad data or bad parsing, stop reading.
break;
}
}
} else if (imgstring.substring(0, 8) == '47494638') {
// GIF
imginfo.success = true;
imginfo.format = 'GIF';
imginfo.width = hexToInt(imgstring.substring(7*2, 8*2) + imgstring.substring(6*2, 7*2));
imginfo.height = hexToInt(imgstring.substring(9*2, 10*2) + imgstring.substring(8*2, 9*2));
}
return imginfo;
}
public static final List<String> hex = '0123456789abcdef'.split('');
public static Integer hexToInt(String str) {
str = str.toLowerCase();
String[] hexValue = str.split('');
Integer retVal = 0;
for(Integer i = 0; i < hexValue.size(); i++) {
retVal = (retVal << 4) + hex.indexOf(hexValue[i]);
}
return retVal;
}
}
Credit for hexToInt() goes to Henk3000 for his answer to this question.