【轻知识】php图片处理——仿Oss

依赖

php 处理类库:intervention/image
php扩展:imagic

分析需求

截图自阿里

照猫画虎。我们要做的事情。就是每步的操作跟Oss保持一致。

  1. / 分隔动作。每步的动作都像是链式调用一样。
  2. 解析每一步的动作。

逻辑实现

1.解析url中的query。变成规则。
2.遍历规则,调用不同的处理类。
3.渲染。

代码实现

代码结构

ImageHandle
├── CircleTool.php
├── CropTool.php
├── FormatTool.php
├── ImageHandleInterface.php
├── InfoTool.php
├── QualityTool.php
├── ResizeTool.php
├── RotateTool.php
└── WatermarkTool.php

接口类

interface ImageHandleInterface
{
    public function parseParams(array $params);
    public function handle();
}

旋转图片示例代码

class RotateTool
{
    private $processImage;
    private $angle;


    public function __construct(ProcessImageObject $processImage, array $params)
    {
        $this->processImage = $processImage;
        // 解析规则
        $this->parseParams($params);
    }

    public function parseParams(array $params)
    {
        $this->angle = -array_key_first($params); // 负号表示顺时针
    }

    public function handle()
    {
        $img = $this->processImage->getProcessor();
        $img->rotate($this->angle);
        return $this->processImage;
    }
}

解析规则

解析这样的image/resize,w_200,h_200/rotate,90

    public function parseHandle(string $query)
    {
        $imageRules = explode('/', $query);

        if (!(count($imageRules) > 1)) {
            throw new \Exception('不符合规则');
        }

        if ('image' === $imageRules[0]) { // 是图片处理的规则
            // 开始解析之后的每一条
            $rules = []; // key表示handle名,之后的标识处理所需要的参数。
            foreach ($imageRules as $rk => $rule) {
                $ruleArr = explode(',', $rule);
                foreach ($ruleArr as $pk => $param) {
                    if (0 === $pk) {
                        $rules[$rk]['handle'] = $ruleArr[0];
                        $rules[$rk]['params'] = [];
                    } else {
                        $parmArr = explode('_', $param, 2);
                        $rules[$rk]['params'][$parmArr[0]] = $parmArr[1] ?? null;
                    }
                }
            }

            return $rules;
        }
        throw new \Exception('不符合规则');
    }

遍历规则进行渲染

 public function getImage1(StoragePlatformInterface $client, string $key, string $process)
    {
        $meta = $client->headObject($key);
        $imgSize = $meta['content-length'];

        $imageContent = $client->getFileContent($key);
        if (empty($process)) { // 直接输出图片
            return $imageContent;
        }

        $processImageObject = new ProcessImageObject($client, $imageContent);
        // 走图片处理
        $handleRules = $this->parseHandle($process);
        foreach ($handleRules as $handle) {
            if (isset($this->imageHandles[$handle['handle']])) {
                // 就开始循环的去处理
                $handleTool = new $this->imageHandles[$handle['handle']]($processImageObject, $handle['params']);
                $processImageObject = $handleTool->handle();
            }
        }
        $response = Context::get(ResponseInterface::class);
        $response = $response->withHeader('Content-type', $processImageObject->getContentType());
        Context::set(ResponseInterface::class, $response);

        return $processImageObject->stream();
    }

使用ProcessImageObject对象管理处理的图片。方便管理。另外,像format跟quality这样的操作。直接修改该对象的属性。统一输出不依赖顺序。

ProcessImageObject 的对象代码。

class ProcessImageObject
{
    public static $limitWidth = 5300;
    public static $limitHeight = 4096;
    /**
     * @var StoragePlatformInterface 桶的所在 客户端;
     */
    private $client; // 桶的所在 客户端
    private $content; // 图片的内容
    private $format; // 图片的格式,
    private $contentType;
    private $quality = 90; // 用于传递质量,默认值为90。因为stream 方法默认为90。可根据图片处理的参数quality,q_* 来进行更改
    private $processor; // 图像处理者。一次处理实例化一次 Imanager 然后传递下去。

    /**
     * @var int 图像的高
     */
    private $height;

    /**
     * @var int 图像的宽
     */
    private $width;

    public function __construct(StoragePlatformInterface $client, $content)
    {
        $this->client = $client;
        $this->content = $content;
        $manager = new ImageManager(['driver' => 'gd']);
        $img = $manager->make($this->content);
        $mime = $img->mime();

        $this->format = explode('/', $mime)[1];
        $this->contentType = $mime;
        $this->processor = $img;
        $this->height = $img->getHeight();
        $this->width = $img->getWidth();
        if ($this->width > self::$limitWidth || $this->height > self::$limitHeight) {
            throw new StorageException(ErrorResultCode::IMAGE_WIDTH_HEIGHT_LARGE, 403);
        }
    }

    public function getClient(): StoragePlatformInterface
    {
        return $this->client;
    }

    public function getProcessor()
    {
        return $this->processor;
    }

    public function stream(): StreamInterface
    {
        return $this->processor->stream($this->format, $this->quality);
    }

    /**
     * 最后输出时使用.
     */
    public function getContentType(): string
    {
        return $this->contentType;
    }

    public function setContentType(string $contentType)  {
        $this->contentType = $contentType;
    }
    public function setQuality(int $q = 90): void
    {
        $this->quality = $q;
    }

    public function setFormat(string $format): void
    {
        $this->format = $format;
        $this->contentType = 'image/'.$this->format;
    }

    public function getWidth()
    {
        return $this->processor->getWidth();
    }

    public function getHeight()
    {
        return $this->processor->getHeight();
    }

    public function setContent(string $content)
    {
        $this->content = $content;
    }

    public function toArray(): array
    {
        return [
            'client' => [
                'bucket' => $this->client->getBucketName(),
            ],
            'format' => $this->format,
            'quality ' => $this->quality,
            'width'=>$this->width,
            'height'=>$this->height,
        ];
    }
}

自定义裁剪

裁剪最主要算坐标。

image.png

代码如下

<?php

declare(strict_types=1);

namespace App\Lib\ImageHandle;

use App\Object\ProcessImageObject;

class CropTool implements ImageHandleInterface
{
    private $image;

    //image/crop,x_10,y_10,w_200,h_200,g_se
    private $x;
    private $y;
    private $w;
    private $h;
    private $g; //nw、north、ne、west、center、east、sw、south、se

    /**
     * @var ProcessImageObject
     */
    private $processImage;

    // ne 是按照右上角为坐标

    public function __construct(ProcessImageObject $processImage, array $params)
    {
        $this->processImage = $processImage;
        // 解析规则
        $this->parseParams($params);
    }

    public function parseParams(array $params)
    {
        $this->x = $params['x'];
        $this->y = $params['y'];
        $this->w = $params['w'];
        $this->h = $params['h'];
        $this->g = $params['g'];
    }

    public function handle()
    {
        $img = $this->processImage->getProcessor();
        // 如果g 存在 ,则按照 g 去做偏移
        $originH = $img->getHeight();
        $originW = $img->getWidth();
        $halfX = (int) ($originW / 2);
        $halfY = (int) ($originH / 2);
        $centerX = (int) ($originW / 2);
        $centerY = (int) ($originH / 2);
        $oneThirdX = floor($originW / 3);
        $oneThirdY = floor($originH / 3);
        $rightTopX = $originW;
        $rightTopY = 0;
        $leftBottomY = $originH;
        $leftBottomX = $originW;
        if (empty($this->h)) {
            $this->h = $originH;
        }
        if (empty($this->w)) {
            $this->w = $originW;
        }
        if ('nw' === $this->g) {
            $x = 0 + $this->x;
            $y = 0 + $this->y;
            // 如果 超出了边界则 截止到边界
            $limitX = $x + $this->w;
        } elseif ('north' == $this->g) {
            $x = $this->x + ($halfX - floor($this->w / 2));
            $y = $this->y;
            // 如果 超出了边界则 截止到边界
            $limitX = $x + $this->w;
        } elseif ('ne' == $this->g) {
            $x = $rightTopX - $this->w + $this->x;
            $y = $this->y;
            // 如果 超出了边界则 截止到边界
            $limitX = $x + $this->w;
        } elseif ('west' == $this->g) {
            $x = 0 + $this->x;
            $y = ($halfY - floor($this->h / 2)) + $this->y;
            // 如果 超出了边界则 截止到边界
            $limitX = $x + $this->w;
        } elseif ('center' === $this->g) {
            $x = $this->x + ($halfX - floor($this->w / 2));
            $y = ($halfY - floor($this->h / 2)) + $this->y;
            // 如果 超出了边界则 截止到边界
            $limitX = $x + $this->w;
        } elseif ('east' === $this->g) {
            $x = $rightTopX - $this->w + $this->x;
            $y = ($halfY - floor($this->h / 2)) + $this->y;
            // 如果 超出了边界则 截止到边界
            $limitX = $x + $this->w;
        } elseif ('sw' === $this->g) {
            $x = 0 + $this->x;
            $y = $leftBottomY - $this->h + $this->y;
            // 如果 超出了边界则 截止到边界
            $limitX = $x + $this->w;
        } elseif ('south' === $this->g) {
            $x = $this->x + ($halfX - floor($this->w / 2));
            $y = $leftBottomY - $this->h + $this->y;
            // 如果 超出了边界则 截止到边界
            $limitX = $x + $this->w;
        } elseif ('se' == $this->g) {
            $x = $rightTopX - $this->w + $this->x;
            $y = $leftBottomY - $this->h + $this->y;
            // 如果 超出了边界则 截止到边界
            $limitX = $x + $this->w;
        }
        if ($limitX > $originW) { // 目标宽大于 可用宽
            if (($originW - $x) < 0) {
                $x = $originW - $this->w;
                $w = $this->w;
            } else {
                $w = $originW - $x;
            }
        } else {
            $w = $this->w;
        }

        $limitY = $y + $this->h;
        if ($limitY > $originH) {
            if (($originH - $y) < 0) {
                $y = $originH - $this->h;
                $h = $this->h;
            } else {
                $h = ($originH - $y);
            }
        } else {
            $h = $this->h;
        }
        $this->x = $x;
        $this->y = $y;
        $this->w = $w;
        $this->h = $h;
        $img->crop($this->w, $this->h, $this->x, $this->y);

        return $this->processImage;
    }

    public function toArray()
    {
        return [
            'w' => $this->w,
            'h' => $this->h,
            'x' => $this->x,
            'y' => $this->y,
            'g' => $this->g,
        ];
    }
}

水印、以及嵌套水印

遍历规则,在水印处理类,发现了有水印图片。则继续解析该水印图片。相当于又走解析规则处理水印图。

class WatermarkTool implements ImageHandleInterface
{
    public $image;
    /**
     * @var int 透明度
     */
    public $t;
    public $g;
    public $x;
    public $y;
    public $voffset;
    public $markImage;

    public $fontDir = BASE_PATH.'/src/font/';
    /**
     * @var string 字体
     */
    public $text;
    /**
     * @var string 字体颜色
     */
    public $color;
    /**
     * @var int 字体大小
     */
    public $size;

    /**
     * @var int 文字旋转角度
     */
    public $rotate;

    /**
     * @var ProcessImageObject
     */
    private $processImage;


    public $waterMarkPosMap = [
        'nw' => 'top-left',
        'north' => 'top',
        'ne' => 'top-right',

        'west' => 'left',
        'center' => 'center',
        'east' => 'right',

        'sw' => 'bottom-left',
        'south' => 'bottom',
        'se' => 'bottom-right', // default
    ];


    public function __construct(ProcessImageObject $processImage, array $params)
    {
        $this->processImage = $processImage;

        $this->parseParams($params);
    }

    public function parseParams(array $params)
    {
        // TODO: Implement parseParams() method.
        $this->x = $params['x'] ?? 10; // 对照 oss,默认值为 10
        $this->y = $params['y'] ?? 10; // 对照 oss,默认值为 10
        $this->g = $params['g'] ?? 'se'; // se 默认
        $this->t = $params['t'] ?? 90;
        $this->markImage = !empty($params['image']) ? urldecode(Base64Tool::urlsafeDecode($params['image'])) : '';
        $this->text = !empty($params['text']) ? Base64Tool::urlsafeDecode($params['text']) : '';

        $this->color = '#'.($params['color'] ?? '000000');
        $this->size = $params['size'] ?? 40; // 对照oss 默认 40
        $this->rotate = -($params['rotate'] ?? 0);
    }

    public function handle()
    {
        $img = $this->processImage->getProcessor();

        if (!empty($this->markImage)) {
            $imageService = new ImageService();
            $parseArr = parse_url($this->markImage);
            $process = '';
            if (isset($parseArr['query'])) {
                $query = explode('=', $parseArr['query']);
                $process = $query[1];
            }
            $pos = $this->waterMarkPosMap['se']; // 默认se 也就是 bottom-right
            if (isset($this->waterMarkPosMap[$this->g])) {
                $pos = $this->waterMarkPosMap[$this->g];
            }
            $object = $imageService->getImage2($this->processImage->getClient(), $parseArr['path'], $process);
            $object->getProcessor()->opacity($this->t);
            $img->insert($object->getProcessor()->stream(), $pos, $this->x, $this->y);
        } elseif (!empty($this->text)) { // text_b3Nz5paH5a2X5rC05Y2wMQ,size_20,x_10,y_200
            $img->text($this->text, $this->x, $this->y, function ($font) {
                $font->file($this->fontDir.'wqy-zenhei.ttf');
                $font->size($this->size);
                $font->color($this->color);
                $font->angle($this->rotate);
            });
        }

        return $this->processImage;
    }

    public function toArray()
    {
        return [
            'x' => $this->x,
            'y' => $this->y,
            'markImage' => $this->markImage,
        ];
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342