Иллюстрация к статье
Иллюстрация к статье

Представим распространённые ситуации:

  • пользователь вашего сайта пытается выложить фото или другое изображение, но натыкается на фрюстрирующее сообщение об ошибке, гласящее, что размер файла непозволительно большой;

  • ваше сайт активно использует изображения, присылаемые пользователями, и по мере роста аудитории, вы всё чаще сталкиваетесь с падением производительности серверов, а то и с падением оных.

Данная статья предлагает решение данных проблем путём изменения и/или создания файлов изображений "налету" прямо перед отправкой на сервер. (Спойлер: это просто). Здесь вы найдёте исчерпывающую информацию для разработки специальных собственных скриптов, а так же готовое решение, которое можно просто подключить и пользоваться.

Итак, суть проста

  1. Берём данные файла изображения (инстанс класса "File"), например, из заполненного пользователем элемента ввода "input" типа "file" (нужные данные лежат в списке input.files).

  2. Затем, с помощью этих данных создаётся элемент "Image" ("img") и размещается на специально созданном элементе "canvas" нужного размера, где и происходит автоматическое масштабирование, подстройка под нужные размеры.

  3. После этого, штатными средствами содержимое "canvas" сохраняется в нужном формате с нужным качеством в виде опять же инстанса "Files". Это и есть новый, созданный нами, файл с установленными характеристиками.

  4. Наконец, делаем с этим файлом, то что нам подходит. Например, сразу заносим его в FormData и отправляем средствами Ajax, либо заменяем оригинальный файл в элементе ввода и используем штатную отправку формы, либо заполняем скрытые элементы ввода файлов для вариаций отправляемого изображения в разных размерах.

Примечание. Поддерживаемые типы изображений: jpeg, png, webp, gif

Рассмотрим код

Примечание. Далее и везде Float - это обозначение вещественных чисел чисто удобства ради (моего) (по "канону" в JS оно обозначается по-другому).

window.onload = () => {
  /*
    Собственно, вот асинхронная функция принимающая файл изображения image_file 
    и возвращающая новый файл изображения с следующими характеристиками:
    type (String) - тип/формат изображения; поддерживается jpeg, png, webp и gif
    height (Float) - высота в пикселях
    width (Float) - ширина в пикселях
    quality (Float) - качество от 0 до 1; только для jpeg и webp (поумолчанию браузеры устанавливают ~ .95)
    file_name (String) - имя файла
  */
  const resize_image = (image_file, type, height, width, quality, file_name) => new Promise((resolve, reject) => {
      // создаём Image и заполняем его
      const img = new Image();
      img.src = URL.createObjectURL(image_file);
      
      // Ждём окончание загрузки
      img.onload = () => {
          const
              // создаём canvas и 2d контекст
              canvas = document.createElement('canvas'),
              ctx = canvas.getContext('2d');
       
        // устанавливаем размеры canvas как-раз на нужные нам
        canvas.setAttribute('width', width);
        canvas.setAttribute('height', height);
        
        // размещаем наше изображение в контекст на весь размер canvas
        ctx.drawImage(img, 0, 0, width, height);
        
        // сохраненяем содержимое canvas в виде blob-данных 
        canvas.toBlob(blob => {
            // создаём целевой инстанс File из blob и возвращаем его
            const new_file = new File([blob], file_name, {type: 'image/' + type, lastModified: Date.now()});
            resolve(new_file);
        }, 'image/' + type, quality);
      };
  });

  // созданим элемент ввода файлов и разместим прямо в body
  const file_input = document.createElement('input');
  document.body.append(file_input);
  file_input.type = 'file';
  
  // ожидаем ввод файла пользователем
  file_input.addEventListener('change', () => {
      
    // используем созданную выше функцию
      resize_image(file_input.files[0], 'jpeg', 200, 300, 0.8, 'resized_img.jpg').then(new_image_file => {
          
          // создаём img, заполняем его и размещаем после элемента ввода, дабы пронаблюдать результат
          const new_img = new Image();
          new_img.src = URL.createObjectURL(new_image_file);
          document.body.append(new_img);
          
          /* 
            так, с помощью DataTransfer устанавливаем новое содержимое для элемента ввода файлов input'а;
            в частноти, заменяем первый, выбранный пользователем, файл на созданный нами 
          */
          const dt = new DataTransfer();
          dt.items.add(new_image_file);
          file_input.files = dt.files;
      });
  });
 };

Готовое решение

Создано, как не трудно догадаться, мной.

Называется Uploading-Image-Resizer.

Лежит на Гитхабе тут https://github.com/admtoha/uploading-image-resizer .

Интерактивная демонстрация здесь https://admtoha.is-a.dev/html/demo_example_uploading_image_resizer.html . Пользуйтесь не только, чтобы оценить работу "в живую", но и для подбора наиболее подходящих опций специально для ваших нужд.

Интерактивная демонстрация
Интерактивная демонстрация

Как пользоваться

Примечание. Далее информация будет часто повторяться. Это сделано специально для того, чтобы не приходилось перечитывать всю статью для выяснения частных нюансов, читать "по диагонали".

Есть два пути использования:

  • условно "высокоуровневый", с помощью атрибута data-uploading-image-resizer элемента ввода input;

  • условно "низкоуровневый", с помощью соответствующей функции resize_image_file(image_file, options).

Подключите файл uploading_image_resizer.js к вашей странице.

<script language="JavaScript" src="./uploading_image_resizer.js"></script>

Пропишите в целевом элементе ввода файла изображения атрибут data-uploading-image-resizer и заполните его значение нужными вам опциями, разделёнными запятой.

Пример:

<input type=file name=my_image data-uploading-image-resizer='max_height: 700, max_width: 900, type: jpg, quality: 0.85'>

Все опции

  • type (String) - тип изображения; доступно jpeg, png, webp и gif (если не указано, сохраняется тип оригинала)

  • max_height (Float) - максимальна высота изображения в пикселях (размер изменяется, только если оригинальная высота превышает этот параметр)

  • max_width (Float) - максимальна ширина изображения в пикселях (размер изменяется, только если оригинальная ширина превышает этот параметр)

  • quality (Float) - уровень качества в диапазоне от 0 до 1; доступно только на типах jpeg и webp (по умолчанию 1)

  • name (String) - имя файла; расширение имени файла может использоваться для указания требуемого типа (если не указано, сохраняется имя оригинала)

  • height (Float) - высота изображения в пикселях

  • width (Float) - ширина изображения в пикселях

  • callback (String) - имя колбэк функции, вызываемой, в момент, когда изменение файла закончено

    • Аргументы, передаваемые в функцию:

      • node (HTMLInputElement) - целевой элемент input

      • extra_ls (Array) - список дополнительных данных вида:

{
    input: {
        file: file, // (Object instance File) - файл оригинального изображения 
        name: file name, // (String) - имя оригинального изображения
        type: file type, // (String) - тип оригинального изображения
        size: file size, // (Float) - размер оригинального изображения в байтах
        height: image height, // (Float) - высота оригинального изображения в пикселях
        width: image width // (Float) - ширина оригинального изображения в пикселях
    },
    output: {
        file: file, // (Object instance File) - файл изменённого изображения 
        name: file name, // (String) - имя изменённого изображения
        type: file type, // (String) - тип изменённого изображения
        size: file size, // (Float) - размер изменённого изображения в байтах
        height: image height, // (Float) - высота изменённого изображения в пикселях
        width: image width // (Float) - ширина изменённого изображения в пикселях
    }
}
<!-- Пример: -->

<input type=file name=my_image data-uploading-image-resizer='max_height: 700, max_width: 900, type: jpg, callback: foo'>
				
<script>
    function foo(node, extra_ls){
        console.log(JSON.stringifi(extra_ls));
    }
</script>
  • target (String) - идентификатор элемента ввода файла, назначаемого, в качестве целевого; используется для создания нескольких изменённых версий версий одного изображения

<!-- Пример: -->
<input type=file id=origin_img name=origin_img>
<input type=file name=medium_img style='display: none' data-uploading-image-resizer='target: origin_img, type: webp, max_height: 1080, max_width: 1920'>
<input type=file name=small_img style='display: none' data-uploading-image-resizer='target: origin_img, type: webp, max_height: 500, max_width: 850'>

<!-- Можно сочетать с изменением самого оригинала. Пример: -->
<input type=file id=origin_img name=big_img data-uploading-image-resizer='type: webp, max_height: 2000, max_width: 4000'>
<input type=file name=medium_img style='display: none' data-uploading-image-resizer='target: origin_img, type: webp, max_height: 1080, max_width: 1920'>
<input type=file name=small_img style='display: none' data-uploading-image-resizer='target: origin_img, type: webp, max_height: 500, max_width: 850'>	

Примечания

Все опции не являются обязательными.

Для того, чтобы сохранить соотношение сторон нужно указать только один параметр точного размера (либо height, либо width), либо один или оба параметра max_height и max_width.

При множественном выборе файлов все они также преобразуются согласно опциям.

Внимание. работа по преобразованию изображений занимает время, и, если вы используете незамедлительную отправку файлов, то есть, сразу, как только пользователь прикрепляет файл - может возникнуть проблема, заключающаяся в том, что изменение изображений не успеет закончится, и на сервер будут отправлены оригинальные файлы вместо изменённых. Чтобы этого избежать, используйте вышеописанную опцию "callback". То есть начинайте выгружать файлы изображений именно тогда, когда сработает колбэк, дабы удостовериться, что вся работа закончена.

Использование функции преобразования файлов изображений напрямую

resize_image_file(image_file, options)
  Аргументы:
      
      image_file (instance File) - файл изображения в виде blob-данных; можно взять из input.files или создать динамически 
      
      options (Object) - опции в виде объекта (почти полностью совпадает с опциями для атрибута, указанными выше)
          
          Все опции:
              
              - type (String) - тип изображения; доступно jpeg, png, webp и gif (если не указано, сохраняется тип оригинала)
  
              - max_height (Float) - максимальна высота изображения в пикселях (размер изменяется, только если оригинальная высота превышает этот параметр)
              
              - max_width (Float) - максимальна ширина изображения в пикселях (размер изменяется, только если оригинальная ширина превышает этот параметр)
              
              - quality (Float) - уровень качества в диапазоне от 0 до 1; доступно только на типах jpeg и webp (поумолчанию 1)
              
              - name (String) - имя файла; расширение имени файла может использоваться для указания требуемого типа (если не указано, сохраняется имя оригинала)
              
              - height (Float) - высота изображения в пикселях
              
              - width (Float) - ширина изображения в пикселях
              
              - get_extra_data (Boolean) - функция возвращает дополнительные данные вместо просто файла blob-данных (поумолчанию - false)
                  Дополнительные данные имеют вид:
                      {
                          input: {
                              file: file, // (Object instance File) - файл оригинального изображения 
                              name: file name, // (String) - имя оригинального изображения
                              type: file type, // (String) - тип оригинального изображения
                              size: file size, // (Float) - размер оригинального изображения в байтах
                              height: image height, // (Float) - высота оригинального изображения в пикселях
                              width: image width // (Float) - ширина оригинального изображения в пикселях
                          },
                          output: {
                              file: file, // (Object instance File) - файл изменённого изображения 
                              name: file name, // (String) - имя изменённого изображения
                              type: file type, // (String) - тип изменённого изображения
                              size: file size, // (Float) - размер изменённого изображения в байтах
                              height: image height, // (Float) - высота изменённого изображения в пикселях
                              width: image width // (Float) - ширина изменённого изображения в пикселях
                          }
                      }
  
  Возвращает: 
      Promise -> (instance File) данные нового файла изображения, если get_extra_data равно false, иначе дополнительные данные (смотри выше). 

  
  Пример:
      
      <input type=file id=img_file>
      
      <script>
          
          const input_file = document.getElementById('img_file');
          input_file.addEventListener('change', () => {
              if(!input_file.value) return;
              resize_image_file(input_file.files[0], {heigth: 200, get_extra_data: true}).then(res => {
                  console.log(res);
                  const img = new Image();
                  img.src = URL.createObjectURL(res.output.file);
                  document.body.append(img);
              });
          });
          
      </script>

P.S. Если для кого-то важно, вся статья и весь код писались без использования ИИ.

P.P.S. Телеграмм-канала у меня нет, подписываться некуда, извините.

Комментарии (3)



  1. JBFW
    30.11.2025 13:41

    Когда-то давно один умный человек высказывал такую мысль:

    Компьютер плохо уменьшает графику, если уменьшение нецелое: то есть, условно, 2 пикселя в 1 - хорошо, 3 пикселя в два - плохо.
    Сжать 800х600 в 400х300 - изображение будет визуально лучше, чем если 800х600 в 450х350.

    Способ решения там - поиск наименьшего общего произведения для сторон исходной и целевой картинки, растягивание исходной до него в X раз, а потом сжатие в Y.

    И вот я делал как-то, причем как раз по вашему варианту - загрузка файла, создание картинки, ее масштабирование туда-сюда, и загрузка итогового результата на сайт.

    Действительно, картинки "чище" получались, чем если напрямую уменьшать.


    1. admtoha Автор
      30.11.2025 13:41

      Интересно.

      Не знал про такую особенность уменьшения графики.