[bug] Fallback to heic-decode for HEIC/HEIF when Sharp fails; Fix bug 76727

This commit is contained in:
Sergey Konovalov
2025-10-01 03:05:29 +03:00
parent c79b01f0e3
commit a8356eda6b
4 changed files with 63 additions and 24 deletions

View File

@ -37,6 +37,7 @@
- fakeredis 2.0.0 ([MIT](https://github.com/hdachev/fakeredis?tab=readme-ov-file#license))
- ioredis 5.6.0 ([MIT](https://raw.githubusercontent.com/redis/ioredis/main/LICENSE))
- jsonwebtoken 9.0.2 ([MIT](https://raw.githubusercontent.com/auth0/node-jsonwebtoken/master/LICENSE))
- heic-decode 2.1.0 ([ISC](https://opensource.org/license/isc-license-txt))
- mime 2.3.1 ([MIT](https://raw.githubusercontent.com/broofa/mime/main/LICENSE))
- mime-db 1.53.0 ([MIT](https://raw.githubusercontent.com/jshttp/mime-db/master/LICENSE))
- ms 2.1.3 ([MIT](https://raw.githubusercontent.com/vercel/ms/master/license.md))

View File

@ -1189,6 +1189,14 @@
"function-bind": "^1.1.2"
}
},
"heic-decode": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/heic-decode/-/heic-decode-2.1.0.tgz",
"integrity": "sha512-0fB3O3WMk38+PScbHLVp66jcNhsZ/ErtQ6u2lMYu/YxXgbBtl+oKOhGQHa4RpvE68k8IzbWkABzHnyAIjR758A==",
"requires": {
"libheif-js": "^1.19.8"
}
},
"http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@ -1437,6 +1445,11 @@
}
}
},
"libheif-js": {
"version": "1.19.8",
"resolved": "https://registry.npmjs.org/libheif-js/-/libheif-js-1.19.8.tgz",
"integrity": "sha512-vQJWusIxO7wavpON1dusciL8Go9jsIQ+EUrckauFYAiSTjcmLAsuJh3SszLpvkwPci3JcL41ek2n+LUZGFpPIQ=="
},
"lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",

View File

@ -21,6 +21,7 @@
"dmdb": "1.0.36002",
"ejs": "3.1.10",
"express": "4.21.2",
"heic-decode": "2.1.0",
"ioredis": "5.6.0",
"jsonwebtoken": "9.0.2",
"mime": "2.3.1",

View File

@ -37,6 +37,7 @@ const util = require('util');
const config = require('config');
const locale = require('windows-locale');
const ms = require('ms');
const decodeHeic = require('heic-decode');
const operationContext = require('./../../Common/sources/operationContext');
function initializeSharp() {
@ -117,6 +118,36 @@ function determineOptimalFormat(ctx, metadata) {
return 'jpeg';
}
/**
* Convert Sharp pipeline to buffer in optimal format (PNG or JPEG).
* @param {Object} pipeline Sharp pipeline instance
* @param {string} format Target format ('png' or 'jpeg')
* @returns {Promise<Buffer>} Converted image buffer
*/
async function convertToFormat(pipeline, format) {
if (format === 'png') {
return await pipeline.png({compressionLevel: 7}).toBuffer();
}
return await pipeline.jpeg({quality: 90, chromaSubsampling: '4:4:4'}).toBuffer();
}
/**
* Decode HEIC/HEIF buffer using heic-decode library and create Sharp instance.
* @param {Buffer} buffer HEIC/HEIF image buffer
* @returns {Promise<Object>} Sharp instance with decoded raw image data
*/
async function decodeHeicToSharp(buffer) {
const decodedImage = await decodeHeic({buffer});
return sharp(decodedImage.data, {
failOn: 'none',
raw: {
width: decodedImage.width,
height: decodedImage.height,
channels: 4
}
});
}
/**
* Process and optimize image buffer with EXIF rotation fix and modern format conversion.
* 1. Fixes EXIF rotation and strips metadata for all images
@ -135,42 +166,35 @@ async function processImageOptimal(ctx, buffer) {
return buffer;
}
let needsRotation = false;
try {
const meta = await sharp(buffer, {failOn: 'none'}).metadata();
needsRotation = meta.orientation && meta.orientation > 1;
const fmt = (meta.format || '').toLowerCase();
const needsRotation = meta.orientation && meta.orientation > 1;
// Handle modern formats that need conversion
if (fmt === 'webp' || fmt === 'heic' || fmt === 'heif' || fmt === 'avif') {
// Handle modern formats requiring conversion
if (fmt === 'heic' || fmt === 'heif' || fmt === 'webp' || fmt === 'avif' || fmt === 'tiff' || fmt === 'tif') {
const optimalFormat = determineOptimalFormat(ctx, meta);
ctx.logger.debug('processImageOptimal: detected %s, converting to %s%s', fmt, optimalFormat, needsRotation ? ' with EXIF rotation' : '');
ctx.logger.debug('processImageOptimal: converting %s to %s%s', fmt, optimalFormat, needsRotation ? ' with rotation' : '');
try {
const pipeline = sharp(buffer, {failOn: 'none'}).rotate();
if (optimalFormat === 'png') {
return await pipeline.png({compressionLevel: 7}).toBuffer();
} else {
return await pipeline.jpeg({quality: 90, chromaSubsampling: '4:4:4'}).toBuffer();
return await convertToFormat(pipeline, optimalFormat);
} catch (sharpError) {
// Fallback to heic-decode for HEIC/HEIF when Sharp fails
if (fmt === 'heic' || fmt === 'heif') {
ctx.logger.debug('processImageOptimal: Sharp failed for %s, using heic-decode fallback', fmt);
const heicPipeline = await decodeHeicToSharp(buffer);
return await convertToFormat(heicPipeline, optimalFormat);
}
throw sharpError;
}
}
if (fmt === 'tiff' || fmt === 'tif') {
const optimalFormat = determineOptimalFormat(ctx, meta);
ctx.logger.debug('processImageOptimal: detected TIFF, converting to %s%s', optimalFormat, needsRotation ? ' with EXIF rotation' : '');
const pipeline = sharp(buffer, {failOn: 'none'}).rotate();
if (optimalFormat === 'png') {
return await pipeline.png({compressionLevel: 7}).toBuffer();
} else {
return await pipeline.jpeg({quality: 90, chromaSubsampling: '4:4:4'}).toBuffer();
}
}
// For other formats, only apply EXIF rotation if needed
// For standard formats, only apply EXIF rotation if needed
if (needsRotation) {
ctx.logger.debug('processImageOptimal: applying EXIF rotation to %s', fmt);
const pipeline = sharp(buffer, {failOn: 'none'}).rotate();
if (fmt === 'jpeg' || fmt === 'jpg') {
return await pipeline.jpeg({quality: 90, chromaSubsampling: '4:4:4'}).toBuffer();
}