基本逻辑思路
1、计算出图片集的总宽和高,提取出最大宽和高
2、当最大宽和高度超出总宽度或高度的1/4则只按横向或纵向直接依次合并,跳到步骤13
3、取出所有图片有大小与位置,并进行排序(宽度+高度的各从大到小排)
4、初始空间为总宽度的1/4,开始横向加图
5、判断图片集是否还有图片,如果没有则跳到步骤13
6、依次取出一张可以放入空余空间的图片并计算出剩下空余空间,并记录这个图片的位置和增加到下排空余空间集,直到没有合适的图片可以放入
7、有贴图则把最后有剩余空间的数据增加到下排剩余空间集,没有合并新图跳到步骤9
8、下排剩余空间集按高度从小到大排序
9、依次取出最小高度的下排剩余空间,跳到步骤5
10、依次取出最小高度的下排剩余空间与上次没有合并新图的剩余空间尝试进行合并,如果相邻则合并成功并跳到步骤5
11、把不能合并的剩余空间添加到临时集合中,当临时集合的数据个数超过下排空余空间集个数时则强制合并循环这些剩余空间合并相邻一个再放入下排空余空间集中
12、跳到步骤5
13、取出合并后最大的宽和高
环境要求
1、php7.2+
2、GD扩展
合并逻辑处理代码
/**
* 合并碎图
*/
class ImageMerge {
/**
* @var array 支持处理图片
*/
const IMAGE_CREATE_FUNC = [
IMAGETYPE_GIF => 'imagecreatefromgif',
IMAGETYPE_JPEG => 'imagecreatefromjpeg',
IMAGETYPE_PNG => 'imagecreatefrompng',
IMAGETYPE_BMP => 'imagecreatefrombmp',
IMAGETYPE_WBMP => 'imagecreatefromwbmp',
IMAGETYPE_XBM => 'imagecreatefromxbm',
];
/**
* @var int 宽度索引
*/
const KEY_WIDTH = 0;
/**
* @var int 高度索引
*/
const KEY_HEIGHT = 1;
/**
* @var int 图片类型索引
*/
const KEY_TYPE = 2;
/**
* @var int 图片文件名索引
*/
const KEY_FILENAME = 3;
/**
* @var int 图片X轴坐标索引
*/
const KEY_X = 4;
/**
* @var int 图片Y轴坐标索引
*/
const KEY_Y = 5;
/**
* @var array 要处理的图像集
*/
protected $images = [];
/**
* @var array 背景色值
*/
protected $bgColor = ['red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 127];
/**
* 初始化处理
* @param array $bgColor
*/
public function __construct(array $bgColor = null) {
if ($bgColor) {
$this->bgColor = array_merge($this->bgColor, $bgColor);
}
}
/**
* 添加合并图片所在目录
* @param string $dir
* @param bool $recursion
*/
public function addDir(string $dir, bool $recursion = true) {
if (file_exists($dir) && is_dir($dir) && $handle = opendir($dir)) {
while (false !== $file = readdir($handle)) {
if ($file == '.' || $file == '..') {
continue;
}
$path = $dir . DIRECTORY_SEPARATOR . $file;
if (is_dir($file)) {
$recursion && $this->addDir($path, $recursion);
} else {
$this->add($path);
}
}
}
}
/**
* 添加要合并的图片集
* @param string $files
*/
public function addFile(string ...$files) {
foreach ($files as $file) {
$this->add($file);
}
}
/**
* 保存合并图片
* @param string $savepath
* @param int $space
* @param int $quality
* @return bool
*/
public function save(string $savepath = null, int $space = 2, int $quality = 3) {
if ($image = $this->make($space)) {
if ($savepath) {
$dir = dirname($savepath);
if (!file_exists($dir)) {
mkdir($dir, 0777, true);
}
}
return imagepng($image, $savepath, $quality);
}
return false;
}
/**
* 生成图片
* @param int $space
* @return resource|null
*/
public function make(int $space = 2) {
if (count($this->images)) {
list($array, $width, $height) = $this->equidistribution($space);
$image = $this->create($width, $height);
foreach ($array as $img) {
imagecopy($image, call_user_func(static::IMAGE_CREATE_FUNC[$img[static::KEY_TYPE]], $img[static::KEY_FILENAME]), $img[static::KEY_X], $img[static::KEY_Y], 0, 0, $img[static::KEY_WIDTH], $img[static::KEY_HEIGHT]);
}
return $image;
}
}
/**
* 分布处理
* @param int $space
* @return array
*/
protected function equidistribution(int $space) {
$maxwidth = $totalwidth = $maxheight = $totalheight = 0;
foreach ($this->images as $item) {
$totalwidth += $item[static::KEY_WIDTH];
$totalheight += $item[static::KEY_HEIGHT];
$maxwidth = max($maxwidth, $item[static::KEY_WIDTH]);
$maxheight = max($maxheight, $item[static::KEY_HEIGHT]);
}
$theory = ceil($totalwidth / 4);
if ($theory < $maxwidth) { //直接横放
list($array, $width) = $this->oneway($space, static::KEY_Y, static::KEY_X, static::KEY_WIDTH, static::KEY_HEIGHT);
$height = $maxheight;
} elseif ($totalheight / 4 < $maxheight) { //直接坚放
list($array, $height) = $this->oneway($space, static::KEY_X, static::KEY_Y, static::KEY_HEIGHT, static::KEY_WIDTH);
$width = $maxwidth;
} else { //铺开
return $this->spread($this->images, $space, $theory + $space);
}
return [$array, $width, $height];
}
/**
* 均匀铺开处理
* @param array $images
* @param int $space
* @param int $maxWidth
* @return array
*/
protected function spread(array $images, int $space, int $maxWidth) {
$lists = $heights = $array = [];
$surplusWidth = $maxWidth;
$x = $y = 0;
usort($images, function($prev, $next) {
return $prev[static::KEY_WIDTH] + $prev[static::KEY_HEIGHT] < $next[static::KEY_WIDTH] + $next[static::KEY_HEIGHT] ? 1 : -1;
});
while ($count = count($images)) {
$surplusWidth -= $space;
foreach ($images as $key => $img) {
if ($img[static::KEY_WIDTH] <= $surplusWidth) {
$width = $img[static::KEY_WIDTH] + $space;
$img[static::KEY_X] = $x;
$img[static::KEY_Y] = $y;
$array[] = $img;
$heights[] = [static::KEY_WIDTH => $width, static::KEY_X => $x, static::KEY_Y => $y + $img[static::KEY_HEIGHT] + $space];
$surplusWidth -= $width;
$x += $width;
unset($images[$key]);
if ($surplusWidth <= 0) {
break;
}
}
}
$surplusWidth += $space;
if ($count > count($images)) {//上面找到合适的需要新位置
if ($surplusWidth > 0) {//追加多余部分
$heights[] = [static::KEY_WIDTH => $surplusWidth, static::KEY_X => $x, static::KEY_Y => $y];
}
$heights = $this->sortHeights(array_merge($lists, $heights));
$lists = [];
} else {// 上面没合适的需要位置
$prev = [static::KEY_WIDTH => $surplusWidth, static::KEY_X => $x, static::KEY_Y => $y];
if (count($heights)) {
$merge = $this->mergeAdjoin([$prev, $heights[0]]);
if (count($merge) == 1) { //合并确认是否为相邻
$heights[0] = end($merge);
goto INIT_SET;
}
}
$lists[] = $prev;
if (count($heights) <= count($lists)) {
$heights = $this->mergeAdjoin(array_merge($lists, $heights));
$lists = [];
}
}
INIT_SET: [static::KEY_X => $x, static::KEY_Y => $y, static::KEY_WIDTH => $surplusWidth] = array_shift($heights);
}
[$width, $height] = $this->getMaxSize($array);
return [$array, $width, $height];
}
/**
* 获取最大空间
* @param array $array
* @return array
*/
protected function getMaxSize(array $array) {
$maxWidth = $maxHeight = 0;
foreach ($array as $item) {
$width = $item[static::KEY_WIDTH] + $item[static::KEY_X];
$height = $item[static::KEY_Y] + $item[static::KEY_HEIGHT];
if ($width > $maxWidth) {
$maxWidth = $width;
}
if ($height > $maxHeight) {
$maxHeight = $height;
}
}
return [$maxWidth, $maxHeight];
}
/**
* 位置高度排序
* @param array $heights
* @return array
*/
protected function sortHeights(array $heights) {
usort($heights, function($prev, $next) {
return $prev[static::KEY_Y] > $next[static::KEY_Y] ? 1 : -1;
});
return $heights;
}
/**
* 合并相邻的位置
* @param array $heights
* @return array
*/
protected function mergeAdjoin(array $heights) {
$array = [];
while (count($heights)) {
$current = array_shift($heights);
[static::KEY_X => $x, static::KEY_Y => $y, static::KEY_WIDTH => $surplusWidth] = $current;
foreach ($heights as $key => $item) {
if ($item[static::KEY_X] + $item[static::KEY_WIDTH] == $x) {//相邻右边
$x = $item[static::KEY_X];
} elseif ($item[static::KEY_X] - $surplusWidth != $x) {//不相邻右边
continue;
}
$array[] = [static::KEY_X => $x, static::KEY_Y => $item[static::KEY_Y], static::KEY_WIDTH => $surplusWidth + $item[static::KEY_WIDTH]];
unset($heights[$key]);
continue 2;
}
$array[] = $current;
}
return $this->sortHeights($array);
}
/**
* 单向放
* @param int $space
* @param int $fixed
* @param int $move
* @param int $size
* @return array
*/
protected function oneway(int $space, int $fixed, int $move, int $size) {
$array = [];
$pos = 0;
foreach ($this->images as $img) {
$img[$move] = $pos;
$img[$fixed] = 0;
$pos += $space + $img[$size];
$array[] = $img;
}
return [$array, $pos - $space];
}
/**
* 创建底图
* @param int $width
* @param int $height
* @return resource
*/
protected function create(int $width, int $height) {
$image = imagecreatetruecolor($width, $height);
imagesavealpha($image, true);
$color = imagecolorallocatealpha($image, $this->bgColor['red'], $this->bgColor['green'], $this->bgColor['blue'], $this->bgColor['alpha']);
imagefill($image, 0, 0, $color);
return $image;
}
/**
* 添加图片文件
* @param string $filename
*/
protected function add(string $filename) {
if (file_exists($filename) && strpos(mime_content_type($filename), 'image/') === 0 && (false !== $array = getimagesize($filename)) && isset(static::IMAGE_CREATE_FUNC[$array[2]])) {
$this->images[] = [
static::KEY_WIDTH => $array[0],
static::KEY_HEIGHT => $array[1],
static::KEY_TYPE => $array[2],
static::KEY_FILENAME => $filename,
];
}
}
}
测试验证代码
//合并
$dir = '要合并的图片集目录'
$imgMerge = new ImageMerge();
$imgMerge->addDir($dir);
$imgMerge->save($dir . '/merge-image.png', 5);