getImageExtracts.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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 '@hapi/boom'
  7. import { asyncMiddleware, checkSceneName, checkRequiredParameters } from '../functions'
  8. import { imageServedUrl, imagesPath, extractsDirName } from '../../config'
  9. import { getImage } from './getImage'
  10. const router = express.Router()
  11. /**
  12. * @api {get} /getImageExtracts?sceneName=:sceneName&imageQuality=:imageQuality&horizontalExtractCount=:horizontalExtractCount&verticalExtractCount=:verticalExtractCount Get image extracts
  13. * @apiVersion 0.1.0
  14. * @apiName GetImageExtracts
  15. * @apiGroup API
  16. *
  17. * @apiDescription Get an image from a scene with the required quality and cut it with the requested configuration
  18. *
  19. * @apiParam {String} sceneName The selected scene
  20. * @apiParam {Number} imageQuality The required quality of the image
  21. * @apiParam {Number} horizontalExtractCount The amount of extracts for the horizontal axis
  22. * @apiParam {Number} verticalExtractCount The amount of extracts for the vertical axis
  23. *
  24. * @apiExample Usage example
  25. * curl -i -L -X GET "http://diran.univ-littoral.fr/api/getImageExtracts?sceneName=bathroom&imageQuality=200&horizontalExtractCount=1&verticalExtractCount=2"
  26. *
  27. * @apiSuccess {String[]} data Path to the extracted images
  28. * @apiSuccessExample {json} Success response example
  29. * HTTP/1.1 200 OK /api/getImageExtracts?sceneName=bathroom&imageQuality=200&horizontalExtractCount=1&verticalExtractCount=2
  30. * {
  31. * "data": [
  32. * "/api/images/bathroom/extracts/x1_y2/zone00001/bathroom_zone00001_200.png",
  33. * "/api/images/bathroom/extracts/x1_y2/zone00002/bathroom_zone00002_200.png"
  34. * ]
  35. * }
  36. *
  37. * @apiError (Error 4xx) 400_[1] Missing parameter(s)
  38. * @apiErrorExample {json} Missing parameter
  39. * HTTP/1.1 400 Bad Request
  40. * {
  41. * "message": "Missing parameter(s). Required parameters : sceneName, imageQuality, horizontalExtractCount, verticalExtractCount."
  42. * }
  43. *
  44. * @apiError (Error 4xx) 400_[2] Invalid query parameter
  45. * @apiErrorExample {json} Invalid query parameter(s)
  46. * HTTP/1.1 400 Bad Request
  47. * {
  48. * "message": "Invalid query parameter(s).",
  49. * "data": [
  50. * "The requested scene name \".//../\" is not valid.",
  51. * "The specified quality is not an integer.",
  52. * "The specified number of extract for the horizontal axis is not an integer.",
  53. * "The specified number of extract for the vertical axis is not an integer."
  54. * ]
  55. * }
  56. *
  57. * @apiError (Error 4xx) 400_[3] Invalid configuration
  58. * @apiErrorExample {json} Invalid configuration
  59. * HTTP/1.1 400 Bad Request
  60. * {
  61. * "message": "Invalid query parameter(s).",
  62. * "data": [
  63. * "Incompatible number of horizontal extracts (width % numberOfExtracts != 0).",
  64. * "Incompatible number of vertical extracts (height % numberOfExtracts != 0)."
  65. * ]
  66. * }
  67. *
  68. * @apiError (Error 4xx) 404_[1] Quality not found
  69. * @apiErrorExample {json} Quality not found
  70. * HTTP/1.1 404 Not Found
  71. * {
  72. * "message": "The requested quality (9999) was not found for the requested scene (bathroom)."
  73. * }
  74. *
  75. * @apiError (Error 5xx) 500_[1] Can't access the `IMAGES_PATH` directory
  76. * @apiErrorExample {json} Images directory not accessible
  77. * HTTP/1.1 500 Internal Server Error
  78. * {
  79. * "message": "Can't access the \"images\" directory. Check it exists and you have read permission on it"
  80. * }
  81. *
  82. * @apiError (Error 5xx) 500_[2] Failed to parse a file's name
  83. * @apiErrorExample {json} Failed to parse a file's name
  84. * HTTP/1.1 500 Internal Server Error
  85. * {
  86. * "message": "Failed to parse file names in the \"bathroom\"'s scene directory.",
  87. * "data": [
  88. * "The file name does not match convention (scene_000150.ext - /^(.*)?_([0-9]{2,})\\.(.*)$/) : \"bathroom_adz00020.png\".",
  89. * "The file name does not match convention (scene_000150.ext - /^(.*)?_([0-9]{2,})\\.(.*)$/) : \"bathroom_adz00020.png\"."
  90. * ]
  91. * }
  92. *
  93. */
  94. /**
  95. * Cut an image, save its extracts and get the url of these extracts
  96. *
  97. * @param {object} image the path to the image to cut
  98. * @param {Number} xExtracts the number of extract to do on the horizontal axis (integer)
  99. * @param {Number} yExtracts the number of extract to do on the vertical axis (integer)
  100. * @returns {Promise<Image[]>} the list of extracted images
  101. */
  102. const cutImage = async (image, xExtracts, yExtracts) => {
  103. const input = sharp(image.path)
  104. const { width, height } = await input.metadata()
  105. const xCropSize = width / xExtracts
  106. const yCropSize = height / yExtracts
  107. // Check the image is cuttable with the current parameters
  108. let errorsList = []
  109. if (!Number.isInteger(xCropSize)) errorsList.push('Incompatible number of horizontal extracts (width % numberOfExtracts != 0).')
  110. if (!Number.isInteger(yCropSize)) errorsList.push('Incompatible number of vertical extracts (height % numberOfExtracts != 0).')
  111. if (errorsList.length > 0) throw boom.badRequest('Invalid query parameter(s).', errorsList)
  112. let extracts = []
  113. // Cut images
  114. // Vertical
  115. for (let y = 0; y < yExtracts; y++) {
  116. // Horizontal
  117. for (let x = 0; x < xExtracts; x++) {
  118. // How to cut the image
  119. const config = {
  120. left: x * xCropSize,
  121. top: y * yCropSize,
  122. width: xCropSize,
  123. height: yCropSize
  124. }
  125. // Zone number of the extract `00020`
  126. const fileNameCount = (extracts.length + 1).toString().padStart(5, '0')
  127. // File name of the extract : `Scene2_zone00199_100.png`
  128. const extractName = `${image.sceneName}_zone${fileNameCount}_${image.quality}.${image.ext}`
  129. // Configured path to the image (Check defined convention)
  130. const pathToImage = [image.sceneName, extractsDirName, `x${xExtracts}_y${yExtracts}`, `zone${fileNameCount}`, extractName]
  131. // File system path to the extract
  132. const extractPath = path.resolve(imagesPath, ...pathToImage)
  133. // URL to the extract on the app
  134. const extractLink = `${imageServedUrl}/${pathToImage.join('/')}`
  135. const extractObj = {
  136. link: extractLink,
  137. path: extractPath,
  138. fileName: extractName,
  139. sceneName: image.sceneName
  140. }
  141. // Check the file already exist
  142. let fileAlreadyExists = false
  143. try {
  144. await fs.access(extractPath, fsConstants.R_OK)
  145. fileAlreadyExists = true
  146. }
  147. catch (err) {
  148. // File does not exist already
  149. }
  150. // File already exist, just send its data
  151. if (fileAlreadyExists) {
  152. extracts.push(extractObj)
  153. continue
  154. }
  155. // File does not already exist, create it
  156. // Create the arborescence
  157. try {
  158. await fs.mkdir(path.resolve(imagesPath, ...pathToImage.slice(0, pathToImage.length - 1)), { recursive: true })
  159. }
  160. catch (err) {
  161. // An error was caught, add it and go to next extract
  162. errorsList.push(err.message)
  163. continue
  164. }
  165. // Cut and save the extract
  166. try {
  167. await input.extract(config).toFile(extractPath)
  168. extracts.push(extractObj)
  169. }
  170. catch (err) {
  171. // Error while cutting image
  172. errorsList.push(err)
  173. }
  174. }
  175. }
  176. // Extraction finished, check for errors
  177. if (errorsList.length > 0) throw boom.internal('Error(s) while extracting from image.', errorsList)
  178. return extracts
  179. }
  180. router.get('/', asyncMiddleware(async (req, res) => {
  181. // Check the request contains all the required parameters
  182. checkRequiredParameters(['sceneName', 'imageQuality', 'horizontalExtractCount', 'verticalExtractCount'], req.query)
  183. const { sceneName, imageQuality, horizontalExtractCount, verticalExtractCount } = req.query
  184. let errorList = []
  185. // Check the scene name is valid
  186. try {
  187. checkSceneName(sceneName)
  188. }
  189. catch (err) {
  190. errorList.push(err.message)
  191. }
  192. // Check `imageQuality` is an integer
  193. const qualityInt = parseInt(imageQuality, 10)
  194. if (isNaN(qualityInt)) errorList.push('The specified quality is not an integer.')
  195. // Check `horizontalExtractCount` is an integer
  196. const horizontalExtractCountInt = parseInt(horizontalExtractCount, 10)
  197. if (isNaN(horizontalExtractCountInt)) errorList.push('The specified number of extract for the horizontal axis is not an integer.')
  198. // Check `imageQuality` is an integer
  199. const verticalExtractCountInt = parseInt(verticalExtractCount, 10)
  200. if (isNaN(verticalExtractCountInt)) errorList.push('The specified number of extract for the vertical axis is not an integer.')
  201. // Check there is no errors with parameters
  202. if (errorList.length > 0)
  203. throw boom.badRequest('Invalid query parameter(s).', errorList)
  204. // Get the image path and link
  205. const image = await getImage(sceneName, qualityInt)
  206. // Cut the image
  207. const extracts = await cutImage(image, horizontalExtractCountInt, verticalExtractCountInt)
  208. // Send an array of links
  209. res.json({ data: extracts.map(x => x.link) })
  210. }))
  211. export default router
  212. // Send an array of links
  213. res.json({ data: extracts.map(x => x.link) })
  214. })
  215. )
  216. export default router