|
|
@@ -48,7 +48,7 @@ class PosterUtil
|
|
|
|
|
|
const MID_LINE_GAP = 6;
|
|
|
|
|
|
- const MID_NAME_SIZE = 32;
|
|
|
+ const MID_NAME_SIZE = 38;
|
|
|
|
|
|
const MID_REMARK_SIZE = 26;
|
|
|
|
|
|
@@ -60,14 +60,18 @@ class PosterUtil
|
|
|
|
|
|
const QR_PAD_BOTTOM = 32;
|
|
|
|
|
|
- /** 价格下方:长按识别提示(原二维码旁「微信扫码下单」已取消) */
|
|
|
+ /** 价格下方:灰色圆角条 + 文案(布局参考快团团) */
|
|
|
const FOOTER_BUY_HINT = '长按识别小程序购买';
|
|
|
|
|
|
const FOOTER_BUY_HINT_SIZE = 26;
|
|
|
|
|
|
- const FOOTER_BUY_HINT_SHADOW = 55;
|
|
|
+ const FOOTER_BUY_HINT_BAR_W = 430;
|
|
|
|
|
|
- const FOOTER_BUY_HINT_Y = 24;
|
|
|
+ const FOOTER_BUY_HINT_BAR_H = 56;
|
|
|
+
|
|
|
+ const FOOTER_BUY_HINT_BAR_COLOR = 'F2F2F2';
|
|
|
+
|
|
|
+ const FOOTER_BUY_HINT_TEXT_PAD_X = 28;
|
|
|
|
|
|
const FOOTER_TEXT_GAP = 12;
|
|
|
|
|
|
@@ -82,11 +86,11 @@ class PosterUtil
|
|
|
$options['shopTitle'] = $meta['shopTitle'];
|
|
|
$options['shopLogo'] = $meta['shopLogo'];
|
|
|
if (is_string($itemOrRemark)) {
|
|
|
- $options['itemRemark'] = trim($itemOrRemark);
|
|
|
+ $options['itemRemark'] = self::filterPosterUtf8Text($itemOrRemark);
|
|
|
} elseif ($itemOrRemark !== null) {
|
|
|
- $options['itemRemark'] = trim($itemOrRemark->itemRemark ?? '');
|
|
|
+ $options['itemRemark'] = self::filterPosterUtf8Text($itemOrRemark->itemRemark ?? '');
|
|
|
} else {
|
|
|
- $options['itemRemark'] = trim($options['itemRemark'] ?? '');
|
|
|
+ $options['itemRemark'] = self::filterPosterUtf8Text($options['itemRemark'] ?? '');
|
|
|
}
|
|
|
|
|
|
return $options;
|
|
|
@@ -120,7 +124,7 @@ class PosterUtil
|
|
|
}
|
|
|
}
|
|
|
$shopName = $shop ? trim((string)($shop->shopName ?? '')) : '';
|
|
|
- $title = self::buildShopDisplayTitle($sjName, $shopName);
|
|
|
+ $title = self::filterPosterUtf8Text(self::buildShopDisplayTitle($sjName, $shopName));
|
|
|
if (mb_strlen($title, 'UTF-8') > self::SHOP_TITLE_MAX_LEN) {
|
|
|
$title = mb_substr($title, 0, self::SHOP_TITLE_MAX_LEN, 'UTF-8') . '…';
|
|
|
}
|
|
|
@@ -141,12 +145,45 @@ class PosterUtil
|
|
|
return imgUtil::normalizeOssObjectPath($avatar);
|
|
|
}
|
|
|
|
|
|
- public static function truncateText($text, $maxLen = 14)
|
|
|
+ /**
|
|
|
+ * 海报文案 UTF-8 清洗:剔除非法字节、替换符及常见乱码字符
|
|
|
+ */
|
|
|
+ public static function filterPosterUtf8Text($text)
|
|
|
{
|
|
|
$text = trim((string)$text);
|
|
|
if ($text === '') {
|
|
|
return '';
|
|
|
}
|
|
|
+ if (function_exists('iconv')) {
|
|
|
+ $fixed = @iconv('UTF-8', 'UTF-8//IGNORE', $text);
|
|
|
+ if ($fixed !== false) {
|
|
|
+ $text = $fixed;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (function_exists('mb_convert_encoding')) {
|
|
|
+ $converted = @mb_convert_encoding($text, 'UTF-8', 'UTF-8');
|
|
|
+ if (is_string($converted) && $converted !== '') {
|
|
|
+ $text = $converted;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $text = preg_replace('/[\x{FFFD}\x{FFF0}-\x{FFFF}]/u', '', $text);
|
|
|
+ $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $text);
|
|
|
+ $text = preg_replace(
|
|
|
+ '/[^\x{4e00}-\x{9fff}a-zA-Z0-9\s,。、!?:;…·~%()【】\[\]\/\-_+|《》「」\'\"\.#&@\x{3000}-\x{303F}\x{FF01}-\x{FF5E}]/u',
|
|
|
+ '',
|
|
|
+ $text
|
|
|
+ );
|
|
|
+ $text = trim(preg_replace('/\s+/u', ' ', $text));
|
|
|
+
|
|
|
+ return $text;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static function truncateText($text, $maxLen = 14)
|
|
|
+ {
|
|
|
+ $text = self::filterPosterUtf8Text($text);
|
|
|
+ if ($text === '') {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
if (mb_strlen($text, 'UTF-8') > $maxLen) {
|
|
|
return mb_substr($text, 0, $maxLen, 'UTF-8') . '…';
|
|
|
}
|
|
|
@@ -159,7 +196,7 @@ class PosterUtil
|
|
|
*/
|
|
|
public static function splitPosterTextLines($text, $maxPerLine = self::TEXT_CHARS_PER_LINE)
|
|
|
{
|
|
|
- $text = trim((string)$text);
|
|
|
+ $text = self::filterPosterUtf8Text($text);
|
|
|
if ($text === '') {
|
|
|
return ['', ''];
|
|
|
}
|
|
|
@@ -202,6 +239,8 @@ class PosterUtil
|
|
|
*/
|
|
|
public static function buildPosterNameText($name, $flowerNum = 0, $subtitle = '')
|
|
|
{
|
|
|
+ $name = self::filterPosterUtf8Text($name);
|
|
|
+ $subtitle = self::filterPosterUtf8Text($subtitle);
|
|
|
list($line1, $line2) = self::splitPosterTextLines($name);
|
|
|
$suffix = '';
|
|
|
if ($flowerNum > 0) {
|
|
|
@@ -236,7 +275,7 @@ class PosterUtil
|
|
|
*/
|
|
|
public static function buildPosterRemarkText($itemRemark)
|
|
|
{
|
|
|
- $itemRemark = trim((string)$itemRemark);
|
|
|
+ $itemRemark = self::filterPosterUtf8Text($itemRemark);
|
|
|
if ($itemRemark === '') {
|
|
|
return '';
|
|
|
}
|
|
|
@@ -285,18 +324,44 @@ class PosterUtil
|
|
|
return self::CANVAS_PAD_Y + self::SHOP_LOGO_SIZE + self::HEADER_BOTTOM_GAP;
|
|
|
}
|
|
|
|
|
|
- private static function footerTextYPositions()
|
|
|
+ /**
|
|
|
+ * 底部左侧(价格+灰条)与右侧太阳码垂直居中对齐
|
|
|
+ *
|
|
|
+ * @return array{buyHintBarY:int,buyHintTextY:int,buyHintTextX:int,priceY:int,qrY:int}
|
|
|
+ */
|
|
|
+ private static function calcFooterLayout()
|
|
|
{
|
|
|
- $buyHintH = (int)ceil(self::FOOTER_BUY_HINT_SIZE * 1.25);
|
|
|
- $buyHintY = self::FOOTER_BUY_HINT_Y;
|
|
|
- $priceY = $buyHintY + $buyHintH + self::FOOTER_TEXT_GAP;
|
|
|
+ $qrCenterFromBottom = self::QR_PAD_BOTTOM + (int)(self::QR_SIZE / 2);
|
|
|
+ $hintBarH = self::FOOTER_BUY_HINT_BAR_H;
|
|
|
+ $priceH = (int)ceil(self::FOOTER_PRICE_SIZE * 1.3);
|
|
|
+ $gap = self::FOOTER_TEXT_GAP;
|
|
|
+ $blockH = $priceH + $gap + $hintBarH;
|
|
|
+ $buyHintBarY = max(16, $qrCenterFromBottom - (int)($blockH / 2));
|
|
|
+ $priceY = $buyHintBarY + $hintBarH + $gap;
|
|
|
+ $textLineH = (int)ceil(self::FOOTER_BUY_HINT_SIZE * 1.2);
|
|
|
+ $buyHintTextY = $buyHintBarY + (int)(($hintBarH - $textLineH) / 2);
|
|
|
+ $qrY = max(16, $qrCenterFromBottom - (int)(self::QR_SIZE / 2));
|
|
|
|
|
|
return [
|
|
|
- 'buyHint' => $buyHintY,
|
|
|
- 'price' => $priceY,
|
|
|
+ 'buyHintBarY' => $buyHintBarY,
|
|
|
+ 'buyHintTextY' => $buyHintTextY,
|
|
|
+ 'buyHintTextX' => self::CANVAS_PAD_X + self::FOOTER_BUY_HINT_TEXT_PAD_X,
|
|
|
+ 'priceY' => $priceY,
|
|
|
+ 'qrY' => $qrY,
|
|
|
];
|
|
|
}
|
|
|
|
|
|
+ /** OSS 生成灰色圆角底条(无需单独上传底图) */
|
|
|
+ private static function buildBuyHintBarWatermarkPath()
|
|
|
+ {
|
|
|
+ $w = self::FOOTER_BUY_HINT_BAR_W;
|
|
|
+ $h = self::FOOTER_BUY_HINT_BAR_H;
|
|
|
+ $r = (int)($h / 2);
|
|
|
+
|
|
|
+ return self::BLANK_CANVAS . '?x-oss-process=image/resize,m_pad,w_' . $w . ',h_' . $h
|
|
|
+ . ',color_' . self::FOOTER_BUY_HINT_BAR_COLOR . '/rounded-corners,r_' . $r;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* @return array{imgH:int,totalH:int,scaledH:int,posterW:int,coverY:int,nameY:int,remarkY:int}
|
|
|
*/
|
|
|
@@ -419,12 +484,12 @@ class PosterUtil
|
|
|
$cover = $options['cover'] ?? '';
|
|
|
$miniCode = $options['miniCode'] ?? '';
|
|
|
$priceText = $options['priceText'] ?? '';
|
|
|
- $name = $options['name'] ?? '';
|
|
|
+ $name = self::filterPosterUtf8Text($options['name'] ?? '');
|
|
|
$mainId = (int)($options['mainId'] ?? 0);
|
|
|
$flowerNum = (int)($options['flowerNum'] ?? 0);
|
|
|
- $subtitle = trim((string)($options['subtitle'] ?? ''));
|
|
|
- $itemRemark = trim((string)($options['itemRemark'] ?? ''));
|
|
|
- $shopTitle = trim((string)($options['shopTitle'] ?? ''));
|
|
|
+ $subtitle = self::filterPosterUtf8Text($options['subtitle'] ?? '');
|
|
|
+ $itemRemark = self::filterPosterUtf8Text($options['itemRemark'] ?? '');
|
|
|
+ $shopTitle = self::filterPosterUtf8Text($options['shopTitle'] ?? '');
|
|
|
$shopLogo = trim((string)($options['shopLogo'] ?? ''));
|
|
|
|
|
|
$coverW = (int)($options['coverWidth'] ?? 0);
|
|
|
@@ -441,7 +506,7 @@ class PosterUtil
|
|
|
]);
|
|
|
$displayName = self::buildPosterNameText($name, $flowerNum, $subtitle);
|
|
|
$displayRemark = self::buildPosterRemarkText($itemRemark);
|
|
|
- $footerY = self::footerTextYPositions();
|
|
|
+ $footer = self::calcFooterLayout();
|
|
|
|
|
|
$coverWatermark = self::buildCoverWatermarkPath($cover, $layout['scaledH']);
|
|
|
$qrWatermark = $miniCode . '?x-oss-process=image/resize,m_fill,h_' . self::QR_SIZE . ',w_' . self::QR_SIZE;
|
|
|
@@ -474,20 +539,21 @@ class PosterUtil
|
|
|
}
|
|
|
|
|
|
$url .= '/watermark,image_' . stringUtil::ossBase64($qrWatermark)
|
|
|
- . ',g_se,x_' . self::QR_PAD_RIGHT . ',y_' . self::QR_PAD_BOTTOM;
|
|
|
+ . ',g_se,x_' . self::QR_PAD_RIGHT . ',y_' . $footer['qrY'];
|
|
|
|
|
|
$logoB64 = self::buildHyxhLogoWatermark($mainId);
|
|
|
if ($logoB64 !== '') {
|
|
|
$url .= '/watermark,image_' . $logoB64 . ',g_se,x_210,y_48';
|
|
|
}
|
|
|
|
|
|
+ $url .= '/watermark,image_' . stringUtil::ossBase64(self::buildBuyHintBarWatermarkPath())
|
|
|
+ . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_' . $footer['buyHintBarY'];
|
|
|
+ $url .= '/watermark,text_' . stringUtil::ossBase64(self::FOOTER_BUY_HINT)
|
|
|
+ . ',g_sw,x_' . $footer['buyHintTextX'] . ',y_' . $footer['buyHintTextY']
|
|
|
+ . ',color_666666,type_' . self::FONT . ',size_' . self::FOOTER_BUY_HINT_SIZE;
|
|
|
$url .= '/watermark,text_' . stringUtil::ossBase64($priceText)
|
|
|
- . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_' . $footerY['price']
|
|
|
+ . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_' . $footer['priceY']
|
|
|
. ',color_EE2C2C,type_' . self::FONT . ',size_' . self::FOOTER_PRICE_SIZE;
|
|
|
- $url .= '/watermark,text_' . stringUtil::ossBase64(self::FOOTER_BUY_HINT)
|
|
|
- . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_' . $footerY['buyHint']
|
|
|
- . ',color_888888,type_' . self::FONT . ',size_' . self::FOOTER_BUY_HINT_SIZE
|
|
|
- . ',shadow_' . self::FOOTER_BUY_HINT_SHADOW;
|
|
|
|
|
|
return $url;
|
|
|
}
|