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.

Licencja

## BSD-3-Clause License Agreement
BSD-3-Clause
Сopyright (c) 2026 Dariusz Rorat

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
   contributors may be used to endorse or promote products derived from
   this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
9 listopada 2025 4

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.