投稿日:2025-09-07

ウォーターマークをつけてサーバに保存するアップローダ

ウォーターマークをつけるWebアプリ(改良版) の元になった「透かし入れアップローダ」を少し改良したので、紹介。


今回は…

AVIF,JPEG XLの読み込み

オリジナルファイルが1200px以上の場合はリサイズ処理が入りますので、オリジナルは劣化が少ない方がいいかと思いJPEG XLの読み込みを実装したのですが…Chromeだとネイティブで読み取れず、動作がめちゃ重いという…。

仕方ないのでAVIFも実装したのですが、これ両方とも使うことあるのかな?どうせだったらHEIF対応にすればよかったかも…と思いましたが、「使うことあるのかな?」が先に浮かんでしまったので未実装w

サーバ保存時にEXIF撮影日時を参照

/YYYY ディレクトリに「mmdd-元ファイル名」として保存。

これ前はEXIF読み込みまでやっておらず、アップロード日を元にやっていました。

EXIFの埋め込み可・不可

上記仕様でEXIF参照が必要なため、アップロードするファイルからEXIFを除去するか残すか選べます。

ただし、仕様が面倒なのでJPEGのみとなっていて、通常PNGのローカル保存の場合はEXIF残すとJPEGに変更となります。


スクリーンショット


仕様書

ほぼAIまかせで書いたので、コードの公開はしませんが、AIに読み込ませれば似たような感じに作ってくれるかと…。

もっと見る…
# photo_uploader 仕様書(LLM生成用)
> これは既存の `photo_album.php` を**「photo_uploader」**として1から再実装できるようにまとめた仕様です。  
> 目的:画像(JPEG/PNG/HEIC/AVIF/JPEG XL 等)をブラウザで開き、**透かし文字**を重ねて、**ダウンロード or サーバ保存**できる簡易ツール。  
> 実装対象:**単一PHPファイル**(UI+保存API)+フロントJS。外部ライブラリはCDN読み込み。
---
## 1. 想定環境・例示パス
- サーバ:Apache + PHP 7.4+(8.x推奨)。GDは**不要**(サーバ側で再エンコードしないため)。
- タイムゾーン:`Asia/Tokyo` をPHP開始時に指定。
- 認証:超簡易な**パスワード1つ**(例:`example_password`)でセッション認証。  
- 保存先(例):`/var/www/html/photo_uploader/`  
  - 公開ディレクトリに `photo_uploader.php` を置く想定。  
  - 画像保存は `../gallery/YYYY/`(例:`../gallery/2025/`)に書き込み。
> 例は環境依存。LLMは**相対パス**と**権限チェック**が通るように作ること。
---
## 2. 大枠アーキテクチャ
- **1ファイル構成**:`photo_uploader.php`
  - (A) **認証ページ**(パスワード入力 → セッション開始)  
  - (B) **UIページ**(HTML/CSS/JSで編集UIとCanvas表示)  
  - (C) **保存API**(同一ファイルにPOST:`imageData` 等を受けて保存)
- **フロント側処理**(JavaScript)
  - 画像読込(input[type=file])
  - AVIF/JPEG XLの**デコード**(ネイティブ→失敗時WASM)
  - 透かしレンダリング(Canvas)
  - ダウンロード(PNG or JPEG)
  - サーバ保存(JPEGをDataURLでPOST)
  - EXIFの読込/移植/ストリップの制御(後述)
  - UI状態の `localStorage` 保存/復元
---
## 3. 主要機能
### 3.1 画像の読込
- `<input type="file" id="upload" accept="image/*,.jxl,image/jxl,.avif,image/avif">`
- 判定ロジック  
  - `file.type` と拡張子から **`isAvif`** / **`isJxl`** を判定。  
  - **ネイティブ表示可能**なら `Image.src = URL.createObjectURL(file)`。  
  - 失敗時は**WASMフォールバック**:
    - JXL: `@jsquash/jxl`  
    - AVIF: `@jsquash/avif`  
    - 両者とも `decode(ArrayBuffer)` → `ImageData` 相当をCanvasに `putImageData` → `toDataURL('image/png')` → `Image` に流す。
> 目的:**どの形式でも最終的にブラウザで見られる状態(Image)**を得る。
### 3.2 EXIF(撮影日時・移植・ストリップ)
- **撮影日時の抽出(クライアント)**  
  - `exifr`(UMD)で `DateTimeOriginal → CreateDate → ModifyDate` を優先的に読み取り。  
  - 取得不能時は `file.lastModified` を使用。  
  - 取得した日時は `Date` として保持し、**サーバ保存時に `takenAt`(ISO8601)** としてPOST。
- **EXIF移植(JPEGのみ)**  
  - 元ファイルがJPEGの場合のみ、ダウンロード時/サーバ保存時に `piexifjs` で**元EXIFを新JPEGに移植**。  
  - AVIF/JXL/PNG由来では移植**しない**(形式差異と互換性のため)。
- **EXIFストリップ(共通設定)**  
  - UIに「**EXIFをカットする**」チェック(`#stripExif`)を用意。  
  - **AVIF/JXL読み込み時は非表示**(ストリップ対象外)。  
  - チェック**ON**:ダウンロードはPNG(EXIFなし)/サーバ保存はEXIFなしJPEG(移植せず)。  
  - チェック**OFF**:ダウンロードはJPEG(可能ならEXIF移植)/サーバ保存もJPEG(可能なら移植)。
  - **状態は共通**で `localStorage("strip_exif")` に保存。
### 3.3 透かし(ウォーターマーク)
- UI項目:
  - テキスト(`#watermarkText`)
  - 文字色(白/黒の簡易トグル `#blackColor`)
  - 縁取りの有無(`#outline`)
  - フォント選択(`#fontFamily`)
  - フォントサイズ(`#fontSize`)
  - 画像リサイズ目標長(`#resizeLength`:長辺基準)
  - 「文字位置を中央にする」(`#centerTextButton`)
- 操作:
  - プレビューCanvas上で**ドラッグ移動**(マウス&タッチ対応)。
- 描画:
  - 表示用Canvas(縮小比は後述)に画像→文字を描画。
  - 保存時は非表示の本番Canvasに**等倍**で文字合成して出力。
### 3.4 ダウンロード
- ボタン:`#downloadButton`(ラベルは**EXIF設定に応じて**自動切替)
  - **ON**(ストリップ)→「**PNGとしてダウンロード**」
  - **OFF**(保持)→「**JPEGとしてダウンロード**」
- ファイル名:元名の拡張子を置換(`.png` or `.jpg`)。  
- JPEG時は**可能ならEXIF移植**してからDataURLをリンククリック。
### 3.5 サーバ保存
- ボタン:`#uploadServerButton`(「JPEGでサーバ保存」)
- 送信内容(`multipart/form-data`):
  - `imageData`: `data:image/jpeg;base64,...`(常に**JPEG**)
  - `fileName`: 元ファイル名(UIで扱った元の名前)
  - `takenAt`: ISO8601文字列(クライアントで取得・決定)
- サーバ処理(PHP):
  - **認証必須**(未認証は `403`+メッセージ)。
  - `imageData` を `base64_decode`。
  - `takenAt` → `strtotime` 成功ならその時刻を採用、失敗時は `time()`。
  - ディレクトリ:`../gallery/YYYY/` を作成(0777, 再帰)。
  - ファイル名:
    - `fileName` があればベース名を**英数_-以外は `_`**に置換し、**先頭に `MMDD_`** を付与、拡張子は `.jpg` 固定。
    - なければ `MMDD-watermarked_YYYYmmdd_His.jpg`
    - 重複は `_1`, `_2`, … を末尾に付与。
  - **再エンコードせず** `file_put_contents($savePath, $decodedImage)` で保存(EXIF移植されたJPEGを維持)。
  - レスポンス:保存した**ファイル名(テキスト)**。
---
## 4. UIレイアウトとレスポンシブ要件
### 4.1 配置
- `.layout`:フォームとプレビューのコンテナ
  - **モバイル**(デフォルト):縦並び
  - **900px以上**:横並び(左:`.form-area`、右:`#displayCanvas`)
- `.form-area`:カード風のフォーム。最大幅 ~460px。
- `#displayCanvas`:プレビュー用Canvas。枠線+ドロップシャドウ。
### 4.2 横並び/縦並びの別
- **フォーム内の二列**(`#fontSize` と `#resizeLength`)
  - `.form-row` + `.form-col`
  - **広い画面(PC)**:縦並び  
  - **スマホ(600px以下)**:横並び
### 4.3 プレビュー拡大率
- **基本倍率 `BASE_DISPLAY_SCALE = 0.3`**  
- **900px以上**の画面では**1.5倍**(= 0.45)で表示  
- 実装は `currentDisplayScale()` で判定し、`resize` イベントで再適用。
---
## 5. ローカルストレージ仕様(キーと意味)
| Key                | 値例       | 説明 |
|---|---|---|
| `resize`           | `"1200"`   | リサイズ長辺ピクセル |
| `watermarkText`    | `"Watermark"` | 透かし文字 |
| `textColor`        | `"black"` / `"white"` | 文字色 |
| `watermark_size`   | `"30"`     | 文字サイズ(px) |
| `watermark_font`   | `"Arial"`  | フォント名 |
| `strip_exif`       | `"1"` / `"0"` | EXIFをカット(1=ON) |
> 初期化:未保存なら `resize=1200` をセット。  
> チェックの復元後、**ダウンロードボタンのラベル**も即時更新。
---
## 6. 依存ライブラリ(CDN / 動的読み込み)
- **EXIF読取**:`exifr`(UMD, `https://unpkg.com/exifr/dist/full.umd.js`)
- **EXIF移植(JPEGのみ)**:`piexifjs`(`https://unpkg.com/piexifjs`)
- **AVIFデコード**:`@jsquash/avif`(ESM, `https://unpkg.com/@jsquash/avif@1.3.0?module`)
- **JPEG XLデコード**:`@jsquash/jxl`(ESM, `https://unpkg.com/@jsquash/jxl@1.3.0?module`)
> `import()`(動的インポート)で必要時のみロード。  
> 互換性:**モダンなエバーグリーンブラウザ**を対象。古い環境はフォールバック不要で可。
---
## 7. サーバAPI 仕様(同一ファイル内)
### 7.1 認証
- `POST password` でログイン。正しければ `$_SESSION['logged_in']=true`。
- 間違いはエラーメッセージ表示のみ。
### 7.2 保存エンドポイント
- **URL**:`photo_uploader.php`(同一)  
- **Method**:`POST`  
- **Headers**:`multipart/form-data`  
- **Body**:
  - `imageData`(必須):`data:image/jpeg;base64,...`
  - `fileName`(任意):元名(拡張子不要でもよい)
  - `takenAt`(任意):ISO8601(例:`2025-09-07T12:34:56.000Z`)
- **Response**(`text/plain`):
  - 成功:保存**ファイル名**(例:`0906_IMG_1234.jpg`)
  - 失敗:`403`(未ログイン)/`500`(保存失敗)
---
## 8. 例外・制約・既知の注意点
- **AVIF/JXL**:EXIFの移植は**対象外**(UIチェックも非表示)。  
- **大画像**:WASMデコードは重い。UIは固まらないが、ユーザに待機が発生。  
- **JPEG品質**:ダウンロードは `0.9`、サーバ保存は `0.7` を目安(調整可)。  
- **ファイル名サニタイズ**:`[^a-zA-Z0-9_-]` を `_` 置換。  
- **重複回避**:`_1, _2...` 連番。  
- **タイムゾーン**:`takenAt` 解釈はサーバ側の `Asia/Tokyo` に依存。  
- **安全**:同一オリジン前提。公開時は**強い認証**や**CSRF対策**を別途検討。
---
## 9. 受け入れテスト(Acceptance)
1. **ログイン**:正しいPWでUI表示、誤りで警告。  
2. **JPEG読込**:透かし表示、ドラッグ移動、`strip_exif` OFFでJPEGダウンロード→**EXIF保持**。  
3. **PNG読込**:`strip_exif` ONでPNGダウンロード(EXIFなし)。  
4. **AVIF読込**:ネイティブ or WASMでプレビュー。`strip_exif` UIが**非表示**。保存はJPEGで成功。  
5. **JXL読込**:AVIFと同様。  
6. **撮影日→保存先**:`takenAt` 由来の `YYYY` ディレクトリに保存、ファイル名先頭に `MMDD_`。  
7. **重複名**:`_1` 連番で回避。  
8. **レスポンシブ**:  
   - 600px以下:フォームの `fontSize / resizeLength` が**横並び**  
   - 900px以上:フォームとプレビューが**左右**に分割、プレビューは**1.5倍**表示  
9. **ボタン文言**:`strip_exif` の状態で**PNG/JPEG**表示が切替。
---
## 10. 実装ガイド(LLM向け手順)
1. **PHP冒頭**
   - `session_start();`
   - `date_default_timezone_set('Asia/Tokyo');`
   - `if (POST imageData)` → 保存APIの処理(認証/デコード/ディレクトリ/ファイル名/`file_put_contents`)。
   - `if (POST password)` → 認証ハンドリング。
   - 認証チェック:未ログインは**ログインHTML**を返して `exit;`。
2. **UIページ(認証済み)**
   - `<style>`:カードUI、`.layout` コンテナ、レスポンシブ(600/900pxブレーク)、`.form-row/.form-col`。
   - `<body>`:  
     - `.layout` 内に **`.form-area`** と **`#displayCanvas`**  
     - `.form-area` にフォーム要素(上記)+ `#stripExifLabel`(チェック)  
     - 下部にボタン行(`#downloadButton`, `#uploadServerButton`)  
     - 非表示 `#saveCanvas`(保存用)
   - `<script>`:
     - 変数群:画像、座標、縮小率、ファイル状態、`currentTakenAt`。
     - `loadScript()` 汎用ローダ。
     - `updateDownloadButtonLabel()`:ボタン文言を切替。
     - `extractTakenAt(file)`:EXIF or lastModified。
     - AVIF/JXL用 `decode*WithWasm()`。
     - `currentDisplayScale()` / `applyDisplayScale()`:900px以上で1.5倍。
     - 画像ハンドラ `handleFileUpload()`:  
       - 種別判定、`stripExif` の表示切替、元JPEGならDataURL保持、ネイティブ→WASM。
     - `resizeAndRedraw()`:長辺基準で縮小→`resizedImg`→プレビューCanvas設定→描画。
     - 透かし描画系:`drawWatermarkOnDisplayCanvas()` / `drawWatermark()`。
     - ドラッグ系:`startDragging` / `dragWatermark` / `stopDragging`。
     - 設定保存:`localStorage`(上表)。
     - ダウンロード:PNG/JPEG切替、JPEG時はEXIF移植試行。
     - サーバ保存:JPEG生成→必要ならEXIF移植→`imageData`/`fileName`/`takenAt` をPOST。
     - リスナー初期化/`resize` で `applyDisplayScale()`。
---
## 11. 将来拡張(任意)
- **保存形式選択**(WEBP/JPEG/PNG切替)
- **シャドウ/外枠/縁取りの詳細設定**
- **撮影日オーバーライド**(手動修正欄)
- **ドラッグ時のガイド(中央線/端寄せスナップ)**
- **複数画像の一括処理**
---
## 12. 用語メモ
- **EXIFをカット**:メタデータを**付与しない/除去**する挙動全般(このツールではPNG出力や再エンコード無移植で実現)。
- **移植**:元JPEGのEXIFを新規JPEGへ**コピー**すること(`piexifjs`でDataURLに挿入)。
---
## 13. まとめ(LLMへの指示要約)
- この仕様に沿って**単一の `photo_uploader.php`** を生成。  
- **UI**・**描画**・**保存API**・**認証**を一体で実装。  
- 外部ライブラリは**必要時のみ動的読込**。  
- **AVIF/JXL**は**ネイティブ→WASM**の順でデコード。  
- **撮影年ディレクトリ**と**`MMDD_`ファイル名**を**`takenAt`から決定**。  
- **EXIFの挙動**(カット/移植/非表示条件)と**ボタン文言切替**を忠実に実装。  
- **レスポンシブ要件**(600px/900px)と**プレビュー1.5倍**を満たすこと。


ジャンル: IT カテゴリー: プログラム

← 記事一覧に戻る