写真・動画へEXIF情報付きのフレームを合成する、Web専用のブラウザアプリです。
Sample photograph: Jo Sorgenfri on Unsplash. The caption uses the camera settings supplied with the photograph.
写真・動画のデコード、EXIF読み取り、フレーム描画、エンコード、ZIP生成はブラウザ内で行われます。メディアファイルそのものをInstaFrameのサーバーへアップロードする処理はありません。
位置情報機能だけは、ユーザーが確認画面で明示的に許可した場合に外部通信します。
| 操作 | 送信先 | 送信される情報 |
|---|---|---|
| 地名をオンライン検索 | Nominatim | 緯度・経度、表示言語 |
| マップを表示 | OpenStreetMap | 表示範囲に対応するタイル番号 |
| 端末位置を取得できない場合の地図初期位置 | ipapi | 接続元IPアドレス |
| マップオーバーレイ生成(標準) | OpenStreetMap | 写真の位置周辺のタイル番号(ズーム14) |
| マップオーバーレイ生成(Mapboxトークンを設定した場合のみ) | Mapbox Static Images API | 緯度・経度、ズーム、公開アクセストークン |
- GPS付き写真を追加しただけでは外部通信しません。座標は端末内で文字列化されます。
- 許可は「このブラウザのセッション中」または「この端末で常に許可」から選択できます。
- 許可は カスタマイズ → プライバシー から取り消せます。
- EXIF・ZIP・動画処理・地図UI用JavaScript、HEICデコーダー(libheif・WebAssembly)と16種類のフレーム書体は、バージョン固定したnpmパッケージから生成し、サイト自身から配信します。EXIF解析器・ZIP生成器・HEICデコーダー・フレーム書体は必要になるまで読み込みません。地図タイルと位置情報APIは同意後にだけ外部へ接続します。
- 公開ページには、自己ホストスクリプト・スタイル・フォントだけを許可するContent-Security-Policyを設定しています。HEICデコーダーのWebAssemblyのために
'wasm-unsafe-eval'だけを追加で許可しています(JavaScriptのevalは許可していません)。外部接続先は、同意後に使うNominatim・ipapi・OpenStreetMapタイル・Mapboxだけに限定し、画像ファイル自体の送信先はありません。
- 写真・動画のライブプレビューと一括処理
- 「適用」操作は不要。ダウンロード時に現在の設定でフレームを描画し、1件はそのままファイル、複数件はZIPで保存
- 大量の一括ダウンロード:1枚ずつ「描画 → エンコード → ZIPへ格納 → 原寸Canvasを解放」と流すため、枚数が増えてもメモリはほぼ1枚分。ZIPが安全上限を超える場合は分割せず、保存を中止して案内
- ダウンロード前の出力サイズ表示:選択中の写真の出力ピクセル数と推定ファイルサイズ(形式・画質・比率に連動)、カードごとの寸法、複数枚の合計目安
- レイアウト警告:文字がフレームの端で切れる、写真に重なる、フレーム下端からはみ出す、位置情報がカメラ名と重なる場合にプレビュー上で知らせる
- フォント選択では、各フォント名をそのフォント自体で表示(書体ファイルは一覧を開いたときに初めて読み込み)
- 文字スタイルは「Shot on」「カメラ名」「メーカー」「EXIF」ごとに太字・斜体を切り替え(以前の「カメラ名を太字」設定は自動で引き継ぎ)
- マップオーバーレイはトークン不要。OpenStreetMapのタイルからモノクロの地図を合成し、中心のピンと「© OpenStreetMap contributors」の表記を入れて写真に重ねる(Mapboxトークンを設定した場合はMapboxを使用)
- HEIC / HEIF:ネイティブ対応していないブラウザ(Chrome、Edge、Firefox、Vivaldiなど)でも、同梱のlibheifでWorker内デコードして読み込む
- 空間写真(iPhoneなどの左右ペア):自動判別して「空間」バッジを表示。左右の視差を推定し、写真をフレームの奥へ下げることでフレームと文字が手前に浮いて見えるように両目分を描画。プレビューの「3D」で左右を交互表示して立体感を確認でき、「フレームの飛び出し」で量を調整。書き出しは空間HEIC(HEVCで書き出せるブラウザのみ。ステレオペア・カメラパラメータを保持)、左右並び(SBS)、通常の写真から選択
- 動画を開けないときは、ファイルからコーデック(HEVC 10-bit / Dolby Vision、H.264、ProRes、AV1など)を読み取り、原因と開き方(対応ブラウザ、iPhoneの「互換性優先」など)を表示
- 写真を追加する前から、フレーム色・太さ・文字色などの設定がサンプルプリントに反映される空画面
- メーカー、機種、レンズ、焦点距離、絞り、シャッター速度、ISO、位置情報の編集
- フレーム色、ぼかし背景(元写真から生成し、比率変更・余白を含む出力全体を端まで覆う)、文字色(自動・明るい・暗い・カスタム)
- 6144px固定レイアウト、最大1200%ズーム、ズーム追従の高密度ライブプレビュー
- 4:5 Instagram投稿、3:4プロフィールグリッド、9:16ストーリーの用途付きプリセット
- EXIF編集パネルのコンパクト・標準・大サイズ切り替え(スマートフォンでは写真を隠さないよう初期状態で折りたたみ)
- JPEG / WebP / PNG出力、動画出力、複数ファイルのZIP保存
- 初回表示・ハードリロード時のちらつき防止:保存済みテーマを最初の描画前に適用し、翻訳と設定の復元が終わるまで画面を表示しない(スクリプトを読み込めない場合も数秒後に表示)
- 日本語 / English、デスクトップ / モバイル対応
形式名だけではなく、実際のブラウザデコーダーがファイル内のコーデックへ対応している必要があります。
| 入力 | 対応方針 | 検証 |
|---|---|---|
| JPEG | 標準対応 | ChromiumフルE2Eに加え、Firefox・WebKit・Microsoft Edgeで読込、プレビュー、実JPEG署名の出力を自動検証 |
| PNG | 標準対応 | Chromium・Firefox・WebKit・Microsoft Edgeで実PNG入力のデコード、プレビュー、PNG署名の出力を自動検証 |
| WebP | 標準対応 | Chromium・Firefox・WebKit・Microsoft Edgeで実WebP入力のデコード、プレビュー、RIFF/WebP署名の出力を自動検証 |
| HEIC / HEIF | 標準対応 | 同梱のlibheif(WebAssembly)でデコードするため、ネイティブ非対応のブラウザでも読み込み可能。合成HEICと空間写真形式(sterステレオペア)のfixtureで読込・プレビュー・書き出しをChromium E2Eで検証。実機iPhoneの空間写真での確認は未実施 |
| WebM (VP8 / VP9) | 条件付き | VP8入力はChromium・Firefox・Microsoft Edgeでプレビュー成功を検証。Playwright WebKitは実行環境のコーデック差を許容し、実寸のあるデコード済みフレームまたは明示的な非対応エラーのどちらかを検証します。Linux Chromiumでは動画出力と音声トラック保持も検証。VP9入力はブラウザのコーデック対応に依存し、CI対象外 |
| MP4 (H.264 / AAC) | 条件付き | H.264 Baseline映像とAAC-LC音声を含む有効なMP4 fixtureを4ブラウザCIへ入力。Chromium・Firefox・Microsoft Edgeでは映像フレームのデコード済みプレビューを必須とし、Playwright WebKitでは明示的なコーデックエラーを許容。入力AAC音声のデコード・保持はこの契約の検証対象外 |
| MOV (H.264 / AAC) | 条件付き | 同じ映像・音声をQuickTimeコンテナへ再格納した有効fixtureを4ブラウザCIへ入力。映像プレビューの要件と入力AAC音声の未検証範囲はMP4と同じ |
| M4V (H.264 / AAC) | 条件付き | 検証済みMP4 fixtureを.m4v・video/x-m4vとして4ブラウザCIへ入力。映像プレビューの要件と入力AAC音声の未検証範囲はMP4と同じ |
| AVI / MKV / 3GP | 条件付き | コンテナと内部コーデックの両方をブラウザが再生できる場合のみ。4ブラウザCIで入力候補として受理され、無効な内容は項目ごとの明示エラーになることを検証。成功経路は未検証 |
表はファイル選択ダイアログで受け付ける主要形式です。ドラッグ&ドロップでは、このほかのimage/*・video/*もブラウザがネイティブデコードできる場合に利用できます。画像・動画ではない未対応形式は追加せず拒否件数を表示し、候補形式をブラウザがデコードできない場合も項目ごとのエラーを画面に表示します。
写真の出力はJPEG・PNG・WebPです。Blobの申告MIMEだけに依存せず実ファイル署名を検査し、署名が要求形式と一致する場合だけ、その拡張子で保存します。署名が正しくBlob MIMEだけが誤っている場合は、安全なMIMEへ補正します。動画出力はMediaRecorder.isTypeSupported()とCanvas stream captureの両方を満たすMP4またはWebM(VP8 / VP9 / ブラウザ選択)だけを表示し、完全な生成経路がないブラウザでは動画出力を無効化します。入力音声を保持できない場合は成功扱いにしません。MP4の音声はAACにし、録画方式で生成した断片化MP4は映像を再圧縮せず通常のMP4に仕上げます。MOV・M4V・AVI・MKV・3GPは入力候補であり、同じコンテナでの出力を意味しません。
動画書き出し中はプレビューのデコーダーを解放します。対応する環境では30 fps・60 fpsともフレーム単位で処理し、30 fps選択時はそのレートに変換します。対応できず実時間の録画方式に戻る場合は、端末の負荷によりフレーム落ちや重複が生じる場合があります。エンコーダーに対応する解像度が小さい場合や、録画に失敗して縮小再試行する場合は通知し、実際の出力サイズを表示します。エラーのカードを押すと詳細を確認できます。プレビューで動画データの読み出しに失敗した場合は、元ファイルがまだ読めれば小分けに読み直して一時コピーで再試行します。ビットレートの「元に近く」は元ファイル全体の容量と長さから音声・コンテナ込みの平均を概算し、0.5〜80 Mbpsに収めた値を映像エンコーダーへ指定します。実際のビットレートや画質はブラウザやコーデックにより変わり、無劣化ではありません。以前に保存したビットレート設定は保持するので、元に近い画質を目指す場合は「元に近く」を選択してください。
すべてを端末内で処理するため、入力ファイルより大きいデコード済み画素・Canvas・動画チャンク・ZIP Blobがメモリに存在します。ブラウザやタブ全体が停止する前に失敗を画面へ返すため、次の安全上限を設けています。
- 1回の作業領域は最大50項目、単一ファイル256 MiB、入力合計512 MiB
- 写真は最大60メガピクセル
- 単一Canvasは一辺16,384pxかつ64メガピクセル以下
- 保持する生成済み出力の推定合計は384 MiB以下。超える場合は古い写真の原寸出力から解放し(必要になれば元ファイルから再描画)、エラーにしない
- ZIP作成中の保持済み出力・写真Blob・一時バッファ・完成ZIPの推定ピークは512 MiB以下。超える場合は分割せず保存を中止して案内する
- 動画は選択ビットレートから見積もった出力が512 MiB以下
- 写真・動画カードのサムネイル同時デコードは種類ごと最大2件、同一項目の生成処理は最大1件
- 動画の読み直しは一度に4 MiBずつ処理するが、完成した一時コピーの保存には動画サイズ相当の追加領域が必要
- MediaRecorderの実チャンクとWebCodecsの圧縮済みchunkが512 MiBを超える前に生成を中断。WebCodecsの未処理エンコード待ち行列は最大4フレーム
- 写真CanvasのJPEG / PNG / WebPエンコードが60秒応答しない場合は終了し、書き出しUIを復帰
- 動画書き出しはメタデータを15秒、デコードフレーム進行を60秒待っても進まない場合に終了し、一時リソースを解放
- EXIF・動画メタデータ読み取りは15秒で終了。全削除時は実行中のFileReaderも中断
通常の2D写真では、フレームを付けた後の画像がアプリのCanvas上限を超える場合、収まる解像度へ自動で縮小してJPEG / PNG / WebPを書き出します。デスクトップの出力上限は一辺16,384px・64メガピクセル、スマートフォンでは一辺4,096px・16メガピクセルです。出力設定には実際のピクセル寸法と「縮小」を表示します。書き出し前にはカードやライブプレビューの重複した画像復号を終了し、読み込み済みの画像を再利用します。例えば50メガピクセルのJPEGは入力の40 MBという容量だけを理由に拒否されませんが、フレーム込みの寸法によって出力解像度が少し下がる場合があります。端末のCanvas制限や空きメモリによっては、この上限内でも失敗する場合があります。元の画素数を保ったままこの上限を超える出力には、別の分割描画・符号化方式が必要です。HEICの変換と立体写真は別の処理経路です。
30項目を超える場合は上限内でも先に警告します。上限を超えたファイルは追加または生成せず、既に追加した項目は保持します。書き出しはキャンセルでき、削除・再編集・ページ離脱時にはCanvas、Blob URL、MediaStream、AudioContextを解放します。戻る/進むキャッシュから復帰した場合は、解放済みの生成結果を未処理へ戻してライブプレビューを再構築します。
以前は画質ごとに元画像を600〜2400pxへ縮小してからフレームを再計算していたため、丸め誤差で文字位置が変化していました。現在は次の二層構造です。
- 構図、枠、文字座標を固定された6144pxの論理解像度で一度だけ計算
- 下書き・標準・高画質・最高画質は、表示キャンバスのピクセル密度だけを変更
- Autoは通常表示でも最低2倍、ズーム時は倍率に合わせて最大12倍のバックバッファへ再描画
- プレビュー用の縮小画素はJPEGへ再圧縮せず、ロスレスなCanvasのままフレームへ渡す
- 高倍率で全体Canvasが上限に達した場合は、画面内に見えている元画像領域だけを最大8メガピクセルで追加描画
そのため画質変更や最大1200%までのズームはシャープさと表示範囲だけに影響し、アスペクト比、余白、文字位置には影響しません。全体のライブCanvasはデスクトップ24メガピクセル、モバイル12メガピクセルを上限とします。それで表示密度が足りない場合だけ、ズーム中の可視領域を元の6144px論理Canvasから最大8メガピクセルの一時Canvasへ再描画します。この一時Canvasはパン・ピンチ中に破棄し、操作終了後の表示範囲だけを再生成します。
ブラウザ用の公開トークンはソースから見えることが前提です。秘密トークンは絶対に配置しないでください。
リポジトリにはトークンを同梱していません。マップオーバーレイは標準でOpenStreetMapのタイルを使うため、トークンは不要です。Mapboxの地図を使いたい場合だけ、カスタマイズ → プライバシー へ自分のMapbox公開トークンを入力します。値はそのブラウザのlocalStorageだけに保存されます。
サイト管理者が共通トークンを提供する場合は、次の手順が必要です。
- Mapboxでデフォルトトークンではない専用の公開トークンを作成します。
- Static Images APIに必要な公開
styles:tilesスコープだけを付与します。秘密スコープは付与しません。 - MapboxのAccess Tokens画面でAllowed URLを
https://lingmulongtai.github.ioに制限します。/InstaFrame/は付けません。 - js/config.js の空の
publicTokenを、制限確認済みの専用トークンへ置き換えます。 - MapboxのStatistics画面と請求書を定期的に監視します。Statisticsの反映には最大24時間かかる場合があります。
公開ページはstrict-origin-when-cross-originを明示し、Mapboxへのcross-origin要求ではRefererをhttps://lingmulongtai.github.io/までに制限します。このため、MapboxのAllowed URLへ/InstaFrame/を含めるとRefererに一致せず403になる可能性があります。パス単位では分離できないため、InstaFrame専用トークンを使い、他プロジェクトと共有しないでください。
共通トークンにはアプリ側でも同じオリジンだけを許可し、共通・利用者トークンの両方に端末ごと1日100回・1か月1000回のソフト上限を設けています。成功、失敗、タイムアウトを問わず、ブラウザが開始したStatic Images要求を1回として数えます。ただし、クライアント側の上限は改変できるため、Mapbox側のURL制限やアカウント監視の代替にはなりません。Mapboxは設定可能な支出アラートや月額上限を現在提供していません。free tierを初めて超えた際の通知はありますが、確実な遮断が必要ならStatistics・請求書を監視し、専用トークンを削除またはローテーションしてください。URL制限はMapboxの公式トークン管理ガイド、課金上限の制約は公式請求ガイドを確認してください。
コードとCIだけでは代替できない検証は、完了扱いにせず次のIssueで追跡しています。
- Issue #40: Mapbox管理画面での専用公開トークン、Allowed URL、実Refererの許可/拒否、監視・ローテーション運用
- Issue #41: 実Safari / iOSと有効なHEIC / HEIF / MOV / MP4 / VP9 fixtureによる成功経路
- Issue #42: 実VoiceOver / NVDAと全テーマの手動コントラスト確認
- Issue #43: 低メモリ実機での長時間・大規模な写真/動画混在プロファイル
初回に npm.cmd ci を実行してください。index.htmlを直接開くこともできますが、依存ライブラリの準備とブラウザのセキュリティ制限回避を兼ねた、同梱のローカルHTTPサーバーを推奨します。
npm.cmd run serveその後 http://127.0.0.1:4173 を開きます。serve はPagesと同じ許可済みファイルだけを dist/ へ生成して配信し、リポジトリ内の設定・テスト・.git はHTTP公開しません。サイト管理者が設定する共通Mapboxトークンは本番オリジンだけで利用できます。ローカル環境でも、利用者自身の公開トークンを設定して位置情報通信へ同意すればマップオーバーレイを利用できます。
npm.cmd ci
npm.cmd run prepare:vendor
npx.cmd playwright install chromium
npm.cmd testクロスブラウザ契約をローカルで再現する場合はFirefoxとWebKitも導入し、PLAYWRIGHT_BROWSERとPLAYWRIGHT_SUITE=cross-browserを設定します。Microsoft Edge契約は、Windowsにインストールされた実際のmsedgeチャンネルを使用します。
手元の大きなJPEGでデスクトップとスマートフォン表示の書き出しを再現する場合は、INSTAFRAME_LARGE_JPEGにそのファイルの絶対パスを指定し、npx.cmd playwright test -g 'real large JPEG'を実行します。写真はローカルテストサーバーで処理され、fixtureとしてリポジトリへ追加されません。
npm testは、既定ではChromiumのフルE2Eを含む次の検証を実行します。
- JavaScript構文検査
- ESLint
- 純粋関数のユニットテスト
- axeによるCritical / Seriousアクセシビリティ検査
- 初期画面、動的設定、位置情報同意、地図、エクスポート中UIの全axe違反検査
- 翻訳・保存設定の適用前に画面を描画しないこと、アプリ本体を読み込めない場合も画面が表示されること
- ライト、ソフトホワイト、ブルーグレー、ダーク、OS連動(明・暗)の動的設定と削除ダイアログに対するWCAG AA色コントラスト検査
- JPEG読込 → プレビュー → 書き出し
- 50メガピクセルの写真でフレームがCanvas上限を超えても、安全な寸法へ調整して描画でき、スマートフォン表示には縮小後の出力寸法が表示されること
- HEIC読込(ネイティブ非対応ブラウザ)→ プレビュー → フレーム付きJPEG書き出し、デコーダーがHEICを追加したときだけ読み込まれること
- 空間写真の判別、視差推定、3Dプレビュー、左右並び書き出しで写真がフレームより奥になること、空間HEICの書き出し構造(ステレオペア・カメラパラメータ)とHEVC非対応時の通常写真へのフォールバック
- フォント一覧が各フォントで表示され、開くまで書体を取得しないこと、キーボード操作
- 文字スタイルのパーツ別太字・斜体と旧設定の移行
- トークンなしのマップオーバーレイがOpenStreetMapタイルだけを取得すること、HEVC動画の失敗時にコーデックと開き方を表示すること
- PNG / WebPの実ファイル読込 → プレビュー → 各形式での書き出しとファイル署名
- EXIF編集と設定保存
- 複数JPEGのZIP出力、安全上限を超える場合の保存中止と通知、出力予算を超えたときの古い出力の解放
- ぼかし背景が文字入りの画像ではなく元写真から作られ、比率変更・余白の端まで不透明に覆うこと
- 文字切れ・位置情報の重なりの警告表示と解消、表示される出力サイズと実際の書き出し画像の一致
- 大量バッチのメモリ警告、入力・画素・Canvas・生成済み出力の安全上限、受理済みファイルの読込完了待ち、書き出しキャンセル
- EXIF・ZIP・フォントが必要になるまで初回取得されないこと
- 処理中エンコーダーと写真エンコードへのAbortSignal伝播、キャンセル後の同一Canvas再エンコード直列化、非表示タブでのMediaRecorder・動画ソース同時停止と復帰、WebCodecsの背圧・生成途中の出力上限、動画メタデータ・フレーム進行のタイムアウト
- デコード済みフレーム単位の動画プレビュー、カード・ダウンロードBlob URL、Canvas、MediaStream、AudioContextの解放
- 戻る/進むキャッシュを繰り返した場合のCanvas・Blob URL解放とプレビュー復帰
- 日本語画質UI、6144px固定レイアウト、最大1200%のズーム追従バックバッファ
- リサイズ境界のキーボード操作と、デスクトップ / モバイル遷移時のフォーカス維持
- GPS読込時に同意前の位置情報通信がないこと
- 外部ネットワークを遮断した状態での写真プレビュー・書き出し
- 同意後の地図UIが自己ホストLeafletを読み、Leaflet CDNへ接続しないこと
- モバイル表示とEXIF編集パネル
GitHub Actionsでは、上記に加えて次のCI専用契約を実行します。
- Chromium・Firefox・WebKit・Microsoft Edgeで、16種類すべての自己ホストフォント、写真/UI/axe/プライバシー契約を検証
- Chromium・Firefox・Microsoft EdgeでのVP8 WebM動画プレビューと、WebKitでのデコード成功または明示的な非対応エラー
- H.264 Baseline映像とAAC-LC音声を含むMP4・QuickTime MOV・M4V入力の4ブラウザ契約(Chromium・Firefox・Microsoft Edgeは映像フレームのデコード必須、Playwright WebKitは明示エラーを許容。入力AAC音声のデコード・保持は対象外)
captureStream、mozCaptureStream、Web Audio fallbackから出力streamへ音声trackを渡す決定的な4ブラウザ契約(実コーデックでの音声保持証拠とは区別)- Linux Chromiumでの音声付きWebMフレーム合成 → 動画・音声トラックを保持した書き出し
- 全品質ゲートと4ブラウザ契約が成功した
mainコミットだけをGitHub Pagesへデプロイ
GitHub Pagesへの公開は、構文・Lint・ユニットテストに加え、Chromium・Firefox・WebKit・Microsoft Edgeのブラウザマトリクスがすべて成功したmainコミットだけを対象にします。その後 dist/ を生成し、次のWeb公開物だけをデプロイします。
index.htmlcss/js/vendor/(固定バージョンのブラウザライブラリとライセンス)assets/(存在する場合)- ファビコンSVG
IDE設定、テスト、Node.js依存関係、READMEなどは公開アーティファクトに含めません。Pull Requestとmainへのpushでは、CIが構文・Lint・ユニット検査を1回実行し、Pagesと同じ dist/ を生成してから、ChromiumのフルE2EとFirefox・WebKit・Microsoft Edgeの写真/UI/axe/プライバシー/音声ルーティング契約をマトリクス実行します。各ブラウザはローカル資産のrevisionと、リポジトリ内部ファイルが配信されないことも確認します。動画エンコード、HEIC/HEIF、MP4/MOVはブラウザ・OSコーデック依存のため、対応を一律には保証しません。
InstaFrame/
├── index.html
├── css/style.css
├── js/
│ ├── core-utils.js
│ ├── config.js
│ ├── i18n.js
│ ├── frame-engine.js
│ └── app.js
├── tests/
│ ├── unit/
│ └── e2e/
├── scripts/prepare-site.mjs
├── playwright.config.cjs
├── eslint.config.mjs
└── .github/workflows/
└── ci.yml # 品質ゲートとGitHub Pagesデプロイ
InstaFrame is a browser-only EXIF frame generator for photos and videos. Media processing stays on the device. Location services are opt-in and clearly disclose coordinate transfers to Nominatim, OpenStreetMap/ipapi, and Mapbox. No unrestricted Mapbox token is shipped; users may store their own public token locally. Frame fonts and browser libraries are self-hosted. Preview quality changes raster density without recalculating the 6144px logical layout, so typography and composition remain stable through 1200% zoom. When the full preview reaches its 12/24-megapixel mobile/desktop limit, only the visible source crop receives an additional memory-bounded detail pass.
Run npm ci, npx playwright install chromium, and npm test for syntax, lint, unit, privacy, mobile, export, and photo/video browser tests. Browser tests build and serve the same allowlisted dist/ boundary used by Pages, without exposing repository internals. CI also runs Firefox, WebKit, and the installed Microsoft Edge channel against the portable photo/UI/accessibility contract.