PHP构建实现高斯模糊处理平台


简简单单就好

PHP构建实现高斯模糊处理平台

  • 小米这澎湃OS,壁纸设置模糊,居然是高强度模糊,连强度都不给设置下,太拉了,相册里也没有单独高斯模糊的选项。
  • 之前锤子手机设置壁纸的模糊强度我很喜欢,适中,但现在一看小米连这么简单的功能都不做好,怎么越做越倒退了。
  • 既然系统设置不了,那我上浏览器找平台模糊下吧,这澎湃,居然只给调用系统相册,调用系统相册就算了,还是不完整的系统相册,只显示相机(DCIM),截图(Screenshot)和下载内容(Download),这按时间排序,一直划都划不到底,要上传我那壁纸真不容易。
  • 好不容易上传了吧,这些平台一个个处理的好慢。
  • 后来还是说在大早上打开了PS,一拉一点一拖,心目中完美的高斯模糊就搞好了,就跟这页面一样,简单又快速

预览地址

http://mxzfun.xyz/GB/fontPage.html

页面展示

PC端

默认页面(未上传图片)

默认页面(已上传图片并对图片进行模糊处理)

移动端

默认页面(未上传图片)

默认页面(已上传图片并对图片进行模糊处理)

项目结构

    fontPage.html # 前端页面
    process_image.php # 后端逻辑(GD 库图像处理函数)

前端页面设计

四个核心模块:

  • 图片上传区:通过input[type="file"]控件接收用户上传的图片,并用FileReaderAPI 将图片转为 DataURL 在页面预览。
  • 模糊强度调节:使用 range 滑块控件(min=0, max=50)让用户直观调整模糊程度,实时显示当前数值,并通过防抖处理(debounce)避免频繁触发后端请求。
  • 进度反馈:根据模糊强度预估处理时间(强度越高需要迭代次数越多,耗时越长),模拟进度条动画,让用户感知处理状态。
  • 结果展示与下载:处理完成后更新预览图,显示 "渲染完成" 状态,并提供下载按钮,通过URL.createObjectURL生成临时链接实现图片下载。

后端逻辑实现

  • 参数校验:检查是否收到图片文件和模糊强度参数,若缺失则返回 400 错误。
  • 图片加载:通过imagecreatefromstring函数读取上传的图片数据,支持多种常见图片格式(PNG、JPG 等)。
  • 高斯模糊处理:这里的关键是将用户输入的模糊强度(0-50)转换为算法迭代次数(max(1, blur * 2)),通过 GD 库的imagefilter函数重复应用IMG_FILTER_GAUSSIAN_BLUR滤镜,实现可控的模糊效果。
  • 响应处理:设置正确的Content-Type(image/png)和缓存控制头,避免浏览器缓存旧结果,最后输出处理后的图片流并销毁资源。
  • 为了应对大图片处理可能出现的超时问题,还通过set_time_limit(30)将脚本执行时间延长至 30 秒。

项目代码

fontPage.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片高斯模糊</title>
<style>
    * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    }

    body {
        background-color: #f5f7fa;
        display: flex;
        flex-direction: column;
        align-items: center;
        padding: 2rem 1rem;
    }

    .container {
        background: #fff;
        border-radius: 12px;
        box-shadow: 0 4px 20px rgba(0,0,0,0.08);
        width: 100%;
        max-width: 1200px;
        padding: 2rem;
        margin-bottom: 2rem;
        display: flex;
        gap: 2rem;
        align-items: flex-start;
    }

    h1 {
        color: #2d3748;
        font-size: 1.8rem;
        margin-bottom: 1.5rem;
        text-align: center;
        width: 100%;
    }

    .control-panel {
        flex: 0 0 350px;
        display: flex;
        flex-direction: column;
        gap: 1.5rem;
    }

    .upload-area {
        display: flex;
        flex-direction: column;
        gap: 0.5rem;
    }

    .upload-label {
        font-size: 1rem;
        color: #4a5568;
    }

    .file-input {
        padding: 0.8rem;
        border: 2px dashed #cbd5e0;
        border-radius: 8px;
        background-color: #f8f9fa;
        cursor: pointer;
        transition: border-color 0.3s;
    }

    .file-input:hover {
        border-color: #4299e1;
    }

    .range-container {
        display: flex;
        flex-direction: column;
        gap: 0.5rem;
    }

    .range-label {
        font-size: 1rem;
        color: #4a5568;
    }

    .range-wrapper {
        display: flex;
        align-items: center;
        gap: 1rem;
    }

    input[type="range"] {
        flex: 1;
        height: 8px;
        background: #e2e8f0;
        border-radius: 4px;
        outline: none;
        -webkit-appearance: none;
    }

    input[type="range"]::-webkit-slider-thumb {
        -webkit-appearance: none;
        width: 20px;
        height: 20px;
        background: #4299e1;
        border-radius: 50%;
        cursor: pointer;
        transition: background 0.3s;
    }

    input[type="range"]::-webkit-slider-thumb:hover {
        background: #38b2ac;
    }

    .range-value {
        font-weight: bold;
        color: #4299e1;
        min-width: 40px;
        text-align: center;
    }

    .hint {
        font-size: 0.9rem;
        color: #718096;
        font-style: italic;
    }

    .progress-container {
        height: 12px;
        background: #e2e8f0;
        border-radius: 6px;
        overflow: hidden;
        display: none;
        position: relative;
    }

    .progress-bar {
        height: 100%;
        background: linear-gradient(90deg, #4299e1, #38b2ac);
        width: 0%;
        transition: width 0.3s ease;
    }

    .progress-text {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        font-size: 0.8rem;
        color: #fff;
        font-weight: bold;
    }

    #status {
        text-align: center;
        font-weight: bold;
        padding: 0.5rem;
        border-radius: 4px;
        display: none;
    }

    #status.success {
        background-color: #c6f6d5;
        color: #22c55e;
    }

    #status.error {
        background-color: #fed7aa;
        color: #f97316;
    }

    button {
        padding: 0.8rem 1.5rem;
        border: none;
        border-radius: 6px;
        font-size: 1rem;
        font-weight: 600;
        cursor: pointer;
        transition: all 0.3s;
    }

    #downloadBtn {
        background: linear-gradient(90deg, #4299e1, #38b2ac);
        color: #fff;
        display: none;
    }

    #downloadBtn:hover {
        transform: translateY(-2px);
        box-shadow: 0 4px 12px rgba(66, 153, 225, 0.3);
    }

    .preview-panel {
        flex: 1;
        min-width: 300px;
        display: flex;
        justify-content: center;
        align-items: flex-start;
        padding-top: 1rem;
        overflow-y: auto; /* 纵向滚动,防止卡片被撑开 */
    }
    
    #preview {
        max-width: 100%; /* 宽度跟随容器 */
        height: auto;    /* 高度自动缩放,保证全图显示 */
        border-radius: 8px;
        box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        display: none;
        object-fit: contain; /* 保证比例,完整显示 */
    }
    
    /* 移动端单列布局 */
    @media (max-width: 768px) {
        .container {
            flex-direction: column;
            padding: 1rem;
        }
        .control-panel {
            flex: none;
            width: 100%;
        }
        .preview-panel {
            width: 100%; /* 宽度等于卡片宽度,避免左右溢出 */
            overflow-x: hidden; /* 禁止横向滚动 */
        }
    }
</style>
</head>
<body>
    <h1>图片高斯模糊处理</h1>

    <div class="container">
        <!-- 左侧操作区 -->
        <div class="control-panel">
            <div class="upload-area">
                <label class="upload-label" for="imageUpload">上传图片:</label>
                <input type="file" id="imageUpload" accept="image/*" class="file-input">
            </div>

            <div class="range-container">
                <label class="range-label" for="blurRange">高斯模糊强度:</label>
                <div class="range-wrapper">
                    <input type="range" id="blurRange" min="0" max="50" value="0">
                    <span id="blurValue" class="range-value">0</span>
                </div>
                <div class="hint">(优先拉取到10以上,效果为佳)</div>
            </div>

            <div class="progress-container" id="progressContainer">
                <div class="progress-bar" id="progressBar"></div>
                <div class="progress-text" id="progressText">0%</div>
            </div>

            <div id="status"></div>

            <button id="downloadBtn">下载处理后的图片</button>
        </div>

        <!-- 右侧预览区 -->
        <div class="preview-panel">
            <img id="preview" src="" alt="预览图" />
        </div>
    </div>

    <script>
        const imageUpload = document.getElementById('imageUpload');
        const blurRange = document.getElementById('blurRange');
        const blurValue = document.getElementById('blurValue');
        const previewImage = document.getElementById('preview');
        const downloadBtn = document.getElementById('downloadBtn');
        const progressContainer = document.getElementById('progressContainer');
        const progressBar = document.getElementById('progressBar');
        const progressText = document.getElementById('progressText');
        const statusText = document.getElementById('status');

        let debounceTimer = null;
        let originalImageDataURL = null;
        let progressInterval = null;

        const blurTimes = {
            10: 8,
            20: 15,
            30: 20,
            40: 25,
            50: 30
        };

        function debounce(func, wait) {
            return function() {
                const context = this;
                const args = arguments;
                clearTimeout(debounceTimer);
                debounceTimer = setTimeout(() => func.apply(context, args), wait);
            };
        }

        imageUpload.addEventListener('change', function(event) {
            const file = event.target.files[0];
            if (file) {
                const reader = new FileReader();
                reader.onload = function(e) {
                    originalImageDataURL = e.target.result;
                    previewImage.src = originalImageDataURL;
                    previewImage.style.display = 'block';
                    updateBlur();
                };
                reader.readAsDataURL(file);
            }
        });

        blurRange.addEventListener('input', function(event) {
            blurValue.textContent = event.target.value;
            debounce(updateBlur, 500)();
        });

        function updateBlur() {
            const blurValue = parseInt(document.getElementById('blurRange').value);
            const file = document.getElementById('imageUpload').files[0];

            if (file) {
                progressContainer.style.display = 'block';
                progressBar.style.width = '0%';
                progressText.textContent = '0%';
                statusText.style.display = 'none';
                statusText.className = '';

                let estimatedTime = 0;
                if (blurValue >= 50) estimatedTime = blurTimes[50];
                else if (blurValue >= 40) estimatedTime = blurTimes[40];
                else if (blurValue >= 30) estimatedTime = blurTimes[30];
                else if (blurValue >= 20) estimatedTime = blurTimes[20];
                else if (blurValue >= 10) estimatedTime = blurTimes[10];
                else estimatedTime = 1;

                const totalSteps = 100;
                const stepTime = (estimatedTime * 1000) / totalSteps;
                let currentStep = 0;

                if (progressInterval) clearInterval(progressInterval);
                progressInterval = setInterval(() => {
                    currentStep++;
                    const percent = currentStep + '%';
                    progressBar.style.width = percent;
                    progressText.textContent = percent;
                    if (currentStep >= totalSteps) {
                        clearInterval(progressInterval);
                    }
                }, stepTime);

                const formData = new FormData();
                formData.append('image', file);
                formData.append('blur', blurValue);

                fetch('process_image.php', {
                    method: 'POST',
                    body: formData
                })
                .then(response => response.blob())
                .then(data => {
                    clearInterval(progressInterval);
                    progressBar.style.width = '100%';
                    progressText.textContent = '100%';
                    statusText.style.display = 'block';
                    statusText.className = 'success';
                    statusText.textContent = '渲染完成';

                    const url = URL.createObjectURL(data);
                    previewImage.src = url;
                    downloadBtn.style.display = 'inline-block';
                    downloadBtn.onclick = function() {
                        const a = document.createElement('a');
                        a.href = url;
                        a.download = 'blurred_image.png';
                        a.click();
                    };

                    setTimeout(() => {
                        statusText.style.display = 'none';
                    }, 3000);
                })
                .catch(err => {
                    clearInterval(progressInterval);
                    progressContainer.style.display = 'none';
                    statusText.style.display = 'block';
                    statusText.className = 'error';
                    statusText.textContent = '处理失败';
                    console.error("图片处理失败", err);
                });
            }
        }
    </script>
</body>
</html>

process_image.php

<?php
// 增加执行时间限制,防止大图片处理超时
set_time_limit(30);

// 检查是否上传了文件
if (isset($_FILES['image']) && isset($_POST['blur'])) {
    $imageFile = $_FILES['image'];
    $blur = (int)$_POST['blur'];

    // 检查上传是否成功
    if ($imageFile['error'] !== UPLOAD_ERR_OK) {
        http_response_code(500);
        echo "文件上传失败,错误代码: " . $imageFile['error'];
        exit;
    }

    $image = $imageFile['tmp_name'];
    
    // 加载图片
    $img = imagecreatefromstring(file_get_contents($image));
    
    if ($img !== false) {
        // 根据模糊强度计算迭代次数
        $numBlurIterations = max(1, $blur * 2);
        
        for ($i = 0; $i < $numBlurIterations; $i++) {
            imagefilter($img, IMG_FILTER_GAUSSIAN_BLUR);
        }
        
        // 设置图像类型和缓存控制
        header('Content-Type: image/png');
        header('Cache-Control: no-cache, must-revalidate');
        header('Pragma: no-cache');
        header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
        
        // 输出图像到浏览器
        imagepng($img);
        
        // 销毁图像资源
        imagedestroy($img);
    } else {
        http_response_code(500);
        echo "无法加载图片,请确保上传的是有效的图片文件。";
    }
} else {
    http_response_code(400);
    echo "请求参数不完整。";
}
?>

声明:三二一的一的二|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - PHP构建实现高斯模糊处理平台


三二一的一的二