|
|
@@ -18,7 +18,10 @@ class PosterUtil
|
|
|
const CANVAS_PAD_Y = 20;
|
|
|
|
|
|
/** 文案每行最多字数(商品名、注意事项) */
|
|
|
- const TEXT_CHARS_PER_LINE = 13;
|
|
|
+ const TEXT_CHARS_PER_LINE = 16;
|
|
|
+
|
|
|
+ /** OSS 单段文字水印长度上限(约 64,汉字按 3 计,16 字/行安全) */
|
|
|
+ const OSS_WATERMARK_TEXT_MAX = 21;
|
|
|
|
|
|
const SLOT_W = 710;
|
|
|
|
|
|
@@ -190,28 +193,50 @@ class PosterUtil
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * @return array{0:string,1:string}
|
|
|
+ * 按行宽拆成多行(供多条 OSS 文字水印使用,不使用 \n)
|
|
|
+ *
|
|
|
+ * @return string[]
|
|
|
*/
|
|
|
- public static function splitPosterTextLines($text, $maxPerLine = self::TEXT_CHARS_PER_LINE)
|
|
|
+ public static function splitPosterTextToLines($text, $maxPerLine = self::TEXT_CHARS_PER_LINE)
|
|
|
{
|
|
|
$text = self::filterPosterUtf8Text($text);
|
|
|
if ($text === '') {
|
|
|
- return ['', ''];
|
|
|
+ return [];
|
|
|
}
|
|
|
$maxPerLine = (int)$maxPerLine;
|
|
|
if ($maxPerLine < 1) {
|
|
|
$maxPerLine = self::TEXT_CHARS_PER_LINE;
|
|
|
}
|
|
|
+ $maxPerLine = min($maxPerLine, self::OSS_WATERMARK_TEXT_MAX);
|
|
|
+
|
|
|
+ $lines = [];
|
|
|
$len = mb_strlen($text, 'UTF-8');
|
|
|
- if ($len <= $maxPerLine) {
|
|
|
- return [$text, ''];
|
|
|
+ $offset = 0;
|
|
|
+ while ($offset < $len) {
|
|
|
+ $remain = $len - $offset;
|
|
|
+ if ($remain <= $maxPerLine) {
|
|
|
+ $lines[] = mb_substr($text, $offset, null, 'UTF-8');
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ $lines[] = mb_substr($text, $offset, $maxPerLine, 'UTF-8');
|
|
|
+ $offset += $maxPerLine;
|
|
|
}
|
|
|
- $line1 = mb_substr($text, 0, $maxPerLine, 'UTF-8');
|
|
|
- $rest = mb_substr($text, $maxPerLine, null, 'UTF-8');
|
|
|
- if (mb_strlen($rest, 'UTF-8') <= $maxPerLine) {
|
|
|
- return [$line1, $rest];
|
|
|
+
|
|
|
+ return $lines;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @return array{0:string,1:string}
|
|
|
+ * @deprecated 仅兼容旧调用,新逻辑请用 splitPosterTextToLines
|
|
|
+ */
|
|
|
+ public static function splitPosterTextLines($text, $maxPerLine = self::TEXT_CHARS_PER_LINE)
|
|
|
+ {
|
|
|
+ $lines = self::splitPosterTextToLines($text, $maxPerLine);
|
|
|
+ $line1 = $lines[0] ?? '';
|
|
|
+ $line2 = $lines[1] ?? '';
|
|
|
+ if (count($lines) > 2) {
|
|
|
+ $line2 = ($lines[1] ?? '') . '…';
|
|
|
}
|
|
|
- $line2 = mb_substr($rest, 0, $maxPerLine, 'UTF-8') . '…';
|
|
|
|
|
|
return [$line1, $line2];
|
|
|
}
|
|
|
@@ -222,24 +247,24 @@ class PosterUtil
|
|
|
return self::splitPosterTextLines($name, $maxPerLine);
|
|
|
}
|
|
|
|
|
|
+ /** @deprecated 兼容旧调用,返回首行文案 */
|
|
|
public static function buildPosterTwoLineText($text, $maxPerLine = self::TEXT_CHARS_PER_LINE)
|
|
|
{
|
|
|
- list($line1, $line2) = self::splitPosterTextLines($text, $maxPerLine);
|
|
|
- if ($line2 === '') {
|
|
|
- return $line1;
|
|
|
- }
|
|
|
+ $lines = self::splitPosterTextToLines($text, $maxPerLine);
|
|
|
|
|
|
- return $line1 . "\n" . $line2;
|
|
|
+ return $lines[0] ?? '';
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 商品名(图下,最多两行)
|
|
|
+ * 商品名(图下,多行,每行独立 OSS 文字水印)
|
|
|
+ *
|
|
|
+ * @return string[]
|
|
|
*/
|
|
|
- public static function buildPosterNameText($name, $flowerNum = 0, $subtitle = '')
|
|
|
+ public static function buildPosterNameLines($name, $flowerNum = 0, $subtitle = '')
|
|
|
{
|
|
|
$name = self::filterPosterUtf8Text($name);
|
|
|
$subtitle = self::filterPosterUtf8Text($subtitle);
|
|
|
- list($line1, $line2) = self::splitPosterTextLines($name);
|
|
|
+ $lines = self::splitPosterTextToLines($name);
|
|
|
$suffix = '';
|
|
|
if ($flowerNum > 0) {
|
|
|
$suffix = ' · ' . (int)$flowerNum . '支';
|
|
|
@@ -247,48 +272,76 @@ class PosterUtil
|
|
|
$suffix = ' · ' . trim((string)$subtitle);
|
|
|
}
|
|
|
if ($suffix !== '') {
|
|
|
- if ($line2 === '') {
|
|
|
- $merged = $line1 . $suffix;
|
|
|
+ if ($lines === []) {
|
|
|
+ $lines = self::splitPosterTextToLines($suffix);
|
|
|
+ } else {
|
|
|
+ $last = count($lines) - 1;
|
|
|
+ $merged = $lines[$last] . $suffix;
|
|
|
if (mb_strlen($merged, 'UTF-8') <= self::TEXT_CHARS_PER_LINE) {
|
|
|
- $line1 = $merged;
|
|
|
+ $lines[$last] = $merged;
|
|
|
} else {
|
|
|
- $line2 = self::truncateText($suffix, self::TEXT_CHARS_PER_LINE);
|
|
|
+ $lines = array_merge($lines, self::splitPosterTextToLines($suffix));
|
|
|
}
|
|
|
- } else {
|
|
|
- $merged = $line2 . $suffix;
|
|
|
- $line2 = mb_strlen($merged, 'UTF-8') <= self::TEXT_CHARS_PER_LINE
|
|
|
- ? $merged
|
|
|
- : self::truncateText($merged, self::TEXT_CHARS_PER_LINE);
|
|
|
}
|
|
|
}
|
|
|
- if ($line2 === '') {
|
|
|
- return $line1;
|
|
|
- }
|
|
|
|
|
|
- return $line1 . "\n" . $line2;
|
|
|
+ return $lines;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** @deprecated 请用 buildPosterNameLines */
|
|
|
+ public static function buildPosterNameText($name, $flowerNum = 0, $subtitle = '')
|
|
|
+ {
|
|
|
+ $lines = self::buildPosterNameLines($name, $flowerNum, $subtitle);
|
|
|
+
|
|
|
+ return $lines[0] ?? '';
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 注意事项(图下、名称下,无内容返回空串)
|
|
|
+ * 注意事项(图下、名称下,多行)
|
|
|
+ *
|
|
|
+ * @return string[]
|
|
|
*/
|
|
|
- public static function buildPosterRemarkText($itemRemark)
|
|
|
+ public static function buildPosterRemarkLines($itemRemark)
|
|
|
{
|
|
|
$itemRemark = self::filterPosterUtf8Text($itemRemark);
|
|
|
if ($itemRemark === '') {
|
|
|
- return '';
|
|
|
+ return [];
|
|
|
}
|
|
|
|
|
|
- return self::buildPosterTwoLineText(self::REMARK_PREFIX . $itemRemark);
|
|
|
+ return self::splitPosterTextToLines(self::REMARK_PREFIX . $itemRemark);
|
|
|
}
|
|
|
|
|
|
- private static function countTextLines($text)
|
|
|
+ /** @deprecated 请用 buildPosterRemarkLines */
|
|
|
+ public static function buildPosterRemarkText($itemRemark)
|
|
|
{
|
|
|
- $text = trim((string)$text);
|
|
|
- if ($text === '') {
|
|
|
- return 0;
|
|
|
+ $lines = self::buildPosterRemarkLines($itemRemark);
|
|
|
+
|
|
|
+ return $lines[0] ?? '';
|
|
|
+ }
|
|
|
+
|
|
|
+ private static function textLineStep($fontSize)
|
|
|
+ {
|
|
|
+ return (int)ceil($fontSize * 1.2) + self::MID_LINE_GAP;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 多条文字水印纵向排列(OSS 不支持 \n 换行)
|
|
|
+ */
|
|
|
+ private static function appendTextLineWatermarks($url, array $lines, $startY, $fontSize, $colorHex)
|
|
|
+ {
|
|
|
+ $step = self::textLineStep($fontSize);
|
|
|
+ foreach ($lines as $i => $line) {
|
|
|
+ $line = trim((string)$line);
|
|
|
+ if ($line === '') {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $y = (int)$startY + $i * $step;
|
|
|
+ $url .= '/watermark,text_' . stringUtil::ossBase64($line)
|
|
|
+ . ',g_nw,x_' . self::CANVAS_PAD_X . ',y_' . $y
|
|
|
+ . ',color_' . $colorHex . ',type_' . self::FONT . ',size_' . (int)$fontSize;
|
|
|
}
|
|
|
|
|
|
- return substr_count($text, "\n") + 1;
|
|
|
+ return $url;
|
|
|
}
|
|
|
|
|
|
private static function textBlockHeight($lineCount, $fontSize)
|
|
|
@@ -301,17 +354,17 @@ class PosterUtil
|
|
|
return $lineCount * $lineH + max(0, $lineCount - 1) * self::MID_LINE_GAP;
|
|
|
}
|
|
|
|
|
|
- private static function calcMidSectionHeight($nameText, $remarkText)
|
|
|
+ private static function calcMidSectionHeight(array $nameLines, array $remarkLines)
|
|
|
{
|
|
|
$h = 0;
|
|
|
- $nameLines = self::countTextLines($nameText);
|
|
|
- if ($nameLines > 0) {
|
|
|
- $h += self::MID_AFTER_COVER_GAP + self::textBlockHeight($nameLines, self::MID_NAME_SIZE);
|
|
|
+ $nameCount = count($nameLines);
|
|
|
+ if ($nameCount > 0) {
|
|
|
+ $h += self::MID_AFTER_COVER_GAP + self::textBlockHeight($nameCount, self::MID_NAME_SIZE);
|
|
|
}
|
|
|
- $remarkLines = self::countTextLines($remarkText);
|
|
|
- if ($remarkLines > 0) {
|
|
|
- $h += ($nameLines > 0 ? self::MID_BLOCK_GAP : self::MID_AFTER_COVER_GAP);
|
|
|
- $h += self::textBlockHeight($remarkLines, self::MID_REMARK_SIZE);
|
|
|
+ $remarkCount = count($remarkLines);
|
|
|
+ if ($remarkCount > 0) {
|
|
|
+ $h += ($nameCount > 0 ? self::MID_BLOCK_GAP : self::MID_AFTER_COVER_GAP);
|
|
|
+ $h += self::textBlockHeight($remarkCount, self::MID_REMARK_SIZE);
|
|
|
}
|
|
|
|
|
|
return $h;
|
|
|
@@ -362,25 +415,25 @@ class PosterUtil
|
|
|
$imgH = self::IMG_H_MIN;
|
|
|
}
|
|
|
|
|
|
- $nameText = self::buildPosterNameText(
|
|
|
+ $nameLines = self::buildPosterNameLines(
|
|
|
$textOptions['name'] ?? '',
|
|
|
(int)($textOptions['flowerNum'] ?? 0),
|
|
|
$textOptions['subtitle'] ?? ''
|
|
|
);
|
|
|
- $remarkText = self::buildPosterRemarkText($textOptions['itemRemark'] ?? '');
|
|
|
+ $remarkLines = self::buildPosterRemarkLines($textOptions['itemRemark'] ?? '');
|
|
|
|
|
|
$coverY = self::coverStartY();
|
|
|
- $midH = self::calcMidSectionHeight($nameText, $remarkText);
|
|
|
+ $midH = self::calcMidSectionHeight($nameLines, $remarkLines);
|
|
|
$totalH = $coverY + $imgH + $midH + self::FOOTER_H;
|
|
|
$absoluteMin = self::coverStartY() + self::IMG_H_MIN + self::FOOTER_H;
|
|
|
$totalH = max($absoluteMin, min(self::TOTAL_H_MAX, $totalH));
|
|
|
|
|
|
- $nameY = $coverY + $imgH + ($nameText !== '' ? self::MID_AFTER_COVER_GAP : 0);
|
|
|
+ $nameY = $coverY + $imgH + (count($nameLines) > 0 ? self::MID_AFTER_COVER_GAP : 0);
|
|
|
$remarkY = 0;
|
|
|
- if ($remarkText !== '') {
|
|
|
+ if (count($remarkLines) > 0) {
|
|
|
$remarkY = $nameY;
|
|
|
- if ($nameText !== '') {
|
|
|
- $remarkY += self::textBlockHeight(self::countTextLines($nameText), self::MID_NAME_SIZE);
|
|
|
+ if (count($nameLines) > 0) {
|
|
|
+ $remarkY += self::textBlockHeight(count($nameLines), self::MID_NAME_SIZE);
|
|
|
$remarkY += self::MID_BLOCK_GAP;
|
|
|
} else {
|
|
|
$remarkY = $coverY + $imgH + self::MID_AFTER_COVER_GAP;
|
|
|
@@ -490,8 +543,8 @@ class PosterUtil
|
|
|
'subtitle' => $subtitle,
|
|
|
'itemRemark' => $itemRemark,
|
|
|
]);
|
|
|
- $displayName = self::buildPosterNameText($name, $flowerNum, $subtitle);
|
|
|
- $displayRemark = self::buildPosterRemarkText($itemRemark);
|
|
|
+ $nameLines = self::buildPosterNameLines($name, $flowerNum, $subtitle);
|
|
|
+ $remarkLines = self::buildPosterRemarkLines($itemRemark);
|
|
|
$footer = self::calcFooterLayout();
|
|
|
|
|
|
$coverWatermark = self::buildCoverWatermarkPath($cover, $layout['scaledH']);
|
|
|
@@ -513,15 +566,11 @@ class PosterUtil
|
|
|
$url .= '/watermark,image_' . stringUtil::ossBase64($coverWatermark)
|
|
|
. ',g_nw,x_' . self::CANVAS_PAD_X . ',y_' . $layout['coverY'];
|
|
|
|
|
|
- if ($displayName !== '') {
|
|
|
- $url .= '/watermark,text_' . stringUtil::ossBase64($displayName)
|
|
|
- . ',g_nw,x_' . self::CANVAS_PAD_X . ',y_' . $layout['nameY']
|
|
|
- . ',color_333333,type_' . self::FONT . ',size_' . self::MID_NAME_SIZE;
|
|
|
+ if (count($nameLines) > 0) {
|
|
|
+ $url = self::appendTextLineWatermarks($url, $nameLines, $layout['nameY'], self::MID_NAME_SIZE, '333333');
|
|
|
}
|
|
|
- if ($displayRemark !== '') {
|
|
|
- $url .= '/watermark,text_' . stringUtil::ossBase64($displayRemark)
|
|
|
- . ',g_nw,x_' . self::CANVAS_PAD_X . ',y_' . $layout['remarkY']
|
|
|
- . ',color_888888,type_' . self::FONT . ',size_' . self::MID_REMARK_SIZE;
|
|
|
+ if (count($remarkLines) > 0) {
|
|
|
+ $url = self::appendTextLineWatermarks($url, $remarkLines, $layout['remarkY'], self::MID_REMARK_SIZE, '888888');
|
|
|
}
|
|
|
|
|
|
$url .= '/watermark,image_' . stringUtil::ossBase64($qrWatermark)
|