Skip to content

Commit

Permalink
POC of image service
Browse files Browse the repository at this point in the history
  • Loading branch information
humphd committed Feb 3, 2021
1 parent 6ff06ea commit 8d7e240
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ module.exports = {

// JavaScript for Node.js
{
files: ['src/backend/**/*.js', 'src/tools/**/*.js'],
files: ['src/backend/**/*.js', 'src/tools/**/*.js', 'src/api/**/*.js'],
env: {
node: true,
},
Expand Down
27 changes: 27 additions & 0 deletions src/api/image/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Image Service

The Image Service can optimize images at a URL, or provide optimized backgrounds.

## Install

```
npm install
```

## Usage

By default the server is running on http://localhost:4444/.

You can use any/all the following optional query params:

1. `w` - the desired width. Must be between `200` and `4592`. Defaults to `800` if missing.
1. `t` - the desired image type. Must be one of `jpeg`, `jpg`, `webp`, `png`, or `avif`. Defaults to `jpeg`.
1. `u` - an image URL to use as the source. Must be an absolute HTTP/HTTPS encoded URL.

### Examples

- `GET /` - returns the default background JPEG with width = 800px
- `GET /?w=1024`- returns the default background JPEG with width = 1024px
- `GET /?t=png`- returns the default background JPEG with width = 800px as a PNG
- `GET /?u=https%3A%2F%2Fexample.com/image` - returns the image at the URL https://example.com/image as a JPEG at 800px
- `GET /?w=500&t=webp&u=https%3A%2F%2Fexample.com/image` - returns the image at the URL https://example.com/image as a WebP at 500px
Binary file added src/api/image/default.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/api/image/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const app = require('./src/app.js');

const server = app.listen(process.env.PORT || 4444);

module.exports = server;
26 changes: 26 additions & 0 deletions src/api/image/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@seneca/telescope-image",
"private": true,
"version": "0.0.1",
"description": "A service for optimizing images",
"scripts": {
"start": "node index.js"
},
"repository": "Seneca-CDOT/telescope",
"license": "BSD-2-Clause",
"bugs": {
"url": "https://github.com/Seneca-CDOT/telescope/issues"
},
"homepage": "https://github.com/Seneca-CDOT/telescope#readme",
"dependencies": {
"celebrate": "^13.0.4",
"cors": "2.8.5",
"express": "4.17.1",
"got": "^11.8.1",
"helmet": "4.1.1",
"sharp": "^0.27.1"
},
"engines": {
"node": ">=12.0.0"
}
}
21 changes: 21 additions & 0 deletions src/api/image/src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');

const service = require('./service');

const app = express();
app.use(helmet());
app.use(cors());

// TODO: figure out how to do logging for each service...
// app.set('logger', logger);
// app.use(logger);

// Include our router with all endpoints
app.use('/', service);

// TODO: what to do with default error handler...
// app.use(errorHandler);

module.exports = app;
66 changes: 66 additions & 0 deletions src/api/image/src/image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const sharp = require('sharp');

// Supported image types
const JPEG = 'jpeg';
const WEBP = 'webp';
const AVIF = 'avif';
const PNG = 'png';

// TODO: logging

// https://sharp.pixelplumbing.com/api-resize
const resize = (width) => sharp({ failOnError: false }).resize({ width });

const transformers = {
// https://sharp.pixelplumbing.com/api-output#jpeg
[JPEG]: (width) => resize(width).jpeg(),
// https://sharp.pixelplumbing.com/api-output#avif
[AVIF]: (width) => resize(width).avif(),
// https://sharp.pixelplumbing.com/api-output#png
[PNG]: (width) => resize(width).png(),
// https://sharp.pixelplumbing.com/api-output#webp
[WEBP]: (width) => resize(width).webp(),
};

/**
* Optimizes (and maybe resizes) the image stream, streaming back on res.
*/
function optimize({ imgStream, width = 800, imageType = JPEG, res }) {
const transformer = transformers[imageType](width);

// TODO: build more error handling on this...
const transformErrorHandler = (err) => {
console.error(err);
// 400 or 500? The image at the given URL isn't valid
res.status(500).end();
};

// TODO: better logging/errors
return imgStream.pipe(transformer).on('error', transformErrorHandler);
}

/**
* Picks the appropriate image type, sets the content type, and returns
* the image type to use in optimize().
*/
function setType(type = JPEG, res) {
switch (type) {
case 'avif':
res.type('image/avif');
return AVIF;
case 'webp':
res.type('image/webp');
return WEBP;
case 'png':
res.type('image/png');
return PNG;
case 'jpeg':
case 'jpg':
default:
res.type('image/jpeg');
return JPEG;
}
}

module.exports.setType = setType;
module.exports.optimize = optimize;
65 changes: 65 additions & 0 deletions src/api/image/src/service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const path = require('path');
const fs = require('fs');
const express = require('express');
const got = require('got');
const { celebrate, Joi, errors, Segments } = require('celebrate');

const { optimize, setType } = require('./image');

const router = express.Router();

const defaultImagePath = path.join(__dirname, '..', 'default.jpg');

const defaultImageStream = () => fs.createReadStream(defaultImagePath);

const urlImageStream = (url) => got.stream(url);

/**
* Support the following query params, all optional:
*
* - u: the image URL to download and resize. Must be absolute http/https
* - w: the width to resize the image to. Must be 200-4592. Defaults to 800.
* - t: the image type to render, one of: jpeg/jpeg, png, webp, avif. Defaults to jpeg.
*/
const schema = {
[Segments.QUERY]: Joi.object().keys({
t: Joi.string().valid('jpeg', 'jpg', 'webp', 'png', 'avif'),
w: Joi.number().integer().min(200).max(4592),
u: Joi.string().uri({
scheme: ['http', 'https'],
}),
}),
};

const optimizeImage = (req, res) => {
const { t, w, u } = req.query;

// We use 800 for the width if none is given
const width = w ? parseInt(w, 10) : 800;

// Set the header and normalize the image type we'll stream
const imageType = setType(t, res);

// Use the provided URL or fall-back to our default image
const stream = u ? urlImageStream(u) : defaultImageStream();

// Deal with URL not being resolvable
stream.on('error', (err) => {
console.error(err);
// 400 or 500? The client is asking for a URL we can't get...
res.status(500).end();
});

// Optimize this image and stream back to the client
optimize({
imgStream: stream,
width,
imageType,
res,
}).pipe(res);
};

router.use('/', celebrate(schema), optimizeImage);
router.use(errors());

module.exports = router;

0 comments on commit 8d7e240

Please sign in to comment.