Image

IMAGE RESIZER

Resize images by pixels or percentage in your browser. No upload, no server. Aspect ratio lock, presets, live preview.

Drop image here
or click to upload
Upload an image to preview
Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

// ── State ────────────────────────────────────────────────────────────────
  let origImg   = null;   // HTMLImageElement
  let origW     = 0;
  let origH     = 0;
  let origSize  = 0;      // bytes
  let origType  = '';
  let mode      = 'pixels';   // 'pixels' | 'percent'
  let format    = 'jpeg';
  let quality   = 92;
  let fitMode   = 'stretch';  // 'stretch' | 'contain' | 'cover'
  let locked    = true;
  let bgColor   = '#000000';
  let debounceTimer = null;
  let currentBlobURL = null;
  let currentResultW = 0;
  let currentResultH = 0;

  // ── Upload handling ──────────────────────────────────────────────────────
  const uploadZone = document.getElementById('uploadZone');
  const fileInput  = document.getElementById('fileInput');

  uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('drag-over'); });
  uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('drag-over'));
  uploadZone.addEventListener('drop', e => {
    e.preventDefault();
    uploadZone.classList.remove('drag-over');
    const f = e.dataTransfer.files[0];
    if (f && f.type.startsWith('image/')) loadFile(f);
  });

  fileInput.addEventListener('change', () => {
    if (fileInput.files[0]) loadFile(fileInput.files[0]);
  });

  function loadFile(file) {
    origSize = file.size;
    origType = file.type;
    const reader = new FileReader();
    reader.onload = e => {
      const img = new Image();
      img.onload = () => {
        origImg = img;
        origW   = img.naturalWidth;
        origH   = img.naturalHeight;
        onImageLoaded();
      };
      img.src = e.target.result;
    };
    reader.readAsDataURL(file);
  }

  function onImageLoaded() {
    // Update info pills
    document.getElementById('origDimsText').textContent = origW + ' × ' + origH + ' px';
    document.getElementById('origSizeText').textContent = fmtBytes(origSize);
    document.getElementById('origTypeText').textContent = origType.replace('image/', '').toUpperCase();
    document.getElementById('imgInfo').classList.remove('hidden');

    // Show controls
    const cb = document.getElementById('controlsBody');
    cb.classList.remove('hidden');
    cb.style.display = 'flex';

    // Set default dimensions
    document.getElementById('inputW').value = origW;
    document.getElementById('inputH').value = origH;
    document.getElementById('inputPct').value = 100;
    updatePctResult();
    clearErrors();
    scheduleRender();
  }

  // ── Mode ─────────────────────────────────────────────────────────────────
  function setMode(m) {
    mode = m;
    document.getElementById('tabPixels').classList.toggle('active', m === 'pixels');
    document.getElementById('tabPercent').classList.toggle('active', m === 'percent');
    document.getElementById('pixelsMode').classList.toggle('hidden', m !== 'pixels');
    document.getElementById('percentMode').classList.toggle('hidden', m !== 'percent');
    updateFitVisibility();
    scheduleRender();
  }

  // ── Lock ─────────────────────────────────────────────────────────────────
  function toggleLock() {
    locked = !locked;
    const btn = document.getElementById('lockBtn');
    btn.classList.toggle('locked', locked);
    btn.title = locked ? 'Aspect ratio locked' : 'Aspect ratio unlocked';
    if (locked && origW) {
      // sync height to current width
      const w = parseInt(document.getElementById('inputW').value) || origW;
      document.getElementById('inputH').value = Math.round(w * (origH / origW));
    }
    updateFitVisibility();
    scheduleRender();
  }

  function updateFitVisibility() {
    const show = (mode === 'pixels' && !locked);
    document.getElementById('fitModeRow').classList.toggle('hidden', !show);
    if (!show) {
      // hide bg color too
      document.getElementById('bgColorRow').classList.add('hidden');
    } else {
      document.getElementById('bgColorRow').classList.toggle('hidden', fitMode !== 'contain');
    }
  }

  // ── Pixel inputs ─────────────────────────────────────────────────────────
  function onWidthInput() {
    if (!origImg) return;
    const w = parseInt(document.getElementById('inputW').value);
    if (locked && w && origW) {
      document.getElementById('inputH').value = Math.round(w * (origH / origW));
    }
    scheduleRender();
  }

  function onHeightInput() {
    if (!origImg) return;
    const h = parseInt(document.getElementById('inputH').value);
    if (locked && h && origH) {
      document.getElementById('inputW').value = Math.round(h * (origW / origH));
    }
    scheduleRender();
  }

  // ── Percentage input ──────────────────────────────────────────────────────
  function onPctInput() {
    updatePctResult();
    scheduleRender();
  }

  function updatePctResult() {
    if (!origImg) return;
    const pct = parseFloat(document.getElementById('inputPct').value) || 100;
    const rw = Math.round(origW * pct / 100);
    const rh = Math.round(origH * pct / 100);
    document.getElementById('pctResult').innerHTML =
      'Result: <span>' + rw + ' × ' + rh + ' px</span>';
  }

  // ── Format ────────────────────────────────────────────────────────────────
  function setFormat(f) {
    format = f;
    ['jpeg','png','webp'].forEach(x => {
      document.getElementById('fmt' + x.charAt(0).toUpperCase() + x.slice(1))
        .classList.toggle('active', x === f);
    });
    // Quality only relevant for jpeg / webp
    const showQ = (f === 'jpeg' || f === 'webp');
    document.getElementById('qualityRow').classList.toggle('hidden', !showQ);
    scheduleRender();
  }

  // ── Quality ───────────────────────────────────────────────────────────────
  function onQualityChange() {
    quality = parseInt(document.getElementById('qualitySlider').value);
    document.getElementById('qualityVal').textContent = quality;
    scheduleRender();
  }

  // ── Fit mode ──────────────────────────────────────────────────────────────
  function setFit(f) {
    fitMode = f;
    ['stretch','contain','cover'].forEach(x => {
      document.getElementById('fit' + x.charAt(0).toUpperCase() + x.slice(1))
        .classList.toggle('active', x === f);
    });
    document.getElementById('bgColorRow').classList.toggle('hidden', f !== 'contain');
    scheduleRender();
  }

  // ── Background color ──────────────────────────────────────────────────────
  function onBgColorChange() {
    bgColor = document.getElementById('bgColorPicker').value;
    document.getElementById('bgColorText').value = bgColor;
    scheduleRender();
  }

  function onBgColorText() {
    const v = document.getElementById('bgColorText').value.trim();
    if (/^#[0-9a-fA-F]{6}$/.test(v)) {
      bgColor = v;
      document.getElementById('bgColorPicker').value = v;
      scheduleRender();
    }
  }

  // ── Presets ───────────────────────────────────────────────────────────────
  function applyPreset(w, h) {
    if (!origImg) return;
    setMode('pixels');
    locked = false;
    document.getElementById('lockBtn').classList.remove('locked');
    document.getElementById('lockBtn').title = 'Aspect ratio unlocked';
    updateFitVisibility();
    document.getElementById('inputW').value = w;
    document.getElementById('inputH').value = h;
    clearErrors();
    scheduleRender();
  }

  // ── Validation ────────────────────────────────────────────────────────────
  function clearErrors() {
    document.getElementById('inputW').classList.remove('input-error');
    document.getElementById('inputH').classList.remove('input-error');
    document.getElementById('dimError').classList.add('hidden');
  }

  function validateDims(w, h) {
    const errs = [];
    if (!w || w < 1) errs.push('Width must be at least 1 px.');
    if (!h || h < 1) errs.push('Height must be at least 1 px.');
    if (w > 8000) errs.push('Width exceeds 8000 px max.');
    if (h > 8000) errs.push('Height exceeds 8000 px max.');
    if (errs.length) {
      const el = document.getElementById('dimError');
      el.textContent = errs.join(' ');
      el.classList.remove('hidden');
      if (!w || w < 1 || w > 8000) document.getElementById('inputW').classList.add('input-error');
      if (!h || h < 1 || h > 8000) document.getElementById('inputH').classList.add('input-error');
      return false;
    }
    clearErrors();
    return true;
  }

  // ── Debounced render ──────────────────────────────────────────────────────
  function scheduleRender() {
    if (!origImg) return;
    clearTimeout(debounceTimer);
    showSpinner(true);
    debounceTimer = setTimeout(renderPreview, 400);
  }

  function showSpinner(on) {
    document.getElementById('previewSpinner').classList.toggle('hidden', !on);
  }

  // ── Canvas render ─────────────────────────────────────────────────────────
  function getTargetDims() {
    if (mode === 'percent') {
      const pct = parseFloat(document.getElementById('inputPct').value) || 100;
      return {
        w: Math.max(1, Math.round(origW * pct / 100)),
        h: Math.max(1, Math.round(origH * pct / 100))
      };
    } else {
      return {
        w: parseInt(document.getElementById('inputW').value) || 0,
        h: parseInt(document.getElementById('inputH').value) || 0
      };
    }
  }

  function renderPreview() {
    if (!origImg) return;
    const { w, h } = getTargetDims();

    if (mode === 'pixels' && !validateDims(w, h)) {
      showSpinner(false);
      return;
    }

    const canvas = document.getElementById('workCanvas');
    canvas.width  = w;
    canvas.height = h;
    const ctx = canvas.getContext('2d');
    ctx.clearRect(0, 0, w, h);

    const effectiveFit = (mode === 'pixels' && !locked) ? fitMode : 'stretch';

    if (effectiveFit === 'stretch') {
      ctx.drawImage(origImg, 0, 0, w, h);
    } else if (effectiveFit === 'contain') {
      ctx.fillStyle = bgColor;
      ctx.fillRect(0, 0, w, h);
      const scale = Math.min(w / origW, h / origH);
      const dw = origW * scale;
      const dh = origH * scale;
      const dx = (w - dw) / 2;
      const dy = (h - dh) / 2;
      ctx.drawImage(origImg, dx, dy, dw, dh);
    } else if (effectiveFit === 'cover') {
      const scale = Math.max(w / origW, h / origH);
      const sw = w / scale;
      const sh = h / scale;
      const sx = (origW - sw) / 2;
      const sy = (origH - sh) / 2;
      ctx.drawImage(origImg, sx, sy, sw, sh, 0, 0, w, h);
    }

    const mimeType = 'image/' + format;
    const q = (format === 'jpeg' || format === 'webp') ? quality / 100 : undefined;

    canvas.toBlob(blob => {
      if (!blob) { showSpinner(false); return; }

      // Revoke old URL
      if (currentBlobURL) URL.revokeObjectURL(currentBlobURL);
      currentBlobURL = URL.createObjectURL(blob);
      currentResultW = w;
      currentResultH = h;

      const img = document.getElementById('previewImg');
      img.src = currentBlobURL;
      img.classList.remove('hidden');
      document.getElementById('previewPlaceholder').classList.add('hidden');

      // Meta
      document.getElementById('previewMeta').classList.remove('hidden');
      document.getElementById('previewDims').innerHTML =
        '<span>' + w + ' × ' + h + ' px</span> · ' + fmtBytes(blob.size);

      // Upscale warning
      const isUpscaled = (w > origW || h > origH);
      document.getElementById('upscaleWarning').classList.toggle('hidden', !isUpscaled);

      // Download button
      updateDownloadBtn(w, h);
      document.getElementById('downloadBtn').classList.remove('hidden');

      showSpinner(false);
    }, mimeType, q);
  }

  // ── Download ──────────────────────────────────────────────────────────────
  function updateDownloadBtn(w, h) {
    const ext = format === 'jpeg' ? 'jpg' : format;
    document.getElementById('downloadBtn').textContent =
      'Download ' + format.toUpperCase() + ' — ' + w + ' × ' + h;
  }

  function doDownload() {
    if (!currentBlobURL) return;
    const ext = format === 'jpeg' ? 'jpg' : format;
    const a = document.createElement('a');
    a.href = currentBlobURL;
    a.download = 'toolpad-resized-' + currentResultW + 'x' + currentResultH + '.' + ext;
    a.click();
  }

  // ── Utilities ─────────────────────────────────────────────────────────────
  function fmtBytes(n) {
    if (n < 1024) return n + ' B';
    if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
    return (n / (1024 * 1024)).toFixed(2) + ' MB';
  }