dynmap/DynmapCore/src/main/java/org/dynmap/utils/ImageIOManager.java

233 lines
8.9 KiB
Java

package org.dynmap.utils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.awt.image.BufferedImage;
import java.awt.image.DirectColorModel;
import java.awt.image.WritableRaster;
import java.io.IOException;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import org.dynmap.Log;
import org.dynmap.MapType.ImageEncoding;
import org.dynmap.MapType.ImageFormat;
import org.dynmap.storage.MapStorageTile;
import com.google.common.io.Files;
import org.dynmap.DynmapCore;
/**
* Implements soft-locks for prevent concurrency issues with file updates
*/
public class ImageIOManager {
public static String preUpdateCommand = null;
public static String postUpdateCommand = null;
private static Object imageioLock = new Object();
public static DynmapCore core; // Injected during enableCore
private static boolean did_warning = false;
private static ImageFormat validateFormat(ImageFormat fmt) {
// If WEBP, see if supported
if (fmt.getEncoding() == ImageEncoding.WEBP) {
if (core.getCWEBPPath() == null) { // No encoder?
if (!did_warning) {
Log.warning("Attempt to use WEBP support when not usable: using JPEG");
did_warning = true;
}
fmt = ImageFormat.FORMAT_JPG; // Switch to JPEN
}
}
return fmt;
}
private static void doWEBPEncode(BufferedImage img, ImageFormat fmt, OutputStream out) throws IOException {
BufferOutputStream bos = new BufferOutputStream();
ImageIO.write(img, "png", bos); // Encode as PNG in buffere output stream
// Write to a tmp file
File tmpfile = File.createTempFile("pngToWebp", "png");
FileOutputStream fos = new FileOutputStream(tmpfile);
fos.write(bos.buf, 0, bos.len);
fos.close();
// Run encoder to new new temp file
File tmpfile2 = File.createTempFile("pngToWebp", "webp");
ArrayList<String> args = new ArrayList<String>();
args.add(core.getCWEBPPath());
if (fmt.getID().endsWith("-l")) {
args.add("-lossless");
}
args.add("-q");
args.add(Integer.toString((int)fmt.getQuality()));
args.add(tmpfile.getAbsolutePath());
args.add("-o");
args.add(tmpfile2.getAbsolutePath());
Process pr = Runtime.getRuntime().exec(args.toArray(new String[0]));
try {
pr.waitFor();
} catch (InterruptedException ix) {
throw new IOException("Error waiting for encoder");
}
// Read output file into output stream
Files.copy(tmpfile2, out);
out.flush();
// Clean up temp files
tmpfile.delete();
tmpfile2.delete();
}
private static BufferedImage doWEBPDecode(BufferInputStream buf) throws IOException {
// Write to a tmp file
File tmpfile = File.createTempFile("webpToPng", "webp");
Files.write(buf.buffer(), tmpfile);
// Run encoder to new new temp file
File tmpfile2 = File.createTempFile("webpToPng", "png");
String args[] = { core.getDWEBPPath(), tmpfile.getAbsolutePath(), "-o", tmpfile2.getAbsolutePath() };
Process pr = Runtime.getRuntime().exec(args);
try {
pr.waitFor();
} catch (InterruptedException ix) {
throw new IOException("Error waiting for encoder");
}
// Read file
BufferedImage obuf = ImageIO.read(tmpfile2);
// Clean up temp files
tmpfile.delete();
tmpfile2.delete();
return obuf;
}
public static BufferOutputStream imageIOEncode(BufferedImage img, ImageFormat fmt) {
if(isRequiredJDKVersion(17,-1,-1)){
return imageIOEncodeUnsafe(img, fmt); //we can skip Thread safety for more performance
}
synchronized(imageioLock) {
return imageIOEncodeUnsafe(img, fmt);
}
}
private static BufferOutputStream imageIOEncodeUnsafe(BufferedImage img, ImageFormat fmt) {
BufferOutputStream bos = new BufferOutputStream();
try {
ImageIO.setUseCache(false); /* Don't use file cache - too small to be worth it */
fmt = validateFormat(fmt);
if(fmt.getEncoding() == ImageEncoding.JPG) {
WritableRaster raster = img.getRaster();
WritableRaster newRaster = raster.createWritableChild(0, 0, img.getWidth(),
img.getHeight(), 0, 0, new int[] {0, 1, 2});
DirectColorModel cm = (DirectColorModel)img.getColorModel();
DirectColorModel newCM = new DirectColorModel(cm.getPixelSize(),
cm.getRedMask(), cm.getGreenMask(), cm.getBlueMask());
// now create the new buffer that is used ot write the image:
BufferedImage rgbBuffer = new BufferedImage(newCM, newRaster, false, null);
// Find a jpeg writer
ImageWriter writer = null;
Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName("jpg");
if (iter.hasNext()) {
writer = iter.next();
}
if(writer == null) {
Log.severe("No JPEG ENCODER - Java VM does not support JPEG encoding");
return null;
}
ImageWriteParam iwp = writer.getDefaultWriteParam();
iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
iwp.setCompressionQuality(fmt.getQuality());
ImageOutputStream ios;
ios = ImageIO.createImageOutputStream(bos);
writer.setOutput(ios);
writer.write(null, new IIOImage(rgbBuffer, null, null), iwp);
writer.dispose();
rgbBuffer.flush();
}
else if (fmt.getEncoding() == ImageEncoding.WEBP) {
doWEBPEncode(img, fmt, bos);
}
else {
ImageIO.write(img, fmt.getFileExt(), bos); /* Write to byte array stream - prevent bogus I/O errors */
}
} catch (IOException iox) {
Log.info("Error encoding image - " + iox.getMessage());
return null;
}
return bos;
}
public static BufferedImage imageIODecode(MapStorageTile.TileRead tr) throws IOException {
if(isRequiredJDKVersion(17,-1,-1)){
return imageIODecodeUnsafe(tr); //we can skip Thread safety for more performance
}
synchronized(imageioLock) {
return imageIODecodeUnsafe(tr);
}
}
private static BufferedImage imageIODecodeUnsafe(MapStorageTile.TileRead tr) throws IOException {
ImageIO.setUseCache(false); /* Don't use file cache - too small to be worth it */
if (tr.format == ImageEncoding.WEBP) {
return doWEBPDecode(tr.image);
}
return ImageIO.read(tr.image);
}
/**
* Checks if the current JDK is running at least a specific version
* targetMinor and targetBuild can be set to -1, if the java.version only provides a Major release this will then only check for the major release
* @param targetMajor the required minimum major version
* @param targetMinor the required minimum minor version
* @param targetBuild the required minimum build version
* @return true if the current JDK version is the required minimum version
*/
private static boolean isRequiredJDKVersion(int targetMajor, int targetMinor, int targetBuild){
String javaVersion = System.getProperty("java.version");
String[] versionParts = javaVersion.split("\\.");
if(versionParts.length < 3){
if(versionParts.length == 1
&& targetMinor == -1
&& targetBuild == -1
&& parseInt(versionParts[0], -1) >= targetMajor){
return true;//we only have a major version and thats ok
}
return false; //can not evaluate
}
int major = parseInt(versionParts[0], -1);
int minor = parseInt(versionParts[1], -1);
int build = parseInt(versionParts[2], -1);
if(major != -1 && major >= targetMajor &&
minor != -1 && minor >= targetMinor &&
build != -1 && build >= targetBuild
){
return true;
}
return false;
}
/**
* Parses a string to int, with a dynamic fallback value if not parsable
* @param input the String to parse
* @param fallback the Fallback value to use
* @return the parsed integer or the fallback value if unparsable
*/
private static int parseInt(String input, int fallback){
int output = fallback;
try{
output = Integer.parseInt(input);
} catch (NumberFormatException e) {}
return output;
}
}