itemRemark ?? ''); } else { $options['itemRemark'] = self::filterPosterUtf8Text($options['itemRemark'] ?? ''); } return $options; } /** * hdApp/ghsApp me 页门店标题:sjName·shopName(首店仅 sjName) */ public static function buildShopDisplayTitle($sjName, $shopName) { $sjName = trim((string)$sjName); $shopName = trim((string)$shopName); if ($shopName === '' || $shopName === '首店') { return $sjName; } return $sjName . '·' . $shopName; } public static function resolveShopPosterMeta($shop, $sj = null) { $sjName = ''; if ($shop) { $sjName = trim((string)($shop->merchantName ?? '')); } if ($sj !== null) { if (is_array($sj)) { $sjName = trim((string)($sj['name'] ?? $sjName)); } elseif (is_object($sj) && isset($sj->name)) { $sjName = trim((string)$sj->name); } } $shopName = $shop ? trim((string)($shop->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') . '…'; } return [ 'shopTitle' => $title, 'shopLogo' => self::resolveShopLogoObjectPath($shop), ]; } public static function resolveShopLogoObjectPath($shop) { $avatar = ''; if ($shop && !empty($shop->avatar)) { $avatar = $shop->avatar; } return imgUtil::normalizeOssObjectPath($avatar); } /** * 海报文案 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') . '…'; } return $text; } /** * 按行宽拆成多行(供多条 OSS 文字水印使用,不使用 \n) * * @return string[] */ public static function splitPosterTextToLines($text, $maxPerLine = self::TEXT_CHARS_PER_LINE) { $text = self::filterPosterUtf8Text($text); if ($text === '') { 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'); $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; } 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] ?? '') . '…'; } return [$line1, $line2]; } /** @deprecated 兼容旧调用 */ public static function splitPosterNameLines($name, $maxPerLine = self::TEXT_CHARS_PER_LINE) { return self::splitPosterTextLines($name, $maxPerLine); } /** @deprecated 兼容旧调用,返回首行文案 */ public static function buildPosterTwoLineText($text, $maxPerLine = self::TEXT_CHARS_PER_LINE) { $lines = self::splitPosterTextToLines($text, $maxPerLine); return $lines[0] ?? ''; } /** * 商品名(图下,多行,每行独立 OSS 文字水印) * * @return string[] */ public static function buildPosterNameLines($name, $flowerNum = 0, $subtitle = '') { $name = self::filterPosterUtf8Text($name); $subtitle = self::filterPosterUtf8Text($subtitle); $lines = self::splitPosterTextToLines($name); $suffix = ''; if ($flowerNum > 0) { $suffix = ' · ' . (int)$flowerNum . '支'; } elseif (trim((string)$subtitle) !== '') { $suffix = ' · ' . trim((string)$subtitle); } if ($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) { $lines[$last] = $merged; } else { $lines = array_merge($lines, self::splitPosterTextToLines($suffix)); } } } 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 buildPosterRemarkLines($itemRemark) { $itemRemark = self::filterPosterUtf8Text($itemRemark); if ($itemRemark === '') { return []; } return self::splitPosterTextToLines($itemRemark); } /** @deprecated 请用 buildPosterRemarkLines */ public static function buildPosterRemarkText($itemRemark) { $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 $url; } private static function textBlockHeight($lineCount, $fontSize) { if ($lineCount <= 0) { return 0; } $lineH = (int)ceil($fontSize * 1.2); return $lineCount * $lineH + max(0, $lineCount - 1) * self::MID_LINE_GAP; } private static function calcMidSectionHeight(array $nameLines, array $remarkLines) { $h = 0; $nameCount = count($nameLines); if ($nameCount > 0) { $h += self::MID_AFTER_COVER_GAP + self::textBlockHeight($nameCount, self::MID_NAME_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; } private static function coverStartY() { return self::CANVAS_PAD_Y + self::SHOP_LOGO_SIZE + self::HEADER_BOTTOM_GAP; } /** * 底部左侧(价格 + 提示文案)与右侧太阳码垂直居中对齐,均左对齐 * * @return array{buyHintY:int,priceY:int,qrY:int} */ private static function calcFooterLayout() { $qrCenterFromBottom = self::QR_PAD_BOTTOM + (int)(self::QR_SIZE / 2); $buyHintH = (int)ceil(self::FOOTER_BUY_HINT_SIZE * 1.2); $priceH = (int)ceil(self::FOOTER_PRICE_SIZE * 1.3); $gap = self::FOOTER_TEXT_GAP; $blockH = $priceH + $gap + $buyHintH; $buyHintY = max(16, $qrCenterFromBottom - (int)($blockH / 2)); $priceY = $buyHintY + $buyHintH + $gap; $qrY = max(16, $qrCenterFromBottom - (int)(self::QR_SIZE / 2)); return [ 'buyHintY' => $buyHintY, 'priceY' => $priceY, 'qrY' => $qrY, ]; } /** * @return array{imgH:int,totalH:int,scaledH:int,posterW:int,coverY:int,nameY:int,remarkY:int} */ public static function calcPosterLayout($coverWidth, $coverHeight, array $textOptions = []) { $coverWidth = (int)$coverWidth; $coverHeight = (int)$coverHeight; $scaledH = 0; if ($coverWidth > 0 && $coverHeight > 0) { $scaledH = (int)round($coverHeight * (self::SLOT_W / $coverWidth)); $imgH = max(self::IMG_H_MIN, min(self::IMG_H_MAX, $scaledH)); } else { $scaledH = self::IMG_H_MIN; $imgH = self::IMG_H_MIN; } $nameLines = self::buildPosterNameLines( $textOptions['name'] ?? '', (int)($textOptions['flowerNum'] ?? 0), $textOptions['subtitle'] ?? '' ); $remarkLines = self::buildPosterRemarkLines($textOptions['itemRemark'] ?? ''); $coverY = self::coverStartY(); $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 + (count($nameLines) > 0 ? self::MID_AFTER_COVER_GAP : 0); $remarkY = 0; if (count($remarkLines) > 0) { $remarkY = $nameY; 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; } } return [ 'imgH' => $imgH, 'totalH' => $totalH, 'scaledH' => $scaledH, 'posterW' => self::POSTER_W, 'coverY' => $coverY, 'nameY' => $nameY, 'remarkY' => $remarkY, ]; } private static function buildCoverWatermarkPath($cover, $scaledH) { $scaledH = (int)$scaledH; $r = self::COVER_CORNER_RADIUS; if ($scaledH > self::IMG_H_MAX) { return $cover . '?x-oss-process=image/resize,m_fill,w_' . self::SLOT_W . ',h_' . self::IMG_H_MAX . ',limit_0/rounded-corners,r_' . $r; } return $cover . '?x-oss-process=image/resize,m_lfit,w_' . self::SLOT_W . ',limit_0/rounded-corners,r_' . $r; } private static function buildShopLogoWatermarkPath($logoObject) { $logoObject = imgUtil::normalizeOssObjectPath($logoObject); return $logoObject . '?x-oss-process=image/resize,m_fill,h_' . self::SHOP_LOGO_SIZE . ',w_' . self::SHOP_LOGO_SIZE . ',limit_0'; } private static function shopTitleY() { return self::CANVAS_PAD_Y + (int)((self::SHOP_LOGO_SIZE - self::SHOP_TITLE_SIZE) / 2); } public static function buildCanvasBaseUrl($prefix, $totalH) { $absoluteMin = self::coverStartY() + self::IMG_H_MIN + self::FOOTER_H; $totalH = max($absoluteMin, min(self::TOTAL_H_MAX, (int)$totalH)); $process = '?x-oss-process=image/resize,m_pad,w_' . self::POSTER_W . ',h_' . $totalH . ',color_' . self::CANVAS_COLOR; return $prefix . self::BLANK_CANVAS . $process; } public static function buildHyxhLogoWatermark($mainId) { if (!in_array((int)$mainId, [52, 1459, 13495], true)) { return ''; } $logo = 'poster/hyxh_ncd_logo.png?x-oss-process=image/resize,m_fill,h_72,w_72'; if ($mainId == 1459) { $logo = 'poster/hyxh_gcd_logo.png?x-oss-process=image/resize,m_fill,h_72,w_72'; } if ($mainId == 13495) { $logo = 'poster/hyxh_hzd_logo.png?x-oss-process=image/resize,m_fill,h_72,w_72'; } return stringUtil::ossBase64($logo); } public static function buildMallGoodsPosterUrl($prefix, array $options) { return self::buildMallSharePosterUrl($prefix, $options); } public static function buildMallItemPosterUrl($prefix, array $options) { return self::buildMallSharePosterUrl($prefix, $options); } public static function buildGhsHdItemPosterUrl($prefix, array $options) { return self::buildMallSharePosterUrl($prefix, $options); } private static function buildMallSharePosterUrl($prefix, array $options) { $cover = $options['cover'] ?? ''; $miniCode = $options['miniCode'] ?? ''; $priceText = $options['priceText'] ?? ''; $name = self::filterPosterUtf8Text($options['name'] ?? ''); $mainId = (int)($options['mainId'] ?? 0); $flowerNum = (int)($options['flowerNum'] ?? 0); $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); $coverH = (int)($options['coverHeight'] ?? 0); if ($coverW <= 0 || $coverH <= 0) { list($coverW, $coverH) = imgUtil::getOssImageSize($cover, $prefix); } $layout = self::calcPosterLayout($coverW, $coverH, [ 'name' => $name, 'flowerNum' => $flowerNum, 'subtitle' => $subtitle, 'itemRemark' => $itemRemark, ]); $nameLines = self::buildPosterNameLines($name, $flowerNum, $subtitle); $remarkLines = self::buildPosterRemarkLines($itemRemark); $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; $url = self::buildCanvasBaseUrl($prefix, $layout['totalH']); if ($shopLogo !== '') { $url .= '/watermark,image_' . stringUtil::ossBase64(self::buildShopLogoWatermarkPath($shopLogo)) . ',g_nw,x_' . self::CANVAS_PAD_X . ',y_' . self::CANVAS_PAD_Y; } if ($shopTitle !== '') { $titleX = self::CANVAS_PAD_X + self::SHOP_LOGO_SIZE + self::SHOP_LOGO_GAP; $url .= '/watermark,text_' . stringUtil::ossBase64($shopTitle) . ',g_nw,x_' . $titleX . ',y_' . self::shopTitleY() . ',color_333333,type_' . self::FONT . ',size_' . self::SHOP_TITLE_SIZE; } $url .= '/watermark,image_' . stringUtil::ossBase64($coverWatermark) . ',g_nw,x_' . self::CANVAS_PAD_X . ',y_' . $layout['coverY']; if (count($nameLines) > 0) { $url = self::appendTextLineWatermarks($url, $nameLines, $layout['nameY'], self::MID_NAME_SIZE, '333333'); } if (count($remarkLines) > 0) { $url = self::appendTextLineWatermarks($url, $remarkLines, $layout['remarkY'], self::MID_REMARK_SIZE, '888888'); } $url .= '/watermark,image_' . stringUtil::ossBase64($qrWatermark) . ',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,text_' . stringUtil::ossBase64(self::FOOTER_BUY_HINT) . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_' . $footer['buyHintY'] . ',color_888888,type_' . self::FONT . ',size_' . self::FOOTER_BUY_HINT_SIZE; $url .= '/watermark,text_' . stringUtil::ossBase64($priceText) . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_' . $footer['priceY'] . ',color_EE2C2C,type_' . self::FONT . ',size_' . self::FOOTER_PRICE_SIZE; $url .= '/rounded-corners,r_' . self::POSTER_CORNER_RADIUS; return $url; } }