From a7d6287d65c62a43b38f34330294559a9c9fc4a7 Mon Sep 17 00:00:00 2001 From: treamz Date: Sun, 22 Mar 2026 01:15:03 +0300 Subject: [PATCH] Image converter: add AVIF/TIFF/GIF/BMP/HEIC, quality slider, custom resize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Renamed: Сжать картинки → Конвертер изображений - New output formats: AVIF, TIFF, GIF (via sharp) - New input formats: AVIF, TIFF, GIF, BMP, SVG, HEIC - Quality slider (10-100%) in UI - Custom resize option (prompt for px) - 3-column control layout (format, size, quality) - Updated descriptions in dashboard, DB, page title --- public/compress.html | 18 +++++++++++++----- public/dashboard.html | 2 +- routes/compress.js | 38 ++++++++++++++++++++------------------ 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/public/compress.html b/public/compress.html index cc939ff..2ac3c26 100644 --- a/public/compress.html +++ b/public/compress.html @@ -3,7 +3,7 @@ - Сжатие картинок — WA Dev Tools + Конвертер изображений — WA Dev Tools @@ -37,7 +37,7 @@
@@ -73,11 +73,14 @@
-
+
+
80%
@@ -213,7 +218,7 @@ function processFiles(files) { const valid = Array.from(files).filter(f => validTypes.includes(f.type) && f.size <= 20 * 1024 * 1024); if (!valid.length) { - showError('Нет подходящих файлов. Допустимы JPEG, PNG, WebP до 20 МБ.'); + showError('Нет подходящих файлов. Допустимы JPEG, PNG, WebP, AVIF, TIFF, GIF, BMP, HEIC до 20 МБ.'); return; } @@ -263,8 +268,11 @@ compressBtn.addEventListener('click', () => { const formData = new FormData(); selectedFiles.forEach(f => formData.append('images', f)); - formData.append('resize', $('resize').value); formData.append('format', $('format').value); + formData.append('quality', $('quality').value); + var rv = $('resize').value; + if (rv === 'custom') rv = prompt('Размер по длинной стороне (px):', '1024') || '0'; + formData.append('resize', rv); const xhr = new XMLHttpRequest(); xhr.open('POST', '/compress'); diff --git a/public/dashboard.html b/public/dashboard.html index 4b17f88..0c8311c 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -111,7 +111,7 @@
-
Сжатие
JPEG, PNG, WebP до 20МБ
+
Сжатие
Сжатие, ресайз, конвертация
diff --git a/routes/compress.js b/routes/compress.js index 1d1b339..f1c9d52 100644 --- a/routes/compress.js +++ b/routes/compress.js @@ -43,9 +43,9 @@ const upload = multer({ dest: UPLOADS_DIR, limits: { fileSize: maxFileSize }, fileFilter: (req, file, cb) => { - const validTypes = ['image/jpeg', 'image/png', 'image/webp']; - if (validTypes.includes(file.mimetype)) cb(null, true); - else cb(new Error('Unsupported file type')); + const validTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/tiff', 'image/gif', 'image/bmp', 'image/svg+xml']; + if (validTypes.includes(file.mimetype) || file.originalname.match(/\.(jpe?g|png|webp|avif|tiff?|gif|bmp|svg|heic|heif)$/i)) cb(null, true); + else cb(new Error('Неподдерживаемый формат изображения')); }, }); @@ -86,10 +86,8 @@ function transliterate(str) { } function getOutputExtension(format, originalName) { - if (format === 'webp') return '.webp'; - if (format === 'jpeg') return '.jpg'; - if (format === 'png') return '.png'; - return path.extname(originalName); + const extMap = { webp: '.webp', jpeg: '.jpg', png: '.png', avif: '.avif', tiff: '.tiff', gif: '.gif' }; + return extMap[format] || path.extname(originalName); } function changeExtension(filename, newExt) { @@ -112,11 +110,11 @@ router.get('/', (req, res) => { router.post('/', limiter, upload.array('images', maxFiles), async (req, res) => { const resize = parseInt(req.body.resize || '0', 10); const format = (req.body.format || 'original').toLowerCase(); - const quality = Math.min(100, Math.max(1, parseInt(req.body.quality || process.env.COMPRESS_QUALITY || '60', 10))); - const validFormats = ['original', 'webp', 'jpeg', 'png']; + const quality = Math.min(100, Math.max(1, parseInt(req.body.quality || process.env.COMPRESS_QUALITY || '80', 10))); + const validFormats = ['original', 'webp', 'jpeg', 'png', 'avif', 'tiff', 'gif']; if (!validFormats.includes(format)) { - return res.status(400).json({ error: 'Неверный формат. Допустимые: original, webp, jpeg, png' }); + return res.status(400).json({ error: 'Неверный формат' }); } if (!req.files || !req.files.length) { return res.status(400).json({ error: 'Файлы не загружены' }); @@ -154,17 +152,21 @@ router.post('/', limiter, upload.array('images', maxFiles), async (req, res) => } let buffer; - if (format === 'webp') { - buffer = await image.webp({ quality }).toBuffer(); - } else if (format === 'jpeg') { - buffer = await image.jpeg({ quality }).toBuffer(); - } else if (format === 'png') { - buffer = await image.png({ compressionLevel: 9 }).toBuffer(); - } else { + if (format === 'webp') buffer = await image.webp({ quality }).toBuffer(); + else if (format === 'jpeg') buffer = await image.jpeg({ quality }).toBuffer(); + else if (format === 'png') buffer = await image.png({ compressionLevel: 9 }).toBuffer(); + else if (format === 'avif') buffer = await image.avif({ quality }).toBuffer(); + else if (format === 'tiff') buffer = await image.tiff({ quality }).toBuffer(); + else if (format === 'gif') buffer = await image.gif().toBuffer(); + else { + // original — keep source format with compression if (file.mimetype === 'image/jpeg') buffer = await image.jpeg({ quality }).toBuffer(); else if (file.mimetype === 'image/png') buffer = await image.png({ compressionLevel: 9 }).toBuffer(); else if (file.mimetype === 'image/webp') buffer = await image.webp({ quality }).toBuffer(); - else continue; + else if (file.mimetype === 'image/avif') buffer = await image.avif({ quality }).toBuffer(); + else if (file.mimetype === 'image/tiff') buffer = await image.tiff({ quality }).toBuffer(); + else if (file.mimetype === 'image/gif') buffer = await image.gif().toBuffer(); + else buffer = await image.jpeg({ quality }).toBuffer(); // fallback } const readableName = fixMulterFilename(file.originalname);