文章目录
- 需求
- 修改细节
- 前端
- 主要修改点说明:
- 前端传递格式
- 后端
- ArtifactItem 类:
- ScrollServiceImpl 类:
- 修改 `InfoPanel` 结构
- 重构 `ScrollHorizontalRollComposer`
- 修改后的 `ScrollHorizontalRollComposer`
- 移除冗余代码
- 修改总结
- 数据流
- 图片格式兼容性问题
- 成果展示
需求
由于图片和文字交流是相互独立的,故仅保留文字交互信息,然后根据文字中心词,匹配图床上的相应url,进行游览画卷构建
- 数据结构:前端传递给后端的是一个对象数组,每个对象包含:
description
:文物/展品的文字描述(如"陶瓷"、“青铜器”)imageUrl
:与该描述对应的默认图片URL(如陶瓷描述对应陶瓷图片URL)
- 后端处理:
- 接收包含
description
和imageUrl
的对象数组 - 对每个对象:
- 获取
imageUrl
对应的图片 - 将图片和描述组合显示在画卷的同一个面板中(图片上方/下方显示对应文字)
- 获取
- 接收包含
- 展示效果:最终生成的画卷中,每个文物/展品都是一个图文结合的面板,而不是图片和文字分离显示
修改细节
前端
generateScroll()
async generateScroll() {try {// 禁用按钮防止重复点击this.generating = true;uni.showLoading({ title: '生成中...', mask: true });// 构建记录数据 - 只处理文字类型const records = this.interactionRecords.filter(record => record.type === 'text') // 只保留文字类型记录.map(record => ({type: 'text', // 强制设置为text类型content: record.content, // 文字内容imageUrl: this.getDefaultImageForText(record.content) // 根据内容匹配默认图片}));console.log('发送给后端的记录数据:', JSON.stringify(records, null, 2));// 调用后端接口const res = await post('/api/scroll/generate', records);if (!res) {throw new Error('未获取到有效响应');}// 预览生成的画卷uni.previewImage({current: res,urls: [res],success: () => {// 记录生成历史this.interactionRecords.push({type: 'scroll',content: '生成游览画卷',imageUrl: res,timestamp: new Date().getTime(),});},fail: (err) => {throw new Error('图片预览失败: ' + (err.errMsg || '未知错误'));},});} catch (error) {console.error('生成失败:', error);uni.showToast({title: '生成失败: ' + (error.message || '请稍后重试'),icon: 'none',duration: 2000,});} finally {this.generating = false;uni.hideLoading();}
},// 根据文本内容返回匹配的默认图片URL
getDefaultImageForText(text) {const defaultImages = {'佛像': 'https://i.ibb.co/fGH1bnHs/OIP-C-1.webp','佛教': 'https://i.ibb.co/fGH1bnHs/OIP-C-1.webp','陶瓷': 'https://i.ibb.co/R4kywTQs/OIP-C.webp','青铜器': 'https://i.ibb.co/fV1xCcYd/25bb-hyrtarw2279586.jpg','书画': 'https://example.com/default-painting.jpg', // 替换为实际URL'文物': 'https://example.com/default-artifact.jpg' // 替换为实际URL};// 查找匹配的关键词const matchedKey = Object.keys(defaultImages).find(key => text.includes(key));// 返回匹配的图片URL或默认URLreturn matchedKey ? defaultImages[matchedKey] : 'https://example.com/default-museum.jpg';
}
主要修改点说明:
- 过滤非文字类型记录:
- 使用
filter(record => record.type === 'text')
只保留文字类型的交互记录
- 使用
- 统一数据结构:
- 所有记录都设置为
type: 'text'
content
字段包含原始文字内容imageUrl
字段根据文字内容自动匹配默认图片
- 所有记录都设置为
- 改进图片匹配逻辑:
- 使用对象映射方式匹配关键词和图片URL
- 支持多个关键词匹配同一图片(如"佛像"和"佛教")
- 提供默认图片URL作为后备
- 增强日志输出:
- 在发送请求前打印完整的数据结构,便于调试
- 错误处理:
- 保留原有的错误处理逻辑,确保用户体验
前端传递格式
[{"type": "text","content": "这是第一段文字","imageUrl": "https://example.com/background1.jpg"},{"type": "text","content": "这是第二段文字","imageUrl": "https://example.com/background2.jpg"}
]
后端
ArtifactItem 类:
- 当前设计同时支持图片和文字类型,但如果只接受文字类型,可以简化这个类
- 可以移除
type
字段和imageUrl
字段,因为不再需要区分类型
public class ArtifactItem {private String content; // 只需要保留文字内容public String getContent() {return content;}public void setContent(String content) {this.content = content;}
}
ScrollServiceImpl 类:
generate()
方法中的处理逻辑可以简化,因为不再需要处理图片类型- 移除图片下载相关代码(因为现在传递的是图片url,而不是图片格式)
- 背景生成也需要调整
@Override
public String generate(List<ArtifactItem> records) throws Exception {List<InfoPanel> panels = new ArrayList<>();// 1. 生成背景(可选,如果仍需动态背景)BufferedImage bg = generateNewBackground();BufferedImage frame = loadResourceImage(FRAME_IMAGE_PATH);// 2. 直接使用前端传递的 imageUrlfor (ArtifactItem record : records) {if ("text".equals(record.getType())) {panels.add(new InfoPanel(record.getImageUrl(), record.getContent()));}}// 3. 修改 ScrollHorizontalRollComposer.compose() 方法// 现在它需要处理 URL 而不是 BufferedImageBufferedImage content = ScrollHorizontalRollComposer.compose(bg, panels);BufferedImage finalRoll = ScrollFramer.embed(content, frame);// 其余代码保持不变...return uploadToImageHost(finalRoll);
}
修改 InfoPanel
结构
- 从
BufferedImage image
改为String imageUrl
。
package com.museum.pojo;/** 拼画卷时用的“小面板”包装类 */
public class InfoPanel {private String imageUrl; // 改为存储图片URLprivate String text;public InfoPanel(String imageUrl, String text) {this.imageUrl = imageUrl;this.text = text;}public String getImageUrl() { return imageUrl; }public String getText() { return text; }
}
重构 ScrollHorizontalRollComposer
- 动态加载图片(
URLImageLoader.load()
)。 - 添加图片加载失败的降级处理(占位图)。
package com.museum.utils;import com.museum.pojo.InfoPanel;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.CubicCurve2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;public class ScrollHorizontalRollComposer {// 配置参数(保持不变)private static final int PANEL_WIDTH = 560;private static final int PANEL_HEIGHT = 400;private static final int PANEL_VGAP = 50;private static final int TOP_PADDING = 30;private static final int BOTTOM_PADDING = 30;private static final int CARD_MARGIN = 30;private static final int CARD_ROUND = 25;private static final int CARD_ALPHA = 190;private static final int ZIGZAG_OFFSET = 40;private static final int TEXT_PADDING = 40;private static final int FONT_SIZE = 22;private static final int IMAGE_SIZE = 180;// HTTP客户端(用于动态加载图片)private static final OkHttpClient httpClient = new OkHttpClient();public static BufferedImage compose(BufferedImage bg, List<InfoPanel> panels) {int panelCount = panels.size();int totalHeight = TOP_PADDING + BOTTOM_PADDING + panelCount * PANEL_HEIGHT + (panelCount - 1) * PANEL_VGAP;BufferedImage scroll = new BufferedImage(PANEL_WIDTH, totalHeight, BufferedImage.TYPE_INT_ARGB);Graphics2D g = scroll.createGraphics();// 1. 绘制背景(平铺)for (int y = 0; y < totalHeight; y += bg.getHeight()) {g.drawImage(bg, 0, y, PANEL_WIDTH, bg.getHeight(), null);}// 2. 设置字体和抗锯齿g.setFont(new Font("Serif", Font.PLAIN, FONT_SIZE));g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);FontMetrics fm = g.getFontMetrics();int lineHeight = fm.getHeight();// 3. 绘制每个面板int cursorY = TOP_PADDING;List<Point> centers = new ArrayList<>();for (int i = 0; i < panelCount; i++) {InfoPanel panel = panels.get(i);String[] txtLines = panel.getText().split("(?<=\\。)");// 3.1 计算面板位置(Z字型布局)int cardWidth = PANEL_WIDTH - 2 * CARD_MARGIN;int offsetX = (i % 2 == 0) ? ZIGZAG_OFFSET : -ZIGZAG_OFFSET;int cardX = (PANEL_WIDTH - cardWidth) / 2 + offsetX;// 3.2 绘制阴影和卡片背景g.setColor(new Color(0, 0, 0, 28));g.fillRoundRect(cardX + 5, cursorY + 5, cardWidth, PANEL_HEIGHT, CARD_ROUND, CARD_ROUND);g.setColor(new Color(255, 255, 255, CARD_ALPHA));g.fillRoundRect(cardX, cursorY, cardWidth, PANEL_HEIGHT, CARD_ROUND, CARD_ROUND);// 3.3 动态加载并绘制图片(关键修改点)try {BufferedImage img = loadImageFromUrl(panel.getImageUrl());int imgX = cardX + (cardWidth - IMAGE_SIZE) / 2;int imgY = cursorY + 30;g.drawImage(img, imgX, imgY, IMAGE_SIZE, IMAGE_SIZE, null);} catch (IOException e) {// 图片加载失败时绘制占位符g.setColor(Color.LIGHT_GRAY);g.fillRect(cardX + (cardWidth - IMAGE_SIZE)/2, cursorY + 30, IMAGE_SIZE, IMAGE_SIZE);g.setColor(Color.RED);g.drawString("图片加载失败", cardX + 20, cursorY + 60);}// 3.4 绘制文字g.setColor(Color.BLACK);int textX = cardX + TEXT_PADDING;int textY = cursorY + 30 + IMAGE_SIZE + 30;int textMaxWidth = cardWidth - 2 * TEXT_PADDING;drawWrappedText(g, txtLines, textX, textY, textMaxWidth, lineHeight);// 记录面板中心点(用于后续绘制连接线)centers.add(new Point(cardX + cardWidth/2, cursorY + PANEL_HEIGHT/2));cursorY += PANEL_HEIGHT + PANEL_VGAP;}// 4. 绘制面板间的连接线(保持不变)drawConnectingLines(g, centers);g.dispose();return scroll;}// 新增方法:从URL加载图片private static BufferedImage loadImageFromUrl(String imageUrl) throws IOException {Request request = new Request.Builder().url(imageUrl).build();try (Response response = httpClient.newCall(request).execute()) {if (!response.isSuccessful() || response.body() == null) {throw new IOException("HTTP " + response.code());}return ImageIO.read(response.body().byteStream());}}// 绘制连接线(保持不变)private static void drawConnectingLines(Graphics2D g, List<Point> centers) {g.setColor(new Color(90, 90, 90, 180));float[] dash = {10, 5};g.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 10f, dash, 0));for (int i = 0; i < centers.size() - 1; i++) {Point p1 = centers.get(i);Point p2 = centers.get(i + 1);int ctrlY = (p1.y + p2.y)/2 + 60 * ((i%2 == 0) ? 1 : -1);CubicCurve2D curve = new CubicCurve2D.Float(p1.x, p1.y, p1.x, ctrlY, p2.x, ctrlY, p2.x, p2.y);g.draw(curve);}}// 文字换行处理(优化版)private static void drawWrappedText(Graphics2D g, String[] lines, int x, int y, int maxWidth, int lineHeight) {FontMetrics fm = g.getFontMetrics();for (String line : lines) {if (fm.stringWidth(line) <= maxWidth) {g.drawString(line, x, y);y += lineHeight;} else {// 处理长文本换行StringBuilder currentLine = new StringBuilder();for (char c : line.toCharArray()) {if (fm.stringWidth(currentLine.toString() + c) > maxWidth) {g.drawString(currentLine.toString(), x, y);y += lineHeight;currentLine.setLength(0);}currentLine.append(c);}if (currentLine.length() > 0) {g.drawString(currentLine.toString(), x, y);y += lineHeight;}}}}
}
修改后的 ScrollHorizontalRollComposer
InfoPanel
改为存储图片 URL 而非 BufferedImage
,需要重构 ScrollHorizontalRollComposer
类
package com.museum.utils;import com.museum.pojo.InfoPanel;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.CubicCurve2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;public class ScrollHorizontalRollComposer {// 配置参数(保持不变)private static final int PANEL_WIDTH = 560;private static final int PANEL_HEIGHT = 400;private static final int PANEL_VGAP = 50;private static final int TOP_PADDING = 30;private static final int BOTTOM_PADDING = 30;private static final int CARD_MARGIN = 30;private static final int CARD_ROUND = 25;private static final int CARD_ALPHA = 190;private static final int ZIGZAG_OFFSET = 40;private static final int TEXT_PADDING = 40;private static final int FONT_SIZE = 22;private static final int IMAGE_SIZE = 180;// HTTP客户端(用于动态加载图片)private static final OkHttpClient httpClient = new OkHttpClient();public static BufferedImage compose(BufferedImage bg, List<InfoPanel> panels) {int panelCount = panels.size();int totalHeight = TOP_PADDING + BOTTOM_PADDING + panelCount * PANEL_HEIGHT + (panelCount - 1) * PANEL_VGAP;BufferedImage scroll = new BufferedImage(PANEL_WIDTH, totalHeight, BufferedImage.TYPE_INT_ARGB);Graphics2D g = scroll.createGraphics();// 1. 绘制背景(平铺)for (int y = 0; y < totalHeight; y += bg.getHeight()) {g.drawImage(bg, 0, y, PANEL_WIDTH, bg.getHeight(), null);}// 2. 设置字体和抗锯齿g.setFont(new Font("Serif", Font.PLAIN, FONT_SIZE));g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);FontMetrics fm = g.getFontMetrics();int lineHeight = fm.getHeight();// 3. 绘制每个面板int cursorY = TOP_PADDING;List<Point> centers = new ArrayList<>();for (int i = 0; i < panelCount; i++) {InfoPanel panel = panels.get(i);String[] txtLines = panel.getText().split("(?<=\\。)");// 3.1 计算面板位置(Z字型布局)int cardWidth = PANEL_WIDTH - 2 * CARD_MARGIN;int offsetX = (i % 2 == 0) ? ZIGZAG_OFFSET : -ZIGZAG_OFFSET;int cardX = (PANEL_WIDTH - cardWidth) / 2 + offsetX;// 3.2 绘制阴影和卡片背景g.setColor(new Color(0, 0, 0, 28));g.fillRoundRect(cardX + 5, cursorY + 5, cardWidth, PANEL_HEIGHT, CARD_ROUND, CARD_ROUND);g.setColor(new Color(255, 255, 255, CARD_ALPHA));g.fillRoundRect(cardX, cursorY, cardWidth, PANEL_HEIGHT, CARD_ROUND, CARD_ROUND);// 3.3 动态加载并绘制图片(关键修改点)try {BufferedImage img = loadImageFromUrl(panel.getImageUrl());int imgX = cardX + (cardWidth - IMAGE_SIZE) / 2;int imgY = cursorY + 30;g.drawImage(img, imgX, imgY, IMAGE_SIZE, IMAGE_SIZE, null);} catch (IOException e) {// 图片加载失败时绘制占位符g.setColor(Color.LIGHT_GRAY);g.fillRect(cardX + (cardWidth - IMAGE_SIZE)/2, cursorY + 30, IMAGE_SIZE, IMAGE_SIZE);g.setColor(Color.RED);g.drawString("图片加载失败", cardX + 20, cursorY + 60);}// 3.4 绘制文字g.setColor(Color.BLACK);int textX = cardX + TEXT_PADDING;int textY = cursorY + 30 + IMAGE_SIZE + 30;int textMaxWidth = cardWidth - 2 * TEXT_PADDING;drawWrappedText(g, txtLines, textX, textY, textMaxWidth, lineHeight);// 记录面板中心点(用于后续绘制连接线)centers.add(new Point(cardX + cardWidth/2, cursorY + PANEL_HEIGHT/2));cursorY += PANEL_HEIGHT + PANEL_VGAP;}// 4. 绘制面板间的连接线(保持不变)drawConnectingLines(g, centers);g.dispose();return scroll;}// 新增方法:从URL加载图片private static BufferedImage loadImageFromUrl(String imageUrl) throws IOException {Request request = new Request.Builder().url(imageUrl).build();try (Response response = httpClient.newCall(request).execute()) {if (!response.isSuccessful() || response.body() == null) {throw new IOException("HTTP " + response.code());}return ImageIO.read(response.body().byteStream());}}// 绘制连接线(保持不变)private static void drawConnectingLines(Graphics2D g, List<Point> centers) {g.setColor(new Color(90, 90, 90, 180));float[] dash = {10, 5};g.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 10f, dash, 0));for (int i = 0; i < centers.size() - 1; i++) {Point p1 = centers.get(i);Point p2 = centers.get(i + 1);int ctrlY = (p1.y + p2.y)/2 + 60 * ((i%2 == 0) ? 1 : -1);CubicCurve2D curve = new CubicCurve2D.Float(p1.x, p1.y, p1.x, ctrlY, p2.x, ctrlY, p2.x, p2.y);g.draw(curve);}}// 文字换行处理(优化版)private static void drawWrappedText(Graphics2D g, String[] lines, int x, int y, int maxWidth, int lineHeight) {FontMetrics fm = g.getFontMetrics();for (String line : lines) {if (fm.stringWidth(line) <= maxWidth) {g.drawString(line, x, y);y += lineHeight;} else {// 处理长文本换行StringBuilder currentLine = new StringBuilder();for (char c : line.toCharArray()) {if (fm.stringWidth(currentLine.toString() + c) > maxWidth) {g.drawString(currentLine.toString(), x, y);y += lineHeight;currentLine.setLength(0);}currentLine.append(c);}if (currentLine.length() > 0) {g.drawString(currentLine.toString(), x, y);y += lineHeight;}}}}
}
关键修改说明
- 图片加载方式:
- 移除对
InfoPanel.getImage()
的依赖 - 新增
loadImageFromUrl()
方法,通过 HTTP 动态加载图片 - 添加图片加载失败时的降级处理(显示占位符)
- 移除对
- 性能优化:
- 使用静态
OkHttpClient
复用连接 - 图片按需加载,避免预先下载所有图片
- 使用静态
- 错误处理:
- 捕获
IOException
并显示错误提示 - 保持画卷生成流程不被单张图片失败中断
- 捕获
- 兼容性:
- 完全适配修改后的
InfoPanel
结构(imageUrl
+text
) - 保留原有布局和样式逻辑
- 完全适配修改后的
移除冗余代码
删除 ImageCropper
和本地图片裁剪逻辑。
修改总结
文件 | 原版本(本地文件) | 修改版本(URL处理) | 主要改动点 |
---|---|---|---|
ScrollHorizontalRollComposer | 直接使用BufferedImage : panels.get(i).getImage() | 新增loadImageFromUrl() 方法: java<br>BufferedImage img = loadImageFromUrl(panel.getImageUrl());<br> 支持HTTP下载图片,失败时显示占位符 | 1. 通过URL动态加载图片 2. 使用OkHttpClient 3. 错误降级处理 |
ImageCropper | 仅支持文件路径输入: ImageIO.read(new File(path)) | 支持两种输入方式: java<br>// 方式1:URL转临时文件<br>crop(downloadToTemp(url), w, h);<br><br>// 方式2:直接处理BufferedImage<br>crop(bufferedImage, w, h);<br> | 1. 增加日志 2. 支持内存图像处理 3. 优化缩放插值 |
ScrollService | 处理MultipartFile 上传: java<br>multipartFile.transferTo(tempFile);<br>cropImageFile(tempFile...);<br> | 完全重构为URL处理: java<br>// 动态生成背景图<br>BufferedImage bg = generateNewBackground();<br><br>// 直接使用URL创建面板<br>panels.add(new InfoPanel(url, text));<br><br>// 自动上传结果到图床<br>uploadScrollToImageHost(finalRoll);<br> | 1. 移除文件上传逻辑 2. 新增DALL-E背景生成 3. 集成图床自动上传 |
InfoPanel模型 | 存储BufferedImage : java<br>private BufferedImage image;<br> | 改为存储图片URL: java<br>private String imageUrl; // 存储URL<br> | 模型层解耦图像存储 |
ScrollFramer | 简单居中嵌入: java<br>g.drawImage(content, x, y, null);<br> | 智能缩放+裁剪: java<br>// 计算缩放比例<br>double scale = innerH / content.getHeight();<br><br>// 水平居中裁剪<br>if (cropX > 0) {<br> content.getSubimage(cropX, 0, w, h);<br>}<br> | 1. 自适应内容尺寸 2. 精确边框对齐 |
数据流
图片格式兼容性问题
- 使用的图片是
.webp
格式,但 Java 原生ImageIO
不支持 WebP。 - 错误日志中
BufferedImage.getWidth() failed
表明图片已下载但无法解析。
解决方案:
引入 WebP 支持库
<dependency><groupId>com.twelvemonkeys.imageio</groupId><artifactId>imageio-webp</artifactId><version>3.9.4</version>
</dependency>
同时,上传的图床的照片格式尽量使jpg
成果展示
测试版
最终版