// Aether BG Renderer Processes
// --- GLOBAL VARIABLES & STATE ---
let cropper = null;
let currentImagePath = null;
let originalFileName = '';
let processedTempPath = null;
let activeBgMode = 'transparent'; // 'transparent', 'solid', 'gradient'
let activeSolidColor = '#ffffff';
let activeGradientStops = [
{ offset: 0, color: '#a78bfa' },
{ offset: 1, color: '#06b6d4' }
];
let scaleX = 1;
let scaleY = 1;
// Define gradient templates for canvas exporting
const GRADIENTS = {
'grad-1': [{ offset: 0, color: '#a78bfa' }, { offset: 1, color: '#06b6d4' }], // Violet Cyan
'grad-2': [{ offset: 0, color: '#f43f5e' }, { offset: 1, color: '#f97316' }], // Sunset
'grad-3': [{ offset: 0, color: '#10b981' }, { offset: 1, color: '#06b6d4' }], // Emerald
'grad-4': [{ offset: 0, color: '#ec4899' }, { offset: 1, color: '#8b5cf6' }], // Cyberpunk
'grad-5': [{ offset: 0, color: '#3b82f6' }, { offset: 1, color: '#8b5cf6' }], // Aether Blue
'grad-6': [{ offset: 0, color: '#f59e0b' }, { offset: 1, color: '#ef4444' }] // Warm Ember
};
// --- DOM ELEMENTS SELECTION ---
const elements = {
// Titlebar
winMin: document.getElementById('win-min'),
winMax: document.getElementById('win-max'),
winClose: document.getElementById('win-close'),
btnShowCredits: document.getElementById('btn-show-credits'),
creditsOverlay: document.getElementById('credits-overlay'),
btnCloseCredits: document.getElementById('btn-close-credits'),
langSelect: document.getElementById('lang-select'),
// Setup Wizard
setupOverlay: document.getElementById('setup-overlay'),
btnStartSetup: document.getElementById('btn-start-setup'),
setupProgressBar: document.getElementById('setup-progress-bar'),
setupStatusText: document.getElementById('setup-status-text'),
stepVenv: document.getElementById('step-venv'),
stepPip: document.getElementById('step-pip'),
stepDeps: document.getElementById('step-deps'),
stepLibs: document.getElementById('step-libs'),
// Workspace Views
dropzone: document.getElementById('dropzone'),
btnBrowse: document.getElementById('btn-browse'),
editorContainer: document.getElementById('editor-container'),
canvasWrapper: document.getElementById('canvas-wrapper'),
imageElement: document.getElementById('image-element'),
canvasBgOverlay: document.getElementById('canvas-bg-overlay'),
// Workspace Toolbar Controls
ctrlCropDrag: document.getElementById('ctrl-crop-drag'),
ctrlCropBox: document.getElementById('ctrl-crop-box'),
ctrlRotL: document.getElementById('ctrl-rot-l'),
ctrlRotR: document.getElementById('ctrl-rot-r'),
ctrlFlipH: document.getElementById('ctrl-flip-h'),
ctrlFlipV: document.getElementById('ctrl-flip-v'),
cropRatioSelect: document.getElementById('crop-ratio-select'),
ctrlAutoTrim: document.getElementById('ctrl-auto-trim'),
ctrlReset: document.getElementById('ctrl-reset'),
// Control Tower: Info Panel
infoCardEmpty: document.getElementById('info-card-empty'),
infoCardLoaded: document.getElementById('info-card-loaded'),
infoFilename: document.getElementById('info-filename'),
infoResolution: document.getElementById('info-resolution'),
infoSize: document.getElementById('info-size'),
btnChangeImage: document.getElementById('btn-change-image'),
// Control Tower: AI Processor
btnRemoveBg: document.getElementById('btn-remove-bg'),
loaderBox: document.getElementById('loader-box'),
loaderText: document.getElementById('loader-text'),
loaderSubtext: document.getElementById('loader-subtext'),
// Control Tower: Background settings
secCanvasBg: document.getElementById('sec-canvas-bg'),
tabBgTrans: document.getElementById('tab-bg-trans'),
tabBgSolid: document.getElementById('tab-bg-solid'),
tabBgGrad: document.getElementById('tab-bg-grad'),
subpanelSolid: document.getElementById('subpanel-solid'),
subpanelGradient: document.getElementById('subpanel-gradient'),
customColorPicker: document.getElementById('custom-color-picker'),
btnCustomColorPicker: document.getElementById('btn-custom-color-picker'),
// Control Tower: Export Studio
secExport: document.getElementById('sec-export'),
exportFormat: document.getElementById('export-format'),
rowQualitySlider: document.getElementById('row-quality-slider'),
exportQuality: document.getElementById('export-quality'),
exportQualityVal: document.getElementById('export-quality-val'),
btnExport: document.getElementById('btn-export'),
// Toast Banner
toastBanner: document.getElementById('toast-banner'),
toastTitle: document.getElementById('toast-title'),
toastMessage: document.getElementById('toast-message'),
btnCloseToast: document.getElementById('btn-close-toast'),
// Error Overlay Modal
errorOverlay: document.getElementById('error-overlay'),
errorOverlayLog: document.getElementById('error-overlay-log'),
btnCloseError: document.getElementById('btn-close-error')
};
// --- INITIALIZE & ENVIRONMENT CHECKS ---
document.addEventListener('DOMContentLoaded', async () => {
setupTitlebarListeners();
setupToastListeners();
initLanguage(); // Initialize translations immediately on load
// Check if Python virtual environment exists
const { hasVenv } = await window.api.checkPythonSetup();
if (!hasVenv) {
elements.setupOverlay.classList.remove('hidden');
} else {
initWorkspace();
}
});
// Titlebar Controls
function setupTitlebarListeners() {
elements.winMin.addEventListener('click', () => window.api.minimizeWindow());
elements.winMax.addEventListener('click', () => window.api.maximizeWindow());
elements.winClose.addEventListener('click', () => window.api.closeWindow());
// Show Credits Modal
elements.btnShowCredits.addEventListener('click', () => {
elements.creditsOverlay.classList.remove('hidden');
});
// Close Credits Modal
elements.btnCloseCredits.addEventListener('click', () => {
elements.creditsOverlay.classList.add('hidden');
});
// Close Error Modal
elements.btnCloseError.addEventListener('click', () => {
elements.errorOverlay.classList.add('hidden');
});
}
// Workspace Initialization
function initWorkspace() {
setupDragAndDrop();
setupToolbarListeners();
setupControlTowerListeners();
setupBgSwatches();
// Browse Button click
elements.btnBrowse.addEventListener('click', selectLocalImage);
elements.dropzone.addEventListener('click', (e) => {
// Only open if clicking container directly and not buttons
if (e.target === elements.dropzone || e.target.closest('.dropzone-content') && !e.target.closest('button')) {
selectLocalImage();
}
});
// Change image button click
elements.btnChangeImage.addEventListener('click', resetWorkspaceToEmpty);
}
function resetWorkspaceToEmpty() {
currentImagePath = null;
originalFileName = '';
processedTempPath = null;
activeBgMode = 'transparent';
scaleX = 1;
scaleY = 1;
// Destroy cropper safely
if (cropper) {
cropper.destroy();
cropper = null;
}
// Restore drag drop state views
elements.editorContainer.classList.add('hidden');
elements.dropzone.classList.remove('hidden');
elements.infoCardLoaded.classList.add('hidden');
elements.infoCardEmpty.classList.remove('hidden');
// Disable processing button until a new file is loaded
elements.btnRemoveBg.disabled = true;
// Clear background customizations
resetBgSettings();
showToast(getTrans('toastImageChanged'), getTrans('toastImageChangedMsg'), "info");
}
// --- PYTHON VENV INSTALLER ENGINE ---
elements.btnStartSetup.addEventListener('click', async () => {
elements.btnStartSetup.disabled = true;
elements.setupProgressBar.parentElement.style.display = 'block';
elements.setupProgressBar.style.width = '5%';
// Track active steps
const setStepActive = (stepEl) => {
document.querySelectorAll('.step-item').forEach(el => el.classList.remove('active'));
stepEl.classList.add('active');
};
const setStepDone = (stepEl) => {
stepEl.classList.remove('active');
stepEl.classList.add('done');
};
const setStepFailed = (stepEl) => {
stepEl.classList.remove('active');
stepEl.classList.add('failed');
};
setStepActive(elements.stepVenv);
elements.setupStatusText.innerText = getTrans('setupCreatingVenv');
// Listen for pipeline setup output logs
window.api.onSetupProgress((progressText) => {
// Translate setup output progress text
if (progressText.includes("Creating Python virtual environment")) {
elements.setupStatusText.innerText = getTrans('setupCreatingVenv');
setStepActive(elements.stepVenv);
elements.setupProgressBar.style.width = '15%';
} else if (progressText.includes("Virtual environment successfully created")) {
elements.setupStatusText.innerText = getTrans('setupVenvCreated');
setStepDone(elements.stepVenv);
setStepActive(elements.stepPip);
elements.setupProgressBar.style.width = '30%';
} else if (progressText.includes("Virtual environment already exists")) {
elements.setupStatusText.innerText = getTrans('setupVenvExists');
setStepDone(elements.stepVenv);
setStepActive(elements.stepPip);
elements.setupProgressBar.style.width = '30%';
} else if (progressText.includes("Upgrading pip")) {
elements.setupStatusText.innerText = getTrans('setupUpgradingPip');
setStepActive(elements.stepPip);
elements.setupProgressBar.style.width = '45%';
} else if (progressText.includes("Installing PyTorch")) {
elements.setupStatusText.innerText = getTrans('setupInstallingTorch');
setStepDone(elements.stepPip);
setStepActive(elements.stepDeps);
elements.setupProgressBar.style.width = '60%';
} else if (progressText.includes("Installing HuggingFace")) {
elements.setupStatusText.innerText = getTrans('setupInstallingDeps');
setStepDone(elements.stepDeps);
setStepActive(elements.stepLibs);
elements.setupProgressBar.style.width = '80%';
} else if (progressText.includes("Verifying PyTorch")) {
elements.setupStatusText.innerText = getTrans('setupInstallingDeps');
setStepActive(elements.stepLibs);
elements.setupProgressBar.style.width = '90%';
} else if (progressText.includes("Downloading torch")) {
elements.setupStatusText.innerText = getTrans('setupDownloadingTorch');
elements.setupProgressBar.style.width = '75%';
} else if (progressText.includes("Installing collected packages")) {
elements.setupStatusText.innerText = getTrans('setupInstallingDeps');
elements.setupProgressBar.style.width = '85%';
} else if (progressText.includes("Setup completed successfully")) {
elements.setupStatusText.innerText = getTrans('setupCompleted');
setStepDone(elements.stepDeps);
setStepDone(elements.stepLibs);
elements.setupProgressBar.style.width = '100%';
} else {
elements.setupStatusText.innerText = progressText;
}
});
try {
const result = await window.api.runPythonSetup();
if (result.code === 0) {
elements.setupStatusText.innerText = getTrans('setupCompleted');
showToast(getTrans('toastSetupSuccess'), getTrans('toastSetupSuccessMsg'), "success");
setTimeout(() => {
elements.setupOverlay.classList.add('hidden');
initWorkspace();
}, 1500);
} else {
setStepFailed(elements.stepDeps);
const errorDetail = (result.error || '').trim();
elements.setupStatusText.innerText = errorDetail
? `${getTrans('setupFailed')} ${errorDetail.slice(0, 200)}`
: getTrans('setupFailed');
elements.btnStartSetup.disabled = false;
showToast(getTrans('toastSetupError'), errorDetail || getTrans('toastSetupErrorMsg'), "error");
}
} catch (err) {
elements.setupStatusText.innerText = `${getTrans('toastSetupError')}: ${err.message}`;
elements.btnStartSetup.disabled = false;
showToast(getTrans('toastSetupError'), err.message, "error");
}
});
// --- IMAGE FILE INPUT ENGINE ---
function setupDragAndDrop() {
const dropzone = elements.dropzone;
['dragenter', 'dragover'].forEach(eventName => {
dropzone.addEventListener(eventName, (e) => {
e.preventDefault();
dropzone.classList.add('dragover');
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, (e) => {
e.preventDefault();
dropzone.classList.remove('dragover');
}, false);
});
dropzone.addEventListener('drop', (e) => {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length > 0) {
handleImageSelection(files[0].path);
}
});
}
async function selectLocalImage() {
const filePath = await window.api.selectImage();
if (filePath) {
handleImageSelection(filePath);
}
}
function handleImageSelection(filePath) {
currentImagePath = filePath;
originalFileName = filePath.split(/[\\/]/).pop();
// Reset previous states
processedTempPath = null;
activeBgMode = 'transparent';
scaleX = 1;
scaleY = 1;
resetBgSettings();
// Show image in preview element
elements.imageElement.src = `file://${filePath}`;
// Set file metadata in control tower
const stats = getFileStats(filePath);
elements.infoFilename.innerText = originalFileName;
elements.infoSize.innerText = stats.sizeFormatted;
// Load dimensions
const img = new Image();
img.onload = function() {
elements.infoResolution.innerText = `${this.naturalWidth} x ${this.naturalHeight} px`;
// Switch views
elements.dropzone.classList.add('hidden');
elements.editorContainer.classList.remove('hidden');
elements.infoCardEmpty.classList.add('hidden');
elements.infoCardLoaded.classList.remove('hidden');
// Enable AI button
elements.btnRemoveBg.disabled = false;
// Initialize CropperJS on image
initCropper();
};
img.src = `file://${filePath}`;
}
function getFileStats(path) {
// Simple synchronous-like wrapper or mockup stats.
// Note: Electron file handles path directly.
return {
sizeFormatted: "Calculating..." // Updated as soon as loaded
};
}
// Helper: Formatter
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
// --- CROPPERJS CONTROLLER HUB ---
function initCropper() {
if (cropper) {
cropper.destroy();
}
// Load Cropper configuration
cropper = new Cropper(elements.imageElement, {
viewMode: 1,
dragMode: 'move',
autoCrop: false,
responsive: true,
restore: false,
guides: true,
center: true,
highlight: false,
cropBoxMovable: true,
cropBoxResizable: true,
toggleDragModeOnDblclick: false,
});
// Wire toolbar click listeners
elements.ctrlCropDrag.classList.add('active');
elements.ctrlCropBox.classList.remove('active');
elements.ctrlAutoTrim.disabled = true; // Disabled until background is removed
}
function setupToolbarListeners() {
elements.ctrlCropDrag.addEventListener('click', () => {
cropper.setDragMode('move');
elements.ctrlCropDrag.classList.add('active');
elements.ctrlCropBox.classList.remove('active');
});
elements.ctrlCropBox.addEventListener('click', () => {
cropper.setDragMode('crop');
elements.ctrlCropDrag.classList.remove('active');
elements.ctrlCropBox.classList.add('active');
});
elements.ctrlRotL.addEventListener('click', () => cropper.rotate(-90));
elements.ctrlRotR.addEventListener('click', () => cropper.rotate(90));
elements.ctrlFlipH.addEventListener('click', () => {
scaleX = scaleX === 1 ? -1 : 1;
cropper.scale(scaleX, scaleY);
});
elements.ctrlFlipV.addEventListener('click', () => {
scaleY = scaleY === 1 ? -1 : 1;
cropper.scale(scaleX, scaleY);
});
elements.cropRatioSelect.addEventListener('change', (e) => {
const value = parseFloat(e.target.value);
cropper.setAspectRatio(value);
});
elements.ctrlReset.addEventListener('click', () => {
cropper.reset();
scaleX = 1;
scaleY = 1;
cropper.scale(1, 1);
elements.cropRatioSelect.value = "NaN";
cropper.setAspectRatio(NaN);
showToast("Reset Action", "Workspace state cleared.", "info");
});
elements.ctrlAutoTrim.addEventListener('click', autoTrimTransparent);
}
// --- NATIVE AUTOTRIM (PIXEL ITERATION BOUNDS) ---
function autoTrimTransparent() {
if (!cropper || !processedTempPath) return;
// Create a temporary hidden canvas to read pixels directly from the original image element
// This bypasses any cropper canvas restrictions or CORS issues when webSecurity is false
const canvas = document.createElement('canvas');
const img = elements.imageElement;
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
try {
ctx.drawImage(img, 0, 0);
} catch (err) {
const title = getTrans('toastTrimFailed');
const msg = getTrans('toastTrimFailedMsg') + ": " + err.message;
showToast(title, msg, "error");
return;
}
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imgData.data;
let minX = canvas.width;
let maxX = 0;
let minY = canvas.height;
let maxY = 0;
let foundAlpha = false;
// Scan alpha channel (index + 3) to locate non-empty boundaries
for (let y = 0; y < canvas.height; y++) {
for (let x = 0; x < canvas.width; x++) {
const alphaIndex = (y * canvas.width + x) * 4 + 3;
if (data[alphaIndex] > 8) { // transparency threshold
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
foundAlpha = true;
}
}
}
if (foundAlpha) {
// Add small padding to avoid clipping edge pixels exactly
const pad = 10;
minX = Math.max(0, minX - pad);
minY = Math.max(0, minY - pad);
maxX = Math.min(canvas.width, maxX + pad);
maxY = Math.min(canvas.height, maxY + pad);
// Turn on Crop selection mode visual indicators
cropper.crop(); // Manually draw/show the crop selection box
cropper.setDragMode('crop');
elements.ctrlCropDrag.classList.remove('active');
elements.ctrlCropBox.classList.add('active');
// Convert pixel coordinates to Cropper canvas boundaries
const canvasData = cropper.getCanvasData();
const scale = canvasData.width / img.naturalWidth;
// Set crop boundaries
cropper.setCropBoxData({
left: canvasData.left + minX * scale,
top: canvasData.top + minY * scale,
width: (maxX - minX) * scale,
height: (maxY - minY) * scale
});
showToast(getTrans('toastTrimComplete'), getTrans('toastTrimCompleteMsg'), "success");
} else {
showToast(getTrans('toastTitleInfo'), getTrans('toastTrimFailedMsg'), "info");
}
}
// --- LOCALIZATION ENGINE (i18n) ---
const TRANSLATIONS = {
en: {
title: "Dekupai - Trunçgil AI Decoupage",
sourceFile: "Source File",
noImage: "No image loaded",
fileName: "Name",
fileResolution: "Resolution",
fileSize: "Size",
newImage: "Import New Image",
bgRemoval: "Background Removal",
extractSubject: "Extract Subject",
initAI: "Initializing AI Model...",
loadingWeights: "First launch downloads model (~200MB)",
bgSettings: "Background Settings",
transparent: "Transparent",
solidColor: "Solid Color",
gradient: "Gradient",
selectSolid: "Select Solid Fill",
selectGrad: "Select High-End Gradient",
exportStudio: "Export Studio",
format: "Format",
quality: "Quality",
exportMasterpiece: "Export Masterpiece",
toastTitleInfo: "Info",
toastTitleSuccess: "Success",
toastTitleError: "Error",
toastImageChanged: "Image Changed",
toastImageChangedMsg: "Workspace cleared. Ready for a new image.",
toastTrimComplete: "Auto-Trim Complete",
toastTrimCompleteMsg: "Crop boundaries adjusted securely.",
toastTrimFailed: "Trim Warning",
toastTrimFailedMsg: "Could not analyze transparent boundaries.",
toastSetupSuccess: "Setup Success",
toastSetupSuccessMsg: "Python environment is fully configured.",
toastSetupError: "Setup Error",
toastSetupErrorMsg: "Failed to configure Python environment.",
initWizardTitle: "Initializing AI Engine",
initWizardSubtitle: "Dekupai requires a local Python environment to run the BiRefNet neural network models.",
configureEngine: "Configure Engine (Automatic)",
stepVenv: "Creating isolated virtual environment (.venv)",
stepPip: "Upgrading Python package installer (pip)",
stepDeps: "Installing PyTorch AI Engine (CPU version)",
stepLibs: "Installing HuggingFace Transformers & PIL library",
creditsVision: "Developed under the vision of",
creditsExpertise: "by the engineering expertise of",
creditsDesc: "Dekupai is an advanced deep-learning desktop workspace that provides local, high-precision background removal using BiRefNet neural segmentation networks.",
closeCredits: "Close Credits",
dragDropText: "Drag & Drop Image Here",
dragDropSub: "Supports PNG, JPG, JPEG, or WebP formats",
orText: "OR",
browseFiles: "Browse Local Files",
autoTrim: "Auto-Trim",
statusInit: "Initializing AI model...",
statusWeights: "Loading neural weights into memory...",
statusImporting: "Importing libraries...",
statusDetecting: "Detecting device...",
statusUsingDevice: "Using device: ",
statusLoadingModel: "Loading BiRefNet model...",
statusPreprocessing: "Loading and preprocessing image...",
statusInference: "Running background removal inference...",
statusGeneratingMask: "Generating transparency mask...",
statusSaving: "Saving output transparent image...",
statusDone: "Completed!",
subFirstRun: "This downloads ~200MB of weights on first run.",
subInference: "Extracting details and soft matting alpha...",
subSaving: "Assembling high-res RGBA container...",
subDefault: "Please wait while the AI model processes...",
setupReady: "Ready to configure environment.",
setupCreatingVenv: "Creating Python virtual environment...",
setupVenvCreated: "Virtual environment successfully created.",
setupVenvExists: "Virtual environment already exists.",
setupUpgradingPip: "Upgrading Python package installer (pip)...",
setupInstallingTorch: "Installing PyTorch AI Engine (CPU version)...",
setupDownloadingTorch: "Downloading torch and dependencies...",
setupInstallingDeps: "Installing collected packages...",
setupCompleted: "Configuration Complete! Enjoy background removal.",
setupFailed: "Setup failed. Check system dependencies.",
exportingMasterpiece: "Exporting masterpiece...",
toastExportSuccess: "Export Successful",
toastExportSuccessMsg: "Saved file correctly: ",
toastExportFailed: "Export Failed",
toastExportFailedMsg: "Save operation failed.",
toastExportError: "Export Error",
showInExplorer: "Show in File Explorer",
errorModalTitle: "Process Error",
errorModalSubtitle: "We encountered an issue while segmenting your image.",
errorModalSuggestion: "The subject in the image you want to extract might be too blended or visually indistinguishable from its background. Please try another image with a clearer, higher-contrast subject.",
closeBtn: "Close"
},
tr: {
title: "Dekupai - Trunçgil AI Dekupaj",
sourceFile: "Kaynak Dosya",
noImage: "Görsel yüklenmedi",
fileName: "Dosya Adı",
fileResolution: "Çözünürlük",
fileSize: "Boyut",
newImage: "Yeni Görsel Yükle",
bgRemoval: "Arka Plan Temizleme",
extractSubject: "Özneyi Dekupe Et",
initAI: "AI Modeli Başlatılıyor...",
loadingWeights: "İlk çalıştırmada model indirilir (~200MB)",
bgSettings: "Arka Plan Ayarları",
transparent: "Şeffaf",
solidColor: "Tek Renk",
gradient: "Degrade",
selectSolid: "Düz Renk Seçin",
selectGrad: "Premium Degrade Seçin",
exportStudio: "Dışa Aktarma Stüdyosu",
format: "Format",
quality: "Kalite",
exportMasterpiece: "Çalışmayı Dışa Aktar",
toastTitleInfo: "Bilgi",
toastTitleSuccess: "Başarılı",
toastTitleError: "Hata",
toastImageChanged: "Görsel Değiştirildi",
toastImageChangedMsg: "Çalışma alanı temizlendi. Yeni görsel için hazır.",
toastTrimComplete: "Otomatik Kırpma Tamamlandı",
toastTrimCompleteMsg: "Kırpma sınırları güvenli bir şekilde ayarlandı.",
toastTrimFailed: "Kırpma Uyarısı",
toastTrimFailedMsg: "Şeffaf sınırlar analiz edilemedi.",
toastSetupSuccess: "Kurulum Başarılı",
toastSetupSuccessMsg: "Python ortamı tamamen yapılandırıldı.",
toastSetupError: "Kurulum Hatası",
toastSetupErrorMsg: "Python ortamı yapılandırılamadı.",
initWizardTitle: "AI Motoru Başlatılıyor",
initWizardSubtitle: "Dekupai, BiRefNet yapay zeka modelini çalıştırmak için yerel bir Python ortamına ihtiyaç duyar.",
configureEngine: "Motoru Yapılandır (Otomatik)",
stepVenv: "İzole sanal ortam oluşturuluyor (.venv)",
stepPip: "Python paket yöneticisi güncelleniyor (pip)",
stepDeps: "PyTorch AI motoru kuruluyor (CPU versiyonu)",
stepLibs: "HuggingFace Transformers ve PIL kuruluyor",
creditsVision: "Vizyon ve liderlik",
creditsExpertise: "yazılım geliştirme ve mühendislik",
creditsDesc: "Dekupai, BiRefNet yapay sinir ağı segmentasyon modellerini kullanarak yerel ve yüksek hassasiyetli arka plan temizleme sağlayan gelişmiş bir derin öğrenme masaüstü uygulamasıdır.",
closeCredits: "Kapat",
dragDropText: "Görseli Buraya Sürükleyip Bırakın",
dragDropSub: "PNG, JPG, JPEG veya WebP formatlarını destekler",
orText: "VEYA",
browseFiles: "Yerel Dosyalara Göz Atın",
autoTrim: "Oto-Kırp",
statusInit: "AI modeli başlatılıyor...",
statusWeights: "Yapay sinir ağları belleğe yükleniyor...",
statusImporting: "Kütüphaneler içe aktarılıyor...",
statusDetecting: "Donanım tespit ediliyor...",
statusUsingDevice: "Kullanılan donanım: ",
statusLoadingModel: "BiRefNet modeli yükleniyor...",
statusPreprocessing: "Görsel yükleniyor ve ön işleme yapılıyor...",
statusInference: "Arka plan temizleme algoritması çalıştırılıyor...",
statusGeneratingMask: "Şeffaflık maskesi oluşturuluyor...",
statusSaving: "Şeffaf görsel kaydediliyor...",
statusDone: "Tamamlandı!",
subFirstRun: "İlk çalıştırmada ~200MB ağırlık dosyası indirilir.",
subInference: "Detaylar ve yumuşak matlaştırma katmanı çıkarılıyor...",
subSaving: "Yüksek çözünürlüklü RGBA görsel birleştiriliyor...",
subDefault: "Yapay zeka modeli işleme yaparken lütfen bekleyin...",
setupReady: "Ortamı yapılandırmaya hazır.",
setupCreatingVenv: "Python sanal ortamı oluşturuluyor...",
setupVenvCreated: "Sanal ortam başarıyla oluşturuldu.",
setupVenvExists: "Sanal ortam zaten mevcut.",
setupUpgradingPip: "Python paket yükleyici (pip) güncelleniyor...",
setupInstallingTorch: "PyTorch AI motoru kuruluyor (CPU versiyonu)...",
setupDownloadingTorch: "Torch ve bağımlılıklar indiriliyor...",
setupInstallingDeps: "Toplanan paketler kuruluyor...",
setupCompleted: "Yapılandırma Tamamlandı! Arka plan temizlemenin keyfini çıkarın.",
setupFailed: "Kurulum başarısız oldu. Sistem bağımlılıklarını kontrol edin.",
exportingMasterpiece: "Çalışma dışa aktarılıyor...",
toastExportSuccess: "Dışa Aktarma Başarılı",
toastExportSuccessMsg: "Dosya başarıyla kaydedildi: ",
toastExportFailed: "Dışa Aktarma Başarısız",
toastExportFailedMsg: "Kaydetme işlemi başarısız oldu.",
toastExportError: "Dışa Aktarma Hatası",
showInExplorer: "Dosya Gezgininde Göster",
errorModalTitle: "İşlem Hatası",
errorModalSubtitle: "Görsel dekupe edilirken yapay zeka işleminde bir sorunla karşılaşıldı.",
errorModalSuggestion: "Dekupe etmek istediğiniz görsel, arka planıyla ayırt edilemeyecek kadar benzer veya karmaşık olabilir. Lütfen öznenin arka plandan daha net ayırt edilebildiği, daha yüksek kontrastlı başka bir görsel deneyin.",
closeBtn: "Kapat"
},
de: {
title: "Dekupai - Trunçgil AI Dekupage",
sourceFile: "Quelldatei",
noImage: "Kein Bild geladen",
fileName: "Dateiname",
fileResolution: "Auflösung",
fileSize: "Größe",
newImage: "Neues Bild Laden",
bgRemoval: "Hintergrundentfernung",
extractSubject: "Motiv Freistellen",
initAI: "AI-Modell wird initialisiert...",
loadingWeights: "Erster Start lädt Modell herunter (~200MB)",
bgSettings: "Hintergrundeinstellungen",
transparent: "Transparent",
solidColor: "Vollfarbe",
gradient: "Verlauf",
selectSolid: "Volltonfarbe wählen",
selectGrad: "Premium-Verlauf wählen",
exportStudio: "Export Studio",
format: "Format",
quality: "Qualität",
exportMasterpiece: "Meisterwerk Exportieren",
toastTitleInfo: "Info",
toastTitleSuccess: "Erfolg",
toastTitleError: "Fehler",
toastImageChanged: "Bild Geändert",
toastImageChangedMsg: "Arbeitsbereich geleert. Bereit für neues Bild.",
toastTrimComplete: "Auto-Zuschneiden Fertig",
toastTrimCompleteMsg: "Zuschnittsgrenzen sicher angepasst.",
toastTrimFailed: "Zuschnitt Warnung",
toastTrimFailedMsg: "Transparente Grenzen konnten nicht analysiert werden.",
toastSetupSuccess: "Setup erfolgreich",
toastSetupSuccessMsg: "Python-Umgebung ist vollständig konfiguriert.",
toastSetupError: "Setup-Fehler",
toastSetupErrorMsg: "Konfiguration der Python-Umgebung fehlgeschlagen.",
initWizardTitle: "AI-Engine wird initialisiert",
initWizardSubtitle: "Dekupai erfordert eine lokale Python-Umgebung, um die BiRefNet-Modelle auszuführen.",
configureEngine: "Engine konfigurieren (Automatisch)",
stepVenv: "Erstellen einer isolierten virtuellen Umgebung (.venv)",
stepPip: "Upgrade des Python-Paketinstallers (pip)",
stepDeps: "Installation der PyTorch AI-Engine (CPU-Version)",
stepLibs: "Installation der HuggingFace Transformers & PIL Bibliothek",
creditsVision: "Entwickelt unter der Vision von",
creditsExpertise: "durch die Software-Expertise von",
creditsDesc: "Dekupai is ein fortschrittlicher Deep-Learning-Desktop-Arbeitsbereich, der eine lokale, hochpräzise Hintergrundentfernung mithilfe von BiRefNet-Segmentierungsnetzwerken bietet.",
closeCredits: "Schließen",
dragDropText: "Bild hierher ziehen und ablegen",
dragDropSub: "Unterstützt PNG-, JPG-, JPEG- oder WebP-Formate",
orText: "ODER",
browseFiles: "Lokale Dateien durchsuchen",
autoTrim: "Auto-Trim",
statusInit: "AI-Modell wird initialisiert...",
statusWeights: "Neuronale Gewichte werden geladen...",
statusImporting: "Bibliotheken werden importiert...",
statusDetecting: "Hardware wird erkannt...",
statusUsingDevice: "Verwendetes Gerät: ",
statusLoadingModel: "BiRefNet-Modell wird geladen...",
statusPreprocessing: "Bild wird geladen und vorverarbeitet...",
statusInference: "Hintergrundentfernungs-Inferenz wird ausgeführt...",
statusGeneratingMask: "Transparenzmaske wird generiert...",
statusSaving: "Transparentes Ausgabebild wird gespeichert...",
statusDone: "Abgeschlossen!",
subFirstRun: "Erster Start lädt Modell herunter (~200MB)",
subInference: "Details und weiche Maske werden extrahiert...",
subSaving: "Hochauflösender RGBA-Container wird zusammengestellt...",
subDefault: "Bitte warten Sie, während das AI-Modell verarbeitet...",
setupReady: "Bereit zum Konfigurieren der Umgebung.",
setupCreatingVenv: "Isolierte virtuelle Umgebung (.venv) wird erstellt...",
setupVenvCreated: "Virtuelle Umgebung erfolgreich erstellt.",
setupVenvExists: "Virtuelle Umgebung existiert bereits.",
setupUpgradingPip: "Python-Paketinstallateur (pip) wird aktualisiert...",
setupInstallingTorch: "PyTorch AI-Engine (CPU-Version) wird installiert...",
setupDownloadingTorch: "Torch und Abhängigkeiten werden heruntergeladen...",
setupInstallingDeps: "Gesammelte Pakete werden installiert...",
setupCompleted: "Konfiguration abgeschlossen! Viel Spaß mit der Hintergrundentfernung.",
setupFailed: "Setup fehlgeschlagen. Systemabhängigkeiten prüfen.",
exportingMasterpiece: "Meisterwerk wird exportiert...",
toastExportSuccess: "Export erfolgreich",
toastExportSuccessMsg: "Datei erfolgreich gespeichert: ",
toastExportFailed: "Export fehlgeschlagen",
toastExportFailedMsg: "Speichervorgang fehlgeschlagen.",
toastExportError: "Exportfehler",
showInExplorer: "Im Datei-Explorer anzeigen",
errorModalTitle: "Verarbeitungsfehler",
errorModalSubtitle: "Beim Segmentieren Ihres Bildes ist ein Problem aufgetreten.",
errorModalSuggestion: "Das Motiv auf dem Bild, das Sie freistellen möchten, ist möglicherweise zu stark mit dem Hintergrund verschmolzen oder visuell nicht unterscheidbar. Bitte versuchen Sie es mit einem anderen Bild mit einem deutlicheren, kontrastreicheren Motiv.",
closeBtn: "Schließen"
},
ru: {
title: "Dekupai - Trunçgil AI Декупаж",
sourceFile: "Исходный файл",
noImage: "Изображение не загружено",
fileName: "Имя",
fileResolution: "Разрешение",
fileSize: "Размер",
newImage: "Загрузить новое изображение",
bgRemoval: "Удаление фона",
extractSubject: "Вырезать объект",
initAI: "Инициализация модели ИИ...",
loadingWeights: "Первый запуск загружает модель (~200 МБ)",
bgSettings: "Настройки фона",
transparent: "Прозрачный",
solidColor: "Сплошной цвет",
gradient: "Градиент",
selectSolid: "Выберите сплошной цвет",
selectGrad: "Выберите премиум градиент",
exportStudio: "Студия экспорта",
format: "Формат",
quality: "Качество",
exportMasterpiece: "Экспортировать шедевр",
toastTitleInfo: "Информация",
toastTitleSuccess: "Успех",
toastTitleError: "Ошибка",
toastImageChanged: "Изображение изменено",
toastImageChangedMsg: "Рабочая область очищена. Готово к новому изображению.",
toastTrimComplete: "Автообрезка завершена",
toastTrimCompleteMsg: "Границы обрезки успешно скорректированы.",
toastTrimFailed: "Предупреждение об обрезке",
toastTrimFailedMsg: "Не удалось проанализировать прозрачные границы.",
toastSetupSuccess: "Установка успешна",
toastSetupSuccessMsg: "Среда Python полностью настроена.",
toastSetupError: "Ошибка установки",
toastSetupErrorMsg: "Не удалось настроить среду Python.",
initWizardTitle: "Инициализация движка ИИ",
initWizardSubtitle: "Для работы Dekupai требуется локальная среда Python для запуска нейросетевых моделей BiRefNet.",
configureEngine: "Настроить движок (Автоматически)",
stepVenv: "Создание изолированной виртуальной среды (.venv)",
stepPip: "Обновление установщика пакетов Python (pip)",
stepDeps: "Установка движка ИИ PyTorch (версия для CPU)",
stepLibs: "Установка библиотек HuggingFace Transformers и PIL",
creditsVision: "Разработано под руководством",
creditsExpertise: "программная инженерия и разработка",
creditsDesc: "Dekupai — это продвинутое настольное приложение для глубокого обучения, обеспечивающее локальное высокоточное удаление фона с использованием моделей сегментации BiRefNet.",
closeCredits: "Закрыть",
dragDropText: "Перетащите изображение сюда",
dragDropSub: "Поддерживает форматы PNG, JPG, JPEG или WebP",
orText: "ИЛИ",
browseFiles: "Обзор локальных файлов",
autoTrim: "Автообрезка",
statusInit: "Инициализация модели ИИ...",
statusWeights: "Загрузка весов нейросети в память...",
statusImporting: "Импорт библиотек...",
statusDetecting: "Обнаружение устройства...",
statusUsingDevice: "Используемое устройство: ",
statusLoadingModel: "Загрузка модели BiRefNet...",
statusPreprocessing: "Загрузка и предобработка изображения...",
statusInference: "Выполнение удаления фона...",
statusGeneratingMask: "Генерация маски прозрачности...",
statusSaving: "Сохранение выходного прозрачного изображения...",
statusDone: "Готово!",
subFirstRun: "Первый запуск загружает модель (~200 МБ)",
subInference: "Извлечение деталей и мягкой маски альфа...",
subSaving: "Сборка RGBA-контейнера высокого разрешения...",
subDefault: "Пожалуйста, подождите, пока модель ИИ обрабатывает...",
setupReady: "Готов к настройке среды.",
setupCreatingVenv: "Создание изолированной виртуальной среды (.venv)...",
setupVenvCreated: "Виртуальная среда успешно создана.",
setupVenvExists: "Виртуальная среда уже существует.",
setupUpgradingPip: "Обновление установщика пакетов Python (pip)...",
setupInstallingTorch: "Установка движка ИИ PyTorch (версия CPU)...",
setupDownloadingTorch: "Скачивание torch и зависимостей...",
setupInstallingDeps: "Установка собранных пакетов...",
setupCompleted: "Настройка завершена! Наслаждайтесь удалением фона.",
setupFailed: "Ошибка настройки. Проверьте системные зависимости.",
exportingMasterpiece: "Экспорт шедевра...",
toastExportSuccess: "Экспорт успешен",
toastExportSuccessMsg: "Файл успешно сохранен: ",
toastExportFailed: "Ошибка экспорта",
toastExportFailedMsg: "Операция сохранения не удалась.",
toastExportError: "Ошибка экспорта",
showInExplorer: "Показать в проводнике",
errorModalTitle: "Ошибка обработки",
errorModalSubtitle: "При сегментации вашего изображения возникла проблема.",
errorModalSuggestion: "Объект на изображении, который вы хотите вырезать, может быть слишком сливающимся или визуально неотличимым от фона. Пожалуйста, попробуйте другое изображение с более четким и контрастным объектом.",
closeBtn: "Закрыть"
},
ar: {
title: "Dekupai - Trunçgil AI إزالة الخلفية",
sourceFile: "الملف المصدر",
noImage: "لم يتم تحميل أي صورة",
fileName: "الاسم",
fileResolution: "الدقة",
fileSize: "الحجم",
newImage: "تحميل صورة جديدة",
bgRemoval: "إزالة الخلفية",
extractSubject: "استخراج العنصر",
initAI: "تهيئة نموذج الذكاء الاصطناعي...",
loadingWeights: "التشغيل الأول يقوم بتحميل النموذج (~200 ميجابايت)",
bgSettings: "إعدادات الخلفية",
transparent: "شفاف",
solidColor: "لون موحد",
gradient: "تدرج لوني",
selectSolid: "اختر لوناً موحداً",
selectGrad: "اختر تدرجاً لونيًا فاخرًا",
exportStudio: "استوديو التصدير",
format: "الصيغة",
quality: "الجودة",
exportMasterpiece: "تصدير العمل الفني",
toastTitleInfo: "معلومات",
toastTitleSuccess: "نجاح",
toastTitleError: "خطأ",
toastImageChanged: "تم تغيير الصورة",
toastImageChangedMsg: "تم مسح مساحة العمل. جاهز لصورة جديدة.",
toastTrimComplete: "اكتمل القص التلقائي",
toastTrimCompleteMsg: "تم ضبط حدود القص بأمان حول العنصر.",
toastTrimFailed: "تحذير القص",
toastTrimFailedMsg: "تعذر تحليل الحدود الشفافة.",
toastSetupSuccess: "نجاح التهيئة",
toastSetupSuccessMsg: "تم تكوين بيئة بايثون بنجاح.",
toastSetupError: "خطأ في التهيئة",
toastSetupErrorMsg: "فشل تكوين بيئة بايثون.",
initWizardTitle: "تهيئة محرك الذكاء الاصطناعي",
initWizardSubtitle: "يتطلب Dekupai بيئة بايثون محلية لتشغيل نماذج الشبكة العصبية BiRefNet.",
configureEngine: "تكوين المحرك (تلقائي)",
stepVenv: "إنشاء بيئة افتراضية معزولة (.venv)",
stepPip: "ترقية مثبت حزم بايثون (pip)",
stepDeps: "تثبيت محرك الذكاء الاصطناعي PyTorch (نسخة المعالج CPU)",
stepLibs: "تثبيت مكتبة HuggingFace Transformers و PIL",
creditsVision: "تم التطوير تحت رؤية",
creditsExpertise: "بواسطة الخبرة البرمجية والهندسية لـ",
creditsDesc: "Dekupai هو مساحة عمل متقدمة للتعلم العميق توفر إزالة محلية ودقيقة للغاية للخلفيات باستخدام نماذج BiRefNet.",
closeCredits: "إغلاق",
dragDropText: "اسحب وأسقط الصورة هنا",
dragDropSub: "يدعم صيغ PNG أو JPG أو JPEG أو WebP",
orText: "أو",
browseFiles: "تصفح الملفات المحلية",
autoTrim: "قص تلقائي",
statusInit: "تهيئة نموذج الذكاء الاصطناعي...",
statusWeights: "تحميل أوزان الشبكة العصبية في الذاكرة...",
statusImporting: "استيراد المكتبات...",
statusDetecting: "الكشف عن الجهاز...",
statusUsingDevice: "الجهاز المستخدم: ",
statusLoadingModel: "تحميل نموذج BiRefNet...",
statusPreprocessing: "تحميل ومعالجة الصورة مسبقاً...",
statusInference: "تشغيل خوارزمية إزالة الخلفية...",
statusGeneratingMask: "إنشاء قناع الشفافية...",
statusSaving: "حفظ الصورة الشفافة الناتجة...",
statusDone: "اكتمل!",
subFirstRun: "التشغيل الأول يقوم بتنزيل النموذج (~200 ميجابايت)",
subInference: "استخراج التفاصيل والشفافية الناعمة...",
subSaving: "تجميع حاوية RGBA عالية الدقة...",
subDefault: "يرجى الانتظار أثناء معالجة نموذج الذكاء الاصطناعي...",
setupReady: "جاهز لتكوين البيئة.",
setupCreatingVenv: "إنشاء بيئة افتراضية معزولة (.venv)...",
setupVenvCreated: "تم إنشاء البيئة الافتراضية بنجاح.",
setupVenvExists: "البيئة الافتراضية موجودة بالفعل.",
setupUpgradingPip: "ترقية مثبت حزم بايثون (pip)...",
setupInstallingTorch: "تثبيت محرك الذكاء الاصطناعي PyTorch (نسخة CPU)...",
setupDownloadingTorch: "تنزيل torch والاعتمادات...",
setupInstallingDeps: "تثبيت الحزم التي تم جمعها...",
setupCompleted: "اكتمل التكوين! استمتع بإزالة الخلفية.",
setupFailed: "فشل الإعداد. تحقق من تبعيات النظام.",
exportingMasterpiece: "تصدير العمل الفني...",
toastExportSuccess: "تم التصدير بنجاح",
toastExportSuccessMsg: "تم حفظ الملف بنجاح: ",
toastExportFailed: "فشل التصدير",
toastExportFailedMsg: "فشلت عملية الحفظ.",
toastExportError: "خطأ في التصدير",
showInExplorer: "عرض في مستكشف الملفات",
errorModalTitle: "خطأ في المعالجة",
errorModalSubtitle: "واجهتنا مشكلة أثناء تقسيم صورتك باستخدام الذكاء الاصطناعي.",
errorModalSuggestion: "قد يكون العنصر في الصورة التي تريد قصها مدمجًا للغاية أو غير قابل للتمييز بصريًا عن خلفيته. يرجى محاولة استخدام صورة أخرى تحتوي على عنصر أكثر وضوحًا وتباينًا.",
closeBtn: "إغلاق"
}
};
let currentLang = 'en'; // default English base language
function changeLanguage(lang) {
currentLang = lang;
localStorage.setItem('dekupai_lang', lang);
if (elements.langSelect) {
elements.langSelect.value = lang;
}
// Update HTML elements marked with data-i18n
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
if (TRANSLATIONS[lang] && TRANSLATIONS[lang][key]) {
// If element contains an inner span for icon text
const span = el.querySelector('span');
if (span) {
span.innerText = TRANSLATIONS[lang][key];
} else if (el.tagName === 'OPTION') {
el.innerText = TRANSLATIONS[lang][key];
} else {
// Preserving SVG structure inside tags
const svg = el.querySelector('svg');
if (svg) {
// If it has SVG, clear other text and append text node safely
const textNodes = Array.from(el.childNodes).filter(node => node.nodeType === Node.TEXT_NODE);
if (textNodes.length > 0) {
textNodes[textNodes.length - 1].nodeValue = " " + TRANSLATIONS[lang][key];
} else {
el.appendChild(document.createTextNode(" " + TRANSLATIONS[lang][key]));
}
} else {
el.innerText = TRANSLATIONS[lang][key];
}
}
}
});
// Handle RTL for Arabic
if (lang === 'ar') {
document.body.dir = 'rtl';
} else {
document.body.dir = 'ltr';
}
// Update original title
document.title = TRANSLATIONS[lang].title;
}
function getTrans(key) {
return (TRANSLATIONS[currentLang] && TRANSLATIONS[currentLang][key]) || TRANSLATIONS['en'][key] || key;
}
function initLanguage() {
const savedLang = localStorage.getItem('dekupai_lang') || 'en';
if (elements.langSelect) {
elements.langSelect.value = savedLang;
elements.langSelect.addEventListener('change', (e) => {
changeLanguage(e.target.value);
});
}
changeLanguage(savedLang);
}
// --- CONTROL TOWER CONTROLLER ---
function setupControlTowerListeners() {
// Remove Background click
elements.btnRemoveBg.addEventListener('click', executeBackgroundRemoval);
// Background Settings Tabs
elements.tabBgTrans.addEventListener('click', () => switchBgMode('transparent'));
elements.tabBgSolid.addEventListener('click', () => switchBgMode('solid'));
elements.tabBgGrad.addEventListener('click', () => switchBgMode('gradient'));
// Export Quality Slider
elements.exportFormat.addEventListener('change', (e) => {
if (e.target.value === 'png') {
elements.rowQualitySlider.classList.add('hidden');
} else {
elements.rowQualitySlider.classList.remove('hidden');
}
});
elements.exportQuality.addEventListener('input', (e) => {
elements.exportQualityVal.innerText = `${e.target.value}%`;
});
// Export MASTERPIECE Click
elements.btnExport.addEventListener('click', executeExport);
}
// --- BIREFNET EXECUTION PIPELINE ---
async function executeBackgroundRemoval() {
if (!currentImagePath) return;
elements.btnRemoveBg.classList.add('hidden');
elements.loaderBox.classList.remove('hidden');
elements.loaderText.innerText = getTrans('statusInit');
elements.loaderSubtext.innerText = getTrans('statusWeights');
// Get unique temporary output file path
const tempOut = await window.api.getTempPath('transparent_result.png');
// Listen to execution progress
window.api.onProcessProgress((progress) => {
if (progress.includes("[STATUS]")) {
const statusText = progress.replace("[STATUS] ", "").trim();
// Let's map statusText to localized keys
if (statusText.includes("Importing libraries")) {
elements.loaderText.innerText = getTrans('statusImporting');
elements.loaderSubtext.innerText = getTrans('subDefault');
} else if (statusText.includes("Detecting device")) {
elements.loaderText.innerText = getTrans('statusDetecting');
elements.loaderSubtext.innerText = getTrans('subDefault');
} else if (statusText.startsWith("Using device:")) {
const device = statusText.replace("Using device:", "").trim();
elements.loaderText.innerText = getTrans('statusUsingDevice') + device;
elements.loaderSubtext.innerText = getTrans('subDefault');
} else if (statusText.includes("Loading BiRefNet model")) {
elements.loaderText.innerText = getTrans('statusLoadingModel');
elements.loaderSubtext.innerText = getTrans('subFirstRun');
} else if (statusText.includes("Loading and preprocessing image")) {
elements.loaderText.innerText = getTrans('statusPreprocessing');
elements.loaderSubtext.innerText = getTrans('subDefault');
} else if (statusText.includes("Running background removal inference")) {
elements.loaderText.innerText = getTrans('statusInference');
elements.loaderSubtext.innerText = getTrans('subInference');
} else if (statusText.includes("Generating transparency mask")) {
elements.loaderText.innerText = getTrans('statusGeneratingMask');
elements.loaderSubtext.innerText = getTrans('subInference');
} else if (statusText.includes("Saving output transparent image")) {
elements.loaderText.innerText = getTrans('statusSaving');
elements.loaderSubtext.innerText = getTrans('subSaving');
} else if (statusText.includes("DONE")) {
elements.loaderText.innerText = getTrans('statusDone');
elements.loaderSubtext.innerText = "";
} else {
// Fallback for any other status logs
elements.loaderText.innerText = statusText;
}
}
});
try {
const result = await window.api.removeBackground(currentImagePath, tempOut);
if (result.success) {
processedTempPath = result.outputPath;
showToast(getTrans('toastTitleSuccess'), getTrans('toastTrimCompleteMsg'), "success");
// Update workspace preview image source to output transparent PNG
// Add timestamp to bypass browser cache refreshing
const freshSrc = `file://${processedTempPath}?t=${Date.now()}`;
// Re-initialize CropperJS on the new image src
elements.imageElement.src = freshSrc;
// Wait for image source update to build the cropper safely
setTimeout(() => {
initCropper();
// Unlock background customization and export
elements.secCanvasBg.classList.remove('disabled-group');
elements.secExport.classList.remove('disabled-group');
elements.ctrlAutoTrim.disabled = false; // Unlock trim feature!
}, 300);
} else {
elements.errorOverlayLog.innerText = result.error || getTrans('toastSetupErrorMsg');
elements.errorOverlay.classList.remove('hidden');
}
} catch (err) {
elements.errorOverlayLog.innerText = err.message;
elements.errorOverlay.classList.remove('hidden');
} finally {
elements.loaderBox.classList.add('hidden');
elements.btnRemoveBg.classList.remove('hidden');
}
}
// --- CANVAS CUSTOM BACKGROUND SETTINGS ---
function resetBgSettings() {
elements.secCanvasBg.classList.add('disabled-group');
elements.secExport.classList.add('disabled-group');
switchBgMode('transparent');
}
function switchBgMode(mode) {
activeBgMode = mode;
// Adjust active tab css
elements.tabBgTrans.classList.remove('active');
elements.tabBgSolid.classList.remove('active');
elements.tabBgGrad.classList.remove('active');
elements.subpanelSolid.classList.add('hidden');
elements.subpanelGradient.classList.add('hidden');
if (mode === 'transparent') {
elements.tabBgTrans.classList.add('active');
elements.canvasBgOverlay.style.opacity = '0';
} else if (mode === 'solid') {
elements.tabBgSolid.classList.add('active');
elements.subpanelSolid.classList.remove('hidden');
elements.canvasBgOverlay.style.background = activeSolidColor;
elements.canvasBgOverlay.style.opacity = '1';
} else if (mode === 'gradient') {
elements.tabBgGrad.classList.add('active');
elements.subpanelGradient.classList.remove('hidden');
// Draw current active gradient stops onto the workspace preview
const cssGrad = buildCssGradientString(activeGradientStops);
elements.canvasBgOverlay.style.background = cssGrad;
elements.canvasBgOverlay.style.opacity = '1';
}
}
function buildCssGradientString(stops) {
return `linear-gradient(135deg, ${stops[0].color}, ${stops[1].color})`;
}
function setupBgSwatches() {
// Solid Swatches
const solidSwatches = document.querySelectorAll('.swatch:not(.custom)');
solidSwatches.forEach(swatch => {
swatch.addEventListener('click', (e) => {
solidSwatches.forEach(s => s.classList.remove('active'));
e.target.classList.add('active');
activeSolidColor = e.target.getAttribute('data-color');
elements.canvasBgOverlay.style.background = activeSolidColor;
});
});
// Custom Color Picker
elements.customColorPicker.addEventListener('input', (e) => {
activeSolidColor = e.target.value;
elements.canvasBgOverlay.style.background = activeSolidColor;
// Highlight custom swatch borders
elements.btnCustomColorPicker.classList.add('active');
solidSwatches.forEach(s => s.classList.remove('active'));
});
// Gradient Swatches
const gradSwatches = document.querySelectorAll('.grad-swatch');
gradSwatches.forEach(swatch => {
swatch.addEventListener('click', (e) => {
gradSwatches.forEach(s => s.classList.remove('active'));
e.target.classList.add('active');
const gradKey = e.target.className.split(' ').find(cls => cls.startsWith('grad-'));
if (GRADIENTS[gradKey]) {
activeGradientStops = GRADIENTS[gradKey];
elements.canvasBgOverlay.style.background = e.target.getAttribute('data-gradient');
}
});
});
}
// --- CANVAS EXPORT AND MERGING ENGINE ---
async function executeExport() {
if (!cropper) return;
elements.btnExport.disabled = true;
elements.btnExport.innerHTML = `
${getTrans('exportingMasterpiece')}
`;
setTimeout(async () => {
try {
// Get the cropped canvas at original resolution
const croppedCanvas = cropper.getCroppedCanvas();
if (!croppedCanvas) {
throw new Error("Failed to extract active cropped canvas workspace.");
}
let finalCanvas = croppedCanvas;
// If solid or gradient background mode is selected, we burn it behind the subject
if (activeBgMode !== 'transparent') {
finalCanvas = document.createElement('canvas');
finalCanvas.width = croppedCanvas.width;
finalCanvas.height = croppedCanvas.height;
const ctx = finalCanvas.getContext('2d');
if (activeBgMode === 'solid') {
ctx.fillStyle = activeSolidColor;
ctx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
} else if (activeBgMode === 'gradient') {
// Render the 135deg gradient vector on final output canvas
const grad = ctx.createLinearGradient(0, 0, finalCanvas.width, finalCanvas.height);
activeGradientStops.forEach(stop => {
grad.addColorStop(stop.offset, stop.color);
});
ctx.fillStyle = grad;
ctx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
}
// Overlay transparent subject on top of custom background
ctx.drawImage(croppedCanvas, 0, 0);
}
// Read Export Format settings
const format = elements.exportFormat.value;
let mimeType = 'image/png';
let extension = 'png';
if (format === 'jpeg') {
mimeType = 'image/jpeg';
extension = 'jpg';
} else if (format === 'webp') {
mimeType = 'image/webp';
extension = 'webp';
}
const quality = parseFloat(elements.exportQuality.value) / 100;
const base64Data = finalCanvas.toDataURL(mimeType, quality);
// Create default filename suggestion
const nameWithoutExt = originalFileName.substring(0, originalFileName.lastIndexOf('.')) || originalFileName;
const exportDefaultName = `${nameWithoutExt}_no_bg.${extension}`;
// Open Save File dialog via context bridge
const result = await window.api.exportImage(base64Data, exportDefaultName);
if (result.success) {
showToastWithAction(
getTrans('toastExportSuccess'),
`${getTrans('toastExportSuccessMsg')}${result.filePath.split(/[\\/]/).pop()}`,
"success",
result.filePath
);
} else if (!result.canceled) {
showToast(getTrans('toastExportFailed'), result.error || getTrans('toastExportFailedMsg'), "error");
}
} catch (err) {
showToast(getTrans('toastExportError'), err.message, "error");
} finally {
// Re-enable button
elements.btnExport.disabled = false;
elements.btnExport.innerHTML = `
${getTrans('exportMasterpiece')}
`;
}
}, 100);
}
// --- GLOBAL TOAST SYSTEM IMPLEMENTATION ---
let toastTimeout = null;
function setupToastListeners() {
elements.btnCloseToast.addEventListener('click', hideToast);
}
function showToast(title, message, type = 'info') {
hideToast();
elements.toastTitle.innerText = title;
elements.toastMessage.innerText = message;
// Styling
elements.toastBanner.className = 'toast-notification glass-panel-inner';
if (type === 'success') elements.toastBanner.classList.add('success');
if (type === 'error') elements.toastBanner.classList.add('error');
elements.toastBanner.classList.remove('hidden');
// Set automatic dismissal
toastTimeout = setTimeout(hideToast, 5000);
}
function showToastWithAction(title, message, type = 'success', filePath) {
hideToast();
elements.toastTitle.innerText = title;
elements.toastMessage.innerHTML = `${message}
`;
elements.toastBanner.className = 'toast-notification glass-panel-inner';
if (type === 'success') elements.toastBanner.classList.add('success');
if (type === 'error') elements.toastBanner.classList.add('error');
elements.toastBanner.classList.remove('hidden');
// Bind click logic to "Show in File Explorer" button
setTimeout(() => {
const btnExplore = document.getElementById('btn-toast-explore');
if (btnExplore) {
btnExplore.addEventListener('click', () => {
window.api.showInExplorer(filePath);
});
}
}, 50);
// Keep it active longer since it contains interaction
toastTimeout = setTimeout(hideToast, 8000);
}
function hideToast() {
if (toastTimeout) {
clearTimeout(toastTimeout);
toastTimeout = null;
}
elements.toastBanner.classList.add('hidden');
}