strapi-blurhash-and-dataurl 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -0
- package/package.json +50 -0
- package/server/bootstrap.js +72 -0
- package/server/config.js +16 -0
- package/server/index.js +13 -0
- package/server/register.js +10 -0
- package/server/services/blurhash.js +92 -0
- package/server/services/index.js +7 -0
- package/strapi-server.js +3 -0
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Strapi plugin strapi-blurhash
|
|
2
|
+
|
|
3
|
+
A plugin for <a href="https://github.com/strapi/strapi">Strapi CMS</a> that generates blurhash for your uploaded images.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
To install, run:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install strapi-blurhash
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Open/create file `config/plugins.js`. Enable this plugin by adding:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
module.exports = {
|
|
17
|
+
...
|
|
18
|
+
'strapi-blurhash': {
|
|
19
|
+
enabled: true,
|
|
20
|
+
config: {
|
|
21
|
+
regenerateOnUpdate: true,
|
|
22
|
+
forceRegenerateOnUpdate: false,
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Config explanations:
|
|
29
|
+
- If `forceRegenerateOnUpdate` is true, the plugin disregards whether the blurhash already exists and generates a new one.
|
|
30
|
+
- If `regenerateOnUpdate` is true (and `forceRegenerateOnUpdate` is false), the blurhash will only be generated if it is currently missing. This is a more conservative approach that avoids unnecessary processing but ensures blurhashes are eventually generated for all images.
|
|
31
|
+
|
|
32
|
+
Both of these values are going to be false if omitted.
|
|
33
|
+
|
|
34
|
+
## How to generate blurhash for an image
|
|
35
|
+
|
|
36
|
+
In the Strapi Dashboard open Content Manager. Edit one collection/single type. Add or edit a Media field type and save the collection/single type.
|
|
37
|
+
|
|
38
|
+
## How to get blurhash
|
|
39
|
+
|
|
40
|
+
Target a Strapi REST API endpoint. For example:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
localhost:1337/api/products?populate=Image.*
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The response will be a JSON containing blurhash along with rest of the image data:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
{
|
|
50
|
+
"data": [
|
|
51
|
+
{
|
|
52
|
+
"id": 6,
|
|
53
|
+
"attributes": {
|
|
54
|
+
"name": "Test",
|
|
55
|
+
"createdAt": "2022-10-27T14:52:04.393Z",
|
|
56
|
+
"updatedAt": "2022-10-28T09:58:22.238Z",
|
|
57
|
+
"Image": {
|
|
58
|
+
"data": {
|
|
59
|
+
"id": 80,
|
|
60
|
+
"attributes": {
|
|
61
|
+
"name": "image.png",
|
|
62
|
+
"alternativeText": "image.png",
|
|
63
|
+
"caption": "image.png",
|
|
64
|
+
"width": 960,
|
|
65
|
+
"height": 168,
|
|
66
|
+
"formats": {
|
|
67
|
+
...
|
|
68
|
+
},
|
|
69
|
+
"hash": "image_ed1fbcdba0",
|
|
70
|
+
"ext": ".png",
|
|
71
|
+
"mime": "image/png",
|
|
72
|
+
"size": 4.63,
|
|
73
|
+
"url": "/uploads/image_ed1fbcdba0.png",
|
|
74
|
+
"previewUrl": null,
|
|
75
|
+
"provider": "local",
|
|
76
|
+
"provider_metadata": null,
|
|
77
|
+
"createdAt": "2022-10-28T09:42:02.471Z",
|
|
78
|
+
"updatedAt": "2022-10-28T09:42:02.471Z",
|
|
79
|
+
"blurhash": "U{Nd,T?bof?u_Nxuj[x[objZayoe_Mxuj[x["
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"meta": {
|
|
87
|
+
"pagination": {
|
|
88
|
+
"page": 1,
|
|
89
|
+
"pageSize": 25,
|
|
90
|
+
"pageCount": 1,
|
|
91
|
+
"total": 1
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Regenerating Existing Content
|
|
98
|
+
|
|
99
|
+
If you've recently installed this plugin and already have existing media content, you can easily update the blurhash for these items. Use the **Regenerator** plugin to efficiently handle this task:
|
|
100
|
+
|
|
101
|
+
- [Regenerator Plugin](https://github.com/emil-petras/strapi-regenerator?tab=readme-ov-file)
|
|
102
|
+
|
|
103
|
+
This additional plugin can automate the process.
|
|
104
|
+
|
|
105
|
+
## Links
|
|
106
|
+
|
|
107
|
+
[npm blurhash package](https://www.npmjs.com/package/strapi-blurhash)
|
|
108
|
+
|
|
109
|
+
[github blurhash source](https://github.com/emil-petras/strapi-blurhash)
|
|
110
|
+
|
|
111
|
+
## Changelog
|
|
112
|
+
All notable changes to this project will be documented in this section.
|
|
113
|
+
|
|
114
|
+
### [1.2.1]
|
|
115
|
+
#### Changed
|
|
116
|
+
- now using `sharp` instead of canvas due to issues with .webp images.
|
|
117
|
+
|
|
118
|
+
### [1.2.2]
|
|
119
|
+
#### Added
|
|
120
|
+
- `forceRegenerateOnUpdate` configuration option to force the regeneration of blurhash on every update, regardless of the existing blurhash.
|
|
121
|
+
|
|
122
|
+
### [1.2.3]
|
|
123
|
+
#### Fixed
|
|
124
|
+
- fixed a bug with the image url
|
|
125
|
+
|
|
126
|
+
### [1.2.4]
|
|
127
|
+
#### Fixed
|
|
128
|
+
- fixed a bug with the image url var
|
|
129
|
+
|
|
130
|
+
### [1.2.5]
|
|
131
|
+
#### Changed
|
|
132
|
+
- updated the readme file
|
|
133
|
+
|
|
134
|
+
### [1.2.6]
|
|
135
|
+
#### Changed
|
|
136
|
+
- updated tags
|
|
137
|
+
|
|
138
|
+
### [1.2.7]
|
|
139
|
+
#### Fixed
|
|
140
|
+
- fixed forceRegenerateOnUpdate defaults
|
|
141
|
+
|
|
142
|
+
#### Changed
|
|
143
|
+
- added additional explanation to readme
|
|
144
|
+
|
|
145
|
+
### [1.3.0]
|
|
146
|
+
#### Changed
|
|
147
|
+
- removed sharp dependency
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "strapi-blurhash-and-dataurl",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "A plugin that generates blurhash & dataurl for your uploaded images",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"strapi",
|
|
7
|
+
"plugin",
|
|
8
|
+
"media",
|
|
9
|
+
"blurhash"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/Gaylord-Julien/strapi-blurhash-and-dataurl#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Gaylord-Julien/strapi-blurhash-and-dataurl/issues"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/Gaylord-Julien/strapi-blurhash-and-dataurl.git"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Emil Petras (https://github.com/emil-petras)",
|
|
21
|
+
"maintainers": [
|
|
22
|
+
{
|
|
23
|
+
"name": "Emil Petras",
|
|
24
|
+
"url": "https://github.com/emil-petras"
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"type": "commonjs",
|
|
28
|
+
"main": "strapi-server.js",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"webp-wasm": "^1.0.4",
|
|
34
|
+
"blurhash": "^1.1.4",
|
|
35
|
+
"node-fetch": "^3.1.0",
|
|
36
|
+
"canvas": "^2.11.2"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@strapi/strapi": "^5.0.0"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"npm": ">=6.0.0"
|
|
43
|
+
},
|
|
44
|
+
"strapi": {
|
|
45
|
+
"name": "strapi-blurhash",
|
|
46
|
+
"description": "A plugin that generates blurhash & dataurl for your uploaded images",
|
|
47
|
+
"kind": "plugin",
|
|
48
|
+
"displayName": "Strapi Blurhash and DataURL"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = ({ strapi }) => {
|
|
4
|
+
const blurhashService =
|
|
5
|
+
strapi.plugin('strapi-blurhash').service('blurhash');
|
|
6
|
+
|
|
7
|
+
const host = strapi.config.get('server.host', 'localhost');
|
|
8
|
+
const port = strapi.config.get('server.port', 1337);
|
|
9
|
+
const baseUrl = `${host.startsWith('http') ? host : `http://${host}`}:${port}`;
|
|
10
|
+
|
|
11
|
+
const isImage = (mime) => mime?.startsWith('image/');
|
|
12
|
+
const buildUrl = (url) => (url.startsWith('http') ? url : `${baseUrl}${url}`);
|
|
13
|
+
|
|
14
|
+
const handleBlurhash = async (event, eventType) => {
|
|
15
|
+
const { data, where } = event.params;
|
|
16
|
+
|
|
17
|
+
// ------------------------
|
|
18
|
+
// CREATE
|
|
19
|
+
// ------------------------
|
|
20
|
+
if (eventType === 'beforeCreate' && isImage(data.mime)) {
|
|
21
|
+
const fullUrl = buildUrl(data.url);
|
|
22
|
+
|
|
23
|
+
const { blurhash, blurDataURL } =
|
|
24
|
+
await blurhashService.generateBlurhash(fullUrl, { withDataURL: true });
|
|
25
|
+
|
|
26
|
+
data.blurhash = blurhash;
|
|
27
|
+
data.blurDataURL = blurDataURL;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ------------------------
|
|
32
|
+
// UPDATE
|
|
33
|
+
// ------------------------
|
|
34
|
+
if (eventType !== 'beforeUpdate') return;
|
|
35
|
+
|
|
36
|
+
const regenerateOnUpdate =
|
|
37
|
+
strapi.plugin('strapi-blurhash').config('regenerateOnUpdate');
|
|
38
|
+
const forceRegenerateOnUpdate =
|
|
39
|
+
strapi.plugin('strapi-blurhash').config('forceRegenerateOnUpdate');
|
|
40
|
+
|
|
41
|
+
const existing = await strapi.db
|
|
42
|
+
.query('plugin::upload.file')
|
|
43
|
+
.findOne({
|
|
44
|
+
select: ['url', 'blurhash', 'mime'],
|
|
45
|
+
where,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
!existing ||
|
|
50
|
+
!isImage(existing.mime) ||
|
|
51
|
+
(!forceRegenerateOnUpdate &&
|
|
52
|
+
existing.blurhash &&
|
|
53
|
+
!regenerateOnUpdate)
|
|
54
|
+
) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const fullUrl = buildUrl(existing.url);
|
|
59
|
+
|
|
60
|
+
const { blurhash, blurDataURL } =
|
|
61
|
+
await blurhashService.generateBlurhash(fullUrl, { withDataURL: true });
|
|
62
|
+
|
|
63
|
+
data.blurhash = blurhash;
|
|
64
|
+
data.blurDataURL = blurDataURL;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
strapi.db.lifecycles.subscribe({
|
|
68
|
+
models: ['plugin::upload.file'],
|
|
69
|
+
beforeCreate: (event) => handleBlurhash(event, 'beforeCreate'),
|
|
70
|
+
beforeUpdate: (event) => handleBlurhash(event, 'beforeUpdate'),
|
|
71
|
+
});
|
|
72
|
+
};
|
package/server/config.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
default: ({ env }) => ({
|
|
5
|
+
regenerateOnUpdate: false,
|
|
6
|
+
forceRegenerateOnUpdate: false,
|
|
7
|
+
}),
|
|
8
|
+
validator: (config) => {
|
|
9
|
+
if (typeof config.regenerateOnUpdate !== 'boolean') {
|
|
10
|
+
throw new Error('regenerateOnUpdate has to be a boolean');
|
|
11
|
+
}
|
|
12
|
+
if (typeof config.forceRegenerateOnUpdate !== 'boolean') {
|
|
13
|
+
throw new Error('forceRegenerateOnUpdate has to be a boolean');
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
};
|
package/server/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const register = require('./register');
|
|
4
|
+
const bootstrap = require('./bootstrap');
|
|
5
|
+
const services = require('./services');
|
|
6
|
+
const config = require('./config');
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
register,
|
|
10
|
+
bootstrap,
|
|
11
|
+
services,
|
|
12
|
+
config,
|
|
13
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const { encode, decode } = require('blurhash');
|
|
2
|
+
const webp = require('webp-wasm');
|
|
3
|
+
const { createCanvas, loadImage } = require('canvas');
|
|
4
|
+
|
|
5
|
+
const BLURHASH_COMPONENTS = 4;
|
|
6
|
+
const PLACEHOLDER_SIZE = 16;
|
|
7
|
+
|
|
8
|
+
module.exports = ({ strapi }) => ({
|
|
9
|
+
async generateBlurhash(url, { withDataURL = false } = {}) {
|
|
10
|
+
try {
|
|
11
|
+
const buffer = await fetchImage(url);
|
|
12
|
+
const imageData = await decodeImage(buffer, url);
|
|
13
|
+
|
|
14
|
+
const blurhash = encode(
|
|
15
|
+
imageData.data,
|
|
16
|
+
imageData.width,
|
|
17
|
+
imageData.height,
|
|
18
|
+
BLURHASH_COMPONENTS,
|
|
19
|
+
BLURHASH_COMPONENTS
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
if (!withDataURL) return blurhash;
|
|
23
|
+
|
|
24
|
+
const blurDataURL = blurhashToDataURL(
|
|
25
|
+
blurhash,
|
|
26
|
+
imageData.width,
|
|
27
|
+
imageData.height
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return { blurhash, blurDataURL };
|
|
31
|
+
} catch (error) {
|
|
32
|
+
strapi.log.error(`[blurhash] ${error.message}`);
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/* ----------------------------- */
|
|
39
|
+
/* Helpers */
|
|
40
|
+
/* ----------------------------- */
|
|
41
|
+
|
|
42
|
+
async function fetchImage(url) {
|
|
43
|
+
const fetchModule = await import('node-fetch');
|
|
44
|
+
const fetch = fetchModule.default;
|
|
45
|
+
|
|
46
|
+
const response = await fetch(url);
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(`Failed to fetch image: ${response.statusText}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return Buffer.from(await response.arrayBuffer());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function decodeImage(buffer, url) {
|
|
55
|
+
if (url.endsWith('.webp')) {
|
|
56
|
+
const decoded = await webp.decode(buffer);
|
|
57
|
+
return {
|
|
58
|
+
width: decoded.width,
|
|
59
|
+
height: decoded.height,
|
|
60
|
+
data: Buffer.from(decoded.data),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const img = await loadImage(buffer);
|
|
65
|
+
const canvas = createCanvas(img.width, img.height);
|
|
66
|
+
const ctx = canvas.getContext('2d');
|
|
67
|
+
ctx.drawImage(img, 0, 0);
|
|
68
|
+
|
|
69
|
+
const imgData = ctx.getImageData(0, 0, img.width, img.height);
|
|
70
|
+
return {
|
|
71
|
+
width: imgData.width,
|
|
72
|
+
height: imgData.height,
|
|
73
|
+
data: Buffer.from(imgData.data),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function blurhashToDataURL(blurhash, width, height) {
|
|
78
|
+
const aspectRatio = width / height;
|
|
79
|
+
const h = PLACEHOLDER_SIZE;
|
|
80
|
+
const w = Math.max(1, Math.round(h * aspectRatio));
|
|
81
|
+
|
|
82
|
+
const pixels = decode(blurhash, w, h);
|
|
83
|
+
const canvas = createCanvas(w, h);
|
|
84
|
+
const ctx = canvas.getContext('2d');
|
|
85
|
+
|
|
86
|
+
const imageData = ctx.createImageData(w, h);
|
|
87
|
+
imageData.data.set(pixels);
|
|
88
|
+
ctx.putImageData(imageData, 0, 0);
|
|
89
|
+
|
|
90
|
+
// WebP = much smaller than PNG
|
|
91
|
+
return canvas.toDataURL('image/webp', 0.6);
|
|
92
|
+
}
|
package/strapi-server.js
ADDED