論文ジャーナルのpdf全部画像化して要覧にするやつ

　ジャーナルの論文（全部で100報超）から何報かピックアップするということがあって、最初は個別ページからとんでいたのですが、何十回もクリックしている内に虚無を感じたので、いっそ全部画像化して一覧できないか、ということでそのようなスクリプトを組みました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/3170fb87-bda5-ac26-4905-3204081697e1.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/96cc783b-27a0-4a0b-cefe-a964d1a5ccf0.png)

　こんな感じで画像を取得した後、とりあえずタイトルと雰囲気だけを掴むために、各論文ごとにこのようなトリミングを行いました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/b08aaecb-769b-aee4-5cff-ad8c708c264d.png)

　以下の手順からなっています。

- pdfのダウンロード
- pdfの画像化
- 先頭ページと概要の抽出
- 要覧の作成

# pdfのダウンロード

　対象としたはの[このページ](https://www.siggraph.org/wp-content/uploads/2020/08/ACM-Transactions-on-Graphics-Volume-39-Issue-4-1.html)です。

```python
import re
import requests
import pickle
import os
import cv2
import numpy as np
from pdf2image import convert_from_path
from PIL import Image

def source_to_url():
    f = open('source.txt', mode='r', encoding='utf-8')
    text = f.read()
    pattern = r'title="Full Citation in the ACM Digital Library" href="(.*?)">(.*?)</a>'
    reseult = re.findall(pattern, text)
    f = open('url_title_list.pickle', mode='wb')
    pickle.dump(reseult, f)
```

　ソースコードから全論文のURLを抽出します。論文へのリンクは`<a class="DLtitleLink" title="Full Citation in the ACM Digital Library" href="https://dl.acm.org/doi/abs/##.####/#######.#######">hoge</a>`というhtmlタグで囲われていますので、それにヒットするような正規表現で`findall`すればURLのリストが取得できます。別のページでも、正規表現を工夫すれば対応できる範囲かと思われます。

　一度スクレイピングしたら`pickle`でローカルファイルに保存しておきます。

```python
def url_to_pdf():
    f = open('url_title_list.pickle', mode='rb')
    url_title_list = pickle.load(f)

    for url_title in url_title_list:
        url = url_title[0]
        url = url.replace('/abs/', '/pdf/')
        title = url_title[1]
        title = title.replace(':', '')  # これをしないとファイル名エラーになるので注意
        get = requests.get(url)
        f = open(f'./pdfs/{title}.pdf', mode='wb')
        f.write(get.content)
        f.close()
```

　個別ページから論文本体のpdfへのリンクが取得できます。今回は、幸運にも個別ページの`abs`を`pdf`に変えるだけでよかったのでそのような処理にしていますが、そうでない場合には正規表現などで抽出します。あらかじめ用意しておいた`pdf`フォルダに以下のようなファイル達が出力されます。注意点として、`:`や`?`はファイル名に使えないのでエスケープしておく必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/3deed4dc-344d-eaf2-6f9e-492d2b0b1cd5.png)

- pdfの画像化

　`pdf2image`というそのものズバリのライブラリがあったので使わせていただきました。参考にしたページでは、`poppler`というツールも合わせてインストール必要ということでしたが、私の環境では`pip install pdf2image`をするだけでいけました。バージョンは以下の通りです。

```
Python 3.7.4

pdf2image                1.14.0
pip                      21.0.1
```

```python
def any_exist(title):
    current_dir = os.listdir('./pdf_images/')
    for c in current_dir:
        if title in c:
            return True
    return False

def pdf_to_image():
    pdf_list = os.listdir('./pdfs/')
    for pdf_name in pdf_list:
        title = pdf_name[:-4]
        if any_exist(title):
            continue
        pdf_path = f'./pdfs/{pdf_name}'
        convert_from_path(pdf_path,
                          output_folder='pdf_images', fmt='png', output_file=pdf_name[:-4])
```

　pdfフォルダにあるpdfファイルに、それぞれ`convert_from_path()`でファイルを出力していきます。予め用意しておいたpdf_imagesというフォルダに以下のようなpngファイル達が量産されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/3170fb87-bda5-ac26-4905-3204081697e1.png)

　`any_exist`という自作関数は、すでに画像が生成済の場合はスキップするような処理です。スクリプトを試している間に、時間削減のために必要になったので書いたものをそのまま起きましたが、なくてもまあ問題はないと思います。一つの関数内に収めるのも不可能ではないのですが、2重以上のループを遡ってbreakするにはフラグ変数を別に用意したりと結構めんどくさいので、スッキリ書くためにこうしました。

# 先頭ページと概要の抽出

　次に、論文の雰囲気がひと目でわかるようにタイトルからメインの図（大体の場合は、論文の最初にある）を含む部分を抽出するようにしました。

```python
def triming_top(img):
    img_hsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
    for h in range(2200):
        for w in range(1700):
            if img[h][w][2] < 255:
                print(h)
                return h
    return 550 #白黒のみだった場合は適当に4分の1


def triming_bottom(img):
    img = img[::-1, :, :]
    img_hsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
    for h in range(2200):
        for w in range(1700):
            if img_hsv[h][w][1] > 0:
                print(2200-h)
                return 2200-h
    print(h)
    return 550 #白黒のみだった場合は適当に4分の1


def extract_initial_pages():
    image_list = os.listdir('./pdf_images')
    for img_name in image_list:
        if img_name[-6:-4] != '01' and img_name[-6:-4] != '-1':
            continue
        img = Image.open(f'./pdf_images/{img_name}')
        img = np.array(img)
        img = img[triming_top(img):triming_bottom(img), :, ::-1]
        if img_name[-6:-4] != '01':
            title = img_name[:-10]
        else:
            title = img_name[:-11]
        print(title)
        cv2.imwrite(f'./initial_pages/{title}.png', img)
```

　pdf_imagesフォルダにが、1論文あたり大量の画像が生成されているので、その中から先頭ページを見つけ出す必要があります。pdf2imageの仕様から`~0001-01.png`となっているのでそれを抽出すればよい……とはならずに、出力枚数が10枚未満の場合、`0001-1.png`となってしまうので場合分けする必要があります。最初これに気づかずに一部の論文をロストしていました。システム的な親切が裏目に出てしまった。勝手に流用しているこちらに文句を言える義理はないのですが。

　対策としては、マジックナンバーを使った愚直なif分岐でやりましたが、もっとかっこよく書けるような気もします。これを書いた時はけっこう疲れていたので動けばいいや精神でやりました。

　抽出部分の上端と下端については、それぞれ`triming_top`と`triming_bottom`という自作関数を用意しました。

　`triming_top`では、上の余白を削りたいので、画像を行→列を舐めていって、明度が`255`以下になるようなピクセルを検出した時点でその行をトリミング開始地点とします。`triming_bottom`では、メインの図を含む部分から下を削りたいので、メインの図が終わる行を検出したいです。下の行から行→列を舐めていって、彩度が`0`以上になるようなピクセルを検出した時点でその行をトリミング終了地点とします。

　以上のような処理をOpenCVを使って行っています。forで二重ループを回しているので、秒単位ですがけっこう時間がかかります。恐らくよりスマートに書く手段があると思われますが、これを書いた時はけっこう疲れていたので動けばいいや精神でやりました。

　ところで、OpenCVだけを使えば済むような状態でわざわざ`PIL`を使って画像を読み込んでいるのは、`cv2.imread`で特定の文字が読み取れないというエラーが起こるためです。日本語の文字が使えないのは有名ですが、なんと今回はフランス語でもエラーが出ました。具体的には、ベジェ曲線（B **é** zier curve）でエラーが起こりました。OpenCVは結構好きなのですが、このようなことが続くと嫌いになりそうです。

　結果として、予め用意したinitial_pagesフォルダに、以下のようなpng達が出力されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/b08aaecb-769b-aee4-5cff-ad8c708c264d.png)

　脚注等に色付きの文字がある場合は、ほぼページ全てを抽出してしまいますが、おおむねいい感じにできています。

# 要覧の作成

　ここまできたらやりたいことはほぼ100%できていますが、せっかく画像が出来たので、タイトルと抽出画像を並べて要覧化します。自分が見られればいいので、適当にhtmlファイルを生成します。

```python
def make_html():
    f = open('url_title_list.pickle', mode='rb')
    url_title_list = pickle.load(f)
    text = ""
    for url_title in url_title_list:
        title = url_title[1]
        title = title.replace(':', '')
        print(title)
        text += f'<p>{title}</p>'
        text += f'<p><img src="./initial_pages/{title}.png" width="100%" height="77%">'
    
    f = open('index.html',mode='w',encoding='utf-8')
    f.write(text)
```

　以下のようなhtmlファイルが生成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/18b38b6e-0d98-8a1d-1bfd-c88053bf2003.png)

　後はこれを高速スクロールしていけば、とりあえず論文の雰囲気だけは総覧できます。

# ソースコード全文

```python
import re
import requests
import pickle
import os
import cv2
import numpy as np
from pdf2image import convert_from_path
from PIL import Image


def source_to_url():
    f = open('source.txt', mode='r', encoding='utf-8')
    text = f.read()
    pattern = r'title="Full Citation in the ACM Digital Library" href="(.*?)">(.*?)</a>'
    reseult = re.findall(pattern, text)
    f = open('url_title_list.pickle', mode='wb')
    pickle.dump(reseult, f)


def url_to_pdf():
    f = open('url_title_list.pickle', mode='rb')
    url_title_list = pickle.load(f)

    for url_title in url_title_list:
        url = url_title[0]
        url = url.replace('/abs/', '/pdf/')
        title = url_title[1]
        title = title.replace(':', '')  # これをしないとファイル名エラーになるので注意
        get = requests.get(url)
        f = open(f'./pdfs/{title}.pdf', mode='wb')
        f.write(get.content)
        f.close()


def any_exist(title):
    current_dir = os.listdir('./pdf_images/')
    for c in current_dir:
        if title in c:
            return True
    return False

def pdf_to_image():
    pdf_list = os.listdir('./pdfs/')
    for pdf_name in pdf_list:
        title = pdf_name[:-4]
        if any_exist(title):
            continue
        pdf_path = f'./pdfs/{pdf_name}'
        print(pdf_path)
        convert_from_path(pdf_path,
                          output_folder='pdf_images', fmt='png', output_file=pdf_name[:-4])


def triming_top(img):
    img = img[:, :, :]
    img_hsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
    for h in range(2200):
        for w in range(1700):
            if img[h][w][2] < 255:
                print(h)
                return h
    return 550 #白黒のみだった場合は適当に4分の1


def triming_bottom(img):
    img = img[::-1, :, :]
    img_hsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
    for h in range(2200):
        for w in range(1700):
            if img_hsv[h][w][1] > 0:
                print(2200-h)
                return 2200-h
    print(h)
    return 550 #白黒のみだった場合は適当に4分の1


def extract_initial_pages():
    image_list = os.listdir('./pdf_images')
    for img_name in image_list:
        if img_name[-6:-4] != '01' and img_name[-6:-4] != '-1':
            continue
        img = Image.open(f'./pdf_images/{img_name}')
        img = np.array(img)
        img = img[triming_top(img):triming_bottom(img), :, ::-1]
        if img_name[-6:-4] != '01':
            title = img_name[:-10]
        else:
            title = img_name[:-11]
        print(title)
        cv2.imwrite(f'./initial_pages/{title}.png', img)

def make_html():
    f = open('url_title_list.pickle', mode='rb')
    url_title_list = pickle.load(f)
    text = ""
    for url_title in url_title_list:
        title = url_title[1]
        title = title.replace(':', '')
        print(title)
        text += f'<p>{title}</p>'
        text += f'<p><img src="./initial_pages/{title}.png" width="100%" height="77%">'
    
    f = open('index.html',mode='w',encoding='utf-8')
    f.write(text)

    
source_to_url()
url_to_pdf()
pdf_to_image()
extract_initial_pages()
make_html()
```