image.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. //云端图片操作工具
  2. const EventEmitter = require('events')
  3. const api = require('./api.js')
  4. const env = require('./env.js')
  5. const config = require('./config.js')
  6. const logger = require('./logger.js')('image.js')
  7. const downloadLogger = require('./logger.js')('download')
  8. const fs = require('fs')
  9. const path = require('path')
  10. const readline = require('readline')
  11. const request_util = require('requestretry')
  12. const sizeOf = require('image-size')
  13. const mustache = require('mustache')
  14. const mkdirp = require('mkdirp')
  15. const gm = config.imagemagick != undefined ? require('gm').subClass({
  16. imageMagick: true,
  17. appPath: config.imagemagick
  18. }) : require('gm')
  19. class executor extends EventEmitter {
  20. async readFile(file) {
  21. return new Promise(resolve => {
  22. var data = []
  23. if (fs.existsSync(file)) {
  24. let reader = readline.createInterface({
  25. input: fs.createReadStream(file)
  26. })
  27. reader.on('line', line => {
  28. data.push(line)
  29. })
  30. reader.on('close', () => {
  31. resolve(data)
  32. })
  33. } else {
  34. resolve(data)
  35. }
  36. })
  37. }
  38. async addWatermark(image, file, student, index, trackMode) {
  39. let fontFile = config.watermark.fontFile
  40. let color = config.watermark.color
  41. let size = sizeOf(image)
  42. let imgData = gm(image)
  43. //添加第一页的得分明细
  44. if (index == 1) {
  45. //初始坐标
  46. let x = 30
  47. let y = 10
  48. //最大宽/高限制
  49. let fontSize = config.watermark.fontSize || 30
  50. let maxX = size.width / 2 - x * 2
  51. let height = fontSize + 10
  52. //计算总分
  53. let totalScore = (parseFloat(student.objectiveScore) || 0) + (parseFloat(student.subjectiveScore) || 0)
  54. //显示总分明细
  55. imgData.font(fontFile, fontSize).fill(color)
  56. imgData.drawText(x, y += height, '成绩明细')
  57. //普通考试模式,按客观+主观模式显示总分
  58. if (trackMode === '1') {
  59. imgData.drawText(x, y += height, '总分=(客观+主观) | ' + totalScore + '=' + student.objectiveScore + '+' + student.subjectiveScore)
  60. }
  61. //研究生考试模式,只显示总分
  62. else if (trackMode === '2') {
  63. imgData.drawText(x, y += height, '总分=' + totalScore + '分')
  64. }
  65. //显示客观题明细
  66. if (student.objectiveScoreDetail && student.objectiveScoreDetail.length > 0) {
  67. let lines = []
  68. let array = []
  69. //前置提示文字的字符数
  70. let count = 10
  71. lines.push(array)
  72. for (let i = 0; i < student.objectiveScoreDetail.length; i++) {
  73. let detail = student.objectiveScoreDetail[i]
  74. let content = detail.answer + ':' + detail.score
  75. //超长后另起一行显示客观题
  76. if ((count + content.length) * fontSize * 0.7 > maxX) {
  77. array = []
  78. lines.push(array)
  79. count = 10
  80. }
  81. array.push(content)
  82. count += content.length
  83. }
  84. //显示所有行的客观题明细
  85. for (let l = 0; l < lines.length; l++) {
  86. imgData.drawText(x, y += height, '客观题识别结果 | ' + lines[l].join(';'))
  87. }
  88. }
  89. //显示复核人
  90. if (student.inspector) {
  91. imgData.drawText(x, y += height, '复核人: ' + student.inspector.loginName)
  92. }
  93. //显示主观题明细
  94. if (student.subjectiveScoreDetail && student.subjectiveScoreDetail.length > 0) {
  95. //普通考试模式,按小题显示明细
  96. if (trackMode === '1') {
  97. let title = '主观题号 | 分数 | 评卷员 | 仲裁员'
  98. let startY = y
  99. let width = title.length * fontSize
  100. imgData.drawText(x, y += height, title)
  101. for (let i = 0; i < student.subjectiveScoreDetail.length; i++) {
  102. let detail = student.subjectiveScoreDetail[i]
  103. //超过最大高度了则另起一列
  104. if ((y + height + 15) > size.height) {
  105. y = startY
  106. x += width
  107. imgData.drawText(x, y += height, title)
  108. }
  109. let content = detail.mainNumber + '-' + detail.subNumber + ' : ' + detail.score +
  110. ' ' + (detail.marker || '') +
  111. ' ' + (detail.header || '')
  112. width = Math.max(width, content.length * fontSize)
  113. imgData.drawText(x, y += height, content)
  114. }
  115. }
  116. //研究生考试模式,按分组显示明细
  117. else if (trackMode === '2') {
  118. let title = '评卷分组 | 总分 | 评卷员 | 仲裁员'
  119. let startY = y
  120. let width = title.length * fontSize
  121. imgData.drawText(x, y += height, title)
  122. //所有小题得分按评卷分组聚合
  123. let maxGroupNumber = 0
  124. let groups = {}
  125. for (let i = 0; i < student.subjectiveScoreDetail.length; i++) {
  126. let detail = student.subjectiveScoreDetail[i]
  127. let group = groups[detail.groupNumber]
  128. if (group == undefined) {
  129. group = {
  130. number: detail.groupNumber,
  131. score: 0,
  132. title: {},
  133. titleString: [],
  134. marker: {},
  135. markerString: [],
  136. header: {},
  137. headerString: []
  138. }
  139. groups[detail.groupNumber] = group
  140. maxGroupNumber = Math.max(maxGroupNumber, group.number)
  141. }
  142. group.score = group.score + detail.score
  143. if (detail.mainTitle && !group.title[detail.mainTitle]) {
  144. group.titleString.push(detail.mainTitle)
  145. group.title[detail.mainTitle] = true
  146. }
  147. if (detail.marker && !group.marker[detail.marker]) {
  148. group.markerString.push(detail.marker)
  149. group.marker[detail.marker] = true
  150. }
  151. if (detail.header && !group.header[detail.header]) {
  152. group.headerString.push(detail.header)
  153. group.header[detail.header] = true
  154. }
  155. }
  156. for (let i = 1; i <= maxGroupNumber; i++) {
  157. let group = groups[i]
  158. if (group != undefined) {
  159. //超过最大高度了则另起一列
  160. if ((y + height + 15) > size.height) {
  161. y = startY
  162. x += width
  163. imgData.drawText(x, y += height, title)
  164. }
  165. let content = group.number + '(' + group.titleString.join(',') + ')' +
  166. ' ' + group.score +
  167. ' ' + group.markerString.join(',') +
  168. ' ' + group.headerString.join(',')
  169. width = Math.max(width, content.length * fontSize)
  170. imgData.drawText(x, y += height, content)
  171. }
  172. }
  173. }
  174. }
  175. }
  176. //显示评卷标记
  177. if (student.tags != undefined && student.tags[index] != undefined) {
  178. let fontSize = 60
  179. let height = fontSize + 10
  180. imgData.font(fontFile, fontSize).fill(color)
  181. let tags = student.tags[index]
  182. for (let i = 0; i < tags.length; i++) {
  183. let tag = tags[i]
  184. if (tag.content != undefined) {
  185. let top = tag.top
  186. for (let j = 0; j < tag.content.length; j++) {
  187. imgData.drawText(tag.left, top, tag.content[j])
  188. top += height
  189. }
  190. }
  191. }
  192. }
  193. return new Promise((resolve, reject) => {
  194. imgData.write(file, error => {
  195. if (error) {
  196. logger.error('add watermark error: ' + file)
  197. logger.error(error)
  198. reject(error)
  199. } else {
  200. resolve()
  201. }
  202. })
  203. })
  204. }
  205. async downloadUrl(url) {
  206. return new Promise((resolve, reject) => {
  207. request_util({
  208. url: url,
  209. method: 'GET',
  210. encoding: null,
  211. timeout: 3000,
  212. maxAttempts: 3,
  213. retryDelay: 500,
  214. retryStrategy: request_util.RetryStrategies.HTTPOrNetworkError
  215. }, function (error, response, body) {
  216. if (!error && response.statusCode == 200) {
  217. resolve(body)
  218. } else {
  219. logger.error(error || (url + ' download error'))
  220. error = error || {}
  221. error.code = response ? response.statusCode : 500
  222. reject(error)
  223. }
  224. })
  225. })
  226. }
  227. async downloadFile(type, append, url, localTemplate, data, dir, index, watermark, trackMode) {
  228. data.index = index
  229. let local = path.join(dir, mustache.render(localTemplate, data))
  230. mkdirp.sync(path.dirname(local))
  231. //续传模式下,判断目标文件是否存在,存在则直接跳过
  232. if (append && fs.existsSync(local)) {
  233. return Promise.resolve()
  234. } else {
  235. let imgData
  236. try {
  237. imgData = await this.downloadUrl(url)
  238. } catch (err) {
  239. if (err.code === 404) {
  240. //文件不存在,记录日志并跳过
  241. downloadLogger.error('404 ' + type + ' ' + url)
  242. return Promise.resolve()
  243. } else {
  244. logger.error(err)
  245. return Promise.reject(err)
  246. }
  247. }
  248. //是否需要添加分数水印
  249. if (watermark) {
  250. return this.addWatermark(imgData, local, data, index, trackMode)
  251. } else {
  252. return new Promise((resolve, reject) => {
  253. fs.writeFile(local, imgData, err => {
  254. if (err) {
  255. logger.error('write image file error: ' + local)
  256. logger.error(err)
  257. reject(err)
  258. } else {
  259. resolve()
  260. }
  261. })
  262. })
  263. }
  264. }
  265. }
  266. async downloadSheet(dir, template, append, failover, watermark, trackMode, params) {
  267. params.upload = true
  268. params.withSheetUrl = true
  269. params.withScoreDetail = watermark === true
  270. params.withMarkTrack = watermark === true
  271. params.withGroupScoreTrack = watermark === true && trackMode === '1'
  272. try {
  273. let totalCount = await api.countStudents(env.examId, params)
  274. this.emit('total', totalCount)
  275. let count = 0
  276. let pageNumber = 0
  277. this.emit('count', 0)
  278. for (; ;) {
  279. pageNumber++
  280. let array = await api.getStudents(env.examId, pageNumber, 10, params)
  281. if (array == undefined || array.length == 0) {
  282. break
  283. }
  284. for (let i = 0; i < array.length; i++) {
  285. let promises = []
  286. let student = array[i]
  287. student.examId = env.examId
  288. for (let i = 0; i < student.sheetUrls.length; i++) {
  289. promises.push(this.downloadFile('sheet', append, student.sheetUrls[i], template, student, dir, i + 1, watermark, trackMode))
  290. }
  291. try {
  292. //等待所有图片下载完毕
  293. await Promise.all(promises)
  294. count++
  295. this.emit('count', count)
  296. } catch (err) {
  297. //判断是否异常终止
  298. if (failover) {
  299. throw err
  300. } else {
  301. logger.error('download sheet error:' + err)
  302. logger.error(err)
  303. continue
  304. }
  305. }
  306. }
  307. }
  308. this.emit('finish')
  309. } catch (error) {
  310. logger.error('download sheet error:' + error)
  311. logger.error(error)
  312. this.emit('error', error)
  313. }
  314. }
  315. async downloadPackage(dir, template, append, failover) {
  316. try {
  317. let array = await api.getPackages(env.examId, true, true)
  318. this.emit('total', array.length)
  319. let count = 0
  320. this.emit('count', 0)
  321. for (let i = 0; i < array.length; i++) {
  322. let p = array[i]
  323. p.examId = env.examId
  324. for (let i = 0; i < p.urls.length; i++) {
  325. try {
  326. await this.downloadFile('package', append, p.urls[i], template, p, dir, i + 1)
  327. } catch (err) {
  328. //判断是否异常终止
  329. if (failover) {
  330. throw err
  331. } else {
  332. logger.error('download package error: ' + err)
  333. logger.error(err)
  334. continue
  335. }
  336. }
  337. }
  338. count++
  339. this.emit('count', count)
  340. }
  341. this.emit('finish')
  342. } catch (error) {
  343. logger.error('download package error: ' + error)
  344. logger.error(error)
  345. this.emit('error', error)
  346. }
  347. }
  348. }
  349. module.exports = function () {
  350. return new executor()
  351. }