123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223 |
- 'use strict'
- import express from 'express'
- import path from 'path'
- import sharp from 'sharp'
- import boom from '@hapi/boom'
- import { imagesPath, imageServedUrl } from '../../config'
- import { asyncMiddleware, checkSceneName, checkRequiredParameters, getSceneFilesData } from '../functions'
- const router = express.Router()
- /**
- * @api {get} /getImage?sceneName=:sceneName&imageQuality=:imageQuality&nearestQuality=:nearestQuality /getImage
- * @apiVersion 0.1.0
- * @apiName getImage
- * @apiGroup API
- *
- * @apiDescription Get an image from a scene with the required quality
- *
- * @apiParam {String} sceneName The selected scene
- * @apiParam {String="min","max","median", "any integer"} imageQuality The required quality of the image (can be an integer, `min`, `max` or `median`)
- * @apiParam {Boolean} [nearestQuality=false] if selected quality not availabie, select the nearest one
- *
- * @apiExample Usage example
- * curl -i -L -X GET "http://diran.univ-littoral.fr/api/getImage?sceneName=bathroom&imageQuality=200"
- *
- * @apiSuccess {Object} data Informations on the image
- * @apiSuccess {String} data.link Path to the image
- * @apiSuccess {String} data.fileName File name of the image
- * @apiSuccess {String} data.sceneName Scene name of the image
- * @apiSuccess {Number} data.quality Quality of the image
- * @apiSuccess {String} data.ext Extension of the image
- * @apiSuccess {Object} data.metadata Metadata of the image, @see https://sharp.dimens.io/en/stable/api-input/#metadata
- * @apiSuccessExample {json} Success response example
- * HTTP/1.1 200 OK /api/getImage?sceneName=bathroom&imageQuality=200
- * {
- * "data": {
- * "link": "/api/images/bathroom/bathroom_00200.png",
- * "fileName": "bathroom_00200.png",
- * "sceneName": "bathroom",
- * "quality": 200,
- * "ext": "png",
- * "metadata": {
- * "format": "png",
- * "width": 800,
- * "height": 800,
- * "space": "rgb16",
- * "channels": 3,
- * "depth": "ushort",
- * "density": 72,
- * "isProgressive": false,
- * "hasProfile": false,
- * "hasAlpha": false
- * }
- * }
- * }
- *
- * @apiError (Error 4xx) 400_[1] Missing parameter(s)
- * @apiErrorExample {json} Missing parameter
- * HTTP/1.1 400 Bad Request
- * {
- * "message": "Missing parameter(s). Required parameters : sceneName, imageQuality."
- * }
- *
- * @apiError (Error 4xx) 400_[2] Invalid query parameter
- * @apiErrorExample {json} Invalid query parameter(s)
- * HTTP/1.1 400 Bad Request
- * {
- * "message": "Invalid query parameter(s).",
- * "data": [
- * "The requested scene name \".//../\" is not valid.",
- * "The specified quality is not an integer.",
- * "Impossible to use \"min\", \"max\" or \"median\" with \"nearestQuality\" on."
- * ]
- * }
- *
- * @apiError (Error 4xx) 404_[1] Quality not found
- * @apiErrorExample {json} Quality not found
- * HTTP/1.1 404 Not Found
- * {
- * "message": "The requested quality (9999) was not found for the requested scene (bathroom)."
- * }
- *
- * @apiError (Error 5xx) 500_[1] Can't access the `IMAGES_PATH` directory
- * @apiErrorExample {json} Images directory not accessible
- * HTTP/1.1 500 Internal Server Error
- * {
- * "message": "Can't access the \"images\" directory. Check it exists and you have read permission on it"
- * }
- *
- * @apiError (Error 5xx) 500_[2] Failed to parse a file's name
- * @apiErrorExample {json} Failed to parse a file's name
- * HTTP/1.1 500 Internal Server Error
- * {
- * "message": "Failed to parse file names in the \"bathroom\"'s scene directory.",
- * "data": [
- * "The file name does not match convention (scene_000150.ext - /^(.*)?_([0-9]{2,})\\.(.*)$/) : \"bathroom_adz00020.png\".",
- * "The file name does not match convention (scene_000150.ext - /^(.*)?_([0-9]{2,})\\.(.*)$/) : \"bathroom_adz00020.png\"."
- * ]
- * }
- *
- */
- /**
- * @typedef {Object} Image
- * @property {string} link the link (URL) to an image on the app
- * @property {string} path the path to the image in the file system
- * @property {string} fileName the name of the image
- * @property {string} sceneName the scene of the image
- * @property {number} quality the quality of the image
- * @property {string} ext the extension of the image
- */
- /**
- * Get the link and path to an image
- * @param {string} sceneName the scene to get the image from
- * @param {number|"min"|"max"|"median"} quality the requested quality
- * @param {boolean} [nearestQuality=false] if selected quality not availabie, select the nearest one
- * @returns {Promise<Image>} the image data
- */
- export const getImage = async (sceneName, quality, nearestQuality = false) => {
- const throwErrIfTrue = x => {
- if (x) throw boom.badRequest('Impossible to use "min", "max" or "median" with "nearestQuality" on.')
- }
- const sceneData = await getSceneFilesData(sceneName)
- let imageData = null
- // Search an image with the requested quality in the scene
- if (quality === 'min') {
- throwErrIfTrue(nearestQuality)
- const toFind = Math.min(...sceneData.map(x => x.quality))
- imageData = sceneData.find(x => x.quality === toFind)
- }
- else if (quality === 'max') {
- throwErrIfTrue(nearestQuality)
- const toFind = Math.max(...sceneData.map(x => x.quality))
- imageData = sceneData.find(x => x.quality === toFind)
- }
- else if (quality === 'median') {
- throwErrIfTrue(nearestQuality)
- imageData = sceneData.length > 0 ? sceneData[Math.ceil(sceneData.length / 2) - 1] : null
- }
- else {
- if (nearestQuality && sceneData.length > 0 && !isNaN(parseInt(quality, 10))) {
- let minGap = Number.MAX_SAFE_INTEGER
- let minGapImageData = null
- for (const x of sceneData) {
- const tempGap = Math.abs(x.quality - quality)
- if (tempGap < minGap) {
- minGap = tempGap
- minGapImageData = x
- }
- }
- imageData = minGapImageData
- }
- else imageData = sceneData.find(x => quality === x.quality)
- }
- if (imageData) {
- // Data gathered from file system
- const result = {
- link: `${imageServedUrl}/${sceneName}/${imageData.fileName}`,
- path: path.resolve(imagesPath, sceneName, imageData.fileName),
- fileName: imageData.fileName,
- sceneName: imageData.sceneName,
- quality: imageData.quality,
- ext: imageData.ext
- }
- // Data gathered by analysing the image
- const input = sharp(result.path)
- const metadata = await input.metadata()
- result.metadata = metadata
- return result
- }
- // Image not found
- throw boom.notFound(`The requested quality "${quality}" was not found for the requested scene "${sceneName}".`)
- }
- router.get('/', asyncMiddleware(async (req, res) => {
- // Check the request contains all the required parameters
- checkRequiredParameters(['sceneName', 'imageQuality'], req.query)
- const { sceneName, imageQuality } = req.query
- const nearestQuality = req.query.nearestQuality === 'true'
- let errorList = []
- // Check the scene name is valid
- try {
- checkSceneName(sceneName)
- }
- catch (err) {
- errorList.push(err.message)
- }
- // Check `imageQuality` is an integer or `min`, `max` or `median`
- const qualityInt = parseInt(imageQuality, 10)
- let quality = null
- if (['min', 'median', 'max'].some(x => x === imageQuality)) {
- if (nearestQuality)
- errorList.push('Impossible to use "min", "max" or "median" with "nearestQuality" on.')
- else quality = imageQuality
- }
- else if (!isNaN(qualityInt))
- quality = qualityInt
- else
- errorList.push('The specified quality is not an integer or "min", "max" or "median".')
- // Check there is no errors with parameters
- if (errorList.length > 0)
- throw boom.badRequest('Invalid query parameter(s).', errorList)
- const data = await getImage(sceneName, quality, nearestQuality)
- data.path = undefined
- res.json({ data })
- }))
- export default router
|