Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion apps/docs/contents/index.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Overview
createdAt: 2025-07-20T22:35:03+08:00
lastModified: 2025-11-14T21:52:55+08:00
lastModified: 2025-11-21T21:41:13+08:00
---

# Overview
Expand Down Expand Up @@ -45,6 +45,7 @@ Live Photo Galleries:
- 🌈 **Blurhash Placeholders** - Elegant image loading experience
- 📱 **Live Photo Support** - Detection and display of iPhone Live Photos
- ☀️ **HDR Image Support** - Display HDR images
- 🌍 **Reverse Geocoding** - Automatic location extraction from GPS coordinates with support for Mapbox and Nominatim providers

### Advanced Features

Expand Down Expand Up @@ -127,6 +128,15 @@ This will automatically pull resources from the remote repository, avoiding rebu
- `digestSuffixLength`: Digest suffix length for deterministic IDs
- `supportedFormats`: Optional allowlist of file extensions to process

#### Geocoding (via `geocodingPlugin`)

- `enable`: Enable reverse geocoding from GPS coordinates (default: `false`)
- `provider`: Geocoding service provider (`'mapbox'` | `'nominatim'` | `'auto'`, default: `'auto'`)
- `mapboxToken`: Mapbox access token (required when using Mapbox provider)
- `nominatimBaseUrl`: Custom Nominatim base URL (optional, defaults to OpenStreetMap's public instance)
- `cachePrecision`: Coordinate cache precision (decimals, default: `4`)
- `language`: Preferred language(s) for results (BCP47, comma-separated or array). Omit to use provider default.

#### System Observability (`system.observability`)

- `showProgress`: Show build progress
Expand All @@ -138,6 +148,53 @@ This will automatically pull resources from the remote repository, avoiding rebu
- `performance.worker.timeout`: Worker timeout (milliseconds)
- `performance.worker.useClusterMode`: Enable cluster mode

#### Geocoding Configuration Example

Enable reverse geocoding by adding the geocoding plugin in `builder.config.ts`:

**Using Nominatim (Free, but rate-limited):**

```typescript
import { defineBuilderConfig, geocodingPlugin } from '@afilmory/builder'

export default defineBuilderConfig(() => ({
plugins: [
geocodingPlugin({
enable: true,
provider: 'nominatim',
// language: 'en,zh',
// Optional: use custom Nominatim instance
// nominatimBaseUrl: 'https://your-nominatim-instance.com'
}),
],
}))

```

**Using Mapbox:**

```typescript
import { defineBuilderConfig, geocodingPlugin } from '@afilmory/builder'

export default defineBuilderConfig(() => ({
plugins: [
geocodingPlugin({
enable: true,
provider: 'mapbox',
mapboxToken: process.env.MAPBOX_TOKEN,
// language: 'en-US',
}),
],
}))
```

**Notes:**
- Mapbox may provide higher accuracy and better rate limits but requires an API token
- Nominatim is free but has strict rate limits (1 request/second)
- Both providers support intelligent caching to minimize API calls
- Location data includes country, city, and location name extracted from GPS coordinates
- Reverse geocoding is handled by the built-in geocoding plugin and runs only when the plugin is added to `plugins` with `enable: true`

## 📋 CLI Commands

### Build Commands
Expand Down
21 changes: 21 additions & 0 deletions apps/web/src/components/ui/photo-viewer/ExifPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,27 @@ export const ExifPanel: FC<{
<Row label={t('exif.gps.altitude')} value={`${formattedExifData.gps.altitude}m`} />
)}

{/* 反向地理编码位置信息 */}
{currentPhoto.location && (
<div className="mt-3 space-y-1">
{(currentPhoto.location.city || currentPhoto.location.country) && (
<Row
label={t('exif.gps.city')}
value={[currentPhoto.location.city, currentPhoto.location.country]
.filter(Boolean)
.join(', ')}
/>
)}
{currentPhoto.location.locationName && (
<Row
label={t('exif.gps.address')}
value={currentPhoto.location.locationName}
ellipsis={true}
/>
)}
</div>
)}

{/* Maplibre MiniMap */}
{decimalLatitude !== null && decimalLongitude !== null && (
<div className="mt-3">
Expand Down
8 changes: 5 additions & 3 deletions locales/app/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@
"exif.fujirecipe-sharpness.soft": "Soft",
"exif.fujirecipe-whitebalance.auto": "Auto",
"exif.fujirecipe-whitebalance.kelvin": "{{kelvin}}K",
"exif.gps.address": "Address",
"exif.gps.altitude": "Altitude",
"exif.gps.city": "City",
"exif.gps.latitude": "Latitude",
"exif.gps.location.info": "Location Information",
"exif.gps.location.name": "Location Name",
Expand Down Expand Up @@ -358,7 +360,6 @@
"slider.columns": "{{count}} column",
"video.codec.keyword": "Encoder",
"video.conversion.cached.result": "Using cached result",
"video.motion-photo.extracting": "Extracting embedded video...",
"video.conversion.codec.fallback": "No MP4 codec found that supports this resolution. Falling back to WebM.",
"video.conversion.complete": "Conversion complete",
"video.conversion.converting": "Converting... {{current}}/{{total}} frames",
Expand All @@ -378,5 +379,6 @@
"video.conversion.webcodecs.high.quality": "Using high-quality WebCodecs converter...",
"video.conversion.webcodecs.not.supported": "WebCodecs is not supported in this browser",
"video.format.mov.not.supported": "Browser does not support MOV format, conversion required",
"video.format.mov.supported": "Browser natively supports MOV format, skipping conversion"
}
"video.format.mov.supported": "Browser natively supports MOV format, skipping conversion",
"video.motion-photo.extracting": "Extracting embedded video..."
}
8 changes: 5 additions & 3 deletions locales/app/jp.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@
"exif.fujirecipe-sharpness.soft": "軟調",
"exif.fujirecipe-whitebalance.auto": "自動",
"exif.fujirecipe-whitebalance.kelvin": "{{kelvin}}K",
"exif.gps.address": "住所",
"exif.gps.altitude": "高度",
"exif.gps.city": "都市",
"exif.gps.latitude": "緯度",
"exif.gps.location.info": "位置情報",
"exif.gps.location.name": "位置名",
Expand Down Expand Up @@ -351,7 +353,6 @@
"slider.columns": "{{count}} 列",
"video.codec.keyword": "エンコーダー",
"video.conversion.cached.result": "キャッシュされた結果を使用",
"video.motion-photo.extracting": "埋め込み動画を抽出しています...",
"video.conversion.codec.fallback": "この解像度でサポートされている MP4 コーデックが見つかりません。WebM にフォールバックします。",
"video.conversion.complete": "変換完了",
"video.conversion.converting": "変換中... {{current}}/{{total}}フレーム",
Expand All @@ -371,5 +372,6 @@
"video.conversion.webcodecs.high.quality": "高品質の WebCodecs コンバーターを使用しています...",
"video.conversion.webcodecs.not.supported": "このブラウザは WebCodecs をサポートしていません",
"video.format.mov.not.supported": "ブラウザが MOV 形式をサポートしていないため、変換が必要です",
"video.format.mov.supported": "ブラウザが MOV 形式をネイティブでサポートしているため、変換をスキップします"
}
"video.format.mov.supported": "ブラウザが MOV 形式をネイティブでサポートしているため、変換をスキップします",
"video.motion-photo.extracting": "埋め込み動画を抽出しています..."
}
8 changes: 5 additions & 3 deletions locales/app/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@
"exif.fujirecipe-sharpness.soft": "소프트",
"exif.fujirecipe-whitebalance.auto": "자동",
"exif.fujirecipe-whitebalance.kelvin": "{{kelvin}}K",
"exif.gps.address": "주소",
"exif.gps.altitude": "고도",
"exif.gps.city": "도시",
"exif.gps.latitude": "위도",
"exif.gps.location.info": "위치 정보",
"exif.gps.location.name": "위치 이름",
Expand Down Expand Up @@ -351,7 +353,6 @@
"slider.columns": "{{count}} 열",
"video.codec.keyword": "인코더",
"video.conversion.cached.result": "캐시된 결과 사용",
"video.motion-photo.extracting": "내장된 비디오 추출 중...",
"video.conversion.codec.fallback": "이 해상도에서 지원되는 MP4 코덱을 찾을 수 없습니다. WebM 으로 대체합니다.",
"video.conversion.complete": "변환 완료",
"video.conversion.converting": "변환 중... {{current}}/{{total}} 프레임",
Expand All @@ -371,5 +372,6 @@
"video.conversion.webcodecs.high.quality": "고품질 WebCodecs 변환기 사용 중...",
"video.conversion.webcodecs.not.supported": "이 브라우저는 WebCodecs 를 지원하지 않습니다",
"video.format.mov.not.supported": "브라우저가 MOV 형식을 지원하지 않아 변환이 필요합니다.",
"video.format.mov.supported": "브라우저가 MOV 형식을 기본적으로 지원하므로 변환을 건너뜁니다."
}
"video.format.mov.supported": "브라우저가 MOV 형식을 기본적으로 지원하므로 변환을 건너뜁니다.",
"video.motion-photo.extracting": "내장된 비디오 추출 중..."
}
8 changes: 5 additions & 3 deletions locales/app/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@
"exif.fujirecipe-sharpness.soft": "柔和",
"exif.fujirecipe-whitebalance.auto": "自动",
"exif.fujirecipe-whitebalance.kelvin": "{{kelvin}}K",
"exif.gps.address": "地址",
"exif.gps.altitude": "海拔",
"exif.gps.city": "城市",
"exif.gps.latitude": "纬度",
"exif.gps.location.info": "位置信息",
"exif.gps.location.name": "位置名称",
Expand Down Expand Up @@ -355,7 +357,6 @@
"slider.columns": "{{count}} 列",
"video.codec.keyword": "编码器",
"video.conversion.cached.result": "使用缓存结果",
"video.motion-photo.extracting": "正在提取嵌入的视频...",
"video.conversion.codec.fallback": "找不到此分辨率支持的 MP4 编解码器。回退到 WebM。",
"video.conversion.complete": "转换完成",
"video.conversion.converting": "转换中... {{current}}/{{total}} 帧",
Expand All @@ -375,5 +376,6 @@
"video.conversion.webcodecs.high.quality": "使用高质量 WebCodecs 转换器...",
"video.conversion.webcodecs.not.supported": "此浏览器不支持 WebCodecs",
"video.format.mov.not.supported": "浏览器不支持 MOV 格式,需要转换",
"video.format.mov.supported": "浏览器原生支持 MOV 格式,跳过转换"
}
"video.format.mov.supported": "浏览器原生支持 MOV 格式,跳过转换",
"video.motion-photo.extracting": "正在提取嵌入的视频..."
}
8 changes: 5 additions & 3 deletions locales/app/zh-HK.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@
"exif.fujirecipe-sharpness.soft": "柔和",
"exif.fujirecipe-whitebalance.auto": "自動",
"exif.fujirecipe-whitebalance.kelvin": "{{kelvin}}K",
"exif.gps.address": "地址",
"exif.gps.altitude": "海拔",
"exif.gps.city": "城市",
"exif.gps.latitude": "緯度",
"exif.gps.location.info": "位置信息",
"exif.gps.location.name": "位置名稱",
Expand Down Expand Up @@ -351,7 +353,6 @@
"slider.columns": "{{count}} 列",
"video.codec.keyword": "編碼器",
"video.conversion.cached.result": "使用快取結果",
"video.motion-photo.extracting": "正在提取嵌入的影片...",
"video.conversion.codec.fallback": "找不到此解析度支援的 MP4 編解碼器。回退到 WebM。",
"video.conversion.complete": "轉換完成",
"video.conversion.converting": "轉換中... {{current}}/{{total}} 幀",
Expand All @@ -371,5 +372,6 @@
"video.conversion.webcodecs.high.quality": "使用高品質 WebCodecs 轉換器...",
"video.conversion.webcodecs.not.supported": "此瀏覽器不支援 WebCodecs",
"video.format.mov.not.supported": "瀏覽器不支援 MOV 格式,需要轉換",
"video.format.mov.supported": "瀏覽器原生支援 MOV 格式,跳過轉換"
}
"video.format.mov.supported": "瀏覽器原生支援 MOV 格式,跳過轉換",
"video.motion-photo.extracting": "正在提取嵌入的影片..."
}
8 changes: 5 additions & 3 deletions locales/app/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@
"exif.fujirecipe-sharpness.soft": "柔和",
"exif.fujirecipe-whitebalance.auto": "自動",
"exif.fujirecipe-whitebalance.kelvin": "{{kelvin}}K",
"exif.gps.address": "地址",
"exif.gps.altitude": "海拔",
"exif.gps.city": "城市",
"exif.gps.latitude": "緯度",
"exif.gps.location.info": "位置信息",
"exif.gps.location.name": "位置名稱",
Expand Down Expand Up @@ -350,7 +352,6 @@
"slider.columns": "{{count}} 列",
"video.codec.keyword": "編碼器",
"video.conversion.cached.result": "使用快取結果",
"video.motion-photo.extracting": "正在提取嵌入的影片...",
"video.conversion.codec.fallback": "找不到此解析度支援的 MP4 編解碼器。回退到 WebM。",
"video.conversion.complete": "轉換完成",
"video.conversion.converting": "轉換中... {{current}}/{{total}} 幀",
Expand All @@ -370,5 +371,6 @@
"video.conversion.webcodecs.high.quality": "使用高品質 WebCodecs 轉換器...",
"video.conversion.webcodecs.not.supported": "此瀏覽器不支援 WebCodecs",
"video.format.mov.not.supported": "瀏覽器不支援 MOV 格式,需要轉換",
"video.format.mov.supported": "瀏覽器原生支援 MOV 格式,跳過轉換"
}
"video.format.mov.supported": "瀏覽器原生支援 MOV 格式,跳過轉換",
"video.motion-photo.extracting": "正在提取嵌入的影片..."
}
11 changes: 10 additions & 1 deletion packages/builder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ src/core/
│ └── exif.ts # EXIF 数据提取
├── photo/ # 照片处理
│ ├── info-extractor.ts # 照片信息提取
│ └── processor.ts # 照片处理主逻辑
│ ├── processor.ts # 照片处理主逻辑
│ └── geocoding.ts # 反向地理编码
├── manifest/ # Manifest 管理
│ └── manager.ts # Manifest 读写和管理
├── worker/ # 并发处理
Expand Down Expand Up @@ -62,6 +63,7 @@ src/core/

- **info-extractor.ts**: 从文件名和 EXIF 提取照片信息
- **processor.ts**: 照片处理主流程,整合所有处理步骤
- **geocoding.ts**: 反向地理编码,支持 Mapbox 和 Nominatim 提供者

### 6. Manifest 管理 (`manifest/`)

Expand Down Expand Up @@ -136,6 +138,13 @@ const exif = await extractExifData(buffer)
- 可配置的并发数
- 环境变量配置

### 6. 地理编码支持

- 从 GPS 坐标提取位置信息
- 支持 Mapbox 和 Nominatim 两种提供者
- 智能缓存和速率限制
- 自动重试和并发安全

## 扩展指南

### 添加新的图像处理功能
Expand Down
2 changes: 2 additions & 0 deletions packages/builder/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export {
processPhotoWithPipeline,
} from './photo/image-pipeline.js'
export type { PhotoProcessorOptions } from './photo/processor.js'
export type { GeocodingPluginOptions } from './plugins/geocoding.js'
export { default as geocodingPlugin } from './plugins/geocoding.js'
export type { GitHubRepoSyncPluginOptions } from './plugins/github-repo-sync.js'
export { createGitHubRepoSyncPlugin, default as githubRepoSyncPlugin } from './plugins/github-repo-sync.js'
export type { B2StoragePluginOptions } from './plugins/storage/b2.js'
Expand Down
30 changes: 26 additions & 4 deletions packages/builder/src/photo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- **`live-photo-handler.ts`** - Live Photo 检测和处理
- **`logger-adapter.ts`** - Logger 适配器,实现适配器模式
- **`info-extractor.ts`** - 照片信息提取
- **`geocoding.ts`** - 反向地理编码提供者定义(通过 geocoding 插件调用)

### 设计模式

Expand Down Expand Up @@ -42,9 +43,9 @@ class CompatibleLoggerAdapter implements PhotoLogger {
2. 创建 Sharp 实例
3. 处理缩略图和 blurhash
4. 处理 EXIF 数据
5. 处理影调分析
6. 提取照片信息
7. 处理 Live Photo
5. HDR / Motion Photo / Live Photo 检测
6. 处理影调分析
7. 提取照片信息
8. 构建照片清单项

### 主要改进
Expand All @@ -53,7 +54,8 @@ class CompatibleLoggerAdapter implements PhotoLogger {
2. **Logger 适配器**: 使用异步执行上下文管理 logger,避免全局状态污染
3. **缓存管理**: 统一管理各种数据的缓存和复用逻辑
4. **Live Photo 处理**: 专门的模块处理 Live Photo 检测和匹配
5. **类型安全**: 完善的 TypeScript 类型定义
5. **反向地理编码插件**: 通过 geocoding 插件在构建生命周期中写入位置信息,支持多个地理编码提供商
6. **类型安全**: 完善的 TypeScript 类型定义

### 使用方法

Expand Down Expand Up @@ -92,6 +94,26 @@ const thumbnailResult = await processThumbnailAndBlurhash(imageBuffer, photoId,
const exifData = await processExifData(imageBuffer, rawImageBuffer, photoKey, existingItem, options)
```

#### 启用反向地理编码

在 `builder.config.ts` 中通过插件开启:

```typescript
import { defineBuilderConfig, geocodingPlugin } from '@afilmory/builder'

export default defineBuilderConfig(() => ({
plugins: [
geocodingPlugin({
enable: true,
provider: 'auto',
mapboxToken: process.env.MAPBOX_TOKEN,
// language: 'en,zh', // 可选,按需设置语言
// nominatimBaseUrl: 'https://your-nominatim-instance.com',
}),
],
}))
```

### 扩展性

新的模块化设计使得扩展新功能变得更加容易:
Expand Down
Loading
Loading