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 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
+ };
@@ -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
+ };
@@ -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,10 @@
1
+ 'use strict';
2
+
3
+ module.exports = ({ strapi }) => {
4
+ strapi.plugin('upload').contentTypes.file.attributes.blurhash = {
5
+ type: 'text',
6
+ };
7
+ strapi.plugin('upload').contentTypes.file.attributes.blurDataURL = {
8
+ type: 'text',
9
+ };
10
+ };
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ const blurhash = require('./blurhash');
4
+
5
+ module.exports = {
6
+ blurhash,
7
+ };
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('./server');