Last active
April 30, 2025 23:37
-
-
Save twobob/fff66c909ced33e3c11d378257aa640f to your computer and use it in GitHub Desktop.
Service to remove sepia from open AI pictures. free. Client side
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>DeSepAI Image</title> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js"></script> | |
<style> | |
body { margin:0; padding:1rem; background:#121212; color:#e0e0e0; font-family:sans-serif; display:flex; justify-content:center; } | |
.container { background:#1e1e1e; padding:1.5rem; border-radius:6px; width:100%; max-width:800px; position:relative; } | |
.header-row { display:flex; justify-content:space-between; flex-wrap:wrap; gap:1rem; } | |
.header-left h1 { margin:0; font-size:1.25rem; } | |
.header-left p { margin:0.25rem 0 0; font-size:0.9rem; color:#bbb; } | |
.header-right { text-align:right; } | |
.file-btn{background:#6200ee;color:#fff;padding:.5rem 1rem;border:none;border-radius:4px;cursor:pointer;display:inline-block;} | |
.file-btn input{display:none;} | |
.hint{font-size:.8rem;color:#888;margin-top:.5rem;} | |
.controls{display:flex;gap:.5rem;flex-wrap:wrap;margin:1rem 0;} | |
.controls>*{flex:1;min-width:100px;} | |
.controls button, .controls input{padding:.5rem;border:none;border-radius:4px;font-size:.9rem;width:90%;} | |
.controls button{background:#6200ee;color:#fff;cursor:pointer;} | |
.controls input[type=range]{background:#333;height:4px;margin-top:.5rem;-webkit-appearance:none;} | |
.controls label{display:block;font-size:.85rem;margin-bottom:.2rem;} | |
.progress-info{font-size:.85rem;color:#ccc;margin-bottom:.5rem;min-height:1em;} | |
#fileProgress, #zipProgress{width:100%; margin-bottom:1rem;} | |
.info-bar{display:flex;gap:1rem;align-items:center;font-size:.85rem;margin-top:1rem;} | |
.info-swatch{width:24px;height:24px;border:1px solid #888;border-radius:4px;} | |
.previews{display:flex;flex-direction:column;gap:1rem;} @media(min-width:480px){.previews{flex-direction:row;}} | |
.preview-pane{position:relative;flex:1;} | |
.previews img{width:100%;border:1px solid #333;border-radius:4px;cursor:pointer;object-fit:contain;} | |
.nav-btn{position:absolute;top:50%;transform:translateY(-50%);background:rgba(0,0,0,0.5);color:#fff;border:none;border-radius:50%;width:32px;height:32px;cursor:pointer;font-size:1.2rem;line-height:32px;text-align:center;} | |
.nav-prev{left:8px;} .nav-next{right:8px;} | |
.download-container{text-align:right;margin-top:.5rem;} | |
.download-container a{background:#03dac6;color:#000;padding:.5rem 1rem;border-radius:4px;text-decoration:none;display:inline-block;font-size:.9rem;} | |
.magnifier{display:none;position:absolute;width:100px;height:100px;border:2px solid #fff;border-radius:50%;overflow:hidden;pointer-events:none;box-shadow:0 0 8px rgba(0,0,0,0.5);background-repeat:no-repeat;} | |
canvas{display:none;} | |
.drop-overlay{position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(255,255,255,0.1);border:2px dashed #555;border-radius:6px;display:none;align-items:center;justify-content:center;color:#bbb;font-size:1rem;pointer-events:none;} | |
</style> | |
</head> | |
<body> | |
<div class="container" id="container"> | |
<div class="header-row"> | |
<div class="header-left"> | |
<h1>DeSepAI Image</h1> | |
<p>Upload or batch process images, adjust brightness, detect/sample white point.</p> | |
</div> | |
<div class="header-right"> | |
<label class="file-btn">Browse<input type="file" id="upload" accept="image/*" multiple></label> | |
<div class="hint">or drag & drop, or paste an image</div> | |
</div> | |
</div> | |
<div class="controls"> | |
<label>Strength: <span id="strengthValue">100%</span><input type="range" id="strength" min="0" max="300" value="100"></label> | |
<button id="detectBalance">Detect Balance</button> | |
<button id="zipDownload" style="display:none;">Download All ZIP</button> | |
</div> | |
<div class="progress-info" id="progressInfo"></div> | |
<progress id="fileProgress" max="100" value="0" style="display:none;"></progress> | |
<progress id="zipProgress" max="100" value="0" style="display:none;"></progress> | |
<div class="info-bar"> | |
<div>Detected RGB: <span id="detectedRGB">[9,15,27]</span></div> | |
<div id="rgbSwatch" class="info-swatch" style="background:rgb(246,240,228);"></div> | |
</div> | |
<div class="previews"> | |
<div class="preview-pane"> | |
<button class="nav-btn nav-prev" id="prevBtn">‹</button> | |
<img id="originalPreview" alt="Original"> | |
<button class="nav-btn nav-next" id="nextBtn">›</button> | |
<div id="magnifier" class="magnifier"></div> | |
</div> | |
<div class="preview-pane"> | |
<img id="processedPreview" alt="Processed"> | |
<div class="download-container"><a id="downloadLink" download="shifted.png">Download</a></div> | |
</div> | |
</div> | |
<canvas id="canvas"></canvas> | |
<canvas id="origCanvas"></canvas> | |
<div id="dropOverlay" class="drop-overlay">Drop image to upload</div> | |
</div> | |
<script> | |
let images = [], current = 0, shiftBase = [9,15,27], currentImgObj = null, processedCache = []; | |
const zoom = 4, magSize = 100, magRadius = magSize/2; | |
const container = document.getElementById('container'), | |
upload = document.getElementById('upload'), | |
strength = document.getElementById('strength'), | |
strengthValue = document.getElementById('strengthValue'), | |
detectBtn = document.getElementById('detectBalance'), | |
zipBtn = document.getElementById('zipDownload'), | |
progressInfo = document.getElementById('progressInfo'), | |
fileProgress = document.getElementById('fileProgress'), | |
zipProgress = document.getElementById('zipProgress'), | |
origCanvas = document.getElementById('origCanvas'), origCtx = origCanvas.getContext('2d'), | |
canvas = document.getElementById('canvas'), ctx = canvas.getContext('2d'), | |
origPreview = document.getElementById('originalPreview'), | |
processedPreview = document.getElementById('processedPreview'), | |
downloadLink = document.getElementById('downloadLink'), | |
detectedRGB = document.getElementById('detectedRGB'), | |
rgbSwatch = document.getElementById('rgbSwatch'), | |
magnifier = document.getElementById('magnifier'); | |
function updateDisplay(sample, sb) { | |
detectedRGB.textContent = `[${Math.round(sb[0])},${Math.round(sb[1])},${Math.round(sb[2])}]`; | |
rgbSwatch.style.backgroundColor = `rgb(${sample[0]},${sample[1]},${sample[2]})`; | |
} | |
origPreview.addEventListener('click', e => { | |
const rect = origPreview.getBoundingClientRect(); | |
const x = Math.floor((e.clientX - rect.left) * (origCanvas.width / rect.width)); | |
const y = Math.floor((e.clientY - rect.top) * (origCanvas.height / rect.height)); | |
const p = origCtx.getImageData(x, y, 1, 1).data; | |
shiftBase = [255 - p[0], 255 - p[1], 255 - p[2]]; | |
updateDisplay([p[0], p[1], p[2]], shiftBase); | |
processedCache[current] = null; | |
const dataUrl = applyShiftToCanvas(); | |
processedCache[current] = dataUrl; | |
processedPreview.src = dataUrl; | |
downloadLink.href = dataUrl; | |
downloadLink.style.display = 'inline-block'; | |
}); | |
function computeShiftBase() { | |
const perc = 0.9; | |
const data = origCtx.getImageData(0, 0, origCanvas.width, origCanvas.height).data; | |
const lum = []; | |
for (let i = 0; i < data.length; i += 4) lum.push(0.2126 * data[i] + 0.7152 * data[i+1] + 0.0722 * data[i+2]); | |
const sorted = lum.slice().sort((a, b) => a - b); | |
const thresh = sorted[Math.floor(sorted.length * perc)]; | |
let sum = [0, 0, 0], count = 0; | |
lum.forEach((l, i) => { if (l >= thresh) { sum[0] += data[i*4]; sum[1] += data[i*4+1]; sum[2] += data[i*4+2]; count++; }}); | |
const avg = count ? sum.map(v => v / count) : [255,255,255]; | |
shiftBase = [255 - avg[0], 255 - avg[1], 255 - avg[2]]; | |
updateDisplay(avg, shiftBase); | |
} | |
detectBtn.addEventListener('click', () => { | |
computeShiftBase(); | |
const dataUrl = applyShiftToCanvas(); | |
processedCache[current] = dataUrl; | |
processedPreview.src = dataUrl; | |
downloadLink.href = dataUrl; | |
downloadLink.style.display = 'inline-block'; | |
}); | |
function applyShiftToCanvas() { | |
const scale = strength.value / 100; | |
const preserveDark = scale > 0.5; | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
ctx.drawImage(currentImgObj, 0, 0); | |
const d = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
for (let i = 0; i < d.data.length; i += 4) { | |
const r = d.data[i], g = d.data[i+1], b = d.data[i+2]; | |
const bri = 0.2126*r + 0.7152*g + 0.0722*b; | |
const att = preserveDark ? Math.min(1, bri / 128) : 1; | |
d.data[i] = Math.min(255, r + shiftBase[0] * scale * att); | |
d.data[i+1] = Math.min(255, g + shiftBase[1] * scale * att); | |
d.data[i+2] = Math.min(255, b + shiftBase[2] * scale * att); | |
} | |
ctx.putImageData(d, 0, 0); | |
return canvas.toDataURL(); | |
} | |
function loadCurrent() { | |
document.getElementById('prevBtn').style.display = current > 0 ? 'block' : 'none'; | |
document.getElementById('nextBtn').style.display = current < images.length - 1 ? 'block' : 'none'; | |
zipBtn.style.display = images.length > 1 ? 'block' : 'none'; | |
origPreview.src = ''; | |
processedPreview.src = ''; | |
fileProgress.style.display = 'none'; | |
zipProgress.style.display = 'none'; | |
const src = images[current]; | |
progressInfo.textContent = `Loading image ${current+1} of ${images.length}...`; | |
currentImgObj = new Image(); | |
currentImgObj.onload = () => { | |
origCanvas.width = canvas.width = currentImgObj.width; | |
origCanvas.height = canvas.height = currentImgObj.height; | |
origCtx.drawImage(currentImgObj, 0, 0); | |
origPreview.src = origCanvas.toDataURL(); | |
if (processedCache[current]) { | |
processedPreview.src = processedCache[current]; | |
downloadLink.href = processedCache[current]; | |
downloadLink.style.display = 'inline-block'; | |
progressInfo.textContent = ''; | |
} else { | |
progressInfo.textContent = `Applying filter to image ${current+1} of ${images.length}...`; | |
setTimeout(() => { | |
const dataUrl = applyShiftToCanvas(); | |
processedCache[current] = dataUrl; | |
processedPreview.src = dataUrl; | |
downloadLink.href = dataUrl; | |
downloadLink.style.display = 'inline-block'; | |
progressInfo.textContent = ''; | |
}, 50); | |
} | |
}; | |
currentImgObj.src = src; | |
} | |
async function readFiles(files) { | |
images = []; | |
processedCache = []; | |
current = 0; | |
fileProgress.value = 0; | |
fileProgress.style.display = 'block'; | |
progressInfo.textContent = `Reading file 1 of ${files.length}`; | |
for (let i = 0; i < files.length; i++) { | |
await new Promise(resolve => { | |
const reader = new FileReader(); | |
reader.onprogress = ev => { if (ev.lengthComputable) fileProgress.value = (ev.loaded / ev.total) * 100; }; | |
reader.onload = () => { images.push(reader.result); progressInfo.textContent = `Reading file ${i+1} of ${files.length}`; resolve(); }; | |
reader.readAsDataURL(files[i]); | |
}); | |
} | |
fileProgress.style.display = 'none'; | |
progressInfo.textContent = `Loaded ${images.length} images`; | |
loadCurrent(); | |
} | |
async function zipAll() { | |
zipProgress.value = 0; | |
zipProgress.style.display = 'block'; | |
progressInfo.textContent = `Preparing ZIP of ${images.length} images...`; | |
const zip = new JSZip(); | |
for (let i = 0; i < images.length; i++) { | |
progressInfo.textContent = `Zipping image ${i+1} of ${images.length}...`; | |
if (!processedCache[i]) { | |
current = i; | |
loadCurrent(); | |
await new Promise(r => { currentImgObj.onload = () => setTimeout(r, 50); }); | |
} | |
const data = processedCache[i].split(',')[1]; | |
zip.file(`image_${i+1}.png`, data, { base64:true }); | |
} | |
progressInfo.textContent = 'Generating ZIP...'; | |
zip.generateAsync({ type:'blob' }, meta => { | |
zipProgress.value = meta.percent; | |
progressInfo.textContent = `ZIP ${meta.percent.toFixed(0)}%`; | |
}).then(blob => { | |
progressInfo.textContent = 'ZIP complete'; | |
zipProgress.style.display = 'none'; | |
const link = document.createElement('a'); | |
link.href = URL.createObjectURL(blob); | |
link.download = 'images.zip'; | |
link.click(); | |
}); | |
} | |
document.addEventListener('DOMContentLoaded', () => { | |
const defaults = ['test1.png', 'test2.png', 'test3.png', 'test4.png', 'test5.png', 'test6.png']; | |
const choice = defaults[Math.floor(Math.random() * defaults.length)]; | |
progressInfo.textContent = `Loading default image: ${choice}`; | |
fetch(choice) | |
.then(r => r.blob()) | |
.then(blob => new Promise(res => { | |
const reader = new FileReader(); | |
reader.onload = () => res(reader.result); | |
reader.readAsDataURL(blob); | |
})) | |
.then(dataUrl => { | |
images = [dataUrl]; | |
processedCache = []; | |
current = 0; | |
loadCurrent(); | |
}) | |
.catch(err => { | |
progressInfo.textContent = 'Failed to load default image'; | |
console.error(err); | |
}); | |
}); | |
upload.addEventListener('change', e => { if (e.target.files.length) readFiles(e.target.files); }); | |
['dragenter','dragover'].forEach(evt => container.addEventListener(evt, e => { e.preventDefault(); dropOverlay.style.display='flex'; })); | |
['dragleave','drop'].forEach(evt => container.addEventListener(evt, e => { e.preventDefault(); dropOverlay.style.display='none'; })); | |
container.addEventListener('drop', e => { if (e.dataTransfer.files.length) readFiles(e.dataTransfer.files); }); | |
document.addEventListener('paste', e => { for (const it of e.clipboardData.items) if (it.type.includes('image')) { readFiles([it.getAsFile()]); break; }}); | |
strength.addEventListener('input', () => strengthValue.textContent = `${strength.value}%`); | |
strength.addEventListener('change', () => { | |
const dataUrl = applyShiftToCanvas(); | |
processedCache[current] = dataUrl; | |
processedPreview.src = dataUrl; | |
downloadLink.href = dataUrl; | |
downloadLink.style.display = 'inline-block'; | |
}); | |
zipBtn.addEventListener('click', zipAll); | |
document.getElementById('prevBtn').onclick = () => { if (current > 0) { current--; loadCurrent(); }}; | |
document.getElementById('nextBtn').onclick = () => { if (current < images.length - 1) { current++; loadCurrent(); }}; | |
origPreview.addEventListener('mouseenter', () => magnifier.style.display='block'); | |
origPreview.addEventListener('mouseleave', () => magnifier.style.display='none'); | |
origPreview.addEventListener('mousemove', e => { | |
const rect = origPreview.getBoundingClientRect(); | |
const x = (e.clientX - rect.left) * (origCanvas.width / rect.width); | |
const y = (e.clientY - rect.top) * (origCanvas.height / rect.height); | |
magnifier.style.left = `${e.clientX - rect.left - magRadius}px`; | |
magnifier.style.top = `${e.clientY - rect.top - magRadius}px`; | |
magnifier.style.backgroundImage = `url('${origCanvas.toDataURL()}')`; | |
magnifier.style.backgroundSize = `${origCanvas.width*zoom}px ${origCanvas.height*zoom}px`; | |
magnifier.style.backgroundPosition = `${-x*zoom+magRadius}px ${-y*zoom+magRadius}px`; | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment