- 投稿日:2020-01-13T19:19:13+09:00
NodeJSの画像処理ライブラリ「sharp」を使って画像を連結する
はじめに
NodeJSでImageMagickのappendのようにサイズがバラバラの画像を単純に横並びで連結させたい機会があったのですが、
単純すぎて中々記事が見つからなかったので備忘録的な意味合いも込めて書き残します。やること
- sharpで画像情報取得、連結
バージョン
NodeJS: v12.14.1
sharp: v0.23.4成果物
出来上がる画像
ソースコード
terminalyarn init -y yarn add sharp
ソースはこちら
https://github.com/engabesi/appendImagesまず全文を貼ります。
index.jsconst sharp = require("sharp"); (async () => { const imagePaths = ["images/1.jpg", "images/2.jpg", "images/3.jpg"]; const imageAttrs = []; // 連結する画像の情報取得 const promises = []; const imagePromise = path => new Promise(async resolve => { const image = await sharp(path); let width = 0, height = 0; await image .metadata() .then(meta => ([width, height] = [meta.width, meta.height])); const buf = await image.toBuffer(); resolve({ width, height, buf }); }); imagePaths.forEach(path => promises.push(imagePromise(path))); await Promise.all(promises).then(values => { values.forEach(value => imageAttrs.push(value)); }); // outputする画像の設定 const outputImgWidth = imageAttrs.reduce((acc, cur) => acc + cur.width, 0); const outputImgHeight = Math.max(...imageAttrs.map(v => v.height)); let totalLeft = 0; const compositeParams = imageAttrs.map(image => { const left = totalLeft; totalLeft += image.width; return { input: image.buf, gravity: "northwest", left: left, top: 0 }; }); // 連結処理 sharp({ create: { width: outputImgWidth, height: outputImgHeight, channels: 4, background: { r: 255, g: 255, b: 255, alpha: 0 } } }) .composite(compositeParams) .toFile("output.png"); })();これで
node index.js
で実行すると上記のように左上詰めで画像が連結されます。解説
まず
sharp
をimportします。
sharpについて詳しくは公式Documentを見てください。
https://github.com/lovell/sharp
https://sharp.pixelplumbing.com/en/stable/const sharp = require("sharp");画像情報取得
連結する画像パスの配列と画像情報を格納する配列を宣言します。
const imagePaths = ["images/1.jpg", "images/2.jpg", "images/3.jpg"]; const imageAttrs = [];画像のwidth, height, bufferを取得する
Promise
を作成し、Promise.all
で全画像分並列実行させます。
Promise.all
のthen
で先程宣言した画像情報を格納する配列に取得した情報をpushしますconst promises = []; const imagePromise = path => new Promise(async resolve => { const image = await sharp(path); let width = 0, height = 0; await image .metadata() .then(meta => ([width, height] = [meta.width, meta.height])); const buf = await image.toBuffer(); resolve({ width, height, buf }); }); imagePaths.forEach(path => promises.push(imagePromise(path))); await Promise.all(promises).then(values => { values.forEach(value => imageAttrs.push(value)); });なぜPromise実行時(
imagePromise(path)
)のthen
で処理しないのかですが、こちらのthen
は実行が完了した順に走ることになります。
今回は連結される並びを固定にしたいのでPromise.all
のthen
で処理を行っています。出力画像設定
width
には全画像のwidth合計値、height
には全画像のheightの最大値を取得しますconst outputImgWidth = imageAttrs.reduce((acc, cur) => acc + cur.width, 0); const outputImgHeight = Math.max(...imageAttrs.map(v => v.height));
sharp
はcomposite
というメソッドで画像の結合処理等を行います。
そのcomposite
メソッドのparam設定を行いますlet totalLeft = 0; const compositeParams = imageAttrs.map(image => { const left = totalLeft; totalLeft += image.width; return { input: image.buf, gravity: "northwest", left: left, top: 0 }; });
input
に画像のbufferを入れます。
gravity
は方角で指定します。今回左上詰めで連結させるのでnorthwest
を入れます。
left
とtop
はoffsetをpx単位で設定します。
詳しくはsharp
の公式Docを読んでください。
https://sharp.pixelplumbing.com/en/stable/api-composite/画像出力
最後に
sharp
で画像を出力します。sharp({ create: { width: outputImgWidth, height: outputImgHeight, channels: 4, background: { r: 255, g: 255, b: 255, alpha: 0 } } }) .composite(compositeParams) .toFile("output.png");これで
output.png
がrootに作成されます。
連結の余白部分が透明でなくてもよかったり、サイズを削減したい場合は、
channels
を4から3に変更してbackground
のalphaを消したり、
出力画像拡張子をjpgに変更等好きなように変更してください。まとめ
これで
reject
を使ってエラーハンドリングをしていない等、色々ゴリ押し気味ですが画像連結処理を実装できました。
一応車輪の再発明をしていないかとドキュメントを眺めましたがそれらしい処理が見当たりませんでした。
もしあれば教えていただけると助かります。
- 投稿日:2020-01-13T13:26:03+09:00
クラウドストレージ「Box」にAPIを使ってファイルをアップロードする
概要
クラウドストレージのBoxにAPI経由でファイルのアップロードを行う必要があったのですが、
調べてもあまり情報がなかったので記載しておきます。環境
開発環境はMacでNodebrewを使っています。
$ nodebrew nodebrew 1.0.1 $ node -v v12.14.1カスタムアプリの作成と承認
カスタムアプリの作成
BoxAPIを使うためにまずカスタムアプリを作成します。
デベロッパーコンソールにアクセスしてBoxアカウントにログインします。
https://app.box.com/developers/consoleアプリの新規作成を押下します。
アプリの種類でカスタムアプリを選択します。
認証方法でJWTを使用したOAuth 2.0 (サーバー認証)を選択します。
アプリの名前を入力してアプリの作成を押下します。
アプリを作成するとデベロッパーコンソールのマイアプリに追加されるので、
作成したアプリを選択して左のメニューから構成画面を表示します。ここに表示されているDeveloperトークンを使ってAPIを実行することもできますが、
今回は認証キーを使用します。構成画面内に公開キーの追加と管理という項目があるので、そちらから公開/秘密キーペアを生成します。
※アカウントに二段階認証が設定されている必要があります。キーペアを生成すると認証情報が記載されたJSONファイルが保存されるので、こちらを使って認証を行います。
カスタムアプリの承認
次にBoxアカウントにカスタムアプリの承認を行います。
Boxアカウントの管理コンソールからEnterprise設定を開いて
アプリ→カスタムアプリケーション→新しいアプリケーションを承認を押下します。APIキーの入力画面が表示されるので、
先ほど保存したJSONファイルの中のclientIDを入力して承認します。これでカスタムアプリからAPIを実行する準備ができました。
BoxSDKからAPIの実行
フォルダ内の項目一覧の取得
BoxAPIにはPythonやJAVAのSDKも用意されていますが、今回はbox-node-sdkを使って実行してみます。
https://github.com/box/box-node-sdkAPIリファレンス
https://developer.box.com/jp/reference/まず適当なフォルダを作成してbox-node-sdkをインストールします。
$ mkdir box-app $ cd box-app/ $ npm init -y $ npm install box-node-sdk作成したフォルダ内に認証情報のJSONファイルを保存しておき、
以下のようなスクリプトを作成します。こちらの処理では指定したフォルダID内の項目の一覧を取得するAPIを実行しています。
フォルダID'0'はルートディレクトリのIDです。
https://developer.box.com/jp/reference/get-folders-id-items/script.jsconst BoxSDK = require('box-node-sdk'); // 認証キーの読み込み const sdkConfig = require('./config.json'); const sdk = BoxSDK.getPreconfiguredInstance(sdkConfig); const client = sdk.getAppAuthClient('enterprise'); // フォルダ内の項目の取得 client.folders.getItems('0') .then(folder => { console.log(folder); }) .catch(err => { console.log(err); })APIを実行してみます。
$ node script.js { total_count: 0, entries: [], offset: 0, limit: 100, order: [ { by: 'type', direction: 'ASC' }, { by: 'name', direction: 'ASC' } ]実行するとAPIで取得した項目一覧が出力されますが、
もし自分のBoxアカウントのルートディレクトリにファイルやフォルダを保存していたとしても
ここではなにも表示されないと思います。ここで取得しているルートディレクトリというのはカスタムアプリ(というアカウント)のルートディレクトリとなるため、
自分のBoxアカウントのルートディレクトリとは別の場所になります。これだとBoxSDKを使っても結果がわかりづらいのでBoxアカウントとの共有フォルダを作成します。
共有フォルダの作成
まずブラウザで自分のBoxアカウントにアクセスして適当なフォルダを作成します。
管理コンソールからユーザーとグループを開いて
グループ→作成を押下します。グループ名を入力して権限設定でグループメンバーを選択します。
メンバーを追加を押下してユーザーの入力欄にカスタムアプリ名を入力すると
サジェストが表示されるのでそちらを選択して追加します。最後にフォルダを共有を押下して先ほど作成したフォルダを選択して完了します。
これでカスタムアプリとフォルダの共有を行なっているグループが作成されました。
もう一度APIを実行してみると今度は共有フォルダが取得されていることが確認できます。
$ node script.js { total_count: 1, entries: [ { type: 'folder', id: '99896711320', sequence_id: '0', etag: '0', name: 'shared' } ], offset: 0, limit: 100, order: [ { by: 'type', direction: 'ASC' }, { by: 'name', direction: 'ASC' } ] }ファイルアップロード
共有フォルダが作成できたのでファイルアップロードAPIを実行します。
https://developer.box.com/jp/reference/post-files-content/ソースを以下のように修正します。
script.jsconst BoxSDK = require('box-node-sdk'); // 追加 const fs = require('fs'); // 認証キーの読み込み const sdkConfig = require('./config.json'); const sdk = BoxSDK.getPreconfiguredInstance(sdkConfig); const client = sdk.getAppAuthClient('enterprise'); // フォルダ内の項目の取得 client.folders.getItems('0') .then(folder => { // 追加 fs.readFile('./upload_file.txt', (err, data) => { if (err) { console.log(err); return; } // ファイルアップロード client.files.uploadFile(folder.entries[0].id, 'upload_file.txt', data) .then(file => { console.log(file); }) .catch(err => { console.log(err); }) }) }) .catch(err => { console.log(err); })スクリプトを実行するとアップロードしたファイルの情報が出力されます。
$ node script.js { total_count: 1, entries: [ { type: 'file', id: '595943878038', file_version: [Object], sequence_id: '0', etag: '0', sha1: '', name: 'upload_file.txt', description: '', size: 247379, path_collection: [Object], created_at: '2020-01-12T08:54:09-08:00', modified_at: '2020-01-12T08:54:09-08:00', trashed_at: null, purged_at: null, content_created_at: '2020-01-12T08:54:09-08:00', content_modified_at: '2020-01-12T08:54:09-08:00', created_by: [Object], modified_by: [Object], owned_by: [Object], shared_link: null, parent: [Object], item_status: 'active' } ] }Boxアカウントでフォルダを確認するとファイルが保存されています。
※フォルダ内に同名ファイルがある状態でアップロードを行うと409エラーが返ってくるので
その場合はファイル名を変更するかファイルのバージョンアップAPIを実行してください。
https://developer.box.com/jp/reference/post-files-id-content/大容量ファイルのアップロード
サイズの小さいファイルは上の方法でアップロードを行えば良いのですが、
50MBを超えるファイルは分割アップロードを使用することが推奨されているとリファレンスに記載があります。分割アップロードAPI
https://developer.box.com/jp/reference/get-files-upload-sessions-id/ただ、APIリファレンスのやり方だと分割アップロードのセッションの作成、
パーツごとに分割アップロード、アップロードセッションのコミットなど複数のAPIを実行する必要があり少し面倒なのですが、
box-node-sdkのドキュメントを見てみるとこれらをまとめて行なってくれる分割アップロード用のAPIが用意されているようです。
https://github.com/box/box-node-sdk/blob/master/docs/files.md#chunked-uploadこちらを使って分割アップロードを試してみます。
サイズが大きいファイルを用意してソースを修正します。
※20MB以下のファイルを分割アップロードするとエラーになります。script.jsconst BoxSDK = require('box-node-sdk'); const fs = require('fs'); // 認証キーの読み込み const sdkConfig = require('./config.json'); const sdk = BoxSDK.getPreconfiguredInstance(sdkConfig); const client = sdk.getAppAuthClient('enterprise'); // フォルダ内の項目の取得 client.folders.getItems('0') .then(folder => { // 修正 fs.readFile('./large_file.csv', (err, data) => { if (err) { console.log(err); return; } // 分割ファイルアップロード client.files.getChunkedUploader(folder.entries[0].id, data.length, 'large_file.csv', data) .then(uploader => uploader.start()) .then(file => { console.log(file); }) .catch(err => { console.log(err); }) }) }) .catch(err => { console.log(err); })スクリプトを実行するとアップロードしたファイルの情報が出力されます。
$ node script.js { total_count: 1, entries: [ { type: 'file', id: '595967596582', file_version: [Object], sequence_id: '0', etag: '0', sha1: '', name: 'large_file.csv', description: '', size: 20243278, path_collection: [Object], created_at: '2020-01-12T09:31:27-08:00', modified_at: '2020-01-12T09:31:27-08:00', trashed_at: null, purged_at: null, content_created_at: '2020-01-12T09:31:27-08:00', content_modified_at: '2020-01-12T09:31:27-08:00', created_by: [Object], modified_by: [Object], owned_by: [Object], shared_link: null, parent: [Object], item_status: 'active' } ] }Boxアカウントで確認すると大容量ファイルも保存されていることが確認できます。
おわりに
今回BoxAPIをいくつか試していたのですが、検索APIでファイルやフォルダの検索を行うと
作成してから少し時間が経ったものでないと検索に引っかかりませんでした。もし原因を知っている方がいれば教えていただけると助かります。
- 投稿日:2020-01-13T13:18:04+09:00
複数のファイルをアップロードしてExpressサーバーで保存する
概要
クライアントアプリからExpressサーバーへの複数ファイルのアップロード時にハマったので、
方法を記載します。環境
開発環境はMacでNodebrewを使っています。
$ nodebrew nodebrew 1.0.1 $ node -v v12.14.1単一ファイルのアップロード
まず単一ファイルのアップロード方法を記載します。
Expressサーバー立ち上げ
express-generatorでExpressサーバーを作成します。
$ npm install -g express-generatorexpress-generatorをインストールしてexpressコマンドが使えるようになったので
アプリを作成します。$ express --view=ejs express-app create : express-app/ create : express-app/public/ create : express-app/public/javascripts/ create : express-app/public/images/ create : express-app/public/stylesheets/ create : express-app/public/stylesheets/style.css create : express-app/routes/ create : express-app/routes/index.js create : express-app/routes/users.js create : express-app/views/ create : express-app/views/error.ejs create : express-app/views/index.ejs create : express-app/app.js create : express-app/package.json create : express-app/bin/ create : express-app/bin/www change directory: $ cd express-app install dependencies: $ npm install run the app: $ DEBUG=express-app:* npm start上の手順の通りにサーバーを立ち上げます。
$ cd express-app/ $ npm install $ npm start以下のURLにアクセスしてサーバーに接続できることを確認します。
http://localhost:3000アップロードAPI作成
サーバーを立ち上げることができたので、一度停止してアップロードAPIを作成します。
app.jsを以下のように修正します。app.jsvar createError = require('http-errors'); var express = require('express'); var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); var indexRouter = require('./routes/index'); var usersRouter = require('./routes/users'); // 追加 var uploadRouter = require('./routes/upload'); var app = express(); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs'); app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); app.use('/', indexRouter); app.use('/users', usersRouter); // 追加 app.use('/upload', uploadRouter);FormDataを処理するためにmulterを追加します。
https://github.com/expressjs/multer$ npm install multerroutesフォルダ内にupload.jsを作成します。
destで指定したフォルダ内にアップロードされたファイルが保存されます。upload.jsvar express = require('express'); var multer = require('multer'); var upload = multer({ dest: 'uploads/' }); var router = express.Router(); router.post('/', upload.single('file'), function(req, res, next) { console.log(req.file); console.log(req.body); res.send('upload success'); }); module.exports = router;これでAPIが作成できたのでnpm startでサーバーを立ち上げておきます。
アップロードフォーム作成
クライアントアプリ側に以下のようなファイルを作成してアップロードフォームを用意します。
Ajaxを使ってファイルと一緒に適当なパラメータも送信しています。upload.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>Uploader</title> <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script> </head> <body> <div> <input type="file" id="upload"> <input type="button" id="uploadButton" value="送信"> </div> <script> $(function(){ $('#uploadButton').click(function() { const files = $('#upload')[0].files; const formData = new FormData(); formData.append('file', files[0]); formData.append('hoge', 123); $.ajax({ url: 'http://localhost:3000/upload', method: 'post', data: formData, processData: false, contentType: false }).done(function(res){ console.log(res); }).fail(function(err) { console.log(err); }) }) }); </script> </body> </html>ファイルアップロード
Expressサーバーを立ち上げている状態で上で作成したupload.htmlをブラウザで開いて、
ファイルを選択後に送信ボタンを押下します。Expressのコンソールに以下のような値が出力されています。
req.fileにアップロードしたファイルの情報、req.bodyにパラメータが格納されています。{ fieldname: 'file', originalname: 'upload_file.txt', encoding: '7bit', mimetype: 'text/plain', destination: 'uploads/', filename: '12dee747383a844dd7d1888578cf720e', path: 'uploads/12dee747383a844dd7d1888578cf720e', size: 247379 } [Object: null prototype] { hoge: '123' } POST /upload 200 5.874 ms - 14multerのdestにuploadsを指定したので、Expressアプリのルートディレクトリにuploadsフォルダが作成されて
その中にアップロードしたファイルが保存されています。もし送信ボタン押下時にクロスドメインのエラーが発生する場合は、
以下のようにExpressサーバーにCORSの許可設定を行なってください。CORS対応
ファイルアップロード時にクロスドメインエラーが発生した場合はExpressにCORSの許可を設定する必要があります。
方法はいくつかありますが、今回はcorsモジュールを使用します。$ npm install cors上のコマンドを実行後にapp.jsに以下を追記してください。
今回は全リクエストを許可としていますが、本番運用などする際は適切に設定を行なってください。app.jsvar createError = require('http-errors'); var express = require('express'); var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); // 追加 var cors = require('cors'); var indexRouter = require('./routes/index'); var usersRouter = require('./routes/users'); var uploadRouter = require('./routes/upload'); var app = express(); // 追加 app.use(cors());この状態でサーバーを立ち上げてファイルアップロードを行うとクロスドメインエラーが発生しないようになっています。
複数ファイルのアップロード
前置きが長くなりましたが、次に複数ファイルのアップロードを行います。
アップロードAPI作成
まずExpressに複数ファイルのアップロードAPIを作成します。
比較用に単一ファイル用のAPIも残しています。upload.jsvar express = require('express'); var multer = require('multer'); var upload = multer({ dest: 'uploads/' }); var router = express.Router(); // 単一ファイルアップロード router.post('/', upload.single('file'), function(req, res, next) { console.log(req.file); console.log(req.body); res.send('upload success'); }); // 追加 // 複数ファイルアップロード router.post('/multiple', upload.array('files'), function(req, res, next) { console.log(req.files); console.log(req.body); res.send('multiple upload success'); }); module.exports = router;アップロードフォーム作成
クライアント側にも複数ファイルのアップロードフォームを追加します。
ポイントは複数ファイルをFormDataに追加する際に'files'を指定する部分です。upload.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>Uploader</title> <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script> </head> <body> <!-- 単一ファイルアップロードフォーム --> <div> <input type="file" id="upload"> <input type="button" id="uploadButton" value="送信"> </div> <!-- 追加 --> <!-- 複数ファイルアップロードフォーム --> <div> <input type="file" id="multipleUpload" multiple> <input type="button" id="multipleUploadButton" value="送信"> </div> <script> $(function(){ // 単一ファイルアップロード $('#uploadButton').click(function() { const files = $('#upload')[0].files; const formData = new FormData(); formData.append('file', files[0]); formData.append('hoge', 123); $.ajax({ url: 'http://localhost:3000/upload', method: 'post', data: formData, processData: false, contentType: false }).done(function(res){ console.log(res); }).fail(function(err) { console.log(err); }) }) // 追加 // 複数ファイルアップロード $('#multipleUploadButton').click(function() { const files = $('#multipleUpload')[0].files; const formData = new FormData(); for (let i = 0; i < files.length; i++) { formData.append('files', files[i]); } formData.append('hoge', 123); $.ajax({ url: 'http://localhost:3000/upload/multiple', method: 'post', data: formData, processData: false, contentType: false }).done(function(res){ console.log(res); }).fail(function(err) { console.log(err); }) }) }); </script> </body> </html>ファイルアップロード
Expressサーバーを立ち上げている状態で、先ほどと同じようにブラウザから複数ファイルを選択してアップロードを行います。
Expressのコンソールでファイルの情報が出力されて、uploadsフォルダにファイルが保存されていることが確認できます。[ { fieldname: 'files', originalname: 'upload_file 2.txt', encoding: '7bit', mimetype: 'text/plain', destination: 'uploads/', filename: '2c8436e8d77723dfaf7a75e38fe1785c', path: 'uploads/2c8436e8d77723dfaf7a75e38fe1785c', size: 247379 }, { fieldname: 'files', originalname: 'upload_file.txt', encoding: '7bit', mimetype: 'text/plain', destination: 'uploads/', filename: '5ed6ed7671d65f269dfaa2c456b2b95b', path: 'uploads/5ed6ed7671d65f269dfaa2c456b2b95b', size: 247379 } ] [Object: null prototype] { hoge: '123' } POST /upload/multiple 200 19.628 ms - 23まとめ
単一ファイルのアップロードは割と簡単に実装できましたが、
複数ファイルのアップロードでのクライアントからのFormDataへの追加と
サーバーでの保存がなかなかうまくいかずハマってしまいました。Formタグを使えばもう少し簡単に実装できたかもしれませんが、
今回は使用せずに実装したかったためこのような方法になりました。
- 投稿日:2020-01-13T12:52:07+09:00
gulpでsassとbabel環境構築(nodeインストールから)
gulpセットアップ
自分用メモなので、大まかな手順のみ。
作業詳細はリンクの各記事を参照。
環境構築を一から行ったことがない人向け(自分!)作業環境
macOS 10.14.6
手順
node.jsインストール
nodeのバージョン管理をするため、下記記事を参照しインストール
MacにNode.jsをインストールgulp-sass設定
gulp-babel設定
BabelAutoprefixer設定
Autoprefixer参照
絶対つまずかないGulp 4入門(2019年版) インストールとSassを使うまでの手順
gulp3→4の変更点に気をつけよう!意味のない感想
自分の投稿見返したら、ほぼ一年前にnpmでwebpack環境構築に手をつけてた、、
けど現在実業務ではgulpで色々ごにょごにょしているのでgulp使い方復習&gulp4の書き方覚えようてことでまずはsassとbabelの設定やってみた。昨年末はphpとDBの基本だけやったので、Laravel・Vueで開発できる環境構築できればな、、というところ。
手を出しすぎると意味わからなくなるので、少しずつ着実にやっていこう。っていう。