/* editor.js — UI controller for the Raw Editor WebApp. Wires the Camera-Raw style controls to the WebGL develop pipeline, handles file loading (ArozOS input files, file picker, drag & drop), the live histogram, LUT loading, auto white-balance / auto tone, and saving the developed image back to the user's storage as a JPEG. */ (function () { "use strict"; var renderer = null; var glOK = true; var decoded = null; // last decoded {data,width,height,meta,source} var sourceFile = null; // {filename, filepath} of the opened file var lut = null; // parsed LUT var renderQueued = false; var defaults = { temperature: 5500, tint: 0, exposure: 0, contrast: 0, highlights: 0, shadows: 0, whites: 0, blacks: 0, texture: 0, clarity: 0, dehaze: 0, vibrance: 0, saturation: 0, vignette: 0, grain: 0, lutAmount: 100 }; var state = Object.assign({}, defaults); state.baseTemp = 5500; state.treatment = "color"; state.lutEnabled = true; // Per-group bypass ("eye") toggles. state.groupOn = { light: true, color: true, effects: true, lut: true }; // ---- init WebGL ------------------------------------------------------ try { renderer = GLRender.create(document.getElementById("view")); } catch (e) { glOK = false; showFatal(e.message); } // ===================================================================== // Slider wiring // ===================================================================== function clampToRange(el, v) { var min = parseFloat(el.dataset.min), max = parseFloat(el.dataset.max); if (v < min) v = min; if (v > max) v = max; return v; } function initSliders() { document.querySelectorAll(".slider").forEach(function (row) { var key = row.dataset.key; var range = row.querySelector("input[type=range]"); var num = row.querySelector(".s-val"); var step = row.dataset.step || "1"; range.min = row.dataset.min; range.max = row.dataset.max; range.step = step; num.min = row.dataset.min; num.max = row.dataset.max; num.step = step; var def = parseFloat(row.dataset.def); setSlider(row, def); range.addEventListener("input", function () { var v = parseFloat(range.value); num.value = fmt(v, step); state[key] = v; scheduleRender(); }); num.addEventListener("change", function () { var v = parseFloat(num.value); if (isNaN(v)) v = parseFloat(row.dataset.def); v = clampToRange(row, v); num.value = fmt(v, step); range.value = v; state[key] = v; scheduleRender(); }); // double click resets one slider to its default row.querySelector(".s-name").addEventListener("dblclick", function () { setSlider(row, parseFloat(row.dataset.def)); state[key] = parseFloat(row.dataset.def); scheduleRender(); }); }); } function fmt(v, step) { return (parseFloat(step) < 1) ? (Math.round(v * 100) / 100).toString() : Math.round(v).toString(); } function setSlider(row, v) { var range = row.querySelector("input[type=range]"); var num = row.querySelector(".s-val"); range.value = v; num.value = fmt(v, row.dataset.step || "1"); } function setSliderByKey(key, v) { var row = document.querySelector('.slider[data-key="' + key + '"]'); if (row) setSlider(row, v); state[key] = v; } // ===================================================================== // Render // ===================================================================== function scheduleRender() { if (renderQueued || !renderer || !decoded) return; renderQueued = true; requestAnimationFrame(function () { renderQueued = false; doRender(); }); } function getParams() { var g = state.groupOn; var p = { baseTemp: state.baseTemp, // Color group temperature: g.color ? state.temperature : state.baseTemp, tint: g.color ? state.tint : 0, vibrance: g.color ? state.vibrance : 0, saturation: g.color ? state.saturation : 0, // Light group exposure: g.light ? state.exposure : 0, contrast: g.light ? state.contrast : 0, highlights: g.light ? state.highlights : 0, shadows: g.light ? state.shadows : 0, whites: g.light ? state.whites : 0, blacks: g.light ? state.blacks : 0, // Effects group texture: g.effects ? state.texture : 0, clarity: g.effects ? state.clarity : 0, dehaze: g.effects ? state.dehaze : 0, vignette: g.effects ? state.vignette : 0, grain: g.effects ? state.grain : 0, // LUT group lutEnabled: !!(lut && state.lutEnabled && g.lut), lutAmount: state.lutAmount / 100 }; if (state.treatment === "bw") { p.saturation = -100; p.vibrance = 0; } return p; } function doRender() { if (!renderer || !decoded) return; renderer.render(getParams()); updateHistogram(); updateFilmstrip(); } // ===================================================================== // Histogram // ===================================================================== var histSmall = document.createElement("canvas"); histSmall.width = 252; histSmall.height = 84; var histSmallCtx = histSmall.getContext("2d"); function updateHistogram() { var view = document.getElementById("view"); var hc = document.getElementById("histogram"); var ctx = hc.getContext("2d"); ctx.clearRect(0, 0, hc.width, hc.height); if (!decoded) return; try { histSmallCtx.drawImage(view, 0, 0, histSmall.width, histSmall.height); } catch (e) { return; } var img = histSmallCtx.getImageData(0, 0, histSmall.width, histSmall.height).data; var r = new Uint32Array(256), g = new Uint32Array(256), b = new Uint32Array(256); for (var i = 0; i < img.length; i += 4) { r[img[i]]++; g[img[i + 1]]++; b[img[i + 2]]++; } var max = 1; for (var k = 1; k < 255; k++) { // ignore pure black/white spikes for scaling if (r[k] > max) max = r[k]; if (g[k] > max) max = g[k]; if (b[k] > max) max = b[k]; } drawChannel(ctx, r, max, "rgba(255,80,80,0.75)"); drawChannel(ctx, g, max, "rgba(90,220,90,0.75)"); drawChannel(ctx, b, max, "rgba(90,140,255,0.75)"); // Clipping indicators (fraction of pixels pinned to 0 / 255). var totalPx = histSmall.width * histSmall.height; var hi = Math.max(r[255], g[255], b[255]); var lo = Math.max(r[0], g[0], b[0]); var ch = document.getElementById("clipHigh"); var cs = document.getElementById("clipShadow"); if (ch) ch.classList.toggle("active-high", hi / totalPx > 0.01); if (cs) cs.classList.toggle("active-shadow", lo / totalPx > 0.01); } // Small preview thumbnail in the bottom filmstrip. function updateFilmstrip() { var fs = document.getElementById("filmstrip"); var v = document.getElementById("view"); if (!fs || !v.width) return; var ctx = fs.getContext("2d"); ctx.fillStyle = "#111"; ctx.fillRect(0, 0, fs.width, fs.height); var s = Math.min(fs.width / v.width, fs.height / v.height); var w = v.width * s, h = v.height * s; try { ctx.drawImage(v, (fs.width - w) / 2, (fs.height - h) / 2, w, h); } catch (e) { /* ignore */ } } function drawChannel(ctx, arr, max, color) { var w = ctx.canvas.width, h = ctx.canvas.height; ctx.globalCompositeOperation = "lighter"; ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(0, h); for (var x = 0; x < 256; x++) { var v = Math.min(1, arr[x] / max); var px = (x / 255) * w; var py = h - v * (h - 2); ctx.lineTo(px, py); } ctx.lineTo(w, h); ctx.closePath(); ctx.fill(); ctx.globalCompositeOperation = "source-over"; } // ===================================================================== // File loading // ===================================================================== function showLoader(text) { document.getElementById("loaderText").textContent = text || "Working..."; document.getElementById("loader").style.display = "flex"; } function hideLoader() { document.getElementById("loader").style.display = "none"; } function showFatal(msg) { var dh = document.getElementById("dropHint"); if (dh) dh.innerHTML = '
' + escapeHtml(msg) + "
"; } function escapeHtml(s) { return String(s).replace(/[&<>"]/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """ }[c]; }); } function loadFromPath(filepath, filename) { if (!glOK) return; sourceFile = { filepath: filepath, filename: filename }; showLoader("Reading file..."); document.getElementById("dropHint").style.display = "none"; var url = "../media?file=" + encodeURIComponent(filepath); fetch(url).then(function (resp) { if (!resp.ok) throw new Error("Could not read file (HTTP " + resp.status + ")"); return resp.arrayBuffer(); }).then(function (buf) { showLoader("Decoding " + (filename || "image") + " ..."); // Defer so the loader paints before the heavy decode. setTimeout(function () { decodeBuffer(buf, filename); }, 30); }).catch(function (err) { hideLoader(); showFatal(err.message || String(err)); }); } function loadFromArrayBuffer(buf, filename) { showLoader("Decoding " + (filename || "image") + " ..."); document.getElementById("dropHint").style.display = "none"; setTimeout(function () { decodeBuffer(buf, filename); }, 30); } function decodeBuffer(buf, filename) { RawDecoder.decode(buf, filename).then(function (res) { decoded = res; renderer.setImage(res); document.getElementById("view").style.display = "block"; applyMeta(res.meta, filename, res); resetAll(true); fitToWindow(); hideLoader(); }).catch(function (err) { hideLoader(); showFatal(err.message || String(err)); }); } function applyMeta(meta, filename, res) { state.baseTemp = (meta && meta.temp) ? meta.temp : 5500; defaults.temperature = state.baseTemp; document.getElementById("fileTitle").textContent = (filename || "Untitled") + (meta && meta.camera ? " — " + meta.camera : ""); // EXIF line (set before the title call so nothing can block it) setText("exifShutter", meta && meta.shutter ? formatShutter(meta.shutter) : "--"); setText("exifAperture", meta && meta.aperture ? "f/" + round1(meta.aperture) : "--"); setText("exifIso", meta && meta.iso ? "ISO " + meta.iso : "--"); setText("exifFocal", meta && meta.focal ? Math.round(meta.focal) + " mm" : "--"); try { ao_module_setWindowTitle("Raw Editor - " + (filename || "Untitled")); } catch (e) { /* ignore */ } // Status line var srcLabel = { "raw-demosaic": "RAW demosaiced", "embedded-preview": "Embedded preview", "image": "Image" }[res.source] || res.source; setText("statusInfo", srcLabel + " · " + res.width + " x " + res.height + " px"); } function formatShutter(t) { if (t >= 1) return round1(t) + " s"; return "1/" + Math.round(1 / t) + " s"; } function round1(v) { return Math.round(v * 10) / 10; } function setText(id, t) { var e = document.getElementById(id); if (e) e.textContent = t; } // ===================================================================== // White balance presets + auto // ===================================================================== var wbPresets = { daylight: 5500, cloudy: 6500, shade: 7500, tungsten: 2850, fluorescent: 3800 }; document.getElementById("wbPreset").addEventListener("change", function () { var v = this.value; if (v === "asshot") { setSliderByKey("temperature", state.baseTemp); setSliderByKey("tint", 0); } else if (v === "auto") { autoWhiteBalance(); } else if (wbPresets[v] != null) { setSliderByKey("temperature", wbPresets[v]); setSliderByKey("tint", 0); } scheduleRender(); }); // When temp/tint are edited manually flip the preset to Custom. ["temperature", "tint"].forEach(function (key) { var row = document.querySelector('.slider[data-key="' + key + '"]'); row.querySelector("input[type=range]").addEventListener("input", function () { document.getElementById("wbPreset").value = "custom"; }); }); function autoWhiteBalance() { if (!decoded) return; var d = decoded.data, n = d.length; var sr = 0, sg = 0, sb = 0, cnt = 0; var stride = Math.max(4, Math.floor(n / 4 / 40000) * 4); for (var i = 0; i < n; i += stride) { sr += d[i]; sg += d[i + 1]; sb += d[i + 2]; cnt++; } var ar = sr / cnt, ag = sg / cnt, ab = sb / cnt; if (ar <= 0 || ab <= 0) return; // Solve kelvinGain to equalise R and B: (1+0.9wr)/(1-0.9wr) = ab/ar. var ratio = ab / ar; var wr = (ratio - 1) / (0.9 * (ratio + 1)); wr = Math.max(-1, Math.min(1, wr)); var temp = state.baseTemp * Math.exp(wr * (Math.log(50000) - Math.log(2000))); temp = Math.max(2000, Math.min(50000, temp)); // Tint: green vs magenta balance. var tint = ((ar + ab) / 2 - ag) / ((ar + ab) / 2 + ag) * 150; tint = Math.max(-150, Math.min(150, tint)); setSliderByKey("temperature", Math.round(temp)); setSliderByKey("tint", Math.round(tint)); } // ===================================================================== // Auto tone / reset // ===================================================================== document.getElementById("btnAuto").addEventListener("click", function () { autoTone(); scheduleRender(); }); document.getElementById("btnDefault").addEventListener("click", function () { ["exposure", "contrast", "highlights", "shadows", "whites", "blacks"].forEach(function (k) { setSliderByKey(k, defaults[k]); }); scheduleRender(); }); function autoTone() { if (!decoded) return; var d = decoded.data, n = d.length; var sum = 0, cnt = 0, hiClip = 0, loClip = 0; var stride = Math.max(4, Math.floor(n / 4 / 40000) * 4); for (var i = 0; i < n; i += stride) { var lum = Math.pow(Math.max(0, 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2]), 1 / 2.2); sum += lum; cnt++; if (lum > 0.96) hiClip++; if (lum < 0.03) loClip++; } var mean = sum / cnt; var exposure = Math.log(0.46 / Math.max(0.03, mean)) / Math.log(2); exposure = Math.max(-2.5, Math.min(2.5, exposure)); setSliderByKey("exposure", Math.round(exposure * 100) / 100); setSliderByKey("contrast", 8); setSliderByKey("highlights", hiClip / cnt > 0.02 ? -35 : -10); setSliderByKey("shadows", loClip / cnt > 0.02 ? 35 : 12); setSliderByKey("whites", 8); setSliderByKey("blacks", -6); } function resetAll(keepImage) { Object.keys(defaults).forEach(function (k) { var def = (k === "temperature") ? state.baseTemp : defaults[k]; setSliderByKey(k, def); }); state.treatment = "color"; document.getElementById("btnBW").classList.remove("active"); document.getElementById("wbPreset").value = "asshot"; if (keepImage) scheduleRender(); } document.getElementById("btnReset").addEventListener("click", function () { resetAll(true); }); // Treatment (B&W) toggle in the Edit header document.getElementById("btnBW").addEventListener("click", function () { state.treatment = (state.treatment === "bw") ? "color" : "bw"; this.classList.toggle("active", state.treatment === "bw"); scheduleRender(); }); // Auto white balance eyedropper document.getElementById("btnEyedrop").addEventListener("click", function () { autoWhiteBalance(); document.getElementById("wbPreset").value = "auto"; scheduleRender(); }); // ===================================================================== // Collapsible groups (chevron) + per-group bypass (eye) // ===================================================================== document.querySelectorAll(".group-head").forEach(function (head) { var group = head.parentElement; head.addEventListener("click", function (e) { if (e.target.classList.contains("eye-toggle")) return; group.classList.toggle("collapsed"); }); var eye = head.querySelector(".eye-toggle"); if (eye) { eye.addEventListener("click", function (e) { e.stopPropagation(); var key = group.dataset.group; state.groupOn[key] = !state.groupOn[key]; group.classList.toggle("bypassed", !state.groupOn[key]); eye.className = state.groupOn[key] ? "eye icon eye-toggle" : "eye slash icon eye-toggle"; scheduleRender(); }); } }); // ===================================================================== // LUT (library folder in the user's ArozOS storage + local import) // ===================================================================== var LUT_ROOT = "user:/RawEditor"; // parent folder var LUT_DIR = "user:/RawEditor/LUTs"; // where .cube files live function inDesktop() { return (typeof ao_module_virtualDesktop !== "undefined" && ao_module_virtualDesktop) || window.parent !== window; } // --- minimal ArozOS file-system helpers --- function fsPost(url, body) { return fetch(url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: body }); } function fsListDir(dir) { return fsPost("../system/file_system/listDir", "dir=" + encodeURIComponent(dir)).then(function (r) { return r.json(); }); } function fsCSRF() { return fetch("../system/csrf/new").then(function (r) { return r.text(); }); } function fsNewFolder(src, name) { return fsCSRF().then(function (token) { return fsPost("../system/file_system/newItem", "type=folder&src=" + encodeURIComponent(src) + "&filename=" + encodeURIComponent(name) + "&csrft=" + encodeURIComponent(token)); }); } function applyLut(parsed, displayName) { lut = parsed; renderer.setLUT(lut); state.lutEnabled = true; var g = document.querySelector('.group[data-group="lut"]'); if (g) { g.classList.remove("bypassed"); } state.groupOn.lut = true; document.getElementById("lutEnabled").checked = true; document.getElementById("lutInfo").style.display = "block"; document.getElementById("lutName").textContent = (parsed.title ? parsed.title + " " : "") + displayName + " (" + parsed.size + "³)"; scheduleRender(); } // Load a LUT stored in the user's storage by virtual path. function loadLutFromPath(filepath, name) { fetch("../media?file=" + encodeURIComponent(filepath)).then(function (r) { if (!r.ok) throw new Error("Could not read file (HTTP " + r.status + ")"); return r.text(); }).then(function (text) { applyLut(LUTParser.parse(text), name); }).catch(function (e) { alert("Could not load LUT: " + e.message); }); } // Populate the library dropdown from a listDir result. function populateLutLibrary(list) { var sel = document.getElementById("lutLibrary"); var hint = document.getElementById("lutEmpty"); var current = sel.value; sel.innerHTML = ''; var cubes = (list || []).filter(function (f) { return !f.IsDir && /\.cube$/i.test(f.Filename); }); cubes.sort(function (a, b) { return a.Filename.toLowerCase() < b.Filename.toLowerCase() ? -1 : 1; }); cubes.forEach(function (f) { var o = document.createElement("option"); o.value = f.Filepath; o.textContent = f.Filename.replace(/\.cube$/i, ""); sel.appendChild(o); }); if (current) sel.value = current; if (hint) { hint.textContent = cubes.length ? (cubes.length + " LUT" + (cubes.length > 1 ? "s" : "") + " in library") : "No LUTs yet — add .cube files to user:/RawEditor/LUTs or import one below."; } } // List the LUT folder, creating it (and its parent) on first use. function refreshLutLibrary() { if (!inDesktop()) { var hint = document.getElementById("lutEmpty"); if (hint) hint.textContent = "Library needs the ArozOS desktop — use Import below."; return; } fsListDir(LUT_DIR).then(function (list) { if (list && list.error) { // Folder missing — create parent then LUT folder, then retry. return fsNewFolder("user:/", "RawEditor").catch(function () { }).then(function () { return fsNewFolder(LUT_ROOT + "/", "LUTs").catch(function () { }); }).then(function () { return fsListDir(LUT_DIR); }); } return list; }).then(function (list) { populateLutLibrary(Array.isArray(list) ? list : []); }).catch(function () { var hint = document.getElementById("lutEmpty"); if (hint) hint.textContent = "Could not read the LUT folder."; }); } document.getElementById("lutLibrary").addEventListener("change", function () { if (!this.value) return; loadLutFromPath(this.value, this.options[this.selectedIndex].text); }); document.getElementById("btnLutRefresh").addEventListener("click", refreshLutLibrary); // Import: load a local .cube immediately and (in desktop) save it to the library. document.getElementById("btnLoadLut").addEventListener("click", function () { document.getElementById("lutFile").click(); }); document.getElementById("lutFile").addEventListener("change", function () { var f = this.files[0]; this.value = ""; if (!f) return; var reader = new FileReader(); reader.onload = function () { var parsed; try { parsed = LUTParser.parse(reader.result); } catch (e) { alert("Could not load LUT: " + e.message); return; } applyLut(parsed, f.name.replace(/\.cube$/i, "")); // Persist into the library so it shows up next time. if (inDesktop() && typeof ao_module_uploadFile === "function") { fsNewFolder("user:/", "RawEditor").catch(function () { }).then(function () { return fsNewFolder(LUT_ROOT + "/", "LUTs").catch(function () { }); }).then(function () { ao_module_uploadFile(f, LUT_DIR, function () { refreshLutLibrary(); }); }); } }; reader.readAsText(f); }); document.getElementById("lutEnabled").addEventListener("change", function () { state.lutEnabled = this.checked; scheduleRender(); }); document.getElementById("btnClearLut").addEventListener("click", function () { lut = null; renderer.setLUT(null); document.getElementById("lutInfo").style.display = "none"; document.getElementById("lutLibrary").value = ""; scheduleRender(); }); // ===================================================================== // Open / drag & drop // ===================================================================== function openPicker() { if (typeof ao_module_openFileSelector === "function" && window.parent !== window) { ao_module_openFileSelector(function (files) { if (files && files.length) loadFromPath(files[0].filepath, files[0].filename); }, "user:/", "file", false); } else { var inp = document.createElement("input"); inp.type = "file"; inp.accept = ".arw,.dng,.nef,.cr2,.cr3,.orf,.raf,.rw2,.pef,.srw,.tif,.tiff,.jpg,.jpeg,.png,.webp"; inp.onchange = function () { var f = inp.files[0]; if (!f) return; sourceFile = null; f.arrayBuffer().then(function (buf) { loadFromArrayBuffer(buf, f.name); }); }; inp.click(); } } document.getElementById("btnOpen").addEventListener("click", openPicker); document.getElementById("btnOpen2").addEventListener("click", openPicker); var stage = document.getElementById("stage"); stage.addEventListener("dragover", function (e) { e.preventDefault(); stage.classList.add("dragover"); }); stage.addEventListener("dragleave", function () { stage.classList.remove("dragover"); }); stage.addEventListener("drop", function (e) { e.preventDefault(); stage.classList.remove("dragover"); // Local OS file drop. if (e.dataTransfer.files && e.dataTransfer.files.length) { var f = e.dataTransfer.files[0]; sourceFile = null; f.arrayBuffer().then(function (buf) { loadFromArrayBuffer(buf, f.name); }); return; } // ArozOS file-explorer drop. try { var info = ao_module_utils.getDropFileInfo(e); if (info && info.length) loadFromPath(info[0].filepath, info[0].filename); } catch (err) { /* ignore */ } }); // ===================================================================== // Zoom & pan (scroll to zoom at cursor, right/middle-drag to pan) // ===================================================================== var vz = { zoom: 1, fit: 1, panX: 0, panY: 0 }; function applyTransform() { var v = document.getElementById("view"); v.style.transform = "translate(-50%,-50%) translate(" + vz.panX + "px," + vz.panY + "px) scale(" + vz.zoom + ")"; } function updateZoomLabel() { var el = document.getElementById("zoomReadout"); if (el) el.textContent = Math.round(vz.zoom * 100) + "%"; } function computeFit() { var v = document.getElementById("view"); if (!v.width) return 1; var pad = 32; return Math.min((stage.clientWidth - pad) / v.width, (stage.clientHeight - pad) / v.height); } function fitToWindow() { vz.fit = computeFit(); vz.zoom = vz.fit; vz.panX = 0; vz.panY = 0; applyTransform(); updateZoomLabel(); } function zoomActual() { vz.zoom = 1; vz.panX = 0; vz.panY = 0; applyTransform(); updateZoomLabel(); } function setZoomAt(z2, cx, cy) { z2 = Math.max(vz.fit * 0.5, Math.min(16, z2)); // Keep the image point under the cursor fixed while zooming. vz.panX = cx - (z2 / vz.zoom) * (cx - vz.panX); vz.panY = cy - (z2 / vz.zoom) * (cy - vz.panY); vz.zoom = z2; applyTransform(); updateZoomLabel(); } document.getElementById("btnFit").addEventListener("click", fitToWindow); document.getElementById("btnZoomFit").addEventListener("click", fitToWindow); document.getElementById("btnZoom100").addEventListener("click", zoomActual); // Scroll to zoom, centred on the cursor. stage.addEventListener("wheel", function (e) { if (!decoded) return; e.preventDefault(); var rect = stage.getBoundingClientRect(); var cx = e.clientX - (rect.left + rect.width / 2); var cy = e.clientY - (rect.top + rect.height / 2); var factor = Math.pow(1.0016, -e.deltaY); setZoomAt(vz.zoom * factor, cx, cy); }, { passive: false }); // Left (or middle) mouse button drag to pan. var panning = false, lastX = 0, lastY = 0; stage.addEventListener("mousedown", function (e) { if (!decoded || (e.button !== 0 && e.button !== 1)) return; panning = true; lastX = e.clientX; lastY = e.clientY; stage.classList.add("panning"); e.preventDefault(); }); window.addEventListener("mousemove", function (e) { if (!panning) return; vz.panX += e.clientX - lastX; vz.panY += e.clientY - lastY; lastX = e.clientX; lastY = e.clientY; applyTransform(); }); window.addEventListener("mouseup", function () { if (panning) { panning = false; stage.classList.remove("panning"); } }); // Re-fit on window resize while the view is at fit zoom. window.addEventListener("resize", function () { if (!decoded) return; if (Math.abs(vz.zoom - vz.fit) < 0.001 && vz.panX === 0 && vz.panY === 0) fitToWindow(); else vz.fit = computeFit(); }); // ===================================================================== // Save + Done // ===================================================================== document.getElementById("btnSave").addEventListener("click", saveImage); document.getElementById("btnDone").addEventListener("click", openInPixelStudio); document.getElementById("btnCancel").addEventListener("click", function () { if (typeof ao_module_close === "function") ao_module_close(); }); // Hand the developed image off to Pixel Studio: write the current develop to // a temporary file (tmp:/ is cleared automatically) then launch Pixel Studio // as a float window with that file as its input. function openInPixelStudio() { var inDesktop = (typeof ao_module_virtualDesktop !== "undefined" && ao_module_virtualDesktop); if (!decoded || !renderer) { if (typeof ao_module_close === "function") ao_module_close(); return; } if (!inDesktop || typeof ao_module_uploadFile !== "function") { // Outside the ArozOS desktop we cannot open another module — just save. saveImage(); return; } doRender(); // ensure the canvas holds the latest develop var view = document.getElementById("view"); view.toBlob(function (blob) { if (!blob) { alert("Failed to encode image."); return; } var base = (sourceFile && sourceFile.filename) ? stripExt(sourceFile.filename) : "Untitled"; var fname = base + "_raw_" + Date.now() + ".jpg"; var tmpDir = "tmp:/RawEditor"; var file = ao_module_utils.blobToFile(blob, fname); showLoader("Opening in Pixel Studio..."); ao_module_uploadFile(file, tmpDir, function () { hideLoader(); launchPixelStudio(tmpDir + "/" + fname, fname); if (typeof ao_module_close === "function") ao_module_close(); }, undefined, function () { hideLoader(); alert("Could not hand the image to Pixel Studio. Try 'Save Image...' instead."); }); }, "image/jpeg", 0.95); } function launchPixelStudio(filepath, filename) { var hash = encodeURIComponent(JSON.stringify([{ filename: filename, filepath: filepath }])); ao_module_newfw({ url: "Pixel Studio/index.html#" + hash, width: 1280, height: 820, appicon: "Pixel Studio/img/module_icon.png", title: "Pixel Studio - " + filename }); } function saveImage() { if (!decoded || !renderer) { alert("Nothing to save yet."); return; } doRender(); // make sure the canvas holds the latest develop var view = document.getElementById("view"); view.toBlob(function (blob) { if (!blob) { alert("Failed to encode image."); return; } var baseName = (sourceFile && sourceFile.filename ? stripExt(sourceFile.filename) : "Untitled") + "_edited.jpg"; if (window.parent !== window && typeof ao_module_openFileSelector === "function") { var defDir = "user:/Desktop"; if (sourceFile && sourceFile.filepath) { var parts = sourceFile.filepath.split("/"); parts.pop(); defDir = parts.join("/"); } ao_module_openFileSelector(function (files) { if (!files || !files.length) return; var fp = files[0].filepath.split("/"); var fn = fp.pop(); var dir = fp.join("/"); if (!/\.jpe?g$/i.test(fn)) fn = stripExt(fn) + ".jpg"; uploadBlob(blob, fn, dir); }, defDir, "new", false, { defaultName: baseName }); } else { // Fallback: browser download. var a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = baseName; a.click(); setTimeout(function () { URL.revokeObjectURL(a.href); }, 4000); } }, "image/jpeg", 0.92); } function uploadBlob(blob, filename, dir) { var file = ao_module_utils.blobToFile(blob, filename); showLoader("Saving " + filename + " ..."); ao_module_uploadFile(file, dir, function () { hideLoader(); setText("statusInfo", "Saved: " + dir + "/" + filename); }, undefined, function () { hideLoader(); alert("Failed to save image to " + dir); }); } function stripExt(name) { var i = name.lastIndexOf("."); return i < 0 ? name : name.substring(0, i); } // ===================================================================== // Boot // ===================================================================== initSliders(); refreshLutLibrary(); if (glOK) { var inputFiles = (typeof ao_module_loadInputFiles === "function") ? ao_module_loadInputFiles() : null; if (inputFiles && inputFiles.length) { loadFromPath(inputFiles[0].filepath, inputFiles[0].filename); } } })();