From 472a8fc13c58e1718ac359dc30ef0a504c6427ba Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?=
Date: Wed, 4 Feb 2026 12:54:58 -0500
Subject: [PATCH] feat: allow converting pasted images, closes #10352
---
install/data/defaults.json | 1 +
.../en-GB/admin/settings/uploads.json | 5 ++
public/openapi/read/config.yaml | 2 +
public/src/modules/uploadHelpers.js | 77 +++++++++++++++----
src/controllers/api.js | 1 +
src/views/admin/settings/uploads.tpl | 12 ++-
6 files changed, 82 insertions(+), 16 deletions(-)
diff --git a/install/data/defaults.json b/install/data/defaults.json
index 2f733961ea..5b1f91eeb7 100644
--- a/install/data/defaults.json
+++ b/install/data/defaults.json
@@ -57,6 +57,7 @@
"rejectImageWidth": 5000,
"rejectImageHeight": 5000,
"resizeImageQuality": 80,
+ "convertPastedImageTo": "image/jpeg",
"topicThumbSize": 512,
"minimumTitleLength": 3,
"maximumTitleLength": 255,
diff --git a/public/language/en-GB/admin/settings/uploads.json b/public/language/en-GB/admin/settings/uploads.json
index e91a7bee36..b08d56a5f8 100644
--- a/public/language/en-GB/admin/settings/uploads.json
+++ b/public/language/en-GB/admin/settings/uploads.json
@@ -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",
diff --git a/public/openapi/read/config.yaml b/public/openapi/read/config.yaml
index d6e012bdae..16d2043667 100644
--- a/public/openapi/read/config.yaml
+++ b/public/openapi/read/config.yaml
@@ -87,6 +87,8 @@ get:
type: number
maximumFileSize:
type: number
+ convertPastedImageTo:
+ type: string
theme:id:
type: string
theme:src:
diff --git a/public/src/modules/uploadHelpers.js b/public/src/modules/uploadHelpers.js
index a465a44054..3e56550315 100644
--- a/public/src/modules/uploadHelpers.js
+++ b/public/src/modules/uploadHelpers.js
@@ -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;
});
diff --git a/src/controllers/api.js b/src/controllers/api.js
index f55b411bfa..4fd83dcd5b 100644
--- a/src/controllers/api.js
+++ b/src/controllers/api.js
@@ -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',
diff --git a/src/views/admin/settings/uploads.tpl b/src/views/admin/settings/uploads.tpl
index 400058eb20..d2bb25076c 100644
--- a/src/views/admin/settings/uploads.tpl
+++ b/src/views/admin/settings/uploads.tpl
@@ -83,12 +83,22 @@
+
+
+
+
+
-