mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
refactor: remove blurhash replace to thumbhash
- Added Thumbhash component to replace Blurhash in PhotoViewer and PhotoMasonryItem for enhanced image loading performance. - Updated the manifest version from 'v4' to 'v5' across multiple files to reflect the new data structure. - Introduced thumbHash generation in the image processing pipeline, ensuring compatibility with existing thumbnail logic. - Updated package dependencies to include thumbhash version 0.1.1. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -69,6 +69,7 @@
|
||||
"swiper": "11.2.8",
|
||||
"swr": "2.3.3",
|
||||
"tailwind-merge": "3.3.1",
|
||||
"thumbhash": "0.1.1",
|
||||
"tiff": "^7.0.0",
|
||||
"usehooks-ts": "3.1.1",
|
||||
"vaul": "1.1.2",
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { Blurhash } from 'react-blurhash'
|
||||
import { ErrorBoundary } from 'react-error-boundary'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Swiper as SwiperType } from 'swiper'
|
||||
@@ -26,6 +25,7 @@ import { useMobile } from '~/hooks/useMobile'
|
||||
import { Spring } from '~/lib/spring'
|
||||
import type { PhotoManifest } from '~/types/photo'
|
||||
|
||||
import { Thumbhash } from '../thumbhash'
|
||||
import { ExifPanel } from './ExifPanel'
|
||||
import { GalleryThumbnail } from './GalleryThumbnail'
|
||||
import { ProgressiveImage } from './ProgressiveImage'
|
||||
@@ -147,7 +147,7 @@ export const PhotoViewer = ({
|
||||
{/* 固定背景层防止透出 */}
|
||||
{/* 交叉溶解的 Blurhash 背景 */}
|
||||
<AnimatePresence mode="popLayout">
|
||||
{isOpen && currentPhoto.blurhash && (
|
||||
{isOpen && currentPhoto.thumbHash && (
|
||||
<ErrorBoundary fallback={null}>
|
||||
<PassiveFragment>
|
||||
<m.div
|
||||
@@ -158,20 +158,15 @@ export const PhotoViewer = ({
|
||||
className="bg-material-opaque fixed inset-0"
|
||||
/>
|
||||
<m.div
|
||||
key={currentPhoto.blurhash}
|
||||
key={currentPhoto.id}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={Spring.presets.smooth}
|
||||
className="fixed inset-0"
|
||||
>
|
||||
<Blurhash
|
||||
hash={currentPhoto.blurhash}
|
||||
width="100%"
|
||||
height="100%"
|
||||
resolutionX={32}
|
||||
resolutionY={32}
|
||||
punch={1}
|
||||
<Thumbhash
|
||||
thumbHash={currentPhoto.thumbHash}
|
||||
className="size-fill"
|
||||
/>
|
||||
</m.div>
|
||||
|
||||
22
apps/web/src/components/ui/thumbhash/index.tsx
Normal file
22
apps/web/src/components/ui/thumbhash/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { decompressUint8Array } from '@afilmory/builder'
|
||||
import { useMemo } from 'react'
|
||||
import { thumbHashToDataURL } from 'thumbhash'
|
||||
|
||||
import { clsxm } from '~/lib/cn'
|
||||
|
||||
export const Thumbhash = ({
|
||||
thumbHash,
|
||||
className,
|
||||
}: {
|
||||
thumbHash: ArrayLike<number> | string
|
||||
className?: string
|
||||
}) => {
|
||||
const dataURL = useMemo(() => {
|
||||
if (typeof thumbHash === 'string') {
|
||||
return thumbHashToDataURL(decompressUint8Array(thumbHash))
|
||||
}
|
||||
return thumbHashToDataURL(thumbHash)
|
||||
}, [thumbHash])
|
||||
|
||||
return <img src={dataURL} className={clsxm('h-full w-full', className)} />
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import clsx from 'clsx'
|
||||
import { m } from 'motion/react'
|
||||
import { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Blurhash } from 'react-blurhash'
|
||||
import { ErrorBoundary } from 'react-error-boundary'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Thumbhash } from '~/components/ui/thumbhash'
|
||||
import {
|
||||
CarbonIsoOutline,
|
||||
MaterialSymbolsShutterSpeed,
|
||||
@@ -229,18 +228,8 @@ export const PhotoMasonryItem = ({
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{/* Blurhash 占位符 */}
|
||||
{data.blurhash && (
|
||||
<ErrorBoundary fallback={null}>
|
||||
<Blurhash
|
||||
hash={data.blurhash}
|
||||
width="100%"
|
||||
height="100%"
|
||||
resolutionX={32}
|
||||
resolutionY={32}
|
||||
punch={1}
|
||||
className="absolute inset-0"
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
{data.thumbHash && (
|
||||
<Thumbhash thumbHash={data.thumbHash} className="absolute inset-0" />
|
||||
)}
|
||||
|
||||
{!imageError && (
|
||||
|
||||
@@ -192,7 +192,7 @@ export const Component = () => {
|
||||
|
||||
const photos = photoLoader.getPhotos()
|
||||
const manifestData = {
|
||||
version: 'v4',
|
||||
version: 'v5',
|
||||
data: photos,
|
||||
}
|
||||
|
||||
@@ -355,7 +355,7 @@ export const Component = () => {
|
||||
<JsonHighlight
|
||||
data={
|
||||
searchTerm
|
||||
? { version: 'v4', data: filteredPhotos }
|
||||
? { version: 'v5', data: filteredPhotos }
|
||||
: manifestData
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,43 +1,37 @@
|
||||
import { photoLoader } from '@afilmory/data'
|
||||
import { Blurhash } from 'react-blurhash'
|
||||
import { ErrorBoundary } from 'react-error-boundary'
|
||||
|
||||
import { ScrollArea } from '~/components/ui/scroll-areas/ScrollArea'
|
||||
import { Thumbhash } from '~/components/ui/thumbhash'
|
||||
|
||||
export const Component = () => {
|
||||
const photos = photoLoader.getPhotos()
|
||||
|
||||
return (
|
||||
<div className="columns-4 gap-4">
|
||||
{photos.map((photo) => (
|
||||
<div
|
||||
key={photo.id}
|
||||
className="group relative"
|
||||
style={{
|
||||
paddingBottom: `${(photo.height / photo.width) * 100}%`,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={photo.thumbnailUrl}
|
||||
alt={photo.title}
|
||||
height={photo.height}
|
||||
width={photo.width}
|
||||
className="absolute inset-0"
|
||||
/>
|
||||
{photo.blurhash && (
|
||||
<ErrorBoundary fallback={null}>
|
||||
<ScrollArea rootClassName="h-screen">
|
||||
<div className="columns-4 gap-0">
|
||||
{photos.map((photo) => (
|
||||
<div
|
||||
key={photo.id}
|
||||
className="group relative m-2"
|
||||
style={{
|
||||
paddingBottom: `${(photo.height / photo.width) * 100}%`,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={photo.thumbnailUrl}
|
||||
alt={photo.title}
|
||||
height={photo.height}
|
||||
width={photo.width}
|
||||
className="absolute inset-0"
|
||||
/>
|
||||
{photo.thumbHash && (
|
||||
<div className="absolute inset-0 opacity-0 group-hover:opacity-100">
|
||||
<Blurhash
|
||||
hash={photo.blurhash}
|
||||
width="100%"
|
||||
height="100%"
|
||||
resolutionX={32}
|
||||
resolutionY={32}
|
||||
punch={1}
|
||||
/>
|
||||
<Thumbhash thumbHash={photo.thumbHash} />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"exiftool-vendored": "30.2.0",
|
||||
"heic-convert": "2.1.0",
|
||||
"heic-to": "1.1.14",
|
||||
"sharp": "0.34.2"
|
||||
"sharp": "0.34.2",
|
||||
"thumbhash": "0.1.1"
|
||||
}
|
||||
}
|
||||
@@ -312,7 +312,7 @@ class PhotoGalleryBuilder {
|
||||
): Promise<AfilmoryManifest> {
|
||||
return options.isForceMode || options.isForceManifest
|
||||
? {
|
||||
version: 'v4',
|
||||
version: 'v5',
|
||||
data: [],
|
||||
}
|
||||
: await loadExistingManifest()
|
||||
|
||||
@@ -1,47 +1,25 @@
|
||||
import { encode, isBlurhashValid } from 'blurhash'
|
||||
import sharp from 'sharp'
|
||||
import { rgbaToThumbHash } from 'thumbhash'
|
||||
|
||||
import { logger } from '../logger/index.js'
|
||||
|
||||
// 生成 blurhash(基于缩略图数据,保持长宽比)
|
||||
export async function generateBlurhash(
|
||||
thumbnailBuffer: Buffer,
|
||||
originalWidth: number,
|
||||
originalHeight: number,
|
||||
): Promise<string | null> {
|
||||
): Promise<Uint8Array | null> {
|
||||
try {
|
||||
// 复用缩略图的 Sharp 实例来生成 blurhash
|
||||
// 确保转换为 raw RGBA 格式
|
||||
const { data, info } = await sharp(thumbnailBuffer)
|
||||
.resize(100, 100, { fit: 'inside' })
|
||||
.raw()
|
||||
.ensureAlpha()
|
||||
.toBuffer({
|
||||
resolveWithObject: true,
|
||||
})
|
||||
|
||||
const xComponents = 4
|
||||
const yComponents = 4
|
||||
|
||||
logger.blurhash.info(
|
||||
`生成参数:原始 ${originalWidth}x${originalHeight}, 实际 ${info.width}x${info.height}, 组件 ${xComponents}x${yComponents}`,
|
||||
)
|
||||
|
||||
// 生成 blurhash
|
||||
const blurhash = encode(
|
||||
new Uint8ClampedArray(data),
|
||||
info.width,
|
||||
info.height,
|
||||
xComponents,
|
||||
yComponents,
|
||||
)
|
||||
|
||||
if (!isBlurhashValid(blurhash)) {
|
||||
logger.blurhash.error('生成失败:blurhash 无效', blurhash)
|
||||
return null
|
||||
}
|
||||
|
||||
logger.blurhash.success(`生成成功:${blurhash}`)
|
||||
return blurhash
|
||||
const thumbHash = rgbaToThumbHash(info.width, info.height, data)
|
||||
return thumbHash
|
||||
} catch (error) {
|
||||
logger.blurhash.error('生成失败:', error)
|
||||
return null
|
||||
|
||||
@@ -27,7 +27,7 @@ function createFailureResult(): ThumbnailResult {
|
||||
return {
|
||||
thumbnailUrl: null,
|
||||
thumbnailBuffer: null,
|
||||
blurhash: null,
|
||||
thumbHash: null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,12 +35,12 @@ function createFailureResult(): ThumbnailResult {
|
||||
function createSuccessResult(
|
||||
thumbnailUrl: string,
|
||||
thumbnailBuffer: Buffer,
|
||||
blurhash: string | null,
|
||||
thumbHash: Uint8Array | null,
|
||||
): ThumbnailResult {
|
||||
return {
|
||||
thumbnailUrl,
|
||||
thumbnailBuffer,
|
||||
blurhash,
|
||||
thumbHash,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +63,6 @@ export async function thumbnailExists(photoId: string): Promise<boolean> {
|
||||
// 读取现有缩略图并生成 blurhash
|
||||
async function processExistingThumbnail(
|
||||
photoId: string,
|
||||
originalWidth: number,
|
||||
originalHeight: number,
|
||||
): Promise<ThumbnailResult | null> {
|
||||
const { thumbnailPath, thumbnailUrl } = getThumbnailPaths(photoId)
|
||||
|
||||
@@ -73,13 +71,9 @@ async function processExistingThumbnail(
|
||||
|
||||
try {
|
||||
const existingBuffer = await fs.readFile(thumbnailPath)
|
||||
const blurhash = await generateBlurhash(
|
||||
existingBuffer,
|
||||
originalWidth,
|
||||
originalHeight,
|
||||
)
|
||||
const thumbHash = await generateBlurhash(existingBuffer)
|
||||
|
||||
return createSuccessResult(thumbnailUrl, existingBuffer, blurhash)
|
||||
return createSuccessResult(thumbnailUrl, existingBuffer, thumbHash)
|
||||
} catch (error) {
|
||||
thumbnailLog?.warn(`读取现有缩略图失败,重新生成:${photoId}`, error)
|
||||
return null
|
||||
@@ -90,8 +84,6 @@ async function processExistingThumbnail(
|
||||
async function generateNewThumbnail(
|
||||
imageBuffer: Buffer,
|
||||
photoId: string,
|
||||
originalWidth: number,
|
||||
originalHeight: number,
|
||||
): Promise<ThumbnailResult> {
|
||||
const { thumbnailPath, thumbnailUrl } = getThumbnailPaths(photoId)
|
||||
|
||||
@@ -123,13 +115,9 @@ async function generateNewThumbnail(
|
||||
log.success(`生成完成:${photoId} (${sizeKB}KB, ${duration}ms)`)
|
||||
|
||||
// 基于生成的缩略图生成 blurhash
|
||||
const blurhash = await generateBlurhash(
|
||||
thumbnailBuffer,
|
||||
originalWidth,
|
||||
originalHeight,
|
||||
)
|
||||
const thumbHash = await generateBlurhash(thumbnailBuffer)
|
||||
|
||||
return createSuccessResult(thumbnailUrl, thumbnailBuffer, blurhash)
|
||||
return createSuccessResult(thumbnailUrl, thumbnailBuffer, thumbHash)
|
||||
} catch (error) {
|
||||
log.error(`生成失败:${photoId}`, error)
|
||||
return createFailureResult()
|
||||
@@ -140,8 +128,6 @@ async function generateNewThumbnail(
|
||||
export async function generateThumbnailAndBlurhash(
|
||||
imageBuffer: Buffer,
|
||||
photoId: string,
|
||||
originalWidth: number,
|
||||
originalHeight: number,
|
||||
forceRegenerate = false,
|
||||
): Promise<ThumbnailResult> {
|
||||
const thumbnailLog = getGlobalLoggers().thumbnail
|
||||
@@ -151,11 +137,7 @@ export async function generateThumbnailAndBlurhash(
|
||||
|
||||
// 如果不是强制模式且缩略图已存在,尝试复用现有文件
|
||||
if (!forceRegenerate && (await thumbnailExists(photoId))) {
|
||||
const existingResult = await processExistingThumbnail(
|
||||
photoId,
|
||||
originalWidth,
|
||||
originalHeight,
|
||||
)
|
||||
const existingResult = await processExistingThumbnail(photoId)
|
||||
|
||||
if (existingResult) {
|
||||
return existingResult
|
||||
@@ -164,12 +146,7 @@ export async function generateThumbnailAndBlurhash(
|
||||
}
|
||||
|
||||
// 生成新的缩略图
|
||||
return await generateNewThumbnail(
|
||||
imageBuffer,
|
||||
photoId,
|
||||
originalWidth,
|
||||
originalHeight,
|
||||
)
|
||||
return await generateNewThumbnail(imageBuffer, photoId)
|
||||
} catch (error) {
|
||||
thumbnailLog.error(`处理失败:${photoId}`, error)
|
||||
return createFailureResult()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './lib/u8array.js'
|
||||
export type { StorageConfig } from './storage/interfaces.js'
|
||||
export type {
|
||||
FujiRecipe,
|
||||
PhotoManifestItem,
|
||||
|
||||
11
packages/builder/src/lib/u8array.ts
Normal file
11
packages/builder/src/lib/u8array.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const compressUint8Array = (uint8Array: Uint8Array) => {
|
||||
return Array.from(uint8Array)
|
||||
.map((byte) => byte.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
}
|
||||
|
||||
export const decompressUint8Array = (compressed: string) => {
|
||||
return Uint8Array.from(
|
||||
compressed.match(/.{1,2}/g)!.map((byte) => Number.parseInt(byte, 16)),
|
||||
)
|
||||
}
|
||||
@@ -20,15 +20,15 @@ export async function loadExistingManifest(): Promise<AfilmoryManifest> {
|
||||
'🔍 未找到 manifest 文件/解析失败,创建新的 manifest 文件...',
|
||||
)
|
||||
return {
|
||||
version: 'v4',
|
||||
version: 'v5',
|
||||
data: [],
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.version !== 'v4') {
|
||||
if (manifest.version !== 'v5') {
|
||||
logger.fs.error('🔍 无效的 manifest 版本,创建新的 manifest 文件...')
|
||||
return {
|
||||
version: 'v4',
|
||||
version: 'v5',
|
||||
data: [],
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ export async function saveManifest(items: PhotoManifestItem[]): Promise<void> {
|
||||
manifestPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
version: 'v4',
|
||||
version: 'v5',
|
||||
data: sortedManifest,
|
||||
} as AfilmoryManifest,
|
||||
null,
|
||||
|
||||
@@ -82,7 +82,7 @@ export function validateCacheData(
|
||||
needsThumbnail:
|
||||
options.isForceMode ||
|
||||
options.isForceThumbnails ||
|
||||
!existingItem.blurhash,
|
||||
!existingItem.thumbHash,
|
||||
needsExif:
|
||||
options.isForceMode || options.isForceManifest || !existingItem.exif,
|
||||
needsToneAnalysis:
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
generateThumbnailAndBlurhash,
|
||||
thumbnailExists,
|
||||
} from '../image/thumbnail.js'
|
||||
import { decompressUint8Array } from '../lib/u8array.js'
|
||||
import { workdir } from '../path.js'
|
||||
import type {
|
||||
PhotoManifestItem,
|
||||
@@ -22,7 +23,7 @@ import type { PhotoProcessorOptions } from './processor.js'
|
||||
export interface ThumbnailResult {
|
||||
thumbnailUrl: string
|
||||
thumbnailBuffer: Buffer
|
||||
blurhash: string
|
||||
thumbHash: Uint8Array | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,8 +33,6 @@ export interface ThumbnailResult {
|
||||
export async function processThumbnailAndBlurhash(
|
||||
imageBuffer: Buffer,
|
||||
photoId: string,
|
||||
width: number,
|
||||
height: number,
|
||||
existingItem: PhotoManifestItem | undefined,
|
||||
options: PhotoProcessorOptions,
|
||||
): Promise<ThumbnailResult> {
|
||||
@@ -43,7 +42,7 @@ export async function processThumbnailAndBlurhash(
|
||||
if (
|
||||
!options.isForceMode &&
|
||||
!options.isForceThumbnails &&
|
||||
existingItem?.blurhash &&
|
||||
existingItem?.thumbHash &&
|
||||
(await thumbnailExists(photoId))
|
||||
) {
|
||||
try {
|
||||
@@ -61,7 +60,7 @@ export async function processThumbnailAndBlurhash(
|
||||
return {
|
||||
thumbnailUrl,
|
||||
thumbnailBuffer,
|
||||
blurhash: existingItem.blurhash,
|
||||
thumbHash: decompressUint8Array(existingItem.thumbHash),
|
||||
}
|
||||
} catch (error) {
|
||||
loggers.thumbnail.warn(`读取现有缩略图失败,重新生成:${photoId}`, error)
|
||||
@@ -73,15 +72,13 @@ export async function processThumbnailAndBlurhash(
|
||||
const result = await generateThumbnailAndBlurhash(
|
||||
imageBuffer,
|
||||
photoId,
|
||||
width,
|
||||
height,
|
||||
options.isForceMode || options.isForceThumbnails,
|
||||
)
|
||||
|
||||
return {
|
||||
thumbnailUrl: result.thumbnailUrl!,
|
||||
thumbnailBuffer: result.thumbnailBuffer!,
|
||||
blurhash: result.blurhash!,
|
||||
thumbHash: result.thumbHash!,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isBitmap,
|
||||
preprocessImageBuffer,
|
||||
} from '../image/processor.js'
|
||||
import { compressUint8Array } from '../lib/u8array.js'
|
||||
import type { PhotoManifestItem } from '../types/photo.js'
|
||||
import { shouldProcessPhoto } from './cache-manager.js'
|
||||
import {
|
||||
@@ -154,8 +155,6 @@ export async function executePhotoProcessingPipeline(
|
||||
const thumbnailResult = await processThumbnailAndBlurhash(
|
||||
imageBuffer,
|
||||
photoId,
|
||||
metadata.width,
|
||||
metadata.height,
|
||||
existingItem,
|
||||
options,
|
||||
)
|
||||
@@ -196,7 +195,9 @@ export async function executePhotoProcessingPipeline(
|
||||
.getStorageManager()
|
||||
.generatePublicUrl(photoKey),
|
||||
thumbnailUrl: thumbnailResult.thumbnailUrl,
|
||||
blurhash: thumbnailResult.blurhash,
|
||||
thumbHash: thumbnailResult.thumbHash
|
||||
? compressUint8Array(thumbnailResult.thumbHash)
|
||||
: null,
|
||||
width: metadata.width,
|
||||
height: metadata.height,
|
||||
aspectRatio,
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// 重新导出适配器中的函数以保持 API 兼容性
|
||||
// 推荐使用新的 StorageManager API,这些函数将在未来版本中被弃用
|
||||
|
||||
export {
|
||||
detectLivePhotos,
|
||||
generateS3Url,
|
||||
getImageFromS3,
|
||||
listAllFilesFromS3,
|
||||
listImagesFromS3,
|
||||
} from '../storage/adapters.js'
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PhotoManifestItem } from './photo'
|
||||
|
||||
export type AfilmoryManifest = {
|
||||
version: 'v4'
|
||||
version: 'v5'
|
||||
data: PhotoManifestItem[]
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export interface PhotoManifestItem extends PhotoInfo {
|
||||
id: string
|
||||
originalUrl: string
|
||||
thumbnailUrl: string
|
||||
blurhash: string | null
|
||||
thumbHash: string | null
|
||||
width: number
|
||||
height: number
|
||||
aspectRatio: number
|
||||
@@ -151,7 +151,7 @@ export interface PickedExif {
|
||||
export interface ThumbnailResult {
|
||||
thumbnailUrl: string | null
|
||||
thumbnailBuffer: Buffer | null
|
||||
blurhash: string | null
|
||||
thumbHash: Uint8Array | null
|
||||
}
|
||||
|
||||
export type FujiRecipe = {
|
||||
|
||||
13
pnpm-lock.yaml
generated
13
pnpm-lock.yaml
generated
@@ -323,6 +323,9 @@ importers:
|
||||
tailwind-merge:
|
||||
specifier: 3.3.1
|
||||
version: 3.3.1
|
||||
thumbhash:
|
||||
specifier: 0.1.1
|
||||
version: 0.1.1
|
||||
tiff:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
@@ -456,6 +459,9 @@ importers:
|
||||
sharp:
|
||||
specifier: 0.34.2
|
||||
version: 0.34.2
|
||||
thumbhash:
|
||||
specifier: 0.1.1
|
||||
version: 0.1.1
|
||||
|
||||
packages/data:
|
||||
dependencies:
|
||||
@@ -1963,7 +1969,7 @@ packages:
|
||||
resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: 18.3.1
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
@@ -7355,6 +7361,9 @@ packages:
|
||||
resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
|
||||
engines: {node: '>=12.22'}
|
||||
|
||||
thumbhash@0.1.1:
|
||||
resolution: {integrity: sha512-kH5pKeIIBPQXAOni2AiY/Cu/NKdkFREdpH+TLdM0g6WA7RriCv0kPLgP731ady67MhTAqrVG/4mnEeibVuCJcg==}
|
||||
|
||||
tiff@7.0.0:
|
||||
resolution: {integrity: sha512-NhAYu+1cYzWpJ64bxaAC/b1fy6ZwkllMxYUziVZAISZa2TKu8H7AsC7Kpz4epZoMb0SNS4fOA6Ql1gZCZ27UXQ==}
|
||||
|
||||
@@ -16229,6 +16238,8 @@ snapshots:
|
||||
|
||||
throttle-debounce@5.0.2: {}
|
||||
|
||||
thumbhash@0.1.1: {}
|
||||
|
||||
tiff@7.0.0:
|
||||
dependencies:
|
||||
iobuffer: 6.0.0
|
||||
|
||||
Reference in New Issue
Block a user