在 PHP 中加水印以及存储和显示图像的正确方法

Right way of watermarking & storing & displaying images in PHP

我正在构建一个基于 Web 的系统,它将承载大量高分辨率图像,并且它们将可供出售。当然,我永远不会显示高分辨率图像,而是人们在浏览时只会看到低分辨率、带水印的图像。目前工作流程如下:

PHP 脚本处理高分辨率图片上传,上传图片时,它会自动调整为低分辨率图片和缩略图,并且这两个文件都保存在服务器上, (不添加水印)。

当人们浏览时,页面显示图像的缩略图,点击时,它会放大并显示带有水印的低分辨率图像。目前,只要打开低分辨率图像,我就会即时应用水印。

我的问题是,正确的方法是什么:

1) 我应该只在第一次访问时保存带缩略图的低分辨率图像的第二个副本吗?我的意思是,如果有人访问图像,我会即时添加水印,然后显示图像并将其存储在服务器上。下次访问同一图像时,如果存在水印副本,则只显示 wm 副本,否则即时应用水印。 (如果watermark.png被更改,只需删除带水印的图像,它们将在访问时重新创建)。

2) 我是否应该像现在这样不断地即时应用水印。

我最大的问题是 PHP file_exists() 和为图像添加水印之间的区别有多大,例如:

$image = new Imagick();
$image->readImage($workfolder.$event . DIRECTORY_SEPARATOR . $cat . DIRECTORY_SEPARATOR .$mit);
$watermark = new Imagick();
$watermark->readImage($workfolder.$event . DIRECTORY_SEPARATOR . "hires" . DIRECTORY_SEPARATOR ."WATERMARK.PNG");
$image->compositeImage($watermark, imagick::COMPOSITE_OVER, 0, 0);

所有低分辨率图片均为 1024x1024,JPG,质量设置为 45%,去除了所有不必要的滤镜,因此低分辨率图片的文件大小约为 40Kb-80Kb。

this question有点关系,只是规模和场景有点不同。

我在专用服务器上 (Xeon E3-1245v2) cpu, 32 GB ram, 2 TB 存储), 该网站总体流量不大, 但有时会出现巨大的峰值时间。当图像发布时,我们每小时有几千次点击,人们浏览图像、下载、购买等。所以在正常使用时,我确信即时生成是正确的方法,我有点担心关于高峰期。

需要说明的是,我使用的是 ImageMagick 库进行图像处理,而不是 GD。

感谢您的意见。

更新

None 的答案是一个完整的解决方案,但这很好,因为我从未寻找过。这是一个艰难的决定,接受哪个人以及给谁赏金。

@Ambroise-Maupate 解决方案很好,但它需要 PHP 才能完成这项工作。

@Hugo Delsing 建议使用 Web 服务器来提供缓存文件,减少对 PHP 脚本的调用,这意味着使用的资源更少,另一方面,它并不是真正的存储友好型。

我将使用 2 个答案的混合合并解决方案,依靠 CRON 作业来删除垃圾。

谢谢指点。

我建议您即时创建带水印的图像,并按照大家的建议同时缓存它们。

然后您可以创建一个 garbage-collector PHP 每天执行的脚本(使用 cron)。此脚本将浏览您的缓存文件夹以读取每个图像 访问时间。这可以使用 fileatime() PHP 方法来完成。然后当缓存的wm图片在24或48小时内没有被访问时,就删除它。

使用此方法,您可以处理 峰值 时间段,因为图像是在第一次请求时缓存的。并且您将保存您的 HDD space,因为您的垃圾收集器脚本将为您删除未使用的图像。

此方法仅在您的服务器分区启用了 atime 更新时有效。

http://php.net/manual/en/function.fileatime.php

就我个人而言,我会以某种 CDN 方式创建一个 static/cookieless 子域来处理此类图像。主要原因是:

  1. 图像只创建一次
  2. 只创建访问过的图像
  3. 创建后,图像将从缓存中提供,速度要快得多。

第一步是在指向空文件夹的子域上创建一个网站。使用 IIS/Apache 或其他任何设置来禁用此新网站的会话。还要在网站上设置一些长缓存headers,因为内容不应该改变

第二步是创建一个包含以下内容的 .htaccess 文件。

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)   /create.php?path=  [L]

这将确保如果有人访问现有图像,它会直接显示图像而不会 PHP 干扰。每个 non-existing 请求都将由 create.php 脚本处理,这是您接下来应该添加的东西。

<?php
function NotFound()
{
    if (!headers_sent()) {
        $protocol = (isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0');
        header($protocol . ' 404 Not Found');
        echo '<h1>Not Found</h1>';
        exit;
    }
}

$p = $_GET['path'];

//has path
if (strlen($p)<=1)
    NotFound();

$clean = explode('?', $p);
$clean = explode('#', $clean[0]);
$params = explode('/', substr($clean[0], 1)); //drop first /

//I use a check for two, because I dont allow images in the root folder
//I also use the path to determine how it should look
//EG: thumb/125/90/imagecode.jpg
if (count($params)<2)
    NotFound();

$type = $params[0];

//I use the type to handle different methods. For this example I only used the full sized image
//You could use the same to handle thumbnails or cropped/watermarked
switch ($type) {
    //case "crop":if (Crop($params)) return; else break;
    //case "thumb":if (Thumb($params)) return; else break;
    case "image":if (Image($params)) return; else break;
}
NotFound();
?>
<?php
/*
Just some example to show how you could create a responds
Since you already know how to create thumbs, I'm not going into details

Array
(
    [0] => image
    [1] => imagecode.JPG
)
*/
function Image($params) {
    $tmp = explode('.', $params[1]);
    if (count($tmp)!=2)
        return false;

    $code = $tmp[0];


    //WARNING!! SQL INJECTION
    //USE PROPER DB METHODS TO GET REALPATH, THIS IS JUST EXAMPLE
    $query = "SELECT realpath FROM images WHERE Code='".$code."'";
    //exec query here to $row
    $realpath = $row['realpath'];


    $f = file_get_contents($realpath);

    if (strlen($f)<=0)
        return false;

    //create folder structure
    @mkdir($params[0]);

    //if you had more folders, continue creating the structure
    //@mkdir($params[0].'/'.$params[1]);

    //store the image, so a second request won't access this script
    file_put_contents($params[0].'/'.$params[1], $f);

    //you could directly optimize the image for web to make it even better
    //optimizeImage($params[0].'/'.$params[1]);

    //now serve the file to the browser, because even the first request needs to show the image
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    header('Content-Type: '.finfo_file($finfo, $params[0].'/'.$params[1]));

    echo $f;

    return true;
}
?>

考虑到您的 HDD 存储容量和 Pikes。

我只会创建一个带水印的图像,如果它被查看。(所以是即时的)这样你就不会用太多 space 一堆被查看或可能不被查看的文件。

我不会为缩略图加水印,我宁愿制作一个过滤器来伪造水印并防止其被保存。该过滤器将应用于所有缩略图,而无需创建第二张图片。

通过这种方式,您所有的缩略图都带有水印(假的,顶部有其他元素)。

然后,如果其中一个缩略图被查看,它会生成一个带水印的图像(仅一次),因为在生成后您将加载新的带水印图像。

这将是处理 HDD 存储和 Pikes 的最有效方法。

另一种选择是升级您的托管服务。 Godaddy 提供无限存储和带宽,每年约 50 美元。

对于大多数情况,延迟应用水印可能最有意义(在请求时动态生成带水印的图像,然后缓存结果)但是如果您的需求激增,您正在创建一个 DOS 机制:在上传时创建带水印的版本。