jsPDFでiOS Safariでもダウンロードダイアログを出してPDF保存（同じタブで開かないでくれ!!）

## 結論
```js
// jsPDFのインスタンスを作成
const pdf = new jsPDF()

// ～ おのおのPDFをイイカンジに作成してください ～

// PDFをBlob形式で取得
const blob = pdf.output("blob");

// typeがpdfだとiOSでダウンロードできないので、application/octet-streamに変更
const newBlob = new Blob([blob], { type: 'application/octet-stream' })

// Blobを使って一時的なURLを生成
const url = URL.createObjectURL(newBlob);

// ダウンロードリンクを作成
const link = document.createElement("a");
link.href = url;
link.download = "hogehoge.pdf";

// ダウンロードを開始
link.click();

// メモリリークを防ぐため、URLを解放
URL.revokeObjectURL(url);
```

## 困ったこと
jsPDFはフロントエンドでお手軽にPDFを作成できるありがたいライブラリです。
PDFをダウンロードしたいときは、save()を使えばOKです。

PCのChromeではそのままダウンロードが開始され、iPhoneのChromeではダウンロードしますか?のダイアログが表示されました。
しかしiOS(17.5.1) Safariで確認したところ、いきなり同じタブでPDFが開かれました。

それだけでも普通に迷惑なのですが、今回フロントエンドはSPAだったため、別ページに遷移されては非常に困ります。なんとかせねばなりません。

## やりたいこと
今いるタブ内で表示されては困るので、以下どちらかの挙動にしたいところです。
- 別タブで開く
- ブラウザネイティブのダウンロードダイアログを表示
（ユーザー操作で任意にダウンロード）

## 別タブで開いてみる
色々調べていたところ、`window.open()`で開けるよという説を見つけました。
結論、これはできませんでした。

iPhoneのChromeでは「ポップアップがブロックされました」の表示が出てユーザーが操作できるのですが、Safariでは何も表示されず無視されました。

ユーザーが設定を変更すれば許可もできるようですが、デフォルトで開けないのであれば意味がありません。

以前はできていたのであれば、Safariのセキュリティが更新されたことでできなくなったと思われます。

## ブラウザネイティブのダウンロード機能をなんとか使う
以前はiPhoneでファイルを端末にダウンロードすること自体ができなかったようなのですが、現在は可能です。
Chromeでは既に問題なくPDFダウンロードができていたため、Safariの挙動を調べまくりました。

### 画像は普通にダウロードできるようだ
ユーザー操作起因ではなく、JSでファイルをダウンロードさせることにも１つ障害がありそうだと調べていたとき、同様の検証をしている方がいらっしゃいました。

[JavaScriptの力でユーザーのクリック無しでファイルをダウンロードできるか検証する 📦 - みかづきブログ・カスタム](https://blog.kimizuka.org/entry/2021/11/09/203127)

ページを開いただけで、JSで画像をダウンロードさせるという内容です。
シンプルな実装なので、はいはいと思いながらiPhoneのSafariでページを開いてみたところ

え、ちょ、いけてる！！

![2024-08-15_13-54-49_0001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189779/a8f0d081-90e2-219e-0486-defa8af04108.png)

ページを開いた途端ダウンロードダイアログが出る！！！！

これこれこれこれこれこれがしたいのよ！！！！！！

とテンション爆上がりつつ、同じ記述でPDFファイルを試してみたら、できない。
今までの挙動と変わらない。

いやできないんか～～～いといろいろ検証してみたところ、PDFファイルだとダメみたいでした。は～い。

### PDFとして扱われないようにする
実際PDFなのだからPDFとして扱われるのは正しいのですが、PDFであることがネイティブのダウンロードダイアログが出ない原因っぽいということが分かりました。

ここで解決の糸口となるjsPDFのissueにあたりました。

[output/save function not working on safari mobile · Issue #3059 · parallax/jsPDF](https://github.com/parallax/jsPDF/issues/3059)

スマホのSafariで、jsPDFのoutput・save機能がうまくいかないんだが？というissueですね。
まさに今の状況です。

数年前のissueということもあり現在の挙動とは少し違ったのですが、[こちらのコメント](https://github.com/parallax/jsPDF/issues/3059#issuecomment-790672201)で解決に至りました。

重要な手順が以下の2点です。
1. `const blob = pdf.output("blob");`
jsPDFのoutputを使い、PDFデータをblob型で受け取る
1. `const newBlob = new Blob([blob], { type: 'application/octet-stream' })`
PDFデータのバイナリ形式には自動でapplication/pdfが適用されているので、application/octet-streamを指定して新たなblobを作成する

バイナリデータについてまったく詳しくないので調べたところ、application/octet-streamはファイル形式が不明な場合に使われるMIMEタイプとのこと。

[application/octet-streamとは - 意味をわかりやすく - IT用語辞典 e-Words](https://e-words.jp/w/application-octet-stream.html)

この手順を踏むことで、ブラウザネイティブのダウンロードダイアログを表示＆PDFを端末に保存できました！

Issueのコメントでは、この方法だとファイル名が決められなかったり拡張子を手動で変えないとPDFにならなかったりなどの問題が挙げられていましたが、
試してみたところ問題なく指定のファイル名かつPDFファイルとして扱われていました。

このへんはそのときのブラウザの仕様によるので、都度検証してみるしかないかなと思います。

## オマケ
サーバーサイドであれこれできるのであれば、もっと簡単にやれるはずです。
検証されていた方の記事を紹介しておきます！
[【ブラウザ別】PDFダウンロードの動き＆強制ダウンロード harublog](https://harublog.org/programming/pdf-javascript-download/)

## おわり
今回の件はとっっても困りましてもうダメかと思いました。
いったん解決したものの、ブラウザの挙動が変わったらまたあかんくなってしまうかもしれません。

一応本当にムリだったら、PDFではなく画像で出力ではダメですか？など相談しようかなと考えていました。

iOS Safari以外はただsave()するだけで問題なかったので、勘弁して～と思ったものの
スマホユーザーをターゲットにしたサービスだったため、切ることはできず……。

でかい声で参考にしてください!! と言えるような対応方法ではないのですが、
なにかの助けになったら嬉しいです👍️

## 参考
[parallax/jsPDF: Client-side JavaScript PDF generation for everyone.](https://github.com/parallax/jsPDF)
[JavaScriptの力でユーザーのクリック無しでファイルをダウンロードできるか検証する 📦 - みかづきブログ・カスタム](https://blog.kimizuka.org/entry/2021/11/09/203127)
[output/save function not working on safari mobile · Issue #3059 · parallax/jsPDF](https://github.com/parallax/jsPDF/issues/3059)
[application/octet-streamとは - 意味をわかりやすく - IT用語辞典 e-Words](https://e-words.jp/w/application-octet-stream.html)