pixivのうごイラをダウンロードする

2021/01/10 追記
2022/01/15 追記

---

#やったこと
pythonのライブラリに[pixivpy](https://github.com/upbit/pixivpy)があります。pixivの非公式APIなのですがとても便利です。非公式なので乱用は禁物です。
普通のイラストをダウンロードすることはできるのだが、うごイラをダウンロードすることはできません（多分）。
なので、うごイラをダウンロードするプログラムを書いてみました。

#環境
- os: windows10
- python: 3.6.4
- pixivpy: 3.3.4

#ソースコード
```python
# -*- coding:utf-8 -*-
from pixivpy3 import *
from  time import sleep
from PIL import Image
import os, glob

#ログイン処理
aapi = AppPixivAPI()
ID = 'YOUR_ID'
PASS = 'YOUR_PASS'
aapi.login(ID, PASS)

#イラストIDの入力待機
illust_id = int(input('>>'))

illust = aapi.illust_detail(illust_id)
ugoira_url = illust.illust.meta_single_page.original_image_url.rsplit('0', 1)
ugoira = aapi.ugoira_metadata(illust_id)
ugoira_frames = len(ugoira.ugoira_metadata.frames)
ugoira_delay = ugoira.ugoira_metadata.frames[0].delay
dir_name = str(illust_id)+'_ugoira'

#うごイラを保存するフォルダの作成
if not os.path.isdir(dir_name):
    os.mkdir(dir_name)

#うごイラに使われているすべての画像のダウンロード
for frame in range(ugoira_frames):
    frame_url = ugoira_url[0] + str(frame) + ugoira_url[1]
    aapi.download(frame_url, path=dir_name)
    sleep(2) #ダウンロードした後は一応2秒待機

#保存した画像をもとにgifを作成
frames = glob.glob(f'{dir_name}/*')
frames.sort(key=os.path.getmtime, reverse=False)
ims = []
for frame in frames:
    ims.append(Image.open(frame))
ims[0].save(f'{dir_name}/{illust_id}.gif', save_all=True, append_images=ims[1:], optimize=False, duration=ugoira_delay, loop=0)
```
#説明のようなもの
##うごイラについて
うごイラはgifや映像を再生しているのではなく一枚一枚の画像を連続して表示しているだけでした。なので、うごイラに使われている画像をすべてダウンロードしてgifに変換して1枚の画像にすることにしました。

##変数について
`illust_id`イラストID。
`illust`イラストの詳細情報。タイトル、タグ、画像のURLなどの情報。
`ugoira_url`うごイラの一枚目のURLを0を区切りに2分割したもの。
`ugoira`うごイラの詳細情報。zipファイルのURL（これを使ったほうが簡単かも？）、画像一枚一枚のファイル名とdelay。
`ugoira_frames`うごイラに使われている画像の枚数。
`ugoira_delay`delayの値が何なのかはよくわからないけど多分表示時間。gifを作るときに使う。
`dir_name`うごイラの画像を保存するフォルダの名前。[イラストID]_ugoira。

##画像のダウンロードについて
うごイラに使われている画像のURLは`https://(省略)/[イラストID]_ugoira[画像の番号].jpg`のようになっていて画像の番号は0から始まります。
そのため、URLの画像の番号を変えていくことですべての画像のURLを知ることができます。`ugoira_url`で0を区切りに2分割したのは画像の番号を変えやすくするためです。
for文で画像の枚数分回すことですべての画像を保存しています。

##gif作成について
gifを作るときはPILを使用しました。詳しい説明は「[Python, PillowでアニメーションGIFを作成、保存](https://note.nkmk.me/python-pillow-gif/)」を見てもらったほうがわかりやすいと思います。

#追記(2021/01/10)
@yuki_2020さんが「[pythonのpixivpyを使用してpixivからうごイラをmp4に変換してダウンロードする](https://qiita.com/yuki_2020/items/efe6eda433b09ec34203)」という記事を書いていました。
上の記事ではgif画像だときれいなうごイラにならないということでmp4に変換するという手法をとっています。
これに影響されて綺麗なうごイラを作ることに挑戦してみます。

##その1 APNGに変換する
GIF画像では256色までしか表現できないため、GIF画像に変換するのは諦めます。
GIF画像に代わるアニメーション画像として[APNG](https://ja.wikipedia.org/wiki/Animated_Portable_Network_Graphics)というものがあるみたいなのでこれを使うことにしました。

まずはライブラリをインストール

```
pip install apng
```

以下がソースコードになります。
(上で書いたソースコードに続くように書いています)

```python
from apng import APNG
APNG.from_files(frames, delay=ugoira_delay).save(f'{dir_name}/{illust_id}_apng.png')
```

これだけで済みます。簡単ですね。
ただ、画像を見るときはAPNG対応ブラウザまたはビューアで見る必要があり、Windows標準のビューアだとただの静止画として表示されます。

##その2 うごイラを再現する
どうせブラウザで見ることになるならAPNGに変換するのではなく、うごイラを再現すればいいじゃないという発想です。
うごイラは静止画を連続で表示しているだけなので再現はそこまで難しくありません。

以下がソースコードになります。

```python
html = """
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <canvas id="ugoira" width="{width}" height="{height}"></canvas>
    <script>
        const images = [];
        for(let i=0; i<{frames}; i++){{
            const img = new Image();
            img.src = `./{illust_id}_ugoira${{i}}.jpg`;
            images.push(img);
        }}
        const canvas = document.querySelector('#ugoira');
        const context = canvas.getContext('2d');
        let count = 0;
        window.addEventListener('load', function(){{
            setInterval(function(){{
                context.clearRect(0, 0, canvas.width, canvas.height);
                context.drawImage(images[count], 0, 0);
                count++;
                if(count>={frames}) count=0;
            }}, {delay});
        }});
    </script>
</body>
</html>
""".format(width=illust.illust.width, height=illust.illust.height, frames=ugoira_frames, illust_id=illust_id, delay=ugoira_delay)
with open(f'{dir_name}/ugoira.html', 'w', encoding='utf-8') as f:
    f.write(html)
```

うごイラを再現するコードを書いたhtmlを出力するようにしています。
やっていることとしてはcanvasにダウンロードした画像を連続して表示するようにしているだけです。

## その3 すべての画像データをHTMLにまとめる
いっそのこと全てをHTMLのみで完結させようという感じです。

以下がソースコードになります。

```python
import base64
illust_b64 = []
img_ext = frames[0].split('.')[-1]
for frame in frames:
    with open(frame, 'rb') as f:
        illust_b64.append(f'data:image/{img_ext};base64,{base64.b64encode(f.read()).decode()}')
html = """
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <canvas id="ugoira" width="{width}" height="{height}"></canvas>
    <script>
        const illust_b64 = {illust_b64};
        const images = [];
        for(let i=0; i<{frames}; i++){{
            const img = new Image();
            img.src = illust_b64[i];
            images.push(img);
        }}
        const canvas = document.querySelector('#ugoira');
        const context = canvas.getContext('2d');
        let count = 0;
        window.addEventListener('load', function(){{
            setInterval(function(){{
                context.clearRect(0, 0, canvas.width, canvas.height);
                context.drawImage(images[count], 0, 0);
                count++;
                if(count>={frames}) count=0;
            }}, {delay});
        }});
    </script>
</body>
</html>
""".format(width=illust.illust.width, height=illust.illust.height, frames=ugoira_frames, illust_id=illust_id, delay=ugoira_delay, illust_b64=str(illust_b64))
with open(f'{dir_name}/ugoira_onefile.html', 'w', encoding='utf-8') as f:
    f.write(html)
```

ダウンロードした画像をBase64に変換し、HTML内に書いておくことで画像を取っておく必要がなくなります。

## おわりに
目的だった綺麗なうごイラを作るというのは達成できたかなと思います。
ただ、専用ビューアが必要だったりブラウザで開く必要があったりとちょっと微妙な部分がありますね。
また何か思いついたら追記しようと思います。

#追記(2022/01/15)
うごイラはうごイラを構成する画像1枚1枚に異なるdelayが設定されていることがあるみたいなのでそれも再現したいと思います。
以前作成した「[その3 すべての画像データをHTMLにまとめる](#その3-すべての画像データをhtmlにまとめる)」に変更を加えます。
以下ソースコード

```python
illust_b64 = []
img_ext = frames[0].split('.')[-1]
for num, frame in enumerate(frames):
    with open(frame, 'rb') as f:
        illust_b64.append([f'data:image/{img_ext};base64,{base64.b64encode(f.read()).decode()}', ugoira.ugoira_metadata.frames[num]['delay']])

html = """
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <canvas id="ugoira" width="{width}" height="{height}"></canvas>
    <script>
        const illust_b64 = {illust_b64};
        const images = [];
        for(let i=0; i<{frames}; i++){{
            const img = new Image();
            img.src = illust_b64[i][0];
            images.push([img, illust_b64[i][1]]);
        }}
        const canvas = document.querySelector('#ugoira');
        const context = canvas.getContext('2d');
        let count = 0;
        const drawImage = (index) => {{
            if(index>={frames}) index=0;
            context.clearRect(0, 0, canvas.width, canvas.height);
            context.drawImage(images[index][0], 0, 0);
            setTimeout(drawImage, images[index][1], index+1)
        }};
        window.addEventListener('load', () => drawImage(0));
    </script>
</body>
</html>
""".format(width=illust.illust.width, height=illust.illust.height, frames=ugoira_frames, illust_id=illust_id, illust_b64=str(illust_b64))
with open(f'{dir_name}/ugoira_onefile.html', 'w', encoding='utf-8') as f:
    f.write(html)
```
ポイントとしては画像データと一緒にdelayのデータをHTMLファイルに持たせていることと、`setInterval`ではなく`setTimeout`を使っていることです。`setInterval`では同じ間隔でしか関数を実行できませんが、`setTimeout`を使うことで間隔を自由に設定することができます。