|
1 | 1 | --- |
2 | | -import { join } from 'node:path' |
3 | | -import { readFile } from 'node:fs/promises' |
| 2 | +import type { Props } from '../types' |
4 | 3 |
|
5 | | -import type { GetPlaiceholderReturn } from 'plaiceholder' |
6 | | -import { getPlaiceholder } from 'plaiceholder' |
| 4 | +import { resolveImageMetadata } from '../utils/resolveImageMetadata' |
| 5 | +import { renderSVGNode } from '../utils/renderSVGNode' |
| 6 | +import { getLqipStyle } from '../utils/getLqipStyle' |
| 7 | +import { getLqip } from '../utils/getLqip' |
7 | 8 |
|
8 | | -import type { Props as PictureProps } from 'astro/components/Picture.astro' |
9 | 9 | import { Picture as PictureComponent } from 'astro:assets' |
10 | 10 |
|
11 | | -type GetSVGReturn = GetPlaiceholderReturn['svg'] |
12 | | -type LqipType = 'color' | 'css' | 'base64' | 'svg' |
13 | | -type Props = PictureProps & { |
14 | | - lqip?: LqipType |
15 | | -} |
16 | | -
|
17 | 11 | const { class: className, lqip = 'base64', ...props } = Astro.props as Props |
18 | 12 |
|
19 | | -const PREFIX = '[astro-lqip]' |
| 13 | +const isDevelopment = import.meta.env.MODE === 'development' |
20 | 14 |
|
21 | 15 | const imageMetadata = await resolveImageMetadata(props.src) |
22 | | -const lqipImage = await getLqip(imageMetadata, lqip) |
| 16 | +const lqipImage = await getLqip(imageMetadata, isDevelopment, lqip) |
23 | 17 |
|
24 | 18 | let svgHTML = '' |
25 | 19 |
|
26 | | -if (lqip === 'svg') { |
27 | | - function styleToString(style: Record<string, string>) { |
28 | | - return Object.entries(style) |
29 | | - .map(([key, val]) => `${key.replace(/([A-Z])/g, '-$1').toLowerCase()}:${val}`) |
30 | | - .join(';') |
31 | | - } |
32 | | -
|
33 | | - function renderNode([tag, attrs, children]: [string, Record<string, any>, any[]]): string { |
34 | | - let attrString = '' |
35 | | - for (const [k, v] of Object.entries(attrs || {})) { |
36 | | - if (k === 'style') { |
37 | | - attrString += ` style="${styleToString(v)}"` |
38 | | - } else { |
39 | | - attrString += ` ${k}="${v}"` |
40 | | - } |
41 | | - } |
42 | | -
|
43 | | - if (children && children.length > 0) { |
44 | | - return `<${tag}${attrString}>${children.map(renderNode).join('')}</${tag}>` |
45 | | - } else { |
46 | | - return `<${tag}${attrString} />` |
47 | | - } |
48 | | - } |
49 | | -
|
50 | | - if (Array.isArray(lqipImage)) { |
51 | | - svgHTML = renderNode(lqipImage as [string, Record<string, any>, any[]]) |
52 | | - } |
53 | | -} |
54 | | -
|
55 | | -async function resolveImageMetadata(src: any) { |
56 | | - if (typeof src === 'string') return null |
57 | | - if ('then' in src && typeof src.then === 'function') return (await src).default |
58 | | - if ('src' in src) return src |
59 | | - return null |
60 | | -} |
61 | | -
|
62 | | -async function tryGenerateLqip(filePath: string, errorPrefix: string, isDevelopment: boolean, lqipType: LqipType) { |
63 | | - try { |
64 | | - const buffer = await readFile(filePath) |
65 | | - const result = await getPlaiceholder(buffer) |
66 | | - let lqipValue: string | GetSVGReturn | undefined |
67 | | -
|
68 | | - switch (lqipType) { |
69 | | - case 'color': |
70 | | - lqipValue = result.color?.hex |
71 | | - break |
72 | | - case 'css': |
73 | | - lqipValue = typeof result.css === 'object' && result.css.backgroundImage |
74 | | - ? result.css.backgroundImage |
75 | | - : String(result.css) |
76 | | - break |
77 | | - case 'svg': |
78 | | - lqipValue = result.svg |
79 | | - break |
80 | | - case 'base64': |
81 | | - default: |
82 | | - lqipValue = result.base64 |
83 | | - break |
84 | | - } |
85 | | -
|
86 | | - if (isDevelopment) { |
87 | | - console.log(`${PREFIX} LQIP (${lqipType}) successfully generated!`) |
88 | | - } else { |
89 | | - console.log(`${PREFIX} LQIP (${lqipType}) successfully generated for:`, filePath) |
90 | | - } |
91 | | - return lqipValue |
92 | | - } catch (err) { |
93 | | - console.error(`${errorPrefix} Error generating LQIP (${lqipType}) in:`, filePath, '\n', err) |
94 | | - return undefined |
95 | | - } |
96 | | -} |
97 | | -
|
98 | | -async function getLqip(imageMetadata: any, lqipType: LqipType) { |
99 | | - if (!imageMetadata?.src) return undefined |
100 | | -
|
101 | | - const isDevelopment = import.meta.env.MODE === 'development' |
102 | | -
|
103 | | - if (isDevelopment && imageMetadata.src.startsWith('/@fs/')) { |
104 | | - const filePath = imageMetadata.src.replace(/^\/@fs/, '').split('?')[0] |
105 | | - return await tryGenerateLqip(filePath, PREFIX, isDevelopment, lqipType) |
106 | | - } |
107 | | -
|
108 | | - if (!isDevelopment && imageMetadata.src.startsWith('/_astro/')) { |
109 | | - const buildPath = join(process.cwd(), 'dist', imageMetadata.src) |
110 | | - return await tryGenerateLqip(buildPath, PREFIX, isDevelopment, lqipType) |
111 | | - } |
112 | | -} |
113 | | -
|
114 | | -function getLqipStyle(lqipType: LqipType, lqipImage: string | GetSVGReturn | undefined) { |
115 | | - if (!lqipImage) return {} |
116 | | -
|
117 | | - switch (lqipType) { |
118 | | - case 'css': |
119 | | - return { '--lqip-background': lqipImage } |
120 | | - case 'svg': |
121 | | - return { '--lqip-background': `url('data:image/svg+xml;utf8,${encodeURIComponent(svgHTML)}')` } |
122 | | - case 'color': |
123 | | - return { '--lqip-background': lqipImage } |
124 | | - case 'base64': |
125 | | - default: |
126 | | - return { '--lqip-background': `url('${lqipImage}')` } |
127 | | - } |
| 20 | +if (lqip === 'svg' && Array.isArray(lqipImage)) { |
| 21 | + svgHTML = renderSVGNode(lqipImage as [string, Record<string, any>, any[]]) |
128 | 22 | } |
129 | 23 |
|
130 | | -const lqipStyle = getLqipStyle(lqip, lqipImage) |
| 24 | +const lqipStyle = getLqipStyle(lqip, lqipImage, svgHTML) |
131 | 25 | --- |
132 | 26 |
|
133 | 27 | <style is:inline> |
|
0 commit comments