PosterUtil.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. <?php
  2. namespace common\components;
  3. /**
  4. * 散客分享海报(OSS:poster/blank.png 动态画布 + 水印合成)
  5. */
  6. class PosterUtil
  7. {
  8. /** 与 poster/1234.jpg 同目录的 10×10 白底,经 m_pad 拉成本次海报画布 */
  9. const BLANK_CANVAS = 'poster/blank.png';
  10. const FONT = 'd3F5LW1pY3JvaGVp';
  11. const POSTER_W = 750;
  12. /** 画布左右留白(各 20px) */
  13. const CANVAS_PAD_X = 20;
  14. /** 商品图显示宽度 = POSTER_W - 2×CANVAS_PAD_X */
  15. const SLOT_W = 710;
  16. /** 底部价格/标题/太阳码固定区 */
  17. const FOOTER_H = 360;
  18. /** 主图区温和下限,避免扁图海报过矮 */
  19. const IMG_H_MIN = 240;
  20. const IMG_H_MAX = 880;
  21. const TOTAL_H_MAX = 1600;
  22. const CANVAS_COLOR = 'FFFFFF';
  23. const QR_SIZE = 220;
  24. const QR_PAD_RIGHT = 36;
  25. const QR_PAD_BOTTOM = 32;
  26. const QR_HINT_GAP = 14;
  27. const SCAN_HINT = '微信扫码下单';
  28. /**
  29. * 海报标题过长时截断,避免压住底部文案区
  30. */
  31. public static function truncateText($text, $maxLen = 14)
  32. {
  33. $text = trim((string)$text);
  34. if ($text === '') {
  35. return '';
  36. }
  37. if (mb_strlen($text, 'UTF-8') > $maxLen) {
  38. return mb_substr($text, 0, $maxLen, 'UTF-8') . '…';
  39. }
  40. return $text;
  41. }
  42. /**
  43. * 按商品图比例计算主图区高度与画布总高(能多矮就多矮,仅保留温和下限)
  44. *
  45. * @return array{imgH:int,totalH:int,scaledH:int,posterW:int}
  46. */
  47. public static function calcPosterLayout($coverWidth, $coverHeight)
  48. {
  49. $coverWidth = (int)$coverWidth;
  50. $coverHeight = (int)$coverHeight;
  51. $scaledH = 0;
  52. if ($coverWidth > 0 && $coverHeight > 0) {
  53. $scaledH = (int)round($coverHeight * (self::SLOT_W / $coverWidth));
  54. $imgH = max(self::IMG_H_MIN, min(self::IMG_H_MAX, $scaledH));
  55. } else {
  56. $scaledH = self::IMG_H_MIN;
  57. $imgH = self::IMG_H_MIN;
  58. }
  59. $totalH = $imgH + self::FOOTER_H;
  60. $absoluteMin = self::FOOTER_H + 120;
  61. $totalH = max($absoluteMin, min(self::TOTAL_H_MAX, $totalH));
  62. return [
  63. 'imgH' => $imgH,
  64. 'totalH' => $totalH,
  65. 'scaledH' => $scaledH,
  66. 'posterW' => self::POSTER_W,
  67. ];
  68. }
  69. /**
  70. * 商品图:常规按宽 SLOT_W 等比(无左右垫白);超高图用 m_fill 裁高至 IMG_H_MAX
  71. */
  72. private static function buildCoverWatermarkPath($cover, $scaledH)
  73. {
  74. $scaledH = (int)$scaledH;
  75. if ($scaledH > self::IMG_H_MAX) {
  76. return $cover . '?x-oss-process=image/resize,m_fill,w_' . self::SLOT_W . ',h_' . self::IMG_H_MAX . ',limit_0';
  77. }
  78. return $cover . '?x-oss-process=image/resize,m_lfit,w_' . self::SLOT_W . ',limit_0';
  79. }
  80. /**
  81. * 太阳码水平中心 X(用于扫码文案 g_south 居中)
  82. */
  83. private static function qrCenterX()
  84. {
  85. return self::POSTER_W - self::QR_PAD_RIGHT - (int)(self::QR_SIZE / 2);
  86. }
  87. /**
  88. * 扫码提示文案:位于太阳码正上方居中(g_south = 文字下边中点对齐)
  89. */
  90. private static function scanHintYFromBottom()
  91. {
  92. return self::QR_PAD_BOTTOM + self::QR_SIZE + self::QR_HINT_GAP;
  93. }
  94. /**
  95. * 动态白底画布(blank.png → 750 × totalH)
  96. */
  97. public static function buildCanvasBaseUrl($prefix, $totalH)
  98. {
  99. $absoluteMin = self::FOOTER_H + 120;
  100. $totalH = max($absoluteMin, min(self::TOTAL_H_MAX, (int)$totalH));
  101. $process = '?x-oss-process=image/resize,m_pad,w_' . self::POSTER_W . ',h_' . $totalH
  102. . ',color_' . self::CANVAS_COLOR;
  103. return $prefix . self::BLANK_CANVAS . $process;
  104. }
  105. /**
  106. * 和炫鲜花等门店角标
  107. */
  108. public static function buildHyxhLogoWatermark($mainId)
  109. {
  110. if (!in_array((int)$mainId, [52, 1459, 13495], true)) {
  111. return '';
  112. }
  113. $logo = 'poster/hyxh_ncd_logo.png?x-oss-process=image/resize,m_fill,h_72,w_72';
  114. if ($mainId == 1459) {
  115. $logo = 'poster/hyxh_gcd_logo.png?x-oss-process=image/resize,m_fill,h_72,w_72';
  116. }
  117. if ($mainId == 13495) {
  118. $logo = 'poster/hyxh_hzd_logo.png?x-oss-process=image/resize,m_fill,h_72,w_72';
  119. }
  120. return stringUtil::ossBase64($logo);
  121. }
  122. /**
  123. * 花束海报
  124. */
  125. public static function buildMallGoodsPosterUrl($prefix, array $options)
  126. {
  127. return self::buildMallSharePosterUrl($prefix, $options);
  128. }
  129. /**
  130. * 花材海报(商城散客)
  131. */
  132. public static function buildMallItemPosterUrl($prefix, array $options)
  133. {
  134. return self::buildMallSharePosterUrl($prefix, $options);
  135. }
  136. /**
  137. * 花农端花店采购海报(ghs 花材详情太阳码)
  138. */
  139. public static function buildGhsHdItemPosterUrl($prefix, array $options)
  140. {
  141. return self::buildMallSharePosterUrl($prefix, $options);
  142. }
  143. /**
  144. * 散客/花束分享海报:动态画布 + 主图槽位 + 底部信息区
  145. */
  146. private static function buildMallSharePosterUrl($prefix, array $options)
  147. {
  148. $cover = $options['cover'] ?? '';
  149. $miniCode = $options['miniCode'] ?? '';
  150. $priceText = $options['priceText'] ?? '';
  151. $name = $options['name'] ?? '';
  152. $staffText = $options['staffText'] ?? '';
  153. $mainId = (int)($options['mainId'] ?? 0);
  154. $flowerNum = (int)($options['flowerNum'] ?? 0);
  155. $subtitle = trim((string)($options['subtitle'] ?? ''));
  156. $scanHint = trim((string)($options['scanHint'] ?? self::SCAN_HINT));
  157. if ($scanHint === '微信扫码采购') {
  158. $scanHint = self::SCAN_HINT;
  159. }
  160. $coverW = (int)($options['coverWidth'] ?? 0);
  161. $coverH = (int)($options['coverHeight'] ?? 0);
  162. if ($coverW <= 0 || $coverH <= 0) {
  163. list($coverW, $coverH) = imgUtil::getOssImageSize($cover, $prefix);
  164. }
  165. $layout = self::calcPosterLayout($coverW, $coverH);
  166. $imgH = $layout['imgH'];
  167. $totalH = $layout['totalH'];
  168. $displayName = self::truncateText($name, 14);
  169. if ($flowerNum > 0 && $displayName !== '') {
  170. $displayName .= ' · ' . $flowerNum . '支';
  171. } elseif ($subtitle !== '' && $displayName !== '') {
  172. $displayName .= ' · ' . self::truncateText($subtitle, 8);
  173. }
  174. $coverWatermark = self::buildCoverWatermarkPath($cover, $layout['scaledH']);
  175. $qrWatermark = $miniCode . '?x-oss-process=image/resize,m_fill,h_' . self::QR_SIZE . ',w_' . self::QR_SIZE;
  176. $url = self::buildCanvasBaseUrl($prefix, $totalH);
  177. $url .= '/watermark,image_' . stringUtil::ossBase64($coverWatermark)
  178. . ',g_nw,x_' . self::CANVAS_PAD_X . ',y_8';
  179. $url .= '/watermark,image_' . stringUtil::ossBase64($qrWatermark) . ',g_se,x_' . self::QR_PAD_RIGHT . ',y_' . self::QR_PAD_BOTTOM;
  180. $logoB64 = self::buildHyxhLogoWatermark($mainId);
  181. if ($logoB64 !== '') {
  182. $url .= '/watermark,image_' . $logoB64 . ',g_se,x_210,y_48';
  183. }
  184. $url .= '/watermark,text_' . stringUtil::ossBase64($priceText)
  185. . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_32,color_EE2C2C,type_' . self::FONT . ',size_56';
  186. $url .= '/watermark,text_' . stringUtil::ossBase64($displayName)
  187. . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_102,color_333333,type_' . self::FONT . ',size_36';
  188. $url .= '/watermark,text_' . stringUtil::ossBase64($staffText)
  189. . ',g_sw,x_' . self::CANVAS_PAD_X . ',y_148,color_999999,type_' . self::FONT . ',size_26';
  190. $url .= '/watermark,text_' . stringUtil::ossBase64($scanHint)
  191. . ',g_south,x_' . self::qrCenterX() . ',y_' . self::scanHintYFromBottom()
  192. . ',color_888888,type_' . self::FONT . ',size_24';
  193. return $url;
  194. }
  195. }