|
|
@@ -14,9 +14,14 @@ class PosterUtil
|
|
|
|
|
|
const POSTER_W = 750;
|
|
|
|
|
|
- /** 画布左右留白(各 20px) */
|
|
|
+ /** 画布左右、顶部留白(各 20px) */
|
|
|
const CANVAS_PAD_X = 20;
|
|
|
|
|
|
+ const CANVAS_PAD_Y = 20;
|
|
|
+
|
|
|
+ /** 底部文案每行最多字数 */
|
|
|
+ const NAME_CHARS_PER_LINE = 13;
|
|
|
+
|
|
|
/** 商品图显示宽度 = POSTER_W - 2×CANVAS_PAD_X */
|
|
|
const SLOT_W = 710;
|
|
|
|
|
|
@@ -42,6 +47,16 @@ class PosterUtil
|
|
|
|
|
|
const SCAN_HINT = '微信扫码下单';
|
|
|
|
|
|
+ const FOOTER_STAFF_Y = 32;
|
|
|
+
|
|
|
+ const FOOTER_TEXT_GAP = 12;
|
|
|
+
|
|
|
+ const FOOTER_PRICE_SIZE = 56;
|
|
|
+
|
|
|
+ const FOOTER_NAME_SIZE = 36;
|
|
|
+
|
|
|
+ const FOOTER_STAFF_SIZE = 26;
|
|
|
+
|
|
|
/**
|
|
|
* 海报标题过长时截断,避免压住底部文案区
|
|
|
*/
|
|
|
@@ -57,6 +72,93 @@ class PosterUtil
|
|
|
return $text;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 商品名拆成最多两行:首行 13 字;次行剩余,超过 13 字则省略
|
|
|
+ *
|
|
|
+ * @return array{0:string,1:string}
|
|
|
+ */
|
|
|
+ public static function splitPosterNameLines($name, $maxPerLine = self::NAME_CHARS_PER_LINE)
|
|
|
+ {
|
|
|
+ $name = trim((string)$name);
|
|
|
+ if ($name === '') {
|
|
|
+ return ['', ''];
|
|
|
+ }
|
|
|
+ $maxPerLine = (int)$maxPerLine;
|
|
|
+ if ($maxPerLine < 1) {
|
|
|
+ $maxPerLine = self::NAME_CHARS_PER_LINE;
|
|
|
+ }
|
|
|
+ $len = mb_strlen($name, 'UTF-8');
|
|
|
+ if ($len <= $maxPerLine) {
|
|
|
+ return [$name, ''];
|
|
|
+ }
|
|
|
+ $line1 = mb_substr($name, 0, $maxPerLine, 'UTF-8');
|
|
|
+ $rest = mb_substr($name, $maxPerLine, null, 'UTF-8');
|
|
|
+ $restLen = mb_strlen($rest, 'UTF-8');
|
|
|
+ if ($restLen <= $maxPerLine) {
|
|
|
+ return [$line1, $rest];
|
|
|
+ }
|
|
|
+ $line2 = mb_substr($rest, 0, $maxPerLine, 'UTF-8') . '…';
|
|
|
+
|
|
|
+ return [$line1, $line2];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 商品名水印文案(支持 \\n 两行),支数/副标题尽量接在末行
|
|
|
+ */
|
|
|
+ public static function buildPosterNameText($name, $flowerNum = 0, $subtitle = '')
|
|
|
+ {
|
|
|
+ list($line1, $line2) = self::splitPosterNameLines($name);
|
|
|
+ $suffix = '';
|
|
|
+ if ($flowerNum > 0) {
|
|
|
+ $suffix = ' · ' . (int)$flowerNum . '支';
|
|
|
+ } elseif (trim((string)$subtitle) !== '') {
|
|
|
+ $suffix = ' · ' . trim((string)$subtitle);
|
|
|
+ }
|
|
|
+ if ($suffix !== '') {
|
|
|
+ $target = $line2 !== '' ? 'line2' : 'line1';
|
|
|
+ if ($target === 'line1') {
|
|
|
+ $merged = $line1 . $suffix;
|
|
|
+ if (mb_strlen($merged, 'UTF-8') <= self::NAME_CHARS_PER_LINE) {
|
|
|
+ $line1 = $merged;
|
|
|
+ } else {
|
|
|
+ $line2 = self::truncateText($suffix, self::NAME_CHARS_PER_LINE);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ $merged = $line2 . $suffix;
|
|
|
+ if (mb_strlen($merged, 'UTF-8') <= self::NAME_CHARS_PER_LINE) {
|
|
|
+ $line2 = $merged;
|
|
|
+ } else {
|
|
|
+ $line2 = self::truncateText($merged, self::NAME_CHARS_PER_LINE);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if ($line2 === '') {
|
|
|
+ return $line1;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $line1 . "\n" . $line2;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 底部文案 y(g_sw,自下而上:推荐语 → 价格 → 商品名)
|
|
|
+ */
|
|
|
+ private static function footerTextYPositions($nameLineCount)
|
|
|
+ {
|
|
|
+ $staffY = self::FOOTER_STAFF_Y;
|
|
|
+ $staffH = (int)ceil(self::FOOTER_STAFF_SIZE * 1.3);
|
|
|
+ $priceY = $staffY + $staffH + self::FOOTER_TEXT_GAP;
|
|
|
+ $priceH = (int)ceil(self::FOOTER_PRICE_SIZE * 1.1);
|
|
|
+ $nameLineCount = max(1, min(2, (int)$nameLineCount));
|
|
|
+ $nameLineH = (int)ceil(self::FOOTER_NAME_SIZE * 1.15);
|
|
|
+ $nameY = $priceY + $priceH + self::FOOTER_TEXT_GAP + ($nameLineCount - 1) * $nameLineH;
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'staff' => $staffY,
|
|
|
+ 'price' => $priceY,
|
|
|
+ 'name' => $nameY,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 按商品图比例计算主图区高度与画布总高(能多矮就多矮,仅保留温和下限)
|
|
|
*
|
|
|
@@ -200,19 +302,16 @@ class PosterUtil
|
|
|
$imgH = $layout['imgH'];
|
|
|
$totalH = $layout['totalH'];
|
|
|
|
|
|
- $displayName = self::truncateText($name, 14);
|
|
|
- if ($flowerNum > 0 && $displayName !== '') {
|
|
|
- $displayName .= ' · ' . $flowerNum . '支';
|
|
|
- } elseif ($subtitle !== '' && $displayName !== '') {
|
|
|
- $displayName .= ' · ' . self::truncateText($subtitle, 8);
|
|
|
- }
|
|
|
+ $displayName = self::buildPosterNameText($name, $flowerNum, $subtitle);
|
|
|
+ $nameLineCount = ($displayName !== '' && strpos($displayName, "\n") !== false) ? 2 : 1;
|
|
|
+ $footerY = self::footerTextYPositions($nameLineCount);
|
|
|
|
|
|
$coverWatermark = self::buildCoverWatermarkPath($cover, $layout['scaledH']);
|
|
|
$qrWatermark = $miniCode . '?x-oss-process=image/resize,m_fill,h_' . self::QR_SIZE . ',w_' . self::QR_SIZE;
|
|
|
|
|
|
$url = self::buildCanvasBaseUrl($prefix, $totalH);
|
|
|
$url .= '/watermark,image_' . stringUtil::ossBase64($coverWatermark)
|
|
|
- . ',g_nw,x_' . self::CANVAS_PAD_X . ',y_8';
|
|
|
+ . ',g_nw,x_' . self::CANVAS_PAD_X . ',y_' . self::CANVAS_PAD_Y;
|
|
|
$url .= '/watermark,image_' . stringUtil::ossBase64($qrWatermark) . ',g_se,x_' . self::QR_PAD_RIGHT . ',y_' . self::QR_PAD_BOTTOM;
|
|
|
|
|
|
$logoB64 = self::buildHyxhLogoWatermark($mainId);
|
|
|
@@ -220,12 +319,15 @@ class PosterUtil
|
|
|
$url .= '/watermark,image_' . $logoB64 . ',g_se,x_210,y_48';
|
|
|
}
|
|
|
|
|
|
+ $url .= '/watermark,text_' . stringUtil::ossBase64($staffText)
|
|
|
+ . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_' . $footerY['staff']
|
|
|
+ . ',color_999999,type_' . self::FONT . ',size_' . self::FOOTER_STAFF_SIZE;
|
|
|
$url .= '/watermark,text_' . stringUtil::ossBase64($priceText)
|
|
|
- . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_32,color_EE2C2C,type_' . self::FONT . ',size_56';
|
|
|
+ . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_' . $footerY['price']
|
|
|
+ . ',color_EE2C2C,type_' . self::FONT . ',size_' . self::FOOTER_PRICE_SIZE;
|
|
|
$url .= '/watermark,text_' . stringUtil::ossBase64($displayName)
|
|
|
- . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_102,color_333333,type_' . self::FONT . ',size_36';
|
|
|
- $url .= '/watermark,text_' . stringUtil::ossBase64($staffText)
|
|
|
- . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_148,color_999999,type_' . self::FONT . ',size_26';
|
|
|
+ . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_' . $footerY['name']
|
|
|
+ . ',color_333333,type_' . self::FONT . ',size_' . self::FOOTER_NAME_SIZE;
|
|
|
$url .= '/watermark,text_' . stringUtil::ossBase64($scanHint)
|
|
|
. ',g_south,x_' . self::qrCenterX() . ',y_' . self::scanHintYFromBottom()
|
|
|
. ',color_888888,type_' . self::FONT . ',size_24';
|