image.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  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 PromisePool = require('./promise-pool.js')
  10. const fs = require('fs')
  11. const path = require('path')
  12. const readline = require('readline')
  13. const sizeOf = require('image-size')
  14. const mustache = require('mustache')
  15. const mkdirp = require('mkdirp')
  16. const moment = require('moment')
  17. const gm = config.imagemagick != undefined ? require('gm').subClass({
  18. imageMagick: true,
  19. appPath: config.imagemagick
  20. }) : require('gm')
  21. class executor extends EventEmitter {
  22. async readFile(file) {
  23. return new Promise(resolve => {
  24. var data = []
  25. if (fs.existsSync(file)) {
  26. let reader = readline.createInterface({
  27. input: fs.createReadStream(file)
  28. })
  29. reader.on('line', line => {
  30. data.push(line)
  31. })
  32. reader.on('close', () => {
  33. resolve(data)
  34. })
  35. } else {
  36. resolve(data)
  37. }
  38. })
  39. }
  40. async checkFile(url, client) {
  41. let size = sizeOf(await client.download(url))
  42. if (size.width == 0 || size.height == 0) {
  43. throw 'invalid image data:' + url
  44. }
  45. }
  46. async addWatermark(image, file, student, index, showMarker, showHeader) {
  47. let fontFile = config.watermark.fontFile
  48. let color = config.watermark.color
  49. let imgData = gm(image)
  50. let size = sizeOf(image)
  51. //添加第一页的得分明细
  52. if (index == 1) {
  53. //初始坐标
  54. let x = 30
  55. let y = 10
  56. //最大宽/高限制
  57. let fontSize = 30
  58. let maxX = size.width / 2 - x * 2
  59. let height = fontSize + 10
  60. //计算总分
  61. let totalScore = (parseFloat(student.objectiveScore) || 0) + (parseFloat(student.subjectiveScore) || 0)
  62. //显示总分明细
  63. imgData.font(fontFile, fontSize).fill(color)
  64. imgData.drawText(x, y += height, '成绩明细')
  65. imgData.drawText(x, y += height, '总分=(客观+主观) | ' + totalScore + '=' + student.objectiveScore + '+' + student.subjectiveScore)
  66. //显示客观题明细
  67. if (student.objectiveScoreDetail && student.objectiveScoreDetail.length > 0) {
  68. let lines = []
  69. let array = []
  70. //前置提示文字的字符数
  71. let count = 10
  72. lines.push(array)
  73. for (let i = 0; i < student.objectiveScoreDetail.length; i++) {
  74. let detail = student.objectiveScoreDetail[i]
  75. let content = detail.answer + ':' + detail.score
  76. //超长后另起一行显示客观题
  77. if ((count + content.length) * fontSize * 0.7 > maxX) {
  78. array = []
  79. lines.push(array)
  80. count = 10
  81. }
  82. array.push(content)
  83. count += content.length
  84. }
  85. //显示所有行的客观题明细
  86. for (let l = 0; l < lines.length; l++) {
  87. imgData.drawText(x, y += height, '客观题识别结果 | ' + lines[l].join(';'))
  88. }
  89. }
  90. //显示主观题明细
  91. if (student.subjectiveScoreDetail && student.subjectiveScoreDetail.length > 0) {
  92. let title = '主观题号 | 分数'
  93. if (showMarker) {
  94. title += ' | 评卷员'
  95. if (showHeader) {
  96. title += ' | 复核人'
  97. }
  98. }
  99. let startY = y
  100. let width = title.length * fontSize
  101. imgData.drawText(x, y += height, title)
  102. for (let i = 0; i < student.subjectiveScoreDetail.length; i++) {
  103. let detail = student.subjectiveScoreDetail[i]
  104. //超过最大高度了则另起一列
  105. if ((y + height + 15) > size.height) {
  106. y = startY
  107. x += width
  108. imgData.drawText(x, y += height, title)
  109. }
  110. let content = detail.mainNumber + '-' + detail.subNumber + ' : ' + detail.score
  111. if (showMarker) {
  112. content = content + ' ' + (detail.marker || detail.markerName || '')
  113. if (showHeader) {
  114. content = content + ' ' + (detail.header || '')
  115. }
  116. }
  117. width = Math.max(width, content.length * fontSize)
  118. imgData.drawText(x, y += height, content)
  119. }
  120. }
  121. }
  122. //显示评卷标记
  123. if (student.tags != undefined && student.tags[index] != undefined) {
  124. let fontSize = 60
  125. let height = fontSize + 10
  126. imgData.font(fontFile, fontSize).fill(color)
  127. let tags = student.tags[index]
  128. for (let i = 0; i < tags.length; i++) {
  129. let tag = tags[i]
  130. if (tag.content != undefined) {
  131. let top = tag.top
  132. for (let j = 0; j < tag.content.length; j++) {
  133. imgData.drawText(tag.left, top, tag.content[j])
  134. top += height
  135. }
  136. }
  137. }
  138. }
  139. return new Promise((resolve, reject) => {
  140. imgData.write(file, error => {
  141. if (error) {
  142. logger.error('add watermark error: ' + file)
  143. logger.error(error)
  144. reject(error)
  145. } else {
  146. resolve()
  147. }
  148. })
  149. })
  150. }
  151. async downloadFile(append, remoteTemplate, localTemplate, data, dir, client, bucket, index, watermark, showMarker, showHeader) {
  152. data.index = index
  153. let remote = mustache.render(remoteTemplate, data)
  154. let local = path.join(dir, mustache.render(localTemplate, data))
  155. mkdirp.sync(path.dirname(local))
  156. //续传模式下,判断目标文件是否存在,存在则直接跳过
  157. if (append && fs.existsSync(local)) {
  158. return Promise.resolve()
  159. } else {
  160. let imgData
  161. if (config.localStore != undefined && config.localStore.length > 0) {
  162. let cache = path.join(config.localStore, bucket, remote)
  163. if (fs.existsSync(cache)) {
  164. imgData = fs.readFileSync(cache)
  165. }
  166. }
  167. if (imgData == undefined) {
  168. try {
  169. imgData = await client.download(remote)
  170. } catch (err) {
  171. if (err.code === 404) {
  172. //文件不存在,记录日志并跳过
  173. downloadLogger.error('404 ' + bucket + ' ' + remote)
  174. return Promise.resolve()
  175. } else {
  176. logger.error(err)
  177. return Promise.reject(err)
  178. }
  179. }
  180. }
  181. //是否需要添加分数水印
  182. if (watermark) {
  183. return this.addWatermark(imgData, local, data, index, showMarker, showHeader)
  184. } else {
  185. return new Promise((resolve, reject) => {
  186. fs.writeFile(local, imgData, err => {
  187. if (err) {
  188. logger.error('write image file error: ' + local)
  189. logger.error(err)
  190. reject(err)
  191. } else {
  192. resolve()
  193. }
  194. })
  195. })
  196. }
  197. }
  198. }
  199. async downloadSheet(dir, template, append, failover, watermark, showMarker, showHeader, params) {
  200. let bucket = env.server.bucketPrefix + '-sheet'
  201. let client = upyun(bucket, config.upyun.operator, config.upyun.password)
  202. if (env.server.upyunDomain && env.server.upyunDomain != '') {
  203. //局域网模式修改图片服务器地址
  204. client.setDomain(env.server.upyunDomain)
  205. }
  206. try {
  207. let totalCount = await api.countStudents(env.examId, true, undefined)
  208. this.emit('total', totalCount)
  209. let count = 0
  210. let pageNumber = 0
  211. this.emit('count', 0)
  212. for (;;) {
  213. pageNumber++
  214. let array = await api.getStudents(env.examId, pageNumber, 100, true, undefined, watermark === true, watermark === true, params)
  215. if (array == undefined || array.length == 0) {
  216. break
  217. }
  218. for (let i = 0; i < array.length; i++) {
  219. let promises = []
  220. let student = array[i]
  221. student.examId = env.examId
  222. for (let i = 1; i <= student.sheetCount; i++) {
  223. promises.push(this.downloadFile(append, config.imageUrl.sheet, template, student, dir, client, bucket, i, watermark, showMarker, showHeader))
  224. }
  225. try {
  226. //等待所有图片下载完毕
  227. await Promise.all(promises)
  228. count++
  229. this.emit('count', count)
  230. } catch (err) {
  231. //判断是否异常终止
  232. if (failover) {
  233. throw err
  234. } else {
  235. logger.error('download sheet error:' + err)
  236. logger.error(err)
  237. continue
  238. }
  239. }
  240. }
  241. }
  242. this.emit('finish')
  243. } catch (error) {
  244. logger.error('download sheet error:' + error)
  245. logger.error(error)
  246. this.emit('error', error)
  247. }
  248. }
  249. async downloadPackage(dir, template, append, failover) {
  250. let bucket = env.server.bucketPrefix + '-package'
  251. let client = upyun(bucket, config.upyun.operator, config.upyun.password)
  252. if (env.server.upyunDomain && env.server.upyunDomain != '') {
  253. client.setDomain(env.server.upyunDomain)
  254. }
  255. try {
  256. let array = await api.getPackages(env.examId, true)
  257. this.emit('total', array.length)
  258. let count = 0
  259. this.emit('count', 0)
  260. for (let i = 0; i < array.length; i++) {
  261. let p = array[i]
  262. p.examId = env.examId
  263. for (let i = 1; i <= p.picCount; i++) {
  264. try {
  265. await this.downloadFile(append, config.imageUrl.package, template, p, dir, client, bucket, i)
  266. } catch (err) {
  267. //判断是否异常终止
  268. if (failover) {
  269. throw err
  270. } else {
  271. logger.error('download package error: ' + err)
  272. logger.error(err)
  273. continue
  274. }
  275. }
  276. }
  277. count++
  278. this.emit('count', count)
  279. }
  280. this.emit('finish')
  281. } catch (error) {
  282. logger.error('download package error: ' + error)
  283. logger.error(error)
  284. this.emit('error', error)
  285. }
  286. }
  287. async checkSlice(dir, concurrent) {
  288. let bucket = env.server.bucketPrefix + '-slice'
  289. let client = upyun(bucket, config.upyun.operator, config.upyun.password)
  290. if (env.server.upyunDomain && env.server.upyunDomain != '') {
  291. //局域网模式修改图片服务器地址
  292. client.setDomain(env.server.upyunDomain)
  293. }
  294. try {
  295. let logFile = path.join(dir, 'result.txt')
  296. fs.writeFileSync(logFile, moment().format('YYYY-MM-DD HH:mm:ss') + ', examId=' + env.examId + '\r\n')
  297. let totalCount = await api.countStudents(env.examId, true, false)
  298. this.emit('total', totalCount)
  299. let self = this
  300. let count = 0
  301. let pageNumber = 0
  302. let pool = PromisePool.create(concurrent, function(student) {
  303. student.examId = env.examId
  304. student.promises = []
  305. for (let i = 1; i <= student.sliceCount; i++) {
  306. student.index = i
  307. let url = mustache.render(config.imageUrl.slice, student)
  308. student.promises.push(new Promise(resolved => {
  309. self.checkFile(url, client).then(() => {
  310. //fs.appendFileSync(logFile, url + ': success\r\n')
  311. }).catch(() => {
  312. fs.appendFileSync(logFile, url + ': error\r\n')
  313. }).finally(() => {
  314. resolved()
  315. })
  316. }))
  317. }
  318. return Promise.all(student.promises)
  319. })
  320. pool.on('count', function(offset) {
  321. self.emit('count', count + offset)
  322. })
  323. this.emit('count', 0)
  324. for (;;) {
  325. pageNumber++
  326. let array = await api.getStudents(env.examId, pageNumber, 200, true, false, false, false)
  327. if (array == undefined || array.length == 0) {
  328. break
  329. }
  330. await pool.start(array)
  331. count += array.length
  332. }
  333. this.emit('finish')
  334. fs.appendFileSync(logFile, moment().format('YYYY-MM-DD HH:mm:ss') + ', examId=' + env.examId)
  335. } catch (error) {
  336. logger.error('check slice error:' + error)
  337. logger.error(error)
  338. this.emit('error', error)
  339. }
  340. }
  341. async checkSliceSerial(dir) {
  342. let bucket = env.server.bucketPrefix + '-slice'
  343. let client = upyun(bucket, config.upyun.operator, config.upyun.password)
  344. if (env.server.upyunDomain && env.server.upyunDomain != '') {
  345. //局域网模式修改图片服务器地址
  346. client.setDomain(env.server.upyunDomain)
  347. }
  348. try {
  349. let logFile = path.join(dir, 'result.txt')
  350. fs.writeFileSync(logFile, moment().format('YYYY-MM-DD HH:mm:ss') + ', examId=' + env.examId + '\r\n')
  351. let totalCount = await api.countStudents(env.examId, true, false)
  352. this.emit('total', totalCount)
  353. let self = this
  354. let count = 0
  355. let pageNumber = 0
  356. this.emit('count', 0)
  357. for (;;) {
  358. pageNumber++
  359. let array = await api.getStudents(env.examId, pageNumber, 100, true, false, false, false)
  360. if (array == undefined || array.length == 0) {
  361. break
  362. }
  363. for (let i = 0; i < array.length; i++) {
  364. let student = array[i]
  365. student.examId = env.examId
  366. student.promises = []
  367. for (let i = 1; i <= student.sliceCount; i++) {
  368. student.index = i
  369. let url = mustache.render(config.imageUrl.slice, student)
  370. student.promises.push(new Promise(resolved => {
  371. self.checkFile(url, client).then(() => {
  372. //fs.appendFileSync(logFile, url + ': success\r\n')
  373. }).catch(() => {
  374. fs.appendFileSync(logFile, url + ': error\r\n')
  375. }).finally(() => {
  376. resolved()
  377. })
  378. }))
  379. }
  380. await Promise.all(student.promises)
  381. count++
  382. this.emit('count', count)
  383. }
  384. }
  385. this.emit('finish')
  386. fs.appendFileSync(logFile, moment().format('YYYY-MM-DD HH:mm:ss') + ', examId=' + env.examId)
  387. } catch (error) {
  388. logger.error('check slice error:' + error)
  389. logger.error(error)
  390. this.emit('error', error)
  391. }
  392. }
  393. }
  394. module.exports = function() {
  395. return new executor()
  396. }