getImageExtracts.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. 'use strict'
  2. import express from 'express'
  3. import sharp from 'sharp'
  4. import { constants as fsConstants, promises as fs } from 'fs'
  5. import path from 'path'
  6. import boom from 'boom'
  7. import { asyncMiddleware, checkSceneName, checkRequiredParameters } from '../functions'
  8. import { getImage } from './getImage'
  9. import { imageServedUrl, imagesPath, extractsDirName } from '../../config'
  10. const router = express.Router()
  11. /**
  12. * @typedef {import('./getImage').Image} Image
  13. */
  14. /**
  15. * Cut an image, save its extracts and get the url of these extracts
  16. *
  17. * @param {Image} image the path to the image to cut
  18. * @param {Number} xExtracts the number of extract to do on the horizontal axis (integer)
  19. * @param {Number} yExtracts the number of extract to do on the vertical axis (integer)
  20. * @returns {Promise<Image[]>} the list of extracted images
  21. */
  22. const cutImage = async (image, xExtracts, yExtracts) => {
  23. const input = sharp(image.path)
  24. const { width, height } = await input.metadata()
  25. const xCropSize = width / xExtracts
  26. const yCropSize = height / yExtracts
  27. // Check the image is cuttable with the current parameters
  28. let errorsList = []
  29. if (!Number.isInteger(xCropSize)) errorsList.push('Incompatible number of horizontal extracts (width % numberOfExtracts != 0)')
  30. if (!Number.isInteger(yCropSize)) errorsList.push('Incompatible number of vertical extracts (height % numberOfExtracts != 0)')
  31. if (errorsList.length > 0) throw boom.badRequest('Invalid query parameter(s).', errorsList)
  32. let extracts = []
  33. // Cut images
  34. // Vertical
  35. for (let y = 0; y < yExtracts; y++) {
  36. // Horizontal
  37. for (let x = 0; x < xExtracts; x++) {
  38. // How to cut the image
  39. const config = {
  40. left: x * xCropSize,
  41. top: y * yCropSize,
  42. width: xCropSize,
  43. height: yCropSize
  44. }
  45. // Zone number of the extract `00020`
  46. const fileNameCount = (extracts.length + 1).toString().padStart(5, '0')
  47. // File name of the extract : `Scene2_zone00199_100.png`
  48. const extractName = `${image.sceneName}_zone${fileNameCount}_${image.quality}.${image.ext}`
  49. // Configured path to the image (Check defined convention)
  50. const pathToImage = [image.sceneName, extractsDirName, `x${xExtracts}_y${yExtracts}`, `zone${fileNameCount}`, extractName]
  51. // File system path to the extract
  52. const extractPath = path.resolve(imagesPath, ...pathToImage)
  53. // URL to the extract on the app
  54. const extractLink = `${imageServedUrl}/${pathToImage.join('/')}`
  55. const extractObj = {
  56. link: extractLink,
  57. path: extractPath,
  58. fileName: extractName,
  59. sceneName: image.sceneName
  60. }
  61. // Check the file already exist
  62. let fileAlreadyExists = false
  63. try {
  64. await fs.access(extractPath, fsConstants.R_OK)
  65. fileAlreadyExists = true
  66. }
  67. catch (err) {
  68. // File does not exist already
  69. }
  70. // File already exist, just send its data
  71. if (fileAlreadyExists) {
  72. extracts.push(extractObj)
  73. continue
  74. }
  75. // File does not already exist, create it
  76. // Create the arborescence
  77. try {
  78. await fs.mkdir(path.resolve(imagesPath, ...pathToImage.slice(0, pathToImage.length - 1)), { recursive: true })
  79. }
  80. catch (err) {
  81. // An error was caught, add it and go to next extract
  82. errorsList.push(err.message)
  83. continue
  84. }
  85. // Cut and save the extract
  86. try {
  87. await input.extract(config).toFile(extractPath)
  88. extracts.push(extractObj)
  89. }
  90. catch (err) {
  91. // Error while cutting image
  92. errorsList.push(err)
  93. }
  94. }
  95. }
  96. // Extraction finished, check for errors
  97. if (errorsList.length > 0) throw boom.internal('Error(s) while extracting from image.', errorsList)
  98. return extracts
  99. }
  100. router.get('/', asyncMiddleware(async (req, res) => {
  101. // Check the request contains all the required parameters
  102. checkRequiredParameters(['sceneName', 'imageQuality', 'horizontalExtractCount', 'verticalExtractCount'], req.query)
  103. const { sceneName, imageQuality, horizontalExtractCount, verticalExtractCount } = req.query
  104. let errorList = []
  105. // Check the scene name is valid
  106. try {
  107. checkSceneName(sceneName)
  108. }
  109. catch (err) {
  110. errorList.push(err.message)
  111. }
  112. // Check `imageQuality` is an integer
  113. const qualityInt = parseInt(imageQuality, 10)
  114. if (isNaN(qualityInt)) errorList.push('The specified quality is not an integer.')
  115. // Check `horizontalExtractCount` is an integer
  116. const horizontalExtractCountInt = parseInt(horizontalExtractCount, 10)
  117. if (isNaN(horizontalExtractCountInt)) errorList.push('The specified number of extract for the horizontal axis is not an integer.')
  118. // Check `imageQuality` is an integer
  119. const verticalExtractCountInt = parseInt(verticalExtractCount, 10)
  120. if (isNaN(verticalExtractCountInt)) errorList.push('The specified number of extract for the vertical axis is not an integer.')
  121. // Check there is no errors with parameters
  122. if (errorList.length > 0)
  123. throw boom.badRequest('Invalid query parameter(s).', errorList)
  124. // Get the image path and link
  125. const image = await getImage(sceneName, qualityInt)
  126. // Cut the image
  127. const extracts = await cutImage(image, horizontalExtractCountInt, verticalExtractCountInt)
  128. // Send an array of links
  129. res.json({ data: extracts.map(x => x.link) })
  130. }))
  131. export default router