-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
cf8ae15
commit 8a48a35
Showing
10 changed files
with
208 additions
and
152 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,3 @@ | ||
{ | ||
"cSpell.words": [ | ||
"middlewares" | ||
] | ||
"cSpell.words": ["middlewares"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import { parse as urlParse } from 'node:url' | ||
import { match, pathToRegexp } from 'path-to-regexp' | ||
import type { Create, CreateText } from 'sharp' | ||
import sharp from 'sharp' | ||
import { cache } from './cache' | ||
import { DEFAULT_PARAMS } from './constants' | ||
import type { | ||
ImageCacheItem, | ||
ImagePlaceholderOptions, | ||
ImagePlaceholderParams, | ||
ImagePlaceholderQuery, | ||
ImageType, | ||
} from './types' | ||
import { formatColor, formatText, getBackground } from './utils' | ||
|
||
export type CreateOptions = Create | ||
|
||
export type TextOptions = CreateText | ||
|
||
export async function pathToImage( | ||
url: string, | ||
rules: string[], | ||
options: Required<ImagePlaceholderOptions>, | ||
): Promise<ImageCacheItem | undefined> { | ||
if (cache.has(url)) { | ||
return cache.get(url) | ||
} | ||
const { query: urlQuery, pathname } = urlParse(url, true) | ||
|
||
const rule = rules.find((rule) => { | ||
return pathToRegexp(rule).test(pathname!) | ||
}) | ||
|
||
if (!rule) return | ||
|
||
const urlMatch = match(rule, { decode: decodeURIComponent })(pathname!) || { | ||
params: { | ||
width: options.width, | ||
background: getBackground(options.background), | ||
type: options.type, | ||
} as ImagePlaceholderParams, | ||
} | ||
|
||
const params = urlMatch.params as ImagePlaceholderParams | ||
const query = urlQuery as ImagePlaceholderQuery | ||
const imgType = params.type || DEFAULT_PARAMS.type! | ||
const width = Number(params.width) || 300 | ||
const height = Number(params.height) || Math.ceil((width / 16) * 9) | ||
|
||
const createOptions: CreateOptions = { | ||
width, | ||
height, | ||
channels: 4, | ||
background: formatColor( | ||
params.background || getBackground(options.background), | ||
), | ||
} | ||
if (Number(query.noise) === 1) { | ||
createOptions.noise = { | ||
type: 'gaussian', | ||
mean: query.noiseMean || Math.ceil((Math.min(width, height) / 100) * 10), | ||
sigma: query.noiseSigma || 10, | ||
} | ||
} | ||
|
||
const textOptions: TextOptions = { | ||
dpi: Math.floor((Math.min(width, height) / 4) * 3) || 1, | ||
text: formatText( | ||
params.text || options.text || `${width}x${height}`, | ||
formatColor(query.textColor, true) || options.textColor, | ||
), | ||
rgba: true, | ||
} | ||
|
||
const imgBuf = await createImage(imgType, createOptions, textOptions) | ||
const result: ImageCacheItem = { | ||
type: imgType, | ||
buffer: imgBuf, | ||
} | ||
cache.set(url, result) | ||
return result | ||
} | ||
|
||
export async function createImage( | ||
type: ImageType = 'png', | ||
createOptions: CreateOptions, | ||
textOptions?: TextOptions, | ||
) { | ||
let image = sharp({ create: createOptions }) | ||
|
||
textOptions && image.composite([{ input: { text: textOptions } }]) | ||
|
||
switch (type) { | ||
case 'jpg': | ||
case 'jpeg': | ||
image = image.jpeg({ quality: 100 }) | ||
break | ||
case 'webp': | ||
image = image.webp({ quality: 100 }) | ||
break | ||
case 'heif': | ||
image = image.heif({ quality: 100 }) | ||
break | ||
case 'avif': | ||
image = image.avif({ quality: 100 }) | ||
break | ||
case 'gif': | ||
image = image.gif() | ||
break | ||
default: | ||
image = image.png({ compressionLevel: 1 }) | ||
} | ||
|
||
const buf = await image.toBuffer() | ||
|
||
return buf | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,35 +1,89 @@ | ||
import type { Plugin } from 'vite' | ||
import { DEFAULT_PREFIX } from './constants' | ||
import { imagePlaceholderMiddlewares } from './middlewares' | ||
import { generatePathRules } from './pathRules' | ||
import { pathToImage } from './pathToImage' | ||
import type { ImagePlaceholderOptions } from './types' | ||
import { getMimeType, logger } from './utils' | ||
|
||
const parseOptions = ( | ||
options: ImagePlaceholderOptions, | ||
server = false, | ||
): Required<ImagePlaceholderOptions> => { | ||
options = Object.assign( | ||
{ | ||
prefix: DEFAULT_PREFIX, | ||
background: '#ccc', | ||
textColor: '#999', | ||
textColor: '#333', | ||
width: 300, | ||
type: 'png', | ||
} as ImagePlaceholderOptions, | ||
options, | ||
) | ||
options.prefix = `/${options.prefix}`.replace(/\/\//g, '/').replace(/\/$/, '') | ||
|
||
if (!server) { | ||
options.prefix = options.prefix.slice(1) | ||
} | ||
|
||
return options as Required<ImagePlaceholderOptions> | ||
} | ||
|
||
export function imagePlaceholderPlugin( | ||
function placeholderServerPlugin( | ||
options: ImagePlaceholderOptions = {}, | ||
): Plugin { | ||
const opts = parseOptions(options) | ||
const opts = parseOptions(options, true) | ||
const pathRules = generatePathRules(opts.prefix) | ||
|
||
return { | ||
name: 'vite-plugin-image-placeholder', | ||
name: 'vite-plugin-image-placeholder-server', | ||
apply: 'serve', | ||
async configureServer({ middlewares }) { | ||
middlewares.use(imagePlaceholderMiddlewares(opts)) | ||
middlewares.use(async function (req, res, next) { | ||
const url = req.url! | ||
if (!url.startsWith(opts.prefix)) return next() | ||
logger(url) | ||
|
||
try { | ||
const image = await pathToImage(url, pathRules, opts) | ||
|
||
if (!image) return next() | ||
|
||
res.setHeader('Accept-Ranges', 'bytes') | ||
res.setHeader('Content-Type', getMimeType(image.type)) | ||
res.end(image.buffer) | ||
return | ||
} catch (e) { | ||
console.error(e) | ||
} | ||
next() | ||
}) | ||
}, | ||
} | ||
} | ||
|
||
function placeholderInjectPlugin( | ||
options: ImagePlaceholderOptions = {}, | ||
): Plugin { | ||
const opts = parseOptions(options) | ||
const moduleId = `virtual:${opts.prefix}` | ||
const resolveVirtualModuleId = `\0${moduleId}` | ||
return { | ||
name: 'vite-plugin-image-placeholder-inject', | ||
resolveId(id) { | ||
if (id.startsWith(moduleId)) { | ||
return `\0${id}` | ||
} | ||
}, | ||
async load(id) { | ||
if (id.startsWith(resolveVirtualModuleId)) { | ||
// const url = id.replace(resolveVirtualModuleId, '') | ||
} | ||
}, | ||
} | ||
} | ||
|
||
export function imagePlaceholderPlugin( | ||
options: ImagePlaceholderOptions = {}, | ||
): Plugin[] { | ||
return [placeholderServerPlugin(options), placeholderInjectPlugin(options)] | ||
} |
Oops, something went wrong.