From 27baa115e0f755460e2a7f32ed8f3c46c020f9e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Sat, 23 May 2026 10:48:43 +0300 Subject: [PATCH] feat: develop Electron wrapper, safe IPC bridge, and subprocess spawn pipelines --- main.js | 218 +++++++++++++++++++++++++++++++++++++++++++++++++++++ preload.js | 32 ++++++++ 2 files changed, 250 insertions(+) create mode 100644 main.js create mode 100644 preload.js diff --git a/main.js b/main.js new file mode 100644 index 0000000..b3980eb --- /dev/null +++ b/main.js @@ -0,0 +1,218 @@ +const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); +const path = require('path'); +const { spawn } = require('child_process'); +const fs = require('fs'); + +let mainWindow; + +function createWindow() { + mainWindow = new BrowserWindow({ + width: 1200, + height: 820, + minWidth: 1000, + minHeight: 700, + frame: false, // Sleek frameless design + transparent: false, // Set to false to avoid rendering bugs on some Windows setups + backgroundColor: '#0a0a0f', // Dark baseline background + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + contextIsolation: true, + nodeIntegration: false + } + }); + + mainWindow.loadFile(path.join(__dirname, 'index.html')); + + // Open DevTools if running in dev (optional) + // mainWindow.webContents.openDevTools(); +} + +app.whenReady().then(() => { + createWindow(); + + app.on('activate', function () { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); +}); + +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit(); +}); + +// Helper: Get Python Path +function getPythonPaths() { + const isWindows = process.platform === 'win32'; + const venvPython = isWindows + ? path.join(__dirname, '.venv', 'Scripts', 'python.exe') + : path.join(__dirname, '.venv', 'bin', 'python'); + + const systemPython = 'python'; + const hasVenv = fs.existsSync(venvPython); + + return { venvPython, systemPython, hasVenv }; +} + +// IPC: Check Python Setup Status +ipcMain.handle('check-python-setup', async () => { + const { hasVenv } = getPythonPaths(); + return { hasVenv }; +}); + +// IPC: Run Setup +ipcMain.handle('run-python-setup', async (event) => { + const { systemPython } = getPythonPaths(); + const setupScript = path.join(__dirname, 'backend', 'setup.py'); + + return new Promise((resolve) => { + console.log(`[Main Process] Spawning setup: ${systemPython} ${setupScript}`); + // Spawn host python to run setup.py + const process = spawn(systemPython, [setupScript], { shell: true }); + + let stderrData = ''; + + process.stdout.on('data', (data) => { + const output = data.toString(); + console.log(`[Setup Output] ${output.trim()}`); + const lines = output.split('\n'); + for (const line of lines) { + if (line.trim()) { + event.sender.send('setup-progress', line.trim()); + } + } + }); + + process.stderr.on('data', (data) => { + const errStr = data.toString(); + stderrData += errStr; + console.error(`[Setup Error] ${errStr.trim()}`); + event.sender.send('setup-progress', `[ERROR] ${errStr.trim()}`); + }); + + process.on('close', (code) => { + console.log(`[Main Process] Setup completed with exit code ${code}`); + resolve({ code, error: stderrData }); + }); + }); +}); + +// IPC: Run Background Removal Process +ipcMain.handle('remove-background', async (event, { inputPath, outputPath }) => { + const { venvPython, hasVenv } = getPythonPaths(); + + if (!hasVenv) { + return { success: false, error: 'Python virtual environment not set up. Please run setup first.' }; + } + + const processScript = path.join(__dirname, 'backend', 'process.py'); + + return new Promise((resolve) => { + console.log(`[Main Process] Spawning background removal: ${venvPython} ${processScript}`); + // Spawn venv python to run process.py + const args = ['--input', inputPath, '--output', outputPath]; + const process = spawn(venvPython, [processScript, ...args], { shell: true }); + + let stderrData = ''; + + process.stdout.on('data', (data) => { + const output = data.toString(); + console.log(`[Python Output] ${output.trim()}`); + const lines = output.split('\n'); + for (const line of lines) { + if (line.trim()) { + event.sender.send('process-progress', line.trim()); + } + } + }); + + process.stderr.on('data', (data) => { + const errStr = data.toString(); + stderrData += errStr; + console.error(`[Python Error] ${errStr.trim()}`); + event.sender.send('process-progress', `[ERROR] ${errStr.trim()}`); + }); + + process.on('close', (code) => { + console.log(`[Main Process] Process completed with exit code ${code}`); + if (code === 0) { + resolve({ success: true, outputPath }); + } else { + resolve({ success: false, error: stderrData || `Process failed with exit code ${code}` }); + } + }); + }); +}); + +// IPC: Select Image File Dialog +ipcMain.handle('select-image', async () => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ['openFile'], + filters: [ + { name: 'Images', extensions: ['jpg', 'jpeg', 'png', 'webp', 'bmp'] } + ] + }); + + if (!result.canceled && result.filePaths.length > 0) { + return result.filePaths[0]; + } + return null; +}); + +// IPC: Get Temp Output File Path +ipcMain.handle('get-temp-path', async (event, fileName) => { + const tempDir = app.getPath('temp'); + // Ensure unique name to avoid cache issues + const uniqueName = `birefnet_${Date.now()}_${fileName}`; + return path.join(tempDir, uniqueName); +}); + +// IPC: Export Image Dialog +ipcMain.handle('export-image', async (event, { base64Data, defaultName }) => { + const result = await dialog.showSaveDialog(mainWindow, { + defaultPath: defaultName || 'processed_image.png', + filters: [ + { name: 'PNG Image', extensions: ['png'] }, + { name: 'JPEG Image', extensions: ['jpg', 'jpeg'] }, + { name: 'WebP Image', extensions: ['webp'] } + ] + }); + + if (!result.canceled && result.filePath) { + try { + // Strip base64 headers if present + const base64Content = base64Data.replace(/^data:image\/\w+;base64,/, ""); + fs.writeFileSync(result.filePath, base64Content, 'base64'); + return { success: true, filePath: result.filePath }; + } catch (err) { + return { success: false, error: err.message }; + } + } + return { success: false, canceled: true }; +}); + +// IPC: Show in File Explorer +ipcMain.handle('show-in-explorer', async (event, filePath) => { + if (fs.existsSync(filePath)) { + shell.showItemInFolder(filePath); + return true; + } + return false; +}); + +// IPC: Frameless Window Controls +ipcMain.on('window-minimize', () => { + if (mainWindow) mainWindow.minimize(); +}); + +ipcMain.on('window-maximize', () => { + if (mainWindow) { + if (mainWindow.isMaximized()) { + mainWindow.unmaximize(); + } else { + mainWindow.maximize(); + } + } +}); + +ipcMain.on('window-close', () => { + if (mainWindow) mainWindow.close(); +}); diff --git a/preload.js b/preload.js new file mode 100644 index 0000000..cccc1f6 --- /dev/null +++ b/preload.js @@ -0,0 +1,32 @@ +const { contextBridge, ipcRenderer } = require('electron'); + +contextBridge.exposeInMainWorld('api', { + // Python setup APIs + checkPythonSetup: () => ipcRenderer.invoke('check-python-setup'), + runPythonSetup: () => ipcRenderer.invoke('run-python-setup'), + onSetupProgress: (callback) => { + // Clear previous listener to prevent leaks + ipcRenderer.removeAllListeners('setup-progress'); + ipcRenderer.on('setup-progress', (event, data) => callback(data)); + }, + + // Processing APIs + removeBackground: (inputPath, outputPath) => + ipcRenderer.invoke('remove-background', { inputPath, outputPath }), + onProcessProgress: (callback) => { + ipcRenderer.removeAllListeners('process-progress'); + ipcRenderer.on('process-progress', (event, data) => callback(data)); + }, + + // File Operations + selectImage: () => ipcRenderer.invoke('select-image'), + getTempPath: (fileName) => ipcRenderer.invoke('get-temp-path', fileName), + exportImage: (base64Data, defaultName) => + ipcRenderer.invoke('export-image', { base64Data, defaultName }), + showInExplorer: (filePath) => ipcRenderer.invoke('show-in-explorer', filePath), + + // Window Controls + minimizeWindow: () => ipcRenderer.send('window-minimize'), + maximizeWindow: () => ipcRenderer.send('window-maximize'), + closeWindow: () => ipcRenderer.send('window-close') +});