图片资源在软件安装包中所占的分量很重,减少图片大小,安装包的体积改变立竿见影。
怎么压图呢?自己对图像处理也没有研究啊,只能捡现成的。网上一搜,压图工具很多,但测试之后,恕我直言,很多都是垃圾,有的压图之后图片质量下降严重,有的压图之后图片体积减小微乎其微,更有甚者,能把图片越压越大的。
TinyPng(https://tinypng.com/)是个好同志,TinyPng能够对PNG和JPG/JPEG格式图片进行有效压缩,图片体积大幅减小,可达六七成,并且失真较小。
TinyPng并不是一个本地软件,在其官网可直接进行图片压缩操作,然而,对于成百上千的图片,在官网操作不太方便,因此好的选择是利用其提供的API开发一个图片压缩工具。
TinyPng提供免费服务和收费服务,免费帐号一个月只能压缩500张图片,帐号申请很简单,不详述。
使用其API,通过java开发一个压图工具,可以为该工具配置一堆的TinyPng帐号,免费帐号或收费帐号都可,工具自动检查帐号的可用性,跟进帐号使用情况,剔除无效帐号,总是使用有效的帐号来操作。因此,如果为该工具配置多个免费帐号,则可认为可以不受限制地使用TinyPng进行图片压缩了。
工具图:
压缩率很不错:
核心代码1(TinyPngKey),定义了每个TinyPng帐号对应的key,帐号类型等:
package com.dancen.ant.png.domain;
/**
*
* @author dancen
*
*/
public class TinyPngKey
{
private String key;
private String account;
private boolean isFree;
public TinyPngKey(String key, String account, boolean isFree)
{
this.setKey(key);
this.setAccount(account);
this.setFree(isFree);
}
public String getKey()
{
return this.key;
}
public String getAccount()
{
return this.account;
}
public boolean isFree()
{
return this.isFree;
}
private void setKey(String key)
{
if(null == key || key.isEmpty())
{
throw new IllegalArgumentException("the key is null");
}
this.key = key;
}
private void setAccount(String account)
{
this.account = account;
}
private void setFree(boolean isFree)
{
this.isFree = isFree;
}
@Override
public int hashCode()
{
return this.key.hashCode();
}
@Override
public boolean equals(Object obj)
{
boolean rs = false;
if(obj == this)
{
rs = true;
}
else if(null != obj && obj instanceof TinyPngKey)
{
TinyPngKey tinyPngKey = (TinyPngKey)obj;
rs = this.key.equals(tinyPngKey.getKey());
}
return rs;
}
}
核心代码2(TinyPng),包含TinyPng的每一个帐号信息可帐号的压图数目:
package com.dancen.ant.png.domain;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import com.dancen.ant.config.ConfigWriterForPng;
/**
*
* @author dancen
*
*/
public class TinyPng
{
public static final String API_URL = "https://api.tinify.com/shrink";
public static final int MAX_COUNT_PER_MONTH = 500;
private String apiUrl;
private int maxCountPerMonth;
private Map keyMap; //key以及当月使用次数
public TinyPng(String apiUrl, int maxCountPerMonth, Map keyMap)
{
this.setApiUrl(apiUrl);
this.setMaxCountPerMonth(maxCountPerMonth);
this.setKeyMap(keyMap);
}
public String getApiUrl()
{
return this.apiUrl;
}
public int getMaxCountPerMonth()
{
return this.maxCountPerMonth;
}
public synchronized Map getKeyMap()
{
return new HashMap(this.keyMap);
}
public synchronized void updateKeyMap(TinyPngKey key, int count)
{
if(null != key)
{
this.keyMap.put(key, count);
}
}
public synchronized void addCount(TinyPngKey key, int add)
{
if(null != key)
{
Integer temp = this.keyMap.get(key);
this.keyMap.put(key, null == temp ? add : add + temp);
}
}
/**
* 获取可用的key
* @param toDoCount 将要使用key的次数
* @return
*/
public synchronized TinyPngKey getKey(int toDoCount)
{
TinyPngKey rs = null;
for(Entry entry : this.keyMap.entrySet())
{
TinyPngKey key = entry.getKey();
Integer temp = entry.getValue();
int count = null == temp ? 0 : temp;
if(count + toDoCount <= this.maxCountPerMonth || !key.isFree())
{
rs = key;
}
}
return rs;
}
public void dispose()
{
ConfigWriterForPng.getInstance().saveKeyCountMap(this.keyMap);
}
private void setApiUrl(String apiUrl)
{
if(null == apiUrl || apiUrl.isEmpty())
{
apiUrl = API_URL;
}
this.apiUrl = apiUrl;
}
private void setMaxCountPerMonth(int maxCountPerMonth)
{
if(0 >= maxCountPerMonth)
{
maxCountPerMonth = MAX_COUNT_PER_MONTH;
}
this.maxCountPerMonth = maxCountPerMonth;
}
private void setKeyMap(Map keyMap)
{
this.keyMap = new LinkedHashMap();
this.keyMap.putAll(keyMap);
}
}
核心代码3(PngCompressRunnable)图片压缩工作线程:
package com.dancen.ant.png.handler;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.CountDownLatch;
import javax.xml.bind.DatatypeConverter;
import com.dancen.ant.file.domain.FileItem;
import com.dancen.ant.gui.common.FileItemPanel;
import com.dancen.ant.png.CopyHelper;
import com.dancen.ant.png.ProgressUpdater;
import com.dancen.ant.png.domain.PngResult;
import com.dancen.ant.png.domain.TinyPng;
import com.dancen.ant.png.domain.TinyPngKey;
import com.dancen.util.MyLogManager;
/**
*
* @author dancen
*
*/
public class PngCompressRunnable extends PngRunnable
{
private TinyPng tinyPng;
public PngCompressRunnable(TinyPng tinyPng, FileItemPanel fileItemPanel, CountDownLatch countDownLatch)
{
super(fileItemPanel, countDownLatch);
this.setTinyPng(tinyPng);
}
public TinyPng getTinyPng()
{
return this.tinyPng;
}
@Override
public void run()
{
TinyPngKey tinyPngKey = this.tinyPng.getKey(1);
FileItem fileItem = this.fileItemPanel.getFileItem();
String inputPath = fileItem.getLocalPath();
String outputPath = fileItem.getExtPath();
ProgressUpdater progressUpdater = new PngProgressUpdater(this.fileItemPanel);
CopyHelper copyHelper = new CopyHelper();
copyHelper.setProgressUpdater(progressUpdater);
try
{
if(null == tinyPngKey)
{
progressUpdater.setIndeterminate(false);
progressUpdater.setText("TinyPngKey depleted");
this.rs = PngResult.createFailureResult("no tinyPngKey is available");
}
else
{
progressUpdater.setIndeterminate(false);
progressUpdater.setText("正在上传 ...");
this.tinyPng.addCount(tinyPngKey, 1);
HttpURLConnection connection = (HttpURLConnection) new URL(this.tinyPng.getApiUrl()).openConnection();
String auth = DatatypeConverter.printBase64Binary(("api:" + tinyPngKey.getKey()).getBytes("UTF-8"));
connection.setRequestProperty("Authorization", "Basic " + auth);
connection.setDoOutput(true);
connection.setConnectTimeout(10000);
connection.setReadTimeout(50000);
OutputStream requestOutputStream = connection.getOutputStream();
copyHelper.copy(inputPath, requestOutputStream);
progressUpdater.setText("上传完成");
progressUpdater.setIndeterminate(true);
progressUpdater.setText("正在压缩 ...");
int code = connection.getResponseCode();
if(201 == code)
{
progressUpdater.setText("压缩完成,准备下载 ...");
String url = connection.getHeaderField("location");
connection = (HttpURLConnection) new URL(url).openConnection();
connection.setConnectTimeout(10000);
connection.setReadTimeout(30000);
InputStream responseInputStream = connection.getInputStream();
progressUpdater.setIndeterminate(false);
progressUpdater.setText("正在下载 ...");
copyHelper.copy(responseInputStream, outputPath, connection.getContentLength(), true);
progressUpdater.setText("下载完成");
this.rs = PngResult.createSuccessResult(String.format("Compress %s > %s successful", inputPath, outputPath));
MyLogManager.getInstance().info(this.rs.getInfo());
}
else
{
progressUpdater.setIndeterminate(false);
progressUpdater.setText("压缩失败:" + code);
this.rs = PngResult.createFailureResult(String.format("Compress %s > %s failed, errorCode = %s", inputPath, outputPath, code));
MyLogManager.getInstance().warn(this.rs.getInfo());
if(429 == code)
{
this.tinyPng.addCount(tinyPngKey, this.tinyPng.getMaxCountPerMonth());
}
}
}
}
catch(Exception e)
{
e.printStackTrace();
MyLogManager.getInstance().error(String.format("Compress %s > %s failed", inputPath, outputPath), e);
progressUpdater.setIndeterminate(false);
progressUpdater.setText("发生异常:" + e.getClass().getSimpleName());
this.rs = PngResult.createFailureResult(e.getMessage());
}
finally
{
this.countDownLatch.countDown();
}
}
private void setTinyPng(TinyPng tinyPng)
{
if(null == tinyPng)
{
throw new IllegalArgumentException("the tinyPng is null");
}
this.tinyPng = tinyPng;
}
}
我申请了七八个免费帐号,能够满足使用需求了: