package tv.dyndns.kishibe.qmaclone.server.image;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.color.CMMException;
import java.awt.image.AreaAveragingScaleFilter;
import java.awt.image.BufferedImage;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageFilter;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import org.apache.commons.codec.digest.DigestUtils;
import tv.dyndns.kishibe.qmaclone.server.ThreadPool;
import tv.dyndns.kishibe.qmaclone.server.util.Downloader;
import tv.dyndns.kishibe.qmaclone.server.util.DownloaderException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.io.Files;
import com.google.inject.Inject;
public class ImageUtils {
/**
* URLに含まれるパラメーターを保持する
*/
public static class Parameter {
public final String url;
public final int width;
public final int height;
public final boolean keepAspectRatio;
public Parameter(String url, int width, int height, boolean keepAspectRatio) {
this.url = url;
this.width = width;
this.height = height;
this.keepAspectRatio = keepAspectRatio;
}
public String getHashString() {
return toHashString(Joiner.on('+').join(url, width, height, keepAspectRatio));
}
@Override
public int hashCode() {
return Objects.hashCode(url, width, height, keepAspectRatio);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Parameter)) {
return false;
}
Parameter rh = (Parameter) obj;
return Objects.equal(url, rh.url) && width == rh.width && height == rh.height
&& keepAspectRatio == rh.keepAspectRatio;
}
}
private static Logger logger = Logger.getLogger(ImageUtils.class.toString());
private static final String CACHE_ROOT_PATH = "/tmp/qmaclone/image";
private static final String CACHE_INPUT_PATH = CACHE_ROOT_PATH + "/input";
private static final String CACHE_OUTPUT_PATH = CACHE_ROOT_PATH + "/output";
private final Downloader downloader;
private final LoadingCache<Parameter, byte[]> cache = CacheBuilder.newBuilder().softValues()
.build(new CacheLoader<Parameter, byte[]>() {
@Override
public byte[] load(Parameter key) throws Exception {
return getImage(key);
}
});
@Inject
public ImageUtils(ThreadPool threadPool, Downloader downloader) {
this.downloader = Preconditions.checkNotNull(downloader);
new File(CACHE_ROOT_PATH).mkdirs();
new File(CACHE_INPUT_PATH).mkdirs();
new File(CACHE_OUTPUT_PATH).mkdirs();
ImageIO.setCacheDirectory(new File(CACHE_ROOT_PATH));
ImageIO.setUseCache(true);
}
/**
* ダウンロードした画像ファイルのキャッシュ格納先ファイルを返す
*
* @param url
* @return
*/
@VisibleForTesting
File getInputCacheFile(String url) {
String hash = toHashString(url);
return new File(CACHE_INPUT_PATH + "/" + hash.substring(0, 2) + "/" + hash.substring(2));
}
/**
* リサイズ後の画像ファイルのキャッシュ格納先ファイルを返す
*
* @param parameter
* @return
*/
@VisibleForTesting
File getOutputCacheFile(Parameter parameter) {
String hash = parameter.getHashString();
return new File(CACHE_OUTPUT_PATH + "/" + hash.substring(0, 2) + "/" + hash.substring(2));
}
/**
* 画像をリサイズする
*
* @param inputFile
* リサイズ元画像ファイル
* @param canvasWidth
* リサイズ後の画像の幅
* @param canvasHeight
* リサイズ後の画像の高さ
* @param outputFile
* リサイズ後画像ファイル
* @throws IOException
*/
@VisibleForTesting
void resizeImage(File inputFile, int canvasWidth, int canvasHeight, boolean keepAspectRatio,
File outputFile) throws IOException {
BufferedImage inputImage = ImageIO.read(inputFile);
if (inputImage == null) {
throw new IOException("ダウンロードした画像ファイル形式が判別できませんでした inputFile:" + inputFile);
}
int imageWidth = canvasWidth;
int imageHeight = canvasHeight;
int offsetX = 0;
int offsetY = 0;
if (keepAspectRatio) {
if (inputImage.getWidth() * 3 <= inputImage.getHeight() * 4) {
// 縦長
imageWidth = imageHeight * inputImage.getHeight() / inputImage.getWidth();
offsetX = (canvasWidth - imageWidth) / 2;
} else {
// 横長
imageHeight = imageWidth * inputImage.getWidth() / inputImage.getHeight();
offsetY = (canvasHeight - imageHeight) / 2;
}
}
ImageFilter imageFilter = new AreaAveragingScaleFilter(imageWidth, imageHeight);
Image middleImage = new Canvas().createImage(new FilteredImageSource(inputImage.getSource(),
imageFilter));
BufferedImage outputImage = new BufferedImage(canvasWidth, canvasHeight,
BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = outputImage.createGraphics();
// 透明色が黒く表示されるバグへの対処
graphics.setColor(Color.WHITE);
graphics.fill(new Rectangle(canvasWidth, canvasHeight));
graphics.drawImage(middleImage, offsetX, offsetY, imageWidth, imageHeight, null);
ImageIO.write(outputImage, "jpeg", outputFile);
logger.log(Level.INFO, String.format("%d bytes -> %d bytes (%s->%s)", inputFile.length(),
outputFile.length(), inputFile.getPath(), outputFile.getPath()));
}
/**
* 文字列のハッシュを返す
*
* @param data
* @return
*/
@VisibleForTesting
static String toHashString(String data) {
return DigestUtils.shaHex(data);
}
public long getLastModified(Parameter parameter) {
File outputCacheFile = getOutputCacheFile(parameter);
return outputCacheFile.lastModified();
}
public void writeToStream(Parameter parameter, OutputStream outputStream) throws IOException {
try {
outputStream.write(cache.get(parameter));
} catch (ExecutionException e) {
throw new IOException(e);
}
}
@VisibleForTesting
byte[] getImage(Parameter parameter) throws IOException {
URL url;
try {
url = new URL(parameter.url);
} catch (MalformedURLException e) {
throw new IOException("不正なURLです: url=" + parameter.url, e);
}
int width = parameter.width;
int height = parameter.height;
boolean keepAspectRatio = parameter.keepAspectRatio;
File inputCacheFile = getInputCacheFile(parameter.url);
Files.createParentDirs(inputCacheFile);
File outputCacheFile = getOutputCacheFile(parameter);
Files.createParentDirs(outputCacheFile);
// 画像がダウンロードされていなければダウンロードする
// double-check
// TODO(nodchip): 実行速度が早くシンプルな方法に変更する
if (!inputCacheFile.isFile()) {
// BugTrack-QMAClone/434 - QMAClone wiki
// http://kishibe.dyndns.tv/qmaclone/wiki/wiki.cgi?page=BugTrack%2DQMAClone%2F434#1330156832
File tempFile = File.createTempFile("ImageUtils-download", null);
try {
downloader.downloadToFile(url, tempFile);
} catch (DownloaderException e) {
throw new IOException("ダウンロードに失敗しました: url=" + url, e);
}
tempFile.renameTo(inputCacheFile);
}
// 画像がリサイズされていなければリサイズする
if (!outputCacheFile.isFile()) {
// BugTrack-QMAClone/434 - QMAClone wiki
// http://kishibe.dyndns.tv/qmaclone/wiki/wiki.cgi?page=BugTrack%2DQMAClone%2F434#1330156832
File tempFile = File.createTempFile("ImageUtils-resize", null);
resizeImage(inputCacheFile, width, height, keepAspectRatio, tempFile);
tempFile.renameTo(outputCacheFile);
}
return Files.toByteArray(outputCacheFile);
}
public boolean isImage(File file) {
try {
return ImageIO.read(file) != null;
} catch (IOException e) {
logger.log(Level.INFO, "画像ファイル読み込み中に入出力エラーが発生しました", e);
return false;
} catch (CMMException e) {
logger.log(Level.INFO, "画像ファイルの読み込みに失敗しました", e);
return false;
}
}
}