文章目錄
- 需求
- 修改細節
- 前端
- 主要修改點說明:
- 前端傳遞格式
- 后端
- 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
成果展示
測試版
最終版