Edytor Zdjęć Online
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
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.