From 990f6ae372bcfb474b60251a276abbb4ae9cc1ff Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 7 Mar 2026 22:41:49 -0600 Subject: [PATCH] Add backend server with API endpoints --- backend/src/server.js | 157 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 backend/src/server.js diff --git a/backend/src/server.js b/backend/src/server.js new file mode 100644 index 0000000..c8f3ea1 --- /dev/null +++ b/backend/src/server.js @@ -0,0 +1,157 @@ +const express = require('express'); +const cors = require('cors'); +const helmet = require('helmet'); +const multer = require('multer'); +const sharp = require('sharp'); +const path = require('path'); +const fs = require('fs').promises; +require('dotenv').config(); + +const app = express(); +const PORT = process.env.PORT || 3000; +const MAX_FILE_SIZE = (process.env.MAX_FILE_SIZE || 10) * 1024 * 1024; + +// Middleware +app.use(helmet()); +app.use(cors()); +app.use(express.json()); + +// Serve static frontend +app.use(express.static(path.join(__dirname, '../public'))); + +// Configure multer for memory storage +const upload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: MAX_FILE_SIZE }, + fileFilter: (req, file, cb) => { + if (file.mimetype === 'image/png') { + cb(null, true); + } else { + cb(new Error('Only PNG files are allowed')); + } + } +}); + +// Health check endpoint +app.get('/api/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +// Process image endpoint +app.post('/api/process', upload.single('image'), async (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ error: 'No image file provided' }); + } + + const { width, height, quality, maintainAspectRatio } = req.body; + + // Parse dimensions + const targetWidth = width ? parseInt(width) : null; + const targetHeight = height ? parseInt(height) : null; + const compressionQuality = quality ? parseInt(quality) : 80; + + // Validate inputs + if (targetWidth && (targetWidth < 1 || targetWidth > 10000)) { + return res.status(400).json({ error: 'Width must be between 1 and 10000' }); + } + if (targetHeight && (targetHeight < 1 || targetHeight > 10000)) { + return res.status(400).json({ error: 'Height must be between 1 and 10000' }); + } + if (compressionQuality < 1 || compressionQuality > 100) { + return res.status(400).json({ error: 'Quality must be between 1 and 100' }); + } + + // Process image with Sharp + let pipeline = sharp(req.file.buffer); + + // Resize if dimensions provided + if (targetWidth || targetHeight) { + const resizeOptions = { + width: targetWidth, + height: targetHeight, + fit: maintainAspectRatio === 'true' ? 'inside' : 'fill' + }; + pipeline = pipeline.resize(resizeOptions); + } + + // Apply PNG compression + pipeline = pipeline.png({ + quality: compressionQuality, + compressionLevel: 9, + adaptiveFiltering: true + }); + + // Convert to buffer + const processedBuffer = await pipeline.toBuffer(); + + // Get metadata for response + const metadata = await sharp(processedBuffer).metadata(); + + // Send processed image + res.set({ + 'Content-Type': 'image/png', + 'Content-Length': processedBuffer.length, + 'Content-Disposition': `attachment; filename="processed-${Date.now()}.png"`, + 'X-Image-Width': metadata.width, + 'X-Image-Height': metadata.height, + 'X-Original-Size': req.file.size, + 'X-Processed-Size': processedBuffer.length + }); + + res.send(processedBuffer); + + } catch (error) { + console.error('Image processing error:', error); + res.status(500).json({ error: 'Failed to process image', details: error.message }); + } +}); + +// Get image metadata endpoint +app.post('/api/metadata', upload.single('image'), async (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ error: 'No image file provided' }); + } + + const metadata = await sharp(req.file.buffer).metadata(); + + res.json({ + width: metadata.width, + height: metadata.height, + format: metadata.format, + size: req.file.size, + hasAlpha: metadata.hasAlpha, + channels: metadata.channels + }); + + } catch (error) { + console.error('Metadata extraction error:', error); + res.status(500).json({ error: 'Failed to extract metadata', details: error.message }); + } +}); + +// Error handling middleware +app.use((err, req, res, next) => { + if (err instanceof multer.MulterError) { + if (err.code === 'LIMIT_FILE_SIZE') { + return res.status(400).json({ error: `File too large. Max size is ${MAX_FILE_SIZE / 1024 / 1024}MB` }); + } + return res.status(400).json({ error: err.message }); + } + + console.error('Server error:', err); + res.status(500).json({ error: 'Internal server error' }); +}); + +// Serve frontend for all other routes +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, '../public/index.html')); +}); + +// Start server +app.listen(PORT, () => { + console.log(`PNGer server running on port ${PORT}`); + console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); + console.log(`Max file size: ${MAX_FILE_SIZE / 1024 / 1024}MB`); +}); \ No newline at end of file