作者 钟来

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

@@ -48,6 +48,28 @@ public class FishAquaticPublicOpinion extends BaseEntity @@ -48,6 +48,28 @@ public class FishAquaticPublicOpinion extends BaseEntity
48 @ApiModelProperty(value="鱼价") 48 @ApiModelProperty(value="鱼价")
49 private String fishPrice; 49 private String fishPrice;
50 50
  51 + @ApiModelProperty(value="简介")
  52 + private String briefIntroduction;
  53 +
  54 + @ApiModelProperty(value="短标题")
  55 + private String shortTitle;
  56 +
  57 + public String getShortTitle() {
  58 + return shortTitle;
  59 + }
  60 +
  61 + public void setShortTitle(String shortTitle) {
  62 + this.shortTitle = shortTitle;
  63 + }
  64 +
  65 + public String getBriefIntroduction() {
  66 + return briefIntroduction;
  67 + }
  68 +
  69 + public void setBriefIntroduction(String briefIntroduction) {
  70 + this.briefIntroduction = briefIntroduction;
  71 + }
  72 +
51 public String getFishPrice() { 73 public String getFishPrice() {
52 return fishPrice; 74 return fishPrice;
53 } 75 }
@@ -48,109 +48,147 @@ public class MultiTextImageGenerator { @@ -48,109 +48,147 @@ public class MultiTextImageGenerator {
48 * 背景图分为顶部 logo、中部纯色、底部 logo 三部分。 48 * 背景图分为顶部 logo、中部纯色、底部 logo 三部分。
49 * 中间内容区根据数据动态拉伸,高度自动适配。 49 * 中间内容区根据数据动态拉伸,高度自动适配。
50 */ 50 */
51 - public static String generateImage(List<AbstractMap.SimpleEntry<String, String>> dataList, StyleConfig style) throws IOException {  
52 - // 读取背景图 51 + public static String generateImage(List<AbstractMap.SimpleEntry<String, String>> dataList,
  52 + StyleConfig style) throws IOException {
  53 + // 加载背景图及参数
53 BufferedImage baseImage = ImageIO.read(new File(style.backgroundPath)); 54 BufferedImage baseImage = ImageIO.read(new File(style.backgroundPath));
54 int width = baseImage.getWidth(); 55 int width = baseImage.getWidth();
55 int maxTextWidth = width - 2 * style.startX; 56 int maxTextWidth = width - 2 * style.startX;
56 57
57 - // 取背景图中部颜色作为中间背景区的纯色背景 58 + // 获取中部纯色背景色
58 int centerY = baseImage.getHeight() / 2; 59 int centerY = baseImage.getHeight() / 2;
59 - int centerColorRGB = baseImage.getRGB(width / 2, centerY);  
60 - Color pureBackgroundColor = new Color(centerColorRGB);  
61 -  
62 - // 创建临时画布用于计算文字高度  
63 - BufferedImage tempImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);  
64 - Graphics2D tempG = tempImage.createGraphics();  
65 - tempG.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);  
66 - tempG.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);  
67 - int contentHeight = calculateTotalHeight(dataList, style, tempG, maxTextWidth); // 动态内容高度  
68 - tempG.dispose();  
69 -  
70 - // 新图总高度 = 顶部logo + 内容区 + 底部logo  
71 - int finalHeight = style.topLogoHeight + contentHeight + style.bottomLogoHeight;  
72 - BufferedImage newImage = new BufferedImage(width, finalHeight, BufferedImage.TYPE_INT_ARGB);  
73 - Graphics2D g = newImage.createGraphics();  
74 -  
75 - // 绘制顶部 logo 区域 60 + Color pureBackgroundColor = new Color(baseImage.getRGB(width / 2, centerY));
  61 +
  62 + // 计算内容区高度
  63 + BufferedImage tmp = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
  64 + Graphics2D gTemp = tmp.createGraphics();
  65 + gTemp.setFont(style.contentFont);
  66 + FontMetrics fmTemp = gTemp.getFontMetrics();
  67 + int contentHeight = calculateTotalHeight(dataList, style, gTemp, maxTextWidth);
  68 + gTemp.dispose();
  69 +
  70 + // 加载 logo 和二维码
  71 + BufferedImage logoImg = ImageIO.read(new File("logo_baise.png"));
  72 + BufferedImage qr1 = ImageIO.read(new File(style.qrCodePath1));
  73 + BufferedImage qr2 = ImageIO.read(new File(style.qrCodePath2));
  74 + int qrWidth = qr1.getWidth(), qrHeight = qr1.getHeight();
  75 +
  76 + // 准备文字高度
  77 + BufferedImage tmp2 = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
  78 + Graphics2D gTemp2 = tmp2.createGraphics();
  79 + gTemp2.setFont(style.contentFont);
  80 + FontMetrics fm2 = gTemp2.getFontMetrics();
  81 + int textHeight = fm2.getHeight();
  82 + gTemp2.dispose();
  83 +
  84 + // 间距配置
  85 + int qrTextSpacing = style.qrTextSpacing; // 二维码下文字间距
  86 + int minBottomPadding = style.minBottomPadding; // 底部最少空白
  87 + int qrSpacing = style.qrSpacing;
  88 +
  89 + // 计算底部总高度:二维码 + 文字 + spacing + padding
  90 + int bottomBlockHeight = qrHeight + qrTextSpacing + textHeight + minBottomPadding;
  91 +
  92 + // 总高度
  93 + int finalHeight = style.topLogoHeight + contentHeight + bottomBlockHeight;
  94 +
  95 + // 真正画图
  96 + BufferedImage newImg = new BufferedImage(width, finalHeight, BufferedImage.TYPE_INT_ARGB);
  97 + Graphics2D g = newImg.createGraphics();
  98 + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  99 + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
  100 +
  101 + // 顶部 logo 区
76 BufferedImage topPart = baseImage.getSubimage(0, 0, width, style.topLogoHeight); 102 BufferedImage topPart = baseImage.getSubimage(0, 0, width, style.topLogoHeight);
77 g.drawImage(topPart, 0, 0, null); 103 g.drawImage(topPart, 0, 0, null);
78 104
79 - // 绘制底部 logo 区域  
80 - BufferedImage bottomPart = baseImage.getSubimage(0, baseImage.getHeight() - style.bottomLogoHeight, width, style.bottomLogoHeight);  
81 - g.drawImage(bottomPart, 0, finalHeight - style.bottomLogoHeight, null);  
82 -  
83 - // 绘制中部纯色背景(中间拉伸区域) 105 + // 中部背景
84 g.setColor(pureBackgroundColor); 106 g.setColor(pureBackgroundColor);
85 g.fillRect(0, style.topLogoHeight, width, contentHeight); 107 g.fillRect(0, style.topLogoHeight, width, contentHeight);
86 108
87 - // 开启抗锯齿和文字抗锯齿  
88 - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);  
89 - g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); 109 + // 顶部右上角 logo
  110 + int logoW = logoImg.getWidth(), logoH = logoImg.getHeight();
  111 + g.drawImage(logoImg, width - style.startX - logoW, 20, null);
90 112
91 - // 内容绘制起始 Y 坐标 113 + // 内容绘制
92 int currY = style.topLogoHeight + style.startY; 114 int currY = style.topLogoHeight + style.startY;
93 -  
94 for (int i = 0; i < dataList.size(); i++) { 115 for (int i = 0; i < dataList.size(); i++) {
95 - String title = (i + 1) + ". " + dataList.get(i).getKey(); // 标题编号 + 标题  
96 - String content = dataList.get(i).getValue(); // 内容文本 116 + String title = (i + 1) + ". " + dataList.get(i).getKey();
  117 + String content = dataList.get(i).getValue();
97 118
98 - // 获取标题高度  
99 g.setFont(style.titleFont); 119 g.setFont(style.titleFont);
100 - FontMetrics titleMetrics = g.getFontMetrics();  
101 - int titleHeight = titleMetrics.getHeight(); 120 + FontMetrics tfm = g.getFontMetrics();
  121 + int th = tfm.getHeight();
102 122
103 - // 获取内容文本行  
104 g.setFont(style.contentFont); 123 g.setFont(style.contentFont);
105 - FontMetrics contentMetrics = g.getFontMetrics();  
106 - List<String> lines = splitTextIntoLines(content, contentMetrics, maxTextWidth);  
107 - int lineHeight = contentMetrics.getHeight() + style.lineSpacing;  
108 - int contentBlockHeight = lines.size() * lineHeight;  
109 -  
110 - // 整体文字高度 = 标题 + 间距 + 内容块  
111 - int totalTextHeight = titleHeight + 10 + contentBlockHeight;  
112 - int blockHeight = totalTextHeight + 2 * style.padding; 124 + FontMetrics cfm = g.getFontMetrics();
  125 + List<String> lines = splitTextIntoLines(content, cfm, maxTextWidth);
  126 + int lineH = cfm.getHeight() + style.lineSpacing;
  127 + int blockContentH = lines.size() * lineH;
113 128
114 - int blockWidth = maxTextWidth + 2 * style.padding;  
115 - int blockX = style.startX - style.padding;  
116 - int blockY = currY; 129 + int totalTextH = th + 10 + blockContentH;
  130 + int blockH = totalTextH + 2 * style.padding;
  131 + int blockW = maxTextWidth + 2 * style.padding;
117 132
118 - // 绘制圆角背景块(可配置是否显示) 133 + int bx = style.startX - style.padding;
  134 + int by = currY;
119 if (style.showBackgroundBlock) { 135 if (style.showBackgroundBlock) {
120 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, style.backgroundAlpha)); 136 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, style.backgroundAlpha));
121 g.setColor(style.backgroundColor); 137 g.setColor(style.backgroundColor);
122 - g.fillRoundRect(blockX, blockY, blockWidth, blockHeight, style.cornerRadius, style.cornerRadius); 138 + g.fillRoundRect(bx, by, blockW, blockH, style.cornerRadius, style.cornerRadius);
123 } 139 }
124 -  
125 - // 恢复正常透明度,绘制文本内容  
126 - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)); 140 + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f));
127 g.setColor(style.textColor); 141 g.setColor(style.textColor);
128 142
129 - int textStartY = blockY + style.padding; 143 + int ty0 = by + style.padding;
130 g.setFont(style.titleFont); 144 g.setFont(style.titleFont);
131 - int titleY = textStartY + titleHeight;  
132 - g.drawString(title, style.startX, titleY); 145 + g.drawString(title, style.startX, ty0 + th);
133 146
134 g.setFont(style.contentFont); 147 g.setFont(style.contentFont);
135 - int contentStartY = titleY + 10 + lineHeight; 148 + int contentStart = ty0 + th + 10 + lineH;
136 for (int j = 0; j < lines.size(); j++) { 149 for (int j = 0; j < lines.size(); j++) {
137 - int y = contentStartY + j * lineHeight;  
138 - g.drawString(lines.get(j), style.startX, y); 150 + g.drawString(lines.get(j), style.startX, contentStart + j * lineH);
139 } 151 }
140 152
141 - currY = blockY + blockHeight + style.blockSpacing; // 下一个块起始位置 153 + currY = by + blockH + style.blockSpacing;
142 } 154 }
143 155
144 - // 绘制水印文字(右下角) 156 + // 底部背景填充
  157 + int bottomY = style.topLogoHeight + contentHeight;
  158 + g.setColor(pureBackgroundColor);
  159 + g.fillRect(0, bottomY, width, bottomBlockHeight);
  160 +
  161 + // 绘制二维码(右对齐)
  162 + int totalQW = 2 * qrWidth + qrSpacing;
  163 + int qr2X = width - style.startX - qrWidth;
  164 + int qr1X = qr2X - qrSpacing - qrWidth;
  165 + int qrY = bottomY + minBottomPadding / 2;
  166 +
  167 + g.drawImage(qr1, qr1X, qrY, null);
  168 + g.drawImage(qr2, qr2X, qrY, null);
  169 +
  170 + // 绘制每个二维码下方说明文字
  171 + g.setFont(style.tishiFont);
  172 + g.setColor(style.textColor);
  173 + String txt1 = "学习渔业知识";
  174 + String txt2 = "掌握渔价信息";
  175 + int w1 = fm2.stringWidth(txt1), w2 = fm2.stringWidth(txt2);
  176 + int textY = qrY + qrHeight + qrTextSpacing;
  177 +
  178 + g.drawString(txt1, qr1X + (qrWidth - w1) / 2, textY);
  179 + g.drawString(txt2, qr2X + (qrWidth - w2) / 2, textY);
  180 +
145 drawTextWatermark(g, width, finalHeight, style); 181 drawTextWatermark(g, width, finalHeight, style);
146 g.dispose(); 182 g.dispose();
147 183
148 - // 生成保存文件名  
149 - String saveFile = style.savePath + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()) + "_xinwen.png";  
150 - ImageIO.write(newImage, "png", new File(saveFile));  
151 - return saveFile; 184 + String savePath = style.savePath + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()) + "_xinwen.png";
  185 + ImageIO.write(newImg, "png", new File(savePath));
  186 + return savePath;
152 } 187 }
153 188
  189 +
  190 +
  191 +
154 /** 192 /**
155 * 将文本按像素宽度进行自动换行 193 * 将文本按像素宽度进行自动换行
156 */ 194 */
@@ -196,18 +234,48 @@ public class MultiTextImageGenerator { @@ -196,18 +234,48 @@ public class MultiTextImageGenerator {
196 return currY; 234 return currY;
197 } 235 }
198 236
  237 +// /**
  238 +// * 绘制右下角文字水印
  239 +// */
  240 +// private static void drawTextWatermark(Graphics2D g, int width, int height, StyleConfig style) {
  241 +// g.setFont(style.watermarkFont);
  242 +// g.setColor(style.watermarkColor);
  243 +// FontMetrics metrics = g.getFontMetrics();
  244 +// int textWidth = metrics.stringWidth(style.watermarkText);
  245 +// int x = width - textWidth - style.watermarkTextMarginX;
  246 +// int y = height - style.watermarkTextMarginY;
  247 +// g.drawString(style.watermarkText, x, y);
  248 +// }
  249 +
199 /** 250 /**
200 - * 绘制右下角文字水印 251 + * 在图像上多个随机位置绘制水印文字
201 */ 252 */
202 private static void drawTextWatermark(Graphics2D g, int width, int height, StyleConfig style) { 253 private static void drawTextWatermark(Graphics2D g, int width, int height, StyleConfig style) {
203 g.setFont(style.watermarkFont); 254 g.setFont(style.watermarkFont);
204 - g.setColor(style.watermarkColor);  
205 FontMetrics metrics = g.getFontMetrics(); 255 FontMetrics metrics = g.getFontMetrics();
206 int textWidth = metrics.stringWidth(style.watermarkText); 256 int textWidth = metrics.stringWidth(style.watermarkText);
207 - int x = width - textWidth - style.watermarkTextMarginX;  
208 - int y = height - style.watermarkTextMarginY; 257 + int textHeight = metrics.getHeight();
  258 +
  259 + int count = style.watermarkCount; // 水印数量
  260 + Random random = new Random(); // 在方法内生成随机数对象
  261 +
  262 + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, style.watermarkAlpha)); // 半透明
  263 +
  264 + for (int i = 0; i < count; i++) {
  265 + int maxX = Math.max(1, width - textWidth - style.watermarkTextMarginX * 2);
  266 + int maxY = Math.max(1, height - textHeight - style.watermarkTextMarginY * 2);
  267 +
  268 + int x = random.nextInt(maxX) + style.watermarkTextMarginX;
  269 + int y = random.nextInt(maxY) + style.watermarkTextMarginY + textHeight;
  270 +
  271 + g.setColor(style.watermarkColor);
209 g.drawString(style.watermarkText, x, y); 272 g.drawString(style.watermarkText, x, y);
210 } 273 }
211 274
  275 + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
  276 + }
  277 +
  278 +
  279 +
212 } 280 }
213 281
@@ -16,12 +16,15 @@ public class StyleConfig { @@ -16,12 +16,15 @@ public class StyleConfig {
16 titleFont = baseFont.deriveFont(Font.BOLD, 36f); 16 titleFont = baseFont.deriveFont(Font.BOLD, 36f);
17 contentFont = baseFont.deriveFont(Font.PLAIN, 28f); 17 contentFont = baseFont.deriveFont(Font.PLAIN, 28f);
18 watermarkFont = baseFont.deriveFont(Font.PLAIN, 20f); 18 watermarkFont = baseFont.deriveFont(Font.PLAIN, 20f);
  19 + tishiFont = baseFont.deriveFont(Font.PLAIN, 28f);
19 20
20 GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(baseFont); 21 GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(baseFont);
21 } catch (Exception e) { 22 } catch (Exception e) {
22 System.err.println("❌ 加载字体失败,使用默认字体:" + e.getMessage()); 23 System.err.println("❌ 加载字体失败,使用默认字体:" + e.getMessage());
23 titleFont = new Font("Serif", Font.BOLD, 36); 24 titleFont = new Font("Serif", Font.BOLD, 36);
24 contentFont = new Font("Serif", Font.PLAIN, 28); 25 contentFont = new Font("Serif", Font.PLAIN, 28);
  26 + watermarkFont = new Font("Serif", Font.PLAIN, 20);
  27 + tishiFont = new Font("Serif", Font.PLAIN, 28);
25 } 28 }
26 } 29 }
27 public String backgroundPath = "moban.png"; 30 public String backgroundPath = "moban.png";
@@ -35,6 +38,7 @@ public class StyleConfig { @@ -35,6 +38,7 @@ public class StyleConfig {
35 public Font contentFont = new Font("微软雅黑", Font.PLAIN, 28); 38 public Font contentFont = new Font("微软雅黑", Font.PLAIN, 28);
36 // 正文字体,微软雅黑,常规,字号28 39 // 正文字体,微软雅黑,常规,字号28
37 40
  41 + public Font tishiFont = new Font("微软雅黑", Font.PLAIN, 28);
38 public Color textColor = Color.WHITE; 42 public Color textColor = Color.WHITE;
39 // 文字颜色,默认为白色 43 // 文字颜色,默认为白色
40 44
@@ -67,7 +71,7 @@ public class StyleConfig { @@ -67,7 +71,7 @@ public class StyleConfig {
67 // 内容段落之间的行间距(单位像素) 71 // 内容段落之间的行间距(单位像素)
68 72
69 // 文字水印配置 73 // 文字水印配置
70 - public String watermarkText = "@鱼儿乐科技"; 74 + public String watermarkText = "@鱼儿乐";
71 // 添加的文字水印内容 75 // 添加的文字水印内容
72 76
73 public Font watermarkFont = new Font("微软雅黑", Font.PLAIN, 20); 77 public Font watermarkFont = new Font("微软雅黑", Font.PLAIN, 20);
@@ -76,10 +80,10 @@ public class StyleConfig { @@ -76,10 +80,10 @@ public class StyleConfig {
76 public Color watermarkColor = new Color(255, 255, 255, 180); 80 public Color watermarkColor = new Color(255, 255, 255, 180);
77 // 水印颜色:白色,透明度约70%(255=不透明,180≈70%透明) 81 // 水印颜色:白色,透明度约70%(255=不透明,180≈70%透明)
78 82
79 - public int watermarkTextMarginX = 567; 83 + public int watermarkTextMarginX = 20;
80 // 文字水印的横坐标偏移量,单位像素(从左上角或右下角偏移) 84 // 文字水印的横坐标偏移量,单位像素(从左上角或右下角偏移)
81 85
82 - public int watermarkTextMarginY = 1236; 86 + public int watermarkTextMarginY = 20;
83 // 文字水印的纵坐标偏移量,单位像素(从左上角或右下角偏移) 87 // 文字水印的纵坐标偏移量,单位像素(从左上角或右下角偏移)
84 88
85 // 图片水印配置 89 // 图片水印配置
@@ -98,7 +102,22 @@ public class StyleConfig { @@ -98,7 +102,22 @@ public class StyleConfig {
98 public int watermarkImageMarginY = 20; 102 public int watermarkImageMarginY = 20;
99 // 图片水印的纵向偏移量(从右下角向内缩进的距离) 103 // 图片水印的纵向偏移量(从右下角向内缩进的距离)
100 104
  105 + //控制 两个二维码之间的横向间距。
  106 + public int qrSpacing = 100;
101 107
  108 + ///控制 logo_baise.png 和 第一个二维码之间的水平间距。
  109 + public int logoToQrSpacing = 40;
102 110
  111 + //第一个二维码
  112 + public String qrCodePath1 = "yujiangtang.png";
  113 +
  114 + //第二个二维码
  115 + public String qrCodePath2 = "huimaiyu.png";
  116 +
  117 + public int qrTextSpacing = 30; // 二维码下文字间距,建议 10
  118 + public int minBottomPadding = 30; // 底部留白,建议 20~40
  119 +
  120 + public int watermarkCount = 10; // 水印数量
  121 + public float watermarkAlpha = 0.3f; // 透明度(0~1)
103 } 122 }
104 123
@@ -228,6 +228,18 @@ public class ServerController extends BaseController @@ -228,6 +228,18 @@ public class ServerController extends BaseController
228 return AjaxResult.success(publicService.updateObject(fishAquaticPublicOpinion,"id")); 228 return AjaxResult.success(publicService.updateObject(fishAquaticPublicOpinion,"id"));
229 } 229 }
230 230
  231 + @ApiOperation("修改简介")
  232 + @PostMapping("/upBriefIntroduction/{id}")
  233 + public AjaxResult upBriefIntroduction(@PathVariable Integer id, String briefIntroduction,String shortTitle)
  234 + {
  235 +
  236 + FishAquaticPublicOpinion fishAquaticPublicOpinion = new FishAquaticPublicOpinion();
  237 + fishAquaticPublicOpinion.setId(id);
  238 + fishAquaticPublicOpinion.setBriefIntroduction(briefIntroduction);
  239 + fishAquaticPublicOpinion.setShortTitle(shortTitle);
  240 + return AjaxResult.success(publicService.updateObject(fishAquaticPublicOpinion,"id"));
  241 + }
  242 +
231 243
232 @ApiOperation("生成水产新闻海报") 244 @ApiOperation("生成水产新闻海报")
233 @PostMapping("/generateAquaticPublicOpinionPoster") 245 @PostMapping("/generateAquaticPublicOpinionPoster")