@natsuneko-laboratory/memora 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +199 -199
- package/dist/index.d.ts +1 -1
- package/dist/index.js +14 -3
- package/dist/react-native.js +7 -2
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Kanon Mochizuki
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kanon Mochizuki
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,199 +1,199 @@
|
|
|
1
|
-
# Memora
|
|
2
|
-
|
|
3
|
-
Memora は、VRChat・VRCX・ResoniteScreenshotExtensions が画像に埋め込んだメタデータを読み取る TypeScript ライブラリです。撮影者、ワールド、カメラ設定、参加者の位置・姿勢などを、形式ごとの型付きオブジェクトとして取得できます。
|
|
4
|
-
|
|
5
|
-
Node.js、ブラウザー、React Native 向けに、`ArrayBuffer` または `Uint8Array` を受け取る API を提供します。ファイルの読み取りやネットワークアクセスは呼び出し側で行います。
|
|
6
|
-
|
|
7
|
-
## インストール
|
|
8
|
-
|
|
9
|
-
```sh
|
|
10
|
-
$ pnpm install @natsuneko-laboratory/memora
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## クイックスタート
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
import { parseImageMetadata } from "@natsuneko-laboratory/memora";
|
|
17
|
-
|
|
18
|
-
// bytes: ArrayBuffer | Uint8Array
|
|
19
|
-
const metadata = await parseImageMetadata(bytes);
|
|
20
|
-
|
|
21
|
-
if (metadata) {
|
|
22
|
-
switch (metadata.type) {
|
|
23
|
-
case "VRChat":
|
|
24
|
-
console.log(metadata.author, metadata.worldDisplayName, metadata.createDate);
|
|
25
|
-
break;
|
|
26
|
-
|
|
27
|
-
case "VRCX":
|
|
28
|
-
console.log(metadata.author?.displayName, metadata.world?.instanceId);
|
|
29
|
-
console.log(metadata.players);
|
|
30
|
-
break;
|
|
31
|
-
|
|
32
|
-
case "ResoniteScreenshotExtensions":
|
|
33
|
-
console.log(metadata.takenBy?.name, metadata.cameraFOV);
|
|
34
|
-
console.log(metadata.userInfos);
|
|
35
|
-
break;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
`type` で分岐すると、その形式のプロパティへ型安全にアクセスできます。メタデータがない画像や、対応形式を認識できない入力では `null` を返します。
|
|
41
|
-
|
|
42
|
-
## API
|
|
43
|
-
|
|
44
|
-
### `parseImageMetadata(input)`
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
function parseImageMetadata(input: ArrayBuffer | Uint8Array): Promise<ImageMetadata | null>;
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
画像から 1 件のメタデータを取得します。複数形式が同居する場合は ResoniteScreenshotExtensions
|
|
51
|
-
|
|
52
|
-
`parsePhotoMetadata` はこの関数の別名です。
|
|
53
|
-
|
|
54
|
-
### `parseAllImageMetadata(input)`
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
function parseAllImageMetadata(input: ArrayBuffer | Uint8Array): Promise<ImageMetadata[]>;
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
画像内で認識できたすべてのメタデータを取得します。複数形式や複数パケットを扱う場合に使用してください。認識できるメタデータがなければ空配列を返します。
|
|
61
|
-
|
|
62
|
-
### `ImageMetadata`
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
type ImageMetadata =
|
|
66
|
-
VrcxImageMetadata | VRChatImageMetadata | ResoniteScreenshotExtensionsImageMetadata;
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
すべての形式に、次のプロパティがあります。
|
|
70
|
-
|
|
71
|
-
| プロパティ | 型 | 内容 |
|
|
72
|
-
| ---------- | ------------------------------------------------------ | ------------------------------------------------------ |
|
|
73
|
-
| `type` | `"VRCX" \| "VRChat" \| "ResoniteScreenshotExtensions"` | メタデータの形式 |
|
|
74
|
-
| `raw` | `Uint8Array` | 元画像全体のバイト列 |
|
|
75
|
-
| `extra` | `Record<string, unknown>` | 型定義にない項目、または期待する型へ解析できなかった値 |
|
|
76
|
-
|
|
77
|
-
画像に含まれない項目は optional プロパティとして扱います。空文字が埋め込まれている場合は、その空文字を保持します。
|
|
78
|
-
|
|
79
|
-
## 対応形式とフィールド
|
|
80
|
-
|
|
81
|
-
### VRChat
|
|
82
|
-
|
|
83
|
-
`type: "VRChat"`
|
|
84
|
-
|
|
85
|
-
- `creatorTool`, `author`, `authorId`
|
|
86
|
-
- `worldId`, `worldDisplayName`
|
|
87
|
-
- `createDate`, `modifyDate`, `dateTime`
|
|
88
|
-
- `title`: 言語ごとの `{ value: string; language?: string }[]`
|
|
89
|
-
- `world`: 旧形式の `vrc:World`
|
|
90
|
-
|
|
91
|
-
旧形式の `author` にはユーザー ID が入る場合があります。新しい形式では表示名と `authorId` を別々に取得できます。
|
|
92
|
-
|
|
93
|
-
### VRCX
|
|
94
|
-
|
|
95
|
-
`type: "VRCX"`
|
|
96
|
-
|
|
97
|
-
- `application`, `version`
|
|
98
|
-
- `author`: `id`, `displayName`
|
|
99
|
-
- `world`: `id`, `name`, `instanceId`
|
|
100
|
-
- `players`: `id`, `displayName` を持つユーザーの配列
|
|
101
|
-
|
|
102
|
-
ユーザーとワールドには、それぞれ `extra` もあります。
|
|
103
|
-
|
|
104
|
-
### ResoniteScreenshotExtensions
|
|
105
|
-
|
|
106
|
-
`type: "ResoniteScreenshotExtensions"`
|
|
107
|
-
|
|
108
|
-
- 場所: `locationName`, `locationAccessLevel`, `locationHiddenFromListing`, `locationHost`
|
|
109
|
-
- 撮影: `timeTaken`, `takenBy`, `takenGlobalPosition`, `takenGlobalRotation`, `takenGlobalScale`
|
|
110
|
-
- アプリ・カメラ: `appVersion`, `cameraManufacturer`, `cameraModel`, `cameraFOV`, `is360`, `stereoLayout`
|
|
111
|
-
- 参加者: `userInfos`
|
|
112
|
-
|
|
113
|
-
`locationHost` と `takenBy` は `id`, `name`, `machineId`, `extra` を持ちます。`userInfos` の各要素には、さらに次のフィールドがあります。
|
|
114
|
-
|
|
115
|
-
- `isInVR`, `isPresent`
|
|
116
|
-
- `headPosition`, `headOrientation`
|
|
117
|
-
- `sessionJoinTimestamp`
|
|
118
|
-
|
|
119
|
-
`userInfos` は、ユーザーが 1 人の場合も配列です。
|
|
120
|
-
|
|
121
|
-
## 値の扱い
|
|
122
|
-
|
|
123
|
-
- 日時は元の文字列を保持します。タイムゾーン変換や `Date` 化を行わず、小数秒の精度も維持します。
|
|
124
|
-
- Resonite の名前は Unicode / URI エスケープを復元します。
|
|
125
|
-
- 真偽値は `boolean`、カメラの画角は `number` に解析します。
|
|
126
|
-
- 位置・スケールは `Vector3`(`[number, number, number]`)、回転は `Quaternion`(`[number, number, number, number]`)に解析します。座標変換や正規化は行いません。
|
|
127
|
-
- 未知の項目や解析できない値は `extra` に保持し、既定値で補完しません。
|
|
128
|
-
|
|
129
|
-
XML の属性や名前空間の構造は、形式ごとのプロパティへ整理されます。構文表記まで含めた元データが必要な場合は `raw` を利用してください。JSON 数値の精度は JavaScript の `number` に従います。
|
|
130
|
-
|
|
131
|
-
## 実行環境ごとの利用例
|
|
132
|
-
|
|
133
|
-
### Node.js
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
import { readFile } from "node:fs/promises";
|
|
137
|
-
import { parseImageMetadata } from "@natsuneko-laboratory/memora";
|
|
138
|
-
|
|
139
|
-
const bytes = await readFile("screenshot.png");
|
|
140
|
-
const metadata = await parseImageMetadata(bytes);
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
### ブラウザー
|
|
144
|
-
|
|
145
|
-
```ts
|
|
146
|
-
import { parseImageMetadata } from "@natsuneko-laboratory/memora";
|
|
147
|
-
|
|
148
|
-
async function readScreenshot(file: File | Blob) {
|
|
149
|
-
return parseImageMetadata(await file.arrayBuffer());
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
### React Native
|
|
154
|
-
|
|
155
|
-
```ts
|
|
156
|
-
import { parseImageMetadata } from "@natsuneko-laboratory/memora";
|
|
157
|
-
|
|
158
|
-
async function readScreenshot(uri: string, readBytes: (uri: string) => Promise<Uint8Array>) {
|
|
159
|
-
const bytes = await readBytes(uri);
|
|
160
|
-
return parseImageMetadata(bytes);
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
`readBytes` には、アプリで採用しているファイルアクセスライブラリを使った読み取り関数を渡してください。URI、Base64、`File`、`Blob` をパーサーへ直接渡すことはできません。
|
|
165
|
-
|
|
166
|
-
Metro は `react-native` 条件(従来の解決方式では同名のフィールド)から専用バンドルを読み込みます。Node.js モジュールのポリフィルや Metro の追加設定は不要です。
|
|
167
|
-
|
|
168
|
-
## 対応範囲と制約
|
|
169
|
-
|
|
170
|
-
PNG の iTXt(非圧縮・zlib 圧縮)と XMP を読み取ります。ResoniteScreenshotExtensions の JPEG XMP にも対応します。
|
|
171
|
-
|
|
172
|
-
画像のリサイズ・再圧縮・形式変換により、メタデータが削除される場合があります。変換前の元画像を使用してください。
|
|
173
|
-
|
|
174
|
-
コアは React やネイティブモジュールに依存しません。Node.js の `Buffer` / `process`、DOM、`TextDecoder` がない環境でのバンドルテストを用意しています。
|
|
175
|
-
|
|
176
|
-
React Native 向けには、依存先 `exifr` の Node.js 専用ローダーを除いたバンドルを生成します。v0.1.0 では、このローダーの動的 `import` が Metro の依存解析エラーを起こしていました。Metro によるパッケージ解決・バンドルと、ホスト API のない環境での PNG・JPEG 解析を回帰テストで検証しています。iOS / Android 実機でのテストではありません。
|
|
177
|
-
|
|
178
|
-
`navigator` が存在しても `userAgent` がない React Native 環境に対応するため、専用バンドルでは exifr のブラウザー描画補正用 UA 参照を空文字に置き換えます。アプリ側で `navigator.userAgent` を補完する必要はなく、グローバル変数も変更しません。
|
|
179
|
-
|
|
180
|
-
## 開発
|
|
181
|
-
|
|
182
|
-
リポジトリを取得した後、パッケージのディレクトリで実行します。
|
|
183
|
-
|
|
184
|
-
```sh
|
|
185
|
-
npm install
|
|
186
|
-
npm test
|
|
187
|
-
npm run typecheck
|
|
188
|
-
npm run build
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
テストは TypeScript と Vitest で記述しています。実画像と合成画像を用い、形式の判別、全フィールドの取得、元バイト列の保持、部分バッファ入力、実行環境への依存を検証します。
|
|
192
|
-
|
|
193
|
-
`npm run typecheck` は本体とテストの両方を検証します。テストのみの型チェックには `npm run typecheck:test` を使用できます。
|
|
194
|
-
|
|
195
|
-
ビルドすると、ESM、React Native 向けバンドル、型定義が `dist/` に生成されます。インストール時のスクリプトを無効にしている場合は、利用前に `npm run build` を実行してください。
|
|
196
|
-
|
|
197
|
-
## ライセンス
|
|
198
|
-
|
|
199
|
-
MIT
|
|
1
|
+
# Memora
|
|
2
|
+
|
|
3
|
+
Memora は、VRChat・VRCX・ResoniteScreenshotExtensions が画像に埋め込んだメタデータを読み取る TypeScript ライブラリです。撮影者、ワールド、カメラ設定、参加者の位置・姿勢などを、形式ごとの型付きオブジェクトとして取得できます。
|
|
4
|
+
|
|
5
|
+
Node.js、ブラウザー、React Native 向けに、`ArrayBuffer` または `Uint8Array` を受け取る API を提供します。ファイルの読み取りやネットワークアクセスは呼び出し側で行います。
|
|
6
|
+
|
|
7
|
+
## インストール
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
$ pnpm install @natsuneko-laboratory/memora
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## クイックスタート
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { parseImageMetadata } from "@natsuneko-laboratory/memora";
|
|
17
|
+
|
|
18
|
+
// bytes: ArrayBuffer | Uint8Array
|
|
19
|
+
const metadata = await parseImageMetadata(bytes);
|
|
20
|
+
|
|
21
|
+
if (metadata) {
|
|
22
|
+
switch (metadata.type) {
|
|
23
|
+
case "VRChat":
|
|
24
|
+
console.log(metadata.author, metadata.worldDisplayName, metadata.createDate);
|
|
25
|
+
break;
|
|
26
|
+
|
|
27
|
+
case "VRCX":
|
|
28
|
+
console.log(metadata.author?.displayName, metadata.world?.instanceId);
|
|
29
|
+
console.log(metadata.players);
|
|
30
|
+
break;
|
|
31
|
+
|
|
32
|
+
case "ResoniteScreenshotExtensions":
|
|
33
|
+
console.log(metadata.takenBy?.name, metadata.cameraFOV);
|
|
34
|
+
console.log(metadata.userInfos);
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`type` で分岐すると、その形式のプロパティへ型安全にアクセスできます。メタデータがない画像や、対応形式を認識できない入力では `null` を返します。
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
### `parseImageMetadata(input)`
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
function parseImageMetadata(input: ArrayBuffer | Uint8Array): Promise<ImageMetadata | null>;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
画像から 1 件のメタデータを取得します。複数形式が同居する場合は VRChat、VRCX、ResoniteScreenshotExtensions の順で優先します。
|
|
51
|
+
|
|
52
|
+
`parsePhotoMetadata` はこの関数の別名です。
|
|
53
|
+
|
|
54
|
+
### `parseAllImageMetadata(input)`
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
function parseAllImageMetadata(input: ArrayBuffer | Uint8Array): Promise<ImageMetadata[]>;
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
画像内で認識できたすべてのメタデータを取得します。複数形式や複数パケットを扱う場合に使用してください。認識できるメタデータがなければ空配列を返します。
|
|
61
|
+
|
|
62
|
+
### `ImageMetadata`
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
type ImageMetadata =
|
|
66
|
+
VrcxImageMetadata | VRChatImageMetadata | ResoniteScreenshotExtensionsImageMetadata;
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
すべての形式に、次のプロパティがあります。
|
|
70
|
+
|
|
71
|
+
| プロパティ | 型 | 内容 |
|
|
72
|
+
| ---------- | ------------------------------------------------------ | ------------------------------------------------------ |
|
|
73
|
+
| `type` | `"VRCX" \| "VRChat" \| "ResoniteScreenshotExtensions"` | メタデータの形式 |
|
|
74
|
+
| `raw` | `Uint8Array` | 元画像全体のバイト列 |
|
|
75
|
+
| `extra` | `Record<string, unknown>` | 型定義にない項目、または期待する型へ解析できなかった値 |
|
|
76
|
+
|
|
77
|
+
画像に含まれない項目は optional プロパティとして扱います。空文字が埋め込まれている場合は、その空文字を保持します。
|
|
78
|
+
|
|
79
|
+
## 対応形式とフィールド
|
|
80
|
+
|
|
81
|
+
### VRChat
|
|
82
|
+
|
|
83
|
+
`type: "VRChat"`
|
|
84
|
+
|
|
85
|
+
- `creatorTool`, `author`, `authorId`
|
|
86
|
+
- `worldId`, `worldDisplayName`
|
|
87
|
+
- `createDate`, `modifyDate`, `dateTime`
|
|
88
|
+
- `title`: 言語ごとの `{ value: string; language?: string }[]`
|
|
89
|
+
- `world`: 旧形式の `vrc:World`
|
|
90
|
+
|
|
91
|
+
旧形式の `author` にはユーザー ID が入る場合があります。新しい形式では表示名と `authorId` を別々に取得できます。
|
|
92
|
+
|
|
93
|
+
### VRCX
|
|
94
|
+
|
|
95
|
+
`type: "VRCX"`
|
|
96
|
+
|
|
97
|
+
- `application`, `version`
|
|
98
|
+
- `author`: `id`, `displayName`
|
|
99
|
+
- `world`: `id`, `name`, `instanceId`
|
|
100
|
+
- `players`: `id`, `displayName` を持つユーザーの配列
|
|
101
|
+
|
|
102
|
+
ユーザーとワールドには、それぞれ `extra` もあります。
|
|
103
|
+
|
|
104
|
+
### ResoniteScreenshotExtensions
|
|
105
|
+
|
|
106
|
+
`type: "ResoniteScreenshotExtensions"`
|
|
107
|
+
|
|
108
|
+
- 場所: `locationName`, `locationAccessLevel`, `locationHiddenFromListing`, `locationHost`
|
|
109
|
+
- 撮影: `timeTaken`, `takenBy`, `takenGlobalPosition`, `takenGlobalRotation`, `takenGlobalScale`
|
|
110
|
+
- アプリ・カメラ: `appVersion`, `cameraManufacturer`, `cameraModel`, `cameraFOV`, `is360`, `stereoLayout`
|
|
111
|
+
- 参加者: `userInfos`
|
|
112
|
+
|
|
113
|
+
`locationHost` と `takenBy` は `id`, `name`, `machineId`, `extra` を持ちます。`userInfos` の各要素には、さらに次のフィールドがあります。
|
|
114
|
+
|
|
115
|
+
- `isInVR`, `isPresent`
|
|
116
|
+
- `headPosition`, `headOrientation`
|
|
117
|
+
- `sessionJoinTimestamp`
|
|
118
|
+
|
|
119
|
+
`userInfos` は、ユーザーが 1 人の場合も配列です。
|
|
120
|
+
|
|
121
|
+
## 値の扱い
|
|
122
|
+
|
|
123
|
+
- 日時は元の文字列を保持します。タイムゾーン変換や `Date` 化を行わず、小数秒の精度も維持します。
|
|
124
|
+
- Resonite の名前は Unicode / URI エスケープを復元します。
|
|
125
|
+
- 真偽値は `boolean`、カメラの画角は `number` に解析します。
|
|
126
|
+
- 位置・スケールは `Vector3`(`[number, number, number]`)、回転は `Quaternion`(`[number, number, number, number]`)に解析します。座標変換や正規化は行いません。
|
|
127
|
+
- 未知の項目や解析できない値は `extra` に保持し、既定値で補完しません。
|
|
128
|
+
|
|
129
|
+
XML の属性や名前空間の構造は、形式ごとのプロパティへ整理されます。構文表記まで含めた元データが必要な場合は `raw` を利用してください。JSON 数値の精度は JavaScript の `number` に従います。
|
|
130
|
+
|
|
131
|
+
## 実行環境ごとの利用例
|
|
132
|
+
|
|
133
|
+
### Node.js
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { readFile } from "node:fs/promises";
|
|
137
|
+
import { parseImageMetadata } from "@natsuneko-laboratory/memora";
|
|
138
|
+
|
|
139
|
+
const bytes = await readFile("screenshot.png");
|
|
140
|
+
const metadata = await parseImageMetadata(bytes);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### ブラウザー
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { parseImageMetadata } from "@natsuneko-laboratory/memora";
|
|
147
|
+
|
|
148
|
+
async function readScreenshot(file: File | Blob) {
|
|
149
|
+
return parseImageMetadata(await file.arrayBuffer());
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### React Native
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { parseImageMetadata } from "@natsuneko-laboratory/memora";
|
|
157
|
+
|
|
158
|
+
async function readScreenshot(uri: string, readBytes: (uri: string) => Promise<Uint8Array>) {
|
|
159
|
+
const bytes = await readBytes(uri);
|
|
160
|
+
return parseImageMetadata(bytes);
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`readBytes` には、アプリで採用しているファイルアクセスライブラリを使った読み取り関数を渡してください。URI、Base64、`File`、`Blob` をパーサーへ直接渡すことはできません。
|
|
165
|
+
|
|
166
|
+
Metro は `react-native` 条件(従来の解決方式では同名のフィールド)から専用バンドルを読み込みます。Node.js モジュールのポリフィルや Metro の追加設定は不要です。
|
|
167
|
+
|
|
168
|
+
## 対応範囲と制約
|
|
169
|
+
|
|
170
|
+
PNG の iTXt(非圧縮・zlib 圧縮)と XMP を読み取ります。ResoniteScreenshotExtensions の JPEG XMP にも対応します。
|
|
171
|
+
|
|
172
|
+
画像のリサイズ・再圧縮・形式変換により、メタデータが削除される場合があります。変換前の元画像を使用してください。
|
|
173
|
+
|
|
174
|
+
コアは React やネイティブモジュールに依存しません。Node.js の `Buffer` / `process`、DOM、`TextDecoder` がない環境でのバンドルテストを用意しています。
|
|
175
|
+
|
|
176
|
+
React Native 向けには、依存先 `exifr` の Node.js 専用ローダーを除いたバンドルを生成します。v0.1.0 では、このローダーの動的 `import` が Metro の依存解析エラーを起こしていました。Metro によるパッケージ解決・バンドルと、ホスト API のない環境での PNG・JPEG 解析を回帰テストで検証しています。iOS / Android 実機でのテストではありません。
|
|
177
|
+
|
|
178
|
+
`navigator` が存在しても `userAgent` がない React Native 環境に対応するため、専用バンドルでは exifr のブラウザー描画補正用 UA 参照を空文字に置き換えます。アプリ側で `navigator.userAgent` を補完する必要はなく、グローバル変数も変更しません。
|
|
179
|
+
|
|
180
|
+
## 開発
|
|
181
|
+
|
|
182
|
+
リポジトリを取得した後、パッケージのディレクトリで実行します。
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
npm install
|
|
186
|
+
npm test
|
|
187
|
+
npm run typecheck
|
|
188
|
+
npm run build
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
テストは TypeScript と Vitest で記述しています。実画像と合成画像を用い、形式の判別、全フィールドの取得、元バイト列の保持、部分バッファ入力、実行環境への依存を検証します。
|
|
192
|
+
|
|
193
|
+
`npm run typecheck` は本体とテストの両方を検証します。テストのみの型チェックには `npm run typecheck:test` を使用できます。
|
|
194
|
+
|
|
195
|
+
ビルドすると、ESM、React Native 向けバンドル、型定義が `dist/` に生成されます。インストール時のスクリプトを無効にしている場合は、利用前に `npm run build` を実行してください。
|
|
196
|
+
|
|
197
|
+
## ライセンス
|
|
198
|
+
|
|
199
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ImageMetadata, PhotoInput } from "./types.js";
|
|
|
2
2
|
export type * from "./types.js";
|
|
3
3
|
/** Read every recognized packet without dropping coexisting formats. Unknown formats return an empty array. */
|
|
4
4
|
export declare const parseAllImageMetadata: (input: PhotoInput) => Promise<ImageMetadata[]>;
|
|
5
|
-
/**
|
|
5
|
+
/** Prefer VRChat, then VRCX, then ResoniteScreenshotExtensions for mixed images. */
|
|
6
6
|
export declare const parseImageMetadata: (input: PhotoInput) => Promise<ImageMetadata | null>;
|
|
7
7
|
/** Alias for existing callers. */
|
|
8
8
|
export declare const parsePhotoMetadata: typeof parseImageMetadata;
|
package/dist/index.js
CHANGED
|
@@ -213,12 +213,23 @@ export const parseAllImageMetadata = async (input) => {
|
|
|
213
213
|
}));
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
|
-
|
|
216
|
+
const priority = {
|
|
217
|
+
VRChat: 0,
|
|
218
|
+
VRCX: 1,
|
|
219
|
+
ResoniteScreenshotExtensions: 2,
|
|
220
|
+
};
|
|
221
|
+
// A screenshot can contain metadata written by more than one application.
|
|
222
|
+
// Keep duplicate records and their relative order, but expose the supported
|
|
223
|
+
// format precedence consistently to callers.
|
|
224
|
+
return result
|
|
225
|
+
.map((metadata, index) => ({ metadata, index }))
|
|
226
|
+
.sort((a, b) => priority[a.metadata.type] - priority[b.metadata.type] || a.index - b.index)
|
|
227
|
+
.map(({ metadata }) => metadata);
|
|
217
228
|
};
|
|
218
|
-
/**
|
|
229
|
+
/** Prefer VRChat, then VRCX, then ResoniteScreenshotExtensions for mixed images. */
|
|
219
230
|
export const parseImageMetadata = async (input) => {
|
|
220
231
|
const results = await parseAllImageMetadata(input);
|
|
221
|
-
return
|
|
232
|
+
return results[0] ?? null;
|
|
222
233
|
};
|
|
223
234
|
/** Alias for existing callers. */
|
|
224
235
|
export const parsePhotoMetadata = parseImageMetadata;
|
package/dist/react-native.js
CHANGED
|
@@ -9380,11 +9380,16 @@ var parseAllImageMetadata = async (input) => {
|
|
|
9380
9380
|
);
|
|
9381
9381
|
}
|
|
9382
9382
|
}
|
|
9383
|
-
|
|
9383
|
+
const priority = {
|
|
9384
|
+
VRChat: 0,
|
|
9385
|
+
VRCX: 1,
|
|
9386
|
+
ResoniteScreenshotExtensions: 2
|
|
9387
|
+
};
|
|
9388
|
+
return result.map((metadata, index) => ({ metadata, index })).sort((a, b) => priority[a.metadata.type] - priority[b.metadata.type] || a.index - b.index).map(({ metadata }) => metadata);
|
|
9384
9389
|
};
|
|
9385
9390
|
var parseImageMetadata = async (input) => {
|
|
9386
9391
|
const results = await parseAllImageMetadata(input);
|
|
9387
|
-
return results
|
|
9392
|
+
return results[0] ?? null;
|
|
9388
9393
|
};
|
|
9389
9394
|
var parsePhotoMetadata = parseImageMetadata;
|
|
9390
9395
|
export {
|
package/package.json
CHANGED