218 lines
11 KiB
Java
218 lines
11 KiB
Java
|
|
package com.mosquito.project.service;
|
||
|
|
|
||
|
|
import com.mosquito.project.config.PosterConfig;
|
||
|
|
import com.mosquito.project.domain.Activity;
|
||
|
|
import org.slf4j.Logger;
|
||
|
|
import org.slf4j.LoggerFactory;
|
||
|
|
import org.springframework.stereotype.Service;
|
||
|
|
|
||
|
|
import java.awt.*;
|
||
|
|
import java.awt.image.BufferedImage;
|
||
|
|
import java.io.ByteArrayOutputStream;
|
||
|
|
import java.net.URL;
|
||
|
|
import java.util.Map;
|
||
|
|
import java.util.concurrent.ConcurrentHashMap;
|
||
|
|
import javax.imageio.ImageIO;
|
||
|
|
|
||
|
|
@Service
|
||
|
|
public class PosterRenderService {
|
||
|
|
|
||
|
|
private static final Logger log = LoggerFactory.getLogger(PosterRenderService.class);
|
||
|
|
|
||
|
|
private final PosterConfig posterConfig;
|
||
|
|
private final ShortLinkService shortLinkService;
|
||
|
|
private final Map<String, Image> imageCache = new ConcurrentHashMap<>();
|
||
|
|
|
||
|
|
public PosterRenderService(PosterConfig posterConfig, ShortLinkService shortLinkService) {
|
||
|
|
this.posterConfig = posterConfig;
|
||
|
|
this.shortLinkService = shortLinkService;
|
||
|
|
}
|
||
|
|
|
||
|
|
public byte[] renderPoster(Long activityId, Long userId, String templateName) {
|
||
|
|
PosterConfig.PosterTemplate template = posterConfig.getTemplate(templateName);
|
||
|
|
if (template == null) {
|
||
|
|
template = posterConfig.getTemplate(posterConfig.getDefaultTemplate());
|
||
|
|
}
|
||
|
|
|
||
|
|
Activity activity = null;
|
||
|
|
try {
|
||
|
|
// 获取活动信息
|
||
|
|
// activity = activityService.getActivityById(activityId);
|
||
|
|
} catch (Exception e) {
|
||
|
|
log.debug("Could not load activity: {}", e.getMessage());
|
||
|
|
}
|
||
|
|
|
||
|
|
BufferedImage image = new BufferedImage(template.getWidth(), template.getHeight(), BufferedImage.TYPE_INT_RGB);
|
||
|
|
Graphics2D g = image.createGraphics();
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 绘制背景
|
||
|
|
if (template.getBackground() != null && !template.getBackground().isBlank()) {
|
||
|
|
Image bgImage = loadImage(template.getBackground());
|
||
|
|
if (bgImage != null) {
|
||
|
|
g.drawImage(bgImage, 0, 0, template.getWidth(), template.getHeight(), null);
|
||
|
|
} else {
|
||
|
|
g.setColor(Color.decode(template.getBackgroundColor()));
|
||
|
|
g.fillRect(0, 0, template.getWidth(), template.getHeight());
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
g.setColor(Color.decode(template.getBackgroundColor()));
|
||
|
|
g.fillRect(0, 0, template.getWidth(), template.getHeight());
|
||
|
|
}
|
||
|
|
|
||
|
|
// 绘制元素
|
||
|
|
for (Map.Entry<String, PosterConfig.PosterElement> entry : template.getElements().entrySet()) {
|
||
|
|
PosterConfig.PosterElement element = entry.getValue();
|
||
|
|
drawElement(g, element, activity, activityId, userId);
|
||
|
|
}
|
||
|
|
|
||
|
|
} finally {
|
||
|
|
g.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||
|
|
try {
|
||
|
|
ImageIO.write(image, "PNG", baos);
|
||
|
|
return baos.toByteArray();
|
||
|
|
} catch (Exception e) {
|
||
|
|
log.error("Failed to generate poster image", e);
|
||
|
|
throw new RuntimeException("Failed to generate poster", e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public String renderPosterHtml(Long activityId, Long userId, String templateName) {
|
||
|
|
PosterConfig.PosterTemplate template = posterConfig.getTemplate(templateName);
|
||
|
|
if (template == null) {
|
||
|
|
template = posterConfig.getTemplate(posterConfig.getDefaultTemplate());
|
||
|
|
}
|
||
|
|
|
||
|
|
Activity activity = null;
|
||
|
|
try {
|
||
|
|
// activity = activityService.getActivityById(activityId);
|
||
|
|
} catch (Exception e) {
|
||
|
|
log.debug("Could not load activity: {}", e.getMessage());
|
||
|
|
}
|
||
|
|
|
||
|
|
String shortUrl = "/r/" + shortLinkService.create(
|
||
|
|
"https://example.com/landing?activityId=" + activityId + "&inviter=" + userId
|
||
|
|
).getCode();
|
||
|
|
|
||
|
|
StringBuilder html = new StringBuilder();
|
||
|
|
html.append("<!DOCTYPE html>");
|
||
|
|
html.append("<html><head>");
|
||
|
|
html.append("<meta charset=\"UTF-8\">");
|
||
|
|
html.append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
|
||
|
|
html.append("<title>").append(activity != null ? escapeHtml(activity.getName()) : "分享").append("</title>");
|
||
|
|
html.append("<style>");
|
||
|
|
html.append("body { margin: 0; padding: 0; background: ").append(template.getBackgroundColor()).append("; }");
|
||
|
|
html.append(".poster { position: relative; width: ").append(template.getWidth()).append("px; margin: 0 auto; }");
|
||
|
|
if (template.getBackground() != null) {
|
||
|
|
html.append(".poster { background-image: url('").append(template.getBackground()).append("'); background-size: cover; }");
|
||
|
|
}
|
||
|
|
html.append("</style></head><body>");
|
||
|
|
html.append("<div class=\"poster\" style=\"width: ").append(template.getWidth()).append("px; height: ").append(template.getHeight()).append("px;\">");
|
||
|
|
|
||
|
|
for (Map.Entry<String, PosterConfig.PosterElement> entry : template.getElements().entrySet()) {
|
||
|
|
PosterConfig.PosterElement element = entry.getValue();
|
||
|
|
String content = resolveContent(element, activity, activityId, userId, shortUrl);
|
||
|
|
|
||
|
|
if ("text".equals(element.getType())) {
|
||
|
|
html.append("<div style=\"position: absolute; left: ").append(element.getX()).append("px; top: ").append(element.getY()).append("px;");
|
||
|
|
html.append(" width: ").append(element.getWidth()).append("px; height: ").append(element.getHeight()).append("px;");
|
||
|
|
html.append(" color: ").append(element.getColor()).append("; font-size: ").append(element.getFontSize()).append(";");
|
||
|
|
html.append(" font-family: ").append(element.getFontFamily()).append("; text-align: ").append(element.getTextAlign()).append(";");
|
||
|
|
html.append(" overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\">");
|
||
|
|
html.append(content);
|
||
|
|
html.append("</div>");
|
||
|
|
} else if ("qrcode".equals(element.getType())) {
|
||
|
|
String encodedUrl;
|
||
|
|
try {
|
||
|
|
encodedUrl = java.net.URLEncoder.encode(shortUrl, java.nio.charset.StandardCharsets.UTF_8);
|
||
|
|
} catch (Exception e) {
|
||
|
|
encodedUrl = shortUrl;
|
||
|
|
}
|
||
|
|
html.append("<div style=\"position: absolute; left: ").append(element.getX()).append("px; top: ").append(element.getY()).append("px;");
|
||
|
|
html.append(" width: ").append(element.getWidth()).append("px; height: ").append(element.getHeight()).append("px;");
|
||
|
|
html.append(" background: #fff; padding: 10px;\">");
|
||
|
|
html.append("<img src=\"https://api.qrserver.com/v1/create-qr-code/?size=").append(element.getWidth() - 20).append("x").append(element.getHeight() - 20);
|
||
|
|
html.append("&data=").append(encodedUrl).append("\" style=\"width: 100%; height: 100%;\">");
|
||
|
|
html.append("</div>");
|
||
|
|
} else if ("image".equals(element.getType())) {
|
||
|
|
html.append("<img src=\"").append(content).append("\" style=\"position: absolute; left: ").append(element.getX()).append("px; top: ").append(element.getY());
|
||
|
|
html.append("px; width: ").append(element.getWidth()).append("px; height: ").append(element.getHeight()).append("px;\">");
|
||
|
|
} else if ("button".equals(element.getType())) {
|
||
|
|
html.append("<a href=\"").append(shortUrl).append("\" style=\"position: absolute; left: ").append(element.getX()).append("px; top: ").append(element.getY());
|
||
|
|
html.append("px; width: ").append(element.getWidth()).append("px; height: ").append(element.getHeight()).append("px; display: block;");
|
||
|
|
if (element.getBackground() != null) {
|
||
|
|
html.append(" background: ").append(element.getBackground()).append(";");
|
||
|
|
}
|
||
|
|
html.append(" border-radius: ").append(element.getBorderRadius() != null ? element.getBorderRadius() : "0").append(";\">");
|
||
|
|
html.append("<span style=\"display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; color: ").append(element.getColor()).append("; font-size: ").append(element.getFontSize()).append(";\">");
|
||
|
|
html.append(content);
|
||
|
|
html.append("</span></a>");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
html.append("</div></body></html>");
|
||
|
|
return html.toString();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void drawElement(Graphics2D g, PosterConfig.PosterElement element, Activity activity, Long activityId, Long userId) {
|
||
|
|
String content = resolveContent(element, activity, activityId, userId, "/r/abc123");
|
||
|
|
|
||
|
|
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||
|
|
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||
|
|
|
||
|
|
if ("text".equals(element.getType())) {
|
||
|
|
g.setColor(Color.decode(element.getColor()));
|
||
|
|
Font font = new Font(element.getFontFamily(), Font.BOLD, parseFontSize(element.getFontSize()));
|
||
|
|
g.setFont(font);
|
||
|
|
FontMetrics fm = g.getFontMetrics();
|
||
|
|
int textY = element.getY() + fm.getAscent();
|
||
|
|
g.drawString(content, element.getX(), textY);
|
||
|
|
} else if ("qrcode".equals(element.getType())) {
|
||
|
|
g.setColor(Color.WHITE);
|
||
|
|
g.fillRect(element.getX(), element.getY(), element.getWidth(), element.getHeight());
|
||
|
|
} else if ("rect".equals(element.getType())) {
|
||
|
|
g.setColor(Color.decode(element.getBackground() != null ? element.getBackground() : "#ffffff"));
|
||
|
|
g.fillRect(element.getX(), element.getY(), element.getWidth(), element.getHeight());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private String resolveContent(PosterConfig.PosterElement element, Activity activity, Long activityId, Long userId, String shortUrl) {
|
||
|
|
String raw = element.getContent();
|
||
|
|
if (raw == null) return "";
|
||
|
|
|
||
|
|
return raw.replace("{{activityName}}", activity != null ? activity.getName() : "活动")
|
||
|
|
.replace("{{activityId}}", String.valueOf(activityId))
|
||
|
|
.replace("{{userId}}", String.valueOf(userId))
|
||
|
|
.replace("{{shortUrl}}", shortUrl)
|
||
|
|
.replace("{{title}}", element.getContent());
|
||
|
|
}
|
||
|
|
|
||
|
|
private Image loadImage(String urlStr) {
|
||
|
|
return imageCache.computeIfAbsent(urlStr, k -> {
|
||
|
|
try {
|
||
|
|
URL url = new URL(posterConfig.getCdnBaseUrl() + "/" + urlStr);
|
||
|
|
return ImageIO.read(url);
|
||
|
|
} catch (Exception e) {
|
||
|
|
log.debug("Failed to load image: {}", urlStr);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private int parseFontSize(String size) {
|
||
|
|
try {
|
||
|
|
return Integer.parseInt(size.replace("px", ""));
|
||
|
|
} catch (Exception e) {
|
||
|
|
return 16;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private String escapeHtml(String text) {
|
||
|
|
if (text == null) return "";
|
||
|
|
return text.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """);
|
||
|
|
}
|
||
|
|
}
|