diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts
index 109e886106..427b54d72b 100644
--- a/src/api/common/add-file-to-drive.ts
+++ b/src/api/common/add-file-to-drive.ts
@@ -110,7 +110,7 @@ const addFile = async (
 		}
 	}
 
-	const [wh, folder] = await Promise.all([
+	const [wh, averageColor, folder] = await Promise.all([
 		// Width and height (when image)
 		(async () => {
 			// 画像かどうか
@@ -125,14 +125,45 @@ const addFile = async (
 				return null;
 			}
 
+			log('calculate image width and height...');
+
 			// Calculate width and height
 			const g = gm(fs.createReadStream(path), name);
 			const size = await prominence(g).size();
 
-			log('image width and height is calculated');
+			log(`image width and height is calculated: ${size.width}, ${size.height}`);
 
 			return [size.width, size.height];
 		})(),
+		// average color (when image)
+		(async () => {
+			// 画像かどうか
+			if (!/^image\/.*$/.test(mime)) {
+				return null;
+			}
+
+			const imageType = mime.split('/')[1];
+
+			// 画像でもPNGかJPEGでないならスキップ
+			if (imageType != 'png' && imageType != 'jpeg') {
+				return null;
+			}
+
+			log('calculate average color...');
+
+			const buffer = await prominence(gm(fs.createReadStream(path), name)
+				.setFormat('ppm')
+				.resize(1, 1)) // 1pxのサイズに縮小して平均色を取得するというハック
+				.toBuffer();
+
+			const r = buffer.readUInt8(buffer.length - 3);
+			const g = buffer.readUInt8(buffer.length - 2);
+			const b = buffer.readUInt8(buffer.length - 1);
+
+			log(`average color is calculated: ${r}, ${g}, ${b}`);
+
+			return [r, g, b];
+		})(),
 		// folder
 		(async () => {
 			if (!folderId) {
@@ -188,6 +219,10 @@ const addFile = async (
 		properties['height'] = wh[1];
 	}
 
+	if (averageColor) {
+		properties['average_color'] = averageColor;
+	}
+
 	return addToGridFS(detectedName, readable, mime, {
 		user_id: user._id,
 		folder_id: folder !== null ? folder._id : null,
diff --git a/src/api/endpoints/drive/files/create.ts b/src/api/endpoints/drive/files/create.ts
index 7546eca309..437348a1ef 100644
--- a/src/api/endpoints/drive/files/create.ts
+++ b/src/api/endpoints/drive/files/create.ts
@@ -38,9 +38,15 @@ module.exports = async (file, params, user): Promise<any> => {
 	const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
 	if (folderIdErr) throw 'invalid folder_id param';
 
-	// Create file
-	const driveFile = await create(user, file.path, name, null, folderId);
+	try {
+		// Create file
+		const driveFile = await create(user, file.path, name, null, folderId);
 
-	// Serialize
-	return serialize(driveFile);
+		// Serialize
+		return serialize(driveFile);
+	} catch (e) {
+		console.error(e);
+
+		throw e;
+	}
 };
diff --git a/src/web/app/desktop/tags/drive/file.tag b/src/web/app/desktop/tags/drive/file.tag
index 0f019d95bf..8b3d36b3f3 100644
--- a/src/web/app/desktop/tags/drive/file.tag
+++ b/src/web/app/desktop/tags/drive/file.tag
@@ -5,7 +5,9 @@
 	<div class="label" if={ I.banner_id == file.id }><img src="/assets/label.svg"/>
 		<p>%i18n:desktop.tags.mk-drive-browser-file.banner%</p>
 	</div>
-	<div class="thumbnail"><img src={ file.url + '?thumbnail&size=128' } alt=""/></div>
+	<div class="thumbnail" ref="thumbnail" style="background-color:{ file.properties.average_color ? 'rgb(' + file.properties.average_color.join(',') + ')' : 'transparent' }">
+		<img src={ file.url + '?thumbnail&size=128' } alt="" onload={ onload }/>
+	</div>
 	<p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" if={ file.name.lastIndexOf('.') != -1 }>{ file.name.substr(file.name.lastIndexOf('.')) }</span></p>
 	<style>
 		:scope
@@ -139,6 +141,7 @@
 
 	</style>
 	<script>
+		import anime from 'animejs';
 		import bytesToSize from '../../../common/scripts/bytes-to-size';
 
 		this.mixin('i');
@@ -199,5 +202,16 @@
 			this.isDragging = false;
 			this.browser.isDragSource = false;
 		};
+
+		this.onload = () => {
+			if (this.file.properties.average_color) {
+				anime({
+					targets: this.refs.thumbnail,
+					backgroundColor: `rgba(${this.file.properties.average_color.join(',')}, 0)`,
+					duration: 100,
+					easing: 'linear'
+				});
+			}
+		};
 	</script>
 </mk-drive-browser-file>
diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/tags/images.tag
index ce67d26a9f..5e4be481dc 100644
--- a/src/web/app/desktop/tags/images.tag
+++ b/src/web/app/desktop/tags/images.tag
@@ -53,7 +53,13 @@
 </mk-images>
 
 <mk-images-image>
-	<a ref="view" href={ image.url } onmousemove={ mousemove } onmouseleave={ mouseleave } style={ 'background-image: url(' + image.url + '?thumbnail&size=512' } onclick={ click } title={ image.name }></a>
+	<a ref="view"
+		href={ image.url }
+		onmousemove={ mousemove }
+		onmouseleave={ mouseleave }
+		style={ styles }
+		onclick={ click }
+		title={ image.name }></a>
 	<style>
 		:scope
 			display block
@@ -74,6 +80,11 @@
 	</style>
 	<script>
 		this.image = this.opts.image;
+		this.styles = {
+			'background-color': `rgb(${this.image.properties.average_color.join(',')})`,
+			'background-image': `url(${this.image.url}?thumbnail&size=512)`
+		};
+		console.log(this.styles);
 
 		this.mousemove = e => {
 			const rect = this.refs.view.getBoundingClientRect();
diff --git a/src/web/app/mobile/tags/drive/file-viewer.tag b/src/web/app/mobile/tags/drive/file-viewer.tag
index 48fc83fa67..259873d95c 100644
--- a/src/web/app/mobile/tags/drive/file-viewer.tag
+++ b/src/web/app/mobile/tags/drive/file-viewer.tag
@@ -1,6 +1,11 @@
 <mk-drive-file-viewer>
 	<div class="preview">
-		<img if={ kind == 'image' } src={ file.url } alt={ file.name } title={ file.name } onload={ onImageLoaded } ref="img">
+		<img if={ kind == 'image' } ref="img"
+			src={ file.url }
+			alt={ file.name }
+			title={ file.name }
+			onload={ onImageLoaded }
+			style="background-color:rgb({ file.properties.average_color.join(',') })">
 		<virtual if={ kind != 'image' }>%fa:file%</virtual>
 		<footer if={ kind == 'image' && file.properties && file.properties.width && file.properties.height }>
 			<span class="size">
diff --git a/src/web/app/mobile/tags/drive/file.tag b/src/web/app/mobile/tags/drive/file.tag
index 196dd1141e..684df7dd08 100644
--- a/src/web/app/mobile/tags/drive/file.tag
+++ b/src/web/app/mobile/tags/drive/file.tag
@@ -1,7 +1,7 @@
 <mk-drive-file data-is-selected={ isSelected }>
 	<a onclick={ onclick } href="/i/drive/file/{ file.id }">
 		<div class="container">
-			<div class="thumbnail" style={ 'background-image: url(' + file.url + '?thumbnail&size=128)' }></div>
+			<div class="thumbnail" style={ thumbnail }></div>
 			<div class="body">
 				<p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" if={ file.name.lastIndexOf('.') != -1 }>{ file.name.substr(file.name.lastIndexOf('.')) }</span></p>
 				<!--
@@ -132,6 +132,10 @@
 
 		this.browser = this.parent;
 		this.file = this.opts.file;
+		this.thumbnail = {
+			'background-color': this.file.properties.average_color ? `rgb(${this.file.properties.average_color.join(',')})` : 'transparent',
+			'background-image': `url(${this.file.url}?thumbnail&size=128)`
+		};
 		this.isSelected = this.browser.selectedFiles.some(f => f.id == this.file.id);
 
 		this.browser.on('change-selection', selections => {
diff --git a/src/web/app/mobile/tags/images.tag b/src/web/app/mobile/tags/images.tag
index aaa80e4fd1..b200eefe7e 100644
--- a/src/web/app/mobile/tags/images.tag
+++ b/src/web/app/mobile/tags/images.tag
@@ -56,7 +56,7 @@
 </mk-images>
 
 <mk-images-image>
-	<a ref="view" href={ image.url } target="_blank" style={ 'background-image: url(' + image.url + '?thumbnail&size=512' } title={ image.name }></a>
+	<a ref="view" href={ image.url } target="_blank" style={ styles } title={ image.name }></a>
 	<style>
 		:scope
 			display block
@@ -74,5 +74,9 @@
 	</style>
 	<script>
 		this.image = this.opts.image;
+		this.styles = {
+			'background-color': `rgb(${this.image.properties.average_color.join(',')})`,
+			'background-image': `url(${this.image.url}?thumbnail&size=512)`
+		};
 	</script>
 </mk-images-image>
diff --git a/tools/migration/node.2017-12-11.js b/tools/migration/node.2017-12-11.js
new file mode 100644
index 0000000000..3a3fef0518
--- /dev/null
+++ b/tools/migration/node.2017-12-11.js
@@ -0,0 +1,67 @@
+// for Node.js interpret
+
+const { default: DriveFile, getGridFSBucket } = require('../../built/api/models/drive-file')
+const { default: zip } = require('@prezzemolo/zip')
+
+const _gm = require('gm');
+const gm = _gm.subClass({
+	imageMagick: true
+});
+
+const migrate = doc => new Promise(async (res, rej) => {
+	const bucket = await getGridFSBucket();
+
+	const readable = bucket.openDownloadStream(doc._id);
+
+	gm(readable)
+		.setFormat('ppm')
+		.resize(1, 1)
+		.toBuffer(async (err, buffer) => {
+			if (err) rej(err);
+			const r = buffer.readUInt8(buffer.length - 3);
+			const g = buffer.readUInt8(buffer.length - 2);
+			const b = buffer.readUInt8(buffer.length - 1);
+
+			const result = await DriveFile.update(doc._id, {
+				$set: {
+					'metadata.properties.average_color': [r, g, b]
+				}
+			})
+
+			res(result.ok === 1);
+		});
+});
+
+async function main() {
+	const query = {
+		contentType: {
+			$in: [
+				'image/png',
+				'image/jpeg'
+			]
+		}
+	}
+
+	const count = await DriveFile.count(query);
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await DriveFile.find(query, {
+				limit: dop, skip: time * dop
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)