由于图片和文字交流是相互独立的,故仅保留文字交互信息,然后根据文字中心词,匹配图床上的相应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或默认URL
return matchedKey ? defaultImages[matchedKey] : 'https://example.com/default-museum.jpg';
}
filter(record => record.type === 'text')
只保留文字类型的交互记录type: 'text'
content
字段包含原始文字内容imageUrl
字段根据文字内容自动匹配默认图片[
{
"type": "text",
"content": "这是第一段文字",
"imageUrl": "https://example.com/background1.jpg"
},
{
"type": "text",
"content": "这是第二段文字",
"imageUrl": "https://example.com/background2.jpg"
}
]
type
字段和 imageUrl
字段,因为不再需要区分类型public class ArtifactItem {
private String content; // 只需要保留文字内容
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
generate()
方法中的处理逻辑可以简化,因为不再需要处理图片类型@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. 直接使用前端传递的 imageUrl
for (ArtifactItem record : records) {
if ("text".equals(record.getType())) {
panels.add(new InfoPanel(record.getImageUrl(), record.getContent()));
}
}
// 3. 修改 ScrollHorizontalRollComposer.compose() 方法
// 现在它需要处理 URL 而不是 BufferedImage
BufferedImage 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; // 改为存储图片URL
private 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 支持HTTP下载图片,失败时显示占位符 |
1. 通过URL动态加载图片 2. 使用OkHttpClient 3. 错误降级处理 |
ImageCropper | 仅支持文件路径输入: ImageIO.read(new File(path)) |
支持两种输入方式: java |
1. 增加日志 2. 支持内存图像处理 3. 优化缩放插值 |
ScrollService | 处理MultipartFile 上传: java |
完全重构为URL处理: java |
1. 移除文件上传逻辑 2. 新增DALL-E背景生成 3. 集成图床自动上传 |
InfoPanel模型 | 存储BufferedImage : java |
改为存储图片URL: java |
模型层解耦图像存储 |
ScrollFramer | 简单居中嵌入: java |
智能缩放+裁剪: java |
1. 自适应内容尺寸 2. 精确边框对齐 |
.webp
格式,但 Java 原生 ImageIO
不支持 WebP。BufferedImage.getWidth() failed
表明图片已下载但无法解析。解决方案:
引入 WebP 支持库
<dependency>
<groupId>com.twelvemonkeys.imageiogroupId>
<artifactId>imageio-webpartifactId>
<version>3.9.4version>
dependency>
同时,上传的图床的照片格式尽量使jpg
测试版
最终版