getImage.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. 'use strict'
  2. import express from 'express'
  3. import path from 'path'
  4. import sharp from 'sharp'
  5. import boom from '@hapi/boom'
  6. import { imagesPath, imageServedUrl } from '../../config'
  7. import { asyncMiddleware, checkSceneName, checkRequiredParameters, getSceneFilesData } from '../functions'
  8. const router = express.Router()
  9. /**
  10. * @api {get} /getImage?sceneName=:sceneName&imageQuality=:imageQuality&nearestQuality=:nearestQuality /getImage
  11. * @apiVersion 0.1.0
  12. * @apiName getImage
  13. * @apiGroup API
  14. *
  15. * @apiDescription Get an image from a scene with the required quality
  16. *
  17. * @apiParam {String} sceneName The selected scene
  18. * @apiParam {String="min","max","median", "any integer"} imageQuality The required quality of the image (can be an integer, `min`, `max` or `median`)
  19. * @apiParam {Boolean} [nearestQuality=false] if selected quality not availabie, select the nearest one
  20. *
  21. * @apiExample Usage example
  22. * curl -i -L -X GET "http://diran.univ-littoral.fr/api/getImage?sceneName=bathroom&imageQuality=200"
  23. *
  24. * @apiSuccess {Object} data Informations on the image
  25. * @apiSuccess {String} data.link Path to the image
  26. * @apiSuccess {String} data.fileName File name of the image
  27. * @apiSuccess {String} data.sceneName Scene name of the image
  28. * @apiSuccess {Number} data.quality Quality of the image
  29. * @apiSuccess {String} data.ext Extension of the image
  30. * @apiSuccess {Object} data.metadata Metadata of the image, @see https://sharp.dimens.io/en/stable/api-input/#metadata
  31. * @apiSuccessExample {json} Success response example
  32. * HTTP/1.1 200 OK /api/getImage?sceneName=bathroom&imageQuality=200
  33. * {
  34. * "data": {
  35. * "link": "/api/images/bathroom/bathroom_00200.png",
  36. * "fileName": "bathroom_00200.png",
  37. * "sceneName": "bathroom",
  38. * "quality": 200,
  39. * "ext": "png",
  40. * "metadata": {
  41. * "format": "png",
  42. * "width": 800,
  43. * "height": 800,
  44. * "space": "rgb16",
  45. * "channels": 3,
  46. * "depth": "ushort",
  47. * "density": 72,
  48. * "isProgressive": false,
  49. * "hasProfile": false,
  50. * "hasAlpha": false
  51. * }
  52. * }
  53. * }
  54. *
  55. * @apiError (Error 4xx) 400_[1] Missing parameter(s)
  56. * @apiErrorExample {json} Missing parameter
  57. * HTTP/1.1 400 Bad Request
  58. * {
  59. * "message": "Missing parameter(s). Required parameters : sceneName, imageQuality."
  60. * }
  61. *
  62. * @apiError (Error 4xx) 400_[2] Invalid query parameter
  63. * @apiErrorExample {json} Invalid query parameter(s)
  64. * HTTP/1.1 400 Bad Request
  65. * {
  66. * "message": "Invalid query parameter(s).",
  67. * "data": [
  68. * "The requested scene name \".//../\" is not valid.",
  69. * "The specified quality is not an integer.",
  70. * "Impossible to use \"min\", \"max\" or \"median\" with \"nearestQuality\" on."
  71. * ]
  72. * }
  73. *
  74. * @apiError (Error 4xx) 404_[1] Quality not found
  75. * @apiErrorExample {json} Quality not found
  76. * HTTP/1.1 404 Not Found
  77. * {
  78. * "message": "The requested quality (9999) was not found for the requested scene (bathroom)."
  79. * }
  80. *
  81. * @apiError (Error 5xx) 500_[1] Can't access the `IMAGES_PATH` directory
  82. * @apiErrorExample {json} Images directory not accessible
  83. * HTTP/1.1 500 Internal Server Error
  84. * {
  85. * "message": "Can't access the \"images\" directory. Check it exists and you have read permission on it"
  86. * }
  87. *
  88. * @apiError (Error 5xx) 500_[2] Failed to parse a file's name
  89. * @apiErrorExample {json} Failed to parse a file's name
  90. * HTTP/1.1 500 Internal Server Error
  91. * {
  92. * "message": "Failed to parse file names in the \"bathroom\"'s scene directory.",
  93. * "data": [
  94. * "The file name does not match convention (scene_000150.ext - /^(.*)?_([0-9]{2,})\\.(.*)$/) : \"bathroom_adz00020.png\".",
  95. * "The file name does not match convention (scene_000150.ext - /^(.*)?_([0-9]{2,})\\.(.*)$/) : \"bathroom_adz00020.png\"."
  96. * ]
  97. * }
  98. *
  99. */
  100. /**
  101. * @typedef {Object} Image
  102. * @property {string} link the link (URL) to an image on the app
  103. * @property {string} path the path to the image in the file system
  104. * @property {string} fileName the name of the image
  105. * @property {string} sceneName the scene of the image
  106. * @property {number} quality the quality of the image
  107. * @property {string} ext the extension of the image
  108. */
  109. /**
  110. * Get the link and path to an image
  111. * @param {string} sceneName the scene to get the image from
  112. * @param {number|"min"|"max"|"median"} quality the requested quality
  113. * @param {boolean} [nearestQuality=false] if selected quality not availabie, select the nearest one
  114. * @returns {Promise<Image>} the image data
  115. */
  116. export const getImage = async (sceneName, quality, nearestQuality = false) => {
  117. const throwErrIfTrue = x => {
  118. if (x) throw boom.badRequest('Impossible to use "min", "max" or "median" with "nearestQuality" on.')
  119. }
  120. const sceneData = await getSceneFilesData(sceneName)
  121. let imageData = null
  122. // Search an image with the requested quality in the scene
  123. if (quality === 'min') {
  124. throwErrIfTrue(nearestQuality)
  125. const toFind = Math.min(...sceneData.map(x => x.quality))
  126. imageData = sceneData.find(x => x.quality === toFind)
  127. }
  128. else if (quality === 'max') {
  129. throwErrIfTrue(nearestQuality)
  130. const toFind = Math.max(...sceneData.map(x => x.quality))
  131. imageData = sceneData.find(x => x.quality === toFind)
  132. }
  133. else if (quality === 'median') {
  134. throwErrIfTrue(nearestQuality)
  135. imageData = sceneData.length > 0 ? sceneData[Math.ceil(sceneData.length / 2) - 1] : null
  136. }
  137. else {
  138. if (nearestQuality && sceneData.length > 0 && !isNaN(parseInt(quality, 10))) {
  139. let minGap = Number.MAX_SAFE_INTEGER
  140. let minGapImageData = null
  141. for (const x of sceneData) {
  142. const tempGap = Math.abs(x.quality - quality)
  143. if (tempGap < minGap) {
  144. minGap = tempGap
  145. minGapImageData = x
  146. }
  147. }
  148. imageData = minGapImageData
  149. }
  150. else imageData = sceneData.find(x => quality === x.quality)
  151. }
  152. if (imageData) {
  153. // Data gathered from file system
  154. const result = {
  155. link: `${imageServedUrl}/${sceneName}/${imageData.fileName}`,
  156. path: path.resolve(imagesPath, sceneName, imageData.fileName),
  157. fileName: imageData.fileName,
  158. sceneName: imageData.sceneName,
  159. quality: imageData.quality,
  160. ext: imageData.ext
  161. }
  162. // Data gathered by analysing the image
  163. const input = sharp(result.path)
  164. const metadata = await input.metadata()
  165. result.metadata = metadata
  166. return result
  167. }
  168. // Image not found
  169. throw boom.notFound(`The requested quality "${quality}" was not found for the requested scene "${sceneName}".`)
  170. }
  171. router.get('/', asyncMiddleware(async (req, res) => {
  172. // Check the request contains all the required parameters
  173. checkRequiredParameters(['sceneName', 'imageQuality'], req.query)
  174. const { sceneName, imageQuality } = req.query
  175. const nearestQuality = req.query.nearestQuality === 'true'
  176. let errorList = []
  177. // Check the scene name is valid
  178. try {
  179. checkSceneName(sceneName)
  180. }
  181. catch (err) {
  182. errorList.push(err.message)
  183. }
  184. // Check `imageQuality` is an integer or `min`, `max` or `median`
  185. const qualityInt = parseInt(imageQuality, 10)
  186. let quality = null
  187. if (['min', 'median', 'max'].some(x => x === imageQuality)) {
  188. if (nearestQuality)
  189. errorList.push('Impossible to use "min", "max" or "median" with "nearestQuality" on.')
  190. else quality = imageQuality
  191. }
  192. else if (!isNaN(qualityInt))
  193. quality = qualityInt
  194. else
  195. errorList.push('The specified quality is not an integer or "min", "max" or "median".')
  196. // Check there is no errors with parameters
  197. if (errorList.length > 0)
  198. throw boom.badRequest('Invalid query parameter(s).', errorList)
  199. const data = await getImage(sceneName, quality, nearestQuality)
  200. data.path = undefined
  201. res.json({ data })
  202. }))
  203. export default router