Files
dekupai/renderer.js
T

1150 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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')
};
// --- 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');
});
}
// 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("Image Changed", "Workspace cleared. You can now load a new image.", "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);
// Listen for pipeline setup output logs
window.api.onSetupProgress((progressText) => {
elements.setupStatusText.innerText = progressText;
// Parse step state
if (progressText.includes("Creating Python virtual environment")) {
setStepActive(elements.stepVenv);
elements.setupProgressBar.style.width = '15%';
} else if (progressText.includes("Virtual environment successfully created") || progressText.includes("Virtual environment already exists")) {
setStepDone(elements.stepVenv);
setStepActive(elements.stepPip);
elements.setupProgressBar.style.width = '30%';
} else if (progressText.includes("Upgrading pip")) {
setStepActive(elements.stepPip);
elements.setupProgressBar.style.width = '45%';
} else if (progressText.includes("Installing PyTorch")) {
setStepDone(elements.stepPip);
setStepActive(elements.stepDeps);
elements.setupProgressBar.style.width = '60%';
} else if (progressText.includes("Downloading torch") || progressText.includes("Installing collected packages")) {
elements.setupProgressBar.style.width = '75%';
} else if (progressText.includes("Setup completed successfully")) {
setStepDone(elements.stepDeps);
setStepDone(elements.stepLibs);
elements.setupProgressBar.style.width = '100%';
}
});
try {
const result = await window.api.runPythonSetup();
if (result.code === 0) {
elements.setupStatusText.innerText = "Configuration Complete! Enjoy background removal.";
showToast("Setup Success", "Python environment is fully configured.", "success");
setTimeout(() => {
elements.setupOverlay.classList.add('hidden');
initWorkspace();
}, 1500);
} else {
setStepFailed(elements.stepDeps);
elements.setupStatusText.innerText = "Setup failed. Check system dependencies.";
elements.btnStartSetup.disabled = false;
showToast("Setup Error", "Failed to build Python environment.", "error");
}
} catch (err) {
elements.setupStatusText.innerText = `Error: ${err.message}`;
elements.btnStartSetup.disabled = false;
showToast("Setup Error", 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.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"
},
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"
},
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 ist 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"
},
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: "Автообрезка"
},
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: "قص تلقائي"
}
};
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 = "Initializing AI model...";
elements.loaderSubtext.innerText = "Loading neural weights into memory...";
// 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] ", "");
elements.loaderText.innerText = statusText;
if (statusText.includes("Hugging Face")) {
elements.loaderSubtext.innerText = "This downloads ~200MB of weights on first run.";
} else if (statusText.includes("inference")) {
elements.loaderSubtext.innerText = "Extracting details and soft matting alpha...";
} else if (statusText.includes("Saving")) {
elements.loaderSubtext.innerText = "Assembling high-res RGBA container...";
}
}
});
try {
const result = await window.api.removeBackground(currentImagePath, tempOut);
if (result.success) {
processedTempPath = result.outputPath;
showToast("AI Success", "Image background removed successfully!", "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 {
showToast("Inference Error", result.error || "Execution terminated unexpectedly.", "error");
}
} catch (err) {
showToast("AI Fatal Error", err.message, "error");
} 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 = `
<svg class="loader-ring" viewBox="0 0 50 50" style="width:16px;height:16px;margin:0;"><circle cx="25" cy="25" r="20" fill="none" stroke-width="4" stroke="#fff"></circle></svg>
<span>Exporting masterpiece...</span>
`;
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(
"Export Successful",
`Saved file correctly: ${result.filePath.split(/[\\/]/).pop()}`,
"success",
result.filePath
);
} else if (!result.canceled) {
showToast("Export Failed", result.error || "Save operation failed.", "error");
}
} catch (err) {
showToast("Export Error", err.message, "error");
} finally {
// Re-enable button
elements.btnExport.disabled = false;
elements.btnExport.innerHTML = `
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
<span>Export Masterpiece</span>
`;
}
}, 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} <br><button id="btn-toast-explore" class="btn btn-secondary btn-sm" style="margin-top:6px;width:100%;">Show in File Explorer</button>`;
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');
}