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
Signed| @@ -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 file | 84 | # use if you don't have 'localhost' in your hosts file |
| 85 | avoidLocalhost: false | 85 | avoidLocalhost: false |
| 86 | # Disable thumbnail generation | 86 | |
| 87 | disableThumbnails: false | 87 | # THUMBNAILING CONFIGURATION |
| 88 | # Thumbnail quality (0-100) | 88 | thumbnails: |
| 89 | thumbnailsQuality: 95 | 89 | # 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: |
| 92 | avatarThumbnailsPng: false | 92 | # * "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 API | 101 | # Allow secret keys exposure via API |
| 94 | allowKeysExposure: false | 102 | allowKeysExposure: false |
| 95 | # Skip new default content checks | 103 | # Skip new default content checks |
| @@ -28,6 +28,24 @@ const color = { | |||
| 28 | white: (mess) => color.byNum(mess, 37), | 28 | white: (mess) => color.byNum(mess, 37), |
| 29 | }; | 29 | }; |
| 30 | 30 | ||
| 31 | const 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 from | 51 | * @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')); |
| 85 | 103 | ||
| 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 config | 122 | // Get all keys from the original config |
| 87 | const originalKeys = getAllKeys(config); | 123 | const originalKeys = getAllKeys(config); |
| 88 | 124 | ||
| @@ -95,11 +131,18 @@ function addMissingConfigValues() { | |||
| 95 | // Find the keys that were added | 131 | // Find the keys that were added |
| 96 | const addedKeys = _.difference(updatedKeys, originalKeys); | 132 | const addedKeys = _.difference(updatedKeys, originalKeys); |
| 97 | 133 | ||
| 98 | if (addedKeys.length === 0) { | 134 | if (addedKeys.length === 0 && migratedKeys.length === 0) { |
| 99 | return; | 135 | return; |
| 100 | } | 136 | } |
| 101 | 137 | ||
| 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); |
| @@ -12,9 +12,12 @@ import { getAllUserHandles, getUserDirectories } from '../users.js'; | |||
| 12 | import { getConfigValue } from '../util.js'; | 12 | import { getConfigValue } from '../util.js'; |
| 13 | import { jsonParser } from '../express-common.js'; | 13 | import { jsonParser } from '../express-common.js'; |
| 14 | 14 | ||
| 15 | const thumbnailsDisabled = getConfigValue('disableThumbnails', false); | 15 | const thumbnailsEnabled = !!getConfigValue('thumbnails.enabled', true); |
| 16 | const quality = getConfigValue('thumbnailsQuality', 95); | 16 | const quality = Math.min(100, Math.max(1, parseInt(getConfigValue('thumbnails.quality', 95)))); |
| 17 | const pngFormat = getConfigValue('avatarThumbnailsPng', false); | 17 | const pngFormat = String(getConfigValue('thumbnails.format', 'jpg')).toLowerCase().trim() === 'png'; |
| 18 | |||
| 19 | /** @type {Record<string, number[]>} */ | ||
| 20 | const dimensions = getConfigValue('thumbnails.dimensions', { 'bg': [160, 90], 'avatar': [96, 144] }); | ||
| 18 | 21 | ||
| 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 | } |
| 116 | 119 | ||
| 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; |
| 122 | 122 | ||
| 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 | } |
| 195 | 198 | ||
| 196 | if (thumbnailsDisabled) { | 199 | if (!thumbnailsEnabled) { |
| 197 | const folder = getOriginalFolder(request.user.directories, type); | 200 | const folder = getOriginalFolder(request.user.directories, type); |
| 198 | 201 | ||
| 199 | if (folder === undefined) { | 202 | if (folder === undefined) { |