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 | 83 | # Avoids using 'localhost' for autorun in auto mode. |
| 84 | 84 | # use if you don't have 'localhost' in your hosts file |
| 85 | 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 | 101 | # Allow secret keys exposure via API |
| 94 | 102 | allowKeysExposure: false |
| 95 | 103 | # Skip new default content checks |
| @@ -28,6 +28,24 @@ const color = { | ||
| 28 | 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 | 50 | * Gets all keys from an object recursively. |
| 33 | 51 | * @param {object} obj Object to get all keys from |
| @@ -83,6 +101,24 @@ function addMissingConfigValues() { | ||
| 83 | 101 | const defaultConfig = yaml.parse(fs.readFileSync(path.join(process.cwd(), './default/config.yaml'), 'utf8')); |
| 84 | 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 | 122 | // Get all keys from the original config |
| 87 | 123 | const originalKeys = getAllKeys(config); |
| 88 | 124 | |
| @@ -95,11 +131,18 @@ function addMissingConfigValues() { | ||
| 95 | 131 | // Find the keys that were added |
| 96 | 132 | const addedKeys = _.difference(updatedKeys, originalKeys); |
| 97 | 133 | |
| 98 | 134 | if (addedKeys.length === 0 && migratedKeys.length === 0) { |
| 99 | 135 | return; |
| 100 | 136 | } |
| 101 | 137 | |
| 102 | - console.log('Adding missing config values to config.yaml:', addedKeys); | |
| 138 | + if (addedKeys.length > 0) { | |
| 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 | 146 | fs.writeFileSync('./config.yaml', yaml.stringify(config)); |
| 104 | 147 | } catch (error) { |
| 105 | 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 | 12 | import { getConfigValue } from '../util.js'; |
| 13 | 13 | import { jsonParser } from '../express-common.js'; |
| 14 | 14 | |
| 15 | 15 | const thumbnailsDisabledthumbnailsEnabled = !!getConfigValue('disableThumbnailsthumbnails.enabled', falsetrue); |
| 16 | 16 | const quality = Math.min(100, Math.max(1, parseInt(getConfigValue('thumbnailsQualitythumbnails.quality', 95)))); |
| 17 | 17 | const pngFormat = String(getConfigValue('avatarThumbnailsPngthumbnails.format', false'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 | 23 | * Gets a path to thumbnail folder based on the type. |
| @@ -114,16 +117,16 @@ async function generateThumbnail(directories, type, file) { | ||
| 114 | 117 | return null; |
| 115 | 118 | } |
| 116 | 119 | |
| 117 | - const imageSizes = { 'bg': [160, 90], 'avatar': [96, 144] }; | |
| 118 | - const mySize = imageSizes[type]; | |
| 119 | - | |
| 120 | 120 | try { |
| 121 | 121 | let buffer; |
| 122 | 122 | |
| 123 | 123 | try { |
| 124 | + const size = dimensions[type]; | |
| 124 | 125 | const image = await jimp.read(pathToOriginalFile); |
| 125 | 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 | 131 | catch (inner) { |
| 129 | 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 | 196 | return response.sendStatus(403); |
| 194 | 197 | } |
| 195 | 198 | |
| 196 | 199 | if (thumbnailsDisabled!thumbnailsEnabled) { |
| 197 | 200 | const folder = getOriginalFolder(request.user.directories, type); |
| 198 | 201 | |
| 199 | 202 | if (folder === undefined) { |