package face;
import gui.forms.GUIMain;
import lib.JSON.JSONArray;
import lib.JSON.JSONObject;
import lib.pircbot.User;
import lib.scalr.Scalr;
import thread.ThreadEngine;
import util.APIRequests;
import util.Response;
import util.Utils;
import util.settings.Settings;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.swing.*;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.RescaleOp;
import java.io.File;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Created by Nick on 12/28/2014.
*/
public class FaceManager {
public static final int DOWNLOAD_MAX_FACE_HEIGHT = 26;
public static final int DOWNLOAD_MAX_ICON_HEIGHT = 26;
//loading the faces
public static boolean doneWithFaces = false;
public static boolean doneWithTwitchFaces = false;
public static boolean doneWithFrankerFaces = false;
private static boolean checkedEmoteSets = false;
//faces
public static ConcurrentHashMap<String, Face> faceMap;
public static ConcurrentHashMap<String, Face> nameFaceMap;
// twitch
public static CopyOnWriteArraySet<SubscriberIcon> subIconSet;
public static File exSubscriberIcon;
public static ConcurrentHashMap<Integer, TwitchFace> twitchFaceMap;
public static ConcurrentHashMap<Integer, TwitchFace> onlineTwitchFaces;
// ffz
public static ConcurrentHashMap<String, ArrayList<FrankerFaceZ>> ffzFaceMap;
public static void init() {
exSubscriberIcon = null;
faceMap = new ConcurrentHashMap<>();
nameFaceMap = new ConcurrentHashMap<>();
twitchFaceMap = new ConcurrentHashMap<>();
onlineTwitchFaces = new ConcurrentHashMap<>();
ffzFaceMap = new ConcurrentHashMap<>();
subIconSet = new CopyOnWriteArraySet<>();
}
public enum FACE_TYPE {
NAME_FACE,
TWITCH_FACE,
FRANKER_FACE,
NORMAL_FACE
}
/**
* Removes a face from the Face HashMap and deletes the face picture file.
*
* @param key The name of the face to remove.
*/
public static Response removeFace(String key) {
Response toReturn = new Response();
if (!faceMap.containsKey(key)) {
toReturn.setResponseText("Could not remove the face, there is no such face \"" + key + "\"!");
return toReturn;
}
try {
Face toDelete = faceMap.get(key);
File f = new File(toDelete.getFilePath());
if (f.delete()) {
faceMap.remove(key);
toReturn.wasSuccessful();
toReturn.setResponseText("Successfully removed face \"" + key + "\"!");
} else {
toReturn.setResponseText("Could not remove face due to I/O error!");
}
} catch (Exception e) {
toReturn.setResponseText("Could not delete face due to Exception: " + e.getMessage());
}
return toReturn;
}
public static URL getExSubscriberIcon(String channel) {
try {
if (exSubscriberIcon == null) {
URL subIconNormal = FaceManager.getSubIcon(channel);
if (subIconNormal != null) {
BufferedImage img = ImageIO.read(subIconNormal);
//rescaleop does not work with sub icons as is, we need to recreate them as ARGB images
BufferedImage bimage = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = bimage.createGraphics();
g.drawImage(img, 0, 0, null);
g.dispose();
RescaleOp op = new RescaleOp(.35f, 0f, null);
img = op.filter(bimage, bimage);//then re-assign them
exSubscriberIcon = new File(Settings.subIconsDir + File.separator + channel + "_ex.png");
ImageIO.write(img, "PNG", exSubscriberIcon);
exSubscriberIcon.deleteOnExit();
}
}
return exSubscriberIcon.toURI().toURL();
} catch (Exception e) {
GUIMain.log(e);
}
return null;
}
/**
* Gets the subscriber icon for the given channel from either cache or downloads
* it if you do not have it already.
*
* @param channel The channel the icon is for.
* @return The URL of the subscriber icon.
*/
public static URL getSubIcon(String channel) {
for (SubscriberIcon i : subIconSet) {
if (i.getChannel().equalsIgnoreCase(channel)) {
try {
if (Utils.areFilesGood(i.getFileLoc())) {
return new File(i.getFileLoc()).toURI().toURL();
} else {
//This updates the icon, all you need to do is remove the file
subIconSet.remove(i);
break;
}
} catch (Exception e) {
GUIMain.log(e);
}
}
}
String path = APIRequests.Twitch.getSubIcon(channel);
if (path != null)
{
subIconSet.add(new SubscriberIcon(channel, path));
return getSubIcon(channel);
}
return null;
}
/**
* Builds the giant all-containing Twitch Face map.
*/
public static void buildMap() {
try {
// Load twitch faces
String line = APIRequests.Twitch.getAllEmotes();
if (!line.isEmpty()) {
try {
JSONObject init = new JSONObject(line);
JSONArray emotes = init.getJSONArray("emoticons");
for (int i = 0; i < emotes.length(); i++) {
JSONObject emote = emotes.getJSONObject(i);
int ID = emote.getInt("id");
if (twitchFaceMap.get(ID) != null) continue;
String regex = emote.getString("code").replaceAll("\\\\<\\\\;", "\\<").replaceAll("\\\\>\\\\;", "\\>");
String URL = "http://static-cdn.jtvnw.net/emoticons/v1/" + ID + "/1.0";
onlineTwitchFaces.put(ID, new TwitchFace(regex, URL, true));
}
} catch (Exception e) {
GUIMain.log("Failed to load online Twitch faces, is the API endpoint down?");
}
}
} catch (Exception e) {
GUIMain.log(e);
}
}
/**
* Loads the default Twitch faces. This downloads to the local folder in
* <p>
* /My Documents/Botnak/TwitchFaces/
* <p>
* It also checks to see if you may be missing a default face, and downloads it.
* <p>
* This process is threaded, and will only show the faces when it's done downloading.
*/
public static void loadDefaultFaces() {
ThreadEngine.submit(() -> {
buildMap();
GUIMain.log("Loaded Twitch faces!");
Settings.TWITCH_FACES.save();
doneWithTwitchFaces = true;
if (Settings.ffzFacesEnable.getValue()) {
handleFFZChannel("global");//this corrects the global emotes and downloads them if we don't have them
GUIMain.channelSet.forEach(s -> handleFFZChannel(s.replaceAll("#", "")));
doneWithFrankerFaces = true;
GUIMain.log("Loaded FrankerFaceZ faces!");
}
});
}
/**
* Toggles a twitch face on/off.
* <p>
* Ex: !toggleface RitzMitz
* would toggle RitzMitz off/on in showing up on botnak,
* depending on current state.
*
* @param faceName The face name to toggle.
*/
public static Response toggleFace(String faceName) {
Response toReturn = new Response();
if (faceName == null || !doneWithTwitchFaces) {
if (doneWithTwitchFaces) toReturn.setResponseText("Failed to toggle face, the face name is null!");
else toReturn.setResponseText("Failed to toggle face, not done checking Twitch faces!");
return toReturn;
}
Set<Map.Entry<Integer, TwitchFace>> set = twitchFaceMap.entrySet();
for (Map.Entry<Integer, TwitchFace> entry : set) {
TwitchFace fa = entry.getValue();
String regex = fa.getRegex();
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(faceName);
if (m.find()) {
boolean newStatus = !fa.isEnabled();
fa.setEnabled(newStatus);
toReturn.setResponseText("Toggled the face " + faceName + (newStatus ? " ON" : " OFF"));
toReturn.wasSuccessful();
return toReturn;
}
}
String errorMessage = "Could not find face " + faceName + " in the loaded Twitch faces";
if (Settings.ffzFacesEnable.getValue()) {
Set<Map.Entry<String, ArrayList<FrankerFaceZ>>> channels = ffzFaceMap.entrySet();
for (Map.Entry<String, ArrayList<FrankerFaceZ>> entry : channels) {
ArrayList<FrankerFaceZ> faces = entry.getValue();
for (FrankerFaceZ f : faces) {
if (f.getRegex().equalsIgnoreCase(faceName)) {
boolean newStatus = !f.isEnabled();
f.setEnabled(newStatus);
toReturn.setResponseText("Toggled the FrankerFaceZ face " + f.getRegex() + (newStatus ? " ON" : " OFF"));
toReturn.wasSuccessful();
return toReturn;
}
}
}
errorMessage += " or loaded FrankerFaceZ faces";
}
errorMessage += "!";
toReturn.setResponseText(errorMessage);
return toReturn;
}
public static void handleNameFaces(String object, SimpleAttributeSet set) {
Set<Map.Entry<String, Face>> entries = nameFaceMap.entrySet();
for (Map.Entry<String, Face> e : entries) {
if (object.equalsIgnoreCase(e.getKey())) {
insertFace(set, e.getValue().getFilePath());
break;
}
}
}
public static void handleFFZChannel(String channel) {
ThreadEngine.submit(() -> {
ArrayList<FrankerFaceZ> faces = ffzFaceMap.get(channel);
ArrayList<FrankerFaceZ> fromOnline = new ArrayList<>();
FrankerFaceZ.FFZParser.parse(channel, fromOnline);
if (faces != null) { //already have the faces
for (FrankerFaceZ online : fromOnline) {
boolean haveIt = false;
for (FrankerFaceZ f : faces) {//get the ones I need
if (online.getRegex().equalsIgnoreCase(f.getRegex())) {
haveIt = true;
break;
}
}
if (!haveIt) {
FrankerFaceZ downloaded = downloadFFZFace(channel, online);
if (downloaded != null) {
faces.add(downloaded);
}
}
}
} else { //don't have any of them
faces = new ArrayList<>();
for (FrankerFaceZ online : fromOnline) {
FrankerFaceZ downloaded = downloadFFZFace(channel, online);
if (downloaded != null) faces.add(downloaded);
}
ffzFaceMap.put(channel, faces);
}
});
}
public static void handleEmoteSet(String emotes) {
if (checkedEmoteSets) return;
ThreadEngine.submit(() -> {
try {
checkedEmoteSets = true;
String line = APIRequests.Twitch.getEmoteSet(emotes);
if (!line.isEmpty()) {
User main = Settings.channelManager
.getUser(Settings.accountManager.getUserAccount().getName(), true);
JSONObject init = new JSONObject(line);
String[] keys = emotes.split(",");
JSONObject emote_sets = init.getJSONObject("emoticon_sets");
for (String s : keys) {
JSONArray set = emote_sets.getJSONArray(s);
for (int i = 0; i < set.length(); i++) {
JSONObject emote = set.getJSONObject(i);
int ID = emote.getInt("id");
main.addEmote(ID);
if (doneWithTwitchFaces) {
if (twitchFaceMap.get(ID) == null) {
downloadEmote(ID);
}
}
}
}
}
} catch (Exception e) {
GUIMain.log("FaceManager: Failed to download EmoteSets!");
checkedEmoteSets = false;
}
});
}
public static void handleFaces(Map<Integer, Integer> ranges, Map<Integer, SimpleAttributeSet> rangeStyles,
String object, FACE_TYPE type, String channel, Collection<Integer> emotes) {
switch (type) {
case TWITCH_FACE:
if (doneWithTwitchFaces) {
for (int i : emotes) {
TwitchFace f = twitchFaceMap.get(i);
if (f == null) {
f = downloadEmote(i);
}
if (f == null || !f.isEnabled() || !Utils.areFilesGood(f.getFilePath())) continue;
String regex = f.getRegex();
if (!regex.matches("^\\W.*|.*\\W$")) {
//boundary checks are only necessary for emotes that start and end with a word character.
regex = "\\b" + regex + "\\b";
}
insertFaceMetadata(f, ranges, rangeStyles, regex, object);
}
}
break;
case NORMAL_FACE:
if (doneWithFaces) {
Set<Map.Entry<String, Face>> entries = faceMap.entrySet();
for (Map.Entry<String, Face> entry : entries) {
Face f = entry.getValue();
if (!Utils.checkRegex(f.getRegex()) || !Utils.areFilesGood(f.getFilePath())) continue;
insertFaceMetadata(f, ranges, rangeStyles, f.getRegex(), object);
}
}
break;
case FRANKER_FACE:
if (doneWithFrankerFaces) {
String[] channels = Settings.ffzFacesUseAll.getValue() ?
ffzFaceMap.keySet().toArray(new String[ffzFaceMap.keySet().size()]) :
new String[]{"global", channel};
for (String currentChannel : channels) {
ArrayList<FrankerFaceZ> faces = ffzFaceMap.get(currentChannel);
if (faces != null) {
for (FrankerFaceZ f : faces) {
insertFaceMetadata(f, ranges, rangeStyles, currentChannel, object);
}
}
}
}
break;
default:
break;
}
}
private static void insertFaceMetadata(Face f, Map<Integer, Integer> ranges, Map<Integer, SimpleAttributeSet> rangeStyles,
String regex, String object) {
boolean isFFZ = f instanceof FrankerFaceZ;
Pattern p = Pattern.compile(isFFZ ? f.getRegex() : regex);
Matcher m = p.matcher(object);
while (m.find() && !GUIMain.shutDown) {
int start = m.start();
int end = m.end() - 1;
if (!Utils.inRanges(start, ranges) && !Utils.inRanges(end, ranges)) {
ranges.put(start, end);
SimpleAttributeSet attrs = new SimpleAttributeSet();
attrs.addAttribute("faceinfo", f);
attrs.addAttribute(isFFZ ? "channel" : "regex", isFFZ ? regex : m.group());
insertFace(attrs, f.getFilePath());
attrs.addAttribute("start", start);
rangeStyles.put(start, attrs);
}
}
}
private static void insertFace(SimpleAttributeSet set, String face) {
try {
StyleConstants.setIcon(set, sizeIcon(new File(face).toURI().toURL()));
} catch (Exception e) {
GUIMain.log(e);
}
}
private static ImageIcon sizeIcon(URL image) {
ImageIcon icon;
try {
BufferedImage img = ImageIO.read(image);
// Scale the icon if it's too big.
int maxHeight = Settings.faceMaxHeight.getValue();
if (img.getHeight() > maxHeight)
img = Scalr.resize(img, Scalr.Method.ULTRA_QUALITY, Scalr.Mode.FIT_TO_HEIGHT, maxHeight);
icon = new ImageIcon(img);
icon.getImage().flush();
return icon;
} catch (Exception e) {
icon = new ImageIcon(image);
}
return icon;
}
/**
* Downloads the subscriber icon of the specified URL and channel.
*
* @param url The url to download the icon from.
* @param channel The channel the icon is for.
* @return The path of the file of the icon.
*/
public static String downloadIcon(String url, String channel) {
File toSave = new File(Settings.subIconsDir + File.separator + Utils.setExtension(channel.substring(1), ".png"));
if (download(url, toSave, null))
return toSave.getAbsolutePath();
else
return null;
}
public static TwitchFace downloadEmote(int emote) {
try {
TwitchFace f = onlineTwitchFaces.get(emote);
if (f == null) return null;
String fileName = Utils.setExtension(String.valueOf(emote), ".png");
File toSave = new File(Settings.twitchFaceDir.getAbsolutePath() + File.separator + fileName);
if (download(f.getFilePath(), toSave, FACE_TYPE.TWITCH_FACE)) {
TwitchFace newFace = new TwitchFace(f.getRegex(), toSave.getAbsolutePath(), true);
twitchFaceMap.put(emote, newFace);
return newFace;
}
} catch (Exception e) {
GUIMain.log("Failed to download emote ID " + emote + " due to exception: ");
GUIMain.log(e);
}
return null;
}
private static FrankerFaceZ downloadFFZFace(String channel, FrankerFaceZ face) {//URL is stored in the FFZ face
FrankerFaceZ toReturn = null;
try {
String regex = face.getRegex();
String fileName = Utils.setExtension(regex, ".png");
//download into the channel's folder
File directory = new File(Settings.frankerFaceZDir + File.separator + channel);
directory.mkdirs();
File toSave = new File(directory + File.separator + fileName);
if (download(face.getFilePath(), toSave, FACE_TYPE.FRANKER_FACE)) {
toReturn = new FrankerFaceZ(Utils.removeExt(fileName), toSave.getAbsolutePath(), true);
}
} catch (Exception e) {
GUIMain.log("Failed to download FFZ Faces due to Exception: ");
GUIMain.log(e);
}
return toReturn;
}
//overload method for below, rids the use of try{}catch{} in Utils class
public static Response downloadFace(File f, String directory, String name, String regex, FACE_TYPE type) {
Response toReturn = new Response();
try {
return downloadFace(f.toURI().toURL().toString(), directory, name, regex, type);
} catch (Exception e) {
toReturn.setResponseText("Failed to download face due to a malformed URL!");
}
return toReturn;
}
/**
* Downloads a face off of the internet using the given URL and stores it in the given
* directory with the given filename and extension. The regex (or "name") of the face is put in the map
* for later use/comparison.
* <p>
*
* @param url The URL to the face.
* @param directory The directory to save the face in.
* @param name The name of the file for the face, including the extension.
* @param regex The regex pattern ("name") of the face.
* @param type What type of face it is.
*/
public static Response downloadFace(String url, String directory, String name, String regex, FACE_TYPE type) {
Response toReturn = new Response();
if (directory == null || name == null || directory.equals("") || name.equals("")) {
toReturn.setResponseText("Failed to download face, the directory or name is null!");
return toReturn;
}
try {
File toSave = new File(directory + File.separator + name);
if (download(url, toSave, type)) {
if (type == FACE_TYPE.NORMAL_FACE) {
Face face = new Face(regex, toSave.getAbsolutePath());
name = Utils.removeExt(name);
faceMap.put(name, face);//put it
toReturn.setResponseText("Successfully added the normal face: " + name + " !");
} else {
Face face = new Face(regex, toSave.getAbsolutePath());
name = Utils.removeExt(name);
nameFaceMap.put(name, face);
toReturn.setResponseText("Successfully added the nameface for user: " + name + " !");
}
toReturn.wasSuccessful();
} else {
toReturn.setResponseText("Failed to download the face, perhaps a bad URL!");
}
} catch (Exception e) {
toReturn.setResponseText("Failed to download face due to Exception: " + e.getMessage());
}
return toReturn;
}
private static boolean download(String url, File toSave, FACE_TYPE type) {
try {
BufferedImage image;
URL URL = new URL(url);//bad URL or something
if (URL.getHost().equals("imgur.com")) {
URL = new URL(Utils.setExtension("http://i.imgur.com" + URL.getPath(), ".png"));
}
if (sanityCheck(URL)) {
image = ImageIO.read(URL);//just incase the file is null/it can't read it
if (type == FACE_TYPE.NAME_FACE) image = trimWhitespaceFromImage(image);
if (image.getHeight() > DOWNLOAD_MAX_FACE_HEIGHT) {//if it's too big, scale it
image = Scalr.resize(image, Scalr.Method.ULTRA_QUALITY,
Scalr.Mode.FIT_TO_HEIGHT, DOWNLOAD_MAX_FACE_HEIGHT);
}
return ImageIO.write(image, "PNG", toSave);//save it
}
} catch (Exception e) {
GUIMain.log(e);
}
return false;
}
/**
* Tests to see if an image is within reasonable downloading bounds (5000x5000)
*
* @param url The URL to the image to check.
* @return True if within downloadable bounds else false.
*/
private static boolean sanityCheck(URL url) {
try (ImageInputStream in = ImageIO.createImageInputStream(url.openStream())) {
final Iterator<ImageReader> readers = ImageIO.getImageReaders(in);
if (readers.hasNext()) {
ImageReader reader = readers.next();
try {
reader.setInput(in);
Dimension d = new Dimension(reader.getWidth(0), reader.getHeight(0));
return d.getHeight() < 5000 && d.getWidth() < 5000;
} finally {
reader.dispose();
}
}
} catch (Exception e) {
return false;
}
return false;
}
/**
* Either adds a face to the image map or changes a face to another variant.
* If the face image size is too big, it is scaled (using Scalr) to fit the 26 pixel height limit.
*
* @param s The string from the chat.
* @return The response of the method.
*/
public static Response handleFace(String s) {
Response toReturn = new Response();
boolean localCheck = ("".equals(Settings.defaultFaceDir.getValue())
|| Settings.defaultFaceDir.getValue().equals("null"));
String[] split = s.split(" ");
String command = split[0];
String name = split[1];//name of the face, used for file name, and if regex isn't supplied, becomes the regex
String regex;
String file;//or the URL...
if (command.equalsIgnoreCase("addface")) {//a new face
if (faceMap.containsKey(name)) {//!addface is not !changeface, remove the face first or do changeface
toReturn.setResponseText("Failed to add face, " + name + " already exists!");
return toReturn;
}
if (split.length == 4) {//!addface <name> <regex> <URL or file>
regex = split[2];
//regex check
if (!Utils.checkRegex(regex)) {
toReturn.setResponseText("Failed to add face, the supplied regex does not compile!");
return toReturn;
}
//name check (for saving the file)
if (Utils.checkName(name)) {
toReturn.setResponseText("Failed to add face, the supplied name is not Windows-friendly!");
return toReturn;
}
file = split[3];
if (file.startsWith("http")) {//online
return downloadFace(file, Settings.faceDir.getAbsolutePath(),
Utils.setExtension(name, ".png"), regex, FACE_TYPE.NORMAL_FACE);//save locally
} else {//local
if (Utils.checkName(file) || localCheck) {
if (!localCheck)
toReturn.setResponseText("Failed to add face, the supplied name is not Windows-friendly!");
else toReturn.setResponseText("Failed to add face, the local directory is not set properly!");
return toReturn;
}
return downloadFace(new File(Settings.defaultFaceDir.getValue() + File.separator + file),
Settings.faceDir.getAbsolutePath(),
Utils.setExtension(name, ".png"),
regex, FACE_TYPE.NORMAL_FACE);
}
} else if (split.length == 3) {//!addface <name> <URL or file> (name will be the regex, case sensitive)
file = split[2];
//regex (this should never be a problem, however...)
if (!Utils.checkRegex(name)) {
toReturn.setResponseText("Failed to add face, the supplied name is not a valid regex!");
return toReturn;
}
//name check (for saving the file)
if (Utils.checkName(name)) {
toReturn.setResponseText("Failed to add face, the supplied name is not Windows-friendly!");
return toReturn;
}
if (file.startsWith("http")) {//online
return downloadFace(file, Settings.faceDir.getAbsolutePath(),
Utils.setExtension(name, ".png"), name, FACE_TYPE.NORMAL_FACE);//name is regex, so case sensitive
} else {//local
if (Utils.checkName(file) || localCheck) {
if (!localCheck)
toReturn.setResponseText("Failed to add face, the supplied name is not Windows-friendly!");
else toReturn.setResponseText("Failed to add face, the local directory is not set properly!");
return toReturn;
}
return downloadFace(new File(Settings.defaultFaceDir.getValue() + File.separator + file),
Settings.faceDir.getAbsolutePath(),
Utils.setExtension(name, ".png"),
name, //<- this will be the regex, so case sensitive
FACE_TYPE.NORMAL_FACE);
}
}
} else if (command.equalsIgnoreCase("changeface")) {//replace entirely
if (faceMap.containsKey(name)) {//!changeface is not !addface, the map MUST contain it
if (split.length == 5) {//!changeface <name> 2 <new regex> <new URL/file>
try {//gotta make sure the number is the ^
if (Integer.parseInt(split[2]) != 2) {
toReturn.setResponseText("Failed to change face, make sure to designate the \"2\" in the command!");
return toReturn;
}
} catch (Exception e) {
toReturn.setResponseText("Failed to change face, the indicator number cannot be parsed!");
return toReturn;
}
regex = split[3];
//regex check
if (!Utils.checkRegex(regex)) {
toReturn.setResponseText("Failed to add face, the supplied regex does not compile!");
return toReturn;
}
//name check (for saving the file)
if (Utils.checkName(name)) {
toReturn.setResponseText("Failed to add face, the supplied name is not Windows-friendly!");
return toReturn;
}
file = split[4];
if (file.startsWith("http")) {//online
return downloadFace(file, Settings.faceDir.getAbsolutePath(),
Utils.setExtension(name, ".png"), regex, FACE_TYPE.NORMAL_FACE);//save locally
} else {//local
if (Utils.checkName(file) || localCheck) {
if (!localCheck)
toReturn.setResponseText("Failed to add face, the supplied name is not Windows-friendly!");
else
toReturn.setResponseText("Failed to add face, the local directory is not set properly!");
return toReturn;
}
return downloadFace(new File(Settings.defaultFaceDir.getValue() + File.separator + file),
Settings.faceDir.getAbsolutePath(),
Utils.setExtension(name, ".png"),
regex, //< this will be the regex, so case sensitive
FACE_TYPE.NORMAL_FACE);
}
} else if (split.length == 4) {//!changeface <name> <numb> <newregex>|<new URL or file>
int type;
try {//gotta check the number
type = Integer.parseInt(split[2]);
} catch (Exception e) {
toReturn.setResponseText("Failed to change face, the indicator number cannot be parsed!");
return toReturn;
}
Face face = faceMap.get(name);
if (type == 0) {//regex change; !changeface <name> 0 <new regex>
regex = split[3];
if (Utils.checkRegex(regex)) {
faceMap.put(name, new Face(regex, face.getFilePath()));
toReturn.setResponseText("Successfully changed the regex for face: " + name + " !");
toReturn.wasSuccessful();
} else {
toReturn.setResponseText("Failed to change the regex, the new regex could not be compiled!");
}
} else if (type == 1) {//file change; !changeface <name> 1 <new URL/file>
file = split[3];
if (file.startsWith("http")) {//online
return downloadFace(file, Settings.faceDir.getAbsolutePath(),
Utils.setExtension(name, ".png"), face.getRegex(), FACE_TYPE.NORMAL_FACE);//save locally
} else {//local
if (Utils.checkName(file) || localCheck) {
if (!localCheck)
toReturn.setResponseText("Failed to add face, the supplied name is not Windows-friendly!");
else
toReturn.setResponseText("Failed to add face, the local directory is not set properly!");
return toReturn;
}
return downloadFace(new File(Settings.defaultFaceDir.getValue() + File.separator + file),
Settings.faceDir.getAbsolutePath(),
Utils.setExtension(name, ".png"),
face.getRegex(), FACE_TYPE.NORMAL_FACE);
}
}
}
} else {
toReturn.setResponseText("Failed to change face, the face " + name + " does not exist!");
}
}
return toReturn;
}
/**
* This method's dedicated to all those lazy image makers out there.
* <p>
* So we have the following image:
* <p>
* |--------------------------------|
* | (emptyness) |
* | ----------- |
* | |(contents)| (whitespace) |
* | | | |
* | ------------ |
* | (gross laziness) |
* |--------------------------------|
* <p>
* In order to trim it, we're going to be color searching based on a row/column search.
* We start in the top left corner (point [0,0]) and see if it's transparent. If so,
* we then search across that point's height, and see if it intersects any color other than transparent.
* If no intersection happens (the row is completely transparent) the search continues down the column,
* searching the pixels down from the current point's x value to see if it intersects any color.
* <p>
* If no intersection happen, the starter point then continues down diagonally to [1,1].
* The search continues until an intersection is found with a color that is not empty.
* <p>
* Once an intersection with a color is found, the row/column is reverted to the previous pixel
* until both the row and column searches have intersected color.
* <p>
* The search is repeated for the other corner of the image (#getWidth() and #getHeight()) and
* inverted until the image is successfully cropped to the portion with just the color contents.
*
* @param source The source image.
*/
private static BufferedImage trimWhitespaceFromImage(BufferedImage source) {
try {
Dimension originalTopLeft = new Dimension(0, 0);
Dimension originalBottomRight = new Dimension(source.getWidth() - 1, source.getHeight() - 1);
Dimension topLeft = colorSearch(source, true);
Dimension bottomRight = colorSearch(source, false);
if (!topLeft.equals(originalTopLeft) || !bottomRight.equals(originalBottomRight)) {
//We only create an image if the algorithm cropped something
return createNewImage(source, topLeft, bottomRight);
}
} catch (Exception e) {
GUIMain.log("Failed to trim image due to exception: ");
GUIMain.log(e);
}
return source;
}
//scans the row/column for Transparent color
private static boolean check(BufferedImage bi, int value, boolean searchingHorizontally, boolean invert) {
int bound = searchingHorizontally ? bi.getWidth() : bi.getHeight();
int start = invert ? bound - 1 : 0;
int change = invert ? -1 : 1;
for (int i = start; invert ? i > -1 : i < bound; i += change) {
Color c = searchingHorizontally ? getARGBColor(bi, i, value) : getARGBColor(bi, value, i);
if (c.getAlpha() != 0) return false;
}
return true;
}
//Main color search algorithm, searches for rows/columns of transparency
private static Dimension colorSearch(BufferedImage bi, boolean topLeft) {
int startX = topLeft ? 0 : bi.getWidth() - 1;
int startY = topLeft ? 0 : bi.getHeight() - 1;
int previousWidth = startX;
int previousHeight = startY;
Color initial = getARGBColor(bi, startX, startY);
if (initial.getAlpha() == 0) {
//start the search
int width = startX;
int height = startY;
boolean changeWidth = true, changeHeight = true;
while (changeHeight || changeWidth) {
//check across the current row (horizontally) of pixels
if (changeHeight) {
if (check(bi, height, true, !topLeft)) {
//it's empty, change the value
previousHeight = height;
height += topLeft ? 1 : -1;
} else {
//we hit color in this search, revert to previous
changeHeight = false;
height = previousHeight;
}
}
//check going up/down (vertically) at the current width value
if (changeWidth) {
if (check(bi, width, false, !topLeft)) {
//it's empty, change the value
previousWidth = width;
width += topLeft ? 1 : -1;
} else {
//we hit color in this search, revert to previous
changeWidth = false;
width = previousWidth;
}
}
}
return new Dimension(width, height);
}
//if the initial is a color, there's no point to even do the search
return new Dimension(previousWidth, previousHeight);
}
private static Color getARGBColor(BufferedImage bi, int x, int y) {
int pixel = bi.getRGB(x, y);
int alpha = (pixel >> 24) & 0xff;
int red = (pixel >> 16) & 0xff;
int green = (pixel >> 8) & 0xff;
int blue = pixel & 0xff;
return new Color(red, green, blue, alpha);
}
private static BufferedImage createNewImage(BufferedImage bi, Dimension topLeft, Dimension bottomRight)
{
return bi.getSubimage(topLeft.width, topLeft.height, bottomRight.width - topLeft.width, bottomRight.height - topLeft.height);
}
}