image.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  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 upyun = require('./upyun.js')
  9. const fs = require('fs')
  10. const path = require('path')
  11. const readline = require('readline')
  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, showMarker) {
  39. let fontFile = config.watermark.fontFile
  40. let color = config.watermark.color
  41. let imgData = gm(image)
  42. let size = sizeOf(image)
  43. //添加第一页的得分明细
  44. if (index == 1) {
  45. //初始坐标
  46. let x = 30
  47. let y = 10
  48. //最大宽/高限制
  49. let 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. imgData.drawText(x, y += height, '总分=(客观+主观) | ' + totalScore + '=' + student.objectiveScore + '+' + student.subjectiveScore)
  58. //显示客观题明细
  59. if (student.objectiveScoreDetail && student.objectiveScoreDetail.length > 0) {
  60. let lines = []
  61. let array = []
  62. //前置提示文字的字符数
  63. let count = 10
  64. lines.push(array)
  65. for (let i = 0; i < student.objectiveScoreDetail.length; i++) {
  66. let detail = student.objectiveScoreDetail[i]
  67. let content = detail.answer + ':' + detail.score
  68. //超长后另起一行显示客观题
  69. if ((count + content.length) * fontSize * 0.7 > maxX) {
  70. array = []
  71. lines.push(array)
  72. count = 10
  73. }
  74. array.push(content)
  75. count += content.length
  76. }
  77. //显示所有行的客观题明细
  78. for (let l = 0; l < lines.length; l++) {
  79. imgData.drawText(x, y += height, '客观题识别结果 | ' + lines[l].join(';'))
  80. }
  81. }
  82. //显示主观题明细
  83. if (student.subjectiveScoreDetail && student.subjectiveScoreDetail.length > 0) {
  84. let title = '主观题号 | 分数'
  85. if (showMarker) {
  86. title += ' | 评卷员'
  87. }
  88. let startY = y
  89. let width = title.length * fontSize
  90. imgData.drawText(x, y += height, title)
  91. for (let i = 0; i < student.subjectiveScoreDetail.length; i++) {
  92. let detail = student.subjectiveScoreDetail[i]
  93. //超过最大高度了则另起一列
  94. if ((y + height + 15) > size.height) {
  95. y = startY
  96. x += width
  97. imgData.drawText(x, y += height, title)
  98. }
  99. let content = detail.mainNumber + '-' + detail.subNumber + ' : ' + detail.score
  100. if (showMarker) {
  101. content = content + ' ' + (detail.marker || detail.markerName || '')
  102. }
  103. width = Math.max(width, content.length * fontSize)
  104. imgData.drawText(x, y += height, content)
  105. }
  106. }
  107. }
  108. //显示评卷标记
  109. if (student.tags != undefined && student.tags[index] != undefined) {
  110. let fontSize = 60
  111. let height = fontSize + 10
  112. imgData.font(fontFile, fontSize).fill(color)
  113. let tags = student.tags[index]
  114. for (let i = 0; i < tags.length; i++) {
  115. let tag = tags[i]
  116. if (tag.content != undefined) {
  117. let top = tag.top
  118. for (let j = 0; j < tag.content.length; j++) {
  119. imgData.drawText(tag.left, top, tag.content[j])
  120. top += height
  121. }
  122. }
  123. }
  124. }
  125. return new Promise((resolve, reject) => {
  126. imgData.write(file, error => {
  127. if (error) {
  128. logger.error('add watermark error: ' + file)
  129. logger.error(error)
  130. reject(error)
  131. } else {
  132. resolve()
  133. }
  134. })
  135. })
  136. }
  137. async downloadFile(append, remoteTemplate, localTemplate, data, dir, client, bucket, index, watermark, showMarker) {
  138. data.index = index
  139. let remote = mustache.render(remoteTemplate, data)
  140. let local = path.join(dir, mustache.render(localTemplate, data))
  141. mkdirp.sync(path.dirname(local))
  142. //续传模式下,判断目标文件是否存在,存在则直接跳过
  143. if (append && fs.existsSync(local)) {
  144. return Promise.resolve()
  145. } else {
  146. let imgData
  147. if (config.localStore != undefined && config.localStore.length > 0) {
  148. let cache = path.join(config.localStore, bucket, remote)
  149. if (fs.existsSync(cache)) {
  150. imgData = fs.readFileSync(cache)
  151. }
  152. }
  153. if (imgData == undefined) {
  154. try {
  155. imgData = await client.download(remote)
  156. } catch (err) {
  157. if (err.code === 404) {
  158. //文件不存在,记录日志并跳过
  159. downloadLogger.error('404 ' + bucket + ' ' + remote)
  160. return Promise.resolve()
  161. } else {
  162. logger.error(err)
  163. return Promise.reject(err)
  164. }
  165. }
  166. }
  167. //是否需要添加分数水印
  168. if (watermark) {
  169. return this.addWatermark(imgData, local, data, index, showMarker)
  170. } else {
  171. return new Promise((resolve, reject) => {
  172. fs.writeFile(local, imgData, err => {
  173. if (err) {
  174. logger.error('write image file error: ' + local)
  175. logger.error(err)
  176. reject(err)
  177. } else {
  178. resolve()
  179. }
  180. })
  181. })
  182. }
  183. }
  184. }
  185. async downloadSheet(dir, template, append, failover, watermark, showMarker) {
  186. let bucket = env.server.bucketPrefix + '-sheet'
  187. let client = upyun(bucket, config.upyun.operator, config.upyun.password)
  188. if (env.server.upyunDomain && env.server.upyunDomain != '') {
  189. //局域网模式修改图片服务器地址
  190. client.setDomain(env.server.upyunDomain)
  191. }
  192. try {
  193. let totalCount = await api.countStudents(env.examId, true, undefined)
  194. this.emit('total', totalCount)
  195. let count = 0
  196. let pageNumber = 0
  197. this.emit('count', 0)
  198. for (;;) {
  199. pageNumber++
  200. let array = await api.getStudents(env.examId, pageNumber, 100, true, undefined, watermark === true, watermark === true)
  201. if (array == undefined || array.length == 0) {
  202. break
  203. }
  204. for (let i = 0; i < array.length; i++) {
  205. let promises = []
  206. let student = array[i]
  207. student.examId = env.examId
  208. for (let i = 1; i <= student.sheetCount; i++) {
  209. promises.push(this.downloadFile(append, config.imageUrl.sheet, template, student, dir, client, bucket, i, watermark, showMarker))
  210. }
  211. try {
  212. //等待所有图片下载完毕
  213. await Promise.all(promises)
  214. count++
  215. this.emit('count', count)
  216. } catch (err) {
  217. //判断是否异常终止
  218. if (failover) {
  219. throw err
  220. } else {
  221. logger.error('download sheet error:' + err)
  222. logger.error(err)
  223. continue
  224. }
  225. }
  226. }
  227. }
  228. this.emit('finish')
  229. } catch (error) {
  230. logger.error('download sheet error:' + error)
  231. logger.error(error)
  232. this.emit('error', error)
  233. }
  234. }
  235. async downloadPackage(dir, template, append, failover) {
  236. let bucket = env.server.bucketPrefix + '-package'
  237. let client = upyun(bucket, config.upyun.operator, config.upyun.password)
  238. if (env.server.upyunDomain && env.server.upyunDomain != '') {
  239. client.setDomain(env.server.upyunDomain)
  240. }
  241. try {
  242. let array = await api.getPackages(env.examId, true)
  243. this.emit('total', array.length)
  244. let count = 0
  245. this.emit('count', 0)
  246. for (let i = 0; i < array.length; i++) {
  247. let p = array[i]
  248. p.examId = env.examId
  249. for (let i = 1; i <= p.picCount; i++) {
  250. try {
  251. await this.downloadFile(append, config.imageUrl.package, template, p, dir, client, bucket, i)
  252. } catch (err) {
  253. //判断是否异常终止
  254. if (failover) {
  255. throw err
  256. } else {
  257. logger.error('download package error: ' + err)
  258. logger.error(err)
  259. continue
  260. }
  261. }
  262. }
  263. count++
  264. this.emit('count', count)
  265. }
  266. this.emit('finish')
  267. } catch (error) {
  268. logger.error('download package error: ' + error)
  269. logger.error(error)
  270. this.emit('error', error)
  271. }
  272. }
  273. }
  274. module.exports = function() {
  275. return new executor()
  276. }