スプレッドシートのグラフを画像で取得してPDF変換するまでを自動化してみた（ブラウザ完結）

Googleスプレッドシートで作成したグラフを自動で

- 画像として取得
- PDFに変換

これができるブラウザ完結型プログラムを作成してみました。

# なぜ作ろうと思ったか

大学の論文用にグラフをスプレッドシートで作った。
しかし、以下の問題点があった。

- **スプレッドシートには作ったグラフを直接画像に変換する機能はない**
- **研究室から、Latex(論文書くやつ)にグラフ読み込む際はPDFにするよう指定された**

スプレッドシートをhtmlとして保存すれば
そのhtmlを開いて、グラフを右クリックで画像として保存できるみたいだが、

そんなめんどくさいことを毎回するなんて絶対イヤだった。

そんなわけで、画像に変換し、さらにPDFに変換するとこまで**全部自動化**することにした。

# 完成品の動作デモ

**完成品だけ見せてよって方がいると思うので先にお見せします**

GIFで完成したやつの動作デモを置いときます
![asd](https://i.gyazo.com/6a7224d78ed7bee31c99d62f53039a23.gif)

流れとしては

1. スプレッドシートのページでコンソールにコマンド入力
2. 画像に変換され、「画像をPDFに変換できるサイト」に飛ぶ
3. 自動で画像ドラッグ＆ドロップイベントを発生させ、PDFに変換
4. PDFをダウンロードして完了

コマンド入力が面倒に感じる場合はボタンでも設置して押したら実行するようにすればいいかなと。

# どうやって実現したか

とりあえず、全部ブラウザだけで完結させたいと思った。なので以下のものを用意

- Tampermonkey(サイト上で自動で任意のjsコードを実行できる拡張機能）
- png2pdf.com（ブラウザ完結させたいのでオンラインPDF変換サイトを利用）

あとはコードを書いていくだけ。

### グラフを画像に変換する

スプレッドシート内のグラフはhtml5のCanvasAPIで描かれていることがわかった。
Canvasには元々描画されている内容を画像URLとして出力する機能がある。
`toDataURL()`というメソッド？を使用する。

グラフのcanvasを指定して、`toDataURL()`を実行すれば画像URLが得られる。

```javascript     
function getGraphImages() {
        const canvasEls = document.getElementsByTagName("canvas");
        let result = []

        for (let i = 0; i < canvasEls.length; i++) {
            const canvas = canvasEls[i];
            if (canvas.parentElement.className.includes("chart")) {
                const imageURL = canvas.toDataURL();
                console.log(imageURL);
                result.push(imageURL);
            }
        }

        return result;
}
```

ちょっと余計なグラフまで取得するバグがあるけど、
とりあえずclassNameに`"chart"`が含まれてるcanvasだけ取得して画像URLを得た。

### PDF変換サイトで画像をPDFに変換

あとは取得した画像をPDFに変換するだけだが、ここで問題があった。
画像をPDFに変換するサイトには「画像URLをPDFに変換する機能」はない。

直接そのサイトで使われているAPIを叩くにしても、解析するのがめんどくさい。

ということで、もうこれは力技でいくしかないということで以下の手順ですることに。

1. png2pdf.comに遷移するときに画像のDataURLをURLにハッシュとしてくっつける
1. 画像URL（Data URL)を`File`オブジェクト？に変換
2. 変換サイトで無理やりドラッグ＆ドロップイベントを発火させ、ファイルをコンバートさせる
3. PDFをダウンロード

#### 画像DataURLを保持したままPDF変換サイトに遷移

スプレッドシートから得たグラフ画像のData URLを保持したまま、PDF変換サイト（png2pdf.com）に遷移したかったので
最適な方法かはわからないけど、とりあえずhashとしてくっつけた。
（そうすれば遷移した後に`location.hash`すれば画像Data URLを取得できるので）

なので遷移後のURLはこんな感じになる

```
https://png2pdf.com/ja/#data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABL...
```

新しいタブで遷移は以下を参考にしました
https://qiita.com/TK-C/items/c64ca54b634b0cae0059

#### 画像DataURLをFileオブジェクトに変換

コードはstackoverflowのアンサーから拝借しました。
https://stackoverflow.com/questions/16968945/convert-base64-png-data-to-javascript-file-objects/16972036

```javascript
    function dataURLtoFile(dataurl, filename) {
        var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
            bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
        while(n--){
            u8arr[n] = bstr.charCodeAt(n);
        }
        return new File([u8arr], filename, {type:mime});
    }
```

#### JavaScriptでドラッグ＆ドロップイベントを発火

JavaScriptからファイルのドラッグ＆ドロップイベントを発火させる方法は@anqooqieさんの記事を参考させていただいた。

[JavaScript] ファイルのドロップイベントをJavaScriptから発火させる
https://qiita.com/anqooqie/items/c72508810a5be022e03d

イベント発火系は普段扱わないので、非常に参考になりました。ありがとうございます。


# 完成したコード

あとはこれまでの流れを自動で実行できるようにTampermonkeyでスクリプトを読み込んで完了。

需要ないと思うけどコード置いておきますね

ちなみに　**使い方** はスプレッドシートのページで`window.convertToPDF()`コマンドをコンソール上で実行するだけです。

```javascript
// ==UserScript==
// @name         Spreadsheet Graph Downloader + PDF Converter
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       LostMyCode
// @match        https://docs.google.com/spreadsheets/d/*
// @match        https://png2pdf.com/ja/*
// @icon         https://www.google.com/s2/favicons?domain=google.com
// @grant        none
// ==/UserScript==

// Created by LostMyCode (https://github.com/LostMyCode)

(function () {
    'use strict';

    function getGraphImages() {
        const canvasEls = document.getElementsByTagName("canvas");
        let result = []

        for (let i = 0; i < canvasEls.length; i++) {
            const canvas = canvasEls[i];
            if (canvas.parentElement.className.includes("chart")) {
                const imageURL = canvas.toDataURL();
                console.log(imageURL);
                result.push(imageURL);
            }
        }

        return result;
    }

    function convertToPDF() {
        const imageDataURLs = getGraphImages();
        const converter = "https://png2pdf.com/ja/";
        let hash = "#";

        for (let i = 0; i < imageDataURLs.length; i++) {
            const dataURL = imageDataURLs[i];
            hash += dataURL;
            if (i != imageDataURLs.length - 1) {
                hash += "**"
            }
        }

        // location.href = converter + hash;
        window.open(converter + hash, '_blank')
    }

    window.getGraph = getGraphImages;
    window.convertToPDF = convertToPDF;

    function fireDropEvent(domElement, files) {
        // DataTransferの偽物を定義する
        class FakeDataTransfer {
            constructor(files) {
                this.dropEffect = "none";
                this.effectAllowed = "all";
                this.files = files;
                this.types = ["Files"];
            }
            addElement() {
                // do nothing
            }
            clearData() {
                // do nothing
            }
            getData() {
                return "";
            }
            setData() {
                // do nothing
            }
            setDragImage() {
                // do nothing
            }
        }

        const event = new DragEvent("drop");
        Object.defineProperty(event, "dataTransfer", { value: new FakeDataTransfer(files) });
        domElement.dispatchEvent(event);
    }

    function dataURLtoFile(dataurl, filename) {
        var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
            bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
        while(n--){
            u8arr[n] = bstr.charCodeAt(n);
        }
        return new File([u8arr], filename, {type:mime});
    }

    console.log("Graph Downloader loaded.");

    if (location.host == "png2pdf.com") {
        const target = document.getElementById("filesScroll");
        const dataURLs = location.hash.replace(/#/, "").split("**");
        console.log(dataURLs)
        let files = dataURLs.map((v, i) => dataURLtoFile(v, i + ".png"));
        console.log(files)
        fireDropEvent(target, files);
    }
})();
```

# おわりに

ぶっちゃけ大学の研究の論文書くのがいやで、現実逃避するためにこんなことやってます笑
Qiitaのこの記事も卒業論文書く片手間書いております

読んでいただきありがとうございました。

Github
https://github.com/LostMyCode