論文掲載用の図を余白をトリムしても意図した寸法にする方法

# はじめに

論文に掲載する図は大きさを著者が思った通りの大きさにしておいた方が組版の際にきれいに仕上がる可能性が高い。しかし、余白を削除すると、思った通りの大きさにならないことがあるので、そのことを分析し、解決策をメモします。

# 余白の削除
`fig.savefig`で`bbox_inches`を使うと、描画ファイルの外側の余白をtrimmingできます。しかし、それをすると、図の大きさが`plt.figure`で指定した`figsize`の通りになりません。trimmingのあるなしを比較します。
```python
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(6,4))
fig.savefig("original.png")
figure_size_check("original.png")
fig.savefig("trimmed.png",bbox_inches="tight",pad_inches=0)
figure_size_check("trimmed.png")
```

figure_size_checkは出力された画像のサイズを計測するためのもの。気になる人は、[画像サイズの計測](#画像サイズの計測)へジャンプ。出力は下記の通り

> [original.png]
(W: 100.000 dpi, H: 100.000 dpi)
(W:   6.000 in,  H:   4.000 in )
(W: 600.000 px,  H: 400.000 px )

> [trimmed.png]
(W: 100.000 dpi, H: 100.000 dpi)
(W:   5.030 in,  H:   3.340 in )
(W: 503.000 px,  H: 334.000 px )

trimmed.pngは(6 in, 4 in)の幅ではなく、(5.030 in, 3.340 in)になってしまいました。下記の赤い部分だけがくりぬかれたことになります。

![test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3457553/2af7d238-33f0-209c-f773-58a799c46231.png)

:::note info 
インチ(in)とポイント(pt)とセンチメートル(cm)
1 in = 72 pt  
1 in = 2.54 cm
1 cm = 28.346 pt
1 pt = 0.3528 mm
です。pythonのデフォルト設定の図のサイズは6 in × 4 in (15.24 cm x 10.10 cm)です。
:::

:::note warn
紙のサイズ
A4は横210 mm x 縦297 mm = 横8.27 in × 縦11.69 inなので、紙の余白を考えると、横幅6 inより大きな図は論文の図として適当ではありません。
:::

# リサイズする方法 (1)
`fig.tight_layout(pad=0)`を利用すると、問題を解決できます。ただし、この方法は`plt.subplots`を利用している場合に限定されます。

```python
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(6,4),dpi=100)
fig.tight_layout(pad=0.0)
fig.savefig("tight_layout.png")
figure_size_check("tight_layout.png")
```
これにより(6in x 4in)の図に調整されます。

> [tight_layout.png]
(W: 100.000 dpi, H: 100.000 dpi)
(W:   6.000 in,  H:   4.000 in )
(W: 600.000 px,  H: 400.000 px )

![tight_layout.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3457553/c009c7be-8989-bf91-de80-92dda8b8b73a.png)

# リサイズする方法 (2) 
`renderer = fig.canvas.get_renderer()`及び`fig.get_tightbox(renderer=render)`によってtrimmingした際の図の寸法を測ることができます。この寸法が与えられた寸法に一致するように、ずれている倍率を徐々に乗じていきます。この反復によって、与えらえた寸法に一致させます。

```python
def figure_adjustment(fig, pad_inches, eps=1e-15):
    import numpy as np
    
    print("Figure adjustment function")

    figX, figY = fig.get_size_inches()
    print("Given inches:", figX, figY)    
    
    figX -= pad_inches * 2.0
    figY -= pad_inches * 2.0
    fx, fy = 1.0, 1.0
    nt = 0
    error = 1.0
    
    renderer = fig.canvas.get_renderer()
    
    while error > eps:
        tight_render = fig.get_tightbbox(renderer=renderer)
        width = tight_render.width
        height = tight_render.height
        fx *= figX / width
        fy *= figY / height
        fig.set_figwidth(figX * fx)
        fig.set_figheight(figY * fy)
        error = np.sqrt((figX-width)**2 + (figY-height)**2)
        print("iter={0:3d}: (W:{1:15.13f}in, H:{2:15.13f}in)".format(nt, width, height))
        nt += 1
        if nt > 50: break 
    
    tight_render = fig.get_tightbbox(renderer=renderer)
    width = tight_render.width + 2.0 * pad_inches
    height = tight_render.height + 2.0 * pad_inches
    print("final figure size:(W:{0:6.3f}in, H:{1:6.3f}in)".format(width, height))
    print("final figure size:(W:{0:6.0f}px, H:{1:6.0f}px)".format(width * fig.dpi, height * fig.dpi))
    
    print(tight_render.width,tight_render.height)
    
    return 
```

# プログラムの使い方
```python
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(6,4),dpi=100)
figure_adjustment(fig, pad_inches=0)
fig.savefig("resized.png",bbox_inches="tight",pad_inches=0)
figure_size_check("resized.png")
```

出力結果は下記の通り。
>Figure adjustment function
Given inches: 6.0 4.0
iter=  0: (W:5.0378472222222in, H:3.3422222222222in)
iter=  1: (W:5.9259270347524in, H:3.9483924349882in)
iter=  2: (W:5.9951519832012in, H:3.9965726176909in)
iter=  3: (W:5.9996863649405in, H:3.9997751233641in)
iter=  4: (W:5.9999797251924in, H:3.9999852572584in)
iter=  5: (W:5.9999986894076in, H:3.9999990335278in)
iter=  6: (W:5.9999999152817in, H:3.9999999366424in)
iter=  7: (W:5.9999999945237in, H:3.9999999958466in)
iter=  8: (W:5.9999999996460in, H:3.9999999997277in)
iter=  9: (W:5.9999999999771in, H:3.9999999999822in)
iter= 10: (W:5.9999999999985in, H:3.9999999999988in)
iter= 11: (W:5.9999999999999in, H:3.9999999999999in)
iter= 12: (W:6.0000000000000in, H:4.0000000000000in)
iter= 13: (W:6.0000000000000in, H:4.0000000000000in)
iter= 14: (W:6.0000000000000in, H:4.0000000000000in)
final figure size:(W: 6.000in, H: 4.000in)
final figure size:(W:   600px, H:   400px)
6.0 4.0

[resized.png]
(W: 100.000 dpi, H: 100.000 dpi)
(W:   6.000 in,  H:   4.000 in )
(W: 600.000 px,  H: 400.000 px )

# 出力された画像ファイル
6 in×4 inの画像が完成しました。
![resized.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3457553/7a1d22c9-a463-d88a-a7e6-f2dbe3d34a3f.png)

# 画像サイズの計測
```python
def figure_size_check(filepath):
    from PIL import Image
    with Image.open(filepath) as img:
        width, height = img.size
        dpi = img.info.get("dpi")

    print("\n[{:s}]".format(filepath))
    print("(W:{0:8.3f} dpi, H:{1:8.3f} dpi)".format(*dpi))
    print("(W:{0:8.3f} in,  H:{1:8.3f} in )".format(width / dpi[0], height / dpi[1]))
    print("(W:{0:8.3f} px,  H:{1:8.3f} px )".format(width, height))
    
    return