CLIP STUDIO PAINTのファイルからNode.jsでサムネイル画像を出力する

CLIP STUDIO PAINTファイル（.clip）のサムネイル画像をNode.js上で出力してみました。

[CLIPSTUDIO PAINTのファイルからサムネイル画像を取得](https://qiita.com/gutti02/items/f021d18074323009c8ce)

[CLIP STUDIO PAINTで作成した漫画データのサムネール出力の為の調査](https://qiita.com/ariela/items/71986b1bf65a566d2e75)

こちらの記事のように、Pythonやシェル、Goで実装されたライブラリ等はあったものの、JavaScript(TypeScript)で実装されたものが無かったので調べてみました。

# 概要

* clipファイルからSQLite部分のデータを切り出し
* 切り出したSQLiteファイルからサムネイル画像のデータを抽出

次節から実装になりますが、ファイルの読み書きに`fs.readFileSync/writeFileSync`を使用しています。
また、単なる出力テストなので、実装する場合は`fs.readFile/writeFile`の使用及び適宜例外処理をお願いします。

# コード

とりあえず全体像。TypeScriptで記述しています。
外部モジュールとしてsqlite3をインストールしてください。

```ps1
$ npm i sqlite3
```

```typescript
import * as fs from "fs"
import * as sqlite3 from "sqlite3"

// .clipファイルパス
const path = "../sample_asset/illust1"
const clipPath = path + ".clip"
// Bufferの読み込み
const clipBuffer = fs.readFileSync(clipPath)

// SQLiteのデータは「SQLite format 3」から始まるらしい
const searchText = "SQLite format 3"
// Uint8Arrayに変換
const uint8Array = new Uint8Array(Buffer.from(searchText))

// clipからSQLiteのデータのスタート位置を取得
const findIndex = clipBuffer.indexOf(uint8Array)

// SQLite部分のみ切り出し
const dbBuf = clipBuffer.slice(findIndex, clipBuffer.length)

// 一時書き出し用のSQLiteファイルパス
const dbPath = path + ".sqlite"
// いったんsqliteファイルに書き出し
fs.writeFileSync(dbPath, dbBuf)

// sqlite3モジュールでSQLiteファイルを展開
const db = new sqlite3.Database(dbPath)
db.serialize(() => {
  // CanvasPreviewテーブルからImageData（png）を抽出
  db.all("SELECT ImageData FROM CanvasPreview", (err, row) => {
    if (err) {
      console.log(err)
      return
    }

    if (row[0].length > 0) {
      // png出力パス
      const binaryPath = path + ".png"
      // rowのImageDataプロパティからpngを生成
      fs.writeFileSync(binaryPath, row[0].ImageData)
    }
  })
})
```

# 解説

### clipファイルの読み込み

```typescript
// .clipファイルパス
const path = "..\\sample_asset\\illust1"
const clipPath = path + ".clip"
// Bufferの読み込み
const clipBuffer = fs.readFileSync(clipPath)
```

`fs.readFileSync`でclipファイルの読み込みを行う。
`fs.readFileSync`は`Buffer`を返す。

### SQLiteのデータの位置を探索

clipファイルは何らかのメタデータ＋SQLiteで構成されているらしく、
そのままでは下記のとおりSQLiteとして展開できない。

```ps1
// PowerShellで確認
$ C:\...> sqlite3 illust1.clip
SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.

// テーブルリストを出力
$ sqlite> .table
Error: file is not a database
```

そこで、SQLiteのデータだけ切り出してやる必要がある。
噂によるとSQLiteのデータは「SQLite format 3」から始まるらしい。

```typescript
// SQLiteのデータは「SQLite format 3」から始まるらしい
const searchText = "SQLite format 3"
// Uint8Arrayに変換
const unit8Array = new Uint8Array(Buffer.from(searchText))

// clipからSQLiteのデータのスタート位置を取得
const findIndex = clipBuffer.indexOf(uint8Array)

// SQLite部分のみ切り出し
const dbBuf = clipBuffer.slice(findIndex, clipBuffer.length)

// 一時書き出し用のSQLiteファイルパス
const dbPath = path + ".sqlite"
// いったんsqliteファイルに書き出し
fs.writeFileSync(dbPath, dbBuf)
```

`Buffer#indexOf`にて、SQLiteのインデックスを取得する。
インデックスが分かったら、そこから終点まで`slice`してやる。

取得したバッファを`fs.writeFileSync`でSQLiteファイルとして書き出し。

### SQLiteの展開とpngの書き出し

SQLiteファイルのテーブルは次のようになっており、
CanvasPreviewテーブルにプレビュー画像のデータが保持されている。

```ps1
// PowerShellで確認
$ C\...> sqlite3 illust1.sqlite //←上で生成したやつ
$ sqlite3> .table
AnimationCutBank            Layer
Canvas                      LayerThumbnail
CanvasItem                  Mipmap
CanvasItemBank              MipmapInfo
CanvasPreview               Offscreen
ElemScheme                  ParamScheme
ExternalChunk               Project
ExternalTableAndColumnName  RemovedExternal
```

sqlite3モジュールにより、先ほど生成したSQLiteファイルを展開する。

```typescript
// sqlite3モジュールでSQLiteファイルを展開
const db = new sqlite3.Database(dbPath)
db.serialize(() => {
  // CanvasPreviewテーブルからImageData（png）を抽出
  db.all("select ImageData from CanvasPreview", (err, row) => {
    if (err) {
      console.log(err)
      return
    }

    if (row.length > 0) {
      // png出力パス
      const binaryPath = path + ".png"
      // row[0].ImageDataプロパティからpngを生成
      fs.writeFileSync(binaryPath, row[0].ImageData)
    }
  })
})
```

クエリにてCanvasPreviewからImageDataを取得する。
このとき出力結果の`row`には、次の形でデータが格納されている。

```json
{ ImageData:
   <Buffer 89 50 4e 47 0d 0a ... >}
```

つまり、`row[0].ImageData`で目的のサムネイル画像のデータを取得することが可能。
最後に、`fs.writeFileSync`により`illust1.png`に`row.ImageData`を書き出してやれば終了である。

# 参考

[CLIP STUDIO PAINTで作成した漫画データのサムネール出力の為の調査](https://qiita.com/ariela/items/71986b1bf65a566d2e75)

[CLIPSTUDIO PAINTのファイルからサムネイル画像を取得](https://qiita.com/gutti02/items/f021d18074323009c8ce)

[CLIP STUDIO PAINTの.lipファイルをハックして作業動画を書き出すWindowsアプリを作った](https://tech.recruit-mp.co.jp/etc/building-lip-to-animated-gif-converter-named-lip2gif/amp/)

[Node.js で TextEncoder や SHA512 を作る](http://var.blog.jp/archives/73651771.html)

[MDN - オブジェクトでの作業](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Working_with_Objects)

[Node.js で SQLite を扱う](https://neos21.hatenablog.com/entry/2018/04/22/080000)

[Writing to Files in Node.js](https://stackabuse.com/writing-to-files-in-node-js/)

[Get list of tables from SQLite in Node.js](https://stackoverflow.com/questions/34480683/get-list-of-tables-from-sqlite-in-node-js)

### 主にPowerShell関係
<!--  -->
[MS - certutil](https://eijiyoshida.hatenablog.jp/entry/20140618/1403092219)

[Windows10でバイナリ<->HEXテキスト変換](https://qiita.com/tlab/items/57a29f7310fd0931d3ae)

[PowerShellからデータを出力するパターンまとめ](https://qiita.com/TAKA-SHU/items/d4467583c62d9b556fa4)

[cutコマンドについてまとめました 【Linuxコマンド集】](https://eng-entrance.com/linux-command-cut)

[Windows PowerShell : UNIXの cut コマンド相当の作業をする。](https://www.engineer-memo.net/20110122-2750)