astro-lqip 1.1.0 → 1.2.1
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 +39 -14
- package/package.json +1 -1
- package/src/components/Picture.astro +13 -46
- 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,56 @@ 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.
|
|
59
84
|
|
|
60
85
|
## 💡 Knowledge
|
|
61
86
|
|
package/package.json
CHANGED
|
@@ -1,60 +1,27 @@
|
|
|
1
1
|
---
|
|
2
|
-
import {
|
|
3
|
-
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import type { Props } from '../types'
|
|
4
3
|
|
|
5
|
-
import {
|
|
4
|
+
import { resolveImageMetadata } from '../utils/resolveImageMetadata'
|
|
5
|
+
import { renderSVGNode } from '../utils/renderSVGNode'
|
|
6
|
+
import { getLqipStyle } from '../utils/getLqipStyle'
|
|
7
|
+
import { getLqip } from '../utils/getLqip'
|
|
6
8
|
|
|
7
9
|
import { Picture as PictureComponent } from 'astro:assets'
|
|
8
|
-
import type { Props as PictureProps } from 'astro/components/Picture.astro'
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
const { class: className, lqip = 'base64', lqipSize = 4, ...props } = Astro.props as Props
|
|
11
12
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const PREFIX = '[astro-lqip]'
|
|
13
|
+
const isDevelopment = import.meta.env.MODE === 'development'
|
|
15
14
|
|
|
16
15
|
const imageMetadata = await resolveImageMetadata(props.src)
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
async function resolveImageMetadata(src: any) {
|
|
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
|
-
}
|
|
16
|
+
const lqipImage = await getLqip(imageMetadata, isDevelopment, lqip, lqipSize)
|
|
25
17
|
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
const buffer = await readFile(filePath)
|
|
29
|
-
const { base64 } = await getPlaiceholder(buffer)
|
|
18
|
+
let svgHTML = ''
|
|
30
19
|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
}
|
|
20
|
+
if (lqip === 'svg' && Array.isArray(lqipImage)) {
|
|
21
|
+
svgHTML = renderSVGNode(lqipImage as [string, Record<string, any>, any[]])
|
|
41
22
|
}
|
|
42
23
|
|
|
43
|
-
|
|
44
|
-
if (!imageMetadata?.src) return undefined
|
|
45
|
-
|
|
46
|
-
const isDevelopment = import.meta.env.MODE === 'development'
|
|
47
|
-
|
|
48
|
-
if (isDevelopment && imageMetadata.src.startsWith('/@fs/')) {
|
|
49
|
-
const filePath = imageMetadata.src.replace(/^\/@fs/, '').split('?')[0]
|
|
50
|
-
return await tryGenerateLqip(filePath, PREFIX, isDevelopment)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (!isDevelopment && imageMetadata.src.startsWith('/_astro/')) {
|
|
54
|
-
const buildPath = join(process.cwd(), 'dist', imageMetadata.src)
|
|
55
|
-
return await tryGenerateLqip(buildPath, PREFIX, isDevelopment)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
24
|
+
const lqipStyle = getLqipStyle(lqip, lqipImage, svgHTML)
|
|
58
25
|
---
|
|
59
26
|
|
|
60
27
|
<style is:inline>
|
|
@@ -91,6 +58,6 @@ async function getLqip(imageMetadata: any) {
|
|
|
91
58
|
<PictureComponent
|
|
92
59
|
{...props}
|
|
93
60
|
class={className}
|
|
94
|
-
pictureAttributes={{ style:
|
|
61
|
+
pictureAttributes={{ style: lqipStyle }}
|
|
95
62
|
onload="parentElement.style.setProperty('--z-index', 1), parentElement.style.setProperty('--opacity', 0)"
|
|
96
63
|
/>
|
|
@@ -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
|
+
}
|