【慧游鲁博】【11】小程序端·游览画卷修改·支持图片url格式·结合图床上传和加载·数据对接

发布于:2025-06-19 ⋅ 阅读:(12) ⋅ 点赞:(0)

需求

由于图片和文字交流是相互独立的,故仅保留文字交互信息,然后根据文字中心词,匹配图床上的相应url,进行游览画卷构建

  1. 数据结构:前端传递给后端的是一个对象数组,每个对象包含:
    • description:文物/展品的文字描述(如"陶瓷"、“青铜器”)
    • imageUrl:与该描述对应的默认图片URL(如陶瓷描述对应陶瓷图片URL)
  2. 后端处理
    • 接收包含descriptionimageUrl的对象数组
    • 对每个对象:
      • 获取imageUrl对应的图片
      • 将图片和描述组合显示在画卷的同一个面板中(图片上方/下方显示对应文字)
  3. 展示效果:最终生成的画卷中,每个文物/展品都是一个图文结合的面板,而不是图片和文字分离显示

修改细节

前端

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';
}

主要修改点说明:

  1. 过滤非文字类型记录
    • 使用filter(record => record.type === 'text')只保留文字类型的交互记录
  2. 统一数据结构
    • 所有记录都设置为type: 'text'
    • content字段包含原始文字内容
    • imageUrl字段根据文字内容自动匹配默认图片
  3. 改进图片匹配逻辑
    • 使用对象映射方式匹配关键词和图片URL
    • 支持多个关键词匹配同一图片(如"佛像"和"佛教")
    • 提供默认图片URL作为后备
  4. 增强日志输出
    • 在发送请求前打印完整的数据结构,便于调试
  5. 错误处理
    • 保留原有的错误处理逻辑,确保用户体验

前端传递格式

[
    {
        "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. 直接使用前端传递的 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;
                }
            }
        }
    }
}

关键修改说明

  1. 图片加载方式
    • 移除对 InfoPanel.getImage() 的依赖
    • 新增 loadImageFromUrl() 方法,通过 HTTP 动态加载图片
    • 添加图片加载失败时的降级处理(显示占位符)
  2. 性能优化
    • 使用静态 OkHttpClient 复用连接
    • 图片按需加载,避免预先下载所有图片
  3. 错误处理
    • 捕获 IOException 并显示错误提示
    • 保持画卷生成流程不被单张图片失败中断
  4. 兼容性
    • 完全适配修改后的 InfoPanel 结构(imageUrl + text
    • 保留原有布局和样式逻辑

移除冗余代码

删除 ImageCropper 和本地图片裁剪逻辑。

修改总结

文件 原版本(本地文件) 修改版本(URL处理) 主要改动点
ScrollHorizontalRollComposer 直接使用BufferedImagepanels.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模型 存储BufferedImagejava<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. 精确边框对齐

数据流

前端 后端 图床 [{content:"青铜器", imageUrl:"..."},...] 下载图片 合成画卷 上传结果 返回画卷URL 前端 后端 图床

图片格式兼容性问题

  • 使用的图片是 .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

成果展示

测试版

在这里插入图片描述

最终版

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到