Add thumbnail dimensions to config (#3262) * Add thumbnail dimensions to config * Fix default value for thumbnails.enabled * Update comment for thumbnail recreation instructions in config.yaml * Lint config values * Verify config size > 0 * More config lint

8623d1198d75f971ee8547271a7daeee24f7b92d

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
3 files changed, +70 -16Showing whitespace changes
default/config.yaml+15 -7
@@ -83,13 +83,21 @@ autorun: true
83# Avoids using 'localhost' for autorun in auto mode.83# Avoids using 'localhost' for autorun in auto mode.
84# use if you don't have 'localhost' in your hosts file84# use if you don't have 'localhost' in your hosts file
85avoidLocalhost: false85avoidLocalhost: false
86# Disable thumbnail generation86
87disableThumbnails: false87# THUMBNAILING CONFIGURATION
88# Thumbnail quality (0-100)88thumbnails:
89thumbnailsQuality: 9589 # Enable thumbnail generation
90# Generate avatar thumbnails as PNG instead of JPG (preserves transparency but increases filesize by about 100%)90 enabled: true
91# Changing this only affects new thumbnails. To recreate the old ones, clear out your ST/thumbnails/ folder.91 # Image format of avatar thumbnails:
92avatarThumbnailsPng: false92 # * "jpg": best compression with adjustable quality, no transparency
93 # * "png": preserves transparency but increases filesize by about 100%
94 # Changing this only affects new thumbnails. To recreate the old ones, clear out /thumbnails folder in your user data.
95 format: "jpg"
96 # JPG thumbnail quality (0-100)
97 quality: 95
98 # Maximum thumbnail dimensions per type [width, height]
99 dimensions: { 'bg': [160, 90], 'avatar': [96, 144] }
100
93# Allow secret keys exposure via API101# Allow secret keys exposure via API
94allowKeysExposure: false102allowKeysExposure: false
95# Skip new default content checks103# Skip new default content checks
post-install.js+44 -1
@@ -28,6 +28,24 @@ const color = {
28 white: (mess) => color.byNum(mess, 37),28 white: (mess) => color.byNum(mess, 37),
29};29};
3030
31const keyMigrationMap = [
32 {
33 oldKey: 'disableThumbnails',
34 newKey: 'thumbnails.enabled',
35 migrate: (value) => !value,
36 },
37 {
38 oldKey: 'thumbnailsQuality',
39 newKey: 'thumbnails.quality',
40 migrate: (value) => value,
41 },
42 {
43 oldKey: 'avatarThumbnailsPng',
44 newKey: 'thumbnails.format',
45 migrate: (value) => (value ? 'png' : 'jpg'),
46 },
47];
48
31/**49/**
32 * Gets all keys from an object recursively.50 * Gets all keys from an object recursively.
33 * @param {object} obj Object to get all keys from51 * @param {object} obj Object to get all keys from
@@ -83,6 +101,24 @@ function addMissingConfigValues() {
83 const defaultConfig = yaml.parse(fs.readFileSync(path.join(process.cwd(), './default/config.yaml'), 'utf8'));101 const defaultConfig = yaml.parse(fs.readFileSync(path.join(process.cwd(), './default/config.yaml'), 'utf8'));
84 let config = yaml.parse(fs.readFileSync(path.join(process.cwd(), './config.yaml'), 'utf8'));102 let config = yaml.parse(fs.readFileSync(path.join(process.cwd(), './config.yaml'), 'utf8'));
85103
104 // Migrate old keys to new keys
105 const migratedKeys = [];
106 for (const { oldKey, newKey, migrate } of keyMigrationMap) {
107 if (_.has(config, oldKey)) {
108 const oldValue = _.get(config, oldKey);
109 const newValue = migrate(oldValue);
110 _.set(config, newKey, newValue);
111 _.unset(config, oldKey);
112
113 migratedKeys.push({
114 oldKey,
115 newKey,
116 oldValue,
117 newValue,
118 });
119 }
120 }
121
86 // Get all keys from the original config122 // Get all keys from the original config
87 const originalKeys = getAllKeys(config);123 const originalKeys = getAllKeys(config);
88124
@@ -95,11 +131,18 @@ function addMissingConfigValues() {
95 // Find the keys that were added131 // Find the keys that were added
96 const addedKeys = _.difference(updatedKeys, originalKeys);132 const addedKeys = _.difference(updatedKeys, originalKeys);
97133
98 if (addedKeys.length === 0) {134 if (addedKeys.length === 0 && migratedKeys.length === 0) {
99 return;135 return;
100 }136 }
101137
138 if (addedKeys.length > 0) {
102 console.log('Adding missing config values to config.yaml:', addedKeys);139 console.log('Adding missing config values to config.yaml:', addedKeys);
140 }
141
142 if (migratedKeys.length > 0) {
143 console.log('Migrating config values in config.yaml:', migratedKeys);
144 }
145
103 fs.writeFileSync('./config.yaml', yaml.stringify(config));146 fs.writeFileSync('./config.yaml', yaml.stringify(config));
104 } catch (error) {147 } catch (error) {
105 console.error(color.red('FATAL: Could not add missing config values to config.yaml'), error);148 console.error(color.red('FATAL: Could not add missing config values to config.yaml'), error);
src/endpoints/thumbnails.js+11 -8
@@ -12,9 +12,12 @@ import { getAllUserHandles, getUserDirectories } from '../users.js';
12import { getConfigValue } from '../util.js';12import { getConfigValue } from '../util.js';
13import { jsonParser } from '../express-common.js';13import { jsonParser } from '../express-common.js';
1414
15const thumbnailsDisabled = getConfigValue('disableThumbnails', false);15const thumbnailsEnabled = !!getConfigValue('thumbnails.enabled', true);
16const quality = getConfigValue('thumbnailsQuality', 95);16const quality = Math.min(100, Math.max(1, parseInt(getConfigValue('thumbnails.quality', 95))));
17const pngFormat = getConfigValue('avatarThumbnailsPng', false);17const pngFormat = String(getConfigValue('thumbnails.format', 'jpg')).toLowerCase().trim() === 'png';
18
19/** @type {Record<string, number[]>} */
20const dimensions = getConfigValue('thumbnails.dimensions', { 'bg': [160, 90], 'avatar': [96, 144] });
1821
19/**22/**
20 * Gets a path to thumbnail folder based on the type.23 * Gets a path to thumbnail folder based on the type.
@@ -114,16 +117,16 @@ async function generateThumbnail(directories, type, file) {
114 return null;117 return null;
115 }118 }
116119
117 const imageSizes = { 'bg': [160, 90], 'avatar': [96, 144] };
118 const mySize = imageSizes[type];
119
120 try {120 try {
121 let buffer;121 let buffer;
122122
123 try {123 try {
124 const size = dimensions[type];
124 const image = await jimp.read(pathToOriginalFile);125 const image = await jimp.read(pathToOriginalFile);
125 const imgType = type == 'avatar' && pngFormat ? 'image/png' : 'image/jpeg';126 const imgType = type == 'avatar' && pngFormat ? 'image/png' : 'image/jpeg';
126 buffer = await image.cover(mySize[0], mySize[1]).quality(quality).getBufferAsync(imgType);127 const width = !isNaN(size?.[0]) && size?.[0] > 0 ? size[0] : image.bitmap.width;
128 const height = !isNaN(size?.[1]) && size?.[1] > 0 ? size[1] : image.bitmap.height;
129 buffer = await image.cover(width, height).quality(quality).getBufferAsync(imgType);
127 }130 }
128 catch (inner) {131 catch (inner) {
129 console.warn(`Thumbnailer can not process the image: ${pathToOriginalFile}. Using original size`);132 console.warn(`Thumbnailer can not process the image: ${pathToOriginalFile}. Using original size`);
@@ -193,7 +196,7 @@ router.get('/', jsonParser, async function (request, response) {
193 return response.sendStatus(403);196 return response.sendStatus(403);
194 }197 }
195198
196 if (thumbnailsDisabled) {199 if (!thumbnailsEnabled) {
197 const folder = getOriginalFolder(request.user.directories, type);200 const folder = getOriginalFolder(request.user.directories, type);
198201
199 if (folder === undefined) {202 if (folder === undefined) {