//云端图片操作工具 const EventEmitter = require('events') const api = require('./api.js') const env = require('./env.js') const config = require('./config.js') const logger = require('./logger.js')('image.js') const downloadLogger = require('./logger.js')('download') const fs = require('fs') const path = require('path') const readline = require('readline') const request_util = require('requestretry') const sizeOf = require('image-size') const mustache = require('mustache') const mkdirp = require('mkdirp') 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 => { var data = [] if (fs.existsSync(file)) { let 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) { let fontFile = config.watermark.fontFile let color = config.watermark.color let size = sizeOf(image) let imgData = gm(image) //添加第一页的得分明细 if (index == 1) { //初始坐标 let x = 30 let y = 10 //最大宽/高限制 let fontSize = config.watermark.fontSize || 30 let maxX = size.width / 2 - x * 2 let height = fontSize + 10 //计算总分 let 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) { let lines = [] let array = [] //前置提示文字的字符数 let count = 10 lines.push(array) for (let i = 0; i < student.objectiveScoreDetail.length; i++) { let detail = student.objectiveScoreDetail[i] let 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') { let title = '主观题号 | 分数 | 评卷员 | 仲裁员' let startY = y let width = title.length * fontSize imgData.drawText(x, y += height, title) for (let i = 0; i < student.subjectiveScoreDetail.length; i++) { let detail = student.subjectiveScoreDetail[i] //超过最大高度了则另起一列 if ((y + height + 15) > size.height) { y = startY x += width imgData.drawText(x, y += height, title) } let 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') { let title = '评卷分组 | 总分 | 评卷员 | 仲裁员' let startY = y let width = title.length * fontSize imgData.drawText(x, y += height, title) //所有小题得分按评卷分组聚合 let maxGroupNumber = 0 let groups = {} for (let i = 0; i < student.subjectiveScoreDetail.length; i++) { let 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++) { let group = groups[i] if (group != undefined) { //超过最大高度了则另起一列 if ((y + height + 15) > size.height) { y = startY x += width imgData.drawText(x, y += height, title) } let 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) { let fontSize = 60 let height = fontSize + 10 imgData.font(fontFile, fontSize).fill(color) let tags = student.tags[index] for (let i = 0; i < tags.length; i++) { let 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 let 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 { let totalCount = await api.countStudents(env.examId, params) this.emit('total', totalCount) let count = 0 let pageNumber = 0 this.emit('count', 0) for (; ;) { pageNumber++ let array = await api.getStudents(env.examId, pageNumber, 10, params) if (array == undefined || array.length == 0) { break } for (let i = 0; i < array.length; i++) { let promises = [] let 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 { let 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++) { let 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) } } } module.exports = function () { return new executor() }