import fs from "fs"; import path from "path"; // import sizeOf from "image-size"; import mkdirp from "mkdirp"; import { Store, Student } from "@/types"; import gmType from "gm"; const arrayBufferToBase64Img = (buffer: ArrayBuffer) => { const str = String.fromCharCode(...new Uint8Array(buffer)); return `data:image/jpg;base64,${window.btoa(str)}`; }; let gm = null as unknown as typeof gmType; export async function addWatermark( store: Store, imageData: ArrayBuffer, imageWidth: number, imageHeight: number, filePath: string[], student: Student, index: number, trackMode: string, x = 0.01, y = 0.03, colorMap: any = {}, headerColorMap: any = {}, onlyUsePdf = false ): Promise { const canShowDouble = store.env.user.doubleTrack; // console.log("双评是否展示的配置为:", canShowDouble); const file = path.join(...filePath); if ( index !== 1 && (student.tags == undefined || student.tags[index] == undefined) ) { return await saveImage(store, imageData, filePath, onlyUsePdf); } if (store.pageInputs["/image-download"].append && fs.existsSync(file)) { console.log(file + " already exists"); return true; } x = imageWidth * x; y = imageHeight * y; if (!gm) { gm = store.config.imagemagickDev != undefined ? require("gm").subClass({ imageMagick: true, appPath: store.config.imagemagickDev, }) : require("gm").subClass({ imageMagick: true, appPath: path.join(__dirname, "../../imagemagick/"), // windows打包进程序了 }); // console.log(path.join(__dirname, "../../imagemagick/")); // console.log(path.join(__dirname, store.config.imagemagickDev)); require("gm-base64"); } const fontFile = store.config.watermark.fontFile; const color = store.config.watermark.color; const image = Buffer.from(imageData); // const size = sizeOf(image); // console.log(size); const imgData = gm(image); const drawText = (l: number, t: number, content: string) => { const len = (content || "").length; const top = t < 25 ? 50 : t + 25 > imageHeight ? imageHeight : t + 25; const left = l > imageWidth - 25 ? imageWidth - 25 * len : l; imgData.drawText(left, top, content); }; //添加第一页的得分明细 if (index == 1) { //初始坐标 // let x = 30; // let y = 10; //最大宽/高限制 const fontSize = store.config.watermark.fontSize || 30; // const maxX = imageWidth / 2 - x * 2; const saveX = x; let dynamicX = x; const maxStartX = imageWidth - fontSize * 0.7 * 5; const nextLineStartX = x + fontSize * 0.7 * 4; const height = fontSize + 10; //计算总分 const totalScore = (parseFloat(student.objectiveScore) || 0) + (parseFloat(student.subjectiveScore) || 0); //显示总分明细 imgData.font(fontFile, fontSize).fill(color); drawText(x, (y += height), "成绩明细"); //普通考试模式,按客观+主观模式显示总分 if (trackMode === "1") { drawText( x, (y += height), "总分=(客观+主观) | " + totalScore + "=" + student.objectiveScore + "+" + student.subjectiveScore ); } //研究生考试模式,只显示总分 else if (trackMode === "2") { drawText(x, (y += height), "总分=" + totalScore + "分"); } //显示客观题明细 if ( student.objectiveScoreDetail && student.objectiveScoreDetail.length > 0 ) { const title = "大题号 | 大题总分"; drawText(x, (y += height), title); const map: any = {}; for (let i = 0; i < student.objectiveScoreDetail.length; i++) { const item = student.objectiveScoreDetail[i]; if (!map[item.mainNumber]) { map[item.mainNumber] = [item]; } else { map[item.mainNumber].push(item); } } for (const mainNumber in map) { const arr = map[mainNumber]; const mainTotalScore = arr.reduce((num: number, item: any) => { return num + item.score * 100; }, 0) / 100; drawText(x, (y += height), `${mainNumber} | ${mainTotalScore}`); if (store.pageInputs["/image-download"].showSubScore) { dynamicX = saveX; drawText((dynamicX += fontSize * 0.7 * 5), y, ":"); for (let i = 0; i < arr.length; i++) { const v = arr[i]; const joinStr = i == arr.length - 1 ? "" : ","; if (dynamicX > maxStartX) { dynamicX = nextLineStartX; y += height; } drawText( (dynamicX += (v.score + " ").length * fontSize * 0.7), y, v.score + joinStr ); } } } } // if ( // student.objectiveScoreDetail && // student.objectiveScoreDetail.length > 0 // ) { // const lines = []; // let array = []; // //前置提示文字的字符数 // let count = 10; // lines.push(array); // for (let i = 0; i < student.objectiveScoreDetail.length; i++) { // const detail = student.objectiveScoreDetail[i]; // const content = // detail.answer + ":" + (detail.score === -1 ? "未选做" : detail.score); // //超长后另起一行显示客观题 // if ((count + content.length) * fontSize * 0.7 > maxX) { // array = []; // lines.push(array); // count = 10; // } // array.push(content); // count += content.length; // } // //显示所有行的客观题明细 // for (let l = 0; l < lines.length; l++) { // // FIXME: 要在小个版本修复 // // 事先判断,能否打印,情况较多,比如客观题多个答案 // // 事后报错,不让错误的数据保存 // // if (y + height + 15 > imageHeight) { // // y = startY; // // x += width; // // } // drawText(x, (y += height), "客观题识别结果 | " + lines[l].join(";")); // } // } //显示复核人 // if (student.inspector) { // drawText(x, (y += height), "复核人: " + student.inspector.loginName); // } if (student.inspector && student.inspector.length) { const allNameStr = student.inspector .map((item: any) => item.loginName) .join("、"); drawText(x, (y += height), "复核人: " + allNameStr); } //显示主观题明细 if ( student.subjectiveScoreDetail && student.subjectiveScoreDetail.length > 0 ) { //普通考试模式,按小题显示明细 if (trackMode === "1") { const title = "主观题号 | 分数 | 评卷员 | 仲裁员"; const startY = y; let width = title.length * fontSize; drawText(x, (y += height), title); for (let i = 0; i < student.subjectiveScoreDetail.length; i++) { const detail = student.subjectiveScoreDetail[i]; //超过最大高度了则另起一列 if (y + height + 15 > imageHeight) { y = startY; x += width; drawText(x, (y += height), title); } const content = detail.mainNumber + "-" + detail.subNumber + " : " + (detail.score === -1 ? "未选做" : detail.score) + " " + (detail.marker || "") + " " + (detail.header || ""); width = Math.max(width, content.length * fontSize); drawText(x, (y += height), content); } } //研究生考试模式,按分组显示明细 else if (trackMode === "2") { const title = "评卷分组 | 总分 | 评卷员"; const startY = y; let width = title.length * fontSize; drawText(x, (y += height), title); //所有小题得分按评卷分组聚合 let maxGroupNumber = 0; const groups = {}; for (let i = 0; i < student.subjectiveScoreDetail.length; i++) { const detail = student.subjectiveScoreDetail[i]; let group = groups[detail.groupNumber]; if (group == undefined) { group = { number: detail.groupNumber, score: 0, title: {}, titleString: [], marker: {}, markerString: [], header: {}, headerString: [], subScores: [], }; groups[detail.groupNumber] = group; maxGroupNumber = Math.max(maxGroupNumber, group.number); } group.subScores.push(detail.score === -1 ? 0 : detail.score); group.score = (group.score * 1000 + (detail.score === -1 ? 0 : detail.score) * 1000) / 1000; if (detail.mainTitle && !group.title[detail.mainTitle]) { group.titleString.push(detail.mainTitle); group.title[detail.mainTitle] = true; } if (detail.marker && !group.marker[detail.marker]) { group.markerString.push(detail.marker); group.marker[detail.marker] = true; } if (detail.header && !group.header[detail.header]) { group.headerString.push(detail.header); group.header[detail.header] = true; } } for (let i = 1; i <= maxGroupNumber; i++) { const group = groups[i]; if (group != undefined) { //超过最大高度了则另起一列 if (y + height + 15 > imageHeight) { y = startY; x += width; drawText(x, (y += height), title); } const content = group.number + "(" + group.titleString.join(",") + ")" + " " + group.score + (store.pageInputs["/image-download"].showSubScore ? ":" + group.subScores.join(",") : "") + " " + group.markerString.join(",") + " " + " "; //+group.headerString.join(","); width = Math.max(width, content.length * fontSize); drawText(x, (y += height), content); } } } } } //显示评卷标记 if (student.tags != undefined && student.tags[index] != undefined) { const fontSize = 50; const height = fontSize + 10; // imgData.font(fontFile, fontSize).fill(color); const tags = student.tags[index]; for (let i = 0; i < tags.length; i++) { const tag = tags[i]; if ( tag.content != undefined && (!tag.hide || canShowDouble) && !tag.forceHide ) { let top = tag.top; const c = tag.userId == 0 ? color : tag.userRole && tag.userRole !== "MARKER" ? headerColorMap[tag.groupNumber + ""][tag.userId + ""] : colorMap[tag.groupNumber + ""][tag.userId + ""]; imgData.font(fontFile, fontSize).fill(c); for (let j = 0; j < tag.content.length; j++) { drawText(tag.left, top, tag.content[j]); top += height; } } } } return new Promise((resolve, reject) => { if (onlyUsePdf) { imgData.toBase64("jpg", true, function (err: any, base64: any) { if (err) { reject(err); } else { resolve(base64); } }); } else { mkdirp.sync(path.dirname(file)); imgData.write(file, (error) => { if (error) { // logger.error("add watermark error: " + file); // logger.error(error); reject(error); } else { // resolve(true); resolve(file); } }); } }); } export async function saveImage( store: Store, imageData: ArrayBuffer, filePath: string[], onlyUsePdf = false ): Promise { const file = path.join(...filePath); // console.log("saveImage file:", file); if (store.pageInputs["/image-download"].append && fs.existsSync(file)) { console.log(file + " already exists"); return true; } return new Promise((resolve, reject) => { if (onlyUsePdf) { const buffer = Buffer.from(imageData, "base64"); const base64Str = "data:image/jpg;base64," + buffer.toString("base64"); // const base64Str = arrayBufferToBase64Img(imageData); resolve(base64Str); } else { const image = Buffer.from(imageData); mkdirp.sync(path.dirname(file)); fs.writeFile(file, image, (error) => { if (error) { reject(error); } else { // resolve(true); resolve(file); } }); } }); } export function existsImage(pathSepArray: string[]): boolean { return fs.existsSync(path.join(...pathSepArray)); }