|
@@ -1,449 +0,0 @@
|
|
|
-//云端图片操作工具
|
|
|
-import EventEmitter from "events";
|
|
|
-import api from "./api";
|
|
|
-import env from "./env";
|
|
|
-import config from "./config";
|
|
|
-import fs from "fs";
|
|
|
-import path from "path";
|
|
|
-import readline from "readline";
|
|
|
-import request_util from "requestretry";
|
|
|
-import sizeOf from "image-size";
|
|
|
-import mustache from "mustache";
|
|
|
-import mkdirp from "mkdirp";
|
|
|
-
|
|
|
-import _logger from "./logger";
|
|
|
-const logger = _logger("image.js");
|
|
|
-const downloadLogger = _logger("download");
|
|
|
-
|
|
|
-const gm =
|
|
|
- config.imagemagick != undefined
|
|
|
- ? require("gm").subClass({
|
|
|
- imageMagick: true,
|
|
|
- appPath: config.imagemagick,
|
|
|
- })
|
|
|
- : require("gm");
|
|
|
-
|
|
|
-class executor extends EventEmitter {
|
|
|
- async readFile(file) {
|
|
|
- return new Promise((resolve) => {
|
|
|
- const data = [];
|
|
|
- if (fs.existsSync(file)) {
|
|
|
- const reader = readline.createInterface({
|
|
|
- input: fs.createReadStream(file),
|
|
|
- });
|
|
|
- reader.on("line", (line) => {
|
|
|
- data.push(line);
|
|
|
- });
|
|
|
- reader.on("close", () => {
|
|
|
- resolve(data);
|
|
|
- });
|
|
|
- } else {
|
|
|
- resolve(data);
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- async addWatermark(image, file, student, index, trackMode) {
|
|
|
- const fontFile = config.watermark.fontFile;
|
|
|
- const color = config.watermark.color;
|
|
|
- const size = sizeOf(image);
|
|
|
- const imgData = gm(image);
|
|
|
- //添加第一页的得分明细
|
|
|
- if (index == 1) {
|
|
|
- //初始坐标
|
|
|
- let x = 30;
|
|
|
- let y = 10;
|
|
|
- //最大宽/高限制
|
|
|
- const fontSize = config.watermark.fontSize || 30;
|
|
|
- const maxX = size.width / 2 - x * 2;
|
|
|
- const height = fontSize + 10;
|
|
|
- //计算总分
|
|
|
- const totalScore =
|
|
|
- (parseFloat(student.objectiveScore) || 0) +
|
|
|
- (parseFloat(student.subjectiveScore) || 0);
|
|
|
- //显示总分明细
|
|
|
- imgData.font(fontFile, fontSize).fill(color);
|
|
|
- imgData.drawText(x, (y += height), "成绩明细");
|
|
|
- //普通考试模式,按客观+主观模式显示总分
|
|
|
- if (trackMode === "1") {
|
|
|
- imgData.drawText(
|
|
|
- x,
|
|
|
- (y += height),
|
|
|
- "总分=(客观+主观) | " +
|
|
|
- totalScore +
|
|
|
- "=" +
|
|
|
- student.objectiveScore +
|
|
|
- "+" +
|
|
|
- student.subjectiveScore
|
|
|
- );
|
|
|
- }
|
|
|
- //研究生考试模式,只显示总分
|
|
|
- else if (trackMode === "2") {
|
|
|
- imgData.drawText(x, (y += height), "总分=" + totalScore + "分");
|
|
|
- }
|
|
|
- //显示客观题明细
|
|
|
- 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;
|
|
|
- //超长后另起一行显示客观题
|
|
|
- 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++) {
|
|
|
- imgData.drawText(
|
|
|
- x,
|
|
|
- (y += height),
|
|
|
- "客观题识别结果 | " + lines[l].join(";")
|
|
|
- );
|
|
|
- }
|
|
|
- }
|
|
|
- //显示复核人
|
|
|
- if (student.inspector) {
|
|
|
- imgData.drawText(
|
|
|
- x,
|
|
|
- (y += height),
|
|
|
- "复核人: " + student.inspector.loginName
|
|
|
- );
|
|
|
- }
|
|
|
- //显示主观题明细
|
|
|
- if (
|
|
|
- student.subjectiveScoreDetail &&
|
|
|
- student.subjectiveScoreDetail.length > 0
|
|
|
- ) {
|
|
|
- //普通考试模式,按小题显示明细
|
|
|
- if (trackMode === "1") {
|
|
|
- const title = "主观题号 | 分数 | 评卷员 | 仲裁员";
|
|
|
- const startY = y;
|
|
|
- let width = title.length * fontSize;
|
|
|
- imgData.drawText(x, (y += height), title);
|
|
|
- for (let i = 0; i < student.subjectiveScoreDetail.length; i++) {
|
|
|
- const detail = student.subjectiveScoreDetail[i];
|
|
|
- //超过最大高度了则另起一列
|
|
|
- if (y + height + 15 > size.height) {
|
|
|
- y = startY;
|
|
|
- x += width;
|
|
|
- imgData.drawText(x, (y += height), title);
|
|
|
- }
|
|
|
- const content =
|
|
|
- detail.mainNumber +
|
|
|
- "-" +
|
|
|
- detail.subNumber +
|
|
|
- " : " +
|
|
|
- detail.score +
|
|
|
- " " +
|
|
|
- (detail.marker || "") +
|
|
|
- " " +
|
|
|
- (detail.header || "");
|
|
|
- width = Math.max(width, content.length * fontSize);
|
|
|
- imgData.drawText(x, (y += height), content);
|
|
|
- }
|
|
|
- }
|
|
|
- //研究生考试模式,按分组显示明细
|
|
|
- else if (trackMode === "2") {
|
|
|
- const title = "评卷分组 | 总分 | 评卷员 | 仲裁员";
|
|
|
- const startY = y;
|
|
|
- let width = title.length * fontSize;
|
|
|
- imgData.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: [],
|
|
|
- };
|
|
|
- groups[detail.groupNumber] = group;
|
|
|
- maxGroupNumber = Math.max(maxGroupNumber, group.number);
|
|
|
- }
|
|
|
- group.score = group.score + detail.score;
|
|
|
- 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 > size.height) {
|
|
|
- y = startY;
|
|
|
- x += width;
|
|
|
- imgData.drawText(x, (y += height), title);
|
|
|
- }
|
|
|
- const content =
|
|
|
- group.number +
|
|
|
- "(" +
|
|
|
- group.titleString.join(",") +
|
|
|
- ")" +
|
|
|
- " " +
|
|
|
- group.score +
|
|
|
- " " +
|
|
|
- group.markerString.join(",") +
|
|
|
- " " +
|
|
|
- group.headerString.join(",");
|
|
|
- width = Math.max(width, content.length * fontSize);
|
|
|
- imgData.drawText(x, (y += height), content);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- //显示评卷标记
|
|
|
- if (student.tags != undefined && student.tags[index] != undefined) {
|
|
|
- const fontSize = 60;
|
|
|
- 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) {
|
|
|
- let top = tag.top;
|
|
|
- for (let j = 0; j < tag.content.length; j++) {
|
|
|
- imgData.drawText(tag.left, top, tag.content[j]);
|
|
|
- top += height;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- imgData.write(file, (error) => {
|
|
|
- if (error) {
|
|
|
- logger.error("add watermark error: " + file);
|
|
|
- logger.error(error);
|
|
|
- reject(error);
|
|
|
- } else {
|
|
|
- resolve();
|
|
|
- }
|
|
|
- });
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- async downloadUrl(url) {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- request_util(
|
|
|
- {
|
|
|
- url: url,
|
|
|
- method: "GET",
|
|
|
- encoding: null,
|
|
|
- timeout: 3000,
|
|
|
- maxAttempts: 3,
|
|
|
- retryDelay: 500,
|
|
|
- retryStrategy: request_util.RetryStrategies.HTTPOrNetworkError,
|
|
|
- },
|
|
|
- function (error, response, body) {
|
|
|
- if (!error && response.statusCode == 200) {
|
|
|
- resolve(body);
|
|
|
- } else {
|
|
|
- logger.error(error || url + " download error");
|
|
|
- error = error || {};
|
|
|
- error.code = response ? response.statusCode : 500;
|
|
|
- reject(error);
|
|
|
- }
|
|
|
- }
|
|
|
- );
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- async downloadFile(
|
|
|
- type,
|
|
|
- append,
|
|
|
- url,
|
|
|
- localTemplate,
|
|
|
- data,
|
|
|
- dir,
|
|
|
- index,
|
|
|
- watermark,
|
|
|
- trackMode
|
|
|
- ) {
|
|
|
- data.index = index;
|
|
|
- const local = path.join(dir, mustache.render(localTemplate, data));
|
|
|
- mkdirp.sync(path.dirname(local));
|
|
|
-
|
|
|
- //续传模式下,判断目标文件是否存在,存在则直接跳过
|
|
|
- if (append && fs.existsSync(local)) {
|
|
|
- return Promise.resolve();
|
|
|
- } else {
|
|
|
- let imgData;
|
|
|
- try {
|
|
|
- imgData = await this.downloadUrl(url);
|
|
|
- } catch (err) {
|
|
|
- if (err.code === 404) {
|
|
|
- //文件不存在,记录日志并跳过
|
|
|
- downloadLogger.error("404 " + type + " " + url);
|
|
|
- return Promise.resolve();
|
|
|
- } else {
|
|
|
- logger.error(err);
|
|
|
- return Promise.reject(err);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- //是否需要添加分数水印
|
|
|
- if (watermark) {
|
|
|
- return this.addWatermark(imgData, local, data, index, trackMode);
|
|
|
- } else {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- fs.writeFile(local, imgData, (err) => {
|
|
|
- if (err) {
|
|
|
- logger.error("write image file error: " + local);
|
|
|
- logger.error(err);
|
|
|
- reject(err);
|
|
|
- } else {
|
|
|
- resolve();
|
|
|
- }
|
|
|
- });
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- async downloadSheet(
|
|
|
- dir,
|
|
|
- template,
|
|
|
- append,
|
|
|
- failover,
|
|
|
- watermark,
|
|
|
- trackMode,
|
|
|
- params
|
|
|
- ) {
|
|
|
- params.upload = true;
|
|
|
- params.withSheetUrl = true;
|
|
|
- params.withScoreDetail = watermark === true;
|
|
|
- params.withMarkTrack = watermark === true;
|
|
|
- params.withGroupScoreTrack = watermark === true && trackMode === "1";
|
|
|
-
|
|
|
- try {
|
|
|
- const totalCount = await api.countStudents(env.examId, params);
|
|
|
- this.emit("total", totalCount);
|
|
|
-
|
|
|
- let count = 0;
|
|
|
- let pageNumber = 0;
|
|
|
- this.emit("count", 0);
|
|
|
- for (;;) {
|
|
|
- pageNumber++;
|
|
|
- const array = await api.getStudents(env.examId, pageNumber, 10, params);
|
|
|
- if (array == undefined || array.length == 0) {
|
|
|
- break;
|
|
|
- }
|
|
|
- for (let i = 0; i < array.length; i++) {
|
|
|
- const promises = [];
|
|
|
- const student = array[i];
|
|
|
- student.examId = env.examId;
|
|
|
- for (let i = 0; i < student.sheetUrls.length; i++) {
|
|
|
- promises.push(
|
|
|
- this.downloadFile(
|
|
|
- "sheet",
|
|
|
- append,
|
|
|
- student.sheetUrls[i],
|
|
|
- template,
|
|
|
- student,
|
|
|
- dir,
|
|
|
- i + 1,
|
|
|
- watermark,
|
|
|
- trackMode
|
|
|
- )
|
|
|
- );
|
|
|
- }
|
|
|
- try {
|
|
|
- //等待所有图片下载完毕
|
|
|
- await Promise.all(promises);
|
|
|
- count++;
|
|
|
- this.emit("count", count);
|
|
|
- } catch (err) {
|
|
|
- //判断是否异常终止
|
|
|
- if (failover) {
|
|
|
- throw err;
|
|
|
- } else {
|
|
|
- logger.error("download sheet error:" + err);
|
|
|
- logger.error(err);
|
|
|
- continue;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- this.emit("finish");
|
|
|
- } catch (error) {
|
|
|
- logger.error("download sheet error:" + error);
|
|
|
- logger.error(error);
|
|
|
- this.emit("error", error);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- async downloadPackage(dir, template, append, failover) {
|
|
|
- try {
|
|
|
- const array = await api.getPackages(env.examId, true, true);
|
|
|
- this.emit("total", array.length);
|
|
|
- let count = 0;
|
|
|
- this.emit("count", 0);
|
|
|
- for (let i = 0; i < array.length; i++) {
|
|
|
- const p = array[i];
|
|
|
- p.examId = env.examId;
|
|
|
- for (let i = 0; i < p.urls.length; i++) {
|
|
|
- try {
|
|
|
- await this.downloadFile(
|
|
|
- "package",
|
|
|
- append,
|
|
|
- p.urls[i],
|
|
|
- template,
|
|
|
- p,
|
|
|
- dir,
|
|
|
- i + 1
|
|
|
- );
|
|
|
- } catch (err) {
|
|
|
- //判断是否异常终止
|
|
|
- if (failover) {
|
|
|
- throw err;
|
|
|
- } else {
|
|
|
- logger.error("download package error: " + err);
|
|
|
- logger.error(err);
|
|
|
- continue;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- count++;
|
|
|
- this.emit("count", count);
|
|
|
- }
|
|
|
- this.emit("finish");
|
|
|
- } catch (error) {
|
|
|
- logger.error("download package error: " + error);
|
|
|
- logger.error(error);
|
|
|
- this.emit("error", error);
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-export default function () {
|
|
|
- return new executor();
|
|
|
-}
|