mirror of
https://github.com/ONLYOFFICE/server.git
synced 2026-02-10 18:05:07 +08:00
[bug] Fallback to heic-decode for HEIC/HEIF when Sharp fails; Fix bug 76727
This commit is contained in:
@ -37,6 +37,7 @@
|
|||||||
- fakeredis 2.0.0 ([MIT](https://github.com/hdachev/fakeredis?tab=readme-ov-file#license))
|
- 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))
|
- 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))
|
- 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 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))
|
- 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))
|
- ms 2.1.3 ([MIT](https://raw.githubusercontent.com/vercel/ms/master/license.md))
|
||||||
|
|||||||
13
DocService/npm-shrinkwrap.json
generated
13
DocService/npm-shrinkwrap.json
generated
@ -1189,6 +1189,14 @@
|
|||||||
"function-bind": "^1.1.2"
|
"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": {
|
"http-errors": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
"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": {
|
"lodash.defaults": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
"dmdb": "1.0.36002",
|
"dmdb": "1.0.36002",
|
||||||
"ejs": "3.1.10",
|
"ejs": "3.1.10",
|
||||||
"express": "4.21.2",
|
"express": "4.21.2",
|
||||||
|
"heic-decode": "2.1.0",
|
||||||
"ioredis": "5.6.0",
|
"ioredis": "5.6.0",
|
||||||
"jsonwebtoken": "9.0.2",
|
"jsonwebtoken": "9.0.2",
|
||||||
"mime": "2.3.1",
|
"mime": "2.3.1",
|
||||||
|
|||||||
@ -37,6 +37,7 @@ const util = require('util');
|
|||||||
const config = require('config');
|
const config = require('config');
|
||||||
const locale = require('windows-locale');
|
const locale = require('windows-locale');
|
||||||
const ms = require('ms');
|
const ms = require('ms');
|
||||||
|
const decodeHeic = require('heic-decode');
|
||||||
const operationContext = require('./../../Common/sources/operationContext');
|
const operationContext = require('./../../Common/sources/operationContext');
|
||||||
|
|
||||||
function initializeSharp() {
|
function initializeSharp() {
|
||||||
@ -117,6 +118,36 @@ function determineOptimalFormat(ctx, metadata) {
|
|||||||
return 'jpeg';
|
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.
|
* Process and optimize image buffer with EXIF rotation fix and modern format conversion.
|
||||||
* 1. Fixes EXIF rotation and strips metadata for all images
|
* 1. Fixes EXIF rotation and strips metadata for all images
|
||||||
@ -135,42 +166,35 @@ async function processImageOptimal(ctx, buffer) {
|
|||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
let needsRotation = false;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const meta = await sharp(buffer, {failOn: 'none'}).metadata();
|
const meta = await sharp(buffer, {failOn: 'none'}).metadata();
|
||||||
needsRotation = meta.orientation && meta.orientation > 1;
|
|
||||||
const fmt = (meta.format || '').toLowerCase();
|
const fmt = (meta.format || '').toLowerCase();
|
||||||
|
const needsRotation = meta.orientation && meta.orientation > 1;
|
||||||
|
|
||||||
// Handle modern formats that need conversion
|
// Handle modern formats requiring conversion
|
||||||
if (fmt === 'webp' || fmt === 'heic' || fmt === 'heif' || fmt === 'avif') {
|
if (fmt === 'heic' || fmt === 'heif' || fmt === 'webp' || fmt === 'avif' || fmt === 'tiff' || fmt === 'tif') {
|
||||||
const optimalFormat = determineOptimalFormat(ctx, meta);
|
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();
|
const pipeline = sharp(buffer, {failOn: 'none'}).rotate();
|
||||||
if (optimalFormat === 'png') {
|
return await convertToFormat(pipeline, optimalFormat);
|
||||||
return await pipeline.png({compressionLevel: 7}).toBuffer();
|
} catch (sharpError) {
|
||||||
} else {
|
// Fallback to heic-decode for HEIC/HEIF when Sharp fails
|
||||||
return await pipeline.jpeg({quality: 90, chromaSubsampling: '4:4:4'}).toBuffer();
|
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') {
|
// For standard formats, only apply EXIF rotation if needed
|
||||||
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
|
|
||||||
if (needsRotation) {
|
if (needsRotation) {
|
||||||
ctx.logger.debug('processImageOptimal: applying EXIF rotation to %s', fmt);
|
ctx.logger.debug('processImageOptimal: applying EXIF rotation to %s', fmt);
|
||||||
const pipeline = sharp(buffer, {failOn: 'none'}).rotate();
|
const pipeline = sharp(buffer, {failOn: 'none'}).rotate();
|
||||||
|
|
||||||
if (fmt === 'jpeg' || fmt === 'jpg') {
|
if (fmt === 'jpeg' || fmt === 'jpg') {
|
||||||
return await pipeline.jpeg({quality: 90, chromaSubsampling: '4:4:4'}).toBuffer();
|
return await pipeline.jpeg({quality: 90, chromaSubsampling: '4:4:4'}).toBuffer();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user