作者 钟来

水产舆情采集海报生成背景图修改

... ... @@ -48,6 +48,28 @@ public class FishAquaticPublicOpinion extends BaseEntity
@ApiModelProperty(value="鱼价")
private String fishPrice;
@ApiModelProperty(value="简介")
private String briefIntroduction;
@ApiModelProperty(value="短标题")
private String shortTitle;
public String getShortTitle() {
return shortTitle;
}
public void setShortTitle(String shortTitle) {
this.shortTitle = shortTitle;
}
public String getBriefIntroduction() {
return briefIntroduction;
}
public void setBriefIntroduction(String briefIntroduction) {
this.briefIntroduction = briefIntroduction;
}
public String getFishPrice() {
return fishPrice;
}
... ...
... ... @@ -48,109 +48,147 @@ public class MultiTextImageGenerator {
* 背景图分为顶部 logo、中部纯色、底部 logo 三部分。
* 中间内容区根据数据动态拉伸,高度自动适配。
*/
public static String generateImage(List<AbstractMap.SimpleEntry<String, String>> dataList, StyleConfig style) throws IOException {
// 读取背景图
public static String generateImage(List<AbstractMap.SimpleEntry<String, String>> dataList,
StyleConfig style) throws IOException {
// 加载背景图及参数
BufferedImage baseImage = ImageIO.read(new File(style.backgroundPath));
int width = baseImage.getWidth();
int maxTextWidth = width - 2 * style.startX;
// 取背景图中部颜色作为中间背景区的纯色背景
// 获取中部纯色背景色
int centerY = baseImage.getHeight() / 2;
int centerColorRGB = baseImage.getRGB(width / 2, centerY);
Color pureBackgroundColor = new Color(centerColorRGB);
// 创建临时画布用于计算文字高度
BufferedImage tempImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D tempG = tempImage.createGraphics();
tempG.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
tempG.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
int contentHeight = calculateTotalHeight(dataList, style, tempG, maxTextWidth); // 动态内容高度
tempG.dispose();
// 新图总高度 = 顶部logo + 内容区 + 底部logo
int finalHeight = style.topLogoHeight + contentHeight + style.bottomLogoHeight;
BufferedImage newImage = new BufferedImage(width, finalHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = newImage.createGraphics();
// 绘制顶部 logo 区域
Color pureBackgroundColor = new Color(baseImage.getRGB(width / 2, centerY));
// 计算内容区高度
BufferedImage tmp = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D gTemp = tmp.createGraphics();
gTemp.setFont(style.contentFont);
FontMetrics fmTemp = gTemp.getFontMetrics();
int contentHeight = calculateTotalHeight(dataList, style, gTemp, maxTextWidth);
gTemp.dispose();
// 加载 logo 和二维码
BufferedImage logoImg = ImageIO.read(new File("logo_baise.png"));
BufferedImage qr1 = ImageIO.read(new File(style.qrCodePath1));
BufferedImage qr2 = ImageIO.read(new File(style.qrCodePath2));
int qrWidth = qr1.getWidth(), qrHeight = qr1.getHeight();
// 准备文字高度
BufferedImage tmp2 = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D gTemp2 = tmp2.createGraphics();
gTemp2.setFont(style.contentFont);
FontMetrics fm2 = gTemp2.getFontMetrics();
int textHeight = fm2.getHeight();
gTemp2.dispose();
// 间距配置
int qrTextSpacing = style.qrTextSpacing; // 二维码下文字间距
int minBottomPadding = style.minBottomPadding; // 底部最少空白
int qrSpacing = style.qrSpacing;
// 计算底部总高度:二维码 + 文字 + spacing + padding
int bottomBlockHeight = qrHeight + qrTextSpacing + textHeight + minBottomPadding;
// 总高度
int finalHeight = style.topLogoHeight + contentHeight + bottomBlockHeight;
// 真正画图
BufferedImage newImg = new BufferedImage(width, finalHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = newImg.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// 顶部 logo 区
BufferedImage topPart = baseImage.getSubimage(0, 0, width, style.topLogoHeight);
g.drawImage(topPart, 0, 0, null);
// 绘制底部 logo 区域
BufferedImage bottomPart = baseImage.getSubimage(0, baseImage.getHeight() - style.bottomLogoHeight, width, style.bottomLogoHeight);
g.drawImage(bottomPart, 0, finalHeight - style.bottomLogoHeight, null);
// 绘制中部纯色背景(中间拉伸区域)
// 中部背景
g.setColor(pureBackgroundColor);
g.fillRect(0, style.topLogoHeight, width, contentHeight);
// 开启抗锯齿和文字抗锯齿
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// 顶部右上角 logo
int logoW = logoImg.getWidth(), logoH = logoImg.getHeight();
g.drawImage(logoImg, width - style.startX - logoW, 20, null);
// 内容绘制起始 Y 坐标
// 内容绘制
int currY = style.topLogoHeight + style.startY;
for (int i = 0; i < dataList.size(); i++) {
String title = (i + 1) + ". " + dataList.get(i).getKey(); // 标题编号 + 标题
String content = dataList.get(i).getValue(); // 内容文本
String title = (i + 1) + ". " + dataList.get(i).getKey();
String content = dataList.get(i).getValue();
// 获取标题高度
g.setFont(style.titleFont);
FontMetrics titleMetrics = g.getFontMetrics();
int titleHeight = titleMetrics.getHeight();
FontMetrics tfm = g.getFontMetrics();
int th = tfm.getHeight();
// 获取内容文本行
g.setFont(style.contentFont);
FontMetrics contentMetrics = g.getFontMetrics();
List<String> lines = splitTextIntoLines(content, contentMetrics, maxTextWidth);
int lineHeight = contentMetrics.getHeight() + style.lineSpacing;
int contentBlockHeight = lines.size() * lineHeight;
FontMetrics cfm = g.getFontMetrics();
List<String> lines = splitTextIntoLines(content, cfm, maxTextWidth);
int lineH = cfm.getHeight() + style.lineSpacing;
int blockContentH = lines.size() * lineH;
// 整体文字高度 = 标题 + 间距 + 内容块
int totalTextHeight = titleHeight + 10 + contentBlockHeight;
int blockHeight = totalTextHeight + 2 * style.padding;
int totalTextH = th + 10 + blockContentH;
int blockH = totalTextH + 2 * style.padding;
int blockW = maxTextWidth + 2 * style.padding;
int blockWidth = maxTextWidth + 2 * style.padding;
int blockX = style.startX - style.padding;
int blockY = currY;
// 绘制圆角背景块(可配置是否显示)
int bx = style.startX - style.padding;
int by = currY;
if (style.showBackgroundBlock) {
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, style.backgroundAlpha));
g.setColor(style.backgroundColor);
g.fillRoundRect(blockX, blockY, blockWidth, blockHeight, style.cornerRadius, style.cornerRadius);
g.fillRoundRect(bx, by, blockW, blockH, style.cornerRadius, style.cornerRadius);
}
// 恢复正常透明度,绘制文本内容
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f));
g.setColor(style.textColor);
int textStartY = blockY + style.padding;
int ty0 = by + style.padding;
g.setFont(style.titleFont);
int titleY = textStartY + titleHeight;
g.drawString(title, style.startX, titleY);
g.drawString(title, style.startX, ty0 + th);
g.setFont(style.contentFont);
int contentStartY = titleY + 10 + lineHeight;
int contentStart = ty0 + th + 10 + lineH;
for (int j = 0; j < lines.size(); j++) {
int y = contentStartY + j * lineHeight;
g.drawString(lines.get(j), style.startX, y);
g.drawString(lines.get(j), style.startX, contentStart + j * lineH);
}
currY = blockY + blockHeight + style.blockSpacing; // 下一个块起始位置
currY = by + blockH + style.blockSpacing;
}
// 绘制水印文字(右下角)
// 底部背景填充
int bottomY = style.topLogoHeight + contentHeight;
g.setColor(pureBackgroundColor);
g.fillRect(0, bottomY, width, bottomBlockHeight);
// 绘制二维码(右对齐)
int totalQW = 2 * qrWidth + qrSpacing;
int qr2X = width - style.startX - qrWidth;
int qr1X = qr2X - qrSpacing - qrWidth;
int qrY = bottomY + minBottomPadding / 2;
g.drawImage(qr1, qr1X, qrY, null);
g.drawImage(qr2, qr2X, qrY, null);
// 绘制每个二维码下方说明文字
g.setFont(style.tishiFont);
g.setColor(style.textColor);
String txt1 = "学习渔业知识";
String txt2 = "掌握渔价信息";
int w1 = fm2.stringWidth(txt1), w2 = fm2.stringWidth(txt2);
int textY = qrY + qrHeight + qrTextSpacing;
g.drawString(txt1, qr1X + (qrWidth - w1) / 2, textY);
g.drawString(txt2, qr2X + (qrWidth - w2) / 2, textY);
drawTextWatermark(g, width, finalHeight, style);
g.dispose();
// 生成保存文件名
String saveFile = style.savePath + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()) + "_xinwen.png";
ImageIO.write(newImage, "png", new File(saveFile));
return saveFile;
String savePath = style.savePath + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()) + "_xinwen.png";
ImageIO.write(newImg, "png", new File(savePath));
return savePath;
}
/**
* 将文本按像素宽度进行自动换行
*/
... ... @@ -196,18 +234,48 @@ public class MultiTextImageGenerator {
return currY;
}
// /**
// * 绘制右下角文字水印
// */
// private static void drawTextWatermark(Graphics2D g, int width, int height, StyleConfig style) {
// g.setFont(style.watermarkFont);
// g.setColor(style.watermarkColor);
// FontMetrics metrics = g.getFontMetrics();
// int textWidth = metrics.stringWidth(style.watermarkText);
// int x = width - textWidth - style.watermarkTextMarginX;
// int y = height - style.watermarkTextMarginY;
// g.drawString(style.watermarkText, x, y);
// }
/**
* 绘制右下角文字水印
* 在图像上多个随机位置绘制水印文字
*/
private static void drawTextWatermark(Graphics2D g, int width, int height, StyleConfig style) {
g.setFont(style.watermarkFont);
g.setColor(style.watermarkColor);
FontMetrics metrics = g.getFontMetrics();
int textWidth = metrics.stringWidth(style.watermarkText);
int x = width - textWidth - style.watermarkTextMarginX;
int y = height - style.watermarkTextMarginY;
g.drawString(style.watermarkText, x, y);
int textHeight = metrics.getHeight();
int count = style.watermarkCount; // 水印数量
Random random = new Random(); // 在方法内生成随机数对象
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, style.watermarkAlpha)); // 半透明
for (int i = 0; i < count; i++) {
int maxX = Math.max(1, width - textWidth - style.watermarkTextMarginX * 2);
int maxY = Math.max(1, height - textHeight - style.watermarkTextMarginY * 2);
int x = random.nextInt(maxX) + style.watermarkTextMarginX;
int y = random.nextInt(maxY) + style.watermarkTextMarginY + textHeight;
g.setColor(style.watermarkColor);
g.drawString(style.watermarkText, x, y);
}
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
}
}
... ...
... ... @@ -16,12 +16,15 @@ public class StyleConfig {
titleFont = baseFont.deriveFont(Font.BOLD, 36f);
contentFont = baseFont.deriveFont(Font.PLAIN, 28f);
watermarkFont = baseFont.deriveFont(Font.PLAIN, 20f);
tishiFont = baseFont.deriveFont(Font.PLAIN, 28f);
GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(baseFont);
} catch (Exception e) {
System.err.println("❌ 加载字体失败,使用默认字体:" + e.getMessage());
titleFont = new Font("Serif", Font.BOLD, 36);
contentFont = new Font("Serif", Font.PLAIN, 28);
watermarkFont = new Font("Serif", Font.PLAIN, 20);
tishiFont = new Font("Serif", Font.PLAIN, 28);
}
}
public String backgroundPath = "moban.png";
... ... @@ -35,6 +38,7 @@ public class StyleConfig {
public Font contentFont = new Font("微软雅黑", Font.PLAIN, 28);
// 正文字体,微软雅黑,常规,字号28
public Font tishiFont = new Font("微软雅黑", Font.PLAIN, 28);
public Color textColor = Color.WHITE;
// 文字颜色,默认为白色
... ... @@ -67,7 +71,7 @@ public class StyleConfig {
// 内容段落之间的行间距(单位像素)
// 文字水印配置
public String watermarkText = "@鱼儿乐科技";
public String watermarkText = "@鱼儿乐";
// 添加的文字水印内容
public Font watermarkFont = new Font("微软雅黑", Font.PLAIN, 20);
... ... @@ -76,10 +80,10 @@ public class StyleConfig {
public Color watermarkColor = new Color(255, 255, 255, 180);
// 水印颜色:白色,透明度约70%(255=不透明,180≈70%透明)
public int watermarkTextMarginX = 567;
public int watermarkTextMarginX = 20;
// 文字水印的横坐标偏移量,单位像素(从左上角或右下角偏移)
public int watermarkTextMarginY = 1236;
public int watermarkTextMarginY = 20;
// 文字水印的纵坐标偏移量,单位像素(从左上角或右下角偏移)
// 图片水印配置
... ... @@ -98,7 +102,22 @@ public class StyleConfig {
public int watermarkImageMarginY = 20;
// 图片水印的纵向偏移量(从右下角向内缩进的距离)
//控制 两个二维码之间的横向间距。
public int qrSpacing = 100;
///控制 logo_baise.png 和 第一个二维码之间的水平间距。
public int logoToQrSpacing = 40;
//第一个二维码
public String qrCodePath1 = "yujiangtang.png";
//第二个二维码
public String qrCodePath2 = "huimaiyu.png";
public int qrTextSpacing = 30; // 二维码下文字间距,建议 10
public int minBottomPadding = 30; // 底部留白,建议 20~40
public int watermarkCount = 10; // 水印数量
public float watermarkAlpha = 0.3f; // 透明度(0~1)
}
... ...
... ... @@ -228,6 +228,18 @@ public class ServerController extends BaseController
return AjaxResult.success(publicService.updateObject(fishAquaticPublicOpinion,"id"));
}
@ApiOperation("修改简介")
@PostMapping("/upBriefIntroduction/{id}")
public AjaxResult upBriefIntroduction(@PathVariable Integer id, String briefIntroduction,String shortTitle)
{
FishAquaticPublicOpinion fishAquaticPublicOpinion = new FishAquaticPublicOpinion();
fishAquaticPublicOpinion.setId(id);
fishAquaticPublicOpinion.setBriefIntroduction(briefIntroduction);
fishAquaticPublicOpinion.setShortTitle(shortTitle);
return AjaxResult.success(publicService.updateObject(fishAquaticPublicOpinion,"id"));
}
@ApiOperation("生成水产新闻海报")
@PostMapping("/generateAquaticPublicOpinionPoster")
... ...