getPalette() {
- return Collections.unmodifiableMap(palette);
- }
-
- /**
- * 直接返回分割结果(SegmentationResult)
- */
- public SegmentationResult segment(File inputImage) throws Exception {
- return segmenter.segment(inputImage);
- }
-
- /**
- * 把指定 targets(标签名集合)从输入图片中分割并保存到 outDir。
- * 如果 targets 包含单个元素 "all"(忽略大小写),则保存所有标签。
- *
- * 返回值:Map,ResultFiles 包含 maskFile、overlayFile(两个 PNG)
- */
- public abstract Map segmentAndSave(File inputImage, Set targets, Path outDir) throws Exception;
-
- protected static String safeFileName(String s) {
- return s.replaceAll("[^a-zA-Z0-9_\\-\\.]", "_");
- }
-
- protected static Set parseTargetsSet(Set in) {
- if (in == null || in.isEmpty()) return Collections.emptySet();
- // 若包含单个 "all"
- if (in.size() == 1) {
- String only = in.iterator().next();
- if ("all".equalsIgnoreCase(only.trim())) {
- return Set.of("all");
- }
- }
- // 直接返回 trim 后的小写不变集合(保持用户传入的名字)
- Set out = new LinkedHashSet<>();
- for (String s : in) {
- if (s != null) out.add(s.trim());
- }
- return out;
- }
-
- /**
- * 关闭底层资源
- */
- @Override
- public void close() {
- try {
- segmenter.close();
- } catch (Exception ignore) {}
- }
-
- /* ================= helper: 从 modelDir 读取 synset.txt ================= */
-
- protected static Optional> loadLabelsFromSynset(Path modelDir) {
- Path syn = modelDir.resolve("synset.txt");
- if (Files.exists(syn)) {
- try {
- List lines = Files.readAllLines(syn);
- List cleaned = new ArrayList<>();
- for (String l : lines) {
- String s = l.trim();
- if (!s.isEmpty()) cleaned.add(s);
- }
- if (!cleaned.isEmpty()) return Optional.of(cleaned);
- } catch (IOException ignore) {}
- }
- return Optional.empty();
- }
-
- /**
- * 存放结果文件路径
- */
- public static class ResultFiles {
- private final File maskFile;
- private final File overlayFile;
-
- public ResultFiles(File maskFile, File overlayFile) {
- this.maskFile = maskFile;
- this.overlayFile = overlayFile;
- }
-
- public File getMaskFile() {
- return maskFile;
- }
-
- public File getOverlayFile() {
- return overlayFile;
- }
- }
-}
diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeLabelPalette.java b/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeLabelPalette.java
deleted file mode 100644
index efc3265..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeLabelPalette.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.chuangzhou.vivid2D.ai.anime_face_segmentation;
-
-import java.util.*;
-
-/**
- * Anime-Face-Segmentation UNet 模型的标签和颜色调色板。
- * 基于 Anime-Face-Segmentation 项目的 util.py 中的颜色定义。
- * 标签索引必须与模型输出索引一致(0-6)。
- */
-public class AnimeLabelPalette {
-
- /**
- * Anime-Face-Segmentation UNet 模型的标准标签(7个类别,索引 0-6)
- */
- public static List defaultLabels() {
- return Arrays.asList(
- "background", // 0 - 青色 (0,255,255)
- "hair", // 1 - 蓝色 (255,0,0)
- "eye", // 2 - 红色 (0,0,255)
- "mouth", // 3 - 白色 (255,255,255)
- "face", // 4 - 绿色 (0,255,0)
- "skin", // 5 - 黄色 (255,255,0)
- "clothes" // 6 - 紫色 (255,0,255)
- );
- }
-
- /**
- * 返回对应的调色板:类别名 -> ARGB 颜色值。
- * 颜色值基于 util.py 中的 PALETTE 数组的 RGB 值转换为 ARGB 格式 (0xFFRRGGBB)。
- */
- public static Map defaultPalette() {
- Map map = new HashMap<>();
- // 索引 0: background -> (0,255,255) 青色
- map.put("background", 0xFF00FFFF);
- // 索引 1: hair -> (255,0,0) 蓝色
- map.put("hair", 0xFFFF0000);
- // 索引 2: eye -> (0,0,255) 红色
- map.put("eye", 0xFF0000FF);
- // 索引 3: mouth -> (255,255,255) 白色
- map.put("mouth", 0xFFFFFFFF);
- // 索引 4: face -> (0,255,0) 绿色
- map.put("face", 0xFF00FF00);
- // 索引 5: skin -> (255,255,0) 黄色
- map.put("skin", 0xFFFFFF00);
- // 索引 6: clothes -> (255,0,255) 紫色
- map.put("clothes", 0xFFFF00FF);
-
- return map;
- }
-
- /**
- * 获取类别索引到名称的映射
- */
- public static Map getIndexToLabelMap() {
- List labels = defaultLabels();
- Map map = new HashMap<>();
- for (int i = 0; i < labels.size(); i++) {
- map.put(i, labels.get(i));
- }
- return map;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeModelWrapper.java b/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeModelWrapper.java
deleted file mode 100644
index f8a4a3f..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeModelWrapper.java
+++ /dev/null
@@ -1,322 +0,0 @@
-package com.chuangzhou.vivid2D.ai.anime_face_segmentation;
-
-import com.chuangzhou.vivid2D.ai.VividModelWrapper;
-
-import javax.imageio.ImageIO;
-import java.awt.*;
-import java.awt.image.BufferedImage;
-import java.io.File;
-import java.io.IOException;
-import java.lang.reflect.Method;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.*;
-import java.util.List;
-
-/**
- * AnimeModelWrapper - 专门为 Anime-Face-Segmentation 模型封装的 Wrapper
- */
-public class AnimeModelWrapper extends VividModelWrapper {
-
- private AnimeModelWrapper(AnimeSegmenter segmenter, List labels, Map palette) {
- super(segmenter, labels, palette);
- }
-
- /**
- * 加载模型
- */
- public static AnimeModelWrapper load(Path modelDir) throws Exception {
- List labels = loadLabelsFromSynset(modelDir).orElseGet(AnimeLabelPalette::defaultLabels);
- AnimeSegmenter segmenter = new AnimeSegmenter(modelDir, labels);
- Map palette = AnimeLabelPalette.defaultPalette();
- return new AnimeModelWrapper(segmenter, labels, palette);
- }
-
- /**
- * 直接返回分割结果(在丢给底层 segmenter 前会做通用预处理:RGB 转换 + 等比 letterbox 缩放到模型输入尺寸)
- */
- public AnimeSegmentationResult segment(File inputImage) throws Exception {
- File pre = null;
- try {
- pre = preprocessAndSave(inputImage);
- // 将预处理后的临时文件丢给底层 segmenter
- return segmenter.segment(pre);
- } finally {
- if (pre != null && pre.exists()) {
- try { Files.deleteIfExists(pre.toPath()); } catch (Exception ignore) {}
- }
- }
- }
-
- /**
- * 分割并保存结果
- */
- public Map segmentAndSave(File inputImage, Set targets, Path outDir) throws Exception {
- if (!Files.exists(outDir)) {
- Files.createDirectories(outDir);
- }
-
- AnimeSegmentationResult res = segment(inputImage);
- BufferedImage original = ImageIO.read(inputImage);
- BufferedImage maskImage = res.getMaskImage();
-
- int maskW = maskImage.getWidth();
- int maskH = maskImage.getHeight();
-
- // 解析 targets
- Set realTargets = parseTargetsSet(targets);
- Map saved = new LinkedHashMap<>();
-
- for (String target : realTargets) {
- if (!palette.containsKey(target)) {
- // 尝试忽略大小写匹配
- String finalTarget = target;
- Optional matched = palette.keySet().stream()
- .filter(k -> k.equalsIgnoreCase(finalTarget))
- .findFirst();
- if (matched.isPresent()) target = matched.get();
- else {
- System.err.println("Warning: unknown label '" + target + "' - skip.");
- continue;
- }
- }
-
- int targetColor = palette.get(target);
-
- // 1) 生成透明背景的二值掩码(只保留 target 像素)
- BufferedImage partMask = new BufferedImage(maskW, maskH, BufferedImage.TYPE_INT_ARGB);
- for (int y = 0; y < maskH; y++) {
- for (int x = 0; x < maskW; x++) {
- int c = maskImage.getRGB(x, y);
- if (c == targetColor) {
- partMask.setRGB(x, y, targetColor | 0xFF000000); // 保证不透明
- } else {
- partMask.setRGB(x, y, 0x00000000);
- }
- }
- }
-
- // 2) 将 mask 缩放到与原图一致(如果需要),并生成 overlay(半透明)
- BufferedImage maskResized = partMask;
- if (original.getWidth() != maskW || original.getHeight() != maskH) {
- maskResized = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
- Graphics2D g = maskResized.createGraphics();
- g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
- g.drawImage(partMask, 0, 0, original.getWidth(), original.getHeight(), null);
- g.dispose();
- }
-
- BufferedImage overlay = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
- Graphics2D g2 = overlay.createGraphics();
- g2.drawImage(original, 0, 0, null);
- // 半透明颜色(alpha = 0x88)
- int rgbOnly = (targetColor & 0x00FFFFFF);
- int translucent = (0x88 << 24) | rgbOnly;
- BufferedImage colorOverlay = new BufferedImage(overlay.getWidth(), overlay.getHeight(), BufferedImage.TYPE_INT_ARGB);
- for (int y = 0; y < colorOverlay.getHeight(); y++) {
- for (int x = 0; x < colorOverlay.getWidth(); x++) {
- int mc = maskResized.getRGB(x, y);
- if ((mc & 0x00FFFFFF) == (targetColor & 0x00FFFFFF) && ((mc >>> 24) != 0)) {
- colorOverlay.setRGB(x, y, translucent);
- } else {
- colorOverlay.setRGB(x, y, 0x00000000);
- }
- }
- }
- g2.drawImage(colorOverlay, 0, 0, null);
- g2.dispose();
-
- // 保存
- String safe = safeFileName(target);
- File maskOut = outDir.resolve(safe + "_mask.png").toFile();
- File overlayOut = outDir.resolve(safe + "_overlay.png").toFile();
-
- ImageIO.write(maskResized, "png", maskOut);
- ImageIO.write(overlay, "png", overlayOut);
-
- saved.put(target, new ResultFiles(maskOut, overlayOut));
- }
-
- return saved;
- }
-
- /**
- * 专门提取眼睛的方法(在丢给底层 segmenter 前做预处理)
- */
- public ResultFiles extractEyes(File inputImage, Path outDir) throws Exception {
- if (!Files.exists(outDir)) {
- Files.createDirectories(outDir);
- }
-
- File pre = null;
- BufferedImage eyes;
- try {
- pre = preprocessAndSave(inputImage);
- eyes = segmenter.extractEyes(pre);
- } finally {
- if (pre != null && pre.exists()) {
- try { Files.deleteIfExists(pre.toPath()); } catch (Exception ignore) {}
- }
- }
-
- File eyesMask = outDir.resolve("eyes_mask.png").toFile();
- ImageIO.write(eyes, "png", eyesMask);
-
- // 创建眼睛的 overlay(原有逻辑,保持不变)
- BufferedImage original = ImageIO.read(inputImage);
- BufferedImage overlay = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
- Graphics2D g2 = overlay.createGraphics();
- g2.drawImage(original, 0, 0, null);
-
- // 缩放眼睛掩码到原图尺寸
- BufferedImage eyesResized = eyes;
- if (original.getWidth() != eyes.getWidth() || original.getHeight() != eyes.getHeight()) {
- eyesResized = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
- Graphics2D g = eyesResized.createGraphics();
- g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
- g.drawImage(eyes, 0, 0, original.getWidth(), original.getHeight(), null);
- g.dispose();
- }
-
- int eyeColor = palette.getOrDefault("eye", 0xFF00FF); // 若没有 eye,给个显眼默认色
- int rgbOnly = (eyeColor & 0x00FFFFFF);
- int translucent = (0x88 << 24) | rgbOnly;
-
- BufferedImage colorOverlay = new BufferedImage(overlay.getWidth(), overlay.getHeight(), BufferedImage.TYPE_INT_ARGB);
- for (int y = 0; y < colorOverlay.getHeight(); y++) {
- for (int x = 0; x < colorOverlay.getWidth(); x++) {
- int mc = eyesResized.getRGB(x, y);
- if ((mc & 0x00FFFFFF) == (eyeColor & 0x00FFFFFF) && ((mc >>> 24) != 0)) {
- colorOverlay.setRGB(x, y, translucent);
- } else {
- colorOverlay.setRGB(x, y, 0x00000000);
- }
- }
- }
- g2.drawImage(colorOverlay, 0, 0, null);
- g2.dispose();
-
- File eyesOverlay = outDir.resolve("eyes_overlay.png").toFile();
- ImageIO.write(overlay, "png", eyesOverlay);
-
- return new ResultFiles(eyesMask, eyesOverlay);
- }
-
- /**
- * 关闭底层资源
- */
- @Override
- public void close() {
- try {
- segmenter.close();
- } catch (Exception ignore) {}
- }
-
- // ========== 新增:预处理并保存到临时文件 ==========
- private File preprocessAndSave(File inputImage) throws IOException {
- BufferedImage img = ImageIO.read(inputImage);
- if (img == null) throw new IOException("无法读取图片: " + inputImage);
-
- // 转成标准 RGB(去掉 alpha / 保证三通道)
- BufferedImage rgb = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB);
- Graphics2D g = rgb.createGraphics();
- g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
- g.drawImage(img, 0, 0, null);
- g.dispose();
-
- // 获取模型输入尺寸(尝试反射读取,找不到则使用默认 512x512)
- int[] size = getModelInputSize();
- int targetW = size[0], targetH = size[1];
-
- // 等比缩放并居中填充(letterbox),背景用白色
- double scale = Math.min((double) targetW / rgb.getWidth(), (double) targetH / rgb.getHeight());
- int newW = Math.max(1, (int) Math.round(rgb.getWidth() * scale));
- int newH = Math.max(1, (int) Math.round(rgb.getHeight() * scale));
-
- BufferedImage resized = new BufferedImage(targetW, targetH, BufferedImage.TYPE_INT_RGB);
- Graphics2D g2 = resized.createGraphics();
- g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
- g2.setColor(Color.WHITE);
- g2.fillRect(0, 0, targetW, targetH);
- int x = (targetW - newW) / 2;
- int y = (targetH - newH) / 2;
- g2.drawImage(rgb, x, y, newW, newH, null);
- g2.dispose();
-
- // 保存为临时 PNG 文件(确保无压缩失真)
- File tmp = Files.createTempFile("anime_pre_", ".png").toFile();
- ImageIO.write(resized, "png", tmp);
- return tmp;
- }
-
- // ========== 新增:尝试通过反射从 segmenter 上读取模型输入尺寸 ==========
- private int[] getModelInputSize() {
- // 默认值
- int defaultSize = 512;
- int w = defaultSize, h = defaultSize;
-
- try {
- Class> cls = segmenter.getClass();
-
- // 尝试方法 getInputWidth/getInputHeight
- try {
- Method mw = cls.getMethod("getInputWidth");
- Method mh = cls.getMethod("getInputHeight");
- Object ow = mw.invoke(segmenter);
- Object oh = mh.invoke(segmenter);
- if (ow instanceof Number && oh instanceof Number) {
- int iw = ((Number) ow).intValue();
- int ih = ((Number) oh).intValue();
- if (iw > 0 && ih > 0) {
- return new int[]{iw, ih};
- }
- }
- } catch (NoSuchMethodException ignored) {}
-
- // 尝试方法 getInputSize 返回 int[] 或 Dimension
- try {
- Method ms = cls.getMethod("getInputSize");
- Object os = ms.invoke(segmenter);
- if (os instanceof int[] && ((int[]) os).length >= 2) {
- int iw = ((int[]) os)[0];
- int ih = ((int[]) os)[1];
- if (iw > 0 && ih > 0) return new int[]{iw, ih};
- } else if (os != null) {
- // 处理 java.awt.Dimension
- try {
- Method gw = os.getClass().getMethod("getWidth");
- Method gh = os.getClass().getMethod("getHeight");
- Object ow2 = gw.invoke(os);
- Object oh2 = gh.invoke(os);
- if (ow2 instanceof Number && oh2 instanceof Number) {
- int iw = ((Number) ow2).intValue();
- int ih = ((Number) oh2).intValue();
- if (iw > 0 && ih > 0) return new int[]{iw, ih};
- }
- } catch (Exception ignored2) {}
- }
- } catch (NoSuchMethodException ignored) {}
-
- // 尝试字段 inputWidth/inputHeight
- try {
- try {
- java.lang.reflect.Field fw = cls.getDeclaredField("inputWidth");
- java.lang.reflect.Field fh = cls.getDeclaredField("inputHeight");
- fw.setAccessible(true); fh.setAccessible(true);
- Object ow = fw.get(segmenter);
- Object oh = fh.get(segmenter);
- if (ow instanceof Number && oh instanceof Number) {
- int iw = ((Number) ow).intValue();
- int ih = ((Number) oh).intValue();
- if (iw > 0 && ih > 0) return new int[]{iw, ih};
- }
- } catch (NoSuchFieldException ignoredField) {}
- } catch (Exception ignored) {}
-
- } catch (Exception ignored) {
- // 任何反射异常都回退到默认值
- }
-
- return new int[]{w, h};
- }
-}
diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeSegmentationResult.java b/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeSegmentationResult.java
deleted file mode 100644
index bb18069..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeSegmentationResult.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package com.chuangzhou.vivid2D.ai.anime_face_segmentation;
-
-import com.chuangzhou.vivid2D.ai.SegmentationResult;
-
-import java.awt.image.BufferedImage;
-import java.util.Map;
-
-/**
- * 动漫分割结果容器
- */
-public class AnimeSegmentationResult extends SegmentationResult {
- // 分割掩码图(每个像素的颜色为对应类别颜色)
- private final BufferedImage maskImage;
-
- // 分割概率图(每个像素的类别概率分布)
- private final float[][][] probabilityMap;
-
- // 类别索引 -> 类别名称
- private final Map labels;
-
- // 类别名称 -> ARGB 颜色
- private final Map palette;
-
- public AnimeSegmentationResult(BufferedImage maskImage, float[][][] probabilityMap,
- Map labels, Map palette) {
- super(maskImage, labels, palette);
- this.maskImage = maskImage;
- this.probabilityMap = probabilityMap;
- this.labels = labels;
- this.palette = palette;
- }
-
- public BufferedImage getMaskImage() {
- return maskImage;
- }
-
- public float[][][] getProbabilityMap() {
- return probabilityMap;
- }
-
- public Map getLabels() {
- return labels;
- }
-
- public Map getPalette() {
- return palette;
- }
-
- /**
- * 获取指定类别的概率图
- */
- public float[][] getClassProbability(int classIndex) {
- if (probabilityMap == null) return null;
- int height = probabilityMap.length;
- int width = probabilityMap[0].length;
- float[][] result = new float[height][width];
- for (int y = 0; y < height; y++) {
- for (int x = 0; x < width; x++) {
- result[y][x] = probabilityMap[y][x][classIndex];
- }
- }
- return result;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeSegmenter.java b/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeSegmenter.java
deleted file mode 100644
index ed773a2..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeSegmenter.java
+++ /dev/null
@@ -1,214 +0,0 @@
-package com.chuangzhou.vivid2D.ai.anime_face_segmentation;
-
-import ai.djl.MalformedModelException;
-import ai.djl.inference.Predictor;
-import ai.djl.modality.cv.Image;
-import ai.djl.modality.cv.ImageFactory;
-import ai.djl.ndarray.NDArray;
-import ai.djl.ndarray.NDList;
-import ai.djl.ndarray.NDManager;
-import ai.djl.ndarray.types.DataType;
-import ai.djl.ndarray.types.Shape;
-import ai.djl.repository.zoo.Criteria;
-import ai.djl.repository.zoo.ModelNotFoundException;
-import ai.djl.repository.zoo.ZooModel;
-import ai.djl.translate.Batchifier;
-import ai.djl.translate.TranslateException;
-import ai.djl.translate.Translator;
-import ai.djl.translate.TranslatorContext;
-import com.chuangzhou.vivid2D.ai.Segmenter;
-
-import javax.imageio.ImageIO;
-import java.awt.image.BufferedImage;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.*;
-
-/**
- * AnimeSegmenter: 专门为 Anime-Face-Segmentation UNet 模型设计的分割器
- */
-public class AnimeSegmenter extends Segmenter {
-
- // 模型默认输入大小(与训练时一致)。若模型不同可以修改为实际值或让 caller 通过构造参数传入。
- private static final int MODEL_INPUT_W = 512;
- private static final int MODEL_INPUT_H = 512;
-
- // 内部类,用于从Translator安全地传出数据
- public static class SegmentationData {
- final int[] indices; // 类别索引 [H * W]
- final float[][][] probMap; // 概率图 [H][W][C]
- final long[] shape; // 形状 [H, W]
-
- public SegmentationData(int[] indices, float[][][] probMap, long[] shape) {
- this.indices = indices;
- this.probMap = probMap;
- this.shape = shape;
- }
- }
-
- private final ZooModel modelWrapper;
- private final Predictor predictor;
- private final Map palette;
-
- public AnimeSegmenter(Path modelDir, List labels) throws IOException, MalformedModelException, ModelNotFoundException {
- super(modelDir, labels);
- this.palette = AnimeLabelPalette.defaultPalette();
-
- Translator translator = new Translator() {
- @Override
- public NDList processInput(TranslatorContext ctx, Image input) {
- NDManager manager = ctx.getNDManager();
-
- // 如果图片已经是模型输入大小则不再 resize(避免重复缩放导致失真)
- Image toUse = input;
- if (!(input.getWidth() == MODEL_INPUT_W && input.getHeight() == MODEL_INPUT_H)) {
- toUse = input.resize(MODEL_INPUT_W, MODEL_INPUT_H, true);
- }
-
- // 转换为NDArray并预处理
- NDArray array = toUse.toNDArray(manager);
- // DJL 返回 HWC 格式数组,转换为 CHW,并标准化到 [0,1]
- array = array.transpose(2, 0, 1) // HWC -> CHW
- .toType(DataType.FLOAT32, false)
- .div(255f) // 归一化到[0,1]
- .expandDims(0); // 添加batch维度 [1,3,H,W]
-
- return new NDList(array);
- }
-
- @Override
- public SegmentationData processOutput(TranslatorContext ctx, NDList list) {
- if (list == null || list.isEmpty()) {
- throw new IllegalStateException("Model did not return any output.");
- }
-
- NDArray output = list.get(0); // 期望形状 [1,C,H,W] 或 [1,C,W,H](以训练时一致为准)
-
- // 确保维度:把 output 视作 [1, C, H, W]
- Shape outShape = output.getShape();
- if (outShape.dimension() != 4) {
- throw new IllegalStateException("Unexpected output shape: " + outShape);
- }
-
- // 1. 获取类别索引(argmax) -> [H, W]
- NDArray squeezed = output.squeeze(0); // [C,H,W]
- NDArray classMap = squeezed.argMax(0).toType(DataType.INT32, false); // argMax over channel维度
-
- // 2. 获取概率图(softmax 输出或模型已经输出概率),转换为 [H,W,C]
- NDArray probabilities = squeezed.transpose(1, 2, 0) // [H,W,C]
- .toType(DataType.FLOAT32, false);
-
- // 3. 转换为Java数组
- long[] shape = classMap.getShape().getShape(); // [H, W]
- int[] indices = classMap.toIntArray();
- long[] probShape = probabilities.getShape().getShape(); // [H, W, C]
- int height = (int) probShape[0];
- int width = (int) probShape[1];
- int classes = (int) probShape[2];
- float[] flatProbMap = probabilities.toFloatArray();
- float[][][] probMap = new float[height][width][classes];
- for (int i = 0; i < height; i++) {
- for (int j = 0; j < width; j++) {
- for (int k = 0; k < classes; k++) {
- int index = i * width * classes + j * classes + k;
- probMap[i][j][k] = flatProbMap[index];
- }
- }
- }
-
- return new SegmentationData(indices, probMap, shape);
- }
-
- @Override
- public Batchifier getBatchifier() {
- return null;
- }
- };
-
- Criteria criteria = Criteria.builder()
- .setTypes(Image.class, SegmentationData.class)
- .optModelPath(modelDir)
- .optEngine("PyTorch")
- .optTranslator(translator)
- .build();
-
- this.modelWrapper = criteria.loadModel();
- this.predictor = modelWrapper.newPredictor();
- }
-
- @Override
- public NDList processInput(TranslatorContext ctx, Image input) {
- return null;
- }
-
- @Override
- public Segmenter.SegmentationData processOutput(TranslatorContext ctx, NDList list) {
- return null;
- }
-
- public AnimeSegmentationResult segment(File imgFile) throws TranslateException, IOException {
- Image img = ImageFactory.getInstance().fromFile(imgFile.toPath());
-
- // 预测并获取分割数据
- SegmentationData data = predictor.predict(img);
-
- long[] shp = data.shape;
- int[] indices = data.indices;
- float[][][] probMap = data.probMap;
-
- int height = (int) shp[0];
- int width = (int) shp[1];
-
- // 创建掩码图像
- BufferedImage mask = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
- Map labelsMap = AnimeLabelPalette.getIndexToLabelMap();
-
- for (int y = 0; y < height; y++) {
- for (int x = 0; x < width; x++) {
- int idx = indices[y * width + x];
- String label = labelsMap.getOrDefault(idx, "unknown");
- int argb = palette.getOrDefault(label, 0xFF00FF00); // 默认绿色
- mask.setRGB(x, y, argb);
- }
- }
-
- return new AnimeSegmentationResult(mask, probMap, labelsMap, palette);
- }
-
- /**
- * 专门针对眼睛的分割方法
- */
- public BufferedImage extractEyes(File imgFile) throws TranslateException, IOException {
- AnimeSegmentationResult result = segment(imgFile);
- BufferedImage mask = result.getMaskImage();
- BufferedImage eyeMask = new BufferedImage(mask.getWidth(), mask.getHeight(), BufferedImage.TYPE_INT_ARGB);
-
- int eyeColor = palette.get("eye");
-
- for (int y = 0; y < mask.getHeight(); y++) {
- for (int x = 0; x < mask.getWidth(); x++) {
- int rgb = mask.getRGB(x, y);
- if (rgb == eyeColor) {
- eyeMask.setRGB(x, y, eyeColor);
- } else {
- eyeMask.setRGB(x, y, 0x00000000); // 透明
- }
- }
- }
-
- return eyeMask;
- }
-
- @Override
- public void close() {
- try {
- predictor.close();
- } catch (Exception ignore) {
- }
- try {
- modelWrapper.close();
- } catch (Exception ignore) {
- }
- }
-}
diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2LabelPalette.java b/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2LabelPalette.java
deleted file mode 100644
index da9ed07..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2LabelPalette.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.chuangzhou.vivid2D.ai.anime_segmentation;
-
-import java.util.*;
-
-/**
- * 动漫分割模型的标签和颜色调色板。
- * 这是一个二分类模型:背景和前景(动漫人物)
- */
-public class Anime2LabelPalette {
-
- /**
- * 动漫分割模型的标准标签(2个类别)
- */
- public static List defaultLabels() {
- return Arrays.asList(
- "background", // 0
- "foreground" // 1
- );
- }
-
- /**
- * 返回动漫分割模型的调色板
- */
- public static Map defaultPalette() {
- Map map = new HashMap<>();
- // 索引 0: background - 黑色
- map.put("background", 0xFF000000);
- // 索引 1: foreground - 白色
- map.put("foreground", 0xFFFFFFFF);
-
- return map;
- }
-
- /**
- * 专门为动漫分割模型设计的调色板(可视化更友好)
- */
- public static Map animeSegmentationPalette() {
- Map map = new HashMap<>();
- // 背景 - 透明
- map.put("background", 0x00000000);
- // 前景 - 红色(用于可视化)
- map.put("foreground", 0xFFFF0000);
-
- return map;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2SegmentationResult.java b/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2SegmentationResult.java
deleted file mode 100644
index 6b36eba..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2SegmentationResult.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.chuangzhou.vivid2D.ai.anime_segmentation;
-
-import com.chuangzhou.vivid2D.ai.SegmentationResult;
-
-import java.awt.image.BufferedImage;
-import java.util.Map;
-
-/**
- * 动漫分割结果容器
- */
-public class Anime2SegmentationResult extends SegmentationResult {
- public Anime2SegmentationResult(BufferedImage maskImage, Map labels, Map palette) {
- super(maskImage, labels, palette);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2Segmenter.java b/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2Segmenter.java
deleted file mode 100644
index 10a969b..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2Segmenter.java
+++ /dev/null
@@ -1,106 +0,0 @@
-package com.chuangzhou.vivid2D.ai.anime_segmentation;
-
-import ai.djl.MalformedModelException;
-import ai.djl.modality.cv.Image;
-import ai.djl.modality.cv.ImageFactory;
-import ai.djl.ndarray.NDArray;
-import ai.djl.ndarray.NDList;
-import ai.djl.ndarray.NDManager;
-import ai.djl.ndarray.types.DataType;
-import ai.djl.repository.zoo.ModelNotFoundException;
-import ai.djl.translate.TranslateException;
-import ai.djl.translate.TranslatorContext;
-import com.chuangzhou.vivid2D.ai.SegmentationResult;
-import com.chuangzhou.vivid2D.ai.Segmenter;
-
-import javax.imageio.ImageIO;
-import java.awt.image.BufferedImage;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.*;
-
-/**
- * Anime2Segmenter: 专门用于动漫分割模型
- * 处理 anime-segmentation 模型的二值分割输出
- */
-public class Anime2Segmenter extends Segmenter {
- public Anime2Segmenter(Path modelDir, List labels) throws IOException, MalformedModelException, ModelNotFoundException {
- super(modelDir, labels);
- }
-
- @Override
- public NDList processInput(TranslatorContext ctx, Image input) {
- NDManager manager = ctx.getNDManager();
-
- // 调整输入图像尺寸到模型期望的大小 (1024x1024)
- Image resized = input.resize(1024, 1024, true);
- NDArray array = resized.toNDArray(manager);
-
- // 转换为 CHW 格式并归一化
- array = array.transpose(2, 0, 1).toType(DataType.FLOAT32, false);
- array = array.div(255f);
- array = array.expandDims(0); // 添加batch维度
-
- return new NDList(array);
- }
-
- @Override
- public SegmentationData processOutput(TranslatorContext ctx, NDList list) {
- if (list == null || list.isEmpty()) {
- throw new IllegalStateException("Model did not return any output.");
- }
- NDArray out = list.get(0);
- // 动漫分割模型输出形状: [1, 1, H, W] - 单通道概率图
- // 应用sigmoid并二值化
- NDArray probabilities = out.div(out.neg().exp().add(1));
- NDArray binaryMask = probabilities.gt(0.5).toType(DataType.INT32, false);
- if (binaryMask.getShape().dimension() == 4) {
- binaryMask = binaryMask.squeeze(0).squeeze(0);
- }
- long[] finalShape = binaryMask.getShape().getShape();
- int[] indices = binaryMask.toIntArray();
- return new SegmentationData(indices, finalShape);
- }
-
- @Override
- public SegmentationResult segment(File imgFile) throws TranslateException, IOException {
- Image img = ImageFactory.getInstance().fromFile(imgFile.toPath());
- Segmenter.SegmentationData data = predictor.predict(img);
- long[] shp = data.shape;
- int[] indices = data.indices;
- int height, width;
- if (shp.length == 2) {
- height = (int) shp[0];
- width = (int) shp[1];
- } else {
- throw new RuntimeException("Unexpected classMap shape from SegmentationData: " + Arrays.toString(shp));
- }
- BufferedImage mask = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
- Map labelsMap = new HashMap<>();
- for (int i = 0; i < labels.size(); i++) {
- labelsMap.put(i, labels.get(i));
- }
- for (int y = 0; y < height; y++) {
- for (int x = 0; x < width; x++) {
- int idx = indices[y * width + x];
- String label = labelsMap.getOrDefault(idx, "unknown");
- int argb = palette.getOrDefault(label, 0xFFFF0000);
- mask.setRGB(x, y, argb);
- }
- }
- return new SegmentationResult(mask, labelsMap, palette);
- }
-
- @Override
- public void close() {
- try {
- predictor.close();
- } catch (Exception ignore) {
- }
- try {
- modelWrapper.close();
- } catch (Exception ignore) {
- }
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2VividModelWrapper.java b/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2VividModelWrapper.java
deleted file mode 100644
index b76a9be..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2VividModelWrapper.java
+++ /dev/null
@@ -1,154 +0,0 @@
-package com.chuangzhou.vivid2D.ai.anime_segmentation;
-
-import com.chuangzhou.vivid2D.ai.SegmentationResult;
-import com.chuangzhou.vivid2D.ai.VividModelWrapper;
-
-import javax.imageio.ImageIO;
-import java.awt.*;
-import java.awt.image.BufferedImage;
-import java.io.*;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.*;
-import java.util.List;
-
-/**
- * Anime2VividModelWrapper - 对之前 Anime2Segmenter 的封装,提供更便捷的API
- *
- * 用法示例:
- * Anime2VividModelWrapper wrapper = Anime2VividModelWrapper.load(Paths.get("/path/to/modelDir"));
- * Map out = wrapper.segmentAndSave(
- * new File("input.jpg"),
- * Set.of("foreground"), // 动漫分割主要关注前景
- * Paths.get("outDir")
- * );
- * // out contains 每个目标标签对应的 mask+overlay 文件路径
- * wrapper.close();
- */
-public class Anime2VividModelWrapper extends VividModelWrapper {
-
- private Anime2VividModelWrapper(Anime2Segmenter segmenter, List labels, Map palette) {
- super(segmenter, labels, palette);
-
- }
-
- /**
- * 读取 modelDir/synset.txt(每行一个标签),若不存在则使用 Anime2LabelPalette.defaultLabels()
- * 并创建 Anime2Segmenter 实例。
- */
- public static Anime2VividModelWrapper load(Path modelDir) throws Exception {
- List labels = loadLabelsFromSynset(modelDir).orElseGet(Anime2LabelPalette::defaultLabels);
- Anime2Segmenter s = new Anime2Segmenter(modelDir, labels);
- Map palette = Anime2LabelPalette.animeSegmentationPalette();
- return new Anime2VividModelWrapper(s, labels, palette);
- }
-
- public List getLabels() {
- return Collections.unmodifiableList(labels);
- }
-
- public Map getPalette() {
- return Collections.unmodifiableMap(palette);
- }
-
- /**
- * 直接返回分割结果(Anime2SegmentationResult)
- */
- public SegmentationResult segment(File inputImage) throws Exception {
- return segmenter.segment(inputImage);
- }
-
- /**
- * 把指定 targets(标签名集合)从输入图片中分割并保存到 outDir。
- * 如果 targets 包含单个元素 "all"(忽略大小写),则保存所有标签。
- *
- * 返回值:Map,ResultFiles 包含 maskFile、overlayFile(两个 PNG)
- */
- public Map segmentAndSave(File inputImage, Set targets, Path outDir) throws Exception {
- if (!Files.exists(outDir)) {
- Files.createDirectories(outDir);
- }
-
- SegmentationResult res = segment(inputImage);
- BufferedImage original = ImageIO.read(inputImage);
- BufferedImage maskImage = res.getMaskImage();
-
- int maskW = maskImage.getWidth();
- int maskH = maskImage.getHeight();
-
- // 解析 targets
- Set realTargets = parseTargetsSet(targets);
- Map saved = new LinkedHashMap<>();
-
- for (String target : realTargets) {
- if (!palette.containsKey(target)) {
- String finalTarget = target;
- Optional matched = palette.keySet().stream()
- .filter(k -> k.equalsIgnoreCase(finalTarget))
- .findFirst();
- if (matched.isPresent()) target = matched.get();
- else {
- System.err.println("Warning: unknown label '" + target + "' - skip.");
- continue;
- }
- }
-
- int targetColor = palette.get(target);
-
- // 1) 生成透明背景的二值掩码(只保留 target 像素)
- BufferedImage partMask = new BufferedImage(maskW, maskH, BufferedImage.TYPE_INT_ARGB);
- for (int y = 0; y < maskH; y++) {
- for (int x = 0; x < maskW; x++) {
- int c = maskImage.getRGB(x, y);
- if (c == targetColor) {
- partMask.setRGB(x, y, targetColor | 0xFF000000); // 保证不透明
- } else {
- partMask.setRGB(x, y, 0x00000000);
- }
- }
- }
-
- // 2) 将 mask 缩放到与原图一致(如果需要),并生成 overlay(半透明)
- BufferedImage maskResized = partMask;
- if (original.getWidth() != maskW || original.getHeight() != maskH) {
- maskResized = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
- Graphics2D g = maskResized.createGraphics();
- g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
- g.drawImage(partMask, 0, 0, original.getWidth(), original.getHeight(), null);
- g.dispose();
- }
-
- BufferedImage overlay = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
- Graphics2D g2 = overlay.createGraphics();
- g2.drawImage(original, 0, 0, null);
- // 半透明颜色(alpha = 0x88)
- int rgbOnly = (targetColor & 0x00FFFFFF);
- int translucent = (0x88 << 24) | rgbOnly;
- BufferedImage colorOverlay = new BufferedImage(overlay.getWidth(), overlay.getHeight(), BufferedImage.TYPE_INT_ARGB);
- for (int y = 0; y < colorOverlay.getHeight(); y++) {
- for (int x = 0; x < colorOverlay.getWidth(); x++) {
- int mc = maskResized.getRGB(x, y);
- if ((mc & 0x00FFFFFF) == (targetColor & 0x00FFFFFF) && ((mc >>> 24) != 0)) {
- colorOverlay.setRGB(x, y, translucent);
- } else {
- colorOverlay.setRGB(x, y, 0x00000000);
- }
- }
- }
- g2.drawImage(colorOverlay, 0, 0, null);
- g2.dispose();
-
- // 保存
- String safe = safeFileName(target);
- File maskOut = outDir.resolve(safe + "_mask.png").toFile();
- File overlayOut = outDir.resolve(safe + "_overlay.png").toFile();
-
- ImageIO.write(maskResized, "png", maskOut);
- ImageIO.write(overlay, "png", overlayOut);
-
- saved.put(target, new ResultFiles(maskOut, overlayOut));
- }
-
- return saved;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetLabelPalette.java b/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetLabelPalette.java
deleted file mode 100644
index c4c25f5..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetLabelPalette.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package com.chuangzhou.vivid2D.ai.face_parsing;
-
-import java.util.*;
-
-/**
- * BiSeNet 人脸解析模型的标准标签和颜色调色板。
- * 颜色值基于 zllrunning/face-parsing.PyTorch 仓库的 test.py 文件。
- * 标签索引必须与模型输出索引一致(0-18)。
- */
-public class BiSeNetLabelPalette {
-
- /**
- * BiSeNet 人脸解析模型的标准标签(19个类别,索引 0-18)
- */
- public static List defaultLabels() {
- return Arrays.asList(
- "background", // 0
- "skin", // 1
- "nose", // 2
- "eye_left", // 3
- "eye_right", // 4
- "eyebrow_left", // 5
- "eyebrow_right",// 6
- "ear_left", // 7
- "ear_right", // 8
- "mouth", // 9
- "lip_upper", // 10
- "lip_lower", // 11
- "hair", // 12
- "hat", // 13
- "earring", // 14
- "necklace", // 15
- "clothes", // 16
- "facial_hair",// 17
- "neck" // 18
- );
- }
-
- /**
- * 返回一个对应的调色板:类别名 -> ARGB 颜色值。
- * 颜色值基于 test.py 中 part_colors 数组的 RGB 值转换为 ARGB 格式 (0xFFRRGGBB)。
- */
- public static Map defaultPalette() {
- Map map = new HashMap<>();
- // 索引 0: background
- map.put("background", 0xFF000000); // 黑色
-
- // 索引 1-18: 对应 part_colors 数组的前 18 个颜色
- // 注意:这里假设 part_colors[i-1] 对应 索引 i 的标签。
- // 索引 1: skin -> [255, 0, 0]
- map.put("skin", 0xFFFF0000);
- // 索引 2: nose -> [255, 85, 0]
- map.put("nose", 0xFFFF5500);
- // 索引 3: eye_left -> [255, 170, 0]
- map.put("eye_left", 0xFFFFAA00);
- // 索引 4: eye_right -> [255, 0, 85]
- map.put("eye_right", 0xFFFF0055);
- // 索引 5: eyebrow_left -> [255, 0, 170]
- map.put("eyebrow_left",0xFFFF00AA);
- // 索引 6: eyebrow_right -> [0, 255, 0]
- map.put("eyebrow_right",0xFF00FF00);
- // 索引 7: ear_left -> [85, 255, 0]
- map.put("ear_left", 0xFF55FF00);
- // 索引 8: ear_right -> [170, 255, 0]
- map.put("ear_right", 0xFFAAFF00);
- // 索引 9: mouth -> [0, 255, 85]
- map.put("mouth", 0xFF00FF55);
- // 索引 10: lip_upper -> [0, 255, 170]
- map.put("lip_upper", 0xFF00FFAA);
- // 索引 11: lip_lower -> [0, 0, 255]
- map.put("lip_lower", 0xFF0000FF);
- // 索引 12: hair -> [85, 0, 255]
- map.put("hair", 0xFF5500FF);
- // 索引 13: hat -> [170, 0, 255]
- map.put("hat", 0xFFAA00FF);
- // 索引 14: earring -> [0, 85, 255]
- map.put("earring", 0xFF0055FF);
- // 索引 15: necklace -> [0, 170, 255]
- map.put("necklace", 0xFF00AAFF);
- // 索引 16: clothes -> [255, 255, 0]
- map.put("clothes", 0xFFFFFF00);
- // 索引 17: facial_hair -> [255, 85, 85]
- map.put("facial_hair", 0xFFFF5555);
- // 索引 18: neck -> [255, 170, 170]
- map.put("neck", 0xFFFFAAAA);
-
- return map;
- }
-}
diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetSegmentationResult.java b/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetSegmentationResult.java
deleted file mode 100644
index 5e634ac..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetSegmentationResult.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.chuangzhou.vivid2D.ai.face_parsing;
-
-import com.chuangzhou.vivid2D.ai.SegmentationResult;
-
-import java.awt.image.BufferedImage;
-import java.util.Map;
-
-/**
- * 分割结果容器
- */
-public class BiSeNetSegmentationResult extends SegmentationResult {
- public BiSeNetSegmentationResult(BufferedImage maskImage, Map labels, Map palette) {
- super(maskImage, labels, palette);
- }
-}
diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetSegmenter.java b/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetSegmenter.java
deleted file mode 100644
index 30c71e3..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetSegmenter.java
+++ /dev/null
@@ -1,101 +0,0 @@
-package com.chuangzhou.vivid2D.ai.face_parsing;
-
-import ai.djl.MalformedModelException;
-import ai.djl.inference.Predictor;
-import ai.djl.modality.cv.Image;
-import ai.djl.modality.cv.ImageFactory;
-import ai.djl.ndarray.NDArray;
-import ai.djl.ndarray.NDList;
-import ai.djl.ndarray.NDManager;
-import ai.djl.ndarray.types.DataType;
-import ai.djl.repository.zoo.Criteria;
-import ai.djl.repository.zoo.ModelNotFoundException;
-import ai.djl.repository.zoo.ZooModel;
-import ai.djl.translate.Batchifier;
-import ai.djl.translate.TranslateException;
-import ai.djl.translate.Translator;
-import ai.djl.translate.TranslatorContext;
-import com.chuangzhou.vivid2D.ai.SegmentationResult;
-import com.chuangzhou.vivid2D.ai.Segmenter;
-
-import javax.imageio.ImageIO;
-import java.awt.image.BufferedImage;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.*;
-
-/**
- * Segmenter: 加载模型并对图片做语义分割
- *
- * 说明:
- * - Translator.processOutput 在翻译器层就把模型输出处理成 (H, W) 的类别索引 NDArray,
- * 并把该 NDArray 拷贝到 persistentManager 中返回,从而避免后续 native 资源被释放的问题。
- * - 这里改为在 Translator 内部把 classMap 转为 Java int[](通过 classMap.toIntArray()),
- * 再用 persistentManager.create(int[], shape) 创建新的 NDArray 返回,确保安全。
- */
-public class BiSeNetSegmenter extends Segmenter {
-
- public BiSeNetSegmenter(Path modelDir, List labels) throws IOException, MalformedModelException, ModelNotFoundException {
- super(modelDir, labels);
- }
-
- @Override
- public NDList processInput(TranslatorContext ctx, Image input) {
- NDManager manager = ctx.getNDManager();
- NDArray array = input.toNDArray(manager);
- array = array.transpose(2, 0, 1).toType(DataType.FLOAT32, false);
- array = array.div(255f);
- array = array.expandDims(0);
- return new NDList(array);
- }
-
- @Override
- public Segmenter.SegmentationData processOutput(TranslatorContext ctx, NDList list) {
- if (list == null || list.isEmpty()) {
- throw new IllegalStateException("Model did not return any output.");
- }
-
- NDArray out = list.get(0);
- NDArray classMap;
-
- // 1. 解析模型输出,得到类别图谱 (classMap)
- long[] shape = out.getShape().getShape();
- if (shape.length == 4 && shape[1] > 1) {
- classMap = out.argMax(1);
- } else if (shape.length == 3) {
- classMap = (shape[0] == 1) ? out : out.argMax(0);
- } else if (shape.length == 2) {
- classMap = out;
- } else {
- throw new IllegalStateException("Unexpected output shape: " + Arrays.toString(shape));
- }
-
- if (classMap.getShape().dimension() == 3) {
- classMap = classMap.squeeze(0);
- }
-
- // 2. *** 关键步骤 ***
- // 在 NDArray 仍然有效的上下文中,将其转换为 Java 原生类型
-
- // 首先,确保数据类型是 INT32
- NDArray int32ClassMap = classMap.toType(DataType.INT32, false);
-
- // 然后,获取形状和 int[] 数组
- long[] finalShape = int32ClassMap.getShape().getShape();
- int[] indices = int32ClassMap.toIntArray();
-
- // 3. 将 Java 对象封装并返回
- return new SegmentationData(indices, finalShape);
- }
-
- @Override
- public SegmentationResult segment(File imgFile) throws TranslateException, IOException {
- return super.segment(imgFile);
- }
-
- @Override
- public void close() {
- super.close();
- }
-}
diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetVividModelWrapper.java b/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetVividModelWrapper.java
deleted file mode 100644
index 7f2e393..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetVividModelWrapper.java
+++ /dev/null
@@ -1,147 +0,0 @@
-package com.chuangzhou.vivid2D.ai.face_parsing;
-
-import com.chuangzhou.vivid2D.ai.SegmentationResult;
-import com.chuangzhou.vivid2D.ai.VividModelWrapper;
-
-import javax.imageio.ImageIO;
-import java.awt.*;
-import java.awt.image.BufferedImage;
-import java.io.*;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.*;
-import java.util.List;
-
-/**
- * VividModelWrapper - 对之前 Segmenter / SegmenterExample 的封装
- *
- * 用法示例:
- * VividModelWrapper wrapper = VividModelWrapper.load(Paths.get("/path/to/modelDir"));
- * Map out = wrapper.segmentAndSave(
- * new File("input.jpg"),
- * Set.of("eye","face"), // 或 Set.of(all labels...);若想全部传 "all" 可以用 helper parseTargets
- * Paths.get("outDir")
- * );
- * // out contains 每个目标标签对应的 mask+overlay 文件路径
- * wrapper.close();
- */
-public class BiSeNetVividModelWrapper extends VividModelWrapper {
-
- private BiSeNetVividModelWrapper(BiSeNetSegmenter segmenter, List labels, Map palette) {
- super(segmenter, labels, palette);
- }
-
- /**
- * 读取 modelDir/synset.txt(每行一个标签),若不存在则使用 LabelPalette.defaultLabels()
- * 并创建 Segmenter 实例。
- */
- public static BiSeNetVividModelWrapper load(Path modelDir) throws Exception {
- List labels = loadLabelsFromSynset(modelDir).orElseGet(BiSeNetLabelPalette::defaultLabels);
- BiSeNetSegmenter s = new BiSeNetSegmenter(modelDir, labels);
- Map palette = BiSeNetLabelPalette.defaultPalette();
- return new BiSeNetVividModelWrapper(s, labels, palette);
- }
-
- public List getLabels() {
- return super.getLabels();
- }
-
- public Map getPalette() {
- return super.getPalette();
- }
-
- /**
- * 直接返回分割结果(SegmentationResult)
- */
- public SegmentationResult segment(File inputImage) throws Exception {
- return segmenter.segment(inputImage);
- }
-
- /**
- * 把指定 targets(标签名集合)从输入图片中分割并保存到 outDir。
- * 如果 targets 包含单个元素 "all"(忽略大小写),则保存所有标签。
- *
- * 返回值:Map,ResultFiles 包含 maskFile、overlayFile(两个 PNG)
- */
- public Map segmentAndSave(File inputImage, Set targets, Path outDir) throws Exception {
- if (!Files.exists(outDir)) {
- Files.createDirectories(outDir);
- }
- SegmentationResult res = segment(inputImage);
- BufferedImage original = ImageIO.read(inputImage);
- BufferedImage maskImage = res.getMaskImage();
- int maskW = maskImage.getWidth();
- int maskH = maskImage.getHeight();
- Set realTargets = parseTargetsSet(targets);
- Map saved = new LinkedHashMap<>();
- for (String target : realTargets) {
- if (!palette.containsKey(target)) {
- String finalTarget = target;
- Optional matched = palette.keySet().stream()
- .filter(k -> k.equalsIgnoreCase(finalTarget))
- .findFirst();
- if (matched.isPresent()) target = matched.get();
- else {
- System.err.println("Warning: unknown label '" + target + "' - skip.");
- continue;
- }
- }
- int targetColor = palette.get(target);
- BufferedImage partMask = new BufferedImage(maskW, maskH, BufferedImage.TYPE_INT_ARGB);
- for (int y = 0; y < maskH; y++) {
- for (int x = 0; x < maskW; x++) {
- int c = maskImage.getRGB(x, y);
- if (c == targetColor) {
- partMask.setRGB(x, y, targetColor | 0xFF000000); // 保证不透明
- } else {
- partMask.setRGB(x, y, 0x00000000);
- }
- }
- }
- BufferedImage maskResized = partMask;
- if (original.getWidth() != maskW || original.getHeight() != maskH) {
- maskResized = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
- Graphics2D g = maskResized.createGraphics();
- g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
- g.drawImage(partMask, 0, 0, original.getWidth(), original.getHeight(), null);
- g.dispose();
- }
- BufferedImage overlay = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
- Graphics2D g2 = overlay.createGraphics();
- g2.drawImage(original, 0, 0, null);
- int rgbOnly = (targetColor & 0x00FFFFFF);
- int translucent = (0x88 << 24) | rgbOnly;
- BufferedImage colorOverlay = new BufferedImage(overlay.getWidth(), overlay.getHeight(), BufferedImage.TYPE_INT_ARGB);
- for (int y = 0; y < colorOverlay.getHeight(); y++) {
- for (int x = 0; x < colorOverlay.getWidth(); x++) {
- int mc = maskResized.getRGB(x, y);
- if ((mc & 0x00FFFFFF) == (targetColor & 0x00FFFFFF) && ((mc >>> 24) != 0)) {
- colorOverlay.setRGB(x, y, translucent);
- } else {
- colorOverlay.setRGB(x, y, 0x00000000);
- }
- }
- }
- g2.drawImage(colorOverlay, 0, 0, null);
- g2.dispose();
- String safe = safeFileName(target);
- File maskOut = outDir.resolve(safe + "_mask.png").toFile();
- File overlayOut = outDir.resolve(safe + "_overlay.png").toFile();
- ImageIO.write(maskResized, "png", maskOut);
- ImageIO.write(overlay, "png", overlayOut);
- saved.put(target, new ResultFiles(maskOut, overlayOut));
- }
-
- return saved;
- }
-
- /**
- * 关闭底层资源
- */
- @Override
- public void close() {
- try {
- segmenter.close();
- } catch (Exception ignore) {}
- }
-}
diff --git a/src/main/java/com/chuangzhou/vivid2D/block/BlocklyMessageHandler.java b/src/main/java/com/chuangzhou/vivid2D/block/BlocklyMessageHandler.java
deleted file mode 100644
index c9aa788..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/block/BlocklyMessageHandler.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package com.chuangzhou.vivid2D.block;
-
-import com.google.gson.Gson;
-import com.google.gson.JsonSyntaxException;
-
-/**
- * 一个专门用于处理来自Blockly前端的JSON消息的处理器。
- * 它负责解析请求,并将其分派给Vivid2DRendererBridge中的相应方法。
- */
-public class BlocklyMessageHandler {
-
- private final Vivid2DRendererBridge rendererBridge = new Vivid2DRendererBridge();
- private final Gson gson = new Gson();
-
- /**
- * 用于Gson解析JSON请求的内部数据结构类。
- * 它必须与JavaScript中构建的request对象结构匹配。
- * { "action": "...", "params": { ... } }
- */
- private static class RequestData {
- String action;
- java.util.Map params;
- }
-
- /**
- * 尝试处理传入的请求字符串。
- *
- * @param request 从JavaScript的onQuery回调中接收到的原始字符串。
- * @return 如果请求被成功识别并处理,则返回 true;否则返回 false。
- */
- public boolean handle(String request) {
- try {
- // 1. 尝试将请求字符串解析为我们预定义的RequestData结构
- RequestData data = gson.fromJson(request, RequestData.class);
-
- // 2. 验证解析结果。如果不是有效的JSON或缺少action字段,则这不是我们能处理的请求。
- if (data == null || data.action == null) {
- return false;
- }
-
- // 3. 使用 switch 语句根据 action 的值将请求分派到不同的处理方法
- switch (data.action) {
- case "moveObject":
- // 从参数Map中提取所需数据
- String objectIdMove = data.params.get("objectId").toString();
- // JSON数字默认被Gson解析为Double,需要转换
- int x = ((Double) data.params.get("x")).intValue();
- int y = ((Double) data.params.get("y")).intValue();
-
- // 调用实际的业务逻辑
- rendererBridge.moveObject(objectIdMove, x, y);
-
- // 表示我们已经成功处理了这个请求
- return true;
-
- case "changeColor":
- // 从参数Map中提取所需数据
- String objectIdColor = data.params.get("objectId").toString();
- String colorHex = data.params.get("colorHex").toString();
-
- // 调用实际的业务逻辑
- rendererBridge.changeColor(objectIdColor, colorHex);
-
- // 表示我们已经成功处理了这个请求
- return true;
-
- // 在这里可以为未来新的积木添加更多的 case ...
-
- default:
- // 请求是合法的JSON,但action是我们不认识的。记录一下,但不处理。
- System.err.println("BlocklyMessageHandler: 收到一个未知的操作(action): " + data.action);
- return false;
- }
- } catch (JsonSyntaxException | ClassCastException | NullPointerException e) {
- // 如果发生以下情况,说明这个请求不是我们想要的格式:
- // - JsonSyntaxException: 字符串不是一个有效的JSON。
- // - ClassCastException: JSON中的数据类型与我们预期的不符(例如,x坐标是字符串)。
- // - NullPointerException: 缺少必要的参数(例如,params中没有"x"这个键)。
- // 在这些情况下,我们静默地失败并返回false,让其他处理器有机会处理这个请求。
- return false;
- }
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/chuangzhou/vivid2D/block/BlocklyPanel.java b/src/main/java/com/chuangzhou/vivid2D/block/BlocklyPanel.java
deleted file mode 100644
index df71e16..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/block/BlocklyPanel.java
+++ /dev/null
@@ -1,97 +0,0 @@
-package com.chuangzhou.vivid2D.block;
-
-import com.axis.innovators.box.browser.CefAppManager;
-import me.friwi.jcefmaven.CefAppBuilder;
-import me.friwi.jcefmaven.CefInitializationException;
-import me.friwi.jcefmaven.UnsupportedPlatformException;
-import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler;
-import org.cef.CefApp;
-import org.cef.CefClient;
-import org.cef.browser.CefBrowser;
-import org.cef.browser.CefFrame;
-import org.cef.browser.CefMessageRouter;
-import org.cef.callback.CefQueryCallback;
-import org.cef.handler.CefMessageRouterHandlerAdapter;
-import com.google.gson.Gson;
-
-import javax.swing.*;
-import java.awt.*;
-import java.io.File;
-import java.io.IOException;
-
-public class BlocklyPanel extends JPanel {
-
- private final CefClient cefClient;
- private final CefBrowser cefBrowser;
- private final Component browserUI;
- private final Vivid2DRendererBridge rendererBridge = new Vivid2DRendererBridge();
- private final Gson gson = new Gson();
-
- public BlocklyPanel() throws IOException, InterruptedException, UnsupportedPlatformException, CefInitializationException {
- setLayout(new BorderLayout());
- CefApp cefAppManager = CefAppManager.getInstance();
- this.cefClient = cefAppManager.createClient();
- CefMessageRouter msgRouter = CefMessageRouter.create();
- msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
- @Override
- public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, String request,
- boolean persistent, CefQueryCallback callback) {
- try {
- System.out.println("Java端接收到指令: " + request);
- RequestData data = gson.fromJson(request, RequestData.class);
- handleRendererAction(data);
- callback.success("OK"); // 通知JS端调用成功
- } catch (Exception e) {
- e.printStackTrace();
- callback.failure(500, e.getMessage());
- }
- return true;
- }
- }, true);
- cefClient.addMessageRouter(msgRouter);
- String url = new File("C:\\Users\\Administrator\\MCreatorWorkspaces\\AxisInnovatorsBox\\src\\main\\resources\\web\\blockly_editor.html").toURI().toString();
- this.cefBrowser = cefClient.createBrowser(url, false, false);
- this.browserUI = cefBrowser.getUIComponent();
- add(browserUI, BorderLayout.CENTER);
- }
-
- // 辅助方法,根据JS请求调用不同的Java方法
- private void handleRendererAction(RequestData data) {
- if (data == null || data.action == null) return;
- switch (data.action) {
- case "moveObject":
- System.out.println(String.format(
- "Java端接收到指令: 移动对象 '%s' 到坐标 (%d, %d)",
- data.params.get("objectId").toString(),
- ((Double)data.params.get("x")).intValue(),
- ((Double)data.params.get("y")).intValue()
- ));
- rendererBridge.moveObject(
- data.params.get("objectId").toString(),
- ((Double)data.params.get("x")).intValue(),
- ((Double)data.params.get("y")).intValue()
- );
- break;
- case "changeColor":
- System.out.println(String.format(
- "Java端接收到指令: 改变对象 '%s' 的颜色为 %s",
- data.params.get("objectId").toString(),
- data.params.get("colorHex").toString()
- ));
- rendererBridge.changeColor(
- data.params.get("objectId").toString(),
- data.params.get("colorHex").toString()
- );
- break;
- // 在这里添加更多case来处理其他操作
- default:
- System.err.println("未知的操作: " + data.action);
- }
- }
-
- // 用于Gson解析的内部类
- private static class RequestData {
- String action;
- java.util.Map params;
- }
-}
diff --git a/src/main/java/com/chuangzhou/vivid2D/block/MainApplication.java b/src/main/java/com/chuangzhou/vivid2D/block/MainApplication.java
deleted file mode 100644
index d22fb93..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/block/MainApplication.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.chuangzhou;
-
-import com.axis.innovators.box.browser.BrowserWindowJDialog;
-import com.chuangzhou.vivid2D.block.BlocklyMessageHandler;
-import com.formdev.flatlaf.FlatDarkLaf;
-import org.cef.browser.CefBrowser;
-import org.cef.browser.CefFrame;
-import org.cef.browser.CefMessageRouter;
-import org.cef.callback.CefQueryCallback;
-import org.cef.handler.CefMessageRouterHandlerAdapter;
-
-import javax.swing.*;
-import java.util.concurrent.atomic.AtomicReference;
-
-public class MainApplication {
-
- public static void main(String[] args) {
- FlatDarkLaf.setup();
- SwingUtilities.invokeLater(() -> {
- try {
- AtomicReference windowRef = new AtomicReference<>();
- windowRef.set(new BrowserWindowJDialog.Builder("vivid2d-blockly-editor")
- .title("Vivid2D - Blockly Editor")
- .size(1280, 720)
- .htmlPath("C:\\Users\\Administrator\\MCreatorWorkspaces\\AxisInnovatorsBox\\src\\main\\resources\\web\\blockly_editor.html")
- .build());
- BrowserWindowJDialog blocklyWindow = windowRef.get();
- if (blocklyWindow == null) {
- throw new IllegalStateException("BrowserWindowJDialog未能成功创建。");
- }
- CefMessageRouter msgRouter = blocklyWindow.getMsgRouter();
- if (msgRouter != null) {
- final BlocklyMessageHandler blocklyHandler = new BlocklyMessageHandler();
- msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
- @Override
- public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId,
- String request, boolean persistent, CefQueryCallback callback) {
- // 尝试使用我们的处理器来处理请求
- if (blocklyHandler.handle(request)) {
- // 如果 handle 方法返回 true,说明请求已被成功处理
- callback.success("OK from Blockly");
- return true;
- }
- // 如果我们的处理器不处理这个请求,返回 false,让其他处理器(比如默认的)有机会处理它
- return false;
- }
- }, true); // 第二个参数 true 表示这是第一个被检查的处理器
- }
- } catch (Exception e) {
- e.printStackTrace();
- JOptionPane.showMessageDialog(
- null,
- "无法初始化浏览器,请检查环境配置。\n错误: " + e.getMessage(),
- "启动失败",
- JOptionPane.ERROR_MESSAGE
- );
- }
- });
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/chuangzhou/vivid2D/block/Vivid2DRendererBridge.java b/src/main/java/com/chuangzhou/vivid2D/block/Vivid2DRendererBridge.java
deleted file mode 100644
index f84eeb8..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/block/Vivid2DRendererBridge.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.chuangzhou.vivid2D.block;
-
-public class Vivid2DRendererBridge {
-
- /**
- * 当Blockly中的 "移动对象" 积木被执行时,此方法将被调用。
- * @param objectId 要移动的对象的ID
- * @param x X坐标
- * @param y Y坐标
- */
- public void moveObject(String objectId, int x, int y) {
- System.out.println(String.format(
- "Java端接收到指令: 移动对象 '%s' 到坐标 (%d, %d)", objectId, x, y
- ));
- // TODO: 在这里调用您自己的 vivid2D 渲染器代码
- // aether.getRenderer().getObjectById(objectId).setPosition(x, y);
- }
-
- /**
- * 当Blockly中的 "改变颜色" 积木被执行时,此方法将被调用。
- * @param objectId 对象ID
- * @param colorHex 16进制颜色字符串, e.g., "#FF0000"
- */
- public void changeColor(String objectId, String colorHex) {
- System.out.println(String.format(
- "Java端接收到指令: 改变对象 '%s' 的颜色为 %s", objectId, colorHex
- ));
- // TODO: 在这里调用您自己的 vivid2D 渲染器代码
- }
-}
diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindow.java b/src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindow.java
deleted file mode 100644
index cdb3e59..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindow.java
+++ /dev/null
@@ -1,821 +0,0 @@
-package com.chuangzhou.vivid2D.browser;
-
-import com.axis.innovators.box.AxisInnovatorsBox;
-import com.axis.innovators.box.events.BrowserCreationCallback;
-import com.google.gson.Gson;
-import com.google.gson.JsonObject;
-import org.cef.CefApp;
-import org.cef.CefClient;
-import org.cef.CefSettings;
-import org.cef.browser.CefBrowser;
-import org.cef.browser.CefFrame;
-import org.cef.browser.CefMessageRouter;
-import org.cef.callback.CefContextMenuParams;
-import org.cef.callback.CefJSDialogCallback;
-import org.cef.callback.CefMenuModel;
-import org.cef.callback.CefQueryCallback;
-import org.cef.handler.*;
-import org.cef.misc.BoolRef;
-import org.cef.network.CefRequest;
-
-import javax.swing.*;
-import java.awt.*;
-import java.awt.datatransfer.Clipboard;
-import java.awt.datatransfer.DataFlavor;
-import java.awt.datatransfer.UnsupportedFlavorException;
-import java.awt.event.*;
-import java.io.File;
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.util.function.Consumer;
-
-import static org.cef.callback.CefMenuModel.MenuId.MENU_ID_USER_FIRST;
-
-/**
- * @author tzdwindows 7
- */
-public class BrowserWindow extends JFrame {
- private final String windowId;
- private final String htmlUrl;
- private CefApp cefApp;
- private CefClient client;
- private CefBrowser browser;
- private final Component browserComponent;
- private final String htmlPath;
- private static boolean isInitialized = false;
- private WindowOperationHandler operationHandler;
- private static Thread cefThread;
- private CefMessageRouter msgRouter;
-
- public static class Builder {
- private BrowserCreationCallback browserCreationCallback;
- private String windowId;
- private String title = "JCEF Window";
- private Dimension size = new Dimension(800, 600);
- private WindowOperationHandler operationHandler;
- private String htmlPath;
- private Image icon;
- private boolean resizable = true; // 默认允许调整大小
- private boolean maximizable = true; // 默认允许最大化
- private boolean minimizable = true; // 默认允许最小化
- private String htmlUrl = "";
- private boolean openLinksInExternalBrowser = true; // 默认使用外部浏览器
-
- public Builder resizable(boolean resizable) {
- this.resizable = resizable;
- return this;
- }
-
- public Builder maximizable(boolean maximizable) {
- this.maximizable = maximizable;
- return this;
- }
-
- /**
- * 设置链接打开方式
- *
- * @param openInBrowser 是否在当前浏览器窗口中打开链接
- * true - 在当前浏览器窗口中打开链接(本地跳转)
- * false - 使用系统默认浏览器打开链接(外部跳转)
- * @return Builder实例,支持链式调用
- *
- * @apiNote 此方法控制两种不同的链接打开行为:
- * 1. 当设置为true时:
- * - 所有链接将在当前CEF浏览器窗口内打开
- *
- * 2. 当设置为false时(默认值):
- * - 所有链接将在系统默认浏览器中打开
- * - 更安全,避免潜在的安全风险
- * - 适用于简单的信息展示场景
- *
- * @implNote 内部实现说明:
- * - 实际存储的是反向值(openLinksInExternalBrowser)
- * - 这样设置是为了保持与历史版本的兼容性
- * - 方法名使用"openInBrowser"更符合用户直觉
- *
- * @example 使用示例:
- * // 在当前窗口打开链接
- * new Builder().openLinksInBrowser(true).build();
- *
- * // 使用系统浏览器打开链接(默认)
- * new Builder().openLinksInBrowser(false).build();
- *
- * @see #openLinksInExternalBrowser 内部存储字段
- * @see CefLifeSpanHandler#onBeforePopup 弹窗处理实现
- * @see CefRequestHandler#onBeforeBrowse 导航处理实现
- */
- public Builder openLinksInBrowser(boolean openInBrowser) {
- this.openLinksInExternalBrowser = !openInBrowser;
- return this;
- }
-
- public Builder minimizable(boolean minimizable) {
- this.minimizable = minimizable;
- return this;
- }
-
- public Builder(String windowId) {
- this.windowId = windowId;
- }
-
- /**
- * 设置浏览器创建回调
- * @param callback 回调
- */
- public Builder setBrowserCreationCallback(BrowserCreationCallback callback){
- this.browserCreationCallback = callback;
- return this;
- }
-
- /**
- * 设置浏览器窗口标题
- * @param title 标题
- */
- public Builder title(String title) {
- this.title = title;
- return this;
- }
-
- /**
- * 设置浏览器窗口大小
- * @param width 宽度
- * @param height 高度
- */
- public Builder size(int width, int height) {
- this.size = new Dimension(width, height);
- return this;
- }
-
- /**
- * 设置浏览器触发事件
- * @param handler 事件处理器
- */
- public Builder operationHandler(WindowOperationHandler handler) {
- this.operationHandler = handler;
- return this;
- }
-
- /**
- * 设置浏览器图标
- * @param icon 图标
- */
- public Builder icon(Image icon) {
- this.icon = icon;
- return this;
- }
-
- /**
- * 设置HTML路径
- */
- public BrowserWindow build() {
- if (htmlUrl.isEmpty()) {
- if (this.htmlPath == null || this.htmlPath.isEmpty()) {
- throw new IllegalArgumentException("HTML paths cannot be empty");
- }
- File htmlFile = new File(this.htmlPath);
- if (!htmlFile.exists()) {
- throw new RuntimeException("The HTML file does not exist: " + htmlFile.getAbsolutePath());
- }
- }
- return new BrowserWindow(this);
- }
-
- /**
- * 设置HTML路径
- * @param path HTML路径
- */
- public Builder htmlPath(String path) {
- this.htmlPath = path;
- return this;
- }
-
- /**
- * 使用Url
- * @param htmlUrl Url路径
- */
- public Builder htmlUrl(String htmlUrl) {
- this.htmlUrl = htmlUrl;
- return this;
- }
- }
-
- private BrowserWindow(Builder builder) {
- this.windowId = builder.windowId;
- this.htmlPath = builder.htmlPath;
- this.operationHandler = builder.operationHandler;
- this.htmlUrl = builder.htmlUrl;
-
- // 设置图标(如果存在)
- if (builder.icon != null) {
- setIconImage(builder.icon);
- }
-
- // 初始化浏览器组件
- try {
- this.browserComponent = initializeCef(builder);
- if (operationHandler != null) {
- setupMessageHandlers(operationHandler);
- }
- } catch (Exception e) {
- JOptionPane.showMessageDialog(this, "初始化失败: " + e.getMessage());
- throw new RuntimeException(e);
- }
- }
-
- private Component initializeCef(Builder builder) throws MalformedURLException {
- if (!isInitialized) {
- isInitialized = true;
- try {
- this.cefApp = CefAppManager.getInstance();
- //CefAppManager.incrementBrowserCount();
- client = cefApp.createClient();
- client.addDisplayHandler(new CefDisplayHandler (){
- @Override
- public void onAddressChange(CefBrowser browser, CefFrame frame, String url) {
-
- }
-
- @Override
- public void onTitleChange(CefBrowser browser, String title) {
-
- }
-
- @Override
- public void OnFullscreenModeChange(CefBrowser browser, boolean fullscreen) {
-
- }
-
- @Override
- public boolean onTooltip(CefBrowser browser, String text) {
- return false;
- }
-
- @Override
- public void onStatusMessage(CefBrowser browser, String value) {
-
- }
-
- @Override
- public boolean onConsoleMessage(
- CefBrowser browser,
- CefSettings.LogSeverity level,
- String message,
- String source,
- int line
- ) {
- // 格式化输出到 Java 控制台
- //if (level != CefSettings.LogSeverity.LOGSEVERITY_WARNING) {
- String log = String.format(
- "[Browser Console] %s %s (Line %d) -> %s",
- getLogLevelSymbol(level),
- source,
- line,
- message
- );
- System.out.println(log);
- //}
- return false;
- }
-
- @Override
- public boolean onCursorChange(CefBrowser browser, int cursorType) {
- return false;
- }
-
- private String getLogLevelSymbol(CefSettings.LogSeverity level) {
- switch (level) {
- case LOGSEVERITY_ERROR: return "⛔";
- case LOGSEVERITY_WARNING: return "⚠️";
- case LOGSEVERITY_DEFAULT: return "🐞";
- default: return "ℹ️";
- }
- }
- });
-
- if (AxisInnovatorsBox.getMain() != null && AxisInnovatorsBox.getMain().isDebugEnvironment()) {
- client.addKeyboardHandler(new CefKeyboardHandlerAdapter() {
- @Override
- public boolean onKeyEvent(CefBrowser browser, CefKeyEvent event) {
- // 检测 F12
- if (event.windows_key_code == 123) {
- browser.getDevTools().createImmediately();
- return true;
- }
- return false;
- }
- });
- }
-
- client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
- @Override
- public boolean onBeforePopup(CefBrowser browser, CefFrame frame,
- String targetUrl, String targetFrameName) {
- // 处理弹出窗口:根据配置决定打开方式
- if (builder.openLinksInExternalBrowser) {
- // 使用默认浏览器打开
- try {
- Desktop.getDesktop().browse(new URI(targetUrl));
- } catch (Exception e) {
- System.out.println("Failed to open external browser: " + e.getMessage());
- }
- return true; // 拦截弹窗
- } else {
- // 在当前浏览器中打开
- browser.loadURL(targetUrl);
- return true; // 拦截弹窗并在当前窗口打开
- }
- }
- });
-
- client.addRequestHandler(new CefRequestHandlerAdapter() {
- @Override
- public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame,
- CefRequest request, boolean userGesture, boolean isRedirect) {
- // 处理主窗口导航
- if (userGesture) {
- if (builder.openLinksInExternalBrowser) {
- // 使用默认浏览器打开
- try {
- Desktop.getDesktop().browse(new URI(request.getURL()));
- return true; // 取消内置浏览器导航
- } catch (Exception e) {
- System.out.println("Failed to open external browser: " + e.getMessage());
- }
- } else {
- // 允许在当前浏览器中打开
- return false;
- }
- }
- return false;
- }
- });
-
- client.addContextMenuHandler(new CefContextMenuHandlerAdapter() {
- @Override
- public void onBeforeContextMenu(CefBrowser browser, CefFrame frame,
- CefContextMenuParams params, CefMenuModel model) {
- model.clear();
- if (!params.getSelectionText().isEmpty() || params.isEditable()) {
- model.addItem(MENU_ID_USER_FIRST, "复制");
- }
-
- if (params.isEditable()) {
- model.addItem(MENU_ID_USER_FIRST + 1, "粘贴");
- model.addItem(MENU_ID_USER_FIRST + 2, "粘贴纯文本");
- }
- }
-
- @Override
- public boolean onContextMenuCommand(CefBrowser browser, CefFrame frame,
- CefContextMenuParams params, int commandId, int eventFlags) {
- if (commandId == MENU_ID_USER_FIRST) {
- if (params.isEditable()) {
- browser.executeJavaScript("document.execCommand('copy');", browser.getURL(), 0);
- } else {
- browser.executeJavaScript(
- "window.getSelection().toString();",
- browser.getURL(),
- 0
- );
- }
- return true;
- } else if (commandId == MENU_ID_USER_FIRST + 1) {
- pasteContent(browser, false);
- return true;
- } else if (commandId == MENU_ID_USER_FIRST + 2) {
- pasteContent(browser, true);
- return true;
- }
-
- return false;
- }
-
- /**
- * 处理粘贴操作
- * @param plainText 是否去除格式(纯文本模式)
- */
- private void pasteContent(CefBrowser browser, boolean plainText) {
- try {
- Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
- if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
- String text = (String) clipboard.getData(DataFlavor.stringFlavor);
-
- if (plainText) {
- text = text.replaceAll("<[^>]+>", "");
- }
-
- String escapedText = text
- .replace("\\", "\\\\")
- .replace("'", "\\'")
- .replace("\n", "\\n")
- .replace("\r", "\\r");
-
- String script = String.format(
- "if (document.activeElement) {\n" +
- " document.activeElement.value += '%s';\n" + // 简单追加文本
- " document.dispatchEvent(new Event('input', { bubbles: true }));\n" + // 触发输入事件
- "}",
- escapedText
- );
- browser.executeJavaScript(script, browser.getURL(), 0);
- }
- } catch (UnsupportedFlavorException | IOException e) {
- e.printStackTrace();
- }
- }
- });
-
- client.addJSDialogHandler(new CefJSDialogHandlerAdapter() {
- @Override
- public boolean onJSDialog(CefBrowser browser, String origin_url, JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) {
- if (dialog_type == JSDialogType.JSDIALOGTYPE_ALERT) {
- SwingUtilities.invokeLater(() -> {
- JOptionPane.showMessageDialog(
- BrowserWindow.this,
- message_text,
- "警告",
- JOptionPane.INFORMATION_MESSAGE
- );
- });
- callback.Continue(true, "");
- return true;
- }
- return false;
- }
- });
-
-
- // 3. 拦截所有新窗口(关键修复点!)
- //client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
- // @Override
- // public boolean onBeforePopup(CefBrowser browser,
- // CefFrame frame, String target_url, String target_frame_name) {
- // return true; // 返回true表示拦截弹窗
- // }
- //});
-
-
- Thread.currentThread().setName("BrowserRenderThread");
-
- // 4. 加载HTML
- if (htmlUrl.isEmpty()){
- String fileUrl = new File(htmlPath).toURI().toURL().toString();
- System.out.println("Loading HTML from: " + fileUrl);
-
- // 5. 创建浏览器组件(直接添加到内容面板)
- browser = client.createBrowser(fileUrl, false, false);
- } else {
- System.out.println("Loading Url from: " + htmlUrl);
- browser = client.createBrowser(htmlUrl, false, false);
- }
-
- Component browserComponent = browser.getUIComponent();
- if (builder.browserCreationCallback != null) {
- boolean handled = builder.browserCreationCallback.onLayoutCustomization(
- this, // 当前窗口
- getContentPane(), // 内容面板
- browserComponent, // 浏览器组件
- builder // 构建器对象
- );
-
- // 如果回调返回true,跳过默认布局
- if (handled) {
- // 设置窗口基本属性
- setTitle(builder.title);
- setSize(builder.size);
- setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
-
- // 添加资源释放监听器
- addWindowListener(new WindowAdapter() {
- @Override
- public void windowClosed(WindowEvent e) {
- browser.close(true);
- client.dispose();
- }
- });
-
- setVisible(true);
- return browserComponent; // 直接返回,跳过默认布局
- }
- }
-
- CefMessageRouter.CefMessageRouterConfig config = new CefMessageRouter.CefMessageRouterConfig();
- config.jsQueryFunction = "javaQuery";// 定义方法
- config.jsCancelFunction = "javaQueryCancel";// 定义取消方法
-
- updateTheme();
-
- // 6. 配置窗口布局(确保只添加一次)
- SwingUtilities.invokeLater(() -> {
- getContentPane().removeAll();
- getContentPane().setLayout(new BorderLayout());
-
- // 透明拖拽层(仅顶部可拖拽)
- JPanel dragPanel = new JPanel(new BorderLayout());
- dragPanel.setOpaque(false);
-
- JPanel titleBar = new JPanel();
- titleBar.setOpaque(false);
- titleBar.setPreferredSize(new Dimension(builder.size.width, 20));
- final Point[] dragStart = new Point[1];
- titleBar.addMouseListener(new MouseAdapter() {
- @Override
- public void mousePressed(MouseEvent e) {
- dragStart[0] = e.getPoint();
- }
-
- @Override
- public void mouseReleased(MouseEvent e) {
- dragStart[0] = null;
- }
- });
- titleBar.addMouseMotionListener(new MouseMotionAdapter() {
- @Override
- public void mouseDragged(MouseEvent e) {
- if (dragStart[0] != null) {
- Point curr = e.getLocationOnScreen();
- setLocation(curr.x - dragStart[0].x, curr.y - dragStart[0].y);
- }
- }
- });
-
- dragPanel.add(titleBar, BorderLayout.NORTH);
- getContentPane().add(dragPanel, BorderLayout.CENTER);
- getContentPane().add(browserComponent, BorderLayout.CENTER);
-
- // 7. 窗口属性设置
- setTitle(builder.title);
- setSize(builder.size);
- setLocationRelativeTo(null);
- setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
-
- // 8. 资源释放
- addWindowListener(new WindowAdapter() {
- @Override
- public void windowClosed(WindowEvent e) {
- browser.close(true);
- client.dispose();
- }
- });
-
- setVisible(true);
-
- });
- return browserComponent;
- } catch (Exception e) {
- e.printStackTrace();
- JOptionPane.showMessageDialog(null, "初始化失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
- }
- } else {
- isInitialized = false;
- SwingUtilities.invokeLater(() -> {
- dispose();
- });
- }
- return null;
- }
-
- /**
- * 更新主题
- */
- public void updateTheme() {
- // 1. 获取Java字体信息
- String fontInfo = getSystemFontsInfo();
- boolean isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode();
- injectFontInfoToPage(browser, fontInfo, isDarkTheme);
-
- // 2. 注入主题信息
- //injectThemeInfoToPage(browser, isDarkTheme);
-
- //// 3. 刷新浏览器
- //SwingUtilities.invokeLater(() -> {
- // browser.reload();
- //});
- }
-
- /**
- * 获取Java字体信息(从UIManager获取)
- */
- private String getSystemFontsInfo() {
- try {
- Gson gson = new Gson();
- JsonObject fontInfo = new JsonObject();
- JsonObject uiFonts = new JsonObject();
-
- String[] fontKeys = {
- "Label.font", "Button.font", "ToggleButton.font", "RadioButton.font",
- "CheckBox.font", "ColorChooser.font", "ComboBox.font", "EditorPane.font",
- "TextArea.font", "TextField.font", "PasswordField.font", "TextPane.font",
- "FormattedTextField.font", "Table.font", "TableHeader.font", "List.font",
- "Tree.font", "TabbedPane.font", "MenuBar.font", "Menu.font", "MenuItem.font",
- "PopupMenu.font", "CheckBoxMenuItem.font", "RadioButtonMenuItem.font",
- "Spinner.font", "ToolBar.font", "TitledBorder.font", "OptionPane.messageFont",
- "OptionPane.buttonFont", "Panel.font", "Viewport.font", "ToolTip.font"
- };
-
- for (String key : fontKeys) {
- Font font = UIManager.getFont(key);
- if (font != null) {
- JsonObject fontObj = new JsonObject();
- fontObj.addProperty("name", font.getFontName());
- fontObj.addProperty("family", font.getFamily());
- fontObj.addProperty("size", font.getSize());
- fontObj.addProperty("style", font.getStyle());
- fontObj.addProperty("bold", font.isBold());
- fontObj.addProperty("italic", font.isItalic());
- fontObj.addProperty("plain", font.isPlain());
- uiFonts.add(key, fontObj);
- }
- }
-
- fontInfo.add("uiFonts", uiFonts);
- fontInfo.addProperty("timestamp", System.currentTimeMillis());
- fontInfo.addProperty("lookAndFeel", UIManager.getLookAndFeel().getName());
-
- return gson.toJson(fontInfo);
- } catch (Exception e) {
- return "{\"error\": \"无法获取UIManager字体信息: " + e.getMessage() + "\"}";
- }
- }
-
- /**
- * 注入主题信息到页面
- */
- private void injectThemeInfoToPage(CefBrowser browser, boolean isDarkTheme) {
- if (client == null) {
- return;
- }
-
- String themeInfo = String.format(
- "{\"isDarkTheme\": %s, \"timestamp\": %d}",
- isDarkTheme,
- System.currentTimeMillis()
- );
-
- // 最简单的脚本 - 直接设置和分发事件
- String script = String.format(
- "window.javaThemeInfo = %s;" +
- "console.log('主题信息已设置:', window.javaThemeInfo);" +
- "" +
- "var event = new CustomEvent('javaThemeChanged', {" +
- " detail: window.javaThemeInfo" +
- "});" +
- "document.dispatchEvent(event);" +
- "console.log('javaThemeChanged事件已分发');",
- themeInfo);
-
- browser.executeJavaScript(script, browser.getURL(), 0);
- }
-
- /**
- * 注入字体信息到页面并设置字体
- */
- private void injectFontInfoToPage(CefBrowser browser, String fontInfo,boolean isDarkTheme) {
- if (client == null) {
- return;
- }
- client.addLoadHandler(new CefLoadHandlerAdapter() {
- @Override
- public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
- // 使用更简单的脚本来注入字体信息
- String script =
- "if (typeof window.javaFontInfo === 'undefined') {" +
- " window.javaFontInfo = " + fontInfo + ";" +
- " console.log('Java font information has been loaded:', window.javaFontInfo);" +
- " " +
- " var event = new CustomEvent('javaFontsLoaded', {" +
- " detail: window.javaFontInfo" +
- " });" +
- " document.dispatchEvent(event);" +
- " console.log('The javaFontsLoaded event is dispatched');" +
- "}";
-
- browser.executeJavaScript(script, browser.getURL(), 0);
-
- // 添加调试信息
- browser.executeJavaScript(
- "console.log('Font information injection is complete,window.javaFontInfo:', typeof window.javaFontInfo);" +
- "console.log('Number of event listeners:', document.eventListeners ? document.eventListeners('javaFontsLoaded') : '无法获取');",
- browser.getURL(), 0
- );
-
- String themeInfo = String.format(
- "{\"isDarkTheme\": %s, \"timestamp\": %d}",
- isDarkTheme,
- System.currentTimeMillis()
- );
-
- script = String.format(
- "window.javaThemeInfo = %s;" +
- "console.log('主题信息已设置:', window.javaThemeInfo);" +
- "" +
- "var event = new CustomEvent('javaThemeChanged', {" +
- " detail: window.javaThemeInfo" +
- "});" +
- "document.dispatchEvent(event);" +
- "console.log('javaThemeChanged事件已分发');",
- themeInfo);
-
- browser.executeJavaScript(script, browser.getURL(), 0);
- }
- });
-
- }
-
- public static void printStackTrace() {
- StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
- for (int i = 2; i < stackTrace.length; i++) {
- StackTraceElement element = stackTrace[i];
- System.out.println(element.getClassName() + "." + element.getMethodName() +
- "(" + (element.getFileName() != null ? element.getFileName() : "Unknown Source") +
- ":" + element.getLineNumber() + ")");
- }
- }
-
- @Override
- public void setVisible(boolean b) {
- if (b) {
- if (browser != null) {
- updateTheme();
- }
- }
- super.setVisible(b);
- }
-
- public Component getBrowserComponent() {
- return browserComponent;
- }
-
- private void setupMessageHandlers(WindowOperationHandler handler) {
- if (client != null) {
- msgRouter = CefMessageRouter.create();
- msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
- @Override
- public boolean onQuery(CefBrowser browser,
- CefFrame frame,
- long queryId,
- String request,
- boolean persistent,
- CefQueryCallback callback) {
- if (request.startsWith("system:")) {
- String[] parts = request.split(":");
- String operation = parts.length >= 2 ? parts[1] : null;
- String targetWindow = parts.length > 2 ? parts[2] : null;
- handler.handleOperation(
- new WindowOperation(operation, targetWindow, callback) // [!code ++]
- );
- return true;
- }
- if (request.startsWith("java-response:")) {
- String[] parts = request.split(":");
- String requestId = parts[1];
- String responseData = parts.length > 2 ? parts[2] : "";
- Consumer handler = WindowRegistry.getInstance().getCallback(requestId);
- if (handler != null) {
- handler.accept(responseData);
- callback.success("");
- } else {
- callback.failure(-1, "无效的请求ID");
- }
- return true;
- }
- return false;
- }
- }, true);
- client.addMessageRouter(msgRouter);
- }
- }
-
- public String getWindowId() {
- return windowId;
- }
-
- /**
- * 获取消息路由器
- * @return 消息路由器
- */
- public CefMessageRouter getMsgRouter() {
- return msgRouter;
- }
-
- /**
- * 获取浏览器对象
- * @return 浏览器对象
- */
- public CefBrowser getBrowser() {
- return browser;
- }
-
- public void closeWindow() {
- SwingUtilities.invokeLater(() -> {
- if (browser != null) {
- browser.close(true);
- }
- dispose();
- cefApp.dispose();
- WindowRegistry.getInstance().unregisterWindow(windowId);
- });
- }
-}
-
diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindowJDialog.java b/src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindowJDialog.java
deleted file mode 100644
index 2a4f4e6..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindowJDialog.java
+++ /dev/null
@@ -1,833 +0,0 @@
-package com.chuangzhou.vivid2D.browser;
-
-import com.axis.innovators.box.AxisInnovatorsBox;
-import com.axis.innovators.box.events.BrowserCreationCallback;
-import com.google.gson.Gson;
-import com.google.gson.JsonObject;
-import org.cef.CefApp;
-import org.cef.CefClient;
-import org.cef.CefSettings;
-import org.cef.browser.CefBrowser;
-import org.cef.browser.CefFrame;
-import org.cef.browser.CefMessageRouter;
-import org.cef.callback.CefContextMenuParams;
-import org.cef.callback.CefJSDialogCallback;
-import org.cef.callback.CefMenuModel;
-import org.cef.callback.CefQueryCallback;
-import org.cef.handler.*;
-import org.cef.misc.BoolRef;
-import org.cef.network.CefRequest;
-
-import javax.swing.*;
-import java.awt.*;
-import java.awt.datatransfer.Clipboard;
-import java.awt.datatransfer.DataFlavor;
-import java.awt.datatransfer.UnsupportedFlavorException;
-import java.awt.event.*;
-import java.io.File;
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.util.function.Consumer;
-
-import static org.cef.callback.CefMenuModel.MenuId.MENU_ID_USER_FIRST;
-
-/**
- * @author tzdwindows 7
- */
-public class BrowserWindowJDialog extends JDialog {
- private final String windowId;
- private final String htmlUrl;
- private CefApp cefApp;
- private CefClient client;
- private CefBrowser browser;
- private final Component browserComponent;
- private final String htmlPath;
- //private boolean isInitialized = false;
- private WindowOperationHandler operationHandler;
- private static Thread cefThread;
- private CefMessageRouter msgRouter;
-
- public static class Builder {
- private String windowId;
- private String title = "JCEF Window";
- private Dimension size = new Dimension(800, 600);
- private WindowOperationHandler operationHandler;
- private String htmlPath;
- private Image icon;
- private JFrame parentFrame;
- private boolean resizable = true; // 默认允许调整大小
- private boolean maximizable = true; // 默认允许最大化
- private boolean minimizable = true; // 默认允许最小化
- private String htmlUrl = "";
- private BrowserCreationCallback browserCreationCallback;
- private boolean openLinksInExternalBrowser = true; // 默认使用外部浏览器
-
- public Builder resizable(boolean resizable) {
- this.resizable = resizable;
- return this;
- }
-
- public Builder maximizable(boolean maximizable) {
- this.maximizable = maximizable;
- return this;
- }
-
- public Builder minimizable(boolean minimizable) {
- this.minimizable = minimizable;
- return this;
- }
-
- public Builder(String windowId) {
- this.windowId = windowId;
- }
-
- /**
- * 设置浏览器窗口标题
- * @param title 标题
- */
- public Builder title(String title) {
- this.title = title;
- return this;
- }
-
- /**
- * 设置链接打开方式
- *
- * @param openInBrowser 是否在当前浏览器窗口中打开链接
- * true - 在当前浏览器窗口中打开链接(本地跳转)
- * false - 使用系统默认浏览器打开链接(外部跳转)
- * @return Builder实例,支持链式调用
- *
- * @apiNote 此方法控制两种不同的链接打开行为:
- * 1. 当设置为true时:
- * - 所有链接将在当前CEF浏览器窗口内打开
- *
- * 2. 当设置为false时(默认值):
- * - 所有链接将在系统默认浏览器中打开
- * - 更安全,避免潜在的安全风险
- * - 适用于简单的信息展示场景
- *
- * @implNote 内部实现说明:
- * - 实际存储的是反向值(openLinksInExternalBrowser)
- * - 这样设置是为了保持与历史版本的兼容性
- * - 方法名使用"openInBrowser"更符合用户直觉
- *
- * @example 使用示例:
- * // 在当前窗口打开链接
- * new Builder().openLinksInBrowser(true).build();
- *
- * // 使用系统浏览器打开链接(默认)
- * new Builder().openLinksInBrowser(false).build();
- *
- * @see #openLinksInExternalBrowser 内部存储字段
- * @see CefLifeSpanHandler#onBeforePopup 弹窗处理实现
- * @see CefRequestHandler#onBeforeBrowse 导航处理实现
- */
- public Builder openLinksInBrowser(boolean openInBrowser) {
- this.openLinksInExternalBrowser = !openInBrowser;
- return this;
- }
-
-
- /**
- * 设置浏览器创建回调
- * @param callback 回调
- */
- public Builder setBrowserCreationCallback(BrowserCreationCallback callback){
- this.browserCreationCallback = callback;
- return this;
- }
-
- /**
- * 设置浏览器窗口大小
- * @param width 宽度
- * @param height 高度
- */
- public Builder size(int width, int height) {
- this.size = new Dimension(width, height);
- return this;
- }
-
- /**
- * 设置浏览器窗口父窗口
- * @param parent 父窗口
- */
- public Builder parentFrame(JFrame parent) {
- this.parentFrame = parent;
- return this;
- }
-
-
- /**
- * 设置浏览器触发事件
- * @param handler 事件处理器
- */
- public Builder operationHandler(WindowOperationHandler handler) {
- this.operationHandler = handler;
- return this;
- }
-
- /**
- * 设置浏览器图标
- * @param icon 图标
- */
- public Builder icon(Image icon) {
- this.icon = icon;
- return this;
- }
-
- /**
- * 设置HTML路径
- */
- public BrowserWindowJDialog build() {
- if (htmlUrl.isEmpty()) {
- if (this.htmlPath == null || this.htmlPath.isEmpty()) {
- throw new IllegalArgumentException("HTML paths cannot be empty");
- }
- File htmlFile = new File(this.htmlPath);
- if (!htmlFile.exists()) {
- throw new RuntimeException("The HTML file does not exist: " + htmlFile.getAbsolutePath());
- }
- }
- return new BrowserWindowJDialog(this);
- }
-
- /**
- * 设置HTML路径
- * @param path HTML路径
- */
- public Builder htmlPath(String path) {
- this.htmlPath = path;
- return this;
- }
-
- /**
- * 使用Url
- * @param htmlUrl Url路径
- */
- public Builder htmlUrl(String htmlUrl) {
- this.htmlUrl = htmlUrl;
- return this;
- }
- }
-
-
- private BrowserWindowJDialog(Builder builder) {
- // 根据父窗口是否存在,设置是否为模态对话框
- super(builder.parentFrame, builder.title, builder.parentFrame != null);
- this.windowId = builder.windowId;
- this.htmlPath = builder.htmlPath;
- this.htmlUrl = builder.htmlUrl;
- this.operationHandler = builder.operationHandler;
-
- // 设置图标(如果存在)
- if (builder.icon != null) {
- setIconImage(builder.icon);
- }
-
- // 初始化浏览器组件
- try {
- this.browserComponent = initializeCef(builder);
- if (operationHandler != null) {
- setupMessageHandlers(operationHandler);
- }
- } catch (Exception e) {
- JOptionPane.showMessageDialog(this, "初始化失败: " + e.getMessage());
- throw new RuntimeException(e);
- }
- }
- private static boolean isInitialized = false;
- private Component initializeCef(Builder builder) throws MalformedURLException {
- if (!isInitialized) {
- isInitialized = true;
- try {
- this.cefApp = CefAppManager.getInstance();
- //CefAppManager.incrementBrowserCount();
- client = cefApp.createClient();
- client.addDisplayHandler(new CefDisplayHandler (){
- @Override
- public void onAddressChange(CefBrowser browser, CefFrame frame, String url) {}
-
- @Override
- public void onTitleChange(CefBrowser browser, String title) {}
-
- @Override
- public void OnFullscreenModeChange(CefBrowser browser, boolean fullscreen) {}
-
- @Override
- public boolean onTooltip(CefBrowser browser, String text) {
- return false;
- }
-
- @Override
- public void onStatusMessage(CefBrowser browser, String value) {}
-
- @Override
- public boolean onConsoleMessage(
- CefBrowser browser,
- CefSettings.LogSeverity level,
- String message,
- String source,
- int line
- ) {
- // 格式化输出到 Java 控制台
- //if (level != CefSettings.LogSeverity.LOGSEVERITY_WARNING) {
- String log = String.format(
- "[Browser Console] %s %s (Line %d) -> %s",
- getLogLevelSymbol(level),
- source,
- line,
- message
- );
- System.out.println(log);
- //}
- return false;
- }
-
- @Override
- public boolean onCursorChange(CefBrowser browser, int cursorType) {
- return false;
- }
-
- private String getLogLevelSymbol(CefSettings.LogSeverity level) {
- switch (level) {
- case LOGSEVERITY_ERROR: return "⛔";
- case LOGSEVERITY_WARNING: return "⚠️";
- case LOGSEVERITY_DEFAULT: return "🐞";
- default: return "ℹ️";
- }
- }
- });
-
- if (AxisInnovatorsBox.getMain() != null && AxisInnovatorsBox.getMain().isDebugEnvironment()) {
- client.addKeyboardHandler(new CefKeyboardHandlerAdapter() {
- @Override
- public boolean onKeyEvent(CefBrowser browser, CefKeyEvent event) {
- // 检测 F12
- if (event.windows_key_code == 123) {
- browser.getDevTools().createImmediately();
- return true;
- }
- return false;
- }
- });
- }
-
- client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
- @Override
- public boolean onBeforePopup(CefBrowser browser, CefFrame frame,
- String targetUrl, String targetFrameName) {
- // 处理弹出窗口:根据配置决定打开方式
- if (builder.openLinksInExternalBrowser) {
- // 使用默认浏览器打开
- try {
- Desktop.getDesktop().browse(new URI(targetUrl));
- } catch (Exception e) {
- System.out.println("Failed to open external browser: " + e.getMessage());
- }
- return true; // 拦截弹窗
- } else {
- // 在当前浏览器中打开
- browser.loadURL(targetUrl);
- return true; // 拦截弹窗并在当前窗口打开
- }
- }
- });
-
- client.addRequestHandler(new CefRequestHandlerAdapter() {
- @Override
- public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame,
- CefRequest request, boolean userGesture, boolean isRedirect) {
- // 处理主窗口导航
- if (userGesture) {
- if (builder.openLinksInExternalBrowser) {
- // 使用默认浏览器打开
- try {
- Desktop.getDesktop().browse(new URI(request.getURL()));
- return true; // 取消内置浏览器导航
- } catch (Exception e) {
- System.out.println("Failed to open external browser: " + e.getMessage());
- }
- } else {
- // 允许在当前浏览器中打开
- return false;
- }
- }
- return false;
- }
- });
-
- client.addContextMenuHandler(new CefContextMenuHandlerAdapter() {
- @Override
- public void onBeforeContextMenu(CefBrowser browser, CefFrame frame,
- CefContextMenuParams params, CefMenuModel model) {
- model.clear();
- if (!params.getSelectionText().isEmpty() || params.isEditable()) {
- model.addItem(MENU_ID_USER_FIRST, "复制");
- }
-
- if (params.isEditable()) {
- model.addItem(MENU_ID_USER_FIRST + 1, "粘贴");
- model.addItem(MENU_ID_USER_FIRST + 2, "粘贴纯文本");
- }
- }
-
- @Override
- public boolean onContextMenuCommand(CefBrowser browser, CefFrame frame,
- CefContextMenuParams params, int commandId, int eventFlags) {
- if (commandId == MENU_ID_USER_FIRST) {
- if (params.isEditable()) {
- browser.executeJavaScript("document.execCommand('copy');", browser.getURL(), 0);
- } else {
- browser.executeJavaScript(
- "window.getSelection().toString();",
- browser.getURL(),
- 0
- );
- }
- return true;
- } else if (commandId == MENU_ID_USER_FIRST + 1) {
- pasteContent(browser, false);
- return true;
- } else if (commandId == MENU_ID_USER_FIRST + 2) {
- pasteContent(browser, true);
- return true;
- }
-
- return false;
- }
-
- /**
- * 处理粘贴操作
- * @param plainText 是否去除格式(纯文本模式)
- */
- private void pasteContent(CefBrowser browser, boolean plainText) {
- try {
- Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
- if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
- String text = (String) clipboard.getData(DataFlavor.stringFlavor);
-
- if (plainText) {
- text = text.replaceAll("<[^>]+>", "");
- }
-
- String escapedText = text
- .replace("\\", "\\\\")
- .replace("'", "\\'")
- .replace("\n", "\\n")
- .replace("\r", "\\r");
-
- String script = String.format(
- "if (document.activeElement) {\n" +
- " document.activeElement.value += '%s';\n" + // 简单追加文本
- " document.dispatchEvent(new Event('input', { bubbles: true }));\n" + // 触发输入事件
- "}",
- escapedText
- );
- browser.executeJavaScript(script, browser.getURL(), 0);
- }
- } catch (UnsupportedFlavorException | IOException e) {
- e.printStackTrace();
- }
- }
- });
-
- // 添加 alert 弹窗监控处理
- client.addJSDialogHandler(new CefJSDialogHandlerAdapter() {
- @Override
- public boolean onJSDialog(CefBrowser browser, String origin_url, JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) {
- if (dialog_type == JSDialogType.JSDIALOGTYPE_ALERT) {
- SwingUtilities.invokeLater(() -> {
- JOptionPane.showMessageDialog(
- BrowserWindowJDialog.this,
- message_text,
- "警告",
- JOptionPane.INFORMATION_MESSAGE
- );
- });
- callback.Continue(true, "");
- return true;
- }
- return false;
- }
- });
-
-
-
- // 3. 拦截所有新窗口(关键修复点!)
- //client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
- // @Override
- // public boolean onBeforePopup(CefBrowser browser,
- // CefFrame frame, String target_url, String target_frame_name) {
- // return true; // 返回true表示拦截弹窗
- // }
- //});
-
-
- Thread.currentThread().setName("BrowserRenderThread");
-
-
- // 4. 加载HTML
- if (htmlUrl.isEmpty()) {
- String fileUrl = new File(htmlPath).toURI().toURL().toString();
- System.out.println("Loading HTML from: " + fileUrl);
-
- // 5. 创建浏览器组件(直接添加到内容面板)
- browser = client.createBrowser(fileUrl, false, false);
- } else {
- System.out.println("Loading HTML from: " + htmlUrl);
- browser = client.createBrowser(htmlUrl, false, false);
- }
-
- Component browserComponent = browser.getUIComponent();
-
- if (builder.browserCreationCallback != null) {
- boolean handled = builder.browserCreationCallback.onLayoutCustomization(
- this, // 当前窗口
- getContentPane(), // 内容面板
- browserComponent, // 浏览器组件
- builder // 构建器对象
- );
-
- // 如果回调返回true,跳过默认布局
- if (handled) {
- // 设置窗口基本属性
- setTitle(builder.title);
- setSize(builder.size);
- setLocationRelativeTo(builder.parentFrame);
- setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
-
- // 添加资源释放监听器
- addWindowListener(new WindowAdapter() {
- @Override
- public void windowClosed(WindowEvent e) {
- browser.close(true);
- client.dispose();
- }
- });
-
- setVisible(true);
- return browserComponent; // 直接返回,跳过默认布局
- }
- }
-
- updateTheme();
-
- CefMessageRouter.CefMessageRouterConfig config = new CefMessageRouter.CefMessageRouterConfig();
- config.jsQueryFunction = "javaQuery";// 定义方法
- config.jsCancelFunction = "javaQueryCancel";// 定义取消方法
-
-
- // 6. 配置窗口布局(确保只添加一次)
- SwingUtilities.invokeLater(() -> {
- getContentPane().removeAll();
- getContentPane().setLayout(new BorderLayout());
-
- // 透明拖拽层(仅顶部可拖拽)
- JPanel dragPanel = new JPanel(new BorderLayout());
- dragPanel.setOpaque(false);
-
- JPanel titleBar = new JPanel();
- titleBar.setOpaque(false);
- titleBar.setPreferredSize(new Dimension(builder.size.width, 20));
- final Point[] dragStart = new Point[1];
- titleBar.addMouseListener(new MouseAdapter() {
- @Override
- public void mousePressed(MouseEvent e) {
- dragStart[0] = e.getPoint();
- }
-
- @Override
- public void mouseReleased(MouseEvent e) {
- dragStart[0] = null;
- }
- });
- titleBar.addMouseMotionListener(new MouseMotionAdapter() {
- @Override
- public void mouseDragged(MouseEvent e) {
- if (dragStart[0] != null) {
- Point curr = e.getLocationOnScreen();
- setLocation(curr.x - dragStart[0].x, curr.y - dragStart[0].y);
- }
- }
- });
-
-
-
- dragPanel.add(titleBar, BorderLayout.NORTH);
- getContentPane().add(dragPanel, BorderLayout.CENTER);
- getContentPane().add(browserComponent, BorderLayout.CENTER);
-
- // 7. 窗口属性设置
- setTitle(builder.title);
- setSize(builder.size);
- setLocationRelativeTo(builder.parentFrame);
- setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
-
- // 8. 资源释放
- addWindowListener(new WindowAdapter() {
- @Override
- public void windowClosed(WindowEvent e) {
- browser.close(true);
- client.dispose();
- }
- });
-
- setVisible(true);
-
- });
- return browserComponent;
- } catch (Exception e) {
- e.printStackTrace();
- JOptionPane.showMessageDialog(null, "初始化失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
- }
- } else {
- isInitialized = false;
- SwingUtilities.invokeLater(() -> {
- dispose();
- });
- }
- return null;
- }
-
-
-
- /**
- * 更新主题
- */
- public void updateTheme() {
- // 1. 获取Java字体信息
- String fontInfo = getSystemFontsInfo();
- injectFontInfoToPage(browser, fontInfo);
-
- // 2. 注入主题信息
- if (AxisInnovatorsBox.getMain() != null) {
- boolean isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode();
- injectThemeInfoToPage(browser, isDarkTheme);
- }
-
- //// 3. 刷新浏览器
- //SwingUtilities.invokeLater(() -> {
- // browser.reload();
- //});
-
- }
- private void injectThemeInfoToPage(CefBrowser browser, boolean isDarkTheme) {
- if (client == null) {
- return;
- }
-
- client.addLoadHandler(new CefLoadHandlerAdapter() {
- @Override
- public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
- String themeInfo = String.format(
- "{\"isDarkTheme\": %s, \"timestamp\": %d}",
- isDarkTheme,
- System.currentTimeMillis()
- );
-
- String script =
- "window.javaThemeInfo = " + themeInfo + ";\n" +
- "console.log('Java theme information has been loaded:', window.javaThemeInfo);\n" +
- "\n" +
- "if (typeof applyJavaTheme === 'function') {\n" +
- " applyJavaTheme(window.javaThemeInfo);\n" +
- "}\n" +
- "\n" +
- "var event = new CustomEvent('javaThemeChanged', {\n" +
- " detail: window.javaThemeInfo\n" +
- "});\n" +
- "document.dispatchEvent(event);\n" +
- "console.log('The javaThemeChanged event is dispatched');";
-
- browser.executeJavaScript(script, browser.getURL(), 0);
-
- browser.executeJavaScript(
- "console.log('Theme information injection is complete,window.javaThemeInfo:', typeof window.javaThemeInfo);" +
- "console.log('Number of theme event listeners:', document.eventListeners ? document.eventListeners('javaThemeChanged') : '无法获取');",
- browser.getURL(), 0
- );
- }
- });
- }
-
-
- /**
- * 获取Java字体信息(从UIManager获取)
- */
- private String getSystemFontsInfo() {
- try {
- Gson gson = new Gson();
- JsonObject fontInfo = new JsonObject();
- JsonObject uiFonts = new JsonObject();
-
- String[] fontKeys = {
- "Label.font", "Button.font", "ToggleButton.font", "RadioButton.font",
- "CheckBox.font", "ColorChooser.font", "ComboBox.font", "EditorPane.font",
- "TextArea.font", "TextField.font", "PasswordField.font", "TextPane.font",
- "FormattedTextField.font", "Table.font", "TableHeader.font", "List.font",
- "Tree.font", "TabbedPane.font", "MenuBar.font", "Menu.font", "MenuItem.font",
- "PopupMenu.font", "CheckBoxMenuItem.font", "RadioButtonMenuItem.font",
- "Spinner.font", "ToolBar.font", "TitledBorder.font", "OptionPane.messageFont",
- "OptionPane.buttonFont", "Panel.font", "Viewport.font", "ToolTip.font"
- };
-
- for (String key : fontKeys) {
- Font font = UIManager.getFont(key);
- if (font != null) {
- JsonObject fontObj = new JsonObject();
- fontObj.addProperty("name", font.getFontName());
- fontObj.addProperty("family", font.getFamily());
- fontObj.addProperty("size", font.getSize());
- fontObj.addProperty("style", font.getStyle());
- fontObj.addProperty("bold", font.isBold());
- fontObj.addProperty("italic", font.isItalic());
- fontObj.addProperty("plain", font.isPlain());
- uiFonts.add(key, fontObj);
- }
- }
-
- fontInfo.add("uiFonts", uiFonts);
- fontInfo.addProperty("timestamp", System.currentTimeMillis());
- fontInfo.addProperty("lookAndFeel", UIManager.getLookAndFeel().getName());
-
- return gson.toJson(fontInfo);
- } catch (Exception e) {
- return "{\"error\": \"无法获取UIManager字体信息: " + e.getMessage() + "\"}";
- }
- }
-
- /**
- * 注入字体信息到页面并设置字体
- */
- private void injectFontInfoToPage(CefBrowser browser, String fontInfo) {
- if (client == null) {
- return;
- }
- client.addLoadHandler(new CefLoadHandlerAdapter() {
- @Override
- public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
- // 使用更简单的脚本来注入字体信息
- String script =
- "if (typeof window.javaFontInfo === 'undefined') {" +
- " window.javaFontInfo = " + fontInfo + ";" +
- " console.log('Java font information has been loaded:', window.javaFontInfo);" +
- " " +
- " var event = new CustomEvent('javaFontsLoaded', {" +
- " detail: window.javaFontInfo" +
- " });" +
- " document.dispatchEvent(event);" +
- " console.log('The javaFontsLoaded event is dispatched');" +
- "}";
-
- System.out.println("正在注入字体信息到页面...");
- browser.executeJavaScript(script, browser.getURL(), 0);
-
- // 添加调试信息
- browser.executeJavaScript(
- "console.log('Font information injection is complete,window.javaFontInfo:', typeof window.javaFontInfo);" +
- "console.log('Number of event listeners:', document.eventListeners ? document.eventListeners('javaFontsLoaded') : '无法获取');",
- browser.getURL(), 0
- );
- }
- });
-
- }
-
- public static void printStackTrace() {
- StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
- for (int i = 2; i < stackTrace.length; i++) {
- StackTraceElement element = stackTrace[i];
- System.out.println(element.getClassName() + "." + element.getMethodName() +
- "(" + (element.getFileName() != null ? element.getFileName() : "Unknown Source") +
- ":" + element.getLineNumber() + ")");
- }
- }
-
- @Override
- public void setVisible(boolean b) {
- if (b) {
- if (browser != null) {
- updateTheme();
- }
- }
- super.setVisible(b);
- }
-
- public Component getBrowserComponent() {
- return browserComponent;
- }
-
- private void setupMessageHandlers(WindowOperationHandler handler) {
- if (client != null) {
- msgRouter = CefMessageRouter.create();
- msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
- @Override
- public boolean onQuery(CefBrowser browser,
- CefFrame frame,
- long queryId,
- String request,
- boolean persistent,
- CefQueryCallback callback) {
- if (request.startsWith("system:")) {
- String[] parts = request.split(":");
- String operation = parts.length >= 2 ? parts[1] : null;
- String targetWindow = parts.length > 2 ? parts[2] : null;
- handler.handleOperation(
- new WindowOperation(operation, targetWindow, callback) // [!code ++]
- );
- return true;
- }
- if (request.startsWith("java-response:")) {
- String[] parts = request.split(":");
- String requestId = parts[1];
- String responseData = parts.length > 2 ? parts[2] : "";
- Consumer handler = WindowRegistry.getInstance().getCallback(requestId);
- if (handler != null) {
- handler.accept(responseData);
- callback.success("");
- } else {
- callback.failure(-1, "无效的请求ID");
- }
- return true;
- }
- return false;
- }
- }, true);
- client.addMessageRouter(msgRouter);
- }
- }
-
- public String getWindowId() {
- return windowId;
- }
-
- /**
- * 获取消息路由器
- * @return 消息路由器
- */
- public CefMessageRouter getMsgRouter() {
- return msgRouter;
- }
-
- /**
- * 获取浏览器对象
- * @return 浏览器对象
- */
- public CefBrowser getBrowser() {
- return browser;
- }
-
- public void closeWindow() {
- SwingUtilities.invokeLater(() -> {
- if (browser != null) {
- browser.close(true);
- }
- dispose();
- cefApp.dispose();
- WindowRegistry.getInstance().unregisterWindow(windowId);
- });
- }
-}
-
diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/CefAppManager.java b/src/main/java/com/chuangzhou/vivid2D/browser/CefAppManager.java
deleted file mode 100644
index f858675..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/browser/CefAppManager.java
+++ /dev/null
@@ -1,280 +0,0 @@
-package com.chuangzhou.vivid2D.browser;
-
-import com.axis.innovators.box.AxisInnovatorsBox;
-import com.axis.innovators.box.register.LanguageManager;
-import com.axis.innovators.box.tools.FolderCreator;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.cef.CefApp;
-import org.cef.CefSettings;
-import org.cef.callback.CefCommandLine;
-import org.cef.handler.CefAppHandlerAdapter;
-
-import java.io.File;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
-/**
- * 增强版CEF应用管理器
- * 特性:
- * 1. 多级锁并发控制
- * 2. 设置冲突自动恢复
- * 3. 状态跟踪和验证
- * 4. 增强的异常处理
- *
- * @author tzdwindows 7
- */
-public class CefAppManager {
- private static final Logger logger = LogManager.getLogger(CefAppManager.class);
- private static volatile CefApp cefApp;
- private static final CefSettings settings = new CefSettings();
-
- // 状态跟踪
- private static final AtomicBoolean isInitialized = new AtomicBoolean(false);
- private static final AtomicBoolean settingsApplied = new AtomicBoolean(false);
- private static final AtomicBoolean isDisposing = new AtomicBoolean(false);
-
- // 并发控制
- private static final Lock initLock = new ReentrantLock();
- private static final Lock disposeLock = new ReentrantLock();
- private static final AtomicBoolean shutdownHookRegistered = new AtomicBoolean(false);
-
- static {
- initializeDefaultSettings();
- registerShutdownHook();
- }
-
- private static void registerShutdownHook() {
- if (shutdownHookRegistered.compareAndSet(false, true)) {
- Runtime.getRuntime().addShutdownHook(new Thread(() -> {
- logger.info("JVM shutdown hook triggered");
- dispose(true);
- }));
- logger.debug("Shutdown hook registered successfully");
- }
- }
-
- /**
- * 初始化Cef
- */
- private static void initializeDefaultSettings() {
- initLock.lock();
- try {
- settings.windowless_rendering_enabled = false;
- settings.javascript_flags = "";
- settings.cache_path = FolderCreator.getLibraryFolder() + "/jcef/cache";
- settings.root_cache_path = FolderCreator.getLibraryFolder() + "/jcef/cache";
- settings.persist_session_cookies = false;
- settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_WARNING;
-
- String subprocessPath = FolderCreator.getLibraryFolder() + "/jcef/lib/win64/jcef_helper.exe";
- validateSubprocessPath(subprocessPath);
- settings.browser_subprocess_path = subprocessPath;
-
- //settings.background_color = new Color(255, 255, 255, 0);
- settings.command_line_args_disabled = false;
-
- // 转换语言标识格式:system:zh_CN -> zh-CN
-
- CefApp.addAppHandler(new CefAppHandlerAdapter(null) {
- @Override
- public void onBeforeCommandLineProcessing(
- String processType,
- CefCommandLine commandLine
- ) {
- //commandLine.appendSwitch("disable-dev-tools");
- //commandLine.appendSwitch("disable-view-source");
-
- LanguageManager.loadSavedLanguage();
- LanguageManager.Language currentLang = LanguageManager.getLoadedLanguages();
- if (currentLang != null){
- String langCode = currentLang.getRegisteredName()
- .replace("system:", "")
- .replace("_", "-")
- .toLowerCase();
- settings.locale = langCode;
- commandLine.appendSwitchWithValue("--lang", langCode);
- commandLine.appendSwitchWithValue("--accept-language", langCode);
- }
-
- boolean isDarkTheme = isDarkTheme();
- if (isDarkTheme) {
- commandLine.appendSwitch("force-dark-mode");
- commandLine.appendSwitchWithValue("enable-features", "WebContentsForceDark");
- }
-
- logger.info("CEF commandLine: {}", commandLine.getSwitches());
- }
- });
-
- logger.info("Optimized CEF settings initialized");
- } finally {
- initLock.unlock();
- }
- }
-
- /**
- * 判断地区主题是否为黑色主题
- * @return 是否
- */
- private static boolean isDarkTheme() {
- if (AxisInnovatorsBox.getMain() == null){
- return false;
- }
- return AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode();
- }
-
- public static CefApp getInstance() {
- if (cefApp == null) {
- if (initLock.tryLock()) {
- try {
- performSafeInitialization();
- } finally {
- initLock.unlock();
- }
- } else {
- handleConcurrentInitialization();
- }
- }
- return cefApp;
- }
-
- private static void performSafeInitialization() {
- if (cefApp != null) return;
- if (isDisposing.get()) {
- throw new IllegalStateException("CEF is during disposal process");
- }
-
- try {
- // 阶段1:启动CEF运行时
- if (!CefApp.startup(new String[0])) {
- throw new IllegalStateException("CEF native startup failed");
- }
-
- // 阶段2:应用设置(仅首次)
- if (settingsApplied.compareAndSet(false, true)) {
- cefApp = CefApp.getInstance(settings);
- logger.info("CEF initialized with custom settings");
- } else {
- cefApp = CefApp.getInstance();
- logger.info("CEF reused existing instance");
- }
-
- isInitialized.set(true);
- } catch (IllegalStateException ex) {
- handleInitializationError(ex);
- }
- }
-
- private static void handleConcurrentInitialization() {
- try {
- if (initLock.tryLock(3, TimeUnit.SECONDS)) {
- try {
- if (cefApp == null) {
- performSafeInitialization();
- }
- } finally {
- initLock.unlock();
- }
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- logger.warn("CEF initialization interrupted");
- }
- }
-
- private static void handleInitializationError(IllegalStateException ex) {
- if (ex.getMessage().contains("Settings can only be passed")) {
- logger.warn("Settings conflict detected, recovering...");
- recoverFromSettingsConflict();
- } else if (ex.getMessage().contains("was terminated")) {
- handleTerminatedState(ex);
- } else {
- logger.error("Critical CEF error", ex);
- throw new RuntimeException("CEF initialization failed", ex);
- }
- }
-
- private static void recoverFromSettingsConflict() {
- disposeLock.lock();
- try {
- if (cefApp == null) {
- cefApp = CefApp.getInstance();
- settingsApplied.set(true);
- isInitialized.set(true);
- logger.info("Recovered from settings conflict");
- }
- } finally {
- disposeLock.unlock();
- }
- }
-
- private static void handleTerminatedState(IllegalStateException ex) {
- disposeLock.lock();
- try {
- logger.warn("CEF terminated state detected");
- dispose(false);
- performEmergencyRecovery();
- } finally {
- disposeLock.unlock();
- }
- }
-
- private static void performEmergencyRecovery() {
- try {
- logger.info("Attempting emergency recovery...");
- CefApp.startup(new String[0]);
- cefApp = CefApp.getInstance();
- isInitialized.set(true);
- settingsApplied.set(true);
- logger.info("Emergency recovery successful");
- } catch (Exception e) {
- logger.error("Emergency recovery failed", e);
- throw new RuntimeException("Unrecoverable CEF state", e);
- }
- }
-
- public static synchronized void dispose(boolean isShutdownHook) {
- disposeLock.lock();
- try {
- if (cefApp == null || isDisposing.get()) return;
-
- isDisposing.set(true);
- try {
- logger.info("Disposing CEF resources...");
- cefApp.dispose();
-
- if (!isShutdownHook) {
- cefApp = null;
- isInitialized.set(false);
- settingsApplied.set(false);
- }
- logger.info("CEF resources released");
- } catch (Exception e) {
- logger.error("Disposal error", e);
- } finally {
- isDisposing.set(false);
- }
- } finally {
- disposeLock.unlock();
- }
- }
-
- private static void validateSubprocessPath(String path) {
- File exeFile = new File(path);
- if (!exeFile.exists()) {
- String errorMsg = "JCEF helper executable missing: " + path;
- logger.error(errorMsg);
- throw new IllegalStateException(errorMsg);
- }
- logger.debug("Validated JCEF helper at: {}", path);
- }
-
- // 状态查询接口
- public static String getInitStatus() {
- return String.format("Initialized: %s, SettingsApplied: %s, Disposing: %s",
- isInitialized.get(), settingsApplied.get(), isDisposing.get());
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/MainApplication.java b/src/main/java/com/chuangzhou/vivid2D/browser/MainApplication.java
deleted file mode 100644
index 810f6c4..0000000
--- a/src/main/java/com/chuangzhou/vivid2D/browser/MainApplication.java
+++ /dev/null
@@ -1,2002 +0,0 @@
-package com.chuangzhou.vivid2D.browser;
-
-import com.axis.innovators.box.AxisInnovatorsBox;
-import com.axis.innovators.box.tools.FolderCreator;
-import com.chuangzhou.vivid2D.browser.util.CodeExecutor;
-import com.chuangzhou.vivid2D.browser.util.DatabaseConnectionManager;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import org.cef.browser.CefBrowser;
-import org.cef.browser.CefFrame;
-import org.cef.browser.CefMessageRouter;
-import org.cef.callback.CefQueryCallback;
-import org.cef.handler.CefMessageRouterHandlerAdapter;
-import org.graalvm.polyglot.Context;
-import org.graalvm.polyglot.Value;
-import org.json.JSONArray;
-import org.json.JSONObject;
-import org.tzd.lm.LM;
-
-import javax.swing.*;
-import java.io.PrintStream;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.sql.*;
-import java.util.*;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * 这是一个简单的示例程序,用于展示如何使用JCEF来创建一个简单的浏览器窗口。
- * @author tzdwindows 7
- */
-
-public class MainApplication {
- private static final ExecutorService executor = Executors.newCachedThreadPool();
- private static long modelHandle;
- private static long ctxHandle;
- private static boolean isSystem = true;
- public static void main(String[] args) {
- AtomicReference window = new AtomicReference<>();
- WindowRegistry.getInstance().createNewWindow("main", builder ->
- window.set(builder.title("Axis Innovators Box AI 工具箱")
- .size(1280, 720)
- .htmlUrl("https://www.bilibili.com/")
- .openLinksInBrowser(true)
- .operationHandler(createOperationHandler())
- .build())
- );
- }
-
-
- /**
- * 弹出AI窗口
- * @param parent 父窗口
- */
- public static void popupAIWindow(JFrame parent) {
- LM.loadLibrary(LM.CUDA);
- modelHandle = LM.llamaLoadModelFromFile(LM.DEEP_SEEK);
- ctxHandle = LM.createContext(modelHandle);
-
- AtomicReference window = new AtomicReference<>();
- SwingUtilities.invokeLater(() -> {
- WindowRegistry.getInstance().createNewChildWindow("main", builder ->
- window.set(builder.title("Axis Innovators Box AI 工具箱")
- .parentFrame(parent)
- .icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
- .size(1280, 720)
- .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "AIaToolbox_dark.html")
- .operationHandler(createOperationHandler())
- .build())
- );
-
- CefMessageRouter msgRouter = window.get().getMsgRouter();
- if (msgRouter != null) {
- msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
- @Override
- public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId,
- String request, boolean persistent, CefQueryCallback callback) {
- // 处理浏览器请求
- handleBrowserQuery(browser, request, callback);
- return true;
- }
-
- @Override
- public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) {
- // 处理请求取消
- }
- }, true);
- }
-
- });
- }
-
- public static void popupCCodeEditorWindow() {
- AtomicReference window = new AtomicReference<>();
- SwingUtilities.invokeLater(() -> {
- WindowRegistry.getInstance().createNewWindow("main", builder ->
- window.set(builder.title("TzdC 代码编辑器")
- .icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
- .size(1487, 836)
- .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "CCodeEditor.html")
- .operationHandler(createOperationHandler())
- .build())
- );
-
- CefMessageRouter msgRouter = window.get().getMsgRouter();
- if (msgRouter != null) {
- msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
- @Override
- public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId,
- String request, boolean persistent, CefQueryCallback callback) {
- try {
- JSONObject requestJson = new JSONObject(request);
- if ("executeCode".equals(requestJson.optString("type"))) {
- String code = requestJson.optString("code");
- String language = requestJson.optString("language");
-
- // 调用代码执行逻辑
- String result = CodeExecutor.executeCode(code, language,null);
-
- JSONObject response = new JSONObject();
- response.put("status", "success");
- response.put("output", result);
- callback.success(response.toString());
- return true;
- }
- } catch (Exception e) {
- JSONObject error = new JSONObject();
- error.put("status", "error");
- error.put("message", e.getMessage());
- callback.failure(500, error.toString());
- }
- return false;
- }
-
- @Override
- public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) {
- // 处理请求取消
- }
- }, true);
- }
- });
- }
-
- public static void popupCodeEditorWindow() {
- AtomicReference window = new AtomicReference<>();
- SwingUtilities.invokeLater(() -> {
- WindowRegistry.getInstance().createNewWindow("main", builder ->
- window.set(builder.title("代码编辑器")
- .icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
- .size(1487, 836)
- .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "CodeEditor.html")
- .operationHandler(createOperationHandler())
- .build())
- );
-
- CefMessageRouter msgRouter = window.get().getMsgRouter();
- if (msgRouter != null) {
- msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
- @Override
- public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId,
- String request, boolean persistent, CefQueryCallback callback) {
- try {
- JSONObject requestJson = new JSONObject(request);
- if ("executeCode".equals(requestJson.optString("type"))) {
- String code = requestJson.optString("code");
- String language = requestJson.optString("language");
-
- // 调用代码执行逻辑
- String result = executeCode(code, language);
-
- JSONObject response = new JSONObject();
- response.put("status", "success");
- response.put("output", result);
- callback.success(response.toString());
- return true;
- }
- } catch (Exception e) {
- JSONObject error = new JSONObject();
- error.put("status", "error");
- error.put("message", e.getMessage());
- callback.failure(500, error.toString());
- }
- return false;
- }
-
- @Override
- public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) {
- // 处理请求取消
- }
- }, true);
- }
- });
- }
-
- public static String executeCode(String code, String language) {
- return CodeExecutor.executeCode(code, language,new CodeExecutor.OutputListener() {
- @Override
- public void onOutput(String newOutput) {}
- });
- }
-
- private static Value executeC(Context context, String code) {
- return context.eval("c", code);
- }
-
- /**
- * 弹出html预览窗口
- */
- public static void popupHTMLWindow(String path) {
- AtomicReference window = new AtomicReference<>();
- SwingUtilities.invokeLater(() -> {
- WindowRegistry.getInstance().createNewWindow("main", builder ->
- window.set(builder.title("Axis Innovators Box HTML查看器")
- .icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
- .size(1487, 836)
- .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "HtmlViewer.html")
- .operationHandler(createOperationHandler())
- .build())
- );
-
- CefMessageRouter msgRouter = window.get().getMsgRouter();
- if (msgRouter != null) {
- msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
- @Override
- public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId,
- String request, boolean persistent, CefQueryCallback callback) {
- try {
- // 解析JSON请求
- JsonObject requestJson = JsonParser.parseString(request).getAsJsonObject();
- if (requestJson.has("type") && "loadInitialContent".equals(requestJson.get("type").getAsString())) {
-
- Path filePath = Paths.get(path);
-
- // 验证文件存在性
- if (!Files.exists(filePath)) {
- callback.failure(404, "{\"code\":404,\"message\":\"文件未找到\"}");
- return true;
- }
-
- // 读取文件内容
- String content = Files.readString(filePath, StandardCharsets.UTF_8);
- callback.success(content);
- return true;
- }
- } catch (Exception e) {
- callback.failure(500, "{\"code\":500,\"message\":\"" + e.getMessage() + "\"}");
- }
- return false;
- }
-
- @Override
- public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) {
- // 处理请求取消
- }
- }, true);
- }
- });
- }
-
- private static void handleBrowserQuery(CefBrowser browser, String request, CefQueryCallback callback) {
- try {
- String[] parts = request.split(":", 3);
- if (parts.length < 3) {
- callback.failure(400, "请求格式错误");
- return;
- }
-
- String operation = parts[0];
- String requestId = parts[1];
- String prompt = parts[2];
-
- if ("ai-inference".equals(operation)) {
- executor.execute(() -> {
- if (isSystem) {
- isSystem = false;
- }
- List messageList = new ArrayList<>(List.of());
- // 修改后的推理回调处理
- String jsCode = String.format(
- "if (typeof updateResponse === 'function') {" +
- " updateResponse('%s', '%s');" +
- "}",
- requestId, "" +
- "\\n" +
- "推理内容
"
- );
- browser.executeJavaScript(jsCode, null, 0);
- LM.inference(modelHandle, ctxHandle, 0.6f, prompt + "\n","",
- new LM.MessageCallback() {
- private boolean thinkingClosed = false;
-//
- @Override
- public void onMessage(String message) {
- messageList.add(message);
- SwingUtilities.invokeLater(() -> {
- // 统一转义处理
- String escaped = message
- .replace("\\", "\\\\")
- .replace("'", "\\'")
- .replace("\"", "\\\"")
- .replace("\n", "\\n")
- .replace("\r", "\\r");
-//
- if (messageList.contains("") && !thinkingClosed) {
- String endJs = String.format(
- "if (typeof updateResponse === 'function') {" +
- " updateResponse('%s', '%s');" +
- "}",
- requestId, " "
- );
- browser.executeJavaScript(endJs, null, 0);
- thinkingClosed = true;
- }
-//
- // 实时更新内容
- System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8));
-//
- String jsCode = String.format(
- "if (typeof updateResponse === 'function') {" +
- " updateResponse('%s', '%s');" +
- "}",
- requestId, escaped
- );
- browser.executeJavaScript(jsCode, null, 0);
- });
- }
- },isSystem);
- messageList.clear();
- //jsCode = String.format(
- // "if (typeof updateResponse === 'function') {" +
- // " updateResponse('%s', '%s');" +
- // "}",
- // requestId, "嗯,用户问的是“请直接告诉我傅里叶变换公式”。首先,我需要回忆一下傅里叶变换的基本知识。傅里叶变换是将一个时间域的信号转换为频率域的信号,它在工程和科学研究中有着广泛的应用。\n\n接下来,我要确定傅里叶变换的数学表达式。标准形式应该是$$F(\\omega) = \\int_{-\\infty}^{\\infty} f(t) e^{-i\\omega t} dt$$。这里,$f(t)$是原函数,$e^{-i\\omega t}$是指数函数,$\\omega$是频率变量。\n\n然后,我需要考虑是否有其他形式的傅里叶变换,比如离散形式或逆变换。通常,离散傅里叶变换(DFT)使用$$X[k] = \\sum_{n=0}^{N-1} x[n] e^{-i2\\pi kn/N}$$来表示,而逆变换则是$$x[n] = \\frac{1}{N} \\sum_{k=0}^{N-1} X[k] e^{i2\\pi kn/N}$$。不过,用户的问题比较直接,可能只关注基本的连续形式。\n\n最后,我要确保回答准确无误,并且按照用户的格式要求使用标准的 LaTeX æ式来呈现。\n\n\n傅里叶变换的基本公式是:$$F(\\omega) = \\int_{-\\infty}^{\\infty} f(t) e^{-i\\omega t} dt$$".replace("\\", "\\\\")
- // .replace("'", "\\'")
- // .replace("\"", "\\\"")
- // .replace("\n", "\\n")
- // .replace("\r", "\\r")
- //);
- //browser.executeJavaScript(jsCode, null, 0);
- callback.success("COMPLETED:" + requestId);
- });
- }
- } catch (Exception e) {
- callback.failure(500, "服务器错误: " + e.getMessage());
- }
- }
-
- private static WindowOperationHandler createOperationHandler() {
- return new WindowOperationHandler.Builder()
- .withDefaultOperations()
- .build();
- }
-
- public static void popupDataBaseWindow() {
- // 预加载常用 JDBC 驱动(警告级别,不阻塞 UI)
- try {
- try { Class.forName("org.h2.Driver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: org.h2.Driver 未找到"); }
- try { Class.forName("org.sqlite.JDBC"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: org.sqlite.JDBC 未找到"); }
- try { Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: org.postgresql.Driver 未找到"); }
- try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: com.mysql.cj.jdbc.Driver 未找到"); }
- try { Class.forName("oracle.jdbc.OracleDriver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: oracle.jdbc.OracleDriver 未找到"); }
- } catch (Throwable t) {
- System.err.println("预加载 JDBC 驱动时发生异常: " + t.getMessage());
- }
-
- AtomicReference window = new AtomicReference<>();
- SwingUtilities.invokeLater(() -> {
- WindowRegistry.getInstance().createNewWindow("main", builder ->
- window.set(builder.title("Axis Innovators Box 数据库管理工具")
- .icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
- .size(1487, 836)
- .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "DatabaseTool.html")
- .operationHandler(createOperationHandler())
- .build())
- );
-
- if (window.get() == null) {
- System.err.println("popupDataBaseWindow: window 创建失败,window.get() == null");
- return;
- }
-
- CefMessageRouter msgRouter = window.get().getMsgRouter();
- if (msgRouter != null) {
- msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
- @Override
- public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId,
- String request, boolean persistent, CefQueryCallback callback) {
- try {
- JSONObject requestJson = new JSONObject(request);
- String type = requestJson.optString("type", "");
-
- // 补默认端口与标准化 driver 名称
- String drv = requestJson.optString("driver", "").toLowerCase();
- if (!drv.isEmpty()) {
- String port = requestJson.optString("port", "").trim();
- if (port.isEmpty()) {
- switch (drv) {
- case "mysql":
- requestJson.put("port", "3306");
- break;
- case "postgresql":
- case "postgres":
- requestJson.put("port", "5432");
- break;
- case "oracle":
- requestJson.put("port", "1521");
- break;
- }
- }
- if ("postgres".equals(drv)) requestJson.put("driver", "postgresql");
- }
-
- // 安全标识符校验 pattern(表名/列名等仅允许常见字符)
- final java.util.regex.Pattern SAFE_IDENT = java.util.regex.Pattern.compile("^[A-Za-z0-9_\\.\\$]+$");
-
- switch (type) {
- // 已有功能:继续使用现有处理函数(假定这些方法在类中定义)
- case "connectDatabase":
- handleDatabaseConnect(requestJson, callback);
- break;
- case "createLocalDatabase":
- handleCreateLocalDatabase(requestJson, callback);
- break;
- case "disconnectDatabase":
- handleDisconnectDatabase(requestJson, callback);
- break;
- case "executeQuery":
- handleExecuteQuery(requestJson, callback);
- break;
- case "getTables":
- handleGetTables(requestJson, callback);
- break;
- case "getTableData":
- handleGetTableData(requestJson, callback);
- break;
- case "getTableStructure":
- handleGetTableStructure(requestJson, callback);
- break;
- case "updateTheme":
- handleUpdateTheme(requestJson, callback);
- break;
- case "getFonts":
- handleGetFonts(requestJson, callback);
- break;
-
- // 新增后端支持:analyzeQuery(EXPLAIN / EXPLAIN ANALYZE)
- case "analyzeQuery": {
- String connectionId = requestJson.optString("connectionId", "");
- String query = requestJson.optString("query", "").trim();
- if (connectionId.isEmpty() || query.isEmpty()) {
- callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 或 query 为空").toString());
- break;
- }
- Connection conn = DatabaseConnectionManager.getConnection(connectionId);
- if (conn == null || conn.isClosed()) {
- callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString());
- break;
- }
- // 依据驱动选择 EXPLAIN 语法
- DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId);
- String drvName = info == null ? "" : (info.driver == null ? "" : info.driver.toLowerCase());
- String explainSql = "EXPLAIN " + query;
- if ("postgresql".equals(drvName)) {
- explainSql = "EXPLAIN ANALYZE " + query;
- }
- try (Statement st = conn.createStatement();
- ResultSet rs = st.executeQuery(explainSql)) {
- JSONArray out = new JSONArray();
- while (rs.next()) {
- // EXPLAIN 输出常为单列文本
- out.put(rs.getString(1));
- }
- JSONObject resp = new JSONObject();
- resp.put("status","success");
- resp.put("explain", out);
- callback.success(resp.toString());
- } catch (SQLException ex) {
- JSONObject err = new JSONObject();
- err.put("status","error");
- err.put("message","分析失败: " + ex.getMessage());
- callback.failure(500, err.toString());
- }
- break;
- }
-
- // exportData -> 导出为 CSV 或 JSON,写入用户目录下 .axis_innovators_box/exports/
- case "exportData": {
- String connectionId = requestJson.optString("connectionId", "");
- String table = requestJson.optString("table", "");
- String format = requestJson.optString("format", "csv").toLowerCase();
- if (connectionId.isEmpty() || table.isEmpty()) {
- callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 或 table 为空").toString());
- break;
- }
- if (!SAFE_IDENT.matcher(table).matches()) {
- callback.failure(400, new JSONObject().put("status","error").put("message","非法表名").toString());
- break;
- }
- Connection conn = DatabaseConnectionManager.getConnection(connectionId);
- if (conn == null || conn.isClosed()) {
- callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString());
- break;
- }
-
- Path exportDir = Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "exports");
- try { Files.createDirectories(exportDir); } catch (Exception e) { /* ignore */ }
-
- String filenameBase = table + "_" + System.currentTimeMillis();
- Path outPath = exportDir.resolve(filenameBase + (format.equals("json") ? ".json" : ".csv"));
-
- String query = "SELECT * FROM " + table;
- try (Statement st = conn.createStatement();
- ResultSet rs = st.executeQuery(query)) {
-
- ResultSetMetaData md = rs.getMetaData();
- int cols = md.getColumnCount();
-
- if ("json".equals(format)) {
- JSONArray arr = new JSONArray();
- while (rs.next()) {
- JSONObject obj = new JSONObject();
- for (int i = 1; i <= cols; i++) {
- Object val = rs.getObject(i);
- obj.put(md.getColumnLabel(i), val == null ? JSONObject.NULL : val);
- }
- arr.put(obj);
- }
- Files.write(outPath, arr.toString(2).getBytes(StandardCharsets.UTF_8));
- } else {
- try (java.io.BufferedWriter writer = Files.newBufferedWriter(
- outPath,
- StandardCharsets.UTF_8
- )) {
- // 写入 UTF-8 BOM
- writer.write('\uFEFF');
-
- for (int i = 1; i <= cols; i++) {
- if (i > 1) writer.write(",");
- writer.write("\"" + md.getColumnLabel(i).replace("\"", "\"\"") + "\"");
- }
- writer.write("\n");
-
- while (rs.next()) {
- for (int i = 1; i <= cols; i++) {
- if (i > 1) writer.write(",");
- Object val = rs.getObject(i);
- String cell = val == null ? "" : String.valueOf(val);
- writer.write("\"" + cell.replace("\"", "\"\"") + "\"");
- }
- writer.write("\n");
- }
- }
-
- }
-
- try {
- String pathStr = outPath.toAbsolutePath().toString();
- String os = System.getProperty("os.name").toLowerCase();
- if (os.contains("win")) {
- new ProcessBuilder("explorer.exe", "/select," + pathStr).start();
- }
- } catch (Exception ignore) {
- }
-
- JSONObject resp = new JSONObject();
- resp.put("status","success");
- resp.put("path", outPath.toAbsolutePath().toString());
- resp.put("message","导出成功");
- callback.success(resp.toString());
- } catch (SQLException | java.io.IOException ex) {
- JSONObject err = new JSONObject();
- err.put("status","error");
- err.put("message","导出失败: " + ex.getMessage());
- callback.failure(500, err.toString());
- }
- break;
- }
-
- // importCsv -> 从给定 path 导入 CSV,要求首行为列名且列名匹配表字段
- case "importCsv": {
- String connectionId = requestJson.optString("connectionId", "");
- String table = requestJson.optString("table", "");
- String path = requestJson.optString("path", "");
- if (connectionId.isEmpty() || table.isEmpty() || path.isEmpty()) {
- callback.failure(400, new JSONObject().put("status","error").put("message","参数不完整").toString());
- break;
- }
- if (!SAFE_IDENT.matcher(table).matches()) {
- callback.failure(400, new JSONObject().put("status","error").put("message","非法表名").toString());
- break;
- }
- Path csvPath = Paths.get(path);
- if (!Files.exists(csvPath)) {
- callback.failure(400, new JSONObject().put("status","error").put("message","CSV 文件不存在").toString());
- break;
- }
- Connection conn = DatabaseConnectionManager.getConnection(connectionId);
- if (conn == null || conn.isClosed()) {
- callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString());
- break;
- }
-
- // 读取首行作为列头
- try (java.io.BufferedReader br = Files.newBufferedReader(csvPath, StandardCharsets.UTF_8)) {
- String headerLine = br.readLine();
- if (headerLine == null) {
- callback.failure(400, new JSONObject().put("status","error").put("message","CSV 为空").toString());
- break;
- }
- // 简单 CSV 解析(支持双引号),但要求列名没有逗号内部双引号结构复杂情形
- String[] columns = headerLine.split(",");
- for (int i = 0; i < columns.length; i++) {
- columns[i] = columns[i].trim().replaceAll("^\"|\"$", ""); // 去掉可能的两端引号
- if (!SAFE_IDENT.matcher(columns[i]).matches()) {
- callback.failure(400, new JSONObject().put("status","error").put("message","非法列名: " + columns[i]).toString());
- return true;
- }
- }
- // 构建 INSERT SQL
- StringBuilder placeholders = new StringBuilder();
- for (int i = 0; i < columns.length; i++) {
- if (i > 0) placeholders.append(",");
- placeholders.append("?");
- }
- String insertSql = "INSERT INTO " + table + " (" + String.join(",", columns) + ") VALUES (" + placeholders.toString() + ")";
- conn.setAutoCommit(false);
- int imported = 0;
- try (PreparedStatement pstmt = conn.prepareStatement(insertSql)) {
- String line;
- while ((line = br.readLine()) != null) {
- // 简单分割(不处理复杂引号内部逗号)
- String[] parts = line.split(",", -1);
- for (int i = 0; i < columns.length; i++) {
- String cell = i < parts.length ? parts[i].trim().replaceAll("^\"|\"$", "") : "";
- pstmt.setString(i + 1, cell.isEmpty() ? null : cell);
- }
- pstmt.addBatch();
- if (++imported % 500 == 0) pstmt.executeBatch();
- }
- pstmt.executeBatch();
- conn.commit();
- } catch (SQLException ex) {
- conn.rollback();
- throw ex;
- } finally {
- conn.setAutoCommit(true);
- }
- JSONObject resp = new JSONObject();
- resp.put("status","success");
- resp.put("imported", imported);
- resp.put("message", "导入完成");
- callback.success(resp.toString());
- } catch (Exception ex) {
- JSONObject err = new JSONObject();
- err.put("status","error");
- err.put("message","导入失败: " + ex.getMessage());
- callback.failure(500, err.toString());
- }
- break;
- }
-
- // generateEr -> 收集表、列信息并返回 JSON(前端可据此生成 ER 图)
- case "generateEr": {
- String connectionId = requestJson.optString("connectionId", "");
- if (connectionId.isEmpty()) {
- callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 为空").toString());
- break;
- }
- Connection conn = DatabaseConnectionManager.getConnection(connectionId);
- if (conn == null || conn.isClosed()) {
- callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString());
- break;
- }
- try {
- DatabaseMetaData meta = conn.getMetaData();
- JSONArray tablesArr = new JSONArray();
-
- try (ResultSet rsTables = meta.getTables(conn.getCatalog(), null, null, new String[]{"TABLE"})) {
- while (rsTables.next()) {
- String tbl = rsTables.getString("TABLE_NAME");
- JSONObject tObj = new JSONObject();
- tObj.put("name", tbl);
- JSONArray cols = new JSONArray();
- try (ResultSet rsCols = meta.getColumns(conn.getCatalog(), null, tbl, null)) {
- while (rsCols.next()) {
- JSONObject c = new JSONObject();
- c.put("name", rsCols.getString("COLUMN_NAME"));
- c.put("type", rsCols.getString("TYPE_NAME"));
- c.put("size", rsCols.getInt("COLUMN_SIZE"));
- c.put("nullable", rsCols.getInt("NULLABLE") == DatabaseMetaData.columnNullable);
- cols.put(c);
- }
- }
- tObj.put("columns", cols);
- tablesArr.put(tObj);
- }
- }
-
- JSONObject resp = new JSONObject();
- resp.put("status","success");
- resp.put("er", new JSONObject().put("tables", new JSONArray(tablesArr.toString())));
- callback.success(resp.toString());
- } catch (SQLException ex) {
- JSONObject err = new JSONObject();
- err.put("status","error");
- err.put("message","生成 ER 失败: " + ex.getMessage());
- callback.failure(500, err.toString());
- }
- break;
- }
-
- // analyzePerformance -> 尝试返回当前数据库会话/进程信息(简单实现,依据数据库类型)
- case "analyzePerformance": {
- String connectionId = requestJson.optString("connectionId", "");
- if (connectionId.isEmpty()) {
- callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 为空").toString());
- break;
- }
- Connection conn = DatabaseConnectionManager.getConnection(connectionId);
- if (conn == null || conn.isClosed()) {
- callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString());
- break;
- }
- DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId);
- String drvName = info == null ? "" : (info.driver == null ? "" : info.driver.toLowerCase());
- try {
- JSONArray out = new JSONArray();
- if ("postgresql".equals(drvName)) {
- try (Statement st = conn.createStatement();
- ResultSet rs = st.executeQuery("SELECT pid, usename, state, query FROM pg_stat_activity LIMIT 50")) {
- while (rs.next()) {
- JSONObject r = new JSONObject();
- r.put("pid", rs.getObject("pid"));
- r.put("user", rs.getString("usename"));
- r.put("state", rs.getString("state"));
- r.put("query", rs.getString("query"));
- out.put(r);
- }
- }
- } else if ("mysql".equals(drvName)) {
- try (Statement st = conn.createStatement();
- ResultSet rs = st.executeQuery("SHOW PROCESSLIST")) {
- while (rs.next()) {
- JSONObject r = new JSONObject();
- r.put("Id", rs.getObject("Id"));
- r.put("User", rs.getString("User"));
- r.put("Host", rs.getString("Host"));
- r.put("db", rs.getString("db"));
- r.put("Command", rs.getString("Command"));
- r.put("Time", rs.getString("Time"));
- r.put("State", rs.getString("State"));
- r.put("Info", rs.getString("Info"));
- out.put(r);
- }
- }
- } else {
- // 通用替代:返回当前时间与简单连接信息
- JSONObject r = new JSONObject();
- r.put("now", java.time.Instant.now().toString());
- r.put("message","未实现针对该数据库的详细性能查询,返回通用信息");
- out.put(r);
- }
- JSONObject resp = new JSONObject();
- resp.put("status","success");
- resp.put("data", new JSONArray(out.toString()));
- callback.success(resp.toString());
- } catch (SQLException ex) {
- JSONObject err = new JSONObject();
- err.put("status","error");
- err.put("message","性能分析失败: " + ex.getMessage());
- callback.failure(500, err.toString());
- }
- break;
- }
-
- // listUsers -> 列出数据库用户(尝试常用查询)
- case "listUsers": {
- String connectionId = requestJson.optString("connectionId", "");
- if (connectionId.isEmpty()) {
- callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 为空").toString());
- break;
- }
- Connection conn = DatabaseConnectionManager.getConnection(connectionId);
- if (conn == null || conn.isClosed()) {
- callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString());
- break;
- }
- DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId);
- String drvName = info == null ? "" : (info.driver == null ? "" : info.driver.toLowerCase());
- try {
- JSONArray out = new JSONArray();
- if ("postgresql".equals(drvName)) {
- try (Statement st = conn.createStatement();
- ResultSet rs = st.executeQuery("SELECT usename FROM pg_user")) {
- while (rs.next()) {
- out.put(rs.getString(1));
- }
- }
- } else if ("mysql".equals(drvName)) {
- try (Statement st = conn.createStatement();
- ResultSet rs = st.executeQuery("SELECT User, Host FROM mysql.user")) {
- while (rs.next()) {
- JSONObject u = new JSONObject();
- u.put("user", rs.getString("User"));
- u.put("host", rs.getString("Host"));
- out.put(u);
- }
- }
- } else {
- // H2 / SQLite: 列出连接用户或简单返回空
- out.put("not_supported_for_db");
- }
- JSONObject resp = new JSONObject();
- resp.put("status","success");
- resp.put("users", new JSONArray(out.toString()));
- callback.success(resp.toString());
- } catch (SQLException ex) {
- JSONObject err = new JSONObject();
- err.put("status","error");
- err.put("message","列出用户失败: " + ex.getMessage());
- callback.failure(500, err.toString());
- }
- break;
- }
- case "insertRow": {
- String connectionId = requestJson.optString("connectionId", "");
- String tableName = requestJson.optString("tableName", "");
- JSONObject rowData = requestJson.optJSONObject("rowData");
-
- if (connectionId.isEmpty() || tableName.isEmpty() || rowData == null) {
- callback.failure(400, new JSONObject().put("status","error").put("message","参数不完整").toString());
- break;
- }
-
- Connection conn = DatabaseConnectionManager.getConnection(connectionId);
- if (conn == null || conn.isClosed()) {
- callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString());
- break;
- }
-
- try {
- // 构建INSERT语句
- StringBuilder columns = new StringBuilder();
- StringBuilder placeholders = new StringBuilder();
- List