Files
OMS/app/wap/lib/jsontoimage.php
2026-01-04 19:08:31 +08:00

1422 lines
51 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Copyright 2012-2026 ShopeX (https://www.shopex.cn)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* 快递模板图片生成工具
* 将快递标签设计器的JSON模板转换为JPG图片
*
* 版本: 2.2
* 更新: 支持76*130mm尺寸优化左边区域显示修复Code128条码生成逻辑
* 尺寸: 76mm × 130mm = 608px × 1040px (1mm ≈ 8px)
* 参考: https://github.com/paul8888/code128barcode/blob/master/code128.php
*/
class JsonToImageConverter {
// 像素转换比例1mm ≈ 8px (原来是1mm = 3.75px)
private $pixelRatio = 8.0 / 3.75; // 约2.13倍
/**
* Code128B标准编码表
* 参考: https://github.com/paul8888/code128barcode/blob/master/code128.php
*/
private $code128Table = array(
' ' => "212222", // 空格 (32)
'!' => "222122", // ! (33)
'"' => "222221", // " (34)
'#' => "121223", // # (35)
'$' => "121322", // $ (36)
'%' => "131222", // % (37)
'&' => "122213", // & (38)
"'" => "122312", // ' (39)
'(' => "132212", // ( (40)
')' => "221213", // ) (41)
'*' => "221312", // * (42)
'+' => "231212", // + (43)
',' => "112232", // , (44)
'-' => "122132", // - (45)
'.' => "122231", // . (46)
'/' => "113222", // / (47)
'0' => "123122", // 0 (48)
'1' => "123221", // 1 (49)
'2' => "223211", // 2 (50)
'3' => "221132", // 3 (51)
'4' => "221231", // 4 (52)
'5' => "213212", // 5 (53)
'6' => "223112", // 6 (54)
'7' => "312131", // 7 (55)
'8' => "311222", // 8 (56)
'9' => "321122", // 9 (57)
':' => "321221", // : (58)
';' => "312212", // ; (59)
'<' => "322112", // < (60)
'=' => "322211", // = (61)
'>' => "212123", // > (62)
'?' => "212321", // ? (63)
'@' => "232121", // @ (64)
'A' => "111323", // A (65)
'B' => "131123", // B (66)
'C' => "131321", // C (67)
'D' => "112313", // D (68)
'E' => "132113", // E (69)
'F' => "132311", // F (70)
'G' => "211313", // G (71)
'H' => "231113", // H (72)
'I' => "231311", // I (73)
'J' => "112133", // J (74)
'K' => "112331", // K (75)
'L' => "132131", // L (76)
'M' => "113123", // M (77)
'N' => "113321", // N (78)
'O' => "133121", // O (79)
'P' => "313121", // P (80)
'Q' => "211331", // Q (81)
'R' => "231131", // R (82)
'S' => "213113", // S (83)
'T' => "213311", // T (84)
'U' => "213131", // U (85)
'V' => "311123", // V (86)
'W' => "311321", // W (87)
'X' => "331121", // X (88)
'Y' => "312113", // Y (89)
'Z' => "312311", // Z (90)
'[' => "332111", // [ (91)
'\\' => "314111", // \ (92)
']' => "221411", // ] (93)
'^' => "431111", // ^ (94)
'_' => "111224", // _ (95)
'`' => "111422", // ` (96)
'a' => "121124", // a (97)
'b' => "121421", // b (98)
'c' => "141122", // c (99)
'd' => "141221", // d (100)
'e' => "112214", // e (101)
'f' => "112412", // f (102)
'g' => "122114", // g (103)
'h' => "122411", // h (104)
'i' => "142112", // i (105)
'j' => "142211", // j (106)
'k' => "241211", // k (107)
'l' => "221114", // l (108)
'm' => "413111", // m (109)
'n' => "241112", // n (110)
'o' => "134111", // o (111)
'p' => "111242", // p (112)
'q' => "121142", // q (113)
'r' => "121241", // r (114)
's' => "114212", // s (115)
't' => "124112", // t (116)
'u' => "124211", // u (117)
'v' => "411212", // v (118)
'w' => "421112", // w (119)
'x' => "421211", // x (120)
'y' => "212141", // y (121)
'z' => "214121", // z (122)
'{' => "412121", // { (123)
'|' => "111143", // | (124)
'}' => "111341", // } (125)
'~' => "131141" // ~ (126)
);
private $image;
private $width;
private $height;
/**
* 调整元素位置,确保左边边框不被裁剪,并优化右边空白
*/
private function adjustElementPositions($offset) {
// 计算目标居中位置
$targetWidth = 608; // 76*130mm的目标宽度
$contentWidth = 0;
// 先计算内容总宽度
foreach ($this->data as $element) {
if (isset($element['width'])) {
$width = (int)($element['width'] * $this->pixelRatio);
$x = (int)($element['x'] * $this->pixelRatio);
$contentWidth = max($contentWidth, $x + $width);
}
}
// 计算居中偏移量,但确保不会导致左边被裁剪
$centerOffset = max(0, ($targetWidth - $contentWidth) / 2);
// 调整所有元素位置
foreach ($this->data as &$element) {
// 先处理负坐标问题
if ($offset > 0) {
$element['x'] += $offset;
}
// 再添加居中偏移,让内容向右移动,但不超过合理范围
$element['x'] += $centerOffset;
// 确保X坐标不会超出合理范围
if ($element['x'] < 0) {
$element['x'] = 0;
}
}
}
/**
* 重新计算调整元素位置后的尺寸
*/
private function recalculateSize() {
$maxX = 0;
$maxY = 0;
$minX = 0;
$minY = 0;
foreach ($this->data as $element) {
$x = (int)($element['x'] * $this->pixelRatio);
$y = (int)($element['y'] * $this->pixelRatio);
// 跟踪最小坐标
if ($x < $minX) $minX = $x;
if ($y < $minY) $minY = $y;
if ($element['plugin'] === 'sp-label') {
$width = (int)($element['width'] * $this->pixelRatio);
$height = (int)($element['height'] * $this->pixelRatio);
$maxX = max($maxX, $x + $width);
$maxY = max($maxY, $y + $height);
} elseif ($element['plugin'] === 'sp-level') {
$width = (int)($element['width'] * $this->pixelRatio);
$thickness = (int)($element['thickness'] * $this->pixelRatio);
$maxX = max($maxX, $x + $width);
$maxY = max($maxY, $y + $thickness); // 考虑线条粗细
} elseif ($element['plugin'] === 'sp-vertical') {
$height = (int)($element['height'] * $this->pixelRatio);
$maxX = max($maxX, $x);
$maxY = max($maxY, $y + $height);
} elseif ($element['plugin'] === 'sp-input') {
$width = (int)($element['width'] * $this->pixelRatio);
$height = (int)($element['height'] * $this->pixelRatio);
$maxX = max($maxX, $x + $width);
$maxY = max($maxY, $y + $height);
} elseif ($element['plugin'] === 'sp-barcode') {
$width = (int)($element['width'] * $this->pixelRatio);
$height = (int)($element['height'] * $this->pixelRatio);
$maxX = max($maxX, $x + $width);
$maxY = max($maxY, $y + $height);
}
}
// 如果左边有负坐标,需要调整所有元素位置
if ($minX < 0) {
$this->adjustElementPositions(abs($minX));
// 重新计算调整后的尺寸
return $this->recalculateSize();
}
// 完全禁用向上偏移,保护头部区域不被截断
$targetHeight = 1040;
// 注释掉原来的向上偏移逻辑,确保头部文字和线条不被截断
/*
if ($maxY < $targetHeight) {
$upwardShift = ($targetHeight - $maxY) * 0.5;
$upwardShift = min($upwardShift, 60);
foreach ($this->data as &$element) {
$element['y'] -= $upwardShift / $this->pixelRatio;
}
// 重新计算Y坐标
$maxY = 0;
foreach ($this->data as $element) {
$y = (int)($element['y'] * $this->pixelRatio);
if ($element['plugin'] === 'sp-label') {
$height = (int)($element['height'] * $this->pixelRatio);
$maxY = max($maxY, $y + $height);
} elseif ($element['plugin'] === 'sp-input') {
$height = (int)($element['height'] * $this->pixelRatio);
$maxY = max($maxY, $y + $height);
} elseif ($element['plugin'] === 'sp-barcode') {
$height = (int)($element['height'] * $this->pixelRatio);
$maxY = max($maxY, $y + $height);
}
}
}
*/
return array($maxX, $maxY);
}
// 获取JSON中的实际尺寸
private function getImageSize() {
$maxX = 0;
$maxY = 0;
$minX = 0; // 添加最小X坐标跟踪
foreach ($this->data as $element) {
$x = (int)($element['x'] * $this->pixelRatio);
$y = (int)($element['y'] * $this->pixelRatio);
// 跟踪最小X坐标确保左边元素不被裁剪
if ($x < $minX) {
$minX = $x;
}
if ($element['plugin'] === 'sp-label') {
$width = (int)($element['width'] * $this->pixelRatio);
$height = (int)($element['height'] * $this->pixelRatio);
$maxX = max($maxX, $x + $width);
$maxY = max($maxY, $y + $height);
} elseif ($element['plugin'] === 'sp-level') {
$width = (int)($element['width'] * $this->pixelRatio);
$maxX = max($maxX, $x + $width);
$maxY = max($maxY, $y);
} elseif ($element['plugin'] === 'sp-vertical') {
$height = (int)($element['height'] * $this->pixelRatio);
$maxX = max($maxX, $x);
$maxY = max($maxY, $y + $height);
} elseif ($element['plugin'] === 'sp-input') {
$width = (int)($element['width'] * $this->pixelRatio);
$height = (int)($element['height'] * $this->pixelRatio);
$maxX = max($maxX, $x + $width);
$maxY = max($maxY, $y + $height);
} elseif ($element['plugin'] === 'sp-barcode') {
$width = (int)($element['width'] * $this->pixelRatio);
$height = (int)($element['height'] * $this->pixelRatio);
$maxX = max($maxX, $x + $width);
$maxY = max($maxY, $y + $height);
}
}
// 如果左边有负坐标,需要调整所有元素位置
if ($minX < 0) {
$this->adjustElementPositions(abs($minX));
// 重新计算调整后的尺寸
return $this->recalculateSize();
} else {
// 即使没有负坐标,也进行居中调整
$this->adjustElementPositions(0);
return $this->recalculateSize();
}
}
private $data = array();
private $inputData = array(); // 存储输入数据
/**
* __construct
* @param mixed $template template
* @param mixed $width ID
* @param mixed $height height
* @return mixed 返回值
*/
public function __construct($template = null, $width = 608, $height = 1040) {
// 76*130mm 转换为像素76mm × 8px/mm = 608px, 130mm × 8px/mm = 1040px
// 应用像素转换比例1mm ≈ 8px
$this->width = (int)$width;
$this->height = (int)$height;
// 确保最小尺寸
if ($this->width < 608) $this->width = 608;
if ($this->height < 1040) $this->height = 1040;
if ($template) {
$this->setTemplate($template);
}
}
/**
* 设置输入数据
*/
public function setInputData($data) {
$this->inputData = $data;
}
/**
* 设置快递模板数据
*/
public function setTemplate($template) {
$this->data = $template;
}
/**
* 设置测试数据
*/
public function setTestData() {
$this->data = array(
// 这里可以设置测试数据
);
}
/**
* 优化图片尺寸,减少右边空白
*/
private function optimizeImageSize($contentWidth, $contentHeight) {
// 目标尺寸76*130mm = 608*1040px
$targetWidth = 608;
$targetHeight = 1040;
// 如果内容尺寸小于目标尺寸,使用目标尺寸
if ($contentWidth <= $targetWidth && $contentHeight <= $targetHeight) {
return array($targetWidth, $targetHeight);
}
// 如果内容尺寸大于目标尺寸,添加足够的边距确保不被截掉
$minMargin = 15; // 减少最小边距到15px避免过多空白
// 对于商品列表等长文本,增加更多边距
$optimalWidth = max($contentWidth + $minMargin, $targetWidth);
$optimalHeight = max($contentHeight + $minMargin, $targetHeight);
// 如果高度接近目标高度,确保有足够的下边距,但不要过多
if ($optimalHeight > $targetHeight * 0.9) {
$optimalHeight = max($optimalHeight, $targetHeight + 20); // 减少到20px
}
// 限制最大尺寸,避免生成过大的图片
$maxWidth = $targetWidth + 50; // 最大宽度不超过目标宽度+50px
$maxHeight = $targetHeight + 50; // 最大高度不超过目标高度+50px
$optimalWidth = min($optimalWidth, $maxWidth);
$optimalHeight = min($optimalHeight, $maxHeight);
return array($optimalWidth, $optimalHeight);
}
/**
* 紧凑化图片尺寸,严格控制避免空白页
*/
private function compactImageSize($contentWidth, $contentHeight) {
// 目标尺寸76*130mm = 608*1040px
$targetWidth = 608;
$targetHeight = 1040;
// 如果内容尺寸小于目标尺寸,使用目标尺寸
if ($contentWidth <= $targetWidth && $contentHeight <= $targetHeight) {
return array($targetWidth, $targetHeight);
}
// 计算紧凑的尺寸,最小边距
$minMargin = 8; // 减少最小边距
$optimalWidth = max($contentWidth + $minMargin, $targetWidth);
$optimalHeight = max($contentHeight + $minMargin, $targetHeight);
// 严格限制最大尺寸,防止空白页
$maxWidth = $targetWidth + 25; // 严格控制宽度
$maxHeight = $targetHeight + 15; // 严格控制最大高度,避免空白页
$optimalWidth = min($optimalWidth, $maxWidth);
$optimalHeight = min($optimalHeight, $maxHeight);
return array($optimalWidth, $optimalHeight);
}
/**
* 创建图片
*/
public function createImage() {
// 获取实际需要的图片尺寸
list($maxX, $maxY) = $this->getImageSize();
// 使用紧凑化方法计算最优图片尺寸,减少空白区域
list($optimalWidth, $optimalHeight) = $this->compactImageSize($maxX, $maxY);
// compactImageSize已经包含智能头部保护无需额外边距
// 设置图片尺寸
$this->width = $optimalWidth;
$this->height = $optimalHeight;
// 创建图片
$this->image = imagecreate($this->width, $this->height);
// 设置背景色为白色
$white = imagecolorallocate($this->image, 255, 255, 255);
$black = imagecolorallocate($this->image, 0, 0, 0);
// 填充白色背景
imagefill($this->image, 0, 0, $white);
// 渲染所有元素
foreach ($this->data as $element) {
$this->renderElement($element, $black);
}
}
/**
* 渲染单个元素
*/
private function renderElement($element, $color) {
$plugin = $element['plugin'];
switch ($plugin) {
case 'sp-label':
$this->renderLabel($element, $color);
break;
case 'sp-input':
$this->renderInput($element, $color);
break;
case 'sp-barcode':
$this->renderBarcode($element, $color);
break;
case 'sp-level':
$this->renderHorizontalLine($element, $color);
break;
case 'sp-vertical':
$this->renderVerticalLine($element, $color);
break;
}
}
/**
* 渲染标签文字
*/
private function renderLabel($element, $color) {
$x = (int)($element['x'] * $this->pixelRatio);
$y = (int)($element['y'] * $this->pixelRatio);
$width = (int)($element['width'] * $this->pixelRatio);
$height = (int)($element['height'] * $this->pixelRatio);
$text = $element['edit'];
$fontSize = (int)($element['fontSize'] * $this->pixelRatio);
$fontFamily = $element['fontFamily'] ?: 'simhei';
$textColor = $this->parseColor($element['color']);
$bold = $element['bold'] === 'bold';
// 获取字体文件
$fontFile = $this->getFontFile($fontFamily);
// 解析对齐方式
$arrangement = $element['Arrangement'];
$align = 'left';
$valign = 'top';
if ($arrangement['lm']) $align = 'center';
elseif ($arrangement['lr']) $align = 'right';
if ($arrangement['vm']) $valign = 'middle';
elseif ($arrangement['vb']) $valign = 'bottom';
// 计算文字位置
$bbox = imagettfbbox($fontSize, 0, $fontFile, $text);
$textWidth = $bbox[4] - $bbox[0];
$textHeight = $bbox[1] - $bbox[7];
// 计算X坐标
switch ($align) {
case 'center':
$x = $x + ($width - $textWidth) / 2;
break;
case 'right':
$x = $x + $width - $textWidth;
break;
default:
// left对齐x保持不变
break;
}
// 确保X坐标不超出边界
if ($x < 0) $x = 0;
// 处理文字换行
$lines = $this->wrapText($text, $fontFile, $fontSize, $width);
// 检查是否是商品列表区域(通过文本内容判断)
$isItemList = false;
if (strpos($text, '货号+货品名+规格+数量') !== false ||
strpos($text, 'teststore') !== false ||
strpos($text, 'x 1') !== false) {
$isItemList = true;
}
// 根据是否是商品列表调整行间距和起始位置
if ($isItemList) {
// 检查是否包含中英文混合内容
$hasMixedContent = $this->hasMixedChineseEnglish($text);
if ($hasMixedContent) {
// 进一步分析混合内容的类型
$mixedType = $this->analyzeMixedContent($text);
switch ($mixedType) {
case 'product_id':
$lineHeight = $fontSize * 1.6; // 产品ID类型1012155 : 产品名 x 1
break;
case 'description':
$lineHeight = $fontSize * 1.5; // 产品描述类型
break;
default:
$lineHeight = $fontSize * 1.5; // 默认混合内容
}
} else {
$lineHeight = $fontSize * 1.2; // 纯中文或纯英文使用1.2
}
// 商品列表紧贴上方,减少空白
$startY = $y; // 直接从元素顶部开始,不留空白
} else {
$lineHeight = $fontSize * 1.4; // 其他文字使用正常行间距
// 其他文字使用正常对齐
switch ($valign) {
case 'middle':
$startY = $y + ($height - count($lines) * $lineHeight) / 2;
break;
case 'bottom':
$startY = $y + $height - count($lines) * $lineHeight;
break;
case 'top':
default:
$startY = $y;
break;
}
}
$totalHeight = count($lines) * $lineHeight;
// 限制高度但确保至少显示2行
$maxLines = max(2, floor($height / $lineHeight));
if (count($lines) > $maxLines) {
$lines = array_slice($lines, 0, $maxLines);
// 如果最后一行被截断,添加省略号
if (strlen($text) > 50) {
$lastLine = $lines[count($lines) - 1];
if (strlen($lastLine) > 10) {
$lines[count($lines) - 1] = substr($lastLine, 0, -3) . '...';
}
}
}
// 绘制每一行文字
foreach ($lines as $index => $line) {
$lineBbox = imagettfbbox($fontSize, 0, $fontFile, $line);
$lineTextHeight = $lineBbox[1] - $lineBbox[7];
$lineWidth = $this->getTextWidth($line, $fontFile, $fontSize);
// 计算当前行的X坐标
$currentX = $x;
switch ($align) {
case 'center':
$currentX = $x + ($width - $lineWidth) / 2;
break;
case 'right':
$currentX = $x + $width - $lineWidth;
break;
default:
$currentX = $x;
break;
}
// 计算当前行的Y坐标
if ($isItemList) {
// 商品列表紧凑排列,紧贴上方
$currentY = $startY + ($index * $lineHeight) + $lineTextHeight;
} else {
// 其他文字使用正常排列
$currentY = $startY + ($index + 1) * $lineHeight - ($lineHeight - $lineTextHeight) / 2;
}
// 绘制文字
if ($bold) {
imagettftext($this->image, $fontSize, 0, (int)round($currentX-1), (int)round($currentY), $textColor, $fontFile, $line);
imagettftext($this->image, $fontSize, 0, (int)round($currentX+1), (int)round($currentY), $textColor, $fontFile, $line);
}
imagettftext($this->image, $fontSize, 0, (int)round($currentX), (int)round($currentY), $textColor, $fontFile, $line);
}
return; // 提前返回,避免重复绘制
}
/**
* 渲染水平线
*/
private function renderHorizontalLine($element, $color) {
$x = (int)($element['x'] * $this->pixelRatio);
$y = (int)($element['y'] * $this->pixelRatio);
$width = (int)($element['width'] * $this->pixelRatio);
$thickness = (int)($element['thickness'] * $this->pixelRatio);
$lineStyle = $element['lineStyle'];
// 设置线条粗细
imagesetthickness($this->image, $thickness);
// 检查是否是商品列表上方的分隔线
$isItemSeparator = false;
if ($y > 200 && $y < 400) { // 商品列表区域的大致Y坐标范围
$isItemSeparator = true;
}
// 特殊处理签收和时间下面的横线,让它们更靠近文字底部
if (($x == (int)(45 * $this->pixelRatio) && $y == (int)(330 * $this->pixelRatio)) || ($x == (int)(219 * $this->pixelRatio) && $y == (int)(328 * $this->pixelRatio))) {
// 调整Y坐标让横线更靠近文字底部
$adjustedY = $y + (int)(5 * $this->pixelRatio); // 向下移动5像素
imageline($this->image, $x, $adjustedY, $x + $width, $adjustedY, $color);
} elseif ($isItemSeparator) {
// 商品列表上方的分隔线,向下移动,减少与下方文本的空白
$adjustedY = $y + (int)(10 * $this->pixelRatio); // 向下移动10像素
imageline($this->image, $x, $adjustedY, $x + $width, $adjustedY, $color);
} else {
// 绘制线条
imageline($this->image, $x, $y, $x + $width, $y, $color);
}
}
/**
* 渲染输入字段
*/
private function renderInput($element, $color) {
$x = (int)($element['x'] * $this->pixelRatio);
$y = (int)($element['y'] * $this->pixelRatio);
$width = (int)($element['width'] * $this->pixelRatio);
$height = (int)($element['height'] * $this->pixelRatio);
$fontSize = (int)($element['fontSize'] * $this->pixelRatio);
$fontFamily = $element['fontFamily'] ?: 'simhei';
$textColor = $this->parseColor($element['color']);
$bold = $element['bold'] === 'bold';
// 获取输入类型和数据
$type = $element['type'];
$fieldName = $type['value']; // 例如: ship_name
$label = $type['label']; // 例如: 收货人-姓名
// 从输入数据中获取值
$text = isset($this->inputData[$fieldName]) ? $this->inputData[$fieldName] : '';
// 如果没有数据,显示标签
if (empty($text)) {
$text = $label;
}
// 获取字体文件
$fontFile = $this->getFontFile($fontFamily);
// 解析对齐方式
$arrangement = $element['Arrangement'];
$align = 'left';
$valign = 'top';
if ($arrangement['lm']) $align = 'center';
elseif ($arrangement['lr']) $align = 'right';
if ($arrangement['vm']) $valign = 'middle';
elseif ($arrangement['vb']) $valign = 'bottom';
// 处理文字换行
$lines = $this->wrapText($text, $fontFile, $fontSize, $width);
// 检查是否是商品列表区域(通过文本内容判断)
$isItemList = false;
if (strpos($text, 'teststore') !== false ||
strpos($text, 'x 1') !== false ||
strpos($text, '') !== false) {
$isItemList = true;
}
// 根据是否是商品列表调整行间距
if ($isItemList) {
// 检查是否包含中英文混合内容
$hasMixedContent = $this->hasMixedChineseEnglish($text);
if ($hasMixedContent) {
// 进一步分析混合内容的类型
$mixedType = $this->analyzeMixedContent($text);
switch ($mixedType) {
case 'product_id':
$lineHeight = $fontSize * 1.6; // 产品ID类型1012155 : 产品名 x 1
break;
case 'description':
$lineHeight = $fontSize * 1.5; // 产品描述类型
break;
default:
$lineHeight = $fontSize * 1.5; // 默认混合内容
}
} else {
$lineHeight = $fontSize * 1.2; // 纯中文或纯英文使用1.2
}
} else {
$lineHeight = $fontSize * 1.4; // 其他输入使用正常行间距
}
$totalHeight = count($lines) * $lineHeight;
// 限制高度但确保至少显示2行
$maxLines = max(2, floor($height / $lineHeight));
if (count($lines) > $maxLines) {
$lines = array_slice($lines, 0, $maxLines);
// 如果最后一行被截断,添加省略号
if (strlen($text) > 50) {
$lastLine = $lines[count($lines) - 1];
if (strlen($lastLine) > 10) {
$lines[count($lines) - 1] = substr($lastLine, 0, -3) . '...';
}
}
}
// 计算垂直起始位置 - 商品列表紧贴上方
if ($isItemList) {
// 商品列表紧贴上方,不留空白
$startY = $y;
} else {
// 其他输入使用正常对齐
switch ($valign) {
case 'middle':
$startY = $y + ($height - $totalHeight) / 2;
break;
case 'bottom':
$startY = $y + $height - $totalHeight;
break;
case 'top':
default:
$startY = $y;
break;
}
}
// 绘制每一行文字
foreach ($lines as $index => $line) {
$lineBbox = imagettfbbox($fontSize, 0, $fontFile, $line);
$lineTextHeight = $lineBbox[1] - $lineBbox[7];
$lineWidth = $this->getTextWidth($line, $fontFile, $fontSize);
// 计算当前行的X坐标
$currentX = $x;
switch ($align) {
case 'center':
$currentX = $x + ($width - $lineWidth) / 2;
break;
case 'right':
$currentX = $x + $width - $lineWidth;
break;
default:
$currentX = $x;
break;
}
// 计算当前行的Y坐标
if ($isItemList) {
// 商品列表紧凑排列,紧贴上方
$currentY = $startY + ($index * $lineHeight) + $lineTextHeight;
} else {
// 其他输入使用正常排列
$currentY = $startY + ($index + 1) * $lineHeight - ($lineHeight - $lineTextHeight) / 2;
}
// 绘制文字
if ($bold) {
imagettftext($this->image, $fontSize, 0, (int)round($currentX-1), (int)round($currentY), $textColor, $fontFile, $line);
imagettftext($this->image, $fontSize, 0, (int)round($currentX+1), (int)round($currentY), $textColor, $fontFile, $line);
}
imagettftext($this->image, $fontSize, 0, (int)round($currentX), (int)round($currentY), $textColor, $fontFile, $line);
}
}
/**
* 渲染条形码
*/
private function renderBarcode($element, $color) {
$x = (int)($element['x'] * $this->pixelRatio);
$y = (int)($element['y'] * $this->pixelRatio);
$width = (int)($element['width'] * $this->pixelRatio);
$height = (int)($element['height'] * $this->pixelRatio);
$code = $element['code'];
$showText = $element['text'] === 'true';
// 获取条形码数据
$type = $element['type'];
$fieldName = $type['value'];
// 从输入数据中获取条形码值
$barcodeValue = isset($this->inputData[$fieldName]) ? $this->inputData[$fieldName] : $code;
// 获取条形码类型默认为A
$barcodeType = 'A';
if (isset($type['value']) && $type['value'] === 'code128b') {
$barcodeType = 'B';
} elseif (isset($type['label']) && strpos(strtolower($type['label']), '128b') !== false) {
$barcodeType = 'B';
}
$pattern = $this->generateCode128($barcodeValue, $width, $height, $barcodeType);
$this->drawBarcode($this->image, $x, $y, $width, $height, $pattern, $color);
// 如果显示文字,在条形码下方绘制文字
if ($showText) {
$fontSize = (int)(10 * $this->pixelRatio);
$fontFile = $this->getFontFile('simhei');
$textY = $y + $height + (int)(15 * $this->pixelRatio); // 增加间距,确保文字不被条形码遮挡
// 计算文字宽度以居中
$bbox = imagettfbbox($fontSize, 0, $fontFile, $barcodeValue);
$textWidth = $bbox[4] - $bbox[0];
$textX = $x + ($width - $textWidth) / 2;
// 确保文字完全在条形码容器内
if ($textX < $x) {
$textX = $x;
}
if ($textX + $textWidth > $x + $width) {
$textX = $x + $width - $textWidth;
}
// 额外检查:如果文字宽度超过容器宽度,则缩小字体或截断文字
if ($textWidth > $width) {
// 尝试缩小字体
$newFontSize = $fontSize;
while ($textWidth > $width && $newFontSize > 6) {
$newFontSize--;
$bbox = imagettfbbox($newFontSize, 0, $fontFile, $barcodeValue);
$textWidth = $bbox[4] - $bbox[0];
}
// 如果字体已经最小但仍然超出,则截断文字
if ($textWidth > $width) {
$maxChars = floor($width / ($fontSize * 0.6)); // 估算字符数
$barcodeValue = substr($barcodeValue, 0, $maxChars);
$bbox = imagettfbbox($fontSize, 0, $fontFile, $barcodeValue);
$textWidth = $bbox[4] - $bbox[0];
$textX = $x + ($width - $textWidth) / 2;
}
}
// 最终边界检查:确保文字完全在条形码容器内
$textEndX = $textX + $textWidth;
if ($textEndX > $x + $width) {
$textX = $x + $width - $textWidth;
}
if ($textX < $x) {
$textX = $x;
}
imagettftext($this->image, $fontSize, 0, (int)round($textX), (int)round($textY), $color, $fontFile, $barcodeValue);
}
}
/**
* 渲染垂直线
*/
private function renderVerticalLine($element, $color) {
$x = (int)($element['x'] * $this->pixelRatio);
$y = (int)($element['y'] * $this->pixelRatio);
$height = (int)($element['height'] * $this->pixelRatio);
$thickness = (int)($element['thickness'] * $this->pixelRatio);
$lineStyle = $element['lineStyle'];
// 设置线条粗细
imagesetthickness($this->image, $thickness);
// 绘制线条
imageline($this->image, $x, $y, $x, $y + $height, $color);
}
/**
* 解析颜色
*/
private function parseColor($colorStr) {
if (preg_match('/^#([A-Fa-f0-9]{6})$/', $colorStr, $matches)) {
$hex = $matches[1];
$r = hexdec(substr($hex, 0, 2));
$g = hexdec(substr($hex, 2, 2));
$b = hexdec(substr($hex, 4, 2));
return imagecolorallocate($this->image, $r, $g, $b);
}
return imagecolorallocate($this->image, 0, 0, 0); // 默认黑色
}
/**
* 检测文本是否包含中英文混合内容
*/
private function hasMixedChineseEnglish($text) {
// 检查是否包含中文字符
$hasChinese = preg_match('/[\x{4e00}-\x{9fa5}]/u', $text);
// 检查是否包含英文字符或数字
$hasEnglish = preg_match('/[a-zA-Z0-9]/', $text);
// 如果同时包含中文和英文/数字,则为混合内容
return $hasChinese && $hasEnglish;
}
/**
* 分析文本中是否包含中英文混合内容,并返回类型
*/
private function analyzeMixedContent($text) {
// 检查是否包含中文字符
$hasChinese = preg_match('/[\x{4e00}-\x{9fa5}]/u', $text);
// 检查是否包含英文字符或数字
$hasEnglish = preg_match('/[a-zA-Z0-9]/', $text);
// 检查是否包含冒号
$hasColon = preg_match('//', $text);
// 检查是否包含空格
$hasSpace = preg_match('/ /', $text);
// 检查是否包含数字
$hasNumber = preg_match('/[0-9]/', $text);
// 检查是否包含字母
$hasLetter = preg_match('/[a-zA-Z]/', $text);
// 检查是否包含特殊字符
$hasSpecialChar = preg_match('/[:,。!?;:]/u', $text);
// 根据特征判断混合内容类型
if ($hasChinese && $hasEnglish && $hasColon && $hasSpace && $hasNumber && $hasLetter) {
return 'product_id'; // 例如1012155 : 产品名 x 1
} elseif ($hasChinese && $hasEnglish && $hasSpecialChar) {
return 'description'; // 例如:产品描述
}
return 'mixed'; // 默认混合内容
}
/**
* 获取文本宽度(优化中英文混合宽度计算)
*/
private function getTextWidth($text, $fontFile, $fontSize) {
// 使用GD库的imagettfbbox获取准确宽度
$bbox = imagettfbbox($fontSize, 0, $fontFile, $text);
if ($bbox === false) {
// 如果获取失败,使用估算方法
return $this->estimateTextWidth($text, $fontSize);
}
return $bbox[4] - $bbox[0];
}
/**
* 估算文本宽度当GD库方法失败时的备用方案
*/
private function estimateTextWidth($text, $fontSize) {
$width = 0;
$chars = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY);
foreach ($chars as $char) {
// 中文字符宽度约为字体大小的1.0倍
if (preg_match('/[\x{4e00}-\x{9fa5}]/u', $char)) {
$width += $fontSize * 1.0;
}
// 英文字符宽度约为字体大小的0.6倍
elseif (preg_match('/[a-zA-Z0-9]/', $char)) {
$width += $fontSize * 0.6;
}
// 标点符号宽度约为字体大小的0.5倍
elseif (preg_match('/[:,。!?;:]/u', $char)) {
$width += $fontSize * 0.5;
}
// 空格宽度约为字体大小的0.3倍
elseif ($char === ' ') {
$width += $fontSize * 0.3;
}
// 其他字符使用默认宽度
else {
$width += $fontSize * 0.6;
}
}
return $width;
}
/**
* 文字换行处理
*/
private function wrapText($text, $fontFile, $fontSize, $maxWidth) {
$lines = array();
// 检查是否包含换行符
if (strpos($text, "\n") !== false) {
// 如果包含换行符,按换行符分割
$rawLines = explode("\n", $text);
foreach ($rawLines as $rawLine) {
$rawLine = trim($rawLine);
if (!empty($rawLine)) {
// 对每一行进行宽度检查和换行处理
$subLines = $this->processLine($rawLine, $fontFile, $fontSize, $maxWidth);
$lines = array_merge($lines, $subLines);
}
}
} else {
// 没有换行符,使用原来的处理方式
$lines = $this->processLine($text, $fontFile, $fontSize, $maxWidth);
}
return $lines;
}
/**
* 处理单行文字
*/
private function processLine($text, $fontFile, $fontSize, $maxWidth) {
$lines = array();
$currentLine = '';
// 将文字按字符分割(支持中文)
$chars = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY);
foreach ($chars as $index => $char) {
$testLine = $currentLine . $char;
$lineWidth = $this->getTextWidth($testLine, $fontFile, $fontSize);
if ($lineWidth <= $maxWidth) {
$currentLine = $testLine;
} else {
if (!empty($currentLine)) {
// 如果当前字符是冒号,且前面有内容,则强制不换行
if ($char === '' && !empty($currentLine)) {
$currentLine = $testLine;
} else {
$lines[] = trim($currentLine);
$currentLine = $char;
}
} else {
// 单个字符就超过宽度,强制换行
$lines[] = $char;
$currentLine = '';
}
}
}
if (!empty($currentLine)) {
$lines[] = trim($currentLine);
}
// 确保所有行都不为空
$lines = array_filter($lines, function($line) {
return !empty(trim($line));
});
// 如果没有行,至少返回一个空行
if (empty($lines)) {
$lines = array('');
}
return $lines;
}
/**
* 获取字体文件路径
*/
private function getFontFile($fontFamily) {
$fontMap = array(
'simhei' => '/System/Library/Fonts/STHeiti Medium.ttc',
'Arial' => '/System/Library/Fonts/Arial.ttf',
'Times New Roman' => '/System/Library/Fonts/Times.ttc'
);
if (isset($fontMap[$fontFamily])) {
$fontPath = $fontMap[$fontFamily];
if (file_exists($fontPath)) {
return $fontPath;
}
}
// 默认返回中文字体
return ROOT_DIR . '/app/wap/statics/fonts/msyh.ttf';
}
/**
* 保存图片
*/
public function saveImage($outputFile) {
if (!$this->image) {
throw new Exception("请先调用 createImage() 方法");
}
imagejpeg($this->image, $outputFile, 95);
}
/**
* 输出图片到浏览器
*/
public function outputImage() {
if (!$this->image) {
throw new Exception("请先调用 createImage() 方法");
}
header('Content-Type: image/jpeg');
imagejpeg($this->image, null, 95);
}
/**
* 释放资源
*/
public function __destruct() {
if ($this->image) {
imagedestroy($this->image);
}
}
/**
* 生成Code128B条形码
* 参考: https://github.com/paul8888/code128barcode/blob/master/code128.php
*/
public function generateCode128($text, $width, $height, $type = 'B') {
// 确保输入是字符串
$text = (string)$text;
// 限制文本长度,防止条形码过长
$maxChars = floor($width / 8); // 每个字符大约需要8个模块
if (strlen($text) > $maxChars) {
$text = substr($text, 0, $maxChars);
}
// 构建条形码数据
$barcodeData = "211214"; // Code128B起始码
// 添加字符编码
for ($i = 0; $i < strlen($text); $i++) {
$char = $text[$i];
if (isset($this->code128Table[$char])) {
$barcodeData .= $this->code128Table[$char];
} else {
// 如果字符不在编码表中,使用空格
$barcodeData .= $this->code128Table[' '];
}
}
// 计算校验和
$checksum = 104; // Code128B起始字符值
for ($i = 0; $i < strlen($text); $i++) {
$char = $text[$i];
$checksum += (ord($char) - 32) * ($i + 1);
}
$checksum = $checksum % 103;
// 添加校验和字符
$checksumChar = chr($checksum + 32);
if (isset($this->code128Table[$checksumChar])) {
$barcodeData .= $this->code128Table[$checksumChar];
}
// 添加停止码
$barcodeData .= "2331112";
return $barcodeData;
}
/**
* 绘制条形码到图片
*/
private function drawBarcode($image, $x, $y, $width, $height, $barcodeData, $color) {
// 将编码数据转换为黑白条图案
$pattern = $this->convertToBars($barcodeData);
$moduleWidth = $width / strlen($pattern);
// 确保模块宽度不为负数或零
if ($moduleWidth <= 0) {
return;
}
for ($i = 0; $i < strlen($pattern); $i++) {
if ($pattern[$i] == '1') {
$moduleX = $x + ($i * $moduleWidth);
$moduleEndX = $moduleX + $moduleWidth;
// 严格确保条形码不超出容器边界
if ($moduleX < $x) {
$moduleX = $x;
}
if ($moduleEndX > $x + $width) {
$moduleEndX = $x + $width;
}
// 额外检查:确保模块在有效范围内
if ($moduleX >= $x && $moduleEndX <= $x + $width && $moduleX < $moduleEndX) {
// 显式转换为整数,避免精度丢失警告
$moduleXInt = (int)round($moduleX);
$moduleEndXInt = (int)round($moduleEndX);
$yInt = (int)$y;
$heightInt = (int)$height;
imagefilledrectangle($image, $moduleXInt, $yInt, $moduleEndXInt, $yInt + $heightInt, $color);
}
}
}
}
/**
* 将编码数据转换为黑白条图案
*/
public function convertToBars($barcodeData) {
$pattern = '';
$isBlack = true; // 从黑色条开始
for ($i = 0; $i < strlen($barcodeData); $i++) {
$width = (int)$barcodeData[$i];
for ($j = 0; $j < $width; $j++) {
$pattern .= $isBlack ? '1' : '0';
}
$isBlack = !$isBlack; // 切换黑白
}
return $pattern;
}
/**
* 获取图片数据不输出HTTP头
* @param int $quality 图片质量 (1-100)
* @return string|false 图片数据或false
*/
public function getImageData($quality = 95) {
if (!$this->image) {
return false;
}
// 确保质量在有效范围内
$quality = max(1, min(100, (int)$quality));
try {
// 检查输出缓冲状态
$obLevel = ob_get_level();
if ($obLevel > 0) {
}
// 开始新的输出缓冲
ob_start();
// 生成JPEG数据
$result = imagejpeg($this->image, null, $quality);
if ($result === false) {
ob_end_clean();
return false;
}
// 获取缓冲内容
$imageData = ob_get_contents();
if ($imageData === false) {
ob_end_clean();
return false;
}
// 清理输出缓冲
ob_end_clean();
// 验证数据
if (empty($imageData)) {
return false;
}
// 检查JPEG头部
if (strlen($imageData) < 10 || substr($imageData, 0, 2) !== "\xFF\xD8") {
return false;
}
return $imageData;
} catch (Exception $e) {
// 确保清理输出缓冲
while (ob_get_level() > 0) {
ob_end_clean();
}
return false;
}
imagejpeg($this->image, null, 95);
}
}
/**
* 加载快递模板文件支持JSON和序列化格式
*/
function loadTemplate($templateFile) {
if (!file_exists($templateFile)) {
throw new Exception("模板文件不存在: $templateFile");
}
$templateContent = file_get_contents($templateFile);
// 尝试JSON解析
$template = json_decode($templateContent, true);
if (json_last_error() === JSON_ERROR_NONE) {
return $template;
}
// 如果JSON解析失败尝试反序列化
$template = unserialize($templateContent);
if ($template === false) {
throw new Exception("模板文件格式错误既不是有效的JSON也不是有效的序列化数据");
}
return $template;
}
/**
* 加载数据文件支持JSON和序列化格式
*/
function loadData($dataFile) {
if (!file_exists($dataFile)) {
throw new Exception("数据文件不存在: $dataFile");
}
$dataContent = file_get_contents($dataFile);
// 尝试JSON解析
$data = json_decode($dataContent, true);
if (json_last_error() === JSON_ERROR_NONE) {
return $data;
}
// 如果JSON解析失败尝试反序列化
$data = unserialize($dataContent);
if ($data === false) {
throw new Exception("数据文件格式错误既不是有效的JSON也不是有效的序列化数据");
}
return $data;
}
// 命令行使用
if (php_sapi_name() === 'cli' && basename(__FILE__) === basename($_SERVER['SCRIPT_NAME'])) {
if ($argc < 3) {
echo "用法: php json-to-image.php <模板文件> <输出jpg文件> [数据文件] [宽度] [高度]\n";
echo "示例: php json-to-image.php template.json dd-output.jpg\n";
echo "示例: php json-to-image.php template.json dd-output.jpg data.json\n";
echo "示例: php json-to-image.php template.json dd-output.jpg data.json 608 1040\n";
echo "默认尺寸: 76mm × 130mm = 608px × 1040px (1mm ≈ 8px)\n";
echo "注意: 像素转换比例已调整为 1mm ≈ 8px (原为 1mm = 3.75px)\n";
exit(1);
}
$jsonFile = $argv[1];
$outputFile = $argv[2];
$dataFile = isset($argv[3]) ? $argv[3] : null;
$width = isset($argv[4]) ? (int)$argv[4] : 608; // 默认76mm
$height = isset($argv[5]) ? (int)$argv[5] : 1040; // 默认130mm
try {
// 加载快递模板数据
$templateData = loadTemplate($jsonFile);
// 创建转换器实例
$converter = new JsonToImageConverter($templateData, $width, $height);
// 如果提供了数据文件,加载数据
if ($dataFile && file_exists($dataFile)) {
try {
$inputData = loadData($dataFile);
$converter->setInputData($inputData);
} catch (Exception $e) {
echo "警告: 数据文件加载失败: " . $e->getMessage() . "\n";
}
}
$converter->createImage();
$converter->saveImage($outputFile);
echo "图片生成成功: $outputFile (尺寸: {$width}×{$height}px)\n";
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "\n";
exit(1);
}
}
?>