package com.glitchcog.fontificator.emoji; import java.awt.Color; import java.awt.Dimension; import java.awt.Image; import java.awt.image.BufferedImage; import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.List; import javax.imageio.ImageIO; import org.apache.log4j.Logger; /** * Holds the URL for an emoji image until it is needed, at which point it load it into memory. This is a combination of * every possible version of emoji that could be read from its JSON source, be it Twitch V2, Twitch V3, or FrankerFaceZ. * For emoji with multiple images, they will have multiple instances of this LazyLoadEmoji object in the emoji<String, * LazyLoadEmoji[]> map in EmojiManager. * * This object acts as part of an array because Twitch V2 and V3 emoji seem to indicate they'll use multiple instances * of emoji as frames of animation. This is as of yet unused. Third party emoji BetterTTV already use animated GIFs. * * @author Matt Yanos */ public class LazyLoadEmoji { private static final Logger logger = Logger.getLogger(LazyLoadEmoji.class); /** * Keep track of any failed URLs so we don't clobber the display trying to reload them over and over */ private static List<String> BUSTED_URLS = new ArrayList<String>(); /** * The word or regex that identifies this emoji */ private final String identifier; /** * Whether this emoji is meant to replace another emoji (like a FrankerFaceZ bot or moderator badge) */ private final String replaces; private final EmojiType type; private Image image; private Image animatedGifImage; private URL url; private boolean subscriber; private String state; private int width; private int height; private boolean animated; private boolean animatedGif; private static final int DEFAULT_EMOJI_SIZE = 24; private boolean firstLoadFailureReported; /** * Used for FFZ badges only */ private final Color bgColor; public LazyLoadEmoji(String id, String url, EmojiType type) throws MalformedURLException { this(id, null, url, null, type); } public LazyLoadEmoji(String id, String replaces, String url, Color bgColor, EmojiType type) throws MalformedURLException { this(id, replaces, url, DEFAULT_EMOJI_SIZE, DEFAULT_EMOJI_SIZE, bgColor, type); } public LazyLoadEmoji(String identifier, String url, int width, int height, EmojiType type) throws MalformedURLException { this(identifier, url, width, height, null, type); } public LazyLoadEmoji(String identifier, String url, int width, int height, Color bgColor, EmojiType type) throws MalformedURLException { this(identifier, null, url, width, height, bgColor, type); } public LazyLoadEmoji(String identifier, String replaces, String url, int width, int height, Color bgColor, EmojiType type) throws MalformedURLException { this.identifier = identifier; this.replaces = replaces; this.url = new URL(url); this.type = type; this.width = width; this.height = height; this.firstLoadFailureReported = false; this.bgColor = bgColor; } public void cacheImage() { getImage(isAnimatedGif()); } /** * Lazy-loaded image * * @return image */ public Image getImage(boolean animated) { if (url == null || BUSTED_URLS.contains(url.toString())) { return null; } // Lazy load the still image whether or not the emoji is animated if (image == null) { if (!checkUrl(url)) { BUSTED_URLS.add(url.toString()); return null; } try { BufferedImage imageFromTwitch = ImageIO.read(url); // Hack to make image background transparent because Twitch emote V1 of sizes 2.0 and 3.0 sometimes are // not of the correct type for transparency. Kappa (ID 25) is an example of a non transparent emoji in // sizes 2.0 and 3.0. Seriously. Download a Kappa size 2.0 image from the V1 URL and open it in an // editor. The background is solid, but when Twitch displays it in their chat, it displays transparent. if (EmojiOpacityHandler.isCandidateForModification(type, imageFromTwitch.getType(), identifier)) { image = EmojiOpacityHandler.fixOpaqueEmote(identifier, imageFromTwitch); } // No hack required else { image = imageFromTwitch; } if (image != null) { this.width = this.image.getWidth(null); this.height = this.image.getHeight(null); } } catch (IOException e) { if (!firstLoadFailureReported) { logger.error("Unable to load emoji: " + url, e); firstLoadFailureReported = true; image = null; } } } // Only lazy load the animated GIF image if the image is an animatedGif type if (animatedGif && animatedGifImage == null) { // BTTV emote (ditto) gets special care if ("(ditto)".equals(identifier)) { Dimension dim = new Dimension(); animatedGifImage = AnimatedGifUtil.loadDittoAnimatedGif(url, dim); this.width = (int) dim.getWidth(); this.height = (int) dim.getHeight(); } else { animatedGifImage = AnimatedGifUtil.loadAnimatedGif(url); } } // Return the animated GIF image only if animated is requested AND this emoji is an animated GIF return animated && animatedGif ? animatedGifImage : image; } public boolean isSubscriber() { return subscriber; } public void setSubscriber(boolean subscriber) { this.subscriber = subscriber; } public String getState() { return state; } public void setState(String state) { this.state = state; } public EmojiType getType() { return type; } public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public URL getUrl() { return url; } public boolean isAnimated() { return animated; } public void setAnimated(boolean animated) { this.animated = animated; } public boolean isAnimatedGif() { return animatedGif; } public void setAnimatedGif(boolean animatedGif) { this.animatedGif = animatedGif; } /** * Whether coloring is required, like for FrankerFaceZ emotes that are white and transparent * * @return whether coloring is required */ public boolean isColoringRequired() { return type == EmojiType.FRANKERFACEZ_BADGE || bgColor != null; } public Color getBgColor() { return bgColor; } public boolean isReplacement() { return replaces != null; } public String getReplaces() { return replaces; } public static boolean checkUrl(URL url) { try { HttpURLConnection.setFollowRedirects(true); // Does not work for http to https redirects HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); httpURLConnection.setRequestMethod("HEAD"); httpURLConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 (.NET CLR 3.5.30729)"); int responseCode = httpURLConnection.getResponseCode(); return responseCode == HttpURLConnection.HTTP_OK; } catch (Exception e) { logger.debug(e.getMessage(), e); return false; } } }