设计一个功能全面的PHP在线图片压缩与处理工具方案。

设计一个功能全面的PHP在线图片压缩与处理工具方案。

一、系统架构设计

1. 技术栈

  • 后端: PHP 7.4+ (使用GD库、Imagick)
  • 前端: HTML5, CSS3, JavaScript (jQuery + 现代JS)
  • 存储: 本地文件系统 + 可选云存储
  • 队列: Redis + Beanstalkd (处理批量任务)

2. 目录结构

image-tool/
├── index.php              // 入口文件
├── config/                 // 配置文件
├── src/                    // 核心源码
│   ├── Controllers/        // 控制器
│   ├── Models/             // 数据模型
│   ├── Services/           // 业务逻辑
│   │   ├── ImageProcessor.php  // 图片处理核心
│   │   ├── CompressionService.php // 压缩服务
│   │   ├── CropService.php      // 裁剪服务
│   │   └── BatchService.php     // 批量处理
│   └── Utils/              // 工具类
├── public/                 // 公共资源
├── uploads/                // 上传目录
├── temp/                   // 临时文件
└── vendor/                 // Composer依赖

二、核心功能实现

1. 图片处理核心类

<?php
// src/Services/ImageProcessor.php

class ImageProcessor {
    private $image;
    private $imageInfo;
    private $imagick;
    
    public function __construct($filePath) {
        if (extension_loaded('imagick')) {
            $this->imagick = new \Imagick($filePath);
        } else {
            $this->imageInfo = getimagesize($filePath);
            $this->loadImageWithGD($filePath);
        }
    }
    
    /**
     * 压缩图片
     * @param int $quality 压缩质量 1-100
     * @param int $targetSize 目标文件大小(KB)
     * @param int $format 输出格式
     */
    public function compress($quality = 80, $targetSize = null, $format = null) {
        if ($this->imagick) {
            return $this->compressWithImagick($quality, $targetSize, $format);
        }
        return $this->compressWithGD($quality, $targetSize, $format);
    }
    
    /**
     * 智能压缩 - 根据目标大小自动调整质量
     */
    public function smartCompress($targetSizeKB) {
        $quality = 90;
        $outputPath = $this->compress($quality);
        $currentSize = filesize($outputPath) / 1024;
        
        while ($currentSize > $targetSizeKB && $quality > 10) {
            $quality -= 10;
            $outputPath = $this->compress($quality);
            $currentSize = filesize($outputPath) / 1024;
        }
        
        return $outputPath;
    }
    
    /**
     * 裁剪图片
     */
    public function crop($x, $y, $width, $height) {
        if ($this->imagick) {
            $this->imagick->cropImage($width, $height, $x, $y);
            return $this->imagick->getImageBlob();
        } else {
            $newImage = imagecreatetruecolor($width, $height);
            imagecopy($newImage, $this->image, 0, 0, $x, $y, $width, $height);
            return $newImage;
        }
    }
    
    /**
     * 缩放图片
     */
    public function resize($width, $height, $maintainRatio = true) {
        if ($maintainRatio) {
            $ratio = min($width / $this->getWidth(), $height / $this->getHeight());
            $width = round($this->getWidth() * $ratio);
            $height = round($this->getHeight() * $ratio);
        }
        
        if ($this->imagick) {
            $this->imagick->resizeImage($width, $height, \Imagick::FILTER_LANCZOS, 1);
            return $this->imagick;
        } else {
            $newImage = imagecreatetruecolor($width, $height);
            imagecopyresampled($newImage, $this->image, 0, 0, 0, 0, $width, $height, $this->getWidth(), $this->getHeight());
            return $newImage;
        }
    }
}

2. 压缩比例控制类

<?php
// src/Services/CompressionService.php

class CompressionService {
    private $compressionLevels = [
        'lossless' => ['quality' => 100, 'method' => 'PNG'],
        'high' => ['quality' => 85, 'method' => 'JPEG'],
        'medium' => ['quality' => 65, 'method' => 'JPEG'],
        'low' => ['quality' => 45, 'method' => 'JPEG'],
        'tiny' => ['quality' => 25, 'method' => 'JPEG']
    ];
    
    /**
     * 应用压缩级别
     */
    public function applyCompression($filePath, $level = 'medium', $format = null) {
        if (!isset($this->compressionLevels[$level])) {
            $level = 'medium';
        }
        
        $settings = $this->compressionLevels[$level];
        $processor = new ImageProcessor($filePath);
        
        return $processor->compress($settings['quality'], null, $format);
    }
    
    /**
     * 计算压缩率
     */
    public function calculateRatio($originalSize, $compressedSize) {
        $ratio = round((1 - ($compressedSize / $originalSize)) * 100, 2);
        return [
            'original' => $this->formatBytes($originalSize),
            'compressed' => $this->formatBytes($compressedSize),
            'saved' => $this->formatBytes($originalSize - $compressedSize),
            'ratio' => $ratio,
            'ratio_text' => "压缩了 {$ratio}%"
        ];
    }
    
    private function formatBytes($bytes, $precision = 2) {
        $units = ['B', 'KB', 'MB', 'GB'];
        $bytes = max($bytes, 0);
        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
        $pow = min($pow, count($units) - 1);
        $bytes /= pow(1024, $pow);
        
        return round($bytes, $precision) . ' ' . $units[$pow];
    }
}

3. 批量处理类

<?php
// src/Services/BatchService.php

class BatchService {
    private $queue;
    private $uploadDir;
    private $outputDir;
    private $batchId;
    
    public function __construct() {
        $this->queue = new \Beanstalkd\Client();
        $this->queue->connect();
        $this->batchId = uniqid('batch_', true);
    }
    
    /**
     * 添加批量任务
     */
    public function addBatchJob($files, $settings) {
        $jobData = [
            'batch_id' => $this->batchId,
            'files' => $files,
            'settings' => $settings,
            'timestamp' => time()
        ];
        
        // 创建任务目录
        $this->createBatchDirectory($this->batchId);
        
        // 将任务添加到队列
        $this->queue->put(json_encode($jobData));
        
        return [
            'batch_id' => $this->batchId,
            'total_files' => count($files),
            'status_url' => "/status?batch_id={$this->batchId}"
        ];
    }
    
    /**
     * 处理批量任务(由队列Worker执行)
     */
    public function processBatch($jobData) {
        $jobData = json_decode($jobData, true);
        $results = [];
        
        foreach ($jobData['files'] as $index => $file) {
            try {
                // 保存上传文件
                $filePath = $this->saveFile($file, $jobData['batch_id'], $index);
                
                // 处理图片
                $processor = new ImageProcessor($filePath);
                
                // 应用设置
                if (isset($jobData['settings']['compress'])) {
                    $processor->compress($jobData['settings']['quality']);
                }
                
                if (isset($jobData['settings']['crop'])) {
                    $processor->crop(
                        $jobData['settings']['crop']['x'],
                        $jobData['settings']['crop']['y'],
                        $jobData['settings']['crop']['width'],
                        $jobData['settings']['crop']['height']
                    );
                }
                
                // 保存处理后的图片
                $outputPath = $this->getOutputPath($file, $jobData['batch_id'], $index);
                $processor->save($outputPath, $jobData['settings']['format'] ?? null);
                
                // 计算压缩率
                $ratio = $this->calculateRatio($filePath, $outputPath);
                
                $results[] = [
                    'success' => true,
                    'original' => basename($file['name']),
                    'output' => basename($outputPath),
                    'ratio' => $ratio
                ];
                
            } catch (Exception $e) {
                $results[] = [
                    'success' => false,
                    'original' => basename($file['name']),
                    'error' => $e->getMessage()
                ];
            }
        }
        
        // 保存处理结果
        $this->saveResults($jobData['batch_id'], $results);
        
        return $results;
    }
    
    /**
     * 获取批量任务状态
     */
    public function getBatchStatus($batchId) {
        $statusFile = $this->getBatchDir($batchId) . '/status.json';
        
        if (file_exists($statusFile)) {
            return json_decode(file_get_contents($statusFile), true);
        }
        
        return [
            'batch_id' => $batchId,
            'status' => 'processing',
            'progress' => $this->getProgress($batchId)
        ];
    }
}

4. 前端界面(简化版)

<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
    <title>在线图片压缩处理工具</title>
    <link rel="stylesheet" href="css/style.css">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/blueimp-file-upload@10.32.0/js/jquery.fileupload.min.js"></script>
</head>
<body>
    <div class="container">
        <h1>在线图片压缩处理工具</h1>
        
        <!-- 上传区域 -->
        <div class="upload-area" id="dropzone">
            <i class="fas fa-cloud-upload-alt"></i>
            <p>拖拽图片到此处或点击上传</p>
            <input type="file" id="fileupload" name="files[]" multiple accept="image/*">
        </div>
        
        <!-- 处理选项 -->
        <div class="options-panel">
            <h3>处理选项</h3>
            
            <div class="option-group">
                <label>压缩级别:</label>
                <select id="compressionLevel">
                    <option value="lossless">无损压缩</option>
                    <option value="high">高质量 (85%)</option>
                    <option value="medium" selected>中等质量 (65%)</option>
                    <option value="low">低质量 (45%)</option>
                    <option value="tiny">极小文件 (25%)</option>
                </select>
            </div>
            
            <div class="option-group">
                <label>目标大小:</label>
                <input type="number" id="targetSize" placeholder="KB" min="10" max="10240">
                <button id="autoOptimize">智能优化</button>
            </div>
            
            <div class="option-group">
                <label>输出格式:</label>
                <select id="outputFormat">
                    <option value="auto">自动</option>
                    <option value="jpeg">JPEG</option>
                    <option value="png">PNG</option>
                    <option value="webp">WebP</option>
                    <option value="gif">GIF</option>
                </select>
            </div>
            
            <!-- 裁剪工具 -->
            <div class="crop-tools">
                <h4>裁剪设置</h4>
                <button id="enableCrop">启用裁剪</button>
                <div class="crop-options" style="display:none;">
                    <label>X: <input type="number" id="cropX" value="0"></label>
                    <label>Y: <input type="number" id="cropY" value="0"></label>
                    <label>宽度: <input type="number" id="cropWidth"></label>
                    <label>高度: <input type="number" id="cropHeight"></label>
                    <label>比例: 
                        <select id="aspectRatio">
                            <option value="free">自由</option>
                            <option value="1:1">1:1</option>
                            <option value="4:3">4:3</option>
                            <option value="16:9">16:9</option>
                        </select>
                    </label>
                </div>
            </div>
            
            <button id="processBtn" class="btn-primary">开始处理</button>
        </div>
        
        <!-- 文件列表 -->
        <div class="file-list">
            <table id="fileTable">
                <thead>
                    <tr>
                        <th>预览</th>
                        <th>文件名</th>
                        <th>原始大小</th>
                        <th>处理后大小</th>
                        <th>压缩率</th>
                        <th>操作</th>
                    </tr>
                </thead>
                <tbody id="fileListBody"></tbody>
            </table>
        </div>
        
        <!-- 批量操作 -->
        <div class="batch-actions">
            <button id="selectAll">全选</button>
            <button id="downloadAll">下载全部</button>
            <button id="clearAll">清空</button>
        </div>
        
        <!-- 进度条 -->
        <div class="progress-bar" style="display:none;">
            <div class="progress-fill"></div>
            <span class="progress-text">0%</span>
        </div>
    </div>
    
    <script src="js/main.js"></script>
</body>
</html>

5. JavaScript 交互

// public/js/main.js

$(document).ready(function() {
    let uploadedFiles = [];
    let batchId = null;
    
    // 文件上传配置
    $('#fileupload').fileupload({
        url: '/api/upload',
        dataType: 'json',
        dropZone: $('#dropzone'),
        add: function(e, data) {
            data.submit();
        },
        done: function(e, data) {
            if (data.result.success) {
                addFileToList(data.result.file);
                uploadedFiles.push(data.result.file);
            }
        },
        progressall: function(e, data) {
            let progress = parseInt(data.loaded / data.total * 100, 10);
            $('.progress-bar').show();
            $('.progress-fill').css('width', progress + '%');
            $('.progress-text').text(progress + '%');
        }
    });
    
    // 处理按钮点击
    $('#processBtn').click(function() {
        if (uploadedFiles.length === 0) {
            alert('请先上传文件');
            return;
        }
        
        let settings = {
            compressionLevel: $('#compressionLevel').val(),
            targetSize: $('#targetSize').val(),
            format: $('#outputFormat').val(),
            crop: $('#enableCrop').is(':checked') ? {
                x: $('#cropX').val(),
                y: $('#cropY').val(),
                width: $('#cropWidth').val(),
                height: $('#cropHeight').val()
            } : null
        };
        
        $.ajax({
            url: '/api/batch/process',
            method: 'POST',
            data: {
                files: uploadedFiles,
                settings: settings
            },
            success: function(response) {
                batchId = response.batch_id;
                pollBatchStatus(batchId);
            }
        });
    });
    
    // 轮询批量任务状态
    function pollBatchStatus(batchId) {
        let interval = setInterval(function() {
            $.get('/api/batch/status?batch_id=' + batchId, function(data) {
                if (data.status === 'completed') {
                    clearInterval(interval);
                    updateResults(data.results);
                }
                updateProgress(data.progress);
            });
        }, 2000);
    }
    
    // 更新文件列表显示结果
    function updateResults(results) {
        results.forEach(function(result, index) {
            let row = $('#fileListBody tr').eq(index);
            if (result.success) {
                row.find('.compressed-size').text(result.compressedSize);
                row.find('.ratio').text(result.ratio.ratio_text);
                row.find('.download-btn').show();
            } else {
                row.find('.status').text('处理失败: ' + result.error);
            }
        });
    }
    
    // 启用裁剪功能
    $('#enableCrop').change(function() {
        if ($(this).is(':checked')) {
            $('.crop-options').slideDown();
            // 初始化裁剪区域预览
            initCropPreview();
        } else {
            $('.crop-options').slideUp();
        }
    });
    
    // 裁剪比例选择
    $('#aspectRatio').change(function() {
        let ratio = $(this).val();
        if (ratio !== 'free') {
            let parts = ratio.split(':');
            let aspect = parts[0] / parts[1];
            // 根据比例调整裁剪框
            adjustCropBox(aspect);
        }
    });
    
    // 智能优化
    $('#autoOptimize').click(function() {
        let targetSize = $('#targetSize').val();
        if (!targetSize) {
            alert('请输入目标大小');
            return;
        }
        
        $.ajax({
            url: '/api/auto-optimize',
            method: 'POST',
            data: {
                files: uploadedFiles,
                targetSize: targetSize
            },
            success: function(response) {
                // 更新压缩级别建议
                $('#compressionLevel').val(response.suggested_level);
                alert('建议使用 ' + response.suggested_level + ' 压缩级别');
            }
        });
    });
    
    // 全选/取消全选
    $('#selectAll').click(function() {
        let checkboxes = $('.file-checkbox');
        let allChecked = checkboxes.filter(':checked').length === checkboxes.length;
        checkboxes.prop('checked', !allChecked);
    });
    
    // 下载全部
    $('#downloadAll').click(function() {
        if (batchId) {
            window.location.href = '/api/batch/download?batch_id=' + batchId;
        }
    });
});

6. API 路由处理

<?php
// index.php - 路由处理

require_once 'vendor/autoload.php';

$uri = $_SERVER['REQUEST_URI'];
$method = $_SERVER['REQUEST_METHOD'];

// 简单路由
switch ($uri) {
    case '/api/upload':
        if ($method === 'POST') {
            handleUpload();
        }
        break;
        
    case '/api/batch/process':
        if ($method === 'POST') {
            handleBatchProcess();
        }
        break;
        
    case '/api/batch/status':
        if ($method === 'GET') {
            handleBatchStatus();
        }
        break;
        
    case '/api/auto-optimize':
        if ($method === 'POST') {
            handleAutoOptimize();
        }
        break;
        
    default:
        // 返回前端页面
        readfile('public/index.html');
}

function handleUpload() {
    $uploader = new \App\Services\UploadService();
    $result = $uploader->upload($_FILES['files']);
    
    header('Content-Type: application/json');
    echo json_encode($result);
}

function handleBatchProcess() {
    $input = json_decode(file_get_contents('php://input'), true);
    $batchService = new \App\Services\BatchService();
    
    $result = $batchService->addBatchJob(
        $input['files'],
        $input['settings']
    );
    
    header('Content-Type: application/json');
    echo json_encode($result);
}

function handleBatchStatus() {
    $batchId = $_GET['batch_id'];
    $batchService = new \App\Services\BatchService();
    
    $status = $batchService->getBatchStatus($batchId);
    
    header('Content-Type: application/json');
    echo json_encode($status);
}

function handleAutoOptimize() {
    $input = json_decode(file_get_contents('php://input'), true);
    $optimizer = new \App\Services\OptimizationService();
    
    $suggestion = $optimizer->suggestCompressionLevel(
        $input['files'],
        $input['targetSize']
    );
    
    header('Content-Type: application/json');
    echo json_encode($suggestion);
}

7. 数据库设计(可选,用于记录历史)

-- 创建用户上传记录表
CREATE TABLE `uploads` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `batch_id` VARCHAR(64) NOT NULL,
    `original_name` VARCHAR(255) NOT NULL,
    `file_path` VARCHAR(500) NOT NULL,
    `file_size` INT NOT NULL,
    `file_type` VARCHAR(50) NOT NULL,
    `compressed_size` INT,
    `compression_ratio` DECIMAL(5,2),
    `settings` TEXT,
    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    `download_count` INT DEFAULT 0,
    INDEX idx_batch (`batch_id`)
);

-- 创建批量任务表
CREATE TABLE `batch_jobs` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `batch_id` VARCHAR(64) NOT NULL UNIQUE,
    `status` ENUM('pending', 'processing', 'completed', 'failed') DEFAULT 'pending',
    `total_files` INT NOT NULL,
    `processed_files` INT DEFAULT 0,
    `settings` TEXT,
    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    `completed_at` TIMESTAMP NULL,
    INDEX idx_status (`status`)
);

三、部署配置

1. PHP 配置要求

; php.ini 必要配置
upload_max_filesize = 100M
post_max_size = 100M
max_execution_time = 300
memory_limit = 512M

2. Nginx 配置

server {
    listen 80;
    server_name image-tool.local;
    root /var/www/image-tool/public;
    
    client_max_body_size 100M;
    
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
    
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
    
    # 上传文件目录
    location /uploads {
        internal;  # 禁止直接访问
        alias /var/www/image-tool/uploads;
    }
}

3. 队列 Worker 脚本

<?php
// worker.php - 后台处理批量任务

require_once 'vendor/autoload.php';

use Pheanstalk\Pheanstalk;

$pheanstalk = Pheanstalk::create('127.0.0.1');
$batchService = new \App\Services\BatchService();

while (true) {
    $job = $pheanstalk
        ->watch('image_tasks')
        ->ignore('default')
        ->reserve();
    
    try {
        $result = $batchService->processBatch($job->getData());
        $pheanstalk->delete($job);
        
        // 记录日志
        error_log("Processed job: " . $job->getId());
    } catch (Exception $e) {
        $pheanstalk->bury($job);
        error_log("Failed job: " . $job->getId() . " - " . $e->getMessage());
    }
    
    sleep(1);
}

四、功能特点总结

  1. 多格式支持: JPEG, PNG, GIF, WebP, BMP
  2. 灵活压缩: 支持质量调节、目标大小智能压缩
  3. 批量处理: 队列系统支持大量图片并发处理
  4. 实时预览: 前端实时显示处理效果
  5. 裁剪功能: 支持自由裁剪和固定比例裁剪
  6. 压缩统计: 显示压缩率、节省空间等数据
  7. 下载选项: 单文件下载、批量打包下载
  8. 安全性: 文件类型验证、上传目录保护

这个方案提供了一个完整的在线图片处理工具,可根据实际需求进行调整和扩展。

暂无评论