special

Скрипт для обробки зображень у формат WebP

images-to-webp-script.png

Цей скрипт створений для автоматизації процесу обробки зображень на вашому веб-сайті - він конвертує зображення у сучасний формат WebP, що забезпечує зменшення розміру файлів при збереженні високої якості.

Використання WebP допомагає зменшити час завантаження сторінок, що позитивно впливає на користувацький досвід та SEO вашого сайту.

У цьому посібнику ви дізнаєтеся, як правильно налаштувати та використовувати наш скрипт для досягнення найкращих результатів з переводу сайта на webp.

Що таке WebP і для чого?

images-to-webp-script3.jpg

WebP — це формат файлу, розроблений компанією Google у 2010 році. Його основною характеристикою є вдосконалений алгоритм стиснення, який дозволяє зменшити розмір зображення без помітних втрат якості.

Хоча інші формати також підтримують стиснення, технології, на яких базується WebP, є більш прогресивними. У порівнянні з конкурентами, WebP демонструє вищу ефективність у відношенні стиснення до якості зображення.

У середньому, розмір зображень зменшується на 25–35%, що дає вебмайстрам можливість завантажувати більше зображень на сайти, економлячи простір на жорсткому диску.

При розробці формату Google використовував ті ж методи стиснення, що й у кодеках VP8.>

Переваги WebP в порівнянні з іншими форматами

Головна перевага WebP — це зменшений розмір файлів. Це позитивно впливає на кілька аспектів роботи в інтернеті:

  1. Сайти з стиснутими WebP-зображеннями працюють швидше. Менше часу витрачається на обробку невеликих файлів, навіть якщо в статті міститься безліч зображень.
  2. Завантажуючи невеликі зображення на VDS, можна зекономити місце на жорсткому диску.
  3. Користувачі витрачатимуть менше мобільного трафіку, відвідуючи сайт зі смартфона.
  4. Завантаження каналу до сервера буде меншим, якщо передавати менші медіа-файли, що також підвищує продуктивність.

Таким чином, переваги WebP стають очевидними в порівнянні з іншими форматами.

Опис функціоналу та Основні функції

Даний скрипт призначений для автоматичної обробки зображень на веб-сайті. Він конвертує зображення у формат WebP, що забезпечує менший розмір файлів без значної втрати якості.

Це прискорює завантаження сторінок та покращує продуктивність сайту.

Скрипт підтримує кешування, щоб не створювати одні й ті ж зображення повторно, а також виводить лог-повідомлення в консоль для зручності налагодження.

Функції:

  • Конвертація зображень у формат WebP.
  • Кешування вже створених зображень для запобігання повторної обробки.
  • Обробка зображень в атрибутах srcset.
  • Паралельна обробка кількох зображень для підвищення продуктивності.
  • Валідація MIME-типу та розміру завантажуваних зображень.

Принцип роботи

  1. Завантаження сторінки: Скрипт запускається під час завантаження сторінки та шукає всі зображення () на сторінці.
  2. Перевірка кешу: Для кожного зображення скрипт перевіряє наявність кешованого файлу WebP. Якщо файл існує і термін його дії не закінчився, зображення замінюється на кешоване.
  3. Створення WebP: Якщо кешованого файлу немає або термін дії закінчився, оригінальне зображення завантажується, конвертується у WebP, а потім завантажується на сервер.
  4. Оновлення srcset: Якщо у зображення є атрибут srcset, скрипт обробляє його аналогічно основному зображенню, створюючи та кешуючи нові версії у форматі WebP.
  5. Логування: Усі дії скрипта логуються в консолі браузера для зручності налагодження.

Опис параметрів, які можна змінити

CACHE_DURATION

  • Тип: Число (в мілісекундах)
  • Опис: Час дії кешу для зображень (за замовчуванням 30 днів).
  • Приклад: const CACHE_DURATION = 30 * 24 * 60 * 60 * 1000;

DEBUG

  • Тип: Логічне значення
  • Опис: Увімкнення режиму налагодження, який відключає кешування.
  • Приклад: const DEBUG = false;

LOG_TO_CONSOLE

  • Тип: Логічне значення
  • Опис: Увімкнення виводу логів у консоль браузера.
  • Приклад: const LOG_TO_CONSOLE = true;

MAX_IMAGE_SIZE

  • Тип: Число (в байтах)
  • Опис: Максимально допустимий розмір зображення для обробки (за замовчуванням 5 MB).
  • Приклад: const MAX_IMAGE_SIZE = 5 * 1024 * 1024;

VALID_MIME_TYPES

  • Тип: Масив рядків
  • Опис: Дозволені MIME-типи для завантажуваних зображень.
  • Приклад: const VALID_MIME_TYPES = ['image/jpeg', 'image/png'];

COMPRESSION_QUALITY

  • Тип: Число (від 0.0 до 1.0)
  • Опис: Якість стиснення для зображень WebP (за замовчуванням 0.8).
  • Приклад: const COMPRESSION_QUALITY = 0.8;

MAX_CONCURRENT_REQUESTS

  • Тип: Число
  • Опис: Максимальна кількість паралельних запитів на обробку зображень.
  • Приклад: const MAX_CONCURRENT_REQUESTS = 5;

Встановлення та використання

images-to-webp-script2.jpg

Мінімальні вимоги для роботи

  • Веб-сервер з підтримкою PHP (наприклад, Apache або Nginx).
  • PHP версії 5.0 або вище.
  • Підтримка формату WebP на сервері.
  • JavaScript, що підтримує Promises (більшість сучасних браузерів).

Структура скрипта

images-to-webp-script4.jpg

  • Основний скрипт (script.js): Включає в себе всю логіку обробки зображень і взаємодії з сервером.
  • Файл для завантаження зображень (upload.php): PHP-скрипт на сервері, який приймає завантажувані зображення та зберігає їх у вказаній директорії.

Встановлення

  1. Завантажте script.js та upload.php на ваш сервер, у директорію, де знаходяться ваші зображення.
  2. Переконайтеся, що у вас є файл .htaccess у директорії завантаження для обмеження доступу.
  3. Створіть теку "uploads" з правами 777 для загрузки створених зображень та вкажіть її шлях у файлах.
  4. Додайте посилання на script.js у ваш HTML-код перед закриваючим тегом :

<script src="path/to/script.js"></script>

Тепер скрипт готовий до використання, і ваші зображення будуть автоматично конвертуватися у формат WebP під час завантаження сторінки.

Завантажити скрипт

Пароль на архів: shram.kiev.ua

Якщо у вас з'являється повідомлення Virus or Unsafe Browsing detected! будь ласка, зверніть увагу, що це не вірус, а хибне спрацювання на js код. Можно перепровірити будь-яким антівірусом.

Код script.js

/**
 * Улучшенный скрипт для динамической конвертации изображений в WebP.
 * Включает поддержку ленивой загрузки, кеширование в IndexedDB (с fallback на LocalStorage), обработку srcset и адаптивное качество.
 */

const DEBUG_MODE = false; // Режим отладки: отключение кеширования при true
const LOG_LEVEL = 'info'; // Уровень логирования: 'info', 'warn', 'error'
const CACHE_DURATION_MS = 24 * 60 * 60 * 1000; // Длительность кеширования (1 день)
const MAX_FILE_SIZE_MB = 5 * 1024 * 1024; // Максимальный размер файла (5 MB)
const BASE_WEBP_QUALITY = 0.8; // Базовое качество сжатия WebP (0.0 - 1.0)
const MAX_PARALLEL_REQUESTS = 5; // Максимум параллельных запросов
//const VALID_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml']; // Разрешенные типы изображений
const VALID_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif']; // Разрешенные типы изображений

const UPLOAD_ENDPOINT = 'https://www.shram.kiev.ua/mycode/webp/upload.php';
const IMAGE_CACHE_PATH = '/images/uploads/';

let useIndexedDB = false; // Флаг для определения, использовать ли IndexedDB
let dbPromise; // Объявляем dbPromise в глобальной области

// Проверка на поддержку IndexedDB с fallback на LocalStorage в случае ошибки
try {
 if (window.indexedDB) {
 useIndexedDB = true;
 dbPromise = new Promise((resolve, reject) => {
 const request = indexedDB.open('imageCacheDB', 1);
 request.onupgradeneeded = event => {
 let db = event.target.result;
 db.createObjectStore('images', { keyPath: 'url' });
 };
 request.onsuccess = () => resolve(request.result);
 request.onerror = () => reject(request.error);
 });
 console.log("IndexedDB доступен и будет использоваться для кэширования.");
 } else {
 throw new Error("IndexedDB не поддерживается.");
 }
} catch (error) {
 console.warn("Ошибка при инициализации IndexedDB:", error.message);
 console.warn("Использование LocalStorage в качестве альтернативного кэширования.");
 useIndexedDB = false;
}

document.addEventListener('DOMContentLoaded', () => {
 const images = Array.from(document.querySelectorAll('img:not([data-no-webp])'));
 const observer = new IntersectionObserver(handleIntersection, { rootMargin: '0px', threshold: 0.1 });
 images.forEach(img => observer.observe(img));
});

function handleIntersection(entries, observer) {
 entries.forEach(entry => {
 if (entry.isIntersecting) {
 observer.unobserve(entry.target);
 handleImageConversion(entry.target);
 }
 });
}

async function handleImageConversion(img) {
 const src = img.getAttribute('src');
 if (!src) return;

 await processSrcset(img); // Обрабатываем srcset атрибут, если он есть

 const cachedUrl = await checkCache(src);
 if (cachedUrl && !DEBUG_MODE) {
 img.src = cachedUrl;
 log('info', `Кешированное изображение используется: ${cachedUrl}`);
 return;
 }

 try {
 const response = await fetch(src);
 const blob = await response.blob();
 validateImage(blob);

 const adaptiveQuality = calculateAdaptiveQuality(blob.size); // Использование адаптивного качества
 const webpBlob = await convertToWebP(blob, adaptiveQuality);
 const uploadUrl = await uploadWebP(webpBlob, src);

 if (uploadUrl) {
 img.src = uploadUrl;
 saveToCache(src, uploadUrl);
 }
 } catch (error) {
 log('error', `Ошибка при обработке изображения: ${error.message}`);
 }
}


/**
 * Функция для расчета адаптивного качества сжатия.
 * Чем больше изображение, тем выше качество сжатия.
 * @param {number} size - Размер изображения в байтах.
 * @returns {number} - Оптимальное качество сжатия.
 */
function calculateAdaptiveQuality(size) {
 return size > 2 * 1024 * 1024 ? 0.9 : BASE_WEBP_QUALITY;
}



/**
 * Функция для обработки srcset атрибута.
 */
async function processSrcset(img) {
 const srcset = img.getAttribute('srcset');
 if (!srcset) return;

 const sources = srcset.split(',').map(src => src.trim());
 const newSources = await Promise.all(sources.map(async source => {
 const [url, size] = source.split(' ');
 const cachedUrl = await checkCache(url);
 if (cachedUrl && !DEBUG_MODE) {
 return `${cachedUrl} ${size}`;
 }

 try {
 const response = await fetch(url);
 const blob = await response.blob();
 validateImage(blob);

 const webpBlob = await convertToWebP(blob, BASE_WEBP_QUALITY);
 const uploadUrl = await uploadWebP(webpBlob, url);
 saveToCache(url, uploadUrl);

 return `${uploadUrl} ${size}`;
 } catch (error) {
 log('error', `Ошибка при обработке srcset: ${error.message}`);
 return `${url} ${size}`; // Возвращаем оригинальное изображение в случае ошибки
 }
 }));

 img.setAttribute('srcset', newSources.join(', ')); // Обновляем srcset атрибут
}

/**
 * Функция для загрузки WebP изображения на сервер.
 */
async function uploadWebP(blob, originalUrl) {
 const filename = getFileNameFromUrl(originalUrl) + '.webp';
 const formData = new FormData();
 formData.append('file', blob, filename);

 try {
 console.log('Попытка отправки данных через FormData:', filename);

 const response = await fetch(UPLOAD_ENDPOINT, {
 method: 'POST',
 body: formData,
 headers: {
 'X-Filename': filename
 }
 });

 if (!response.ok) {
 throw new Error(`Ошибка загрузки через FormData: ${response.statusText}`);
 }

 const result = await response.json();
 return result.url; 
 } catch (error) {
 console.warn(`Ошибка при загрузке через FormData: ${error.message}`);
 console.log('Попытка отправки данных напрямую через Blob.');

 // Попытка отправить данные напрямую через Blob
 try {
 const response = await fetch(UPLOAD_ENDPOINT, {
 method: 'POST',
 body: blob,
 headers: {
 'X-Filename': filename,
 'Content-Type': 'application/octet-stream'
 }
 });

 if (!response.ok) {
 const errorText = await response.text();
 throw new Error(`Ошибка загрузки через Blob: ${errorText}`);
 }

 const result = await response.json();
 return result.url;
 } catch (blobError) {
 console.error(`Ошибка при загрузке через Blob: ${blobError.message}`);
 throw blobError;
 }
 }
}

/**
 * Функция для определения типа устройства (мобильное, планшет, десктоп).
 * @returns {string} - Тип устройства: 'mobile', 'tablet', 'desktop'.
 */
function detectDeviceType() {
 const width = window.innerWidth;
 if (width <= 480) {
 return 'mobile';
 } else if (width <= 1024) {
 return 'tablet';
 } else {
 return 'desktop';
 }
}

/**
 * Функция для создания WebP изображения из Blob с адаптивным ресайзингом для мобильных и планшетных устройств.
 * @param {Blob} blob - Исходное изображение в формате Blob.
 * @param {number} quality - Качество сжатия (0.0 - 1.0).
 * @returns {Promise<Blob>} - Promise, возвращающий созданное WebP изображение в формате Blob.
 */
async function convertToWebP(blob, quality) {
 return new Promise((resolve, reject) => {
 const canvas = document.createElement('canvas');
 const ctx = canvas.getContext('2d');
 const imgElement = new Image();
 const url = URL.createObjectURL(blob);
 const deviceType = detectDeviceType(); // Определяем тип устройства

 imgElement.onload = () => {
 let targetWidth = imgElement.width;
 let targetHeight = imgElement.height;

 // Уменьшаем ширину изображения для мобильных устройств и планшетов
 if (deviceType === 'mobile' && imgElement.width > 480) {
 targetWidth = 480;
 targetHeight = (imgElement.height * 480) / imgElement.width;
 } else if (deviceType === 'tablet' && imgElement.width > 720) {
 targetWidth = 720;
 targetHeight = (imgElement.height * 720) / imgElement.width;
 }

 canvas.width = targetWidth;
 canvas.height = targetHeight;
 ctx.drawImage(imgElement, 0, 0, targetWidth, targetHeight); // Рисуем изображение с новой шириной

 canvas.toBlob((webpBlob) => {
 URL.revokeObjectURL(url);
 if (webpBlob) {
 resolve(webpBlob);
 } else {
 reject(new Error('Не удалось создать WebP blob'));
 }
 }, 'image/webp', quality);
 };

 imgElement.onerror = () => reject(new Error('Не удалось загрузить изображение'));
 imgElement.src = url;
 });
}

/**
 * Функция для проверки и валидации изображения перед конвертацией.
 */
function validateImage(blob) {
 if (blob.size > MAX_FILE_SIZE_MB) throw new Error('Изображение слишком большое');
 if (!VALID_IMAGE_TYPES.includes(blob.type)) throw new Error('Неподдерживаемый тип изображения');
}

/**
 * Функция для сохранения данных в кэш.
 */
async function saveToCache(url, cachedUrl) {
 try {
 if (useIndexedDB) {
 const db = await dbPromise;
 const tx = db.transaction('images', 'readwrite');
 const store = tx.objectStore('images');
 store.put({ url, cachedUrl });
 await tx.complete;
 } else {
 localStorage.setItem(url, cachedUrl);
 }
 } catch (error) {
 console.warn("Ошибка сохранения в кэш:", error.message);
 localStorage.setItem(url, cachedUrl);
 }
}

/**
 * Функция для проверки кэша.
 */
async function checkCache(url) {
 try {
 if (useIndexedDB) {
 const db = await dbPromise;
 const tx = db.transaction('images', 'readonly');
 const store = tx.objectStore('images');
 const cachedImage = await store.get(url);
 return cachedImage ? cachedImage.cachedUrl : null;
 } else {
 return localStorage.getItem(url);
 }
 } catch (error) {
 console.warn("Ошибка при проверке кэша:", error.message);
 return localStorage.getItem(url);
 }
}

/**
 * Функция для получения имени файла из URL.
 */
function getFileNameFromUrl(url) {
 return url.split('/').pop().split('.')[0];
}

/**
 * Функция логирования.
 */
function log(level, message) {
 if (LOG_LEVEL === 'info' && level === 'info') console.log(message);
 if ((LOG_LEVEL === 'warn' || LOG_LEVEL === 'info') && level === 'warn') console.warn(message);
 if (level === 'error') console.error(message);
}

Код upload.php

<?php
// Конфигурационные переменные
$uploadDir = '/var/www/admin/data/www/shram.kiev.ua/images/uploads/';
$uploadsUrlPath = '/images/uploads'; 

// Заголовки для CORS и методов доступа
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: POST, GET, OPTIONS");
header("Access-Control-Allow-Headers: X-Filename, Content-Type");

// Добавляем заголовки для контроля кэширования
header("Cache-Control: no-cache, must-revalidate");
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT");

/**
 * Основная логика обработки POST-запроса на загрузку файла.
 * Принимает и обрабатывает загруженный файл, сохраняя его в заданную директорию.
 */
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
 error_log("POST-запрос принят, начинаем обработку...");

 $blobData = null;
 $filename = uniqid() . '.webp'; 

 /**
 * Проверка и извлечение файла из запроса.
 * Если файл передан через FormData, получаем его через $_FILES,
 * в противном случае получаем бинарные данные из 'php://input'.
 */
 if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
 $blobData = file_get_contents($_FILES['file']['tmp_name']);
 $headerFilename = basename($_SERVER['HTTP_X_FILENAME']);
 } else {
 $blobData = file_get_contents('php://input');
 if (!$blobData) {
 error_log("Ошибка: не удалось получить данные файла.");
 http_response_code(400);
 echo json_encode(['error' => 'Не удалось получить данные файла']);
 exit;
 }
 $headerFilename = isset($_SERVER['HTTP_X_FILENAME']) ? basename($_SERVER['HTTP_X_FILENAME']) : uniqid();
 }

 // Проверяем, заканчивается ли имя файла на '.webp', прежде чем добавить это расширение
 if (pathinfo($headerFilename, PATHINFO_EXTENSION) !== 'webp') {
 $filename = $headerFilename . '.webp';
 } else {
 $filename = $headerFilename;
 }

 $filePath = $uploadDir . $filename;

 /**
 * Проверка и создание директории для загрузки файла.
 * Если директория не существует, пытаемся создать её.
 */
 if (!is_dir($uploadDir)) {
 if (!mkdir($uploadDir, 0755, true)) {
 error_log("Ошибка: Не удалось создать директорию загрузки: $uploadDir");
 http_response_code(500);
 echo json_encode(['error' => 'Не удалось создать директорию загрузки']);
 exit;
 }
 }

 /**
 * Сохранение файла в заданную директорию.
 * Применяем права доступа и возвращаем URL загруженного файла в случае успеха.
 */
 if (file_put_contents($filePath, $blobData)) {
 chmod($filePath, 0644);
 $url = 'https://' . $_SERVER['HTTP_HOST'] . $uploadsUrlPath . '/' . $filename;
 echo json_encode(['url' => $url]);
 } else {
 error_log("Ошибка: не удалось сохранить файл на сервере.");
 http_response_code(500);
 echo json_encode(['error' => 'Failed to save file']);
 }
} else {
 error_log("Ошибка: некорректный метод запроса. Ожидался POST-запрос.");
 http_response_code(405); 
 echo json_encode(['error' => 'Метод запроса не поддерживается']);
}
?>

 Bonus: UI інтерфейс та конвектор у webp для сайту

Код index.php

Цей скрипт дозволяє зручно конвертувати зображення у формат WebP, забезпечуючи захист від зловживань і надаючи користувачу зрозумілий і функціональний інтерфейс.

Цей скрипт створює веб-інтерфейс для конвертації зображень у формат WebP. Інтерфейс дозволяє користувачам завантажувати кілька зображень, обробляти їх та завантажувати конвертовані файли у зручному форматі. Скрипт реалізує додаткові захисні заходи, такі як обмеження кількості завантажень і розміру файлів, а також генерує тимчасовий токен для обмеження доступу до функціональності. Інтерфейс має зручні повідомлення про помилки, які інформують користувача про можливі проблеми.

Інтерфейс завантаження та управління:

  • Завантаження файлів: Користувач може завантажити кілька зображень одночасно у форматах, підтримуваних браузером.
  • Кнопка "Скинути": Відображається після завантаження файлів, дозволяє очистити всі завантажені зображення і конвертовані дані.
  • Кнопка "Завантажити всі": Дозволяє масово завантажити всі конвертовані зображення у форматі WebP.

Обмеження для захисту від зловживань:

  • Максимальна кількість файлів за один раз: Скрипт обмежує кількість файлів, які можна завантажити за одне завантаження (за замовчуванням – 50). Якщо кількість файлів перевищена, користувач отримує повідомлення про помилку.
  • Максимальний розмір файлу: Скрипт обмежує розмір одного завантаженого файлу (за замовчуванням – 5 МБ). Якщо файл перевищує цей розмір, виводиться повідомлення про помилку.
  • Перевірка формату файлу: Перевіряється, чи є завантажений файл зображенням. Якщо завантажений файл не є зображенням, користувач отримує відповідне повідомлення.

Тимчасовий токен для обмеження доступу:

  • Генерація токена: При відкритті сторінки генерується унікальний токен, який зберігається у sessionStorage і діє протягом 10 хвилин. Токен використовується для обмеження доступу до функцій завантаження і завантаження файлів.
  • Перевірка токена: Перед кожною дією (завантаженням файлів або завантаженням зображень) перевіряється, чи не закінчився термін дії токена. Якщо токен недійсний, користувачу пропонується оновити сторінку.

Конвертація зображень у формат WebP:

  • Конвертація за допомогою Canvas: Кожне завантажене зображення рендериться у Canvas, після чого конвертується у формат WebP. Конвертоване зображення відображається поряд з оригінальним.
  • Індивідуальне завантаження конвертованих файлів: Для кожного конвертованого зображення є кнопка завантаження (значок стрілки), яка дозволяє завантажити окреме зображення.
  • Масове завантаження всіх конвертованих файлів: Кнопка "Завантажити всі" дозволяє одночасно завантажити всі зображення, конвертовані у формат WebP.

Вивід повідомлень про помилки:

  • Зрозумілі повідомлення: Скрипт включає систему сповіщень, яка інформує користувача про різні помилки, такі як неправильний формат файлу, перевищення ліміту розміру, помилки завантаження, проблеми при конвертації тощо.
  • Типи повідомлень: Повідомлення виводяться в спеціальному блоці та мають різне форматування залежно від типу (наприклад, зелений фон для успішного завантаження та червоний для помилок).

Зручний інтерфейс:

  • Кнопка "Вихід": Додана кнопка "Вихід" у правому верхньому куті, яка перенаправляє користувача на зовнішній сайт.
  • Адаптивне розташування елементів: Всі елементи інтерфейсу, включаючи кнопки і блоки для зображень, організовані так, щоб було зручно працювати як на великих екранах, так і на мобільних пристроях.

<!DOCTYPE html>
<html lang="uk">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Конвертація зображень у WebP</title>
 <script src="https://cdn.jsdelivr.net/npm/libwebp.js@0.5.1/dist/libwebp.min.js"></script>
 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
 <style>
 body {
 font-family: Arial, sans-serif;
 margin: 20px;
 background-color: #f4f4f4;
 position: relative;
 }
 .logout {
 position: absolute;
 top: 20px;
 right: 20px;
 color: #dc3545;
 font-size: 1.5em;
 text-decoration: none;
 }
 h1 {
 text-align: center;
 color: #333;
 }
 .container {
 max-width: 800px;
 margin: 0 auto;
 text-align: center;
 }
 input[type="file"] {
 display: block;
 margin: 20px auto;
 }
 .buttons-container {
 display: flex;
 justify-content: space-between;
 margin-top: 10px;
 }
 .btn {
 padding: 10px 20px;
 color: white;
 border: none;
 cursor: pointer;
 font-size: 16px;
 border-radius: 5px;
 display: inline-flex;
 align-items: center;
 }
 .btn-download {
 background-color: #28a745;
 }
 .btn-download:hover {
 background-color: #218838;
 }
 .btn-reset {
 background-color: #dc3545;
 }
 .btn-reset:hover {
 background-color: #c82333;
 }
 .btn i {
 margin-right: 8px;
 }
 .images-wrapper {
 display: flex;
 flex-direction: column;
 gap: 20px;
 margin-top: 20px;
 }
 .image-container {
 display: flex;
 gap: 20px;
 align-items: center;
 border: 1px solid #ddd;
 padding: 10px;
 }
 .image-wrapper {
 width: 50%;
 text-align: center;
 }
 img {
 max-width: 100%;
 height: auto;
 border: 1px solid #ccc;
 padding: 5px;
 }
 .image-label {
 font-weight: bold;
 margin-top: 5px;
 padding: 5px;
 border-radius: 5px;
 }
 .original-label {
 color: red;
 border: 2px solid red;
 }
 .webp-label {
 color: green;
 border: 2px solid green;
 display: flex;
 align-items: center;
 justify-content: center;
 }
 .download-icon {
 margin-left: 10px;
 cursor: pointer;
 color: green;
 font-size: 1.2em;
 }
 .notification {
 display: none;
 margin: 10px 0;
 padding: 10px;
 border-radius: 5px;
 }
 .success {
 background-color: #d4edda;
 color: #155724;
 }
 .error {
 background-color: #f8d7da;
 color: #721c24;
 }
 </style>
</head>
<body>

<a href="https://www.shram.kiev.ua" class="logout" title="Вихід">
 <i class="fas fa-sign-out-alt"></i>
</a>

<div class="container">
 <h1>Конвертація зображень у WebP</h1>

 <input type="file" id="fileInput" accept="image/*" multiple>
 
 <div class="buttons-container">
 <button id="resetButton" class="btn btn-reset" style="display: none;">
 <i class="fas fa-times"></i>Скинути
 </button>
 <button id="downloadAllButton" class="btn btn-download" style="display: none;">
 <i class="fas fa-download"></i>Завантажити всі
 </button>
 </div>

 <div id="notification" class="notification"></div>

 <div class="images-wrapper" id="imagesWrapper"></div>
</div>

<script>
 const MAX_FILES_PER_UPLOAD = 50; // Максимальна кількість файлів за одне завантаження
 const MAX_FILE_SIZE_MB = 5; // Максимальний розмір одного файлу в мегабайтах
 const TOKEN_LIFETIME_MS = 10 * 60 * 1000; // Термін дії токена (10 хвилин в мілісекундах)

 const fileInput = document.getElementById('fileInput');
 const resetButton = document.getElementById('resetButton');
 const downloadAllButton = document.getElementById('downloadAllButton');
 const imagesWrapper = document.getElementById('imagesWrapper');
 const notification = document.getElementById('notification');
 let convertedImages = [];

 generateToken();

 fileInput.addEventListener('change', handleFileChange);
 resetButton.addEventListener('click', resetImages);
 downloadAllButton.addEventListener('click', downloadAllImages);

 function generateToken() {
 const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
 const expirationTime = Date.now() + TOKEN_LIFETIME_MS;
 sessionStorage.setItem('uploadToken', JSON.stringify({ token, expirationTime }));
 }

 function validateToken() {
 const tokenData = JSON.parse(sessionStorage.getItem('uploadToken'));
 if (!tokenData || Date.now() > tokenData.expirationTime) {
 showNotification('Токен закінчився або відсутній. Оновіть сторінку.', 'error');
 fileInput.disabled = true;
 return false;
 }
 return true;
 }

 function handleFileChange(event) {
 if (!validateToken()) return;

 const files = event.target.files;

 if (files.length > MAX_FILES_PER_UPLOAD) {
 showNotification(`Перевищено максимальну кількість файлів (${MAX_FILES_PER_UPLOAD}).`, 'error');
 fileInput.value = '';
 return;
 }

 for (const file of files) {
 if (file.size > MAX_FILE_SIZE_MB * 1024 * 1024) {
 showNotification(`Файл "${file.name}" перевищує допустимий розмір (${MAX_FILE_SIZE_MB} MB).`, 'error');
 fileInput.value = '';
 return;
 }
 if (!file.type.startsWith('image/')) {
 showNotification(`Файл "${file.name}" не є зображенням.`, 'error');
 fileInput.value = '';
 return;
 }
 }

 imagesWrapper.innerHTML = '';
 convertedImages = [];

 Array.from(files).forEach(file => {
 const reader = new FileReader();
 reader.onload = function(e) {
 const img = new Image();
 img.src = e.target.result;

 img.onload = function() {
 try {
 const imageContainer = createImageContainer(img, file.name);
 imagesWrapper.appendChild(imageContainer);
 resetButton.style.display = 'inline-flex';
 downloadAllButton.style.display = 'inline-flex';
 showNotification('Зображення завантажені та оброблені.', 'success');
 } catch (error) {
 showNotification('Помилка при створенні контейнера для зображення.', 'error');
 }
 };

 img.onerror = function() {
 showNotification(`Не вдалося завантажити зображення "${file.name}".`, 'error');
 };
 };

 reader.onerror = function() {
 showNotification(`Помилка читання файлу "${file.name}".`, 'error');
 };

 reader.readAsDataURL(file);
 });
 }

 function createImageContainer(img, filename) {
 const container = document.createElement('div');
 container.className = 'image-container';

 const originalWrapper = document.createElement('div');
 originalWrapper.className = 'image-wrapper';
 
 const originalImage = new Image();
 originalImage.src = img.src;
 originalImage.alt = 'Original Image';

 const originalLabel = document.createElement('div');
 originalLabel.className = 'image-label original-label';
 originalLabel.textContent = 'Оригінальне зображення';

 originalWrapper.appendChild(originalImage);
 originalWrapper.appendChild(originalLabel);

 const webpWrapper = document.createElement('div');
 webpWrapper.className = 'image-wrapper';
 
 convertToWebP(img, webpWrapper, filename);

 container.appendChild(originalWrapper);
 container.appendChild(webpWrapper);

 return container;
 }

 function convertToWebP(img, container, filename) {
 const canvas = document.createElement('canvas');
 canvas.width = img.width;
 canvas.height = img.height;
 const ctx = canvas.getContext('2d');
 ctx.drawImage(img, 0, 0);

 try {
 const webpUrl = canvas.toDataURL('image/webp');
 const webpImage = new Image();
 webpImage.src = webpUrl;
 webpImage.alt = 'WebP Image';

 const webpLabel = document.createElement('div');
 webpLabel.className = 'image-label webp-label';
 webpLabel.textContent = 'Конвертоване у WebP';

 const downloadIcon = document.createElement('i');
 downloadIcon.className = 'fas fa-download download-icon';
 downloadIcon.title = 'Завантажити';
 downloadIcon.onclick = () => downloadImage(webpUrl, filename.replace(/\.\w+$/, '.webp'));

 webpLabel.appendChild(downloadIcon);
 container.appendChild(webpImage);
 container.appendChild(webpLabel);

 convertedImages.push({ url: webpUrl, filename: filename.replace(/\.\w+$/, '.webp') });
 } catch (error) {
 showNotification('Помилка конвертації у WebP: ' + error.message, 'error');
 }
 }

 function downloadImage(url, filename) {
 if (!validateToken()) return;
 try {
 const link = document.createElement('a');
 link.href = url;
 link.download = filename;
 link.click();
 } catch (error) {
 showNotification('Помилка завантаження файлу.', 'error');
 }
 }

 function downloadAllImages() {
 if (!validateToken()) return;
 try {
 convertedImages.forEach(image => {
 downloadImage(image.url, image.filename);
 });
 } catch (error) {
 showNotification('Помилка при масовому завантаженні файлів.', 'error');
 }
 }

 function resetImages() {
 try {
 fileInput.value = '';
 imagesWrapper.innerHTML = '';
 resetButton.style.display = 'none';
 downloadAllButton.style.display = 'none';
 convertedImages = [];
 hideNotification();
 } catch (error) {
 showNotification('Помилка при скиданні.', 'error');
 }
 }

 function showNotification(message, type) {
 notification.textContent = message;
 notification.className = `notification ${type}`;
 notification.style.display = 'block';
 }

 function hideNotification() {
 notification.style.display = 'none';
 }
</script>

</body>
</html>

Via shram.kiev.ua & ChatGPT


Created/Updated: 29.10.2024

';