Files
Yajbir Singh f1b860b25c
Some checks failed
check / markdownlint (push) Has been cancelled
check / spellchecker (push) Has been cancelled
updated
2025-12-11 19:03:17 +05:30

275 lines
10 KiB
JavaScript

/*
* (c) Copyright Ascensio System SIA 2010-2024
*
* This program is a free software product. You can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License (AGPL)
* version 3 as published by the Free Software Foundation. In accordance with
* Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
* that Ascensio System SIA expressly excludes the warranty of non-infringement
* of any third-party rights.
*
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
* details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
*
* You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
* street, Riga, Latvia, EU, LV-1050.
*
* The interactive user interfaces in modified source and object code versions
* of the Program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU AGPL version 3.
*
* Pursuant to Section 7(b) of the License you must retain the original Product
* logo when distributing the program. Pursuant to Section 7(e) we decline to
* grant you any rights under trademark law for use of our trademarks.
*
* All the Product's GUI elements, including illustrations and icon sets, as
* well as technical writing content are licensed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International. See the License
* terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
*
*/
'use strict';
const os = require('os');
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() {
let originalValues = {};
try {
const tmp = os.tmpdir();
// Save original values
originalValues = {
XDG_CACHE_HOME: process.env.XDG_CACHE_HOME,
HOME: process.env.HOME
};
// Set temporary values for Sharp initialization
process.env.XDG_CACHE_HOME = tmp;
process.env.HOME = tmp;
sharp = require('sharp');
} catch (error) {
operationContext.global.logger.warn('Sharp module failed to load. Image processing functionality will be limited.');
operationContext.global.logger.warn('Sharp load error:', error.message);
} finally {
// Restore original values
Object.keys(originalValues).forEach(key => {
if (originalValues[key] !== undefined) {
process.env[key] = originalValues[key];
} else {
delete process.env[key];
}
});
}
if (sharp) {
// todo test.
// Set concurrency to 2 for better performance
sharp.concurrency(2);
// Disable cache - not needed for one-time image conversion (writes to ./.cache dir)
sharp.cache(false);
}
}
// Load Sharp with graceful fallback for pkg-builds and missing dependencies
let sharp = null;
initializeSharp();
const {notificationTypes, ...notificationService} = require('../../Common/sources/notificationService');
const cfgStartNotifyFrom = ms(config.get('license.warning_license_expiration'));
const cfgNotificationRuleLicenseExpirationWarning = config.get('notification.rules.licenseExpirationWarning.template');
const cfgNotificationRuleLicenseExpirationError = config.get('notification.rules.licenseExpirationError.template');
/**
* Determine optimal format (PNG vs JPEG) for image conversion based on image characteristics.
* @param {operationContext} ctx Operation context for logging
* @param {Object} metadata Image metadata from sharp
* @returns {('png'|'jpeg')} Optimal format for conversion
*/
function determineOptimalFormat(ctx, metadata) {
// If image has alpha channel, only PNG can preserve transparency
if (metadata.hasAlpha) {
return 'png';
}
// Analyze color characteristics
const width = metadata.width || 0;
const height = metadata.height || 0;
// Small images (likely icons/logos) - prefer PNG
// Only apply when dimensions are known (greater than zero)
if (width > 0 && height > 0 && width <= 256 && height <= 256) {
return 'png';
}
// Large photographic images - prefer JPEG
if (width > 800 || height > 600) {
return 'jpeg';
}
// Default to JPEG for general compatibility and smaller file sizes
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
* 2. Converts modern/unsupported formats to optimal formats:
* - WebP/HEIC/HEIF/AVIF/TIFF: Convert to optimal format (PNG or JPEG) based on image characteristics
* @param {operationContext} ctx Operation context for logging
* @param {Buffer} buffer Source image bytes
* @returns {Promise<Buffer>} Processed and optimally converted buffer or original buffer
*/
async function processImageOptimal(ctx, buffer) {
if (!buffer) return buffer;
// Check if Sharp is available
if (!sharp) {
ctx.logger.warn('processImageOptimal: Sharp module not available, returning original buffer. Image processing disabled.');
return buffer;
}
try {
const meta = await sharp(buffer, {failOn: 'none'}).metadata();
const fmt = (meta.format || '').toLowerCase();
const needsRotation = meta.orientation && meta.orientation > 1;
// 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: converting %s to %s%s', fmt, optimalFormat, needsRotation ? ' with rotation' : '');
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;
}
}
// 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();
}
if (fmt === 'png') {
return await pipeline.png({compressionLevel: 7}).toBuffer();
}
return await pipeline.toBuffer();
}
} catch (e) {
ctx.logger.debug('processImageOptimal error: %s', e.stack);
}
return buffer;
}
/**
*
* @param {string} lang
* @returns {number | undefined}
*/
function localeToLCID(lang) {
const elem = locale[lang && lang.toLowerCase()];
return elem && elem.id;
}
function humanFriendlyExpirationTime(endTime) {
const month = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
return `${month[endTime.getUTCMonth()]} ${endTime.getUTCDate()}, ${endTime.getUTCFullYear()}`;
}
/**
* Notify server user about license expiration via configured notification transports.
* @param {Context} ctx Context.
* @param {Date} endDate Date of expiration.
* @returns {undefined}
*/
async function notifyLicenseExpiration(ctx, endDate) {
if (!endDate) {
ctx.logger.warn('notifyLicenseExpiration(): expiration date is not defined');
return;
}
const currentDate = new Date();
if (currentDate.getTime() >= endDate.getTime() - cfgStartNotifyFrom) {
//todo remove stub for "new Date(1)" and "setMonth + 1" in license.js; bug 70676
if (endDate.getUTCFullYear() < 2000) {
endDate = currentDate;
}
const formattedExpirationTime = humanFriendlyExpirationTime(endDate);
const applicationName = (process.env.APPLICATION_NAME || '').toUpperCase();
if (endDate <= currentDate) {
const tenNotificationRuleLicenseExpirationError = ctx.getCfg(
'notification.rules.licenseExpirationError.template',
cfgNotificationRuleLicenseExpirationError
);
const title = util.format(tenNotificationRuleLicenseExpirationError.title, applicationName);
const message = util.format(tenNotificationRuleLicenseExpirationError.body, formattedExpirationTime);
ctx.logger.error(message);
await notificationService.notify(ctx, notificationTypes.LICENSE_EXPIRATION_ERROR, title, message);
} else {
const tenNotificationRuleLicenseExpirationWarning = ctx.getCfg(
'notification.rules.licenseExpirationWarning.template',
cfgNotificationRuleLicenseExpirationWarning
);
const title = util.format(tenNotificationRuleLicenseExpirationWarning.title, applicationName);
const message = util.format(tenNotificationRuleLicenseExpirationWarning.body, formattedExpirationTime);
ctx.logger.warn(message);
await notificationService.notify(ctx, notificationTypes.LICENSE_EXPIRATION_WARNING, title, message);
}
}
}
module.exports.processImageOptimal = processImageOptimal;
module.exports.determineOptimalFormat = determineOptimalFormat;
module.exports.localeToLCID = localeToLCID;
module.exports.notifyLicenseExpiration = notifyLicenseExpiration;