From ef92d7ae970be96898159732d852a1092156dcc5 Mon Sep 17 00:00:00 2001 From: huababa1 <2037205722@qq.com> Date: Fri, 4 Jul 2025 18:43:54 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91ps=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E5=88=A4=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- checkPSD.jsx | 693 ++++++++++++++++++ pom.xml | 6 +- .../exam/controller/auto/AutoController.java | 9 + .../example/exam/exam/dal/ExamPsKeyword.java | 46 ++ .../exam/mapper/ExamQuestionAnswerMapper.java | 4 + .../service/autoforps/AutoForPsService.java | 13 + .../autoforps/AutoForPsServiceImpl.java | 111 +++ .../exam/exam/service/ps/PsService.java | 19 + .../exam/exam/service/ps/PsServiceImpl.java | 287 ++++++++ .../example/exam/exam/utils/ps/PsUtil.java | 123 ++++ .../question/ExamQuestionAnswerMapper.xml | 16 + 11 files changed, 1326 insertions(+), 1 deletion(-) create mode 100644 checkPSD.jsx create mode 100644 src/main/java/com/example/exam/exam/dal/ExamPsKeyword.java create mode 100644 src/main/java/com/example/exam/exam/service/autoforps/AutoForPsService.java create mode 100644 src/main/java/com/example/exam/exam/service/autoforps/AutoForPsServiceImpl.java create mode 100644 src/main/java/com/example/exam/exam/service/ps/PsService.java create mode 100644 src/main/java/com/example/exam/exam/service/ps/PsServiceImpl.java create mode 100644 src/main/java/com/example/exam/exam/utils/ps/PsUtil.java diff --git a/checkPSD.jsx b/checkPSD.jsx new file mode 100644 index 0000000..3600b08 --- /dev/null +++ b/checkPSD.jsx @@ -0,0 +1,693 @@ + + +// 简单 JSON.stringify 兼容 +if (typeof JSON === "undefined") { + JSON = {}; +} + +var inputPath1 = '${inputPath1}'; + +// 载入XMP扩展支持(全局调用一次即可) +if (!ExternalObject.AdobeXMPScript) { + ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript'); +} +JSON.stringify = function (obj, replacer, space) { + if (typeof space === "number") { + space = new Array(space + 1).join(" "); + } + function format(value, indent) { + var type = typeof value; + if (value === null) return "null"; + if (type === "string") return '"' + value + '"'; + if (type === "number" || type === "boolean") return String(value); + if (type === "object") { + var isArray = (value instanceof Array); + var items = []; + var pad = indent + (space || ""); + for (var key in value) { + if (value.hasOwnProperty(key)) { + var val = format(value[key], pad); + if (!isArray) { + val = '"' + key + '": ' + val; + } + items.push(val); + } + } + if (isArray) { + return "[\n" + pad + items.join(",\n" + pad) + "\n" + indent + "]"; + } else { + return "{\n" + pad + items.join(",\n" + pad) + "\n" + indent + "}"; + } + } + return "null"; + } + return format(obj, ""); +}; + + + + +// 主函数 +function processFile(path) { + var fileRef = new File(path); + if (!fileRef.exists) { + // alert("PSD文件不存在: " + path); + throw new Error("PSD文件不存在"); + } + + + var doc = app.open(fileRef); + + + + var result = {}; + + // 文档属性 + result["颜色模式"] = getColorModeName(doc.mode); + result["颜色深度"] = getBitsPerChannelLabel(doc.bitsPerChannel); + result["图像宽度"] = doc.width.as('px') + " 像素"; + result["图像高度"] = doc.height.as('px') + " 像素"; + result["画布宽度"]= doc.width.as('px') + " 像素"; + result["画布高度"]= doc.height.as('px') + " 像素"; + result["分辨率"] = doc.resolution + " dpi"; + + // 图层数组 + result["图层数"] = doc.artLayers.length; + result["智能对象图层数"] = countSmartObjects(doc); + + // 图层详细信息 + result["图层信息"] = []; + for (var i = 0; i < doc.artLayers.length; i++) { + var layer = doc.artLayers[i]; + doc.activeLayer = layer; // 激活当前图层 + + + var info = {}; + info["图层名"] = layer.name; + info["类型"] = getLayerKindName(layer.kind); + info["可见"] = layer.visible; + info["锁定"] = layer.allLocked; + + try { + info["图层蒙版密度"] = layer.layerMaskDensity !== undefined ? layer.layerMaskDensity : null; + } catch (e) {} + + + // 文字图层特殊信息 + if (layer.kind === LayerKind.TEXT) { + var t = layer.textItem; + try { + info["文字内容"] = t.contents; + } catch (e) { + + } + try { + info["字体"] = fontMap[t.font] || t.font;} // 如果找不到对应中文名,则保留英文名 + catch (e) { + + } + try { + info["字号"] = t.size.toString(); }// 修正这里 + catch (e) { + } + + try { + info["颜色"] = getSolidColorHex(t.color);} + catch (e) { + + } + try { + info["字距"] = t.tracking;} + catch (e) { + + } + try { + info["仿斜体"] = t.fauxItalic;} + catch (e) { + + } + try { + info["仿粗体"] = t.fauxBold; + } catch (e) { + + } + + var transformProps = getTextTransformProps(); + if (transformProps) { + info["变形"] = transformProps; + } + } + + + // 详细读取样式(如果你需要更详细数据的话) + var layerStyles = getLayerStyles(); + if (layerStyles) { + info["图层样式"] = layerStyles; + } + + // 检测滤镜信息,放到info里 + var filterInfo = detectFilters(layer.name); + if (filterInfo) { + info["滤镜信息"] = filterInfo; + } + + result["图层信息"].push(info); + } + + + + + var jsonFilePath = path.replace(/\.psd$/i, ".json"); + var jsonFile = new File(jsonFilePath); + + if (jsonFile.open("w")) { + jsonFile.encoding = "UTF8"; + jsonFile.write(JSON.stringify(result, null, 2)); + jsonFile.close(); + // alert("JSON 文件已生成: " + jsonFilePath); + } else { + // alert("无法打开文件进行写入: " + jsonFilePath); + throw new Error("无法打开文件进行写入"); + } + + + doc.close(SaveOptions.DONOTSAVECHANGES); + + +} +// 运行两个 PSD 的处理 +processFile(inputPath1); + + +////////////////////////////////////////////// + +///////////////////////////////////////////// + +// 辅助函数 - 颜色模式 +function getColorModeName(mode) { + switch (mode) { + case DocumentMode.RGB: return "RGB"; + case DocumentMode.CMYK: return "CMYK"; + case DocumentMode.GRAYSCALE: return "灰度"; + case DocumentMode.BITMAP: return "位图"; + case DocumentMode.INDEXEDCOLOR: return "索引颜色"; + default: return "未知"; + } +} +function getBitsPerChannelLabel(bits) { + switch (bits) { + case BitsPerChannelType.ONE: return "1 位/通道"; + case BitsPerChannelType.EIGHT: return "8 位/通道"; + case BitsPerChannelType.SIXTEEN: return "16 位/通道"; + case BitsPerChannelType.THIRTYTWO: return "32 位/通道"; + default: return "未知"; + } +} + +// 辅助函数 - 图层类型名 +function getLayerKindName(kind) { + switch (kind) { + case LayerKind.NORMAL: return "普通图层"; + case LayerKind.TEXT: return "文字图层"; + case LayerKind.SMARTOBJECT: return "智能对象图层"; + case LayerKind.SOLIDFILL: return "纯色填充图层"; + case LayerKind.GRADIENTFILL: return "渐变填充图层"; + case LayerKind.PATTERNFILL: return "图案填充图层"; + case LayerKind.BRIGHTNESSCONTRAST: return "亮度/对比度调整层"; + case LayerKind.LEVELS: return "色阶调整层"; + case LayerKind.CURVES: return "曲线调整层"; + case LayerKind.EXPOSURE: return "曝光度调整层"; + case LayerKind.VIBRANCE: return "自然饱和度调整层"; + case LayerKind.HUESATURATION: return "色相/饱和度调整层"; + case LayerKind.COLORBALANCE: return "色彩平衡调整层"; + case LayerKind.BLACKANDWHITE: return "黑白调整层"; + case LayerKind.PHOTOFILTER: return "照片滤镜调整层"; + case LayerKind.CHANNELMIXER: return "通道混合器调整层"; + case LayerKind.COLORLOOKUP: return "颜色查找调整层"; + case LayerKind.INVERSION: return "反相调整层"; + case LayerKind.POSTERIZE: return "色调分离调整层"; + case LayerKind.THRESHOLD: return "阈值调整层"; + case LayerKind.GRADIENTMAP: return "渐变映射调整层"; + case LayerKind.SELECTIVECOLOR: return "可选颜色调整层"; + default: return "未知类型"; + } +} + +// 读取颜色为HEX +function getSolidColorHex(color) { + if (!color) return null; + if (color.typename === "SolidColor") { + if (color.rgb) { + var r = Math.round(color.rgb.red); + var g = Math.round(color.rgb.green); + var b = Math.round(color.rgb.blue); + return "#" + toHex(r) + toHex(g) + toHex(b); + } + } + return null; +} +function toHex(c) { + var h = c.toString(16); + return h.length == 1 ? "0" + h : h; +} + +// 统计智能对象图层数量 +function countSmartObjects(doc) { + var count = 0; + for (var i = 0; i < doc.artLayers.length; i++) { + if (doc.artLayers[i].kind === LayerKind.SMARTOBJECT) { + count++; + } + } + return count; +} + +// 读取文字图层的变形属性 +function getTextTransformProps() { + try { + var ref = new ActionReference(); + ref.putEnumerated(stringIDToTypeID("layer"), charIDToTypeID("Ordn"), charIDToTypeID("Trgt")); + var desc = executeActionGet(ref); + if (desc.hasKey(stringIDToTypeID("textKey"))) { + var textDesc = desc.getObjectValue(stringIDToTypeID("textKey")); + if (textDesc.hasKey(stringIDToTypeID("transform"))) { + var transformDesc = textDesc.getObjectValue(stringIDToTypeID("transform")); + var horizontal = transformDesc.getDouble(stringIDToTypeID("horizontalShear")); + var vertical = transformDesc.getDouble(stringIDToTypeID("verticalShear")); + var scaleX = transformDesc.getDouble(stringIDToTypeID("horizontalScale")); + var scaleY = transformDesc.getDouble(stringIDToTypeID("verticalScale")); + return { + "水平扭曲": horizontal.toFixed(2) + "%", + "垂直扭曲": vertical.toFixed(2) + "%", + "水平缩放": scaleX.toFixed(2) + "%", + "垂直缩放": scaleY.toFixed(2) + "%" + }; + } + } + } catch (e) { + return null; + } + return null; +} + +function getLayerStyles() { + try { + var ref = new ActionReference(); + ref.putEnumerated(stringIDToTypeID("layer"), charIDToTypeID("Ordn"), charIDToTypeID("Trgt")); + var desc = executeActionGet(ref); + + if (!desc.hasKey(stringIDToTypeID("layerEffects"))) { + $.writeln("图层无layerEffects"); + return null; + } + + var effects = desc.getObjectValue(stringIDToTypeID("layerEffects")); + var styles = {}; + + // ============ 描边 =============== + if (effects.hasKey(stringIDToTypeID("frameFX"))) { + var frameFX = effects.getObjectValue(stringIDToTypeID("frameFX")); + styles["描边_启用"] = frameFX.getBoolean(stringIDToTypeID("enabled")); + if (styles["描边_启用"]) { + if (frameFX.hasKey(stringIDToTypeID("size"))) { + styles["描边_大小"] = frameFX.getUnitDoubleValue(stringIDToTypeID("size")) + " 像素"; + } + if (frameFX.hasKey(stringIDToTypeID("color"))) { + var colorDesc = frameFX.getObjectValue(stringIDToTypeID("color")); + styles["描边_颜色"] = colorDescToHex(colorDesc); + } + if (frameFX.hasKey(stringIDToTypeID("position"))) { + var pos = frameFX.getEnumerationValue(stringIDToTypeID("position")); + styles["描边_位置"] = getStrokePositionName(pos); + } + if (frameFX.hasKey(stringIDToTypeID("opacity"))) { + styles["描边_不透明度"] = Math.round(frameFX.getUnitDoubleValue(stringIDToTypeID("opacity"))) + "%"; + } + if (frameFX.hasKey(stringIDToTypeID("blendMode"))) { + var blendMode = frameFX.getEnumerationValue(stringIDToTypeID("blendMode")); + styles["描边_混合模式"] = getBlendModeName(blendMode); + } + } + } + + // ============ 内发光 =============== + if (effects.hasKey(stringIDToTypeID("innerGlow"))) { + var innerGlow = effects.getObjectValue(stringIDToTypeID("innerGlow")); + styles["内发光_启用"] = innerGlow.getBoolean(stringIDToTypeID("enabled")); + if (styles["内发光_启用"]) { + if (innerGlow.hasKey(stringIDToTypeID("opacity"))) { + styles["内发光_不透明度"] = Math.round(innerGlow.getUnitDoubleValue(stringIDToTypeID("opacity")) ) + "%"; + } + if (innerGlow.hasKey(stringIDToTypeID("color"))) { + styles["内发光_颜色"] = colorDescToHex(innerGlow.getObjectValue(stringIDToTypeID("color"))); + } + if (innerGlow.hasKey(stringIDToTypeID("glowTechnique"))) { + var tech = innerGlow.getEnumerationValue(stringIDToTypeID("glowTechnique")); + styles["内发光_技巧"] = getInnerGlowTechniqueName(tech); + } + if (innerGlow.hasKey(stringIDToTypeID("source"))) { + var source = innerGlow.getEnumerationValue(stringIDToTypeID("source")); + styles["内发光_来源"] = getInnerGlowSourceName(source); + } + if (innerGlow.hasKey(stringIDToTypeID("chokeMatte"))) { + styles["内发光_阻塞"] = innerGlow.getUnitDoubleValue(stringIDToTypeID("chokeMatte")) + " 像素"; + } + if (innerGlow.hasKey(stringIDToTypeID("blur"))) { + styles["内发光_模糊"] = innerGlow.getUnitDoubleValue(stringIDToTypeID("blur")) + " 像素"; + } + } + } + + // ============ 斜面和浮雕 =============== + if (effects.hasKey(stringIDToTypeID("bevelEmboss"))) { + var bevel = effects.getObjectValue(stringIDToTypeID("bevelEmboss")); + styles["斜面和浮雕_启用"] = bevel.getBoolean(stringIDToTypeID("enabled")); + if (styles["斜面和浮雕_启用"]) { + if (bevel.hasKey(stringIDToTypeID("style"))) { + styles["斜面和浮雕_样式"] = getBevelStyleName(bevel.getEnumerationValue(stringIDToTypeID("style"))); + } + if (bevel.hasKey(stringIDToTypeID("technique"))) { + styles["斜面和浮雕_方法"] = getBevelTechniqueName(bevel.getEnumerationValue(stringIDToTypeID("technique"))); + } + if (bevel.hasKey(stringIDToTypeID("depth"))) { + styles["斜面和浮雕_深度"] = bevel.getInteger(stringIDToTypeID("depth")) + "%"; + } + if (bevel.hasKey(stringIDToTypeID("direction"))) { + styles["斜面和浮雕_方向"] = getBevelDirectionName(bevel.getEnumerationValue(stringIDToTypeID("direction"))); + } + if (bevel.hasKey(stringIDToTypeID("size"))) { + styles["斜面和浮雕_大小"] = bevel.getUnitDoubleValue(stringIDToTypeID("size")) + " 像素"; + } + if (bevel.hasKey(stringIDToTypeID("soften"))) { + styles["斜面和浮雕_软化"] = bevel.getUnitDoubleValue(stringIDToTypeID("soften")) + " 像素"; + } + if (bevel.hasKey(stringIDToTypeID("useGlobalAngle"))) { + styles["斜面和浮雕_全局光"] = bevel.getBoolean(stringIDToTypeID("useGlobalAngle")); + } + if (bevel.hasKey(stringIDToTypeID("angle"))) { + styles["斜面和浮雕_角度"] = bevel.getDouble(stringIDToTypeID("angle")).toFixed(1) + "°"; + } + if (bevel.hasKey(stringIDToTypeID("highlightMode"))) { + var highlight = bevel.getObjectValue(stringIDToTypeID("highlightMode")); + if (highlight.hasKey(stringIDToTypeID("mode"))) { + styles["斜面和浮雕_高光模式"] = getBlendModeName(highlight.getEnumerationValue(stringIDToTypeID("mode"))); + } + if (highlight.hasKey(stringIDToTypeID("color"))) { + styles["斜面和浮雕_高光颜色"] = colorDescToHex(highlight.getObjectValue(stringIDToTypeID("color"))); + } + } + if (bevel.hasKey(stringIDToTypeID("shadowMode"))) { + var shadow = bevel.getObjectValue(stringIDToTypeID("shadowMode")); + if (shadow.hasKey(stringIDToTypeID("mode"))) { + styles["斜面和浮雕_阴影模式"] = getBlendModeName(shadow.getEnumerationValue(stringIDToTypeID("mode"))); + } + if (shadow.hasKey(stringIDToTypeID("color"))) { + styles["斜面和浮雕_阴影颜色"] = colorDescToHex(shadow.getObjectValue(stringIDToTypeID("color"))); + } + } + } + } + + // ============ 光泽 =============== + if (effects.hasKey(stringIDToTypeID("glossContour"))) { + styles["光泽_启用"] = true; // 简化处理 + + var gloss = effects.getObjectValue(stringIDToTypeID("glossContour")); + if (gloss.hasKey(stringIDToTypeID("range"))) { + styles["光泽_范围"] = (gloss.getDouble(stringIDToTypeID("range")) * 100).toFixed(1) + "%"; + } + if (gloss.hasKey(stringIDToTypeID("highlightSize"))) { + styles["光泽_高光大小"] = gloss.getUnitDoubleValue(stringIDToTypeID("highlightSize")) + " 像素"; + } + } + + // ============ 外发光 =============== + if (effects.hasKey(stringIDToTypeID("outerGlow"))) { + var outerGlow = effects.getObjectValue(stringIDToTypeID("outerGlow")); + styles["外发光_启用"] = outerGlow.getBoolean(stringIDToTypeID("enabled")); + if (styles["外发光_启用"]) { + if (outerGlow.hasKey(stringIDToTypeID("opacity"))) { + styles["外发光_不透明度"] = Math.round(outerGlow.getUnitDoubleValue(stringIDToTypeID("opacity")) ) + "%"; + } + if (outerGlow.hasKey(stringIDToTypeID("color"))) { + styles["外发光_颜色"] = colorDescToHex(outerGlow.getObjectValue(stringIDToTypeID("color"))); + } + if (outerGlow.hasKey(stringIDToTypeID("technique"))) { + styles["外发光_图素方法"] = getOuterGlowTechniqueName(outerGlow.getEnumerationValue(stringIDToTypeID("technique"))); + } + if (outerGlow.hasKey(stringIDToTypeID("size"))) { + styles["外发光_大小"] = outerGlow.getUnitDoubleValue(stringIDToTypeID("size")) + " 像素"; + } + if (outerGlow.hasKey(stringIDToTypeID("spread"))) { + styles["外发光_扩展"] = outerGlow.getUnitDoubleValue(stringIDToTypeID("spread")) + " 像素"; + } + if (outerGlow.hasKey(stringIDToTypeID("contour"))) { + var contour = outerGlow.getObjectValue(stringIDToTypeID("contour")); + styles["外发光_等高线"] = getContourName(contour.getEnumerationValue(stringIDToTypeID("name"))); + } + } + } + + return hasOwnProperties(styles) ? styles : null; + + } catch (e) { + $.writeln("读取图层样式异常: " + e.message); + return null; + } +} + + + + +// 辅助函数:描边位置 +function getStrokePositionName(value) { + switch (value) { + case stringIDToTypeID("inside"): return "内侧"; + case stringIDToTypeID("center"): return "居中"; + case stringIDToTypeID("outside"): return "外侧"; + default: return "未知"; + } +} + +// 斜面和浮雕 样式名称 +function getBevelStyleName(value) { + switch (value) { + case stringIDToTypeID("outerBevel"): return "外斜面"; + case stringIDToTypeID("innerBevel"): return "内斜面"; + case stringIDToTypeID("emboss"): return "浮雕"; + case stringIDToTypeID("pillowEmboss"): return "枕头状浮雕"; + default: return "未知"; + } +} +// 斜面和浮雕 方法 +function getBevelTechniqueName(value) { + switch (value) { + case stringIDToTypeID("smooth"): return "平滑"; + case stringIDToTypeID("chiselHard"): return "雕刻清晰"; + case stringIDToTypeID("chiselSoft"): return "雕刻柔和"; + default: return "未知"; + } +} +// 斜面和浮雕 方向 +function getBevelDirectionName(value) { + switch (value) { + case stringIDToTypeID("up"): return "上"; + case stringIDToTypeID("down"): return "下"; + default: return "未知"; + } +} +// 图层混合模式 +function getBlendModeName(value) { + switch (value) { + case stringIDToTypeID("normal"): return "正常"; + case stringIDToTypeID("multiply"): return "正片叠底"; + case stringIDToTypeID("screen"): return "滤色"; + case stringIDToTypeID("overlay"): return "叠加"; + case stringIDToTypeID("softLight"): return "柔光"; + case stringIDToTypeID("hardLight"): return "强光"; + case stringIDToTypeID("difference"): return "差值"; + case stringIDToTypeID("colorBurn"): return "颜色加深"; + case stringIDToTypeID("colorDodge"): return "颜色减淡"; + case stringIDToTypeID("lighten"): return "变亮"; + case stringIDToTypeID("darken"): return "变暗"; + default: return "其他"; + } +} +// 外发光 图素方法 +function getOuterGlowTechniqueName(value) { + switch (value) { + case stringIDToTypeID("precise"): return "精确"; + case stringIDToTypeID("softer"): return "柔和"; + default: return "未知"; + } +} +// 等高线名称(外发光等) +function getContourName(value) { + switch (value) { + case stringIDToTypeID("linear"): return "线性"; + case stringIDToTypeID("ring"): return "环形"; + case stringIDToTypeID("cone"): return "锥形"; + case stringIDToTypeID("cove"): return "凹形"; + case stringIDToTypeID("stairs"): return "阶梯"; + case stringIDToTypeID("custom"): return "自定义"; + case stringIDToTypeID("zigZag"): return "锯齿1"; + case stringIDToTypeID("roundedSteps"): return "圆角阶梯"; + default: return "未知"; + } +} +function colorDescToHex(colorDesc) { + try { + var red = Math.round(colorDesc.getDouble(stringIDToTypeID('red'))); + var green = Math.round(colorDesc.getDouble(stringIDToTypeID('green'))); + var blue = Math.round(colorDesc.getDouble(stringIDToTypeID('blue'))); + return rgbToHex(red, green, blue); + } catch (e) { + return null; + } +} +function getInnerGlowTechniqueName(value) { + switch(value) { + case stringIDToTypeID("precise"): return "精确"; + case stringIDToTypeID("softer"): return "柔和"; + default: return "未知"; + } +} +function getInnerGlowSourceName(value) { + switch(value) { + case stringIDToTypeID("center"): return "中心"; + case stringIDToTypeID("edge"): return "边缘"; + default: return "未知"; + } +} + +function rgbToHex(r, g, b) { + return "#" + [r, g, b].map(function(x) { + var hex = x.toString(16); + return hex.length === 1 ? "0" + hex : hex; + }).join(''); +} + +// 滤镜检测示例(只检测图层名包含的关键词) +// 修改后的detectFilters函数,优先用XMP元数据读取镜头光晕参数 +function detectFilters(layerName) { + var filters = {}; + if (!layerName || typeof layerName !== "string") { + return null; + } + var name = layerName.toLowerCase(); + + // 先尝试读取XMP中镜头光晕参数 + + var lensFlareMeta = readHistoryFromXMP(); + if (lensFlareMeta) { + return lensFlareMeta; // 直接返回读取的元数据 + } + // else { + // // 没读到元数据,返回默认提示 + // filters["滤镜类型"] = "镜头光晕"; + // filters["亮度"] = "⚠️ 无法读取,建议人工确认"; + // filters["镜头类型"] = "⚠️ 无法读取,建议人工确认"; + // return filters; + // } + + +} + + +// 结合你的读取函数用法举例 +function readHistoryFromXMP() { + try { + if (typeof ExternalObject.AdobeXMPScript === "undefined") { + ExternalObject.AdobeXMPScript = new ExternalObject("lib:AdobeXMPScript"); + } + + var xmp = new XMPMeta(app.activeDocument.xmpMetadata.rawData); + var nsMM = "http://ns.adobe.com/xap/1.0/mm/"; + var nsEvt = "http://ns.adobe.com/xap/1.0/sType/ResourceEvent#"; + + XMPMeta.registerNamespace(nsMM, "xmpMM"); + XMPMeta.registerNamespace(nsEvt, "stEvt"); + + var count = xmp.countArrayItems(nsMM, "History"); + if (count === 0) { + $.writeln("没有找到历史记录"); + return null; + } + + + for (var i = 1; i <= count; i++) { + var itemXMP = xmp.getArrayItem(nsMM, "History", i); + var historyLine = itemXMP.toString(); + + $.writeln("历史记录第" + i + "条: " + historyLine); + + if (historyLine.indexOf("镜头光晕") !== -1 || historyLine.toLowerCase().indexOf("lens flare") !== -1) { + var brightnessMatch = historyLine.match(/亮度[::]?\s*(\d+%?)/); + var typeMatch = historyLine.match(/类型[::]?\s*([^\s]+)/); + + return { + "历史":historyLine, + "滤镜类型": "镜头光晕", + "亮度": brightnessMatch ? brightnessMatch[1] : "未知", + "镜头类型": typeMatch ? typeMatch[1] : "未知" + }; + } + } + + + + + } catch (e) { + return { + "读取XMP历史记录失败:": e.message + }; + + } +} + + + +var fontMap = { + "SimSun": "宋体", + "SimHei": "黑体", + "FangSong_GB2312": "仿宋", + "KaiTi_GB2312": "楷体", + "MicrosoftYaHei": "微软雅黑", + "MicrosoftJhengHei": "微软正黑体", + "NSimSun": "新宋体", + "PMingLiU": "新细明体", + "MingLiU": "细明体", + "DFKai-SB": "标楷体", + "STKaiti": "华文楷体", + "STSong": "华文宋体", + "STHeiti": "华文黑体", + "STFangsong": "华文仿宋", + "STXihei": "华文细黑", + "STZhongsong": "华文中宋", + "STXinwei": "华文新魏", + "STLiti": "华文隶书", + "STHupo": "华文琥珀", + "STCaiyun": "华文彩云", + "STXingkai": "华文行楷", + "STHupo": "华文琥珀", + "STFangsong": "华文仿宋", + "Arial": "Arial(英文字体)", + "TimesNewRomanPSMT": "Times New Roman", + "CourierNewPSMT": "Courier New", + "Georgia": "Georgia", + "Tahoma": "Tahoma", + "Verdana": "Verdana" +}; + +// 判断对象是否含有自有属性(兼容无Object.keys的环境) +function hasOwnProperties(obj) { + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + return true; + } + } + return false; +} + + + diff --git a/pom.xml b/pom.xml index 6a061f9..590d003 100644 --- a/pom.xml +++ b/pom.xml @@ -136,7 +136,11 @@ poi-ooxml-lite 5.2.3 - + + org.json + json + 20231013 + cn.afterturn easypoi-base diff --git a/src/main/java/com/example/exam/exam/controller/auto/AutoController.java b/src/main/java/com/example/exam/exam/controller/auto/AutoController.java index 3c4d421..184c53a 100644 --- a/src/main/java/com/example/exam/exam/controller/auto/AutoController.java +++ b/src/main/java/com/example/exam/exam/controller/auto/AutoController.java @@ -2,6 +2,7 @@ package com.example.exam.exam.controller.auto; import com.example.exam.exam.controller.auto.vo.StuInfoVo; import com.example.exam.exam.service.autoforbrower.AutoForBrowerService; +import com.example.exam.exam.service.autoforps.AutoForPsService; import com.example.exam.exam.service.brower.JudgementBrowerService; import com.example.exam.exam.service.autoforc.AutoForCService; import com.example.exam.exam.service.autoforchoice.AutoForChoiceService; @@ -47,6 +48,8 @@ public class AutoController { JudgementForExcelService judgementForExcelService; @Resource StuPaperScoreService stuPaperScoreService; + @Resource + AutoForPsService autoForPsService; // C语言 @@ -113,6 +116,12 @@ public class AutoController { // win10虚拟界面 + //PS + @PostMapping("/judgementForPS") + public Result judgementForPS(@RequestBody StuInfoVo stuInfoVo) throws SQLException, IOException { + return Result.success(autoForPsService.autoForPs(stuInfoVo)); + } + // 单项选择 /** * 将选择题写入到JSON文件中 diff --git a/src/main/java/com/example/exam/exam/dal/ExamPsKeyword.java b/src/main/java/com/example/exam/exam/dal/ExamPsKeyword.java new file mode 100644 index 0000000..19a4d6f --- /dev/null +++ b/src/main/java/com/example/exam/exam/dal/ExamPsKeyword.java @@ -0,0 +1,46 @@ +package com.example.exam.exam.dal; + +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ExamPsKeyword { + /** + * 主键id + */ + private String id; + /** + * 试题id + */ + private String quId; + /** + * 父id + */ + private String parentId; + /** + * 键 + */ + private String key; + /** + * 值 + */ + private String value; + /** + * 权值 + */ + private String rate; + /** + * 类型(学生考点:1,结果考点:2) + */ + private String type; + + private String sort; + + @TableField(exist = false) + private List children; +} diff --git a/src/main/java/com/example/exam/exam/mapper/ExamQuestionAnswerMapper.java b/src/main/java/com/example/exam/exam/mapper/ExamQuestionAnswerMapper.java index b71d7ca..a868962 100644 --- a/src/main/java/com/example/exam/exam/mapper/ExamQuestionAnswerMapper.java +++ b/src/main/java/com/example/exam/exam/mapper/ExamQuestionAnswerMapper.java @@ -2,6 +2,7 @@ package com.example.exam.exam.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.exam.exam.dal.ExamQuestionAnswer; +import com.example.exam.exam.dal.ExamPsKeyword; import org.apache.ibatis.annotations.Mapper; import java.util.List; @@ -23,4 +24,7 @@ public interface ExamQuestionAnswerMapper extends BaseMapper selectPsAnswerById(String quId); + } diff --git a/src/main/java/com/example/exam/exam/service/autoforps/AutoForPsService.java b/src/main/java/com/example/exam/exam/service/autoforps/AutoForPsService.java new file mode 100644 index 0000000..3330432 --- /dev/null +++ b/src/main/java/com/example/exam/exam/service/autoforps/AutoForPsService.java @@ -0,0 +1,13 @@ +package com.example.exam.exam.service.autoforps; + +import com.example.exam.exam.controller.auto.vo.StuInfoVo; + +import java.io.IOException; + +public interface AutoForPsService { + /** + * 自动判题 Ps + * @param stuInfoVo 学生信息 + */ + Double autoForPs(StuInfoVo stuInfoVo) throws IOException; +} diff --git a/src/main/java/com/example/exam/exam/service/autoforps/AutoForPsServiceImpl.java b/src/main/java/com/example/exam/exam/service/autoforps/AutoForPsServiceImpl.java new file mode 100644 index 0000000..4c326b7 --- /dev/null +++ b/src/main/java/com/example/exam/exam/service/autoforps/AutoForPsServiceImpl.java @@ -0,0 +1,111 @@ +package com.example.exam.exam.service.autoforps; + +import com.example.exam.exam.controller.auto.vo.StuInfoVo; +import com.example.exam.exam.dal.*; +import com.example.exam.exam.mapper.EducationPaperQuMapper; +import com.example.exam.exam.mapper.EducationPaperSchemeMapper; +import com.example.exam.exam.service.mysql.IMysqlLocalService; +import com.example.exam.exam.service.ps.PsService; +import com.example.exam.exam.service.question.IExamQuestionService; +import com.example.exam.exam.service.stupaperscore.StuPaperScoreService; +import com.example.exam.exam.service.tenant.SystemTenantService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + +@Service +public class AutoForPsServiceImpl implements AutoForPsService{ + + @Resource + PsService psService; + @Resource + EducationPaperSchemeMapper educationPaperSchemeMapper; + @Resource + IExamQuestionService examQuestionService; + @Resource + EducationPaperQuMapper educationPaperQuMapper; + @Resource + StuPaperScoreService stuPaperScoreService; + @Resource + SystemTenantService systemTenantService; + + @Override + public Double autoForPs(StuInfoVo stuInfoVo) throws IOException { + SystemTenant systemTenant = systemTenantService.getId(stuInfoVo.getSchoolName()); + Double score = 0.0; + // 0、获取到试卷信息(试卷详情) + List educationPaperSchemeList = educationPaperSchemeMapper.selectEducationPaperTaskByPaperId(stuInfoVo.getPaperId()); + List educationPaperQus = educationPaperQuMapper.selectPaperQuListByPaperId(stuInfoVo.getPaperId()); + // 1、获取到学生文件路径 + String filePath = stuInfoVo.getFilePath(); + File folder = new File(filePath); + // 2、获取到学生文件 + File[] files = folder.listFiles(); + for (File file : files) { + File csFiles = new File(file.getPath()); + String quId = csFiles.getName(); + File[] csFileList = csFiles.listFiles(); + String name = csFileList[0].getName(); + if ("图像处理".equals(name)) { + File[] cs_file_list = csFiles.listFiles(); + String lastFilePath = cs_file_list[0].getPath(); + File[] lastFiles = cs_file_list[0].listFiles(); + // 3、根据题号进行查询试题的相关信息 + Optional result = educationPaperSchemeList.stream().filter(quLists -> quLists.getSpName().equals(name)).findFirst(); + Optional results = educationPaperQus.stream().filter(quLists -> quLists.getQuId().equals(quId)).findFirst(); + + EducationPaperScheme educationPaperScheme = result.get(); + EducationPaperQu educationPaperQu = results.get(); + String quScore = educationPaperScheme.getQuScores(); + ExamQuestion examQuestion = examQuestionService.selectExamQuestionByQuId(quId); + for (File lastFile : lastFiles) { + if (lastFile.getName().contains("原始")) { + String judgementStr = "

-----------------------------------------------------------

"; + judgementStr += "

试题序号:" + educationPaperQu.getSort() + "

"; + judgementStr += "

试题编号:" + examQuestion.getQuNum() + "

"; + judgementStr += "

试题分数:" + Double.parseDouble(quScore) + "

"; + judgementStr += "

试题名称:" + name + "

"; + SourceAndText wordpojo = psService.Judgement(Double.parseDouble(quScore), lastFilePath, lastFile.getPath(), examQuestion, judgementStr); + score += wordpojo.getScore(); + judgementStr = wordpojo.getText(); + judgementStr += "

试题得分:" + wordpojo.getScore() + "

"; + // 4、需要更新学生试题得分,首先需要查询试题的数据库是否保存信息 + // 通过 quId,stuId,paperId 查询 + StuPaperScoreDO stuPaperScoreDO = stuPaperScoreService.getStuScoreByPaperIdAndQuid(stuInfoVo.getStuId(), stuInfoVo.getPaperId(), quId); + if (stuPaperScoreDO != null) { + // 说明已经是做过该题,需要更新数据 + stuPaperScoreDO.setScore(new BigDecimal(wordpojo.getScore())); + stuPaperScoreDO.setContent(judgementStr); + stuPaperScoreDO.setSort(educationPaperQu.getSort()); + stuPaperScoreDO.setSubjectName(name); + stuPaperScoreDO.setIsTrue(wordpojo.getScore() == 0 ? 1 : wordpojo.getScore() == Double.parseDouble(quScore) ? 0 : 2); + stuPaperScoreDO.setTrueScore(new BigDecimal(quScore)); + stuPaperScoreDO.setTenantId(systemTenant.getId()); + stuPaperScoreService.updateStuPaperScore(stuPaperScoreDO); + } else { + StuPaperScoreDO insertInfo = new StuPaperScoreDO(); + insertInfo.setStuId(stuInfoVo.getStuId()); + insertInfo.setPaperId(stuInfoVo.getPaperId()); + insertInfo.setQuId(quId); + insertInfo.setScore(new BigDecimal(wordpojo.getScore())); + insertInfo.setContent(judgementStr); + insertInfo.setSort(educationPaperQu.getSort()); + insertInfo.setSubjectName(name); + insertInfo.setTrueScore(new BigDecimal(quScore)); + insertInfo.setTenantId(systemTenant.getId()); + insertInfo.setIsTrue(wordpojo.getScore() == 0 ? 1 : wordpojo.getScore() == Double.parseDouble(quScore) ? 0 : 2); + stuPaperScoreService.insertStuPaperScore(insertInfo); + } + break; + } + } + } + } + return score; + } +} diff --git a/src/main/java/com/example/exam/exam/service/ps/PsService.java b/src/main/java/com/example/exam/exam/service/ps/PsService.java new file mode 100644 index 0000000..e6fc0f2 --- /dev/null +++ b/src/main/java/com/example/exam/exam/service/ps/PsService.java @@ -0,0 +1,19 @@ +package com.example.exam.exam.service.ps; + +import com.example.exam.exam.dal.ExamQuestion; +import com.example.exam.exam.dal.SourceAndText; + +import java.io.File; +import java.io.IOException; + +public interface PsService { + /** + * 读取考生文件,与题型中要求进行判断 + * @param path 文件路径 + * @param examQuestion 试题参数 + * @param sorce 试题分数 + * @return 得分 + * @throws Exception 异常 + */ + SourceAndText Judgement(double sorce, String lastFilePath, String path, ExamQuestion examQuestion, String judgementStr) throws IOException; +} diff --git a/src/main/java/com/example/exam/exam/service/ps/PsServiceImpl.java b/src/main/java/com/example/exam/exam/service/ps/PsServiceImpl.java new file mode 100644 index 0000000..f27ecf3 --- /dev/null +++ b/src/main/java/com/example/exam/exam/service/ps/PsServiceImpl.java @@ -0,0 +1,287 @@ +package com.example.exam.exam.service.ps; + +import com.example.exam.exam.dal.ExamQuestion; +import com.example.exam.exam.dal.SourceAndText; +import com.example.exam.exam.mapper.ExamQuestionAnswerMapper; +import com.example.exam.exam.dal.ExamPsKeyword; +import com.example.exam.exam.utils.HtmlAppender; +import com.example.exam.exam.utils.c.LogFileUtils; +import com.example.exam.exam.utils.ps.PsUtil; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.Resource; +import org.apache.commons.collections4.CollectionUtils; +import org.json.JSONArray; +import org.json.JSONObject; +import org.springframework.stereotype.Service; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class PsServiceImpl implements PsService{ + static String answerLogPath ; // 文件路径 + @Resource + private ExamQuestionAnswerMapper examQuestionAnswerMapper; + private static final DateTimeFormatter formatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private static final DateTimeFormatter DATE_FORMATTER = + DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"); + + @Override + public SourceAndText Judgement(double score, String lastFilePath, String path, ExamQuestion examQuestion, String judgementStr) throws IOException { + // 创建log文件txt,用于记录 + answerLogPath = lastFilePath + File.separator + "log.txt"; + SourceAndText sourceAndText = new SourceAndText(); + // 构建树形结构 + List examPsKeywordList = examQuestionAnswerMapper.selectPsAnswerById(examQuestion.getQuId()); + List treeAnswerList = buildTree(examPsKeywordList); + + String homeDir = System.getProperty("user.dir"); + String jsxTemplatePath = homeDir + "\\checkPSD.jsx"; // 模板路径 + String photoshopExe = PsUtil.findPhotoshopExe(); + + if (photoshopExe == null) { + appendToFile(answerLogPath, "未检测到本机的PhotoShop!"); + judgementStr = HtmlAppender.appendHtmlLine(judgementStr, "未检测到本机的PhotoShop!"); + sourceAndText.setText(judgementStr); + sourceAndText.setScore(0.0); + return sourceAndText; + } + + String sthJsonPath = path.replaceAll("(?i)\\.psd$", ".json"); + Path jsonFilePath = Paths.get(sthJsonPath); + + Path jsxPath=null; + // 执行 Photoshop 脚本 + try { + String jsxTargetPath= PsUtil.runTwoPsdsInOneScript(path, jsxTemplatePath, photoshopExe); + jsxPath = Paths.get(jsxTargetPath); + + appendToFile(answerLogPath, "Photoshop脚本执行完毕"); + } catch (Exception e) { + appendToFile(answerLogPath, "执行 Photoshop 脚本失败: " + e.getMessage()); + throw new RuntimeException("执行 Photoshop 脚本失败: " + e.getMessage(), e); + } + + // 读取学生的JSON文件 + try { + String sthJsonStr = Files.readString(Paths.get(sthJsonPath), StandardCharsets.UTF_8); + JSONObject studentJson = new JSONObject(sthJsonStr); + + double totalScore = 0.0; + + // 遍历树形结构,对比每个节点 + for (ExamPsKeyword item : treeAnswerList) { + SourceAndText studentScorePojo = compareItemWithStudentJson(item, studentJson, "", judgementStr); + totalScore += studentScorePojo.getScore(); + judgementStr = studentScorePojo.getText(); + } + + // 计算最终得分 + double maxPossibleScore = calculateMaxPossibleScore(treeAnswerList); + double finalScore = ((totalScore / maxPossibleScore) * score); + double finalScoreRatio = Math.round(finalScore * 100.0) / 100.0; // 四舍五入到2位小数 + appendToFile(answerLogPath, "最终得分: " + finalScoreRatio); + judgementStr = HtmlAppender.appendHtmlLine(judgementStr, "最终得分: " +finalScoreRatio); + sourceAndText.setScore(finalScoreRatio); + sourceAndText.setText(judgementStr); + return sourceAndText; + + } catch (IOException e) { + appendToFile(answerLogPath, "读取 JSON 文件失败"); + judgementStr = HtmlAppender.appendHtmlLine(judgementStr, "读取 JSON 文件失败!"); + sourceAndText.setText(judgementStr); + sourceAndText.setScore(0.0); + return sourceAndText; + }finally { + // 检查文件是否存在 + if (Files.exists(jsonFilePath)) { + try { + // 如果存在则删除 + Files.delete(jsonFilePath); + } catch (IOException e) { + } + } + if (Files.exists(jsxPath)) { + try { + // 如果存在则删除 + Files.delete(jsxPath); + } catch (IOException e) { + } + } + + } + } + + private SourceAndText compareItemWithStudentJson(ExamPsKeyword item, JSONObject studentJson, String parentPath, String judgementStr) { + SourceAndText result = new SourceAndText(); + + // 当前节点路径(用中文【】包裹是为了兼容你已有的格式) + String currentPath = parentPath.isEmpty() + ? "【" + item.getKey() + "】" + : parentPath + "【" + item.getKey() + "】"; + + // 如果是叶子节点,直接比较值 + if (item.getChildren() == null || item.getChildren().isEmpty()) { + Object valueObj = getValueFromJson(studentJson, currentPath); // 用路径查找学生值 + String studentValue = valueObj != null ? valueObj.toString() : null; + String correctValue = item.getValue(); + + boolean isCorrect = correctValue != null && correctValue.equals(studentValue); + + if (isCorrect) { + judgementStr = HtmlAppender.appendHtmlLine(judgementStr, currentPath + "【" + correctValue + "】【✅】"); + appendToFile(answerLogPath, currentPath + "【" + correctValue + "】【✅】"); + result.setScore(result.getScore() + Double.parseDouble(item.getRate())); + } else { + judgementStr = HtmlAppender.appendHtmlLine(judgementStr, currentPath + "【" + correctValue + "】【❌】"); + appendToFile(answerLogPath, currentPath + "【" + correctValue + "】【❌】"); + } + + result.setText(judgementStr); + } else { + // 有子节点,递归比较 + for (ExamPsKeyword child : item.getChildren()) { + SourceAndText childResult = compareItemWithStudentJson(child, studentJson, currentPath, judgementStr); + judgementStr = childResult.getText(); // ✅ 更新外层judgementStr + result.setScore(result.getScore() + childResult.getScore()); + } + result.setText(judgementStr); + } + + return result; + } + + // 计算最大可能得分 + private double calculateMaxPossibleScore(List treeAnswerList) { + double total = 0; + for (ExamPsKeyword item : treeAnswerList) { + if (item.getChildren() == null || item.getChildren().isEmpty()) { + if (item.getRate() != null) { + total += Double.parseDouble(item.getRate()); + } + } else { + total += calculateMaxPossibleScore(item.getChildren()); + } + } + return total; + } + + private Object getValueFromJson(JSONObject json, String path) { + String[] keys = path.split("【|】"); + Object current = json; + + for (String rawKey : keys) { + if (rawKey == null || rawKey.isEmpty()) continue; + + if (current instanceof JSONObject) { + current = ((JSONObject) current).opt(rawKey); + } else if (current instanceof JSONArray) { + JSONArray array = (JSONArray) current; + Object matched = null; + + // 如果数组项是 JSONObject,并且包含 "图层名",我们按图层名匹配 + for (int i = 0; i < array.length(); i++) { + JSONObject obj = array.optJSONObject(i); + if (obj != null && rawKey.equals(obj.optString("图层名"))) { + matched = obj; + break; + } + } + + // 若未按图层名匹配成功,就尝试找任何包含该key的对象 + if (matched == null) { + for (int i = 0; i < array.length(); i++) { + JSONObject obj = array.optJSONObject(i); + if (obj != null && obj.has(rawKey)) { + matched = obj.get(rawKey); + break; + } + } + } + + current = matched; + } else { + return null; + } + } + + return current; + } + + + + + + /** + * 构建答案树形结构 + */ + private List buildTree(List flatList) { + if (CollectionUtils.isEmpty(flatList)) { + return Collections.emptyList(); + } + + // 第一步:构建所有节点 + Map nodeMap = new LinkedHashMap<>(); + Map> parentChildMap = new HashMap<>(); + + flatList.forEach(item -> { + ExamPsKeyword node = new ExamPsKeyword(); + node.setId(item.getId()); + node.setKey(item.getKey()); + node.setValue(item.getValue()); // 先保留原始值 + node.setRate(item.getRate()); + node.setType(item.getType()); + node.setSort(item.getSort()); + nodeMap.put(item.getId(), node); + String parentId = item.getParentId() != null ? item.getParentId() : "0"; + parentChildMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(node); + }); + + // 第二步:建立父子关系并识别真正父节点 + Set realParents = new HashSet<>(); + parentChildMap.forEach((parentId, children) -> { + if ("0".equals(parentId)) { + children.forEach(node -> { + // 标记没有子节点的独立节点 + if (!parentChildMap.containsKey(node.getId())) { + node.setChildren(null); + } + }); + } else { + ExamPsKeyword parent = nodeMap.get(parentId); + if (parent != null) { + parent.setChildren(children); + realParents.add(parent); // 记录真正有子节点的父节点 + } + } + }); + + // 第三步:对有子节点的父节点设置value=null + realParents.forEach(parent -> parent.setValue(null)); + + return parentChildMap.getOrDefault("0", Collections.emptyList()); + } + + public static void appendToFile(String filePath, String content) { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath, true))) { + String timestamp = LocalDateTime.now().format(formatter); + String logLine = String.format("[%s] %s", timestamp, content); + writer.write(logLine); + writer.newLine(); // 可选:添加换行符 + } catch (IOException e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/com/example/exam/exam/utils/ps/PsUtil.java b/src/main/java/com/example/exam/exam/utils/ps/PsUtil.java new file mode 100644 index 0000000..a4d4fc0 --- /dev/null +++ b/src/main/java/com/example/exam/exam/utils/ps/PsUtil.java @@ -0,0 +1,123 @@ +package com.example.exam.exam.utils.ps; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class PsUtil { + + + + // 查询注册表路径,返回command字符串(即exe路径+参数) + private static String queryReg(String regPath) throws IOException, InterruptedException { + Process process = Runtime.getRuntime().exec("reg query \"" + regPath + "\" /ve"); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "GBK")); // 注册表输出一般GBK编码 + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.startsWith("(默认)")) { + String[] parts = line.split(" "); + if (parts.length >= 3) { + return parts[parts.length - 1].replace("\"", "").trim(); // 去掉双引号 + } + } + } + process.waitFor(); + return null; + } + + // 尝试读取多个注册表路径获取 Photoshop 路径 + public static String findPhotoshopExe() { + String[] regPaths = { + "HKLM\\SOFTWARE\\Classes\\Applications\\Photoshop.exe\\shell\\edit\\command", + "HKLM\\SOFTWARE\\WOW6432Node\\Classes\\Applications\\Photoshop.exe\\shell\\edit\\command" + }; + + for (String path : regPaths) { + try { + String exePath = queryReg(path); + if (exePath != null && !exePath.isEmpty()) { + // 通常 exe 路径后面会跟参数 %1,只取 exe 路径部分 + int idx = exePath.toLowerCase().indexOf("photoshop.exe"); + if (idx != -1) { + exePath = exePath.substring(0, idx + "photoshop.exe".length()); + } + return exePath; + } + } catch (Exception e) { + // 忽略异常,继续尝试其他路径 + } + } + return null; // 没找到 + } + + + public static String runTwoPsdsInOneScript(String psdPath, String jsxTemplatePath, String photoshopExe) + throws IOException, InterruptedException { + + File psdFile1 = new File(psdPath); + String baseDir = psdFile1.getParent(); + String jsxTargetPath = baseDir + File.separator + "run_both_" + System.currentTimeMillis() + ".jsx"; + + String jsxTemplate = Files.readString(Paths.get(jsxTemplatePath), StandardCharsets.UTF_8); + + String safePath1 = psdPath.replace("\\", "\\\\").replace("'", "\\'"); + String jsxContent = jsxTemplate + .replace("${inputPath1}", safePath1); + + Files.writeString(Paths.get(jsxTargetPath), jsxContent, StandardCharsets.UTF_8); + + String command = String.format("\"%s\" -r \"%s\"", photoshopExe, jsxTargetPath); + System.out.println("运行 Photoshop 脚本: " + command); + + Process process = Runtime.getRuntime().exec(command); + + // 异步打印输出日志 + new Thread(() -> { + try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = br.readLine()) != null) { + System.out.println("[PS OUT] " + line); + } + } catch (IOException ignored) {} + }).start(); + + new Thread(() -> { + try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { + String line; + while ((line = br.readLine()) != null) { + System.err.println("[PS ERR] " + line); + } + } catch (IOException ignored) {} + }).start(); + + // 不使用 waitFor 阻塞,改成轮询判断两个 JSON 文件是否生成 + String jsonPath1 = psdPath.replaceAll("(?i)\\.psd$", ".json"); + + int maxWaitSeconds = 60; // 最多等待 60秒,根据实际调整 + int waited = 0; + while (!(Files.exists(Paths.get(jsonPath1)))) { + Thread.sleep(1000); // 每秒检查一次 + waited++; + if (waited > maxWaitSeconds) { + // 超时,结束进程并抛异常 + process.destroyForcibly(); + throw new RuntimeException("等待 Photoshop 生成 JSON 文件超时"); + } + } + + System.out.println("检测到 JSON 文件生成,关闭 Photoshop 进程..."); + + // 手动杀死 Photoshop 进程,确保释放资源 + Process killProcess = Runtime.getRuntime().exec("taskkill /IM Photoshop.exe /F"); + killProcess.waitFor(); + + System.out.println("Photoshop 进程已关闭"); + + return jsxTargetPath; + } +} diff --git a/src/main/resources/mapper/question/ExamQuestionAnswerMapper.xml b/src/main/resources/mapper/question/ExamQuestionAnswerMapper.xml index 08e22b7..d58ad40 100644 --- a/src/main/resources/mapper/question/ExamQuestionAnswerMapper.xml +++ b/src/main/resources/mapper/question/ExamQuestionAnswerMapper.xml @@ -15,7 +15,17 @@ + + + + + + + + + + select answer_id, qu_id, is_right, image, content,contentIn, score_rate,attribute,sort from exam_question_answer @@ -44,4 +54,10 @@ + \ No newline at end of file