<!-- Generated by SpecStory -->

# Browser Support for HEIC/HEIF Images (2025-05-26 13:45:45)

_**User**_

现在需要在浏览器中支持显示 heic/heif 的图片

---

_**Assistant**_

我需要帮你在浏览器中支持显示 HEIC/HEIF 图片。由于浏览器原生不支持这些格式，我们需要在前端进行转换。让我先查看一下当前的项目结构，然后实现解决方案。

---

<details>
            <summary>Listed current directory • **24** results</summary>
        
| Name |
|-------|
| 📁 `.git` |
| 📁 `public` |
| 📄 `package.json` |
| 📁 `node_modules` |
| 📄 `pnpm-lock.yaml` |
| 📁 `dist` |
| 📁 `src` |
| 📄 `.DS_Store` |
| 📄 `.gitignore` |
| 📁 `scripts` |
| 📁 `docs` |
| 📄 `index.html` |
| 📄 `README.md` |
| 📄 `vite.config.ts` |
| 📁 `config` |
| 📁 `plugins` |
| 📄 `env.ts` |
| 📄 `tsconfig.json` |
| 📄 `eslint.config.mjs` |
| 📁 `.vscode` |
| 📁 `.github` |
| 📄 `.prettierrc.mjs` |
| 📄 `renovate.json` |
| 📄 `vercel.json` |

</details>

---

Read file: package.json

---

<details>
            <summary>Listed directory src • **20** results</summary>
        
| Name |
|-------|
| 📄 `main.tsx` |
| 📄 `.DS_Store` |
| 📁 `lib` |
| 📄 `global.d.ts` |
| 📁 `hooks` |
| 📁 `components` |
| 📁 `icons` |
| 📁 `atoms` |
| 📁 `styles` |
| 📁 `modules` |
| 📄 `App.tsx` |
| 📁 `data` |
| 📁 `pages` |
| 📄 `router.tsx` |
| 📁 `types` |
| 📁 `providers` |
| 📁 `store` |
| 📁 `assets` |
| 📄 `framer-lazy-feature.ts` |
| 📄 `vite-env.d.ts` |

</details>

---

<details>
            <summary>Searched codebase "image photo display component" • **25** results</summary>
        
| File | Lines |
|------|-------|
| `src/modules/gallery/PhotoMasonryItem.tsx` | L88-140 |
| `src/modules/gallery/PhotoMasonryItem.tsx` | L1-53 |
| `src/components/photo-viewer/PhotoViewer.tsx` | L1-24 |
| `src/components/photo-viewer/ExifPanel.tsx` | L85-119 |
| `src/components/photo-viewer/PhotoViewer.tsx` | L24-101 |
| `src/components/photo-viewer/PhotoViewer.tsx` | L101-139 |
| `src/components/photo-viewer/ProgressiveImage.tsx` | L142-214 |
| `src/components/photo-viewer/ProgressiveImage.tsx` | L101-142 |
| `src/components/photo-viewer/ProgressiveImage.tsx` | L1-25 |
| `src/components/photo-viewer/ProgressiveImage.tsx` | L25-101 |
| `src/components/photo-viewer/PhotoViewer.tsx` | L231-268 |
| `src/modules/gallery/MasonryRoot.tsx` | L1-19 |
| `src/components/photo-viewer/ExifPanel.tsx` | L1-31 |
| `src/components/photo-viewer/GalleryThumbnail.tsx` | L32-73 |
| `src/modules/gallery/MasonryRoot.tsx` | L85-113 |
| `src/components/photo-viewer/PhotoViewer.tsx` | L197-231 |
| `src/modules/gallery/MasonryRoot.tsx` | L21-85 |
| `src/modules/gallery/PhotoMasonryItem.tsx` | L157-194 |
| `src/components/photo-viewer/ExifPanel.tsx` | L225-265 |
| `src/components/photo-viewer/ExifPanel.tsx` | L407-449 |
| `src/components/photo-viewer/ExifPanel.tsx` | L31-62 |
| `src/components/photo-viewer/ExifPanel.tsx` | L815-829 |
| `src/components/photo-viewer/ExifPanel.tsx` | L314-357 |
| `src/modules/gallery/PhotoMasonryItem.tsx` | L140-157 |
| `scripts/photo-loader.ts` | L1-21 |

</details>

---

现在我了解了项目结构。这是一个基于 React 的照片画廊应用，使用 Vite 构建。我看到项目已经有了完整的图片显示系统，包括缩略图和原图查看器。现在需要添加对 HEIC/HEIF 格式的浏览器支持。

由于浏览器原生不支持 HEIC/HEIF 格式，我们需要在前端进行转换。我将使用 `heic2any` 库来实现这个功能。

---

```bash
cd /Users/innei/git/vibe-coding/photo-gallery-site && pnpm add heic2any
```

```
be-coding/photo-gallery-site && pnpm add heic2any         cd /Users/innei/git/vibe-coding/photo-gallery-site    pnpm
 WARN  2 deprecated subdependencies found: glob@7.2.3, inflight@1.0.6
Packages: +1
+
Progress: resolved 887, reused 777, downloaded 1, added 1, done

dependencies:
+ heic2any 0.0.4

Done in 5s using pnpm v10.11.0

innei in photo-gallery-site on  main [$!+] via 🎁 v0.0.1 via  v22.15.0 on ☁️  innei@rss3.io at     13bf071 "Enha
nce PhotoViewer with Swiper integration and custom navigation"  by tukon479@gmail.com took 5s 
at 13:46:31 ❯
```

---

```bash
pnpm add -D @types/heic2any
```

```
at 13:46:31 ❯ pnpm add -D @types/heic2any
 ERR_PNPM_FETCH_404  GET https://registry.npmjs.org/@types%2Fheic2any: Not Found - 404

This error happened while installing a direct dependency of /Users/innei/git/vibe-coding/photo-gallery-site

@types/heic2any is not in the npm registry, or you have no permission to fetch it.

An authorization header was used: Bearer npm_[hidden]
Progress: resolved 68, reused 68, downloaded 0, added 0

innei in photo-gallery-site on  main [$!+] via 🎁 v0.0.1 via  v22.15.0 on ☁️  innei@rss3.io at     13bf071 "Enha
nce PhotoViewer with Swiper integration and custom navigation"  by tukon479@gmail.com 
at 13:46:36 ❯
```

---

没有类型定义包，我们需要自己创建类型声明。现在让我创建一个 HEIC 图片处理的工具函数和组件。

---

Cancelled

---

现在我需要为 heic2any 创建类型声明：

---

Cancelled

---

现在我需要创建一个支持 HEIC 格式的图片组件，它会自动检测并转换 HEIC 格式：

---

```diff
- 
+ import { useEffect, useRef, useState } from 'react'
+ 
+ import { clsxm } from '~/lib/cn'
+ import { heicCache, isHeicFormat } from '~/lib/heic-converter'
+ 
+ interface HeicImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
+   src: string
+   alt: string
+   fallbackSrc?: string
+   onHeicConversionStart?: () => void
+   onHeicConversionEnd?: () => void
+   onHeicConversionError?: (error: Error) => void
+   quality?: number
+ }
+ 
+ export const HeicImage = ({
+   src,
+   alt,
+   fallbackSrc,
+   onHeicConversionStart,
+   onHeicConversionEnd,
+   onHeicConversionError,
+   quality = 0.95,
+   className,
+   onLoad,
+   onError,
+   ...props
+ }: HeicImageProps) => {
+   const [displaySrc, setDisplaySrc] = useState<string>(src)
+   const [isConverting, setIsConverting] = useState(false)
+   const [conversionError, setConversionError] = useState<Error | null>(null)
+   const imgRef = useRef<HTMLImageElement>(null)
+   const currentSrcRef = useRef<string>(src)
+ 
+   useEffect(() => {
+     // 如果 src 改变了，重置状态
+     if (currentSrcRef.current !== src) {
+       currentSrcRef.current = src
+       setDisplaySrc(src)
+       setIsConverting(false)
+       setConversionError(null)
+     }
+ 
+     // 检查是否为 HEIC 格式
+     if (!isHeicFormat(src)) {
+       setDisplaySrc(src)
+       return
+     }
+ 
+     // 开始转换 HEIC
+     const convertHeic = async () => {
+       try {
+         setIsConverting(true)
+         setConversionError(null)
+         onHeicConversionStart?.()
+ 
+         const convertedUrl = await heicCache.getConvertedUrl(src, quality)
+         
+         // 确保这仍然是当前的 src（防止竞态条件）
+         if (currentSrcRef.current === src) {
+           setDisplaySrc(convertedUrl)
+           onHeicConversionEnd?.()
+         }
+       } catch (error) {
+         console.error('HEIC 转换失败:', error)
+         const err = error instanceof Error ? error : new Error('HEIC 转换失败')
+         setConversionError(err)
+         onHeicConversionError?.(err)
+         
+         // 如果有 fallback，使用 fallback
+         if (fallbackSrc && currentSrcRef.current === src) {
+           setDisplaySrc(fallbackSrc)
+         }
+       } finally {
+         if (currentSrcRef.current === src) {
+           setIsConverting(false)
+         }
+       }
+     }
+ 
+     convertHeic()
+   }, [src, quality, fallbackSrc, onHeicConversionStart, onHeicConversionEnd, onHeicConversionError])
+ 
+   const handleLoad = (event: React.SyntheticEvent<HTMLImageElement>) => {
+     onLoad?.(event)
+   }
+ 
+   const handleError = (event: React.SyntheticEvent<HTMLImageElement>) => {
+     // 如果是 HEIC 转换后的图片加载失败，且有 fallback，尝试使用 fallback
+     if (isHeicFormat(src) && fallbackSrc && displaySrc !== fallbackSrc) {
+       setDisplaySrc(fallbackSrc)
+       return
+     }
+     
+     onError?.(event)
+   }
+ 
+   return (
+     <img
+       ref={imgRef}
+       src={displaySrc}
+       alt={alt}
+       className={clsxm(
+         className,
+         isConverting && 'opacity-50', // 转换时显示半透明
+         conversionError && 'opacity-75', // 转换错误时显示半透明
+       )}
+       onLoad={handleLoad}
+       onError={handleError}
+       {...props}
+     />
+   )
+ } 
```

---

现在我需要修改现有的图片组件来使用新的 HeicImage 组件。首先修改 PhotoMasonryItem：

---



---



---

现在修改 ProgressiveImage 组件来支持 HEIC：

---



---



---

现在修改 GalleryThumbnail 组件：

---



---



---

现在我需要添加一个转换状态指示器，让用户知道 HEIC 图片正在转换。让我创建一个转换指示器组件：

---

Cancelled

---

现在更新 HeicImage 组件来使用转换指示器：

---



---



---



---



---

现在我需要修复一些问题。首先，HeicImage 组件需要正确处理 ref 转发：

---



---



---



---



---

我需要修复语法错误：

---



---

现在让我测试一下构建是否正常：

---

```bash
pnpm build
```

```
at 13:46:36 ❯ pnpm build

> photo-gallery-site@0.0.1 build /Users/innei/git/vibe-coding/photo-gallery-site
> vite build

📚 Loaded 29 photos from manifest
vite v6.3.5 building for production...
✅ Generated favicon: favicon-16x16.png (16x16)
✅ Generated favicon: favicon-32x32.png (32x32)
✅ Generated favicon: favicon-48x48.png (48x48)
✅ Generated favicon: apple-touch-icon.png (180x180)
✅ Generated favicon: android-chrome-192x192.png (192x192)
✅ Generated favicon: android-chrome-512x512.png (512x512)
✅ Generated main favicon: favicon.ico
✅ Generated web manifest: site.webmanifest
🎨 All favicons generated successfully!
📸 Found 4 latest photos
📷 Added photo: DSCF3154 at position (580, 220) with rotation -12°
📷 Added photo: DSCF2862 at position (670, 260) with rotation 5°
📷 Added photo: DSCF0324 at position (760, 190) with rotation -8°
📷 Added photo: DSCF0419 at position (850, 270) with rotation 10°
✅ OG image generated: /Users/innei/git/vibe-coding/photo-gallery-site/public/og-image-1748238567705.png
🖼️  OG image generated: /og-image-1748238567705.png
🗑️  Deleted old OG image: og-image-1748237383102.png
✅ Cleaned up 1 old OG images
transforming (29) package.json/*! 🌼 daisyUI 5.0.37 */
✓ 984 modules transformed.
dist/index.html                                  2.31 kB │ gzip:   0.77 kB
dist/assets/GeistVF-p2GUc8qZ.woff2              56.80 kB
dist/assets/index-tsZEpkDd.css                   8.34 kB │ gzip:   3.33 kB
dist/assets/index-CgMf0peO.css                 107.62 kB │ gzip:  17.44 kB
dist/assets/visual-element-DrbtW_V5.js          34.72 kB │ gzip:  12.81 kB
dist/assets/framer-lazy-feature-BPrNxg5P.js     64.60 kB │ gzip:  20.45 kB
dist/assets/index-3JZdVGNf.js                  421.20 kB │ gzip: 130.85 kB
dist/assets/index-D3GJza0N.js                1,582.45 kB │ gzip: 405.23 kB

(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#outpu
t-manualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
✓ built in 2.04s

innei in photo-gallery-site on  main [$!+?] via 🎁 v0.0.1 via  v22.15.0 on ☁️  innei@rss3.io at     13bf071 "Enh
ance PhotoViewer with Swiper integration and custom navigation"  by tukon479@gmail.com took 2s 
at 13:49:29 ❯
```

---



很好！构建成功了。现在让我启动开发服务器来测试 HEIC 支持：

---

```bash
pnpm dev
```

---

现在让我创建一个简单的测试页面来验证 HEIC 支持是否正常工作。首先，让我检查一下是否有 HEIC 格式的测试图片：

---

<details>
            <summary>Searched codebase "heic heif manifest photos" • **25** results</summary>
        
| File | Lines |
|------|-------|
| `src/data/photos-manifest.json` | L1-40 |
| `src/data/photos-manifest.json` | L768-801 |
| `scripts/photo-loader.ts` | L1-21 |
| `scripts/build-manifest.ts` | L26-65 |
| `src/data/photos-manifest.json` | L3273-3307 |
| `src/data/photos-manifest.json` | L2102-2135 |
| `src/data/photos-manifest.json` | L1680-1714 |
| `src/data/photos-manifest.json` | L2298-2331 |
| `src/data/photos-manifest.json` | L1420-1454 |
| `src/data/photos-manifest.json` | L474-507 |
| `src/data/photos-manifest.json` | L1160-1194 |
| `src/data/photos-manifest.json` | L572-605 |
| `src/data/photos-manifest.json` | L2494-2528 |
| `src/data/photos-manifest.json` | L670-703 |
| `src/data/photos-manifest.json` | L1062-1095 |
| `src/data/photos-manifest.json` | L2200-2233 |
| `src/data/photos-manifest.json` | L2396-2429 |
| `src/data/photos-manifest.json` | L866-899 |
| `src/data/photos-manifest.json` | L3112-3144 |
| `src/data/photos-manifest.json` | L376-409 |
| `src/data/photos-manifest.json` | L278-311 |
| `src/data/photos-manifest.json` | L964-997 |
| `src/data/photos-manifest.json` | L2004-2037 |
| `src/data/photos-manifest.json` | L2754-2788 |
| `src/data/photos-manifest.json` | L3210-3273 |

</details>

---

我看到项目已经支持 HEIC/HEIF 格式的处理（在 build-manifest.ts 中），但是当前的 manifest 中只有 JPG 格式的图片。让我创建一个简单的测试页面来验证 HEIC 支持功能。

首先，让我创建一个测试组件来演示 HEIC 支持：

---

Cancelled

---

现在让我添加路由来访问这个测试页面：

---

Read file: src/router.tsx

---

看起来路由是通过 glob 模式自动构建的。让我检查一下 pages 目录结构：

---

<details>
            <summary>Listed directory src/pages • **2** results</summary>
        
| Name |
|-------|
| 📄 `HeicTestPage.tsx` |
| 📁 `(main)` |

</details>

---

<details>
            <summary>Listed directory src/pages/(main) • **1** results</summary>
        
| Name |
|-------|
| 📄 `index.tsx` |

</details>

---

Read file: src/pages/(main)/index.tsx

---

现在我需要修改 HeicTestPage 来符合路由系统的要求：

---



---

让我重新读取文件内容：

---

Read file: src/pages/HeicTestPage.tsx

---

看起来文件内容没有正确保存。让我重新创建这个文件：

---

Cancelled

---

