Skip to content

HTML5 image upload, resize and crop

Last week I showed you how to upload images, resize and crop them using PHP. As I promised, this week I will show some modern and advanced HTML5 image upload techniques that you can use on your site.

HTML5 is a big trend and an awesome way of building sexy web applications that your users will just love. HTML5 introduced its File API for representing file objects in web applications, as well as programmaticaly selecting them and accessing their data.

There are two ways of using it:

  • By using <input type=”file”> and its change event
  • By dragging and dropping files from your computer directly in the browser

There are lot of fancy stuff available and I will try to show as many as possible here.

HTML5 image upload – upload multiple files

Lets get our hands dirty and begin some coding. For HTML part, we will reuse the form from the last week and add some features to it:

<form enctype="multipart/form-data" method="post" action="upload.php">
  <div class="row">
    <label for="filesToUpload">Select Files to Upload</label><br />
    <input type="file" name="filesToUpload[]" id="filesToUpload" multiple="multiple" />
    <output id="filesInfo"></output>
  </div>
  <div class="row">
    <input type="submit" value="Upload" />
  </div>
</form>

We are not dealing with fancy styles, so there is no CSS involved. Notice the name of the file input. It allows us to store multiple files as multidimensional array. And notice the multiple attribute which is simply telling the browser to allow multiple files to be selected in it’s browse window.

The real thing here is that the browsers that do not support HTML5 features of FileAPI, will simply ignore multiple attribute and everything will act like in the first tutorial.

JavaScript code for file handling looks like this:

<script>
function fileSelect(evt) {
  var files = evt.target.files;
  var result = '';

  for (var i = 0; i < files.length; i++) {
    var file = files[i];
    result += '<li>' + file.name + ' (' + file.size + ' bytes)</li>';
  }

  document.getElementById('filesInfo').innerHTML = '<ul>' + result + '</ul>';
}

document.getElementById('filesToUpload').addEventListener('change', fileSelect, false);
</script>

Try this now, select multiple files and you should see some file info below your form inside output element. Nice.

This is ideal solution, when browser supports everything. Let me show you how to check for support in your browser:

<script>
(function () {
  function formatBytes(bytes) {
    if (bytes === 0) return '0 B';
    const k = 1024, sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return (bytes / Math.pow(k, i)).toFixed(i ? 1 : 0) + ' ' + sizes[i];
  }

  function escapeHtml(str) {
    return str.replace(/[&<>"']/g, s => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[s]));
  }

  function fileSelect(evt) {
    if (!(window.File && window.FileReader && window.FileList && window.Blob)) {
      alert('The File APIs are not fully supported in this browser.');
      return;
    }

    const files = evt.target.files || [];
    const items = Array.from(files).map(f =>
      `<li>${escapeHtml(f.name)} — ${formatBytes(f.size)}</li>`
    ).join('');

    document.getElementById('filesInfo').innerHTML = `<ul>${items}</ul>`;
  }

  // Ensure the input exists before attaching
  const input = document.getElementById('filesToUpload');
  if (input) input.addEventListener('change', fileSelect, false);
})();
</script>

If you run this code in Internet Explorer, you will be able to select only one file and alert will pop up that the File APIs are not supported.

HTML5 image upload – drag and drop support

This really the interesting part. Some browsers treat input type=”file” like a drop target, so if you try to drag and drop files into a form from previous example in FireFox, it will work out of the box.

HTML for dragging and dropping looks like this:

<div id="dropTarget" style="width: 100%; height: 100px; border: 1px solid #ccc; padding: 10px;">
  Drop some files here
</div>

<output id="filesInfo"></output>

Don’t mind the inline CSS, it is there just to make the drop target bigger. The JavaScript is very similar to before:

<script>
(function () {
  // Helpers
  const formatBytes = (bytes) => {
    if (!bytes) return '0 B';
    const k = 1024, units = ['B', 'KB', 'MB', 'GB', 'TB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return (bytes / Math.pow(k, i)).toFixed(i ? 1 : 0) + ' ' + units[i];
  };

  const escapeHtml = (s) =>
    s.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));

  const renderList = (files) => {
    const list = Array.from(files).map(f =>
      `<li>${escapeHtml(f.name)} — ${formatBytes(f.size)}</li>`
    ).join('');
    filesInfo.innerHTML = `<ul>${list}</ul>`;
  };

  // Guard for File APIs
  if (!(window.File && window.FileReader && window.FileList && window.Blob)) {
    console.warn('The File APIs are not fully supported in this browser.');
  }

  // Elements
  const dropTarget = document.getElementById('dropTarget');
  const filesInfo  = document.getElementById('filesInfo');

  if (!dropTarget || !filesInfo) return;

  // Visual highlight
  const addHighlight = () => dropTarget.style.outline = '2px dashed #888';
  const removeHighlight = () => dropTarget.style.outline = '';

  // Prevent default on whole drop area events
  const preventDefaults = (e) => {
    e.preventDefault();
    e.stopPropagation();
  };

  // Handlers
  const onDragOver = (e) => {
    preventDefaults(e);
    e.dataTransfer.dropEffect = 'copy';
    addHighlight();
  };

  const onDragEnter = (e) => {
    preventDefaults(e);
    addHighlight();
  };

  const onDragLeave = (e) => {
    preventDefaults(e);
    removeHighlight();
  };

  const onDrop = (e) => {
    preventDefaults(e);
    removeHighlight();

    const files = e.dataTransfer && e.dataTransfer.files ? e.dataTransfer.files : [];
    renderList(files);
  };

  // Attach listeners
  ['dragenter', 'dragover'].forEach(ev => dropTarget.addEventListener(ev, onDragOver, false));
  dropTarget.addEventListener('dragleave', onDragLeave, false);
  dropTarget.addEventListener('drop', onDrop, false);
})();
</script>

Oh, really nice.

HTML5 image upload – show file preview before upload

There is plenty more stuff you can do with this. I will show you how to show the preview of the image before actual upload happen.

Some HTML:

<form enctype="multipart/form-data" method="post" action="upload.php">
  <div class="row">
    <label for="filesToUpload">Select Files to Upload</label><br />
    <input type="file" name="filesToUpload[]" id="filesToUpload" multiple="multiple" />
    <output id="filesInfo"></output>
  </div>
  <div class="row">
    <input type="submit" value="Upload" />
  </div>
</form>

And the JavaScript:

<script>
function fileSelect(evt) {
  if (window.File && window.FileReader && window.FileList && window.Blob) {
    const files = evt.target.files;
    const filesInfo = document.getElementById('filesInfo');
    filesInfo.innerHTML = ""; // clear previous previews

    for (let i = 0; i < files.length; i++) {
      const file = files[i];

      // Only process image files
      if (!file.type.match('image.*')) {
        continue;
      }

      const reader = new FileReader();

      // Closure to capture the file info
      reader.onload = (function () {
        return function (e) {
          const div = document.createElement('div');
          div.innerHTML = `<img style="width:90px; margin:5px;" src="${e.target.result}" alt="${file.name}" />`;
          filesInfo.appendChild(div);
        };
      })(file);

      // Read in the image file as a data URL.
      reader.readAsDataURL(file);
    }
  } else {
    alert('The File APIs are not fully supported in this browser.');
  }
}

document.getElementById('filesToUpload')
        .addEventListener('change', fileSelect, false);
</script>

Actual HTML5 image upload

Finally, let’s upload this images to server using XMLHttpRequest:

<script>
(() => {
  if (!(window.File && window.FormData && window.XMLHttpRequest)) {
    alert('Your browser does not fully support file uploads.');
    return;
  }

  const input = document.getElementById('filesToUpload');
  const info  = document.getElementById('filesInfo');

  // Optional: basic validation settings
  const ALLOWED_TYPES = null; // e.g., ['image/jpeg','image/png']
  const MAX_FILE_SIZE = null; // e.g., 10 * 1024 * 1024 (10 MB)

  const human = (b) => {
    if (!b) return '0 B';
    const k = 1024, u = ['B','KB','MB','GB','TB'];
    const i = Math.floor(Math.log(b) / Math.log(k));
    return (b / Math.pow(k, i)).toFixed(i ? 1 : 0) + ' ' + u[i];
  };

  input.addEventListener('change', () => {
    const files = input.files;
    if (!files || !files.length) return;

    // Validate (optional)
    for (const f of files) {
      if (ALLOWED_TYPES && !ALLOWED_TYPES.includes(f.type)) {
        info.textContent = `Blocked: ${f.name} (type ${f.type} not allowed)`;
        return;
      }
      if (MAX_FILE_SIZE && f.size > MAX_FILE_SIZE) {
        info.textContent = `Blocked: ${f.name} (size ${human(f.size)} > limit)`;
        return;
      }
    }

    // Build FormData using the input's name (ensure [] for PHP array)
    const fd = new FormData();
    const baseName = input.name && input.name.length ? input.name : 'filesToUpload[]';
    const fieldName = baseName.endsWith('[]') ? baseName : baseName + '[]';
    for (const f of files) fd.append(fieldName, f, f.name);

    // Start XHR
    const xhr = new XMLHttpRequest();
    xhr.open('POST', 'upload.php', true);

    // Progress UI (minimal)
    xhr.upload.onprogress = (e) => {
      if (!e.lengthComputable) return;
      const pct = Math.round((e.loaded / e.total) * 100);
      info.textContent = `Uploading… ${pct}%`;
    };

    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        // Show server response if present, otherwise success
        info.textContent = xhr.responseText ? xhr.responseText : 'Done!';
      } else {
        info.textContent = `Upload failed: ${xhr.status} ${xhr.statusText}`;
      }
    };

    xhr.onerror = () => {
      info.textContent = 'Network error during upload.';
    };

    xhr.send(fd);
  });
})();
</script>

For resizing and cropping these images, we will modify the script from the last week’s tutorial (ImageManipulator class is available here):

<?php
// upload.php

declare(strict_types=1);

// ---- Config ----
$TARGET_DIR = __DIR__ . '/uploads';
$CROP_W = 200;
$CROP_H = 130;
$MAX_BYTES = 10 * 1024 * 1024; // 10 MB
$ALLOWED_EXT = ['jpg','jpeg','png','gif'];
$ALLOWED_MIME = ['image/jpeg','image/png','image/gif'];

// Include ImageManipulator (adjust path if needed)
require_once __DIR__ . '/ImageManipulator.php';

// Ensure upload dir exists
if (!is_dir($TARGET_DIR) && !mkdir($TARGET_DIR, 0755, true)) {
  http_response_code(500);
  exit('Failed to create uploads directory.');
}

// Normalize incoming files (single or multiple)
function normalize_files_array(array $filesField): array {
  $normalized = [];
  if (is_array($filesField['name'])) {
    foreach ($filesField['name'] as $i => $name) {
      $normalized[] = [
        'name'     => $name,
        'type'     => $filesField['type'][$i] ?? '',
        'tmp_name' => $filesField['tmp_name'][$i] ?? '',
        'error'    => $filesField['error'][$i] ?? UPLOAD_ERR_OK,
        'size'     => $filesField['size'][$i] ?? 0,
      ];
    }
  } else {
    $normalized[] = $filesField;
  }
  return $normalized;
}

if (empty($_FILES['filesToUpload'])) {
  http_response_code(400);
  exit("No files received.");
}

$files = normalize_files_array($_FILES['filesToUpload']);

// MIME detector
$finfo = new finfo(FILEINFO_MIME_TYPE);

$messages = [];

foreach ($files as $idx => $file) {
  if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
    $messages[] = "File #$idx error: " . (string)$file['error'];
    continue;
  }

  if ($file['size'] > $MAX_BYTES) {
    $messages[] = "{$file['name']}: exceeds size limit.";
    continue;
  }

  // Extension & MIME checks
  $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
  if (!in_array($ext, $ALLOWED_EXT, true)) {
    $messages[] = "{$file['name']}: invalid extension.";
    continue;
  }

  $mime = $finfo->file($file['tmp_name']) ?: '';
  if (!in_array($mime, $ALLOWED_MIME, true)) {
    $messages[] = "{$file['name']}: invalid MIME type.";
    continue;
  }

  if (!is_uploaded_file($file['tmp_name'])) {
    $messages[] = "{$file['name']}: not a valid uploaded file.";
    continue;
  }

  // Process image: center crop to 200x130
  try {
    $manipulator = new ImageManipulator($file['tmp_name']);
    $width  = $manipulator->getWidth();
    $height = $manipulator->getHeight();

    // Ensure crop fits within image bounds (centered)
    $halfW = (int) floor($CROP_W / 2);
    $halfH = (int) floor($CROP_H / 2);
    $cx = (int) floor($width / 2);
    $cy = (int) floor($height / 2);

    // Adjust if the image is smaller than crop size
    if ($width < $CROP_W || $height < $CROP_H) {
      // Fallback: just save as is (or you could resize up/down first)
      // $manipulator->resample($CROP_W, $CROP_H); // if ImageManipulator supports it
      // else just continue to crop with clamped bounds
    }

    $x1 = max(0, $cx - $halfW);
    $y1 = max(0, $cy - $halfH);
    $x2 = min($width,  $cx + $halfW);
    $y2 = min($height, $cy + $halfH);

    // If bounds still not exact size (near edges), readjust to ensure target dimensions
    // while staying in-bounds.
    if (($x2 - $x1) < $CROP_W) {
      $delta = $CROP_W - ($x2 - $x1);
      $x1 = max(0, $x1 - (int)ceil($delta / 2));
      $x2 = min($width, $x1 + $CROP_W);
    }
    if (($y2 - $y1) < $CROP_H) {
      $delta = $CROP_H - ($y2 - $y1);
      $y1 = max(0, $y1 - (int)ceil($delta / 2));
      $y2 = min($height, $y1 + $CROP_H);
    }

    $manipulator->crop($x1, $y1, $x2, $y2);

    // Filename: timestamp + sanitized original
    $safeName = preg_replace('/[^A-Za-z0-9._-]/', '_', $file['name']);
    $newName  = time() . '_' . $safeName;
    $savePath = $TARGET_DIR . '/' . $newName;

    $manipulator->save($savePath);

    $messages[] = "Uploaded: " . htmlspecialchars($newName, ENT_QUOTES, 'UTF-8');
  } catch (Throwable $e) {
    $messages[] = "{$file['name']}: processing error.";
  }
}

header('Content-Type: text/plain; charset=UTF-8');
echo implode("\n", $messages);

Just one more thing …

Client size resizing before upload

This is a very neat feature. For example, your user wants to upload a photo directly from the camera. The idea is to resize the image to some normal resolution before uploading and save time uploading it to server.

This can be achieved with canvas and some of the techniques described above. HTML is the same and JavaScript looks like this:

<script>
(() => {
  const input     = document.getElementById('filesToUpload');
  const filesInfo = document.getElementById('filesInfo');

  if (!window.File || !window.FileReader || !window.FileList || !window.Blob) {
    alert('The File APIs are not fully supported in this browser.');
    return;
  }
  if (!input) return;

  const MAX_WIDTH  = 400;
  const MAX_HEIGHT = 300;
  const JPEG_QUALITY = 0.85;

  input.addEventListener('change', () => {
    const files = input.files;
    if (!files || !files.length) return;

    filesInfo.innerHTML = '<ul></ul>';
    const list = filesInfo.querySelector('ul');

    Array.from(files).forEach(file => {
      if (!file.type.startsWith('image/')) {
        const li = document.createElement('li');
        li.textContent = `${file.name}: skipped (not an image)`;
        list.appendChild(li);
        return;
      }
      const li = document.createElement('li');
      li.textContent = `${file.name}: processing…`;
      list.appendChild(li);

      resizeAndUpload(file)
        .then(() => li.textContent = `${file.name}: done`)
        .catch(err => {
          console.error(err);
          li.textContent = `${file.name}: failed (${err.message || 'error'})`;
        });
    });
  });

  function resizeAndUpload(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onerror = () => reject(new Error('FileReader error'));
      reader.onload = () => {
        const img = new Image();
        img.onerror = () => reject(new Error('Image decode error'));
        img.onload = () => {
          // Compute target dimensions (contain within MAX_WIDTH x MAX_HEIGHT)
          let { width: w, height: h } = img;
          const ratio = Math.min(MAX_WIDTH / w, MAX_HEIGHT / h, 1); // never upscale
          const tw = Math.round(w * ratio);
          const th = Math.round(h * ratio);

          const canvas = document.createElement('canvas');
          canvas.width = tw;
          canvas.height = th;
          const ctx = canvas.getContext('2d');
          ctx.drawImage(img, 0, 0, tw, th);

          // Generate JPEG data URL (fallback if toBlob not needed by server)
          const dataURL = canvas.toDataURL('image/jpeg', JPEG_QUALITY);

          // Send via application/x-www-form-urlencoded (matches your PHP)
          const body = new URLSearchParams();
          body.set('filename', file.name);
          body.set('image', dataURL);

          fetch('uploadResized.php', {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
            body: body.toString()
          })
          .then(res => {
            if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
            return res.text().catch(() => '');
          })
          .then(() => resolve())
          .catch(reject);
        };
        img.src = reader.result;
      };
      reader.readAsDataURL(file);
    });
  }
})();
</script>

The code for saving the image on the server is:

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['image'])) {
    define('UPLOAD_DIR', __DIR__ . '/uploads/');

    if (!is_dir(UPLOAD_DIR)) {
        mkdir(UPLOAD_DIR, 0755, true);
    }

    $img = $_POST['image'];

    // Strip the data: URI scheme header if present
    if (strpos($img, 'data:image/') === 0) {
        $img = preg_replace('#^data:image/\w+;base64,#i', '', $img);
    }

    // Replace spaces with +
    $img = str_replace(' ', '+', $img);

    $data = base64_decode($img, true);
    if ($data === false) {
        http_response_code(400);
        exit('Invalid base64 data.');
    }

    $file = UPLOAD_DIR . uniqid('img_', true) . '.jpg';
    $success = file_put_contents($file, $data);

    if ($success === false) {
        http_response_code(500);
        exit('Unable to save the file.');
    }

    // Return just the file path (relative or absolute as needed)
    echo basename($file);
} else {
    http_response_code(400);
    echo 'No image received.';
}

That’s it. I hope you learned something new and will use this knowledge on your next project.

Tags:

15 thoughts on “HTML5 image upload, resize and crop”

  1. Just the thing I was looking for. I didn’t even know you can resize images before uploading, what a cool feature.

    Thanks

  2. Pingback: AjaxUpload - Problem mit IE9 - Zend Framework Forum - ZF1 / ZF2

  3. Like Noel, I didn’t even know you could re-size images before uploading, what a cool feature as I guess it’s quicker to re-size client side then upload 3MB raw cam images and then do it. Not seen that feature in an uploader before.

    Is there a Demo of this or zip to play with please?

  4. How can i add a progress bar or % to the html5 upload? i have all day trying to do that but without results 🙁

  5. When resizing client side, before uploading, the uploaded image isn’t sharp unfortunately.
    Thanks for the tutorial!

  6. Hello, first thanks for tut, really nice.

    I’m trying to resize one image teken from my iphone camera and upload it to my webserver. It works, but…

    Lets say I’ve defined var MAX_WIDTH = 320; and var MAX_HEIGHT = 320;

    What is happening is that my image gets srinked inside a 320px black square…

    Do you have any clue on why this is happening?

    Thanks!

    Pluda

  7. Hello, thanks for the script. i am trying to test the script but it does not work. you call a file named ‘uploadResized.php’. Where is it file. do you have a sample or zip file of your tutorial ? thanks for all

  8. This is great! How would I implement this in a form with other inputs, and upload the whole form as one when pressing submit? I am trying to resize the image client side, but when I am submitting my form (with PHP) it submits the original image!

Comments are closed.