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.