📄 老照片修复 源码

开源透明,所有处理逻辑在此,你也可以自己部署

📋 全部 photo-restore.php photo-restore-proxy.php server.js
Node.js Sharp 处理引擎 262 行 9.5 KB ⬇️ 下载此文件
server.js ⬇️ 下载
const http = require('http');
const QRCode = require('qrcode');
const { PDFDocument } = require('pdf-lib');
const { IncomingForm } = require('formidable');
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');
const os = require('os');

const PORT = 3001;
const TMP_DIR = path.join(os.tmpdir(), 'tools_server');
const MAX_TMP_AGE = 60 * 60 * 1000; // 1 hour

// Ensure temp directory exists and clean old files on startup
if (!fs.existsSync(TMP_DIR)) {
  fs.mkdirSync(TMP_DIR, { recursive: true });
} else {
  // Clean leftover files older than 1 hour
  try {
    const now = Date.now();
    const files = fs.readdirSync(TMP_DIR);
    for (const f of files) {
      const fp = path.join(TMP_DIR, f);
      try {
        const stat = fs.statSync(fp);
        if (now - stat.mtimeMs > MAX_TMP_AGE) {
          fs.unlinkSync(fp);
          console.log('Cleaned old temp file:', f);
        }
      } catch(e) {}
    }
  } catch(e) { console.error('Startup cleanup error:', e.message); }
}

// Periodic cleanup every 30 minutes
setInterval(() => {
  try {
    const now = Date.now();
    const files = fs.readdirSync(TMP_DIR);
    for (const f of files) {
      const fp = path.join(TMP_DIR, f);
      try {
        const stat = fs.statSync(fp);
        if (now - stat.mtimeMs > MAX_TMP_AGE) {
          fs.unlinkSync(fp);
        }
      } catch(e) {}
    }
  } catch(e) { console.error('Periodic cleanup error:', e.message); }
}, 30 * 60 * 1000);

// Safe file cleanup
function safeCleanup(...paths) {
  for (const p of paths) {
    try { if (p && fs.existsSync(p)) fs.unlinkSync(p); } catch(e) {}
  }
}

function parseForm(req) {
  return new Promise((resolve, reject) => {
    const form = new IncomingForm({ uploadDir: TMP_DIR, keepExtensions: true });
    form.parse(req, (err, fields, files) => {
      if (err) return reject(err);
      resolve({ fields, files });
    });
  });
}

function readBody(req) {
  return new Promise((resolve, reject) => {
    let body = '';
    req.on('data', chunk => body += chunk);
    req.on('end', () => {
      try { resolve(JSON.parse(body)); }
      catch (e) { reject(new Error('Invalid JSON')); }
    });
  });
}

const server = http.createServer(async (req, res) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

  if (req.method === 'OPTIONS') {
    res.writeHead(204);
    res.end();
    return;
  }

  try {
    const url = new URL(req.url, `http://localhost:${PORT}`);
    const route = url.pathname;

    if (route === '/qr' && req.method === 'POST') {
      // QR code: pure in-memory, no temp files
      const body = await readBody(req);
      const content = (body.content || '').trim();
      if (!content) throw new Error('Content is required');
      const size = parseInt(body.size) || 5;
      const level = (body.level || 'M').toUpperCase();
      const validLevels = ['L', 'M', 'Q', 'H'];
      const ecLevel = validLevels.includes(level) ? level : 'M';
      const width = [200, 350, 500, 800][[3, 5, 7, 10].indexOf(size)] || 350;
      const qrBuffer = await QRCode.toBuffer(content, {
        type: 'png', width, margin: 2, errorCorrectionLevel: ecLevel
      });
      res.writeHead(200, {
        'Content-Type': 'image/png',
        'Content-Length': qrBuffer.length,
        'Cache-Control': 'no-cache'
      });
      res.end(qrBuffer);

    } else if (route === '/pdf-merge' && req.method === 'POST') {
      const { files } = await parseForm(req);
      const pdfFiles = [];
      try {
        for (const key of Object.keys(files)) {
          const file = files[key];
          const arr = Array.isArray(file) ? file : [file];
          for (const f of arr) {
            if (f.mimetype === 'application/pdf' || f.originalFilename.toLowerCase().endsWith('.pdf')) {
              pdfFiles.push(f.filepath);
            }
          }
        }
        if (pdfFiles.length < 2) throw new Error('需要至少2个PDF文件');
        const mergedPdf = await PDFDocument.create();
        for (const pdfPath of pdfFiles) {
          const pdfBytes = fs.readFileSync(pdfPath);
          const pdf = await PDFDocument.load(pdfBytes, { ignoreEncryption: true });
          const indices = pdf.getPageIndices();
          const pages = await mergedPdf.copyPages(pdf, indices);
          pages.forEach(page => mergedPdf.addPage(page));
        }
        const mergedBytes = await mergedPdf.save();
        res.writeHead(200, {
          'Content-Type': 'application/pdf',
          'Content-Disposition': 'attachment; filename="merged.pdf"',
          'Content-Length': mergedBytes.length
        });
        res.end(mergedBytes);
      } finally {
        safeCleanup(...pdfFiles);
      }

    } else if (route === '/photo-restore' && req.method === 'POST') {
      const { files } = await parseForm(req);
      let inputPath = null;
      let outputPath = null;
      try {
        let imgFile = null;
        for (const key of Object.keys(files)) {
          const file = files[key];
          const arr = Array.isArray(file) ? file : [file];
          for (const f of arr) {
            if (f.mimetype && f.mimetype.startsWith('image/')) { imgFile = f; break; }
          }
          if (imgFile) break;
        }
        if (!imgFile) throw new Error('请上传一张图片');
        inputPath = imgFile.filepath;
        outputPath = path.join(TMP_DIR, 'restored_' + Date.now() + '.jpg');
        const metadata = await sharp(inputPath).metadata();
        let pipeline = sharp(inputPath);
        if (metadata.channels && metadata.channels >= 3) {
          pipeline = pipeline.normalize()
            .modulate({ brightness: 1.05, saturation: 1.15 })
            .sharpen({ sigma: 1.2, m1: 0, m2: 3, x1: 3, y2: 15, y3: 15 });
        } else {
          pipeline = pipeline.normalize()
            .sharpen({ sigma: 1.5, m1: 0, m2: 3, x1: 3, y2: 15, y3: 15 });
        }
        await pipeline.jpeg({ quality: 92, mozjpeg: true }).toFile(outputPath);
        const resultBuffer = fs.readFileSync(outputPath);
        res.writeHead(200, {
          'Content-Type': 'image/jpeg',
          'Content-Length': resultBuffer.length,
          'Cache-Control': 'no-cache'
        });
        res.end(resultBuffer);
      } finally {
        safeCleanup(inputPath, outputPath);
      }


    } else if (route === '/stats') {
      var counterFile = '/www/wwwroot/273059_cn/data/counter.json';
      var count = 0, updated = null;
      try {
        var data = fs.readFileSync(counterFile, 'utf-8');
        var parsed = JSON.parse(data);
        count = parsed.count || 0;
        updated = parsed.updated || null;
      } catch(e) {}
      var stats = {
        uptime: process.uptime(),
        memory: process.memoryUsage(),
        count: count,
        updated: updated,
        node: process.version,
        platform: process.platform,
        arch: process.arch,
        pid: process.pid,
        pages: [
          { name: '首页', url: 'https://273059.cn/' },
          { name: '不务正业', url: 'https://273059.cn/games/' },
          { name: '瞎捣鼓', url: 'https://273059.cn/tools/' },
          { name: '交作业', url: 'https://273059.cn/download/' },
          { name: '留言', url: 'https://273059.cn/liuyan.php' },
          { name: '关于', url: 'https://273059.cn/blog/' },
          { name: '毒鸡汤', url: 'https://273059.cn/tools/soup.php' },
          { name: '极客工具箱', url: 'https://273059.cn/tools/geek-tools.php' },
          { name: '图片压缩', url: 'https://273059.cn/tools/image-compress.php' },
          { name: '二维码生成', url: 'https://273059.cn/tools/qr-generator.php' },
          { name: 'PDF合并', url: 'https://273059.cn/tools/pdf-merge.php' },
          { name: '网络测速', url: 'https://273059.cn/tools/speed-test.php' },
          { name: '天气卡片', url: 'https://273059.cn/tools/weather-card.php' },
          { name: '老照片修复', url: 'https://273059.cn/tools/photo-restore.php' },
          { name: '雷电战机', url: 'https://273059.cn/games/thunder-tank/' },
          { name: '贪吃蛇', url: 'https://273059.cn/games/snake/' },
          { name: '打字练习', url: 'https://273059.cn/python-typing-game.html' },
          { name: '仪表盘', url: 'https://273059.cn/tools/dashboard.php' }
        ],
        timestamp: new Date().toISOString()
      };
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify(stats));

    } else if (route === '/counter') {
      const counterFile = '/www/wwwroot/273059_cn/data/counter.json';
      let count = 0;
      try {
        const data = fs.readFileSync(counterFile, 'utf-8');
        count = JSON.parse(data).count || 0;
      } catch(e) {}
      if (req.method === 'POST') {
        count++;
        try {
          const dirName = path.dirname(counterFile);
          if (!fs.existsSync(dirName)) fs.mkdirSync(dirName, { recursive: true });
          fs.writeFileSync(counterFile, JSON.stringify({ count, updated: new Date().toISOString() }));
        } catch(e) { console.error('Counter write error:', e.message); }
      }
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ count }));

    } else {
      res.writeHead(404);
      res.end('Not found');
    }
  } catch (err) {
    console.error('Error:', err.message);
    res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end(err.message || 'Internal error');
  }
});

server.listen(PORT, '127.0.0.1', () => {
  console.log(`Tools server running on http://127.0.0.1:${PORT}`);
});