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 2, 2021
1 parent 6ff06ea commit 6388601
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 0 deletions.
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://example.com/image` - returns the image at the given URL as a JPEG at 800px
- `GET /?w=500&t=webp&u=https://example.com/image` - returns the image at the given URL as a WebP at 500px
20 changes: 20 additions & 0 deletions src/api/image/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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;
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.
61 changes: 61 additions & 0 deletions src/api/image/image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const sharp = require('sharp');

// TODO: logging

// https://sharp.pixelplumbing.com/api-resize
const resize = (width) => {
const s = sharp({ failOnError: false });
// Only resize if we're given a width
return width > 0 ? s.resize({ width }) : s;
};

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(),
};

/**
* OptDownloads and reduces the image at `url` to the given `width`, sending
* the response back on the `res` stream.
*/
function optimize({ imgStream, width, imageType, 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);
}

function setType(type = 'jpeg', res) {
switch (type.toLowerCase()) {
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;
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('./app.js');

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

module.exports = server;
28 changes: 28 additions & 0 deletions src/api/image/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"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": {
"body-parser": "1.19.0",
"celebrate": "^13.0.4",
"cors": "2.8.5",
"express": "4.17.1",
"express-handlebars": "5.1.0",
"got": "^11.8.1",
"helmet": "4.1.1",
"sharp": "^0.27.1"
},
"engines": {
"node": ">=12.0.0"
}
}
65 changes: 65 additions & 0 deletions src/api/image/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(),
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 6388601

Please sign in to comment.