astro-lqip 1.1.0 → 1.2.2
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 +40 -14
- package/package.json +8 -8
- package/src/components/Picture.astro +31 -41
- package/src/constants/index.ts +1 -0
- package/src/types/index.ts +3 -0
- package/src/types/lqip.type.ts +1 -0
- package/src/types/plaiceholder.type.ts +3 -0
- package/src/types/props.type.ts +18 -0
- package/src/utils/generateLqip.ts +44 -0
- package/src/utils/getLqip.ts +20 -0
- package/src/utils/getLqipStyle.ts +17 -0
- package/src/utils/renderSVGNode.ts +22 -0
- package/src/utils/resolveImageMetadata.ts +6 -0
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<a href="https://github.com/felixicaza/astro-lqip/">
|
|
2
|
-
<img src="./assets/
|
|
2
|
+
<img src="./assets/logo.png" alt="Astro LQIP Logo" width="200" height="200" />
|
|
3
3
|
</a>
|
|
4
4
|
|
|
5
5
|
# 🖼️ astro-lqip
|
|
@@ -7,9 +7,7 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/astro-lqip)
|
|
8
8
|
[](https://github.com/felixicaza/astro-lqip/blob/main/LICENSE)
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
[See Demo.](https://astro-lqip.web.app/)
|
|
10
|
+
Native extended Astro component for generating low quality image placeholders (LQIP).
|
|
13
11
|
|
|
14
12
|
## ⬇️ Installation
|
|
15
13
|
|
|
@@ -33,29 +31,57 @@ yarn add astro-lqip
|
|
|
33
31
|
|
|
34
32
|
## 🚀 Usage
|
|
35
33
|
|
|
36
|
-
In your current Astro project, just replace the native Astro `<Picture
|
|
34
|
+
In your current Astro project, just replace the import of the native Astro `<Picture />` component with the one provided by [astro-lqip](https://www.npmjs.com/package/astro-lqip).
|
|
35
|
+
|
|
36
|
+
```diff
|
|
37
|
+
- import { Picture } from 'astro:assets';
|
|
38
|
+
+ import { Picture } from 'astro-lqip/components';
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Example:
|
|
37
42
|
|
|
38
43
|
```astro
|
|
39
44
|
---
|
|
40
|
-
|
|
41
|
-
import
|
|
45
|
+
import { Picture } from 'astro-lqip/components'
|
|
46
|
+
import image from './path/to/image.png'
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
<Picture src={image} alt="Cover Image" width={220} height={220} />
|
|
50
|
+
```
|
|
42
51
|
|
|
43
|
-
|
|
52
|
+
## ⚙️ Props
|
|
53
|
+
|
|
54
|
+
This `<Picture />` component supports all the props of the [native Astro component](https://docs.astro.build/en/reference/modules/astro-assets/#picture-properties), but adds a couple of props for LQIP management:
|
|
55
|
+
|
|
56
|
+
- `lqip`: The LQIP type to use. It can be one of the following:
|
|
57
|
+
- `base64`: Generates a Base64-encoded LQIP image. (default option)
|
|
58
|
+
- `color`: Generates a solid color placeholder. Not compatible with `lqipSize`.
|
|
59
|
+
- `css`: Generates a CSS-based LQIP image.
|
|
60
|
+
- `svg`: Generates an SVG-based LQIP image.
|
|
61
|
+
- `lqipSize`: The size of the LQIP image, which can be any number from `4` to `64`. (default is 4)
|
|
62
|
+
|
|
63
|
+
> [!WARNING]
|
|
64
|
+
> A major size in the lqipSize prop can significantly impact the performance of your application.
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
|
|
68
|
+
```astro
|
|
69
|
+
---
|
|
70
|
+
import { Picture } from 'astro-lqip/components'
|
|
71
|
+
import image from './path/to/image.png'
|
|
44
72
|
---
|
|
45
73
|
|
|
46
|
-
<Picture
|
|
47
|
-
src={image}
|
|
48
|
-
alt="Description of the image"
|
|
49
|
-
/>
|
|
74
|
+
<Picture src={image} alt="Cover Image" width={220} height={220} lqip="css" lqipSize={7} />
|
|
50
75
|
```
|
|
51
76
|
|
|
52
77
|
## 📝 ToDo
|
|
53
78
|
|
|
54
79
|
- [ ] Add support for Image component.
|
|
55
|
-
- [
|
|
80
|
+
- [x] Add support for more lqip techniques.
|
|
56
81
|
- [ ] Test for remote images.
|
|
57
82
|
- [ ] Optimize current CSS usage.
|
|
58
|
-
- [
|
|
83
|
+
- [x] Improve docs page.
|
|
84
|
+
- [ ] Test support for SSR mode.
|
|
59
85
|
|
|
60
86
|
## 💡 Knowledge
|
|
61
87
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-lqip",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Native extended Astro component for generating low quality image placeholders (LQIP).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -36,20 +36,20 @@
|
|
|
36
36
|
"plaiceholder": "3.0.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@eslint/js": "9.30.
|
|
40
|
-
"@typescript-eslint/parser": "8.
|
|
41
|
-
"astro": "5.
|
|
39
|
+
"@eslint/js": "9.30.1",
|
|
40
|
+
"@typescript-eslint/parser": "8.36.0",
|
|
41
|
+
"astro": "5.11.0",
|
|
42
42
|
"bumpp": "10.2.0",
|
|
43
|
-
"eslint": "9.30.
|
|
43
|
+
"eslint": "9.30.1",
|
|
44
44
|
"eslint-plugin-astro": "1.3.1",
|
|
45
45
|
"eslint-plugin-jsonc": "2.20.1",
|
|
46
46
|
"eslint-plugin-jsx-a11y": "6.10.2",
|
|
47
|
-
"eslint-plugin-package-json": "0.
|
|
47
|
+
"eslint-plugin-package-json": "0.44.1",
|
|
48
48
|
"eslint-plugin-yml": "1.18.0",
|
|
49
|
-
"globals": "16.
|
|
49
|
+
"globals": "16.3.0",
|
|
50
50
|
"jiti": "2.4.2",
|
|
51
51
|
"nano-staged": "0.8.0",
|
|
52
|
-
"neostandard": "0.12.
|
|
52
|
+
"neostandard": "0.12.2",
|
|
53
53
|
"simple-git-hooks": "2.13.0"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
@@ -1,59 +1,49 @@
|
|
|
1
1
|
---
|
|
2
|
-
import {
|
|
3
|
-
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import type { Props } from '../types'
|
|
4
3
|
|
|
5
|
-
import {
|
|
4
|
+
import { PREFIX } from '../constants'
|
|
6
5
|
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
6
|
+
import { resolveImageMetadata } from '../utils/resolveImageMetadata'
|
|
7
|
+
import { renderSVGNode } from '../utils/renderSVGNode'
|
|
8
|
+
import { getLqipStyle } from '../utils/getLqipStyle'
|
|
9
|
+
import { getLqip } from '../utils/getLqip'
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
import { Picture as PictureComponent } from 'astro:assets'
|
|
11
12
|
|
|
12
|
-
const { class: className, ...props } = Astro.props as Props
|
|
13
|
+
const { class: className, lqip = 'base64', lqipSize = 4, pictureAttributes = {}, ...props } = Astro.props as Props
|
|
13
14
|
|
|
14
|
-
const
|
|
15
|
+
const isDevelopment = import.meta.env.MODE === 'development'
|
|
15
16
|
|
|
16
17
|
const imageMetadata = await resolveImageMetadata(props.src)
|
|
17
|
-
const
|
|
18
|
+
const lqipImage = await getLqip(imageMetadata, isDevelopment, lqip, lqipSize)
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
if (typeof src === 'string') return null
|
|
21
|
-
if ('then' in src && typeof src.then === 'function') return (await src).default
|
|
22
|
-
if ('src' in src) return src
|
|
23
|
-
return null
|
|
24
|
-
}
|
|
20
|
+
let svgHTML = ''
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const buffer = await readFile(filePath)
|
|
29
|
-
const { base64 } = await getPlaiceholder(buffer)
|
|
30
|
-
|
|
31
|
-
if (isDevelopment) {
|
|
32
|
-
console.log(`${PREFIX} LQIP successfully generated!`)
|
|
33
|
-
} else {
|
|
34
|
-
console.log(`${PREFIX} LQIP successfully generated for:`, imageMetadata.src)
|
|
35
|
-
}
|
|
36
|
-
return base64
|
|
37
|
-
} catch (err) {
|
|
38
|
-
console.error(`${errorPrefix} Error generating LQIP in:`, filePath, '\n', err)
|
|
39
|
-
return undefined
|
|
40
|
-
}
|
|
22
|
+
if (lqip === 'svg' && Array.isArray(lqipImage)) {
|
|
23
|
+
svgHTML = renderSVGNode(lqipImage as [string, Record<string, any>, any[]])
|
|
41
24
|
}
|
|
42
25
|
|
|
43
|
-
|
|
44
|
-
if (!imageMetadata?.src) return undefined
|
|
26
|
+
const lqipStyle = getLqipStyle(lqip, lqipImage, svgHTML)
|
|
45
27
|
|
|
46
|
-
|
|
28
|
+
const forbiddenVars = ['--lqip-background', '--z-index', '--opacity']
|
|
29
|
+
const styleProps = pictureAttributes.style ?? {}
|
|
47
30
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
31
|
+
for (const key of Object.keys(styleProps)) {
|
|
32
|
+
if (forbiddenVars.includes(key)) {
|
|
33
|
+
console.warn(
|
|
34
|
+
`${PREFIX} The CSS variable “${key}” should not be passed in \`pictureAttributes.style\` because it can override the functionality of LQIP.`
|
|
35
|
+
)
|
|
51
36
|
}
|
|
37
|
+
}
|
|
52
38
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
39
|
+
const combinedStyle = {
|
|
40
|
+
...styleProps,
|
|
41
|
+
...lqipStyle
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const combinedPictureAttributes = {
|
|
45
|
+
...pictureAttributes,
|
|
46
|
+
style: combinedStyle
|
|
57
47
|
}
|
|
58
48
|
---
|
|
59
49
|
|
|
@@ -91,6 +81,6 @@ async function getLqip(imageMetadata: any) {
|
|
|
91
81
|
<PictureComponent
|
|
92
82
|
{...props}
|
|
93
83
|
class={className}
|
|
94
|
-
pictureAttributes={{
|
|
84
|
+
pictureAttributes={{ ...combinedPictureAttributes }}
|
|
95
85
|
onload="parentElement.style.setProperty('--z-index', 1), parentElement.style.setProperty('--opacity', 0)"
|
|
96
86
|
/>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PREFIX = '[astro-lqip]'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type LqipType = 'color' | 'css' | 'base64' | 'svg'
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Props as PictureProps } from 'astro/components/Picture.astro'
|
|
2
|
+
|
|
3
|
+
import type { LqipType } from './lqip.type'
|
|
4
|
+
|
|
5
|
+
export type Props = PictureProps & {
|
|
6
|
+
/**
|
|
7
|
+
* LQIP type.
|
|
8
|
+
* This can be 'color', 'css', 'svg' or 'base64'.
|
|
9
|
+
* The default value is 'base64'.
|
|
10
|
+
*/
|
|
11
|
+
lqip?: LqipType
|
|
12
|
+
/**
|
|
13
|
+
* Size of the LQIP image in pixels.
|
|
14
|
+
* This value should be between 4 and 64.
|
|
15
|
+
* The default value is 4.
|
|
16
|
+
*/
|
|
17
|
+
lqipSize?: number
|
|
18
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
|
|
3
|
+
import type { GetSVGReturn, LqipType } from '../types'
|
|
4
|
+
|
|
5
|
+
import { PREFIX } from '../constants'
|
|
6
|
+
|
|
7
|
+
import { getPlaiceholder } from 'plaiceholder'
|
|
8
|
+
|
|
9
|
+
export async function generateLqip(filePath: string, isDevelopment: boolean, lqipType: LqipType, lqipSize: number) {
|
|
10
|
+
try {
|
|
11
|
+
const buffer = await readFile(filePath)
|
|
12
|
+
const plaiceholderResult = await getPlaiceholder(buffer, { size: lqipSize })
|
|
13
|
+
let lqipValue: string | GetSVGReturn | undefined
|
|
14
|
+
|
|
15
|
+
switch (lqipType) {
|
|
16
|
+
case 'color':
|
|
17
|
+
lqipValue = plaiceholderResult.color?.hex
|
|
18
|
+
break
|
|
19
|
+
case 'css':
|
|
20
|
+
lqipValue = typeof plaiceholderResult.css === 'object' && plaiceholderResult.css.backgroundImage
|
|
21
|
+
? plaiceholderResult.css.backgroundImage
|
|
22
|
+
: String(plaiceholderResult.css)
|
|
23
|
+
break
|
|
24
|
+
case 'svg':
|
|
25
|
+
lqipValue = plaiceholderResult.svg
|
|
26
|
+
break
|
|
27
|
+
case 'base64':
|
|
28
|
+
default:
|
|
29
|
+
lqipValue = plaiceholderResult.base64
|
|
30
|
+
break
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (isDevelopment) {
|
|
34
|
+
console.log(`${PREFIX} LQIP (${lqipType}) successfully generated!`)
|
|
35
|
+
} else {
|
|
36
|
+
console.log(`${PREFIX} LQIP (${lqipType}) successfully generated for:`, filePath)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return lqipValue
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error(`${PREFIX} Error generating LQIP (${lqipType}) in:`, filePath, '\n', err)
|
|
42
|
+
return undefined
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import type { ImageMetadata } from 'astro'
|
|
4
|
+
import type { LqipType } from '../types'
|
|
5
|
+
|
|
6
|
+
import { generateLqip } from './generateLqip'
|
|
7
|
+
|
|
8
|
+
export async function getLqip(imageMetadata: ImageMetadata, envMode: boolean, lqipType: LqipType, lqipSize: number) {
|
|
9
|
+
if (!imageMetadata?.src) return undefined
|
|
10
|
+
|
|
11
|
+
if (envMode && imageMetadata.src.startsWith('/@fs/')) {
|
|
12
|
+
const filePath = imageMetadata.src.replace(/^\/@fs/, '').split('?')[0]
|
|
13
|
+
return await generateLqip(filePath, envMode, lqipType, lqipSize)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!envMode && imageMetadata.src.startsWith('/_astro/')) {
|
|
17
|
+
const buildPath = join(process.cwd(), 'dist', imageMetadata.src)
|
|
18
|
+
return await generateLqip(buildPath, envMode, lqipType, lqipSize)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { GetSVGReturn, LqipType } from '../types'
|
|
2
|
+
|
|
3
|
+
export function getLqipStyle(lqipType: LqipType, lqipImage: string | GetSVGReturn | undefined, svgHTML: string = '') {
|
|
4
|
+
if (!lqipImage) return {}
|
|
5
|
+
|
|
6
|
+
switch (lqipType) {
|
|
7
|
+
case 'css':
|
|
8
|
+
return { '--lqip-background': lqipImage }
|
|
9
|
+
case 'svg':
|
|
10
|
+
return { '--lqip-background': `url('data:image/svg+xml;utf8,${encodeURIComponent(svgHTML)}')` }
|
|
11
|
+
case 'color':
|
|
12
|
+
return { '--lqip-background': lqipImage }
|
|
13
|
+
case 'base64':
|
|
14
|
+
default:
|
|
15
|
+
return { '--lqip-background': `url('${lqipImage}')` }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function styleToString(style: Record<string, string>) {
|
|
2
|
+
return Object.entries(style)
|
|
3
|
+
.map(([key, val]) => `${key.replace(/([A-Z])/g, '-$1').toLowerCase()}:${val}`)
|
|
4
|
+
.join(';')
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function renderSVGNode([tag, attrs, children]: [string, Record<string, any>, any[]]): string {
|
|
8
|
+
let attrString = ''
|
|
9
|
+
for (const [k, v] of Object.entries(attrs || {})) {
|
|
10
|
+
if (k === 'style') {
|
|
11
|
+
attrString += ` style="${styleToString(v)}"`
|
|
12
|
+
} else {
|
|
13
|
+
attrString += ` ${k}="${v}"`
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (children && children.length > 0) {
|
|
18
|
+
return `<${tag}${attrString}>${children.map(renderSVGNode).join('')}</${tag}>`
|
|
19
|
+
} else {
|
|
20
|
+
return `<${tag}${attrString} />`
|
|
21
|
+
}
|
|
22
|
+
}
|