Przejdź do głównej treści

Edytor Zdjęć Online

Grafika Edytor Obrazka

Odkryj prosty, szybki i całkowicie przeglądarkowy edytor zdjęć, który nie wymaga instalacji! Nasza aplikacja umożliwia obróbkę plików JPG, PNG oraz TIFF bezpośrednio w Twojej przeglądarce – wszystko dzieje się lokalnie, bez przesyłania danych na serwer.

Najważniejsze funkcje:

  • Natychmiastowy podgląd zmian na obrazie dzięki technologii canvas i optymalizacji (debouncing przy suwakach).
  • Regulacja parametrów obrazu — jasność, kontrast, nasycenie, odcień, rozmycie.
  • Efekty specjalne — sepia, negatyw, czarno-biały, żywe kolory (vivid) i inne.
  • Filtry konwolucyjne — wyostrzanie, emboss, wykrywanie krawędzi.
  • Presety jednym kliknięciem — szybkie style gotowe do użycia.
  • Historia zmian i miniatury podglądu — możesz cofnąć się do poprzednich wersji.
  • Obsługa TIFF przez automatyczne ładowanie biblioteki UTIF.js (jeśli potrzeba).
  • Eksport do PNG w pełnej rozdzielczości.

Dla kogo?

Dla każdego, kto chce w prosty sposób poprawić lub przekształcić swoje zdjęcia: fotografów, grafików, twórców treści i pasjonatów wizualnych efektów. Wystarczy przeglądarka — żadne instalacje, żadne logowanie.

Dlaczego warto?

  • Działa szybko i płynnie, nawet na dużych plikach.
  • Zmiany widoczne są natychmiast, a historia pozwala wrócić o krok wstecz.
  • Całość działa offline – Twoje zdjęcia pozostają tylko u Ciebie.

Kontrolki

Presety

Uwagi: TIFF może wymagać dodatkowego dekodera (UTIF.js). Pobierz/otwórz plik przez górne pole.
Podgląd
(Canvas — zmiany są natychmiastowe)
Historie / miniatury

Kod po stronie przeglądarki

<link type="text/css" href="http://www.dariuszrorat.ugu.pl/assets/css/bootstrap/wcag-outline.min.css" rel="stylesheet"> 
<style>
    .preview-wrap {border-radius: .5rem; padding: 1rem; box-shadow: 0 6px 18px rgba(0,0,0,.06);}
    canvas { max-width: 100%; height: auto; display:block; }
    .control-label { font-size: .9rem; }    
    .preset-btn { margin-right:.375rem; margin-bottom:.375rem }
</style>
<div id="app">
  <div class="row mb-4 align-items-center">
      <div class="col-md-12 text-md-end mt-3 mt-md-0">
        <input id="fileInput" class="form-control" type="file" accept="image/jpeg,image/png,image/tiff,.tif,.tiff" aria-label="Wybierz zdjęcie" />
      </div>
  </div>
    
  <div class="row g-4">
    <div class="col-lg-4">
      <div class="card p-3">
        <h3 class="h5 mb-3">Kontrolki</h3>

        <div class="mb-3">
          <div class="form-label control-label">Presety</div>
          <div>
            <button class="btn btn-sm btn-outline-secondary preset-btn" data-preset="normal">Normal</button>
            <button class="btn btn-sm btn-outline-secondary preset-btn" data-preset="sepia">Sepia</button>
            <button class="btn btn-sm btn-outline-secondary preset-btn" data-preset="negatyw">Negatyw</button>
            <button class="btn btn-sm btn-outline-secondary preset-btn" data-preset="bw">Czarno-białe</button>
            <button class="btn btn-sm btn-outline-secondary preset-btn" data-preset="vivid">Vivid</button>
          </div>
        </div>

        <hr />

        <div class="mb-3">
          <label for="brightness" class="form-label control-label">Jasność</label>
          <input id="brightness" type="range" class="form-range" min="-100" max="100" value="0">
        </div>
        <div class="mb-3">
          <label for="contrast" class="form-label control-label">Kontrast</label>
          <input id="contrast" type="range" class="form-range" min="-100" max="100" value="0">
        </div>
        <div class="mb-3">
          <label for="saturation" class="form-label control-label">Nasycenie</label>
          <input id="saturation" type="range" class="form-range" min="-100" max="100" value="0">
        </div>
        <div class="mb-3">
          <label for="hue" class="form-label control-label">Odcień (hue)</label>
          <input id="hue" type="range" class="form-range" min="-180" max="180" value="0">
        </div>
        <div class="mb-3">
          <label for="blur" class="form-label control-label">Rozmycie (blur px)</label>
          <input id="blur" type="range" class="form-range" min="0" max="10" value="0">
        </div>

        <div class="d-flex gap-2 mb-3">
          <button id="btnInvert" class="btn btn-outline-primary btn-sm">Negatyw</button>
          <button id="btnSepia" class="btn btn-outline-primary btn-sm">Sepia</button>
          <button id="btnGrayscale" class="btn btn-outline-primary btn-sm">B/W</button>
        </div>

        <div class="mb-3">
          <label for="kernel" class="form-label control-label">Filtr konwolucyjny</label>
          <select id="kernel" class="form-select">
            <option value="none">Brak</option>
            <option value="sharpen">Wyostrz (Sharpen)</option>
            <option value="emboss">Emboss</option>
            <option value="edge">Edge detect</option>
          </select>
        </div>

        <div class="d-flex gap-2">
          <button id="resetBtn" class="btn btn-secondary btn-sm">Resetuj</button>
          <button id="downloadBtn" class="btn btn-success btn-sm">Pobierz PNG</button>
        </div>

        <div class="mt-3 small text-muted">Uwagi: TIFF może wymagać dodatkowego dekodera (UTIF.js). Pobierz/otwórz plik przez górne pole.</div>
      </div>
    </div>

    <div class="col-lg-8">
      <div class="preview-wrap">
        <div class="d-flex justify-content-between align-items-center mb-2">
          <div>
            <strong>Podgląd</strong>
            <div class="small text-muted">(Canvas — zmiany są natychmiastowe)</div>
          </div>
          <div>
            <button class="btn btn-outline-secondary btn-sm" id="fitBtn">Dopasuj do widoku</button>
          </div>
        </div>

        <div class="d-flex flex-column flex-md-row gap-3">
          <div class="flex-grow-1 text-center">
            <canvas id="canvas"></canvas>
          </div>
          <div style="width:220px" class="d-none d-md-block">
            <div class="mb-2"><strong>Historie / miniatury</strong></div>
            <div id="historyList" class="d-flex flex-column gap-2"></div>
          </div>
        </div>

      </div>
    </div>
  </div>    
</div>

<!-- Opcjonalny UTIF.js dla TIFF: ładowany dynamicznie, jeśli potrzeba -->
<script>
  // Global references
  const fileInput = document.getElementById('fileInput');
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');

  // Controls
  const brightnessEl = document.getElementById('brightness');
  const contrastEl = document.getElementById('contrast');
  const saturationEl = document.getElementById('saturation');
  const hueEl = document.getElementById('hue');
  const blurEl = document.getElementById('blur');
  const kernelEl = document.getElementById('kernel');

  const btnInvert = document.getElementById('btnInvert');
  const btnSepia = document.getElementById('btnSepia');
  const btnGrayscale = document.getElementById('btnGrayscale');
  const resetBtn = document.getElementById('resetBtn');
  const downloadBtn = document.getElementById('downloadBtn');
  const fitBtn = document.getElementById('fitBtn');
  const historyList = document.getElementById('historyList');

  let originalImage = null; // Image or ImageData
  let workingImage = null;   // ImageData
  let history = [];

  // Utility: clamp
  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));

  // Load file (including TIFF via UTIF.js when necessary)
  fileInput.addEventListener('change', async (e) => {
    const f = e.target.files[0];
    if (!f) return;
    const name = f.name.toLowerCase();

    if (name.endsWith('.tif') || name.endsWith('.tiff') || f.type === 'image/tiff') {
      // dynamic load UTIF if not present
      if (typeof UTIF === 'undefined') {
        await loadUTIF();
      }
      loadTIFF(f);
    } else {
      loadImageFile(f);
    }
  });

  function loadUTIF() {
    return new Promise((res, rej) => {
      const s = document.createElement('script');
      s.src = 'https://unpkg.com/utif@latest/UTIF.min.js';
      s.onload = () => res();
      s.onerror = (err) => {
        alert('Nie udało się załadować UTIF.js. TIFF może nie być obsługiwany.');
        rej(err);
      };
      document.head.appendChild(s);
    });
  }

  function loadTIFF(file) {
    const reader = new FileReader();
    reader.onload = () => {
      try {
        const arr = new Uint8Array(reader.result);
        const ifds = UTIF.decode(arr);
        UTIF.decodeImage(arr, ifds[0]);
        const rgba = UTIF.toRGBA8(ifds[0]);
        const w = ifds[0].width, h = ifds[0].height;
        canvas.width = w; canvas.height = h;
        const id = ctx.createImageData(w,h);
        id.data.set(rgba);
        ctx.putImageData(id,0,0);
        originalImage = ctx.getImageData(0,0,w,h);
        pushHistory();
      } catch (err) {
        console.error(err);
        alert('Błąd odczytu TIFF: ' + err.message);
      }
    };
    reader.readAsArrayBuffer(file);
  }

  function loadImageFile(file) {
    const url = URL.createObjectURL(file);
    const img = new Image();
    img.onload = () => {
      canvas.width = img.naturalWidth;
      canvas.height = img.naturalHeight;
      ctx.drawImage(img,0,0);
      originalImage = ctx.getImageData(0,0,canvas.width,canvas.height);
      pushHistory();
      URL.revokeObjectURL(url);
    };
    img.onerror = () => { alert('Nie udało się wczytać obrazu.'); URL.revokeObjectURL(url); };
    img.src = url;
  }

  // Apply pipeline based on controls
  function applyAll() {
    if (!originalImage) return;
    // Start from original
    const w = originalImage.width, h = originalImage.height;
    const src = new Uint8ClampedArray(originalImage.data); // copy
    let id = new ImageData(src, w, h);

    // For performance, draw original to an offscreen canvas then use ctx.filter for blur and hue-rotate if supported
    // We'll use a temporary canvas
    const tmp = document.createElement('canvas');
    tmp.width = w; tmp.height = h;
    const tctx = tmp.getContext('2d');

    // Build CSS filter string for blur and hue rotate (if supported). Brightness/contrast/saturate can also be done via ctx.filter
    const blurPx = clamp(parseFloat(blurEl.value), 0, 50);
    const cssFilters = [];
    if (blurPx > 0) cssFilters.push(`blur(${blurPx}px)`);
    // We will still modify brightness/contrast/saturation/hue manually for pixel-perfect control
    tctx.filter = 'none';
    tctx.putImageData(id,0,0);

    // Get back image data (after blur if applied via ctx.filter)
    const afterFilter = tctx.getImageData(0,0,w,h);

    // Pixel manipulation for brightness/contrast/saturation/hue/sepia/invert/grayscale
    const b = parseFloat(brightnessEl.value); // -100..100
    const c = parseFloat(contrastEl.value);   // -100..100
    const s = parseFloat(saturationEl.value); // -100..200
    const hue = parseFloat(hueEl.value);      // -180..180

    // Precompute contrast factor
    const cf = (259 * (c + 255)) / (255 * (259 - c));

    const data = afterFilter.data;
    for (let i=0;i<data.length;i+=4) {
      let r = data[i], g = data[i+1], bl = data[i+2];
      // Brightness
      r = r + b; g = g + b; bl = bl + b;
      // Contrast
      r = cf*(r-128)+128; g = cf*(g-128)+128; bl = cf*(bl-128)+128;
      // Saturation: convert to HSL, adjust S, convert back
      const rgb = rgbToHsl(r,g,bl);
      rgb[1] = clamp(rgb[1] * (1 + s/100), 0, 1);
      // Hue rotate
      rgb[0] = (rgb[0]*360 + hue + 360) % 360 / 360;
      const rgb2 = hslToRgb(rgb[0], rgb[1], rgb[2]);
      r = rgb2[0]; g = rgb2[1]; bl = rgb2[2];

      // Optional presets triggered by buttons set flags on dataset; but also we'll check active classes
      if (btnInvert.classList.contains('active')) {
        r = 255 - r; g = 255 - g; bl = 255 - bl;
      }
      if (btnSepia.classList.contains('active')) {
        // simple sepia
        const rr = 0.393*r + 0.769*g + 0.189*bl;
        const gg = 0.349*r + 0.686*g + 0.168*bl;
        const bb = 0.272*r + 0.534*g + 0.131*bl;
        r = rr; g = gg; bl = bb;
      }
      if (btnGrayscale.classList.contains('active')) {
        const v = 0.299*r + 0.587*g + 0.114*bl;
        r = g = bl = v;
      }

      data[i] = clamp(Math.round(r),0,255);
      data[i+1] = clamp(Math.round(g),0,255);
      data[i+2] = clamp(Math.round(bl),0,255);
    }

    // If a convolution kernel is selected, apply
    const kernel = kernelEl.value;
    let outData = afterFilter;
    if (kernel !== 'none') {
      const kernelMap = {
        sharpen: { size:3, kernel:[0,-1,0,-1,5,-1,0,-1,0], factor:1, offset:0 },
        emboss: { size:3, kernel:[-2,-1,0,-1,1,1,0,1,2], factor:1, offset:128 },
        edge: { size:3, kernel:[-1,-1,-1,-1,8,-1,-1,-1,-1], factor:1, offset:0 }
      };
      const k = kernelMap[kernel];
      outData = convolve(afterFilter, k.kernel, k.size, k.factor, k.offset);
    }

    // Put final data to canvas
    canvas.width = outData.width; canvas.height = outData.height;
    ctx.putImageData(outData,0,0);
    workingImage = ctx.getImageData(0,0,canvas.width,canvas.height);
  }

  // Helper: push snapshot into history (miniaturki)
  function pushHistory() {
    if (!originalImage) return;
    const url = canvas.toDataURL('image/png');
    const img = document.createElement('img');
    img.src = url; img.style.width='100%'; img.style.border='1px solid #e9ecef';
    const wrapper = document.createElement('div');
    wrapper.appendChild(img);
    historyList.prepend(wrapper);
    history.unshift(url);
    if (history.length>6) { history.pop(); if (historyList.children.length>6) historyList.removeChild(historyList.lastChild); }
  }

  // Convolution (simple implementation)
  function convolve(imageData, kernel, size, factor=1, offset=0) {
    const pixels = imageData.data;
    const width = imageData.width;
    const height = imageData.height;
    const output = new ImageData(width, height);
    const out = output.data;
    const half = Math.floor(size/2);
    for (let y=0;y<height;y++){
      for (let x=0;x<width;x++){
        let r=0,g=0,b=0;
        for (let ky=0;ky<size;ky++){
          for (let kx=0;kx<size;kx++){
            const px = x + kx - half;
            const py = y + ky - half;
            if (px>=0 && px<width && py>=0 && py<height) {
              const idx = (py*width + px)*4;
              const kval = kernel[ky*size + kx];
              r += pixels[idx]*kval;
              g += pixels[idx+1]*kval;
              b += pixels[idx+2]*kval;
            }
          }
        }
        const idxOut = (y*width + x)*4;
        out[idxOut] = clamp(Math.round(r/factor + offset),0,255);
        out[idxOut+1] = clamp(Math.round(g/factor + offset),0,255);
        out[idxOut+2] = clamp(Math.round(b/factor + offset),0,255);
        out[idxOut+3] = pixels[idxOut+3];
      }
    }
    return output;
  }

  // RGB <-> HSL helpers (numbers in 0..255 for rgb, 0..1 for h,s,l)
  function rgbToHsl(r,g,b){
    r/=255; g/=255; b/=255;
    const max = Math.max(r,g,b), min = Math.min(r,g,b);
    let h=0, s=0, l=(max+min)/2;
    if (max!==min) {
      const d = max - min;
      s = l>0.5 ? d/(2-max-min) : d/(max+min);
      switch(max){ case r: h = (g-b)/d + (g<b?6:0); break; case g: h = (b-r)/d + 2; break; case b: h = (r-g)/d + 4; break; }
      h /= 6;
    }
    return [h,s,l];
  }
  function hslToRgb(h,s,l){
    let r,g,b;
    if (s===0){ r=g=b = l; }
    else {
      const hue2rgb = (p,q,t) => {
        if (t<0) t+=1; if (t>1) t-=1;
        if (t<1/6) return p + (q-p)*6*t;
        if (t<1/2) return q;
        if (t<2/3) return p + (q-p)*(2/3 - t)*6;
        return p;
      };
      const q = l<0.5 ? l*(1+s) : l+s - l*s;
      const p = 2*l - q;
      r = hue2rgb(p,q,h+1/3);
      g = hue2rgb(p,q,h);
      b = hue2rgb(p,q,h-1/3);
    }
    return [r*255,g*255,b*255];
  }

  function debounce(func, delay) {
    let timer;
    return function(...args) {
      const context = this;
      clearTimeout(timer);
      timer = setTimeout(() => func.apply(context, args), delay);
    };
  }

  // Event listeners for controls
  [brightnessEl, contrastEl, saturationEl, hueEl, blurEl, kernelEl].forEach(el => el.addEventListener('input', debounce(() => { applyAll(); pushHistory(); }, 250)));

  // Preset buttons
  document.querySelectorAll('.preset-btn').forEach(b => b.addEventListener('click', (ev) => {
    const p = b.dataset.preset;
    switch(p) {
      case 'sepia': brightnessEl.value = 0; contrastEl.value=10; saturationEl.value=0; hueEl.value=0; blurEl.value=0; btnSepia.classList.add('active'); btnInvert.classList.remove('active'); btnGrayscale.classList.remove('active'); break;
      case 'negatyw': brightnessEl.value=0; contrastEl.value=0; saturationEl.value=0; hueEl.value=0; blurEl.value=0; btnInvert.classList.add('active'); btnSepia.classList.remove('active'); btnGrayscale.classList.remove('active'); break;
      case 'bw': btnGrayscale.classList.add('active'); btnInvert.classList.remove('active'); btnSepia.classList.remove('active'); break;
      case 'vivid': contrastEl.value=20; saturationEl.value=40; break;
      default: // normal
        brightnessEl.value=0; contrastEl.value=0; saturationEl.value=0; hueEl.value=0; blurEl.value=0; kernelEl.value='none'; btnInvert.classList.remove('active'); btnSepia.classList.remove('active'); btnGrayscale.classList.remove('active');
    }
    applyAll(); pushHistory();
  }));

  // Toggle effect buttons
  [btnInvert, btnSepia, btnGrayscale].forEach(b => b.addEventListener('click', () => { b.classList.toggle('active'); applyAll(); pushHistory(); }));

  // Reset
  resetBtn.addEventListener('click', () => {
    if (!originalImage) return;
    brightnessEl.value=0; contrastEl.value=0; saturationEl.value=0; hueEl.value=0; blurEl.value=0; kernelEl.value='none';
    [btnInvert, btnSepia, btnGrayscale].forEach(x=>x.classList.remove('active'));
    ctx.putImageData(originalImage,0,0);
    workingImage = ctx.getImageData(0,0,canvas.width,canvas.height);
    pushHistory();
  });

  // Download
  downloadBtn.addEventListener('click', () => {
    if (!workingImage && !originalImage) return;
    const dataUrl = canvas.toDataURL('image/png');
    const a = document.createElement('a');
    a.href = dataUrl; a.download = 'edited-image.png';
    document.body.appendChild(a); a.click(); a.remove();
  });

  // Fit button
  fitBtn.addEventListener('click', () => {
    canvas.style.maxWidth = canvas.style.maxWidth ? '' : '100%';
  });

  // Simple keyboard shortcuts
  window.addEventListener('keydown', (e)=>{
    if (e.key === 'z' && (e.ctrlKey||e.metaKey)) { e.preventDefault(); // undo to previous history state if available
      if (history.length>1) {
        history.shift(); const url = history[0]; const img = new Image(); img.onload = ()=>{ canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; ctx.drawImage(img,0,0); workingImage = ctx.getImageData(0,0,canvas.width,canvas.height); }; img.src=url; historyList.removeChild(historyList.firstChild); }
    }
  });

  // Initial empty canvas
  const theme = 'light';
  let fill = null;  
  switch (theme)
  {
      case 'light':    fill = '#f8f9fa'; break;
      case 'golden':   fill = '#f8edda'; break;
      case 'twilight': fill = '#243b53'; break;
      case 'dark':     fill = '#343a40'; break;    
  }
  canvas.width = 800; canvas.height = 500; ctx.fillStyle = fill; ctx.fillRect(0,0,canvas.width,canvas.height);

</script>

Kod po stronie serwera

Brak kodu serwera

Ta aplikacja działa wyłącznie w przeglądarce i nie korzysta z kodu po stronie serwera.

9 listopada 2025 2

Kategorie

Technologie

Dziękujemy!
()

Informacja o cookies

Moja strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania. Nie używam ciasteczek w celach marketingowych ani analitycznych. Korzystając z mojej strony, wyrażasz zgodę na stosowanie tych plików. Możesz dowiedzieć się więcej w mojej polityce prywatności.