feat: allow converting pasted images, closes #10352

This commit is contained in:
Barış Soner Uşaklı
2026-02-04 12:54:58 -05:00
parent b3dc7f4303
commit 472a8fc13c
6 changed files with 82 additions and 16 deletions

View File

@@ -57,6 +57,7 @@
"rejectImageWidth": 5000,
"rejectImageHeight": 5000,
"resizeImageQuality": 80,
"convertPastedImageTo": "image/jpeg",
"topicThumbSize": 512,
"minimumTitleLength": 3,
"maximumTitleLength": 255,

View File

@@ -21,6 +21,11 @@
"reject-image-width-help": "Images wider than this value will be rejected.",
"reject-image-height": "Maximum Image Height (in pixels)",
"reject-image-height-help": "Images taller than this value will be rejected.",
"convert-pasted-images-to": "Convert pasted images to:",
"convert-pasted-images-to-default": "No Conversion (Keep Original Format)",
"convert-pasted-images-to-png": "PNG",
"convert-pasted-images-to-jpeg": "JPEG",
"convert-pasted-images-to-webp": "WebP",
"allow-topic-thumbnails": "Allow users to upload topic thumbnails",
"show-post-uploads-as-thumbnails": "Show post uploads as thumbnails",
"topic-thumb-size": "Topic Thumb Size",

View File

@@ -87,6 +87,8 @@ get:
type: number
maximumFileSize:
type: number
convertPastedImageTo:
type: string
theme:id:
type: string
theme:src:

View File

@@ -123,25 +123,43 @@ define('uploadHelpers', ['alerts'], function (alerts) {
uploadHelpers.handlePaste = function (options) {
const container = options.container;
container.on('paste', function (event) {
container.on('paste', async function (event) {
const items = (event.clipboardData || event.originalEvent.clipboardData || {}).items;
const files = [];
const fileNames = [];
let formData = null;
if (window.FormData) {
formData = new FormData();
}
[].forEach.call(items, function (item) {
const file = item.getAsFile();
if (file) {
const fileName = utils.generateUUID() + '-' + file.name;
if (formData) {
formData.append('files[]', file, fileName);
}
files.push(file);
fileNames.push(fileName);
const formData = window.FormData ? new FormData() : null;
function addFile(file, fileName) {
files.push(file);
fileNames.push(fileName);
if (formData) {
formData.append('files[]', file, fileName);
}
});
}
const { convertPastedImageTo } = config;
for (const item of items) {
const file = item.getAsFile();
if (!file) continue;
try {
if (convertPastedImageTo && file.type.match(/image./) && file.type !== convertPastedImageTo) {
// eslint-disable-next-line no-await-in-loop
const convertedBlob = await convertImage(file, convertPastedImageTo, 0.9);
const ext = convertedBlob.type.split('/')[1];
const fileName = `${utils.generateUUID()}-image.${ext}`;
const convertedFile = new File([convertedBlob], fileName, {
type: convertedBlob.type,
});
addFile(convertedFile, fileName);
} else {
const fileName = utils.generateUUID() + '-' + file.name;
addFile(file, fileName);
}
} catch (err) {
alerts.error(err);
console.error(err);
}
}
if (files.length) {
options.callback({
@@ -217,5 +235,34 @@ define('uploadHelpers', ['alerts'], function (alerts) {
options.uploadForm.submit();
};
function convertImage(file, mime, quality = 0.9) {
return new Promise((resolve, reject) => {
const img = new Image();
const reader = new FileReader();
reader.onload = e => {
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob(blob => {
if (!blob) return reject(new Error('Conversion failed'));
resolve(blob);
}, mime, quality);
};
img.onerror = reject;
img.src = e.target.result;
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
return uploadHelpers;
});

View File

@@ -64,6 +64,7 @@ apiController.loadConfig = async function (req) {
topicsPerPage: meta.config.topicsPerPage || 20,
postsPerPage: meta.config.postsPerPage || 20,
maximumFileSize: meta.config.maximumFileSize,
convertPastedImageTo: meta.config.convertPastedImageTo,
'theme:id': meta.config['theme:id'],
'theme:src': meta.config['theme:src'],
defaultLang: meta.config.defaultLang || 'en-GB',

View File

@@ -83,12 +83,22 @@
</p>
</div>
<div class="mb-3">
<label class="form-label" for="convertPastedImageTo">[[admin/settings/uploads:convert-pasted-images-to]]</label>
<select id="convertPastedImageTo" class="form-select" data-field="convertPastedImageTo">
<option value="">[[admin/settings/uploads:convert-pasted-images-to-default]]</option>
<option value="image/png">[[admin/settings/uploads:convert-pasted-images-to-png]]</option>
<option value="image/jpeg">[[admin/settings/uploads:convert-pasted-images-to-jpeg]]</option>
<option value="image/webp">[[admin/settings/uploads:convert-pasted-images-to-webp]]</option>
</select>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="allowTopicsThumbnail" data-field="allowTopicsThumbnail">
<label for="allowTopicsThumbnail" class="form-check-label">[[admin/settings/uploads:allow-topic-thumbnails]]</label>
</div>
<div class="form-check form-switch mb-3">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="showPostUploadsAsThumbnails" data-field="showPostUploadsAsThumbnails">
<label for="showPostUploadsAsThumbnails" class="form-check-label">[[admin/settings/uploads:show-post-uploads-as-thumbnails]]</label>
</div>