diff --git a/3DPARTY.md b/3DPARTY.md index cd3bc5cc..8349066d 100644 --- a/3DPARTY.md +++ b/3DPARTY.md @@ -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)) diff --git a/DocService/npm-shrinkwrap.json b/DocService/npm-shrinkwrap.json index 14b95ebe..c08c0e87 100644 --- a/DocService/npm-shrinkwrap.json +++ b/DocService/npm-shrinkwrap.json @@ -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", diff --git a/DocService/package.json b/DocService/package.json index 65defc76..2e4b5358 100644 --- a/DocService/package.json +++ b/DocService/package.json @@ -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", diff --git a/DocService/sources/utilsDocService.js b/DocService/sources/utilsDocService.js index 10eed828..77942c43 100644 --- a/DocService/sources/utilsDocService.js +++ b/DocService/sources/utilsDocService.js @@ -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} 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} 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' : ''); - 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(); + try { + const pipeline = sharp(buffer, {failOn: 'none'}).rotate(); + 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(); }